Merge tag 'android-14.0.0_r50' into leaf-3.2

Android 14.0.0 Release 50 (AP2A.240605.024)

* tag 'android-14.0.0_r50': (327 commits)
  Import translations. DO NOT MERGE ANYWHERE
  Add testAllowedUids for devices without FEATURE_TELEPHONY_SUBSCRIPTION
  Fix flakes due to networks obtained via sync APIs
  Import translations. DO NOT MERGE ANYWHERE
  Revert "24Q2: use mainline netbpfload from apex"
  24Q2: use mainline netbpfload from apex
  Fix the ignore_on_* flags for egress tracing.
  Remove unused library visibility
  Connect to IBluetoothFinder and use it
  Update the imports to androidx.test.filters.*
  Move VpnManagerServiceTest and VpnTest to frameworks
  Use a TAP test network for MeshCoP service test cases
  Fix flaky multicast tests in Thread E2E test.
  NetBpfLoader: create /sys/fs/bpf/loader dir
  Revert "Use a TAP test network for MeshCoP service test cases"
  Use a TAP test network for MeshCoP service test cases
  [mdns] add service-side impl for NSD service TTL support
  Adding additional transport satellite support at getSubIdForMobile()
  netbpfload: fail if platform exec of apex fails
  [Thread] make Thread state customisable via resource overlay
  ...

Change-Id: Ie343e88e5ec7307265c6166e8a41c7be455c08a7
diff --git a/.gitignore b/.gitignore
index c9b6393..b517674 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,3 +10,6 @@
 # VS Code project
 **/.vscode
 **/*.code-workspace
+
+# Vim temporary files
+**/*.swp
diff --git a/Cronet/tests/common/Android.bp b/Cronet/tests/common/Android.bp
index e17081a..edeb0b3 100644
--- a/Cronet/tests/common/Android.bp
+++ b/Cronet/tests/common/Android.bp
@@ -17,6 +17,7 @@
 // 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"],
 }
@@ -28,7 +29,10 @@
     name: "NetHttpCoverageTests",
     enforce_default_target_sdk_version: true,
     min_sdk_version: "30",
-    test_suites: ["general-tests", "mts-tethering"],
+    test_suites: [
+        "general-tests",
+        "mts-tethering",
+    ],
     static_libs: [
         "modules-utils-native-coverage-listener",
         "CtsNetHttpTestsLib",
@@ -37,6 +41,8 @@
     jarjar_rules: ":net-http-test-jarjar-rules",
     compile_multilib: "both", // Include both the 32 and 64 bit versions
     jni_libs: [
-       "cronet_aml_components_cronet_android_cronet_tests__testing"
+        "cronet_aml_components_cronet_android_cronet_tests__testing",
+        "cronet_aml_third_party_netty_tcnative_netty_tcnative_so__testing",
     ],
+    data: [":cronet_javatests_resources"],
 }
diff --git a/Cronet/tests/common/AndroidTest.xml b/Cronet/tests/common/AndroidTest.xml
index 2ac418f..bded8fb 100644
--- a/Cronet/tests/common/AndroidTest.xml
+++ b/Cronet/tests/common/AndroidTest.xml
@@ -19,6 +19,11 @@
         <option name="install-arg" value="-t" />
     </target_preparer>
     <option name="test-tag" value="NetHttpCoverageTests" />
+
+    <target_preparer class="com.android.tradefed.targetprep.PushFilePreparer">
+        <option name="push-file" key="net" value="/storage/emulated/0/chromium_tests_root/net" />
+        <option name="push-file" key="test_server" value="/storage/emulated/0/chromium_tests_root/components/cronet/testing/test_server" />
+    </target_preparer>
     <!-- Tethering/Connectivity is a SDK 30+ module -->
     <!-- TODO Switch back to Sdk30 when b/270049141 is fixed -->
     <object type="module_controller"
@@ -28,7 +33,28 @@
     <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
         <option name="package" value="com.android.net.http.tests.coverage" />
         <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
+        <!-- b/298380508 -->
+        <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testSetLibraryLoaderIsEnforcedByDefaultEmbeddedProvider" />
+        <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testSetLibraryLoaderIsIgnoredInNativeCronetEngineBuilderImpl" />
+        <!-- b/316571753 -->
+        <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testBaseFeatureFlagsOverridesEnabled" />
+        <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testHttpFlagsAppliedIfAppIdMatches" />
+        <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testHttpFlagsAreLoaded" />
+        <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testSetLibraryLoaderIsEnforcedByDefaultEmbeddedProvider" />
+        <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testHttpFlagsAppliedIfAtMinVersion" />
+        <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testHttpFlagsAppliedIfAboveMinVersion" />
+        <!-- b/316567693 -->
+        <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestTest#testSSLCertificateError" />
+        <!-- b/316559294 -->
+        <option name="exclude-filter" value="org.chromium.net.NQETest#testQuicDisabled" />
+        <!-- b/316559294 -->
+        <option name="exclude-filter" value="org.chromium.net.NQETest#testPrefsWriteRead" />
+        <!-- b/316554711-->
+        <option name="exclude-filter" value="org.chromium.net.NetworkChangesTest" /> 
+        <!-- b/316550794 -->
+        <option name="exclude-filter" value="org.chromium.net.impl.CronetLoggerTest#testEngineCreation" />
         <option name="hidden-api-checks" value="false"/>
+        <option name="isolated-storage" value="false"/>
         <option
             name="device-listeners"
             value="com.android.modules.utils.testing.NativeCoverageHackInstrumentationListener" />
diff --git a/Cronet/tests/cts/Android.bp b/Cronet/tests/cts/Android.bp
index a0b2434..92b73d9 100644
--- a/Cronet/tests/cts/Android.bp
+++ b/Cronet/tests/cts/Android.bp
@@ -15,6 +15,7 @@
 //
 
 package {
+    default_team: "trendy_team_fwk_core_networking",
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
@@ -46,7 +47,9 @@
         "framework-connectivity",
         "org.apache.http.legacy",
     ],
-    lint: { test: true }
+    lint: {
+        test: true,
+    },
 }
 
 android_test {
@@ -62,6 +65,7 @@
     test_suites: [
         "cts",
         "general-tests",
-        "mts-tethering"
+        "mts-tethering",
+        "mcts-tethering",
     ],
 }
diff --git a/Cronet/tests/cts/src/android/net/http/cts/HttpEngineTest.java b/Cronet/tests/cts/src/android/net/http/cts/HttpEngineTest.java
index 9fc4389..f86ac29 100644
--- a/Cronet/tests/cts/src/android/net/http/cts/HttpEngineTest.java
+++ b/Cronet/tests/cts/src/android/net/http/cts/HttpEngineTest.java
@@ -247,10 +247,8 @@
     @Test
     public void testHttpEngine_requestUsesDefaultUserAgent() throws Exception {
         mEngine = mEngineBuilder.build();
-        HttpCtsTestServer server =
-                new HttpCtsTestServer(ApplicationProvider.getApplicationContext());
 
-        String url = server.getUserAgentUrl();
+        String url = mTestServer.getUserAgentUrl();
         UrlRequest request =
                 mEngine.newUrlRequestBuilder(url, mCallback.getExecutor(), mCallback).build();
         request.start();
@@ -266,14 +264,12 @@
     @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();
+        String url = mTestServer.getUserAgentUrl();
         UrlRequest request =
                 mEngine.newUrlRequestBuilder(url, mCallback.getExecutor(), mCallback).build();
         request.start();
diff --git a/Cronet/tests/mts/Android.bp b/Cronet/tests/mts/Android.bp
index 63905c8..9486e1f 100644
--- a/Cronet/tests/mts/Android.bp
+++ b/Cronet/tests/mts/Android.bp
@@ -13,6 +13,7 @@
 // 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"],
 }
@@ -48,19 +49,20 @@
 }
 
 android_test {
-     name: "NetHttpTests",
-     defaults: [
+    name: "NetHttpTests",
+    defaults: [
         "mts-target-sdk-version-current",
-     ],
-     static_libs: ["NetHttpTestsLibPreJarJar"],
-     jarjar_rules: ":net-http-test-jarjar-rules",
-     jni_libs: [
+    ],
+    static_libs: ["NetHttpTestsLibPreJarJar"],
+    jarjar_rules: ":net-http-test-jarjar-rules",
+    jni_libs: [
         "cronet_aml_components_cronet_android_cronet__testing",
         "cronet_aml_components_cronet_android_cronet_tests__testing",
-     ],
-     test_suites: [
-         "general-tests",
-         "mts-tethering",
-     ],
+        "cronet_aml_third_party_netty_tcnative_netty_tcnative_so__testing",
+    ],
+    test_suites: [
+        "general-tests",
+        "mts-tethering",
+    ],
+    data: [":cronet_javatests_resources"],
 }
-
diff --git a/Cronet/tests/mts/AndroidManifest.xml b/Cronet/tests/mts/AndroidManifest.xml
index f597134..2c56e3a 100644
--- a/Cronet/tests/mts/AndroidManifest.xml
+++ b/Cronet/tests/mts/AndroidManifest.xml
@@ -19,6 +19,7 @@
           package="android.net.http.mts">
 
     <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
     <uses-permission android:name="android.permission.INTERNET"/>
 
     <application android:networkSecurityConfig="@xml/network_security_config">
diff --git a/Cronet/tests/mts/AndroidTest.xml b/Cronet/tests/mts/AndroidTest.xml
index 0d780a1..bccbe29 100644
--- a/Cronet/tests/mts/AndroidTest.xml
+++ b/Cronet/tests/mts/AndroidTest.xml
@@ -24,11 +24,37 @@
         <option name="test-file-name" value="NetHttpTests.apk" />
     </target_preparer>
 
+    <target_preparer class="com.android.tradefed.targetprep.PushFilePreparer">
+        <option name="push-file" key="net" value="/storage/emulated/0/chromium_tests_root/net" />
+        <option name="push-file" key="test_server" value="/storage/emulated/0/chromium_tests_root/components/cronet/testing/test_server" />
+    </target_preparer>
+
     <option name="test-tag" value="NetHttpTests" />
     <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
         <option name="package" value="android.net.http.mts" />
         <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
+        <!-- b/298380508 -->
+        <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testSetLibraryLoaderIsEnforcedByDefaultEmbeddedProvider" />
+        <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testSetLibraryLoaderIsIgnoredInNativeCronetEngineBuilderImpl" />
+        <!-- b/316571753 -->
+        <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testBaseFeatureFlagsOverridesEnabled" />
+        <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testHttpFlagsAppliedIfAppIdMatches" />
+        <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testHttpFlagsAreLoaded" />
+        <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testSetLibraryLoaderIsEnforcedByDefaultEmbeddedProvider" />
+        <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testHttpFlagsAppliedIfAtMinVersion" />
+        <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testHttpFlagsAppliedIfAboveMinVersion" />
+        <!-- b/316567693 -->
+        <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestTest#testSSLCertificateError" />
+        <!-- b/316559294 -->
+        <option name="exclude-filter" value="org.chromium.net.NQETest#testQuicDisabled" />
+        <!-- b/316559294 -->
+        <option name="exclude-filter" value="org.chromium.net.NQETest#testPrefsWriteRead" />
+        <!-- b/316554711-->
+        <option name="exclude-filter" value="org.chromium.net.NetworkChangesTest" />
+        <!-- b/316550794 -->
+        <option name="exclude-filter" value="org.chromium.net.impl.CronetLoggerTest#testEngineCreation" />
         <option name="hidden-api-checks" value="false"/>
+        <option name="isolated-storage" value="false"/>
     </test>
 
     <!-- Only run NetHttpTests in MTS if the Tethering Mainline module is installed. -->
diff --git a/Cronet/tests/mts/jarjar_excludes.txt b/Cronet/tests/mts/jarjar_excludes.txt
index a0ce5c2..b5cdf6e 100644
--- a/Cronet/tests/mts/jarjar_excludes.txt
+++ b/Cronet/tests/mts/jarjar_excludes.txt
@@ -2,6 +2,8 @@
 com\.android\.testutils\..+
 # jarjar-gen can't handle some kotlin object expression, exclude packages that include them
 androidx\..+
+# don't jarjar netty as it does JNI
+io\.netty\..+
 kotlin\.test\..+
 kotlin\.reflect\..+
 org\.mockito\..+
@@ -12,9 +14,16 @@
 org\.chromium\.base\..+
 J\.cronet_tests_N(\$.+)?
 
+# don't jarjar automatically generated FooJni files.
+org\.chromium\.net\..+Jni(\$.+)?
+
 # Do not jarjar the tests and its utils as they also do JNI with cronet_tests.so
 org\.chromium\.net\..*Test.*(\$.+)?
 org\.chromium\.net\.NativeTestServer(\$.+)?
 org\.chromium\.net\.MockUrlRequestJobFactory(\$.+)?
 org\.chromium\.net\.QuicTestServer(\$.+)?
-org\.chromium\.net\.MockCertVerifier(\$.+)?
\ No newline at end of file
+org\.chromium\.net\.MockCertVerifier(\$.+)?
+org\.chromium\.net\.LogcatCapture(\$.+)?
+org\.chromium\.net\.ReportingCollector(\$.+)?
+org\.chromium\.net\.Http2TestServer(\$.+)?
+org\.chromium\.net\.Http2TestHandler(\$.+)?
\ No newline at end of file
diff --git a/Cronet/tests/mts/res/raw/quicroot.pem b/Cronet/tests/mts/res/raw/quicroot.pem
new file mode 100644
index 0000000..af21b3e
--- /dev/null
+++ b/Cronet/tests/mts/res/raw/quicroot.pem
@@ -0,0 +1,19 @@
+-----BEGIN CERTIFICATE-----
+MIIC/jCCAeagAwIBAgIUXOi6XoxnMUjJg4jeOwRhsdqEqEQwDQYJKoZIhvcNAQEL
+BQAwFzEVMBMGA1UEAwwMVGVzdCBSb290IENBMB4XDTIzMDYwMTExMjcwMFoXDTMz
+MDUyOTExMjcwMFowFzEVMBMGA1UEAwwMVGVzdCBSb290IENBMIIBIjANBgkqhkiG
+9w0BAQEFAAOCAQ8AMIIBCgKCAQEAl9xCMPMIvfmJWz25AG/VtgWbqNs67HXQbXWf
+pDF2wjQpHVOYbfl7Zgly5O+5es1aUbJaGyZ9G6xuYSXKFnnYLoP7M86O05fQQBAj
+K+IE5nO6136ksCAfxCFTFfn4vhPvK8Vba5rqox4WeIXYKvHYSoiHz0ELrnFOHcyN
+Innyze7bLtkMCA1ShHpmvDCR+U3Uj6JwOfoirn29jjU/48/ORha7dcJYtYXk2eGo
+RJfrtIx20tXAaKaGnXOCGYbEVXTeQkQPqKFVzqP7+KYS/Y8eNFV35ugpLNES+44T
+bQ2QruTZdrNRjJkEoyiB/E53a0OUltB/R7Z0L0xstnKfsAf3OwIDAQABo0IwQDAP
+BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUVdXNh2lk
+51/6hMmz0Z+OpIe8+f0wDQYJKoZIhvcNAQELBQADggEBADNg7G8n6DUrQ5doXzm9
+kOp5siX6iPs0zFReXKhIT1Gef63l3tb7AdPedF03aj9XkUt0shhNOGG5SK2k5KBQ
+MJc9muYRCAyo2xMr3rFUQdI5B51SCy5HeAMralgTHXN0Hv+TH04YfRrACVmr+5ke
+pH3bF1gYaT+Zy5/pHJnV5lcwS6/H44g9XXWIopjWCwbfzKxIuWofqL4fiToPSIYu
+MCUI4bKZipcJT5O6rdz/S9lbgYVjOJ4HAoT2icNQqNMMfULKevmF8SdJzfNd35yn
+tAKTROhIE2aQRVCclrjo/T3eyjWGGoJlGmxKbeCf/rXzcn1BRtk/UzLnbUFFlg5l
+axw=
+-----END CERTIFICATE-----
\ No newline at end of file
diff --git a/Cronet/tests/mts/res/values/cronet-test-rule-configuration.xml b/Cronet/tests/mts/res/values/cronet-test-rule-configuration.xml
new file mode 100644
index 0000000..48ce420
--- /dev/null
+++ b/Cronet/tests/mts/res/values/cronet-test-rule-configuration.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2023 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<resources>
+    <bool name="is_running_in_aosp">true</bool>
+</resources>
\ No newline at end of file
diff --git a/Cronet/tests/mts/res/xml/network_security_config.xml b/Cronet/tests/mts/res/xml/network_security_config.xml
index d44c36f..32b7171 100644
--- a/Cronet/tests/mts/res/xml/network_security_config.xml
+++ b/Cronet/tests/mts/res/xml/network_security_config.xml
@@ -17,18 +17,31 @@
   -->
 
 <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>
+  <base-config>
+    <trust-anchors>
+      <certificates src="@raw/quicroot"/>
+      <certificates src="system"/>
+    </trust-anchors>
+  </base-config>
+  <!-- Since Android 9 (API 28) cleartext support is disabled by default, this
+       causes some of our tests to fail (see crbug/1220357).
+       The following configs allow http requests for the domains used in these
+       tests.
+
+       TODO(stefanoduo): Figure out if we really need to use http for these tests
+  -->
+  <domain-config cleartextTrafficPermitted="true">
+    <!-- Used as the base URL by native test server (net::EmbeddedTestServer) -->
+    <domain includeSubdomains="true">127.0.0.1</domain>
+    <!-- Used by CronetHttpURLConnectionTest#testIOExceptionInterruptRethrown -->
+    <domain includeSubdomains="true">localhost</domain>
+    <!-- Used by CronetHttpURLConnectionTest#testBadIP -->
+    <domain includeSubdomains="true">0.0.0.0</domain>
+    <!-- Used by CronetHttpURLConnectionTest#testSetUseCachesFalse -->
+    <domain includeSubdomains="true">host-cache-test-host</domain>
+    <!-- Used by CronetHttpURLConnectionTest#testBadHostname -->
+    <domain includeSubdomains="true">this-weird-host-name-does-not-exist</domain>
+    <!-- Used by CronetUrlRequestContextTest#testHostResolverRules -->
+    <domain includeSubdomains="true">some-weird-hostname</domain>
+  </domain-config>
 </network-security-config>
\ No newline at end of file
diff --git a/DnsResolver/Android.bp b/DnsResolver/Android.bp
index d133034..716eb10 100644
--- a/DnsResolver/Android.bp
+++ b/DnsResolver/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"],
 }
 
@@ -56,7 +57,10 @@
 cc_test {
     name: "dns_helper_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",
     header_libs: [
         "bpf_connectivity_headers",
@@ -68,8 +72,8 @@
         "libcom.android.tethering.dns_helper",
     ],
     shared_libs: [
-       "libbase",
-       "libcutils",
+        "libbase",
+        "libcutils",
     ],
     compile_multilib: "both",
     multilib: {
diff --git a/DnsResolver/include/DnsHelperPublic.h b/DnsResolver/include/DnsHelperPublic.h
index 7c9fc9e..44b0012 100644
--- a/DnsResolver/include/DnsHelperPublic.h
+++ b/DnsResolver/include/DnsHelperPublic.h
@@ -25,7 +25,8 @@
  * 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, a negative POSIX error code (see errno.h) on other failures.
+ * 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();
 
@@ -36,7 +37,9 @@
  * |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, a negative POSIX error code (see errno.h) on other failures.
+ * 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);
 
diff --git a/OWNERS_core_networking b/OWNERS_core_networking
index 6d17476..83f798a 100644
--- a/OWNERS_core_networking
+++ b/OWNERS_core_networking
@@ -1,16 +1,13 @@
 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
diff --git a/Tethering/Android.bp b/Tethering/Android.bp
index 414e50a..e4e6c70 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"],
 }
 
@@ -82,7 +83,6 @@
     ],
     manifest: "AndroidManifestBase.xml",
     lint: {
-        strict_updatability_linting: true,
         error_checks: ["NewApi"],
     },
 }
@@ -94,14 +94,16 @@
         "ConnectivityNextEnableDefaults",
         "TetheringAndroidLibraryDefaults",
         "TetheringApiLevel",
-        "TetheringReleaseTargetSdk"
+        "TetheringReleaseTargetSdk",
     ],
     static_libs: [
         "NetworkStackApiCurrentShims",
         "net-utils-device-common-struct",
     ],
     apex_available: ["com.android.tethering"],
-    lint: { strict_updatability_linting: true },
+    lint: {
+        baseline_filename: "lint-baseline.xml",
+    },
 }
 
 android_library {
@@ -109,14 +111,16 @@
     defaults: [
         "TetheringAndroidLibraryDefaults",
         "TetheringApiLevel",
-        "TetheringReleaseTargetSdk"
+        "TetheringReleaseTargetSdk",
     ],
     static_libs: [
         "NetworkStackApiStableShims",
         "net-utils-device-common-struct",
     ],
     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).
@@ -189,20 +193,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,
     privapp_allowlist: ":privapp_allowlist_com.android.tethering",
     apex_available: ["com.android.tethering"],
-    lint: { strict_updatability_linting: true },
 }
 
 android_app {
@@ -219,7 +224,6 @@
     privapp_allowlist: ":privapp_allowlist_com.android.tethering",
     apex_available: ["com.android.tethering"],
     lint: {
-        strict_updatability_linting: true,
         error_checks: ["NewApi"],
     },
 }
@@ -239,7 +243,9 @@
 
 java_library_static {
     name: "tetheringstatsprotos",
-    proto: {type: "lite"},
+    proto: {
+        type: "lite",
+    },
     srcs: [
         "src/com/android/networkstack/tethering/metrics/stats.proto",
     ],
@@ -252,6 +258,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 de9017a..30bdf37 100644
--- a/Tethering/apex/Android.bp
+++ b/Tethering/apex/Android.bp
@@ -15,6 +15,7 @@
 //
 
 package {
+    default_team: "trendy_team_fwk_core_networking",
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
@@ -24,6 +25,7 @@
     name: "ConnectivityNextEnableDefaults",
     enabled: true,
 }
+
 java_defaults {
     name: "NetworkStackApiShimSettingsForCurrentBranch",
     // API shims to include in the networking modules built from the branch. Branches that disable
@@ -31,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
@@ -38,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
@@ -50,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 {
@@ -92,7 +86,7 @@
         both: {
             jni_libs: [
                 "libframework-connectivity-jni",
-                "libframework-connectivity-tiramisu-jni"
+                "libframework-connectivity-tiramisu-jni",
             ],
         },
     },
@@ -117,8 +111,9 @@
         "ServiceConnectivityResources",
     ],
     prebuilts: [
-        "ot-daemon.init.34rc",
         "current_sdkinfo",
+        "netbpfload.mainline.rc",
+        "ot-daemon.init.34rc",
     ],
     manifest: "manifest.json",
     key: "com.android.tethering.key",
diff --git a/Tethering/apex/permissions/Android.bp b/Tethering/apex/permissions/Android.bp
index 69c1aa2..20772a8 100644
--- a/Tethering/apex/permissions/Android.bp
+++ b/Tethering/apex/permissions/Android.bp
@@ -15,6 +15,7 @@
 //
 
 package {
+    default_team: "trendy_team_fwk_core_networking",
     default_applicable_licenses: ["Android-Apache-2.0"],
     default_visibility: ["//packages/modules/Connectivity/Tethering:__subpackages__"],
 }
@@ -22,4 +23,4 @@
 filegroup {
     name: "privapp_allowlist_com.android.tethering",
     srcs: ["permissions.xml"],
-}
\ No newline at end of file
+}
diff --git a/Tethering/common/TetheringLib/Android.bp b/Tethering/common/TetheringLib/Android.bp
index 6e8d0c9..9fa073b 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,7 +36,7 @@
         "//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/opt/telephony/tests/telephonytests",
         "//packages/modules/CaptivePortalLogin/tests",
@@ -43,6 +44,7 @@
         "//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,14 +56,19 @@
 
     hostdex: true, // for hiddenapi check
     permitted_packages: ["android.net"],
-    lint: { strict_updatability_linting: true },
+    lint: {
+        strict_updatability_linting: true,
+    },
+    aconfig_declarations: [
+        "com.android.net.flags-aconfig",
+    ],
 }
 
 java_library {
-  name: "framework-tethering-pre-jarjar",
-  defaults: [
-    "framework-tethering-defaults",
-  ],
+    name: "framework-tethering-pre-jarjar",
+    defaults: [
+        "framework-tethering-defaults",
+    ],
 }
 
 java_genrule {
@@ -87,7 +94,7 @@
     name: "framework-tethering-defaults",
     defaults: ["framework-module-defaults"],
     srcs: [
-      ":framework-tethering-srcs"
+        ":framework-tethering-srcs",
     ],
     libs: ["framework-connectivity.stubs.module_lib"],
     aidl: {
@@ -106,5 +113,5 @@
         "src/**/*.aidl",
         "src/**/*.java",
     ],
-    path: "src"
+    path: "src",
 }
diff --git a/Tethering/common/TetheringLib/api/system-current.txt b/Tethering/common/TetheringLib/api/system-current.txt
index 844ff64..a287b42 100644
--- a/Tethering/common/TetheringLib/api/system-current.txt
+++ b/Tethering/common/TetheringLib/api/system-current.txt
@@ -95,13 +95,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/src/android/net/TetheringManager.java b/Tethering/common/TetheringLib/src/android/net/TetheringManager.java
index cd914d3..7b769d4 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,14 @@
  */
 @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";
+    }
+
     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;
@@ -673,14 +684,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;
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/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 a8c8408..544ba01 100644
--- a/Tethering/src/android/net/ip/IpServer.java
+++ b/Tethering/src/android/net/ip/IpServer.java
@@ -33,6 +33,7 @@
 
 import static com.android.net.module.util.Inet4AddressUtils.intToInet4AddressHTH;
 import static com.android.net.module.util.NetworkStackConstants.RFC7421_PREFIX_LENGTH;
+import static com.android.networkstack.tethering.TetheringConfiguration.USE_SYNC_SM;
 import static com.android.networkstack.tethering.UpstreamNetworkState.isVcnInterface;
 import static com.android.networkstack.tethering.util.PrefixUtils.asIpPrefix;
 import static com.android.networkstack.tethering.util.TetheringMessageBase.BASE_IPSERVER;
@@ -59,6 +60,7 @@
 import android.os.Message;
 import android.os.RemoteException;
 import android.os.ServiceSpecificException;
+import android.util.ArraySet;
 import android.util.Log;
 import android.util.SparseArray;
 
@@ -315,7 +317,6 @@
 
     private final TetheringMetrics mTetheringMetrics;
     private final Handler mHandler;
-    private final boolean mIsSyncSM;
 
     // 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.
@@ -325,7 +326,7 @@
             @Nullable LateSdk<RoutingCoordinatorManager> routingCoordinator, Callback callback,
             TetheringConfiguration config, PrivateAddressCoordinator addressCoordinator,
             TetheringMetrics tetheringMetrics, Dependencies deps) {
-        super(ifaceName, config.isSyncSM() ? null : handler.getLooper());
+        super(ifaceName, USE_SYNC_SM ? null : handler.getLooper());
         mHandler = handler;
         mLog = log.forSubComponent(ifaceName);
         mNetd = netd;
@@ -338,7 +339,6 @@
         mLinkProperties = new LinkProperties();
         mUsingLegacyDhcp = config.useLegacyDhcpServer();
         mP2pLeasesSubnetPrefixLength = config.getP2pLeasesSubnetPrefixLength();
-        mIsSyncSM = config.isSyncSM();
         mPrivateAddressCoordinator = addressCoordinator;
         mDeps = deps;
         mTetheringMetrics = tetheringMetrics;
@@ -516,7 +516,7 @@
 
         private void handleError() {
             mLastError = TETHER_ERROR_DHCPSERVER_ERROR;
-            if (mIsSyncSM) {
+            if (USE_SYNC_SM) {
                 sendMessage(CMD_SERVICE_FAILED_TO_START, TETHER_ERROR_DHCPSERVER_ERROR);
             } else {
                 sendMessageAtFrontOfQueueToAsyncSM(CMD_SERVICE_FAILED_TO_START,
@@ -900,7 +900,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));
@@ -908,7 +908,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);
             }
@@ -920,7 +920,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();
@@ -941,7 +941,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);
             }
@@ -1171,7 +1171,7 @@
                 // in previous versions of the mainline module.
                 // TODO : remove sendMessageAtFrontOfQueueToAsyncSM after migrating to the Sync
                 // StateMachine.
-                if (mIsSyncSM) {
+                if (USE_SYNC_SM) {
                     sendSelfMessageToSyncSM(CMD_SERVICE_FAILED_TO_START, mLastError);
                 } else {
                     sendMessageAtFrontOfQueueToAsyncSM(CMD_SERVICE_FAILED_TO_START, mLastError);
@@ -1548,7 +1548,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));
@@ -1579,8 +1579,8 @@
     /** Get IPv6 prefixes from LinkProperties */
     @NonNull
     @VisibleForTesting
-    static HashSet<IpPrefix> getTetherableIpv6Prefixes(@NonNull Collection<LinkAddress> addrs) {
-        final HashSet<IpPrefix> prefixes = new HashSet<>();
+    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));
@@ -1589,7 +1589,7 @@
     }
 
     @NonNull
-    private HashSet<IpPrefix> getTetherableIpv6Prefixes(@NonNull LinkProperties lp) {
+    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 5e9bbcb..d848ea8 100644
--- a/Tethering/src/android/net/ip/RouterAdvertisementDaemon.java
+++ b/Tethering/src/android/net/ip/RouterAdvertisementDaemon.java
@@ -18,7 +18,6 @@
 
 import static android.system.OsConstants.AF_INET6;
 import static android.system.OsConstants.IPPROTO_ICMPV6;
-import static android.system.OsConstants.SOCK_NONBLOCK;
 import static android.system.OsConstants.SOCK_RAW;
 import static android.system.OsConstants.SOL_SOCKET;
 import static android.system.OsConstants.SO_SNDTIMEO;
@@ -39,21 +38,13 @@
 import android.net.MacAddress;
 import android.net.TrafficStats;
 import android.net.util.SocketUtils;
-import android.os.Handler;
-import android.os.HandlerThread;
-import android.os.Looper;
-import android.os.Message;
 import android.system.ErrnoException;
 import android.system.Os;
 import android.system.StructTimeval;
+import android.util.ArraySet;
 import android.util.Log;
-import android.util.Pair;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
 
 import com.android.internal.annotations.GuardedBy;
-import com.android.net.module.util.FdEventsReader;
 import com.android.net.module.util.InterfaceParams;
 import com.android.net.module.util.structs.Icmpv6Header;
 import com.android.net.module.util.structs.LlaOption;
@@ -113,11 +104,6 @@
 
     private static final int DAY_IN_SECONDS = 86_400;
 
-    // Commands for IpServer to control RouterAdvertisementDaemon
-    private static final int CMD_START        = 1;
-    private static final int CMD_STOP         = 2;
-    private static final int CMD_BUILD_NEW_RA = 3;
-
     private final InterfaceParams mInterface;
     private final InetSocketAddress mAllNodes;
 
@@ -135,13 +121,9 @@
     @GuardedBy("mLock")
     private RaParams mRaParams;
 
-    // To be accessed only from RaMessageHandler
-    private RsPacketListener mRsPacketListener;
-
     private volatile FileDescriptor mSocket;
     private volatile MulticastTransmitter mMulticastTransmitter;
-    private volatile RaMessageHandler mRaMessageHandler;
-    private volatile HandlerThread mRaHandlerThread;
+    private volatile UnicastResponder mUnicastResponder;
 
     /** Encapsulate the RA parameters for RouterAdvertisementDaemon.*/
     public static class RaParams {
@@ -153,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);
         }
 
         /**
@@ -263,94 +245,6 @@
         }
     }
 
-    private class RaMessageHandler extends Handler {
-        RaMessageHandler(Looper looper) {
-            super(looper);
-        }
-
-        @Override
-        public void handleMessage(Message msg) {
-            switch (msg.what) {
-                case CMD_START:
-                    mRsPacketListener = new RsPacketListener(this);
-                    mRsPacketListener.start();
-                    break;
-                case CMD_STOP:
-                    if (mRsPacketListener != null) {
-                        mRsPacketListener.stop();
-                        mRsPacketListener = null;
-                    }
-                    break;
-                case CMD_BUILD_NEW_RA:
-                    synchronized (mLock) {
-                        // raInfo.first is deprecatedParams and raInfo.second is newParams.
-                        final Pair<RaParams, RaParams> raInfo = (Pair<RaParams, RaParams>) msg.obj;
-                        if (raInfo.first != null) {
-                            mDeprecatedInfoTracker.putPrefixes(raInfo.first.prefixes);
-                            mDeprecatedInfoTracker.putDnses(raInfo.first.dnses);
-                        }
-
-                        if (raInfo.second != null) {
-                            // Process information that is no longer deprecated.
-                            mDeprecatedInfoTracker.removePrefixes(raInfo.second.prefixes);
-                            mDeprecatedInfoTracker.removeDnses(raInfo.second.dnses);
-                        }
-                        mRaParams = raInfo.second;
-                        assembleRaLocked();
-                    }
-
-                    maybeNotifyMulticastTransmitter();
-                    break;
-                default:
-                    Log.e(TAG, "Unknown message, cmd = " + String.valueOf(msg.what));
-                    break;
-            }
-        }
-    }
-
-    private class RsPacketListener extends FdEventsReader<RsPacketListener.RecvBuffer> {
-        private static final class RecvBuffer {
-            // The recycled buffer for receiving Router Solicitations from clients.
-            // If the RS is larger than IPV6_MIN_MTU the packets are truncated.
-            // This is fine since currently only byte 0 is examined anyway.
-            final byte[] mBytes = new byte[IPV6_MIN_MTU];
-            final InetSocketAddress mSrcAddr = new InetSocketAddress(0);
-        }
-
-        RsPacketListener(@NonNull Handler handler) {
-            super(handler, new RecvBuffer());
-        }
-
-        @Override
-        protected int recvBufSize(@NonNull RecvBuffer buffer) {
-            return buffer.mBytes.length;
-        }
-
-        @Override
-        protected FileDescriptor createFd() {
-            return mSocket;
-        }
-
-        @Override
-        protected int readPacket(@NonNull FileDescriptor fd, @NonNull RecvBuffer buffer)
-                throws Exception {
-            return Os.recvfrom(
-                    fd, buffer.mBytes, 0, buffer.mBytes.length, 0 /* flags */, buffer.mSrcAddr);
-        }
-
-        @Override
-        protected final void handlePacket(@NonNull RecvBuffer buffer, int length) {
-            // Do the least possible amount of validations.
-            if (buffer.mSrcAddr == null
-                    || length <= 0
-                    || buffer.mBytes[0] != asByte(ICMPV6_ROUTER_SOLICITATION)) {
-                return;
-            }
-
-            maybeSendRA(buffer.mSrcAddr);
-        }
-    }
-
     public RouterAdvertisementDaemon(InterfaceParams ifParams) {
         mInterface = ifParams;
         mAllNodes = new InetSocketAddress(getAllNodesForScopeId(mInterface.index), 0);
@@ -359,43 +253,48 @@
 
     /** Build new RA.*/
     public void buildNewRa(RaParams deprecatedParams, RaParams newParams) {
-        final Pair<RaParams, RaParams> raInfo = new Pair<>(deprecatedParams, newParams);
-        sendMessage(CMD_BUILD_NEW_RA, raInfo);
+        synchronized (mLock) {
+            if (deprecatedParams != null) {
+                mDeprecatedInfoTracker.putPrefixes(deprecatedParams.prefixes);
+                mDeprecatedInfoTracker.putDnses(deprecatedParams.dnses);
+            }
+
+            if (newParams != null) {
+                // Process information that is no longer deprecated.
+                mDeprecatedInfoTracker.removePrefixes(newParams.prefixes);
+                mDeprecatedInfoTracker.removeDnses(newParams.dnses);
+            }
+
+            mRaParams = newParams;
+            assembleRaLocked();
+        }
+
+        maybeNotifyMulticastTransmitter();
     }
 
     /** Start router advertisement daemon. */
     public boolean start() {
         if (!createSocket()) {
-            Log.e(TAG, "Failed to start RouterAdvertisementDaemon.");
             return false;
         }
 
         mMulticastTransmitter = new MulticastTransmitter();
         mMulticastTransmitter.start();
 
-        mRaHandlerThread = new HandlerThread(TAG);
-        mRaHandlerThread.start();
-        mRaMessageHandler = new RaMessageHandler(mRaHandlerThread.getLooper());
+        mUnicastResponder = new UnicastResponder();
+        mUnicastResponder.start();
 
-        return sendMessage(CMD_START);
+        return true;
     }
 
     /** Stop router advertisement daemon. */
     public void stop() {
-        if (!sendMessage(CMD_STOP)) {
-            Log.e(TAG, "RouterAdvertisementDaemon has been stopped or was never started.");
-            return;
-        }
-
-        mRaHandlerThread.quitSafely();
-        mRaHandlerThread = null;
-        mRaMessageHandler = null;
-
         closeSocket();
         // Wake up mMulticastTransmitter thread to interrupt a potential 1 day sleep before
         // the thread's termination.
         maybeNotifyMulticastTransmitter();
         mMulticastTransmitter = null;
+        mUnicastResponder = null;
     }
 
     @GuardedBy("mLock")
@@ -605,7 +504,7 @@
 
         final int oldTag = TrafficStats.getAndSetThreadStatsTag(TAG_SYSTEM_NEIGHBOR);
         try {
-            mSocket = Os.socket(AF_INET6, SOCK_RAW | SOCK_NONBLOCK, IPPROTO_ICMPV6);
+            mSocket = Os.socket(AF_INET6, SOCK_RAW, IPPROTO_ICMPV6);
             // Setting SNDTIMEO is purely for defensive purposes.
             Os.setsockoptTimeval(
                     mSocket, SOL_SOCKET, SO_SNDTIMEO, StructTimeval.fromMillis(send_timout_ms));
@@ -667,17 +566,34 @@
         }
     }
 
-    private boolean sendMessage(int cmd) {
-        return sendMessage(cmd, null);
-    }
+    private final class UnicastResponder extends Thread {
+        private final InetSocketAddress mSolicitor = new InetSocketAddress(0);
+        // The recycled buffer for receiving Router Solicitations from clients.
+        // If the RS is larger than IPV6_MIN_MTU the packets are truncated.
+        // This is fine since currently only byte 0 is examined anyway.
+        private final byte[] mSolicitation = new byte[IPV6_MIN_MTU];
 
-    private boolean sendMessage(int cmd, @Nullable Object obj) {
-        if (mRaMessageHandler == null) {
-            return false;
+        @Override
+        public void run() {
+            while (isSocketValid()) {
+                try {
+                    // Blocking receive.
+                    final int rval = Os.recvfrom(
+                            mSocket, mSolicitation, 0, mSolicitation.length, 0, mSolicitor);
+                    // Do the least possible amount of validation.
+                    if (rval < 1 || mSolicitation[0] != asByte(ICMPV6_ROUTER_SOLICITATION)) {
+                        continue;
+                    }
+                } catch (ErrnoException | SocketException e) {
+                    if (isSocketValid()) {
+                        Log.e(TAG, "recvfrom error: " + e);
+                    }
+                    continue;
+                }
+
+                maybeSendRA(mSolicitor);
+            }
         }
-
-        return mRaMessageHandler.sendMessage(
-                Message.obtain(mRaMessageHandler, cmd, obj));
     }
 
     // TODO: Consider moving this to run on a provided Looper as a Handler,
diff --git a/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java b/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
index 9f542f4..81e18ab 100644
--- a/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
+++ b/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
@@ -379,7 +379,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;
@@ -391,7 +391,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;
@@ -403,7 +403,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;
@@ -414,7 +414,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);
@@ -427,7 +427,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;
@@ -439,7 +439,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;
@@ -451,7 +451,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;
diff --git a/Tethering/src/com/android/networkstack/tethering/OffloadHardwareInterface.java b/Tethering/src/com/android/networkstack/tethering/OffloadHardwareInterface.java
index 53c80ae..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;
diff --git a/Tethering/src/com/android/networkstack/tethering/Tethering.java b/Tethering/src/com/android/networkstack/tethering/Tethering.java
index 8ee28bd..6085a00 100644
--- a/Tethering/src/com/android/networkstack/tethering/Tethering.java
+++ b/Tethering/src/com/android/networkstack/tethering/Tethering.java
@@ -137,6 +137,7 @@
 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.SdkUtil.LateSdk;
 import com.android.net.module.util.SharedLog;
@@ -162,11 +163,8 @@
 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;
 
 /**
  *
@@ -371,6 +369,7 @@
 
         // 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.
@@ -2712,31 +2711,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() {
diff --git a/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java b/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java
index d09183a..298940e 100644
--- a/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java
+++ b/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java
@@ -141,6 +141,9 @@
      */
     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;
@@ -174,7 +177,6 @@
 
     private final boolean mEnableWearTethering;
     private final boolean mRandomPrefixBase;
-    private final boolean mEnableSyncSm;
 
     private final int mUsbTetheringFunction;
     protected final ContentResolver mContentResolver;
@@ -293,7 +295,6 @@
         mEnableWearTethering = shouldEnableWearTethering(ctx);
 
         mRandomPrefixBase = mDeps.isFeatureEnabled(ctx, TETHER_FORCE_RANDOM_PREFIX_BASE_SELECTION);
-        mEnableSyncSm = mDeps.isFeatureEnabled(ctx, TETHER_ENABLE_SYNC_SM);
 
         configLog.log(toString());
     }
@@ -387,8 +388,14 @@
         return mRandomPrefixBase;
     }
 
-    public boolean isSyncSM() {
-        return mEnableSyncSm;
+    /**
+     * 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.*/
@@ -445,8 +452,8 @@
         pw.print("mRandomPrefixBase: ");
         pw.println(mRandomPrefixBase);
 
-        pw.print("mEnableSyncSm: ");
-        pw.println(mEnableSyncSm);
+        pw.print("USE_SYNC_SM: ");
+        pw.println(USE_SYNC_SM);
     }
 
     /** Returns the string representation of this object.*/
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 2594a5e..07fa733 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"],
 }
 
@@ -45,12 +46,11 @@
 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.
@@ -58,7 +58,6 @@
 android_library {
     name: "TetheringIntegrationTestsLatestSdkLib",
     target_sdk_version: "33",
-    platform_apis: true,
     defaults: ["TetheringIntegrationTestsDefaults"],
     srcs: [
         "src/**/*.java",
@@ -67,7 +66,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.
@@ -75,7 +74,6 @@
 android_library {
     name: "TetheringIntegrationTestsLib",
     target_sdk_version: "current",
-    platform_apis: true,
     defaults: ["TetheringIntegrationTestsDefaults"],
     srcs: [
         "src/**/*.java",
@@ -83,7 +81,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 377da91..2933a44 100644
--- a/Tethering/tests/integration/base/android/net/EthernetTetheringTestBase.java
+++ b/Tethering/tests/integration/base/android/net/EthernetTetheringTestBase.java
@@ -31,12 +31,14 @@
 import static android.net.TetheringTester.isExpectedIcmpPacket;
 import static android.net.TetheringTester.isExpectedTcpPacket;
 import static android.net.TetheringTester.isExpectedUdpPacket;
+
 import static com.android.net.module.util.HexDump.dumpHexString;
 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;
 import static com.android.testutils.TestNetworkTrackerKt.initTestNetwork;
 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;
@@ -46,7 +48,6 @@
 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;
@@ -56,8 +57,6 @@
 import android.net.TetheringManager.TetheringRequest;
 import android.net.TetheringTester.TetheredDevice;
 import android.net.cts.util.CtsNetUtils;
-import android.net.cts.util.CtsTetheringUtils;
-import android.net.cts.util.CtsTetheringUtils.TestTetheringEventCallback;
 import android.os.Handler;
 import android.os.HandlerThread;
 import android.os.SystemClock;
@@ -141,11 +140,12 @@
     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 static final Context sContext =
+            InstrumentationRegistry.getInstrumentation().getContext();
+    private 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);
 
     // Late initialization in setUp()
     private boolean mRunTests;
@@ -161,7 +161,7 @@
     private MyTetheringEventCallback mTetheringEventCallback;
 
     public Context getContext() {
-        return mContext;
+        return sContext;
     }
 
     @BeforeClass
@@ -170,19 +170,24 @@
         // Tethering would cache the last upstreams so that the next enabled tethering avoids
         // picking up the address that is in conflict with the upstreams. To protect subsequent
         // tests, turn tethering on and off before running them.
-        final Context ctx = InstrumentationRegistry.getInstrumentation().getContext();
-        final CtsTetheringUtils utils = new CtsTetheringUtils(ctx);
-        final TestTetheringEventCallback callback = utils.registerTetheringEventCallback();
+        MyTetheringEventCallback callback = null;
+        TestNetworkInterface testIface = null;
         try {
-            if (!callback.isWifiTetheringSupported(ctx)) return;
+            // If the physical ethernet interface is available, do nothing.
+            if (isInterfaceForTetheringAvailable()) return;
 
-            callback.expectNoTetheringActive();
+            testIface = createTestInterface();
+            setIncludeTestInterfaces(true);
 
-            utils.startWifiTethering(callback);
-            callback.getCurrentValidUpstream();
-            utils.stopWifiTethering(callback);
+            callback = enableEthernetTethering(testIface.getInterfaceName(), null);
+            callback.awaitUpstreamChanged(true /* throwTimeoutException */);
+        } catch (TimeoutException e) {
+            Log.d(TAG, "WARNNING " + e);
         } finally {
-            utils.unregisterTetheringEventCallback(callback);
+            maybeCloseTestInterface(testIface);
+            maybeUnregisterTetheringEventCallback(callback);
+
+            setIncludeTestInterfaces(false);
         }
     }
 
@@ -195,13 +200,13 @@
         mRunTests = isEthernetTetheringSupported();
         assumeTrue(mRunTests);
 
-        mTetheredInterfaceRequester = new TetheredInterfaceRequester(mHandler, mEm);
+        mTetheredInterfaceRequester = new TetheredInterfaceRequester();
     }
 
     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)
@@ -212,7 +217,7 @@
         }
     }
 
-    protected void maybeCloseTestInterface(final TestNetworkInterface testInterface)
+    protected static void maybeCloseTestInterface(final TestNetworkInterface testInterface)
             throws Exception {
         if (testInterface != null) {
             testInterface.getFileDescriptor().close();
@@ -220,8 +225,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();
@@ -230,7 +235,7 @@
 
     protected void stopEthernetTethering(final MyTetheringEventCallback callback) {
         runAsShell(TETHER_PRIVILEGED, () -> {
-            mTm.stopTethering(TETHERING_ETHERNET);
+            sTm.stopTethering(TETHERING_ETHERNET);
             maybeUnregisterTetheringEventCallback(callback);
         });
     }
@@ -277,18 +282,18 @@
         }
     }
 
-    protected boolean isInterfaceForTetheringAvailable() throws Exception {
+    protected static boolean isInterfaceForTetheringAvailable() throws Exception {
         // Before T, all ethernet interfaces could be used for server mode. Instead of
         // waiting timeout, just checking whether the system currently has any
         // ethernet interface is more reliable.
         if (!SdkLevel.isAtLeastT()) {
-            return runAsShell(CONNECTIVITY_USE_RESTRICTED_NETWORKS, () -> mEm.isAvailable());
+            return runAsShell(CONNECTIVITY_USE_RESTRICTED_NETWORKS, () -> sEm.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);
+        final TetheredInterfaceRequester requester = new TetheredInterfaceRequester();
         try {
             // Use short timeout (200ms) for requesting an existing interface, if any, because
             // it should reurn faster than requesting a new tethering interface. Using default
@@ -306,15 +311,15 @@
         }
     }
 
-    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);
         });
     }
 
@@ -344,7 +349,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);
@@ -355,7 +359,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;
@@ -368,19 +372,21 @@
         // seconds. See b/289881008.
         private static final int EXPANDED_TIMEOUT_MS = 30000;
 
-        MyTetheringEventCallback(TetheringManager tm, String iface) {
-            this(tm, iface, null);
+        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
@@ -504,6 +510,11 @@
 
             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();
             }
@@ -525,18 +536,18 @@
         }
     }
 
-    protected MyTetheringEventCallback enableEthernetTethering(String iface,
+    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();
@@ -556,7 +567,7 @@
         };
         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)) {
@@ -579,7 +590,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)
@@ -605,17 +616,9 @@
     }
 
     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;
-        }
-
         @Override
         public void onAvailable(String iface) {
             Log.d(TAG, "Ethernet interface available: " + iface);
@@ -631,7 +634,7 @@
             assertNull("BUG: more than one tethered interface request", mRequest);
             Log.d(TAG, "Requesting tethered interface");
             mRequest = runAsShell(NETWORK_SETTINGS, () ->
-                    mEm.requestTetheredInterface(mHandler::post, this));
+                    sEm.requestTetheredInterface(c -> c.run() /* executor */, this));
             return mFuture;
         }
 
@@ -652,9 +655,9 @@
         }
     }
 
-    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());
@@ -669,7 +672,7 @@
         lp.setLinkAddresses(addresses);
         lp.setDnsServers(dnses);
 
-        return runAsShell(MANAGE_TEST_NETWORKS, () -> initTestNetwork(mContext, lp, TIMEOUT_MS));
+        return runAsShell(MANAGE_TEST_NETWORKS, () -> initTestNetwork(sContext, lp, TIMEOUT_MS));
     }
 
     protected void sendDownloadPacketUdp(@NonNull final InetAddress srcIp,
@@ -851,7 +854,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
@@ -862,12 +865,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<>();
@@ -881,14 +884,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);
         }
     }
 
@@ -925,7 +928,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/mts/Android.bp b/Tethering/tests/mts/Android.bp
index 4f4b03c..a80e49e 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"],
 }
 
diff --git a/Tethering/tests/privileged/Android.bp b/Tethering/tests/privileged/Android.bp
index c890197..ba6be66 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"],
 }
 
diff --git a/Tethering/tests/privileged/src/android/net/ip/RouterAdvertisementDaemonTest.java b/Tethering/tests/privileged/src/android/net/ip/RouterAdvertisementDaemonTest.java
index dac5b63..90ceaa1 100644
--- a/Tethering/tests/privileged/src/android/net/ip/RouterAdvertisementDaemonTest.java
+++ b/Tethering/tests/privileged/src/android/net/ip/RouterAdvertisementDaemonTest.java
@@ -47,6 +47,7 @@
 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;
@@ -77,7 +78,6 @@
 import java.net.Inet6Address;
 import java.net.InetAddress;
 import java.nio.ByteBuffer;
-import java.util.HashSet;
 
 @RunWith(AndroidJUnit4.class)
 @SmallTest
@@ -236,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);
 
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..d5d71bc 100644
--- a/Tethering/tests/privileged/src/com/android/networkstack/tethering/BpfMapTest.java
+++ b/Tethering/tests/privileged/src/com/android/networkstack/tethering/BpfMapTest.java
@@ -84,7 +84,7 @@
 
     private void initTestMap() throws Exception {
         mTestMap = new BpfMap<>(
-                TETHER_DOWNSTREAM6_FS_PATH, BpfMap.BPF_F_RDWR,
+                TETHER_DOWNSTREAM6_FS_PATH,
                 TetherDownstream6Key.class, Tether6Value.class);
 
         mTestMap.forEach((key, value) -> {
@@ -135,7 +135,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);
         }
@@ -389,7 +389,7 @@
     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);
@@ -409,8 +409,8 @@
         final int before = getNumOpenFds();
         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
             }
         }
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/com/android/networkstack/tethering/TetheringConfigurationTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringConfigurationTest.java
index 19c6e5a..dd51c7a 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringConfigurationTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringConfigurationTest.java
@@ -757,23 +757,24 @@
 
     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 assertEnableSyncSMIs(boolean value) {
-        assertEquals(value, new TetheringConfiguration(
-                mMockContext, mLog, INVALID_SUBSCRIPTION_ID, mDeps).isSyncSM());
+    private void assertEnableSyncSM(boolean value) {
+        assertEquals(value, TetheringConfiguration.USE_SYNC_SM);
     }
 
     @Test
     public void testEnableSyncSMFlag() throws Exception {
         // Test default disabled
         setTetherEnableSyncSMFlagEnabled(null);
-        assertEnableSyncSMIs(false);
+        assertEnableSyncSM(false);
 
         setTetherEnableSyncSMFlagEnabled(true);
-        assertEnableSyncSMIs(true);
+        assertEnableSyncSM(true);
 
         setTetherEnableSyncSMFlagEnabled(false);
-        assertEnableSyncSMIs(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 82b8845..750bfce 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
@@ -2810,12 +2810,10 @@
         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
diff --git a/bpf_progs/Android.bp b/bpf_progs/Android.bp
index cdf47e7..674cd98 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"],
 }
 
diff --git a/bpf_progs/netd.c b/bpf_progs/netd.c
index f223dd1..5e401aa 100644
--- a/bpf_progs/netd.c
+++ b/bpf_progs/netd.c
@@ -446,8 +446,18 @@
                                                       const struct egress_bool egress,
                                                       const bool enable_tracing,
                                                       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) {
@@ -540,7 +550,7 @@
                     bpf_cgroup_egress_trace_user, KVER_5_8, KVER_INF,
                     BPFLOADER_IGNORED_ON_VERSION, BPFLOADER_MAX_VER, OPTIONAL,
                     "fs_bpf_netd_readonly", "",
-                    LOAD_ON_ENG, IGNORE_ON_USER, LOAD_ON_USERDEBUG)
+                    IGNORE_ON_ENG, LOAD_ON_USER, IGNORE_ON_USERDEBUG)
 (struct __sk_buff* skb) {
     return bpf_traffic_account(skb, EGRESS, TRACE_ON, KVER_5_8);
 }
@@ -616,12 +626,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;
diff --git a/bpf_progs/netd.h b/bpf_progs/netd.h
index 64ed633..098147f 100644
--- a/bpf_progs/netd.h
+++ b/bpf_progs/netd.h
@@ -178,7 +178,7 @@
 #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),
@@ -196,14 +196,14 @@
 };
 // 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,
 };
diff --git a/clatd/Android.bp b/clatd/Android.bp
index 595c6b9..43eb2d8 100644
--- a/clatd/Android.bp
+++ b/clatd/Android.bp
@@ -1,4 +1,5 @@
 package {
+    default_team: "trendy_team_fwk_core_networking",
     default_applicable_licenses: ["external_android-clat_license"],
 }
 
@@ -54,7 +55,7 @@
     defaults: ["clatd_defaults"],
     srcs: [
         ":clatd_common",
-        "main.c"
+        "main.c",
     ],
     static_libs: [
         "libip_checksum",
@@ -101,7 +102,7 @@
     defaults: ["clatd_defaults"],
     srcs: [
         ":clatd_common",
-        "clatd_test.cpp"
+        "clatd_test.cpp",
     ],
     static_libs: [
         "libbase",
diff --git a/common/Android.bp b/common/Android.bp
index 1d73a46..0048a0a 100644
--- a/common/Android.bp
+++ b/common/Android.bp
@@ -15,11 +15,12 @@
 //
 
 package {
+    default_team: "trendy_team_fwk_core_networking",
     // See: http://go/android-license-faq
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
-build = ["TrunkStable.bp"]
+build = ["FlaggedApi.bp"]
 
 // This is a placeholder comment to avoid merge conflicts
 // as the above target may not exist
@@ -49,5 +50,7 @@
     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..56625c5
--- /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: "system",
+    srcs: ["flags.aconfig"],
+    visibility: ["//packages/modules/Connectivity:__subpackages__"],
+}
+
+aconfig_declarations {
+    name: "com.android.net.thread.flags-aconfig",
+    package: "com.android.net.thread.flags",
+    container: "system",
+    srcs: ["thread_flags.aconfig"],
+    visibility: ["//packages/modules/Connectivity:__subpackages__"],
+}
+
+aconfig_declarations {
+    name: "nearby_flags",
+    package: "com.android.nearby.flags",
+    container: "system",
+    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..19b522c
--- /dev/null
+++ b/common/flags.aconfig
@@ -0,0 +1,75 @@
+package: "com.android.net.flags"
+container: "system"
+
+# 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"
+  namespace: "android_core_networking"
+  description: "Set data saver through ConnectivityManager API"
+  bug: "297836825"
+}
+
+flag {
+  name: "support_is_uid_networking_blocked"
+  namespace: "android_core_networking"
+  description: "This flag controls whether isUidNetworkingBlocked is supported"
+  bug: "297836825"
+}
+
+flag {
+  name: "basic_background_restrictions_enabled"
+  namespace: "android_core_networking"
+  description: "Block network access for apps in a low importance background state"
+  bug: "304347838"
+}
+
+flag {
+  name: "ipsec_transform_state"
+  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"
+  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"
+  namespace: "android_core_networking"
+  description: "Flag for API to support requesting restricted wifi"
+  bug: "315835605"
+}
+
+flag {
+  name: "net_capability_local_network"
+  namespace: "android_core_networking"
+  description: "Flag for local network capability API"
+  bug: "313000440"
+}
+
+flag {
+  name: "support_transport_satellite"
+  namespace: "android_core_networking"
+  description: "Flag for satellite transport API"
+  bug: "320514105"
+}
+
+flag {
+  name: "nsd_subtypes_support_enabled"
+  namespace: "android_core_networking"
+  description: "Flag for API to support nsd subtypes"
+  bug: "265095929"
+}
+
+flag {
+  name: "register_nsd_offload_engine_api"
+  namespace: "android_core_networking"
+  description: "Flag for API to register nsd offload engine"
+  bug: "301713539"
+}
diff --git a/common/nearby_flags.aconfig b/common/nearby_flags.aconfig
new file mode 100644
index 0000000..b957d33
--- /dev/null
+++ b/common/nearby_flags.aconfig
@@ -0,0 +1,9 @@
+package: "com.android.nearby.flags"
+container: "system"
+
+flag {
+    name: "powered_off_finding"
+    namespace: "nearby"
+    description: "Controls whether the Powered Off Finding feature is enabled"
+    bug: "307898240"
+}
diff --git a/thread/flags/thread_base.aconfig b/common/thread_flags.aconfig
similarity index 90%
rename from thread/flags/thread_base.aconfig
rename to common/thread_flags.aconfig
index bf1f288..09595a6 100644
--- a/thread/flags/thread_base.aconfig
+++ b/common/thread_flags.aconfig
@@ -1,4 +1,5 @@
 package: "com.android.net.thread.flags"
+container: "system"
 
 flag {
     name: "thread_enabled"
diff --git a/framework-t/Android.bp b/framework-t/Android.bp
index deda74e..bc919ac 100644
--- a/framework-t/Android.bp
+++ b/framework-t/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"],
 }
@@ -34,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.
 
@@ -57,6 +59,10 @@
         "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.
@@ -112,6 +118,7 @@
         "framework-bluetooth",
         "framework-wifi",
         "framework-connectivity-pre-jarjar",
+        "framework-location.stubs.module_lib",
     ],
     visibility: ["//packages/modules/Connectivity:__subpackages__"],
 }
@@ -134,6 +141,7 @@
         "sdk_module-lib_current_framework-connectivity",
     ],
     libs: [
+        "framework-location.stubs.module_lib",
         "sdk_module-lib_current_framework-connectivity",
     ],
     permitted_packages: [
@@ -174,6 +182,7 @@
         "//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/opt/net/ethernet/tests:__subpackages__",
         "//frameworks/opt/telephony/tests/telephonytests",
@@ -182,10 +191,16 @@
         "//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",
+    ],
 }
 
 // This rule is not used anymore(b/268440216).
diff --git a/framework-t/api/current.txt b/framework-t/api/current.txt
index fb46ee7..9ae0cf7 100644
--- a/framework-t/api/current.txt
+++ b/framework-t/api/current.txt
@@ -127,7 +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 getIpSecTransformState(@NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<android.net.IpSecTransformState,java.lang.RuntimeException>);
+    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 {
@@ -145,7 +145,7 @@
     method public long getPacketCount();
     method @NonNull public byte[] getReplayBitmap();
     method public long getRxHighestSequenceNumber();
-    method public long getTimestamp();
+    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;
@@ -158,7 +158,7 @@
     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 setTimestamp(long);
+    method @NonNull public android.net.IpSecTransformState.Builder setTimestampMillis(long);
     method @NonNull public android.net.IpSecTransformState.Builder setTxHighestSequenceNumber(long);
   }
 
@@ -210,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);
@@ -275,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);
@@ -283,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/system-current.txt b/framework-t/api/system-current.txt
index 3513573..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 {
@@ -500,6 +506,7 @@
     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
@@ -507,6 +514,9 @@
     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
   }
 
@@ -518,6 +528,7 @@
   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 {
@@ -530,8 +541,10 @@
     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
   }
 
diff --git a/framework-t/src/android/app/usage/NetworkStatsManager.java b/framework-t/src/android/app/usage/NetworkStatsManager.java
index d139544..7fa0661 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
diff --git a/framework-t/src/android/net/ConnectivityFrameworkInitializerTiramisu.java b/framework-t/src/android/net/ConnectivityFrameworkInitializerTiramisu.java
index d89964d..d7cff2c 100644
--- a/framework-t/src/android/net/ConnectivityFrameworkInitializerTiramisu.java
+++ b/framework-t/src/android/net/ConnectivityFrameworkInitializerTiramisu.java
@@ -27,6 +27,8 @@
 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
  * since Android T.
@@ -83,14 +85,17 @@
                 }
         );
 
-        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,
diff --git a/framework-t/src/android/net/IpSecTransform.java b/framework-t/src/android/net/IpSecTransform.java
index 246a2dd..4e10a96 100644
--- a/framework-t/src/android/net/IpSecTransform.java
+++ b/framework-t/src/android/net/IpSecTransform.java
@@ -215,7 +215,7 @@
      * @see IpSecTransformState
      */
     @FlaggedApi(IPSEC_TRANSFORM_STATE)
-    public void getIpSecTransformState(
+    public void requestIpSecTransformState(
             @CallbackExecutor @NonNull Executor executor,
             @NonNull OutcomeReceiver<IpSecTransformState, RuntimeException> callback) {
         Objects.requireNonNull(executor);
diff --git a/framework-t/src/android/net/IpSecTransformState.java b/framework-t/src/android/net/IpSecTransformState.java
index b575dd5..5b80ae2 100644
--- a/framework-t/src/android/net/IpSecTransformState.java
+++ b/framework-t/src/android/net/IpSecTransformState.java
@@ -23,6 +23,7 @@
 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;
@@ -40,7 +41,7 @@
  */
 @FlaggedApi(IPSEC_TRANSFORM_STATE)
 public final class IpSecTransformState implements Parcelable {
-    private final long mTimeStamp;
+    private final long mTimestamp;
     private final long mTxHighestSequenceNumber;
     private final long mRxHighestSequenceNumber;
     private final long mPacketCount;
@@ -54,7 +55,7 @@
             long packetCount,
             long byteCount,
             byte[] replayBitmap) {
-        mTimeStamp = timestamp;
+        mTimestamp = timestamp;
         mTxHighestSequenceNumber = txHighestSequenceNumber;
         mRxHighestSequenceNumber = rxHighestSequenceNumber;
         mPacketCount = packetCount;
@@ -78,7 +79,7 @@
     @VisibleForTesting(visibility = Visibility.PRIVATE)
     public IpSecTransformState(@NonNull Parcel in) {
         Objects.requireNonNull(in, "The input PersistableBundle is null");
-        mTimeStamp = in.readLong();
+        mTimestamp = in.readLong();
         mTxHighestSequenceNumber = in.readLong();
         mRxHighestSequenceNumber = in.readLong();
         mPacketCount = in.readLong();
@@ -97,7 +98,7 @@
 
     @Override
     public void writeToParcel(@NonNull Parcel out, int flags) {
-        out.writeLong(mTimeStamp);
+        out.writeLong(mTimestamp);
         out.writeLong(mTxHighestSequenceNumber);
         out.writeLong(mRxHighestSequenceNumber);
         out.writeLong(mPacketCount);
@@ -120,16 +121,17 @@
             };
 
     /**
-     * Retrieve the epoch timestamp (milliseconds) for when this state was created
+     * Retrieve the timestamp (milliseconds) when this state was created, as per {@link
+     * SystemClock#elapsedRealtime}
      *
-     * @see Builder#setTimestamp(long)
+     * @see Builder#setTimestampMillis(long)
      */
-    public long getTimestamp() {
-        return mTimeStamp;
+    public long getTimestampMillis() {
+        return mTimestamp;
     }
 
     /**
-     * Retrieve the highest sequence number sent so far
+     * Retrieve the highest sequence number sent so far as an unsigned long
      *
      * @see Builder#setTxHighestSequenceNumber(long)
      */
@@ -138,7 +140,7 @@
     }
 
     /**
-     * Retrieve the highest sequence number received so far
+     * Retrieve the highest sequence number received so far as an unsigned long
      *
      * @see Builder#setRxHighestSequenceNumber(long)
      */
@@ -147,7 +149,10 @@
     }
 
     /**
-     * Retrieve the number of packets received AND sent so far
+     * 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)
      */
@@ -156,7 +161,10 @@
     }
 
     /**
-     * Retrieve the number of bytes received AND sent so far
+     * 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)
      */
@@ -183,10 +191,15 @@
         return mReplayBitmap.clone();
     }
 
-    /** Builder class for testing purposes */
+    /**
+     * 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 mTimestamp;
         private long mTxHighestSequenceNumber;
         private long mRxHighestSequenceNumber;
         private long mPacketCount;
@@ -194,22 +207,22 @@
         private byte[] mReplayBitmap;
 
         public Builder() {
-            mTimeStamp = System.currentTimeMillis();
+            mTimestamp = SystemClock.elapsedRealtime();
         }
 
         /**
-         * Set the epoch timestamp (milliseconds) for when this state was created
+         * Set the timestamp (milliseconds) when this state was created
          *
-         * @see IpSecTransformState#getTimestamp()
+         * @see IpSecTransformState#getTimestampMillis()
          */
         @NonNull
-        public Builder setTimestamp(long timeStamp) {
-            mTimeStamp = timeStamp;
+        public Builder setTimestampMillis(long timestamp) {
+            mTimestamp = timestamp;
             return this;
         }
 
         /**
-         * Set the highest sequence number sent so far
+         * Set the highest sequence number sent so far as an unsigned long
          *
          * @see IpSecTransformState#getTxHighestSequenceNumber()
          */
@@ -220,7 +233,7 @@
         }
 
         /**
-         * Set the highest sequence number received so far
+         * Set the highest sequence number received so far as an unsigned long
          *
          * @see IpSecTransformState#getRxHighestSequenceNumber()
          */
@@ -231,7 +244,7 @@
         }
 
         /**
-         * Set the number of packets received AND sent so far
+         * Set the number of packets processed so far as an unsigned long
          *
          * @see IpSecTransformState#getPacketCount()
          */
@@ -242,7 +255,7 @@
         }
 
         /**
-         * Set the number of bytes received AND sent so far
+         * Set the number of bytes processed so far as an unsigned long
          *
          * @see IpSecTransformState#getByteCount()
          */
@@ -271,7 +284,7 @@
         @NonNull
         public IpSecTransformState build() {
             return new IpSecTransformState(
-                    mTimeStamp,
+                    mTimestamp,
                     mTxHighestSequenceNumber,
                     mRxHighestSequenceNumber,
                     mPacketCount,
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 b6f6dbb..934b4c6 100644
--- a/framework-t/src/android/net/NetworkStatsCollection.java
+++ b/framework-t/src/android/net/NetworkStatsCollection.java
@@ -60,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;
 
@@ -116,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();
     }
 
@@ -483,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 {
@@ -967,8 +985,8 @@
      * @hide
      */
     @Nullable
-    public static String compareStats(
-            NetworkStatsCollection migrated, NetworkStatsCollection legacy) {
+    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();
@@ -980,7 +998,7 @@
             final NetworkStatsHistory legHistory = legEntries.get(legKey);
             final NetworkStatsHistory migHistory = migEntries.get(legKey);
 
-            if (migHistory == null && couldKeyChangeOnImport(legKey)) {
+            if (migHistory == null && allowKeyChange && couldKeyChangeOnImport(legKey)) {
                 unmatchedLegKeys.remove(legKey);
                 continue;
             }
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..2895b0c
--- /dev/null
+++ b/framework-t/src/android/net/nsd/AdvertisingRequest.java
@@ -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 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 or
+     * {@code null} if not specified.
+     *
+     * @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 ? -1 : 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: only number of seconds of {@code ttl} is used.
+         *
+         * @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 e671db1..9a31278 100644
--- a/framework-t/src/android/net/nsd/INsdServiceConnector.aidl
+++ b/framework-t/src/android/net/nsd/INsdServiceConnector.aidl
@@ -16,6 +16,8 @@
 
 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;
@@ -27,9 +29,9 @@
  * {@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();
@@ -38,4 +40,4 @@
     void unregisterServiceInfoCallback(int listenerKey);
     void registerOffloadEngine(String ifaceName, in IOffloadEngine cb, long offloadCapabilities, long offloadType);
     void unregisterOffloadEngine(in IOffloadEngine cb);
-}
\ No newline at end of file
+}
diff --git a/framework-t/src/android/net/nsd/NsdManager.java b/framework-t/src/android/net/nsd/NsdManager.java
index bf01a9d..1001423 100644
--- a/framework-t/src/android/net/nsd/NsdManager.java
+++ b/framework-t/src/android/net/nsd/NsdManager.java
@@ -46,10 +46,12 @@
 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;
@@ -57,6 +59,8 @@
 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
@@ -150,9 +154,65 @@
     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
      * information as int.
@@ -269,6 +329,20 @@
     /** 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");
@@ -325,6 +399,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
@@ -654,9 +730,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) {
@@ -677,6 +756,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));
         }
@@ -686,8 +771,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
@@ -965,10 +1050,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) {
@@ -976,17 +1063,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(
@@ -1001,12 +1093,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);
@@ -1079,17 +1171,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);
         }
@@ -1100,6 +1214,7 @@
         synchronized (mMapLock) {
             mListenerMap.remove(key);
             mServiceMap.remove(key);
+            mDiscoveryMap.remove(key);
             mExecutorMap.remove(key);
         }
     }
@@ -1115,9 +1230,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();
     }
 
     /**
@@ -1160,14 +1275,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();
         }
@@ -1261,15 +1473,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();
         }
@@ -1320,12 +1561,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);
@@ -1406,7 +1645,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);
@@ -1457,9 +1696,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);
@@ -1498,13 +1738,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");
@@ -1513,4 +1753,46 @@
             throw new IllegalArgumentException("Service type cannot be empty");
         }
     }
+
+    /**
+     * Check if the {@link NsdServiceInfo} is valid for registration.
+     *
+     * The following can be registered:
+     * - A service with an optional host.
+     * - A hostname with addresses.
+     *
+     * Note that:
+     * - When registering a service, the service name, service type and port must be specified. If
+     *   hostname is specified, the host addresses can optionally be specified.
+     * - When registering a host without a service, the addresses must be specified.
+     *
+     * @hide
+     */
+    public static void checkServiceInfoForRegistration(NsdServiceInfo serviceInfo) {
+        Objects.requireNonNull(serviceInfo, "NsdServiceInfo cannot be null");
+        boolean hasServiceName = !TextUtils.isEmpty(serviceInfo.getServiceName());
+        boolean hasServiceType = !TextUtils.isEmpty(serviceInfo.getServiceType());
+        boolean hasHostname = !TextUtils.isEmpty(serviceInfo.getHostname());
+        boolean hasHostAddresses = !CollectionUtils.isEmpty(serviceInfo.getHostAddresses());
+
+        if (serviceInfo.getPort() < 0) {
+            throw new IllegalArgumentException("Invalid port");
+        }
+
+        if (hasServiceType || hasServiceName || (serviceInfo.getPort() > 0)) {
+            if (!(hasServiceType && hasServiceName && (serviceInfo.getPort() > 0))) {
+                throw new IllegalArgumentException(
+                        "The service type, service name or port is missing");
+            }
+        }
+
+        if (!hasServiceType && !hasHostname) {
+            throw new IllegalArgumentException("No service or host specified in NsdServiceInfo");
+        }
+
+        if (!hasServiceType && hasHostname && !hasHostAddresses) {
+            // TODO: b/317946010 - This may be allowed when it supports registering KEY RR.
+            throw new IllegalArgumentException("No host addresses specified in NsdServiceInfo");
+        }
+    }
 }
diff --git a/framework-t/src/android/net/nsd/NsdServiceInfo.java b/framework-t/src/android/net/nsd/NsdServiceInfo.java
index caeecdd..f4cc2ac 100644
--- a/framework-t/src/android/net/nsd/NsdServiceInfo.java
+++ b/framework-t/src/android/net/nsd/NsdServiceInfo.java
@@ -16,6 +16,7 @@
 
 package android.net.nsd;
 
+import android.annotation.FlaggedApi;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.compat.annotation.UnsupportedAppUsage;
@@ -24,6 +25,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 +33,12 @@
 import java.io.UnsupportedEncodingException;
 import java.net.InetAddress;
 import java.nio.charset.StandardCharsets;
+import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 /**
  * A class representing service information for network service discovery
@@ -44,13 +48,20 @@
 
     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;
 
@@ -59,15 +70,41 @@
 
     private int mInterfaceIndex;
 
+    // The timestamp that all 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;
@@ -142,6 +179,43 @@
     }
 
     /**
+     * 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;
+    }
+
+    /**
      * Unpack txt information from a base-64 encoded byte array.
      *
      * @param txtRecordsRawBytes The raw base64 encoded byte array.
@@ -391,14 +465,78 @@
         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: only number of seconds of {@code expirationTime} is used.
+     *
+     * @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));
@@ -414,6 +552,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 +575,8 @@
         for (InetAddress address : mHostAddresses) {
             InetAddressUtils.parcelInetAddress(dest, address, flags);
         }
+        dest.writeString(mHostname);
+        dest.writeLong(mExpirationTime != null ? mExpirationTime.getEpochSecond() : -1);
     }
 
     /** Implement the Parcelable interface */
@@ -445,6 +586,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 +606,9 @@
                 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));
                 return info;
             }
 
diff --git a/framework/Android.bp b/framework/Android.bp
index 1e6262d..52f2c7c 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"],
 }
@@ -105,7 +106,6 @@
     apex_available: [
         "com.android.tethering",
     ],
-    lint: { strict_updatability_linting: true },
 }
 
 java_library {
@@ -134,7 +134,7 @@
         "framework-tethering.impl",
         "framework-wifi.stubs.module_lib",
     ],
-    visibility: ["//packages/modules/Connectivity:__subpackages__"]
+    visibility: ["//packages/modules/Connectivity:__subpackages__"],
 }
 
 java_defaults {
@@ -165,17 +165,16 @@
         "//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/opt/net/ethernet/tests:__subpackages__",
         "//frameworks/opt/telephony/tests/telephonytests",
@@ -185,10 +184,17 @@
         "//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 {
@@ -293,9 +299,9 @@
     ],
     flags: [
         "--show-for-stub-purposes-annotation android.annotation.SystemApi" +
-        "\\(client=android.annotation.SystemApi.Client.PRIVILEGED_APPS\\)",
+            "\\(client=android.annotation.SystemApi.Client.PRIVILEGED_APPS\\)",
         "--show-for-stub-purposes-annotation android.annotation.SystemApi" +
-        "\\(client=android.annotation.SystemApi.Client.MODULE_LIBRARIES\\)",
+            "\\(client=android.annotation.SystemApi.Client.MODULE_LIBRARIES\\)",
     ],
     aidl: {
         include_dirs: [
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/api/current.txt b/framework/api/current.txt
index 6860c3c..ef8415c 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,6 +333,7 @@
     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
@@ -360,6 +362,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 +421,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 +441,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/system-current.txt b/framework/api/system-current.txt
index e812024..bef29a4 100644
--- a/framework/api/system-current.txt
+++ b/framework/api/system-current.txt
@@ -307,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();
@@ -373,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/jni/android_net_NetworkUtils.cpp b/framework/jni/android_net_NetworkUtils.cpp
index 5403be7..3779a00 100644
--- a/framework/jni/android_net_NetworkUtils.cpp
+++ b/framework/jni/android_net_NetworkUtils.cpp
@@ -24,6 +24,7 @@
 #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>
@@ -250,6 +251,14 @@
     }
 }
 
+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();
+}
+
 // ----------------------------------------------------------------------------
 
 /*
@@ -272,6 +281,8 @@
     { "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..2c0b15f 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,63 @@
         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>
+
 </issues>
\ No newline at end of file
diff --git a/framework/src/android/net/BpfNetMapsUtils.java b/framework/src/android/net/BpfNetMapsUtils.java
index 11d610c..0be30bb 100644
--- a/framework/src/android/net/BpfNetMapsUtils.java
+++ b/framework/src/android/net/BpfNetMapsUtils.java
@@ -125,13 +125,11 @@
         return sj.toString();
     }
 
-    public static final boolean PRE_T = !SdkLevel.isAtLeastT();
-
     /**
      * Throw UnsupportedOperationException if SdkLevel is before T.
      */
     public static void throwIfPreT(final String msg) {
-        if (PRE_T) {
+        if (!SdkLevel.isAtLeastT()) {
             throw new UnsupportedOperationException(msg);
         }
     }
diff --git a/framework/src/android/net/ConnectivityManager.java b/framework/src/android/net/ConnectivityManager.java
index fa27d0e..915ec52 100644
--- a/framework/src/android/net/ConnectivityManager.java
+++ b/framework/src/android/net/ConnectivityManager.java
@@ -74,6 +74,7 @@
 import android.util.SparseIntArray;
 
 import com.android.internal.annotations.GuardedBy;
+import com.android.modules.utils.build.SdkLevel;
 
 import libcore.net.event.NetworkEventDispatcher;
 
@@ -6022,6 +6023,13 @@
     /**
      * 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
@@ -6271,9 +6279,13 @@
     // Only the system server process and the network stack have access.
     @FlaggedApi(Flags.SUPPORT_IS_UID_NETWORKING_BLOCKED)
     @SystemApi(client = MODULE_LIBRARIES)
-    @RequiresApi(Build.VERSION_CODES.TIRAMISU)  // BPF maps were only mainlined in T
+    // 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()) {
+            Log.wtf(TAG, "isUidNetworkingBlocked is not supported on pre-U devices");
+        }
         final BpfNetMapsReader reader = BpfNetMapsReader.getInstance();
         // Note that before V, the data saver status in bpf is written by ConnectivityService
         // when receiving {@link #ACTION_RESTRICT_BACKGROUND_CHANGED}. Thus,
diff --git a/framework/src/android/net/NetworkCapabilities.java b/framework/src/android/net/NetworkCapabilities.java
index efae754..84a0d29 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;
@@ -29,9 +30,6 @@
 import android.annotation.SystemApi;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.net.ConnectivityManager.NetworkCallback;
-// Can't be imported because aconfig tooling doesn't exist on udc-mainline-prod yet
-// See inner class Flags which mimics this for the time being
-// import android.net.flags.Flags;
 import android.os.Build;
 import android.os.Parcel;
 import android.os.Parcelable;
@@ -130,6 +128,12 @@
     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";
     }
 
     /**
@@ -716,17 +720,24 @@
     public static final int NET_CAPABILITY_PRIORITIZE_BANDWIDTH = 35;
 
     /**
-     * This is a local network, e.g. a tethering downstream or a P2P direct network.
+     * Indicates that this network is a local network.
      *
-     * <p>
-     * Note that local networks are not sent to callbacks by default. To receive callbacks about
-     * them, the {@link NetworkRequest} instance must be prepared to see them, either by
-     * adding the capability with {@link NetworkRequest.Builder#addCapability}, by removing
-     * this forbidden capability with {@link NetworkRequest.Builder#removeForbiddenCapability},
-     * or by clearing all capabilites with {@link NetworkRequest.Builder#clearCapabilities()}.
-     * </p>
-     * @hide
+     * 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 MAX_NET_CAPABILITY = NET_CAPABILITY_LOCAL_NETWORK;
@@ -1257,6 +1268,7 @@
             TRANSPORT_TEST,
             TRANSPORT_USB,
             TRANSPORT_THREAD,
+            TRANSPORT_SATELLITE,
     })
     public @interface Transport { }
 
@@ -1313,10 +1325,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 {
@@ -1343,6 +1361,7 @@
         "TEST",
         "USB",
         "THREAD",
+        "SATELLITE",
     };
 
     /**
@@ -1751,9 +1770,13 @@
     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
+                && (mTransportTypes & ~(1L << TRANSPORT_TEST))
+                != (1 << TRANSPORT_CELLULAR | 1 << TRANSPORT_SATELLITE)) {
             throw new IllegalStateException("Must have a single non-test transport specified to "
                     + "use setNetworkSpecifier");
         }
@@ -2794,10 +2817,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);
     }
diff --git a/framework/src/android/net/NetworkRequest.java b/framework/src/android/net/NetworkRequest.java
index 653e41d..4de02ac 100644
--- a/framework/src/android/net/NetworkRequest.java
+++ b/framework/src/android/net/NetworkRequest.java
@@ -34,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;
@@ -145,6 +146,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.
@@ -630,10 +637,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;
@@ -890,4 +896,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/NetworkUtils.java b/framework/src/android/net/NetworkUtils.java
index fbdc024..18feb84 100644
--- a/framework/src/android/net/NetworkUtils.java
+++ b/framework/src/android/net/NetworkUtils.java
@@ -438,4 +438,9 @@
     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/nearby/README.md b/nearby/README.md
index 0d26563..8dac61c 100644
--- a/nearby/README.md
+++ b/nearby/README.md
@@ -47,8 +47,17 @@
 ## Build and Install
 
 ```sh
-Build unbundled module using banchan
+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
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 4bb9efd..4be102c 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"],
 }
 
@@ -49,6 +50,7 @@
         "androidx.annotation_annotation",
         "framework-annotations-lib",
         "framework-bluetooth",
+        "framework-location.stubs.module_lib",
     ],
     static_libs: [
         "modules-utils-preconditions",
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/NearbyManager.java b/nearby/framework/java/android/nearby/NearbyManager.java
index 00f1c38..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,9 +26,12 @@
 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;
 
@@ -37,6 +41,7 @@
 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;
@@ -75,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.
@@ -456,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/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/service/Android.bp b/nearby/service/Android.bp
index 4630902..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"],
 }
 
@@ -30,7 +31,7 @@
     srcs: [":nearby-service-srcs"],
 
     defaults: [
-        "framework-system-server-module-defaults"
+        "framework-system-server-module-defaults",
     ],
     libs: [
         "androidx.annotation_annotation",
@@ -42,6 +43,7 @@
     ],
     static_libs: [
         "androidx.core_core",
+        "android.hardware.bluetooth.finder-V1-java",
         "guava",
         "libprotobuf-java-lite",
         "modules-utils-build",
@@ -66,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/NearbyService.java b/nearby/service/java/com/android/server/nearby/NearbyService.java
index 3c183ec..1575f07 100644
--- a/nearby/service/java/com/android/server/nearby/NearbyService.java
+++ b/nearby/service/java/com/android/server/nearby/NearbyService.java
@@ -35,12 +35,14 @@
 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.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;
@@ -50,6 +52,8 @@
 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";
@@ -79,6 +83,7 @@
             };
     private final DiscoveryManager mDiscoveryProviderManager;
     private final BroadcastProviderManager mBroadcastProviderManager;
+    private final BluetoothFinderManager mBluetoothFinderManager;
 
     public NearbyService(Context context) {
         mContext = context;
@@ -90,6 +95,7 @@
                 mNearbyConfiguration.refactorDiscoveryManager()
                         ? new DiscoveryProviderManager(context, mInjector)
                         : new DiscoveryProviderManagerLegacy(context, mInjector);
+        mBluetoothFinderManager = new BluetoothFinderManager();
     }
 
     @VisibleForTesting
@@ -148,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.
      *
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/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/tests/cts/fastpair/Android.bp b/nearby/tests/cts/fastpair/Android.bp
index 66a1ffe..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"],
 }
 
@@ -33,12 +34,14 @@
         "framework-bluetooth.stubs.module_lib",
         "framework-configinfrastructure",
         "framework-connectivity-t.impl",
+        "framework-location.stubs.module_lib",
     ],
     srcs: ["src/**/*.java"],
     test_suites: [
         "cts",
         "general-tests",
         "mts-tethering",
+        "mcts-tethering",
     ],
     certificate: "platform",
     sdk_version: "module_current",
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..832ac03 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,14 @@
 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.content.Context;
+import android.location.LocationManager;
 import android.nearby.BroadcastCallback;
 import android.nearby.BroadcastRequest;
 import android.nearby.NearbyDevice;
@@ -42,6 +44,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,6 +54,7 @@
 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;
@@ -57,6 +62,7 @@
 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;
@@ -189,6 +195,92 @@
         mScanCallback.onError(ERROR_UNSUPPORTED);
     }
 
+    @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 enableBluetooth() {
         BluetoothManager manager = mContext.getSystemService(BluetoothManager.class);
         BluetoothAdapter bluetoothAdapter = manager.getAdapter();
@@ -197,6 +289,13 @@
         }
     }
 
+    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> {
         @Override
         public void accept(OffloadCapability aBoolean) {
diff --git a/nearby/tests/integration/privileged/Android.bp b/nearby/tests/integration/privileged/Android.bp
index 9b6e488..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"],
 }
 
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/untrusted/Android.bp b/nearby/tests/integration/untrusted/Android.bp
index 75f765b..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"],
 }
 
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/unit/Android.bp b/nearby/tests/unit/Android.bp
index 112c751..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"],
 }
 
@@ -43,7 +44,6 @@
         "platform-test-annotations",
         "service-nearby-pre-jarjar",
         "truth",
-        // "Robolectric_all-target",
     ],
     // these are needed for Extended Mockito
     jni_libs: [
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..671b5c5
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/managers/BluetoothFinderManagerTest.java
@@ -0,0 +1,186 @@
+/*
+ * 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.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 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() {
+        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/netbpfload/Android.bp b/netbpfload/Android.bp
index 1f92374..b71890e 100644
--- a/netbpfload/Android.bp
+++ b/netbpfload/Android.bp
@@ -14,6 +14,10 @@
 // limitations under the License.
 //
 
+package {
+    default_team: "trendy_team_fwk_core_networking",
+}
+
 cc_binary {
     name: "netbpfload",
 
@@ -40,7 +44,7 @@
         "com.android.tethering",
         "//apex_available:platform",
     ],
-    // really should be Android 14/U (34), but we cannot include binaries built
+    // 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).
@@ -49,3 +53,15 @@
     init_rc: ["netbpfload.rc"],
     required: ["bpfloader"],
 }
+
+// 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.mainline.rc",
+    src: "netbpfload.mainline.rc",
+    filename: "netbpfload.33rc",
+    installable: false,
+}
diff --git a/netbpfload/NetBpfLoad.cpp b/netbpfload/NetBpfLoad.cpp
index 9df6877..8f25593 100644
--- a/netbpfload/NetBpfLoad.cpp
+++ b/netbpfload/NetBpfLoad.cpp
@@ -169,16 +169,141 @@
     return 0;
 }
 
+#define APEX_MOUNT_POINT "/apex/com.android.tethering"
+const char * const platformBpfLoader = "/system/bin/bpfloader";
+const char * const platformNetBpfLoad = "/system/bin/netbpfload";
+const char * const apexNetBpfLoad = APEX_MOUNT_POINT "/bin/netbpfload";
+
+int logTetheringApexVersion(void) {
+    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;
+    ALOGD("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;
+}
+
 int main(int argc, char** argv, char * const envp[]) {
     (void)argc;
     android::base::InitLogging(argv, &android::base::KernelLogger);
 
-    const int device_api_level = android_get_device_api_level();
-    const bool isAtLeastU = (device_api_level >= __ANDROID_API_U__);
+    ALOGI("NetBpfLoad '%s' starting...", argv[0]);
 
-    if (!android::bpf::isAtLeastKernelVersion(4, 19, 0) &&
+    // true iff we are running from the module
+    const bool is_mainline = !strcmp(argv[0], apexNetBpfLoad);
+
+    // true iff we are running from the platform
+    const bool is_platform = !strcmp(argv[0], platformNetBpfLoad);
+
+    const int device_api_level = android_get_device_api_level();
+    const bool isAtLeastT = (device_api_level >= __ANDROID_API_T__);
+    const bool isAtLeastU = (device_api_level >= __ANDROID_API_U__);
+    const bool isAtLeastV = (device_api_level >= __ANDROID_API_V__);
+
+    // last in U QPR2 beta1
+    const bool has_platform_bpfloader_rc = exists("/system/etc/init/bpfloader.rc");
+    // first in U QPR2 beta~2
+    const bool has_platform_netbpfload_rc = exists("/system/etc/init/netbpfload.rc");
+
+    ALOGI("NetBpfLoad api:%d/%d kver:%07x platform:%d mainline:%d rc:%d%d",
+          android_get_application_target_sdk_version(), device_api_level,
+          android::bpf::kernelVersion(), is_platform, is_mainline,
+          has_platform_bpfloader_rc, has_platform_netbpfload_rc);
+
+    if (!is_platform && !is_mainline) {
+        ALOGE("Unable to determine if we're platform or mainline netbpfload.");
+        return 1;
+    }
+
+    if (is_platform) {
+        ALOGI("Executing apex netbpfload...");
+        const char * args[] = { apexNetBpfLoad, NULL, };
+        execve(args[0], (char**)args, envp);
+        ALOGE("exec '%s' fail: %d[%s]", apexNetBpfLoad, errno, strerror(errno));
+        return 1;
+    }
+
+    if (!has_platform_bpfloader_rc && !has_platform_netbpfload_rc) {
+        ALOGE("Unable to find platform's bpfloader & netbpfload init scripts.");
+        return 1;
+    }
+
+    if (has_platform_bpfloader_rc && has_platform_netbpfload_rc) {
+        ALOGE("Platform has *both* bpfloader & netbpfload init scripts.");
+        return 1;
+    }
+
+    logTetheringApexVersion();
+
+    if (is_mainline && has_platform_bpfloader_rc && !has_platform_netbpfload_rc) {
+        // Tethering apex shipped initrc file causes us to reach here
+        // but we're not ready to correctly handle anything before U QPR2
+        // in which the 'bpfloader' vs 'netbpfload' split happened
+        const char * args[] = { platformBpfLoader, NULL, };
+        execve(args[0], (char**)args, envp);
+        ALOGE("exec '%s' fail: %d[%s]", platformBpfLoader, errno, strerror(errno));
+        return 1;
+    }
+
+    if (isAtLeastT && !android::bpf::isAtLeastKernelVersion(4, 9, 0)) {
+        ALOGW("Android T requires kernel 4.9.");
+    }
+
+    if (isAtLeastU && !android::bpf::isAtLeastKernelVersion(4, 14, 0)) {
+        ALOGW("Android U requires kernel 4.14.");
+    }
+
+    if (isAtLeastV && !android::bpf::isAtLeastKernelVersion(4, 19, 0) &&
         android::bpf::isAtLeastKernelVersion(4, 14, 0)) {
-        ALOGE("Android U QPR2 requires kernel 4.19.");
+        ALOGE("Android V requires kernel 4.19.");
+        return 1;
+    }
+
+    if (isAtLeastV && android::bpf::isX86() && !android::bpf::isKernel64Bit()) {
+        ALOGE("Android V requires X86 kernel to be 64-bit.");
         return 1;
     }
 
@@ -245,6 +370,13 @@
         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(location) != 0) {
@@ -269,10 +401,8 @@
 
     ALOGI("done, transferring control to platform bpfloader.");
 
-    const char * args[] = { "/system/bin/bpfloader", NULL, };
-    if (execve(args[0], (char**)args, envp)) {
-        ALOGE("FATAL: execve('/system/bin/bpfloader'): %d[%s]", errno, strerror(errno));
-    }
-
+    const char * args[] = { platformBpfLoader, NULL, };
+    execve(args[0], (char**)args, envp);
+    ALOGE("FATAL: execve('%s'): %d[%s]", platformBpfLoader, errno, strerror(errno));
     return 1;
 }
diff --git a/netbpfload/netbpfload.mainline.rc b/netbpfload/netbpfload.mainline.rc
new file mode 100644
index 0000000..0ac5de8
--- /dev/null
+++ b/netbpfload/netbpfload.mainline.rc
@@ -0,0 +1,8 @@
+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
+    rlimit memlock 1073741824 1073741824
+    oneshot
+    reboot_on_failure reboot,bpfloader-failed
+    override
diff --git a/netd/Android.bp b/netd/Android.bp
index 4325d89..eedbdae 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,24 @@
 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",
     ],
     static_libs: [
+        "libbase",
         "libnetd_updatable",
     ],
     shared_libs: [
-        "libbase",
         "libcutils",
         "liblog",
         "libnetdutils",
diff --git a/netd/NetdUpdatable.cpp b/netd/NetdUpdatable.cpp
index 8b9e5a7..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__ << ": Failed. " << ret.code() << " " << ret.msg();
-        return -ret.code();
+        LOG(ERROR) << __func__ << ": Failed: (" << ret.code() << ") " << ret.msg();
+        abort();
     }
     return 0;
 }
diff --git a/remoteauth/framework/Android.bp b/remoteauth/framework/Android.bp
index 71b621a..2f1737f 100644
--- a/remoteauth/framework/Android.bp
+++ b/remoteauth/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"],
 }
 
diff --git a/remoteauth/service/Android.bp b/remoteauth/service/Android.bp
index 8330efc..32ae54f 100644
--- a/remoteauth/service/Android.bp
+++ b/remoteauth/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"],
 }
 
diff --git a/remoteauth/service/jni/Android.bp b/remoteauth/service/jni/Android.bp
index a95a8fb..c0ac779 100644
--- a/remoteauth/service/jni/Android.bp
+++ b/remoteauth/service/jni/Android.bp
@@ -1,4 +1,5 @@
 package {
+    default_team: "trendy_team_fwk_core_networking",
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
diff --git a/remoteauth/service/jni/src/remoteauth_jni_android_platform.rs b/remoteauth/service/jni/src/remoteauth_jni_android_platform.rs
index 0a189f2..421fe7e 100644
--- a/remoteauth/service/jni/src/remoteauth_jni_android_platform.rs
+++ b/remoteauth/service/jni/src/remoteauth_jni_android_platform.rs
@@ -67,7 +67,7 @@
         logger::init(
             logger::Config::default()
                 .with_tag_on_device("remoteauth")
-                .with_min_level(log::Level::Trace)
+                .with_max_level(log::LevelFilter::Trace)
                 .with_filter("trace,jni=info"),
         );
     }
diff --git a/remoteauth/service/jni/src/remoteauth_jni_android_protocol.rs b/remoteauth/service/jni/src/remoteauth_jni_android_protocol.rs
index ac2eb8c..e44ab8b 100644
--- a/remoteauth/service/jni/src/remoteauth_jni_android_protocol.rs
+++ b/remoteauth/service/jni/src/remoteauth_jni_android_protocol.rs
@@ -30,7 +30,7 @@
     logger::init(
         logger::Config::default()
             .with_tag_on_device("remoteauth")
-            .with_min_level(log::Level::Trace)
+            .with_max_level(log::LevelFilter::Trace)
             .with_filter("trace,jni=info"),
     );
     get_boolean_result(native_init(env), "native_init")
diff --git a/remoteauth/tests/unit/Android.bp b/remoteauth/tests/unit/Android.bp
index 77e6f19..47b9e31 100644
--- a/remoteauth/tests/unit/Android.bp
+++ b/remoteauth/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"],
 }
 
diff --git a/service-t/Android.bp b/service-t/Android.bp
index bc49f0e..779f354 100644
--- a/service-t/Android.bp
+++ b/service-t/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"],
 }
@@ -31,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.
@@ -73,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.
@@ -94,11 +100,12 @@
     min_sdk_version: "21",
     lint: {
         error_checks: ["NewApi"],
+
     },
     srcs: [
         "src/com/android/server/connectivity/mdns/**/*.java",
         ":framework-connectivity-t-mdns-standalone-build-sources",
-        ":service-mdns-droidstubs"
+        ":service-mdns-droidstubs",
     ],
     exclude_srcs: [
         "src/com/android/server/connectivity/mdns/internal/SocketNetlinkMonitor.java",
@@ -127,7 +134,7 @@
     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",
diff --git a/service-t/jni/com_android_server_net_NetworkStatsService.cpp b/service-t/jni/com_android_server_net_NetworkStatsService.cpp
index bdbb655..c999398 100644
--- a/service-t/jni/com_android_server_net_NetworkStatsService.cpp
+++ b/service-t/jni/com_android_server_net_NetworkStatsService.cpp
@@ -34,77 +34,77 @@
 
 using android::bpf::bpfGetUidStats;
 using android::bpf::bpfGetIfaceStats;
-using android::bpf::bpfGetIfIndexStats;
+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,
-};
-
-static uint64_t getStatsType(StatsValue* 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;
-        default:
-            return UNKNOWN;
-    }
+static void nativeRegisterIface(JNIEnv* env, jclass clazz, jstring iface) {
+    ScopedUtfChars iface8(env, iface);
+    if (iface8.c_str() == nullptr) return;
+    bpfRegisterIface(iface8.c_str());
 }
 
-static jlong nativeGetTotalStat(JNIEnv* env, jclass clazz, jint type) {
+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;
+    }
+
+    // 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(NULL, &stats) == 0) {
-        return getStatsType(&stats, (StatsType) type);
+    if (bpfGetIfaceStats(nullptr, &stats) == 0) {
+        return statsValueToEntry(env, &stats);
     } else {
-        return UNKNOWN;
+        return nullptr;
     }
 }
 
-static jlong nativeGetIfaceStat(JNIEnv* env, jclass clazz, jstring iface, jint type) {
+static jobject nativeGetIfaceStat(JNIEnv* env, jclass clazz, jstring iface) {
     ScopedUtfChars iface8(env, iface);
-    if (iface8.c_str() == NULL) {
-        return UNKNOWN;
+    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 nativeGetIfIndexStat(JNIEnv* env, jclass clazz, jint ifindex, jint type) {
-    StatsValue stats = {};
-    if (bpfGetIfIndexStats(ifindex, &stats) == 0) {
-        return getStatsType(&stats, (StatsType) type);
-    } else {
-        return UNKNOWN;
-    }
-}
-
-static jlong nativeGetUidStat(JNIEnv* env, jclass clazz, jint uid, jint type) {
+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;
     }
 }
 
@@ -113,11 +113,31 @@
 }
 
 static const JNINativeMethod gMethods[] = {
-        {"nativeGetTotalStat", "(I)J", (void*)nativeGetTotalStat},
-        {"nativeGetIfaceStat", "(Ljava/lang/String;I)J", (void*)nativeGetIfaceStat},
-        {"nativeGetIfIndexStat", "(II)J", (void*)nativeGetIfIndexStat},
-        {"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..c620634 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",
@@ -73,6 +77,7 @@
         "-Wthread-safety",
     ],
     static_libs: [
+        "libbase",
         "libgmock",
         "libnetworkstats",
         "libperfetto_client_experimental",
@@ -80,7 +85,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 3101397..d3e331e 100644
--- a/service-t/native/libs/libnetworkstats/BpfNetworkStats.cpp
+++ b/service-t/native/libs/libnetworkstats/BpfNetworkStats.cpp
@@ -40,6 +40,35 @@
 
 using base::Result;
 
+BpfMap<uint32_t, IfaceValue>& getIfaceIndexNameMap() {
+    static BpfMap<uint32_t, IfaceValue> ifaceIndexNameMap(IFACE_INDEX_NAME_MAP_PATH);
+    return ifaceIndexNameMap;
+}
+
+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);
@@ -58,19 +87,19 @@
 
 int bpfGetIfaceStatsInternal(const char* iface, StatsValue* stats,
                              const BpfMapRO<uint32_t, StatsValue>& ifaceStatsMap,
-                             const BpfMapRO<uint32_t, IfaceValue>& ifaceNameMap) {
+                             const IfIndexToNameFunc ifindex2name) {
     *stats = {};
     int64_t unknownIfaceBytesTotal = 0;
     const auto processIfaceStats =
-            [iface, stats, &ifaceNameMap, &unknownIfaceBytesTotal](
+            [iface, stats, ifindex2name, &unknownIfaceBytesTotal](
                     const uint32_t& key,
                     const BpfMapRO<uint32_t, StatsValue>& ifaceStatsMap) -> Result<void> {
-        char ifname[IFNAMSIZ];
-        if (getIfaceNameFromMap(ifaceNameMap, ifaceStatsMap, key, ifname, key,
-                                &unknownIfaceBytesTotal)) {
+        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();
@@ -84,9 +113,7 @@
 }
 
 int bpfGetIfaceStats(const char* iface, StatsValue* 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);
+    return bpfGetIfaceStatsInternal(iface, stats, getIfaceStatsMap(), ifindex2name);
 }
 
 int bpfGetIfIndexStatsInternal(uint32_t ifindex, StatsValue* stats,
@@ -101,14 +128,13 @@
 }
 
 int bpfGetIfIndexStats(int ifindex, StatsValue* stats) {
-    static BpfMapRO<uint32_t, StatsValue> ifaceStatsMap(IFACE_STATS_MAP_PATH);
-    return bpfGetIfIndexStatsInternal(ifindex, stats, ifaceStatsMap);
+    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;
@@ -121,22 +147,22 @@
 
 int parseBpfNetworkStatsDetailInternal(std::vector<stats_line>& lines,
                                        const BpfMapRO<StatsKey, StatsValue>& statsMap,
-                                       const BpfMapRO<uint32_t, IfaceValue>& ifaceMap) {
+                                       const IfIndexToNameFunc ifindex2name) {
     int64_t unknownIfaceBytesTotal = 0;
     const auto processDetailUidStats =
-            [&lines, &unknownIfaceBytesTotal, &ifaceMap](
+            [&lines, &unknownIfaceBytesTotal, &ifindex2name](
                     const StatsKey& key,
                     const BpfMapRO<StatsKey, StatsValue>& statsMap) -> Result<void> {
-        char ifname[IFNAMSIZ];
-        if (getIfaceNameFromMap(ifaceMap, statsMap, key.ifaceIndex, ifname, key,
-                                &unknownIfaceBytesTotal)) {
+        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?)
@@ -166,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);
@@ -196,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;
@@ -213,13 +238,14 @@
 
 int parseBpfNetworkStatsDevInternal(std::vector<stats_line>& lines,
                                     const BpfMapRO<uint32_t, StatsValue>& statsMap,
-                                    const BpfMapRO<uint32_t, IfaceValue>& ifaceMap) {
+                                    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 BpfMapRO<uint32_t, StatsValue>&) {
-        char ifname[IFNAMSIZ];
-        if (getIfaceNameFromMap(ifaceMap, statsMap, key, ifname, key, &unknownIfaceBytesTotal)) {
+        Result<IfaceValue> ifname = ifindex2name(key);
+        if (!ifname.ok()) {
+            maybeLogUnknownIface(key, statsMap, key, &unknownIfaceBytesTotal);
             return Result<void>();
         }
         StatsKey fakeKey = {
@@ -227,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);
@@ -242,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 bcc4550..484c166 100644
--- a/service-t/native/libs/libnetworkstats/BpfNetworkStatsTest.cpp
+++ b/service-t/native/libs/libnetworkstats/BpfNetworkStatsTest.cpp
@@ -77,6 +77,10 @@
     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());
 
@@ -228,7 +232,7 @@
     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());
 }
 
@@ -256,16 +260,15 @@
     EXPECT_RESULT_OK(mFakeIfaceStatsMap.writeValue(ifaceStatsKey, value1, BPF_ANY));
 
     StatsValue result1 = {};
-    ASSERT_EQ(0, bpfGetIfaceStatsInternal(IFACE_NAME1, &result1, mFakeIfaceStatsMap,
-                                          mFakeIfaceIndexNameMap));
+    ASSERT_EQ(0,
+              bpfGetIfaceStatsInternal(IFACE_NAME1, &result1, mFakeIfaceStatsMap, mIfIndex2Name));
     expectStatsEqual(value1, result1);
     StatsValue result2 = {};
-    ASSERT_EQ(0, bpfGetIfaceStatsInternal(IFACE_NAME2, &result2, mFakeIfaceStatsMap,
-                                          mFakeIfaceIndexNameMap));
+    ASSERT_EQ(0,
+              bpfGetIfaceStatsInternal(IFACE_NAME2, &result2, mFakeIfaceStatsMap, mIfIndex2Name));
     expectStatsEqual(value2, result2);
     StatsValue totalResult = {};
-    ASSERT_EQ(0, bpfGetIfaceStatsInternal(NULL, &totalResult, mFakeIfaceStatsMap,
-                                          mFakeIfaceIndexNameMap));
+    ASSERT_EQ(0, bpfGetIfaceStatsInternal(NULL, &totalResult, mFakeIfaceStatsMap, mIfIndex2Name));
     StatsValue totalValue = {
             .rxPackets = TEST_PACKET0 * 2 + TEST_PACKET1,
             .rxBytes = TEST_BYTES0 * 2 + TEST_BYTES1,
@@ -304,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());
 }
 
@@ -324,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());
 }
 
@@ -352,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());
 }
@@ -394,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]);
@@ -439,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]);
@@ -457,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();
 
@@ -465,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.
@@ -490,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]);
@@ -532,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/include/netdbpf/BpfNetworkStats.h b/service-t/native/libs/libnetworkstats/include/netdbpf/BpfNetworkStats.h
index 8058d05..59eb195 100644
--- a/service-t/native/libs/libnetworkstats/include/netdbpf/BpfNetworkStats.h
+++ b/service-t/native/libs/libnetworkstats/include/netdbpf/BpfNetworkStats.h
@@ -55,36 +55,27 @@
 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, StatsValue* stats,
                            const BpfMapRO<uint32_t, StatsValue>& appUidStatsMap);
 // For test only
 int bpfGetIfaceStatsInternal(const char* iface, StatsValue* stats,
                              const BpfMapRO<uint32_t, StatsValue>& ifaceStatsMap,
-                             const BpfMapRO<uint32_t, IfaceValue>& ifaceNameMap);
+                             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 BpfMapRO<StatsKey, StatsValue>& statsMap,
-                                       const BpfMapRO<uint32_t, IfaceValue>& ifaceMap);
+                                       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 BpfMapRO<uint32_t, IfaceValue>& ifaceMap,
-                        const BpfMapRO<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 BpfMapRO<Key, StatsValue>& statsMap,
@@ -112,8 +103,9 @@
 // For test only
 int parseBpfNetworkStatsDevInternal(std::vector<stats_line>& lines,
                                     const BpfMapRO<uint32_t, StatsValue>& statsMap,
-                                    const BpfMapRO<uint32_t, IfaceValue>& ifaceMap);
+                                    const IfIndexToNameFunc ifindex2name);
 
+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);
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/IpSecXfrmController.java b/service-t/src/com/android/server/IpSecXfrmController.java
index c8abd40..3cfbf83 100644
--- a/service-t/src/com/android/server/IpSecXfrmController.java
+++ b/service-t/src/com/android/server/IpSecXfrmController.java
@@ -15,6 +15,7 @@
  */
 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;
@@ -106,7 +107,8 @@
     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);
+            final FileDescriptor fd =
+                    NetlinkUtils.netlinkSocketForProto(NETLINK_XFRM, SOCKET_RECV_BUFSIZE);
             NetlinkUtils.connectToKernel(fd);
             return fd;
         }
diff --git a/service-t/src/com/android/server/NsdService.java b/service-t/src/com/android/server/NsdService.java
index 2640332..cfb1a33 100644
--- a/service-t/src/com/android/server/NsdService.java
+++ b/service-t/src/com/android/server/NsdService.java
@@ -26,6 +26,9 @@
 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;
@@ -33,6 +36,8 @@
 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;
@@ -51,6 +56,8 @@
 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;
@@ -86,6 +93,7 @@
 import com.android.modules.utils.build.SdkLevel;
 import com.android.net.module.util.CollectionUtils;
 import com.android.net.module.util.DeviceConfigUtils;
+import com.android.net.module.util.HandlerUtils;
 import com.android.net.module.util.InetAddressUtils;
 import com.android.net.module.util.PermissionUtils;
 import com.android.net.module.util.SharedLog;
@@ -109,9 +117,13 @@
 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;
@@ -170,6 +182,8 @@
             "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";
@@ -186,11 +200,13 @@
     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 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;
@@ -240,6 +256,8 @@
 
     private final RemoteCallbackList<IOffloadEngine> mOffloadEngines =
             new RemoteCallbackList<>();
+    @NonNull
+    private final MdnsFeatureFlags mMdnsFeatureFlags;
 
     private static class OffloadEngineInfo {
         @NonNull final String mInterfaceName;
@@ -261,15 +279,11 @@
         protected final int mClientRequestId;
         protected final int mTransactionId;
         @NonNull
-        protected final NsdServiceInfo mReqServiceInfo;
-        @NonNull
         protected final String mListenedServiceType;
 
-        MdnsListener(int clientRequestId, int transactionId, @NonNull NsdServiceInfo reqServiceInfo,
-                @NonNull String listenedServiceType) {
+        MdnsListener(int clientRequestId, int transactionId, @NonNull String listenedServiceType) {
             mClientRequestId = clientRequestId;
             mTransactionId = transactionId;
-            mReqServiceInfo = reqServiceInfo;
             mListenedServiceType = listenedServiceType;
         }
 
@@ -312,8 +326,8 @@
     private class DiscoveryListener extends MdnsListener {
 
         DiscoveryListener(int clientRequestId, int transactionId,
-                @NonNull NsdServiceInfo reqServiceInfo, @NonNull String listenServiceType) {
-            super(clientRequestId, transactionId, reqServiceInfo, listenServiceType);
+                @NonNull String listenServiceType) {
+            super(clientRequestId, transactionId, listenServiceType);
         }
 
         @Override
@@ -342,8 +356,8 @@
     private class ResolutionListener extends MdnsListener {
 
         ResolutionListener(int clientRequestId, int transactionId,
-                @NonNull NsdServiceInfo reqServiceInfo, @NonNull String listenServiceType) {
-            super(clientRequestId, transactionId, reqServiceInfo, listenServiceType);
+                @NonNull String listenServiceType) {
+            super(clientRequestId, transactionId, listenServiceType);
         }
 
         @Override
@@ -364,8 +378,8 @@
     private class ServiceInfoListener extends MdnsListener {
 
         ServiceInfoListener(int clientRequestId, int transactionId,
-                @NonNull NsdServiceInfo reqServiceInfo, @NonNull String listenServiceType) {
-            super(clientRequestId, transactionId, reqServiceInfo, listenServiceType);
+                @NonNull String listenServiceType) {
+            super(clientRequestId, transactionId, listenServiceType);
         }
 
         @Override
@@ -536,6 +550,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;
@@ -548,6 +567,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;
@@ -688,18 +712,73 @@
                 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 transactionId;
                 final int clientRequestId = msg.arg2;
-                final ListenerArgs args;
                 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.
@@ -714,55 +793,70 @@
                             break;
                         }
 
-                        final NsdServiceInfo info = args.serviceInfo;
+                        final DiscoveryRequest discoveryRequest = discoveryArgs.discoveryRequest;
                         transactionId = getUniqueId();
-                        final Pair<String, String> typeAndSubtype =
-                                parseTypeAndSubtype(info.getServiceType());
+                        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) {
+                            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(clientRequestId,
-                                    transactionId, info, listenServiceType);
+                                    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());
                             final ClientRequest request = storeDiscoveryManagerRequestMap(
                                     clientRequestId, transactionId, listener, clientInfo,
-                                    info.getNetwork());
-                            clientInfo.onDiscoverServicesStarted(clientRequestId, info, request);
+                                    discoveryRequest.getNetwork());
+                            clientInfo.onDiscoverServicesStarted(
+                                    clientRequestId, discoveryRequest, request);
                             clientInfo.log("Register a DiscoveryListener " + transactionId
                                     + " for service type:" + listenServiceType);
                         } else {
                             maybeStartDaemon();
-                            if (discoverServices(transactionId, info)) {
+                            if (discoverServices(transactionId, discoveryRequest)) {
                                 if (DBG) {
                                     Log.d(TAG, "Discover " + msg.arg2 + " " + transactionId
-                                            + info.getServiceType());
+                                            + discoveryRequest.getServiceType());
                                 }
                                 final ClientRequest request = storeLegacyRequestMap(clientRequestId,
                                         transactionId, clientInfo, msg.what,
                                         mClock.elapsedRealtime());
                                 clientInfo.onDiscoverServicesStarted(
-                                        clientRequestId, info, request);
+                                        clientRequestId, discoveryRequest, request);
                             } else {
                                 stopServiceDiscovery(transactionId);
                                 clientInfo.onDiscoverServicesFailedImmediately(clientRequestId,
@@ -773,7 +867,7 @@
                     }
                     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
@@ -811,7 +905,7 @@
                     }
                     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
@@ -826,38 +920,100 @@
                                     NsdManager.FAILURE_MAX_LIMIT, true /* isLegacy */);
                             break;
                         }
-
-                        transactionId = 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.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;
+                            }
+
+                            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).
+                            final MdnsAdvertisingOptions mdnsAdvertisingOptions =
+                                    MdnsAdvertisingOptions.newBuilder()
+                                            .setIsOnlyUpdate(isUpdateOnly)
+                                            .setTtl(advertisingRequest.getTtl())
+                                            .build();
                             mAdvertiser.addOrUpdateService(transactionId, serviceInfo,
-                                    typeSubtype.second,
-                                    MdnsAdvertisingOptions.newBuilder().build());
+                                    mdnsAdvertisingOptions, clientInfo.mUid);
                             storeAdvertiserRequestMap(clientRequestId, transactionId, clientInfo,
                                     serviceInfo.getNetwork());
                         } else {
                             maybeStartDaemon();
+                            transactionId = getUniqueId();
                             if (registerService(transactionId, serviceInfo)) {
                                 if (DBG) {
                                     Log.d(TAG, "Register " + clientRequestId
@@ -877,7 +1033,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
@@ -920,7 +1076,7 @@
                     }
                     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
@@ -932,7 +1088,7 @@
 
                         final NsdServiceInfo info = args.serviceInfo;
                         transactionId = getUniqueId();
-                        final Pair<String, String> typeSubtype =
+                        final Pair<String, List<String>> typeSubtype =
                                 parseTypeAndSubtype(info.getServiceType());
                         final String serviceType = typeSubtype == null
                                 ? null : typeSubtype.first;
@@ -948,10 +1104,12 @@
 
                             maybeStartMonitoringSockets();
                             final MdnsListener listener = new ResolutionListener(clientRequestId,
-                                    transactionId, info, resolveServiceType);
+                                    transactionId, resolveServiceType);
                             final MdnsSearchOptions options = MdnsSearchOptions.newBuilder()
                                     .setNetwork(info.getNetwork())
-                                    .setIsPassiveMode(true)
+                                    .setQueryMode(mMdnsFeatureFlags.isAggressiveQueryModeEnabled()
+                                            ? AGGRESSIVE_QUERY_MODE
+                                            : PASSIVE_QUERY_MODE)
                                     .setResolveInstanceName(info.getServiceName())
                                     .setRemoveExpiredService(true)
                                     .build();
@@ -982,7 +1140,7 @@
                     }
                     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
@@ -1021,7 +1179,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
@@ -1033,7 +1191,7 @@
 
                         final NsdServiceInfo info = args.serviceInfo;
                         transactionId = getUniqueId();
-                        final Pair<String, String> typeAndSubtype =
+                        final Pair<String, List<String>> typeAndSubtype =
                                 parseTypeAndSubtype(info.getServiceType());
                         final String serviceType = typeAndSubtype == null
                                 ? null : typeAndSubtype.first;
@@ -1046,10 +1204,12 @@
 
                         maybeStartMonitoringSockets();
                         final MdnsListener listener = new ServiceInfoListener(clientRequestId,
-                                transactionId, info, resolveServiceType);
+                                transactionId, resolveServiceType);
                         final MdnsSearchOptions options = MdnsSearchOptions.newBuilder()
                                 .setNetwork(info.getNetwork())
-                                .setIsPassiveMode(true)
+                                .setQueryMode(mMdnsFeatureFlags.isAggressiveQueryModeEnabled()
+                                        ? AGGRESSIVE_QUERY_MODE
+                                        : PASSIVE_QUERY_MODE)
                                 .setResolveInstanceName(info.getServiceName())
                                 .setRemoveExpiredService(true)
                                 .build();
@@ -1064,7 +1224,7 @@
                     }
                     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
@@ -1388,6 +1548,8 @@
                         servInfo,
                         network == null ? INetd.LOCAL_NET_ID : network.netId,
                         serviceInfo.getInterfaceIndex());
+                servInfo.setSubtypes(dedupSubtypeLabels(serviceInfo.getSubtypes()));
+                servInfo.setExpirationTime(serviceInfo.getExpirationTime());
                 return servInfo;
             }
 
@@ -1441,6 +1603,7 @@
                                 Log.e(TAG, "Invalid attribute", e);
                             }
                         }
+                        info.setHostname(getHostname(serviceInfo));
                         final List<InetAddress> addresses = getInetAddresses(serviceInfo);
                         if (addresses.size() != 0) {
                             info.setHostAddresses(addresses);
@@ -1477,6 +1640,7 @@
                             }
                         }
 
+                        info.setHostname(getHostname(serviceInfo));
                         final List<InetAddress> addresses = getInetAddresses(serviceInfo);
                         info.setHostAddresses(addresses);
                         clientInfo.onServiceUpdated(clientRequestId, info, request);
@@ -1523,6 +1687,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:
@@ -1581,34 +1755,51 @@
      * 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.
+     */
+    public static boolean checkHostname(@Nullable String hostname) {
+        if (hostname == null) {
+            return true;
+        }
+        String HOSTNAME_REGEX = "^[a-zA-Z]([a-zA-Z0-9-_]{0,61}[a-zA-Z0-9])?$";
+        return Pattern.compile(HOSTNAME_REGEX).matcher(hostname).matches();
+    }
+
+    /** 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
@@ -1622,7 +1813,8 @@
         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;
 
@@ -1644,7 +1836,7 @@
         am.addOnUidImportanceListener(new UidImportanceListener(handler),
                 mRunningAppActiveImportanceCutoff);
 
-        final MdnsFeatureFlags flags = new MdnsFeatureFlags.Builder()
+        mMdnsFeatureFlags = new MdnsFeatureFlags.Builder()
                 .setIsMdnsOffloadFeatureEnabled(mDeps.isTetheringFeatureNotChickenedOut(
                         mContext, MdnsFeatureFlags.NSD_FORCE_DISABLE_MDNS_OFFLOAD))
                 .setIncludeInetAddressRecordsInProbing(mDeps.isFeatureEnabled(
@@ -1653,15 +1845,25 @@
                         mContext, MdnsFeatureFlags.NSD_EXPIRED_SERVICES_REMOVAL))
                 .setIsLabelCountLimitEnabled(mDeps.isTetheringFeatureNotChickenedOut(
                         mContext, MdnsFeatureFlags.NSD_LIMIT_LABEL_COUNT))
+                .setIsKnownAnswerSuppressionEnabled(mDeps.isFeatureEnabled(
+                        mContext, MdnsFeatureFlags.NSD_KNOWN_ANSWER_SUPPRESSION))
+                .setIsUnicastReplyEnabled(mDeps.isFeatureEnabled(
+                        mContext, MdnsFeatureFlags.NSD_UNICAST_REPLY_ENABLED))
+                .setIsAggressiveQueryModeEnabled(mDeps.isFeatureEnabled(
+                        mContext, MdnsFeatureFlags.NSD_AGGRESSIVE_QUERY_MODE))
+                .setOverrideProvider(flag -> mDeps.isFeatureEnabled(
+                        mContext, FORCE_ENABLE_FLAG_FOR_TEST_PREFIX + flag))
                 .build();
         mMdnsSocketClient =
                 new MdnsMultinetworkSocketClient(handler.getLooper(), mMdnsSocketProvider,
-                        LOGGER.forSubComponent("MdnsMultinetworkSocketClient"), flags);
+                        LOGGER.forSubComponent("MdnsMultinetworkSocketClient"), mMdnsFeatureFlags);
         mMdnsDiscoveryManager = deps.makeMdnsDiscoveryManager(new ExecutorProvider(),
-                mMdnsSocketClient, LOGGER.forSubComponent("MdnsDiscoveryManager"), flags);
+                mMdnsSocketClient, LOGGER.forSubComponent("MdnsDiscoveryManager"),
+                mMdnsFeatureFlags);
         handler.post(() -> mMdnsSocketClient.setCallback(mMdnsDiscoveryManager));
         mAdvertiser = deps.makeMdnsAdvertiser(handler.getLooper(), mMdnsSocketProvider,
-                new AdvertiserCallback(), LOGGER.forSubComponent("MdnsAdvertiser"), flags);
+                new AdvertiserCallback(), LOGGER.forSubComponent("MdnsAdvertiser"),
+                mMdnsFeatureFlags, mContext);
         mClock = deps.makeClock();
     }
 
@@ -1717,13 +1919,6 @@
         }
 
         /**
-         * @see DeviceConfigUtils#isTrunkStableFeatureEnabled
-         */
-        public boolean isTrunkStableFeatureEnabled(String feature) {
-            return DeviceConfigUtils.isTrunkStableFeatureEnabled(feature);
-        }
-
-        /**
          * @see MdnsDiscoveryManager
          */
         public MdnsDiscoveryManager makeMdnsDiscoveryManager(
@@ -1740,8 +1935,8 @@
         public MdnsAdvertiser makeMdnsAdvertiser(
                 @NonNull Looper looper, @NonNull MdnsSocketProvider socketProvider,
                 @NonNull MdnsAdvertiser.AdvertiserCallback cb, @NonNull SharedLog sharedLog,
-                MdnsFeatureFlags featureFlags) {
-            return new MdnsAdvertiser(looper, socketProvider, cb, sharedLog, featureFlags);
+                MdnsFeatureFlags featureFlags, Context context) {
+            return new MdnsAdvertiser(looper, socketProvider, cb, sharedLog, featureFlags, context);
         }
 
         /**
@@ -1927,9 +2122,10 @@
             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);
+            cbInfo.setHostname(registeredInfo.getHostname());
             final ClientRequest request = clientInfo.mClientRequests.get(clientRequestId);
             clientInfo.onRegisterServiceSucceeded(clientRequestId, cbInfo, request);
         }
@@ -2014,33 +2210,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
@@ -2052,8 +2271,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
@@ -2067,13 +2286,13 @@
         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
@@ -2109,25 +2328,24 @@
                 throw new SecurityException("API is not available in before API level 33");
             }
 
-            // REGISTER_NSD_OFFLOAD_ENGINE was only added to the SDK in V.
-            if (SdkLevel.isAtLeastV() && PermissionUtils.checkAnyPermissionOf(context,
-                    REGISTER_NSD_OFFLOAD_ENGINE)) {
-                return;
+            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);
             }
 
-            // REGISTER_NSD_OFFLOAD_ENGINE cannot be backport to U. In U, check the DEVICE_POWER
-            // permission instead.
-            if (!SdkLevel.isAtLeastV() && SdkLevel.isAtLeastU()
-                    && PermissionUtils.checkAnyPermissionOf(context, DEVICE_POWER)) {
-                return;
-            }
-            if (PermissionUtils.checkAnyPermissionOf(context, NETWORK_STACK,
-                    PERMISSION_MAINLINE_NETWORK_STACK, NETWORK_SETTINGS)) {
+            if (PermissionUtils.hasAnyPermissionOf(context,
+                    permissionsList.toArray(new String[0]))) {
                 return;
             }
             throw new SecurityException("Requires one of the following permissions: "
-                    + String.join(", ", List.of(REGISTER_NSD_OFFLOAD_ENGINE, NETWORK_STACK,
-                    PERMISSION_MAINLINE_NETWORK_STACK, NETWORK_SETTINGS)) + ".");
+                    + String.join(", ", permissionsList) + ".");
         }
     }
 
@@ -2145,6 +2363,11 @@
     }
 
     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: " + transactionId + " " + service);
         }
@@ -2162,13 +2385,22 @@
     }
 
     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 transactionId, 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;
         }
@@ -2176,10 +2408,18 @@
     }
 
     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 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);
@@ -2210,7 +2450,26 @@
             }
             return IFACE_IDX_ANY;
         }
+        return getNetworkInterfaceIndex(network);
+    }
 
+    /**
+     * 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;
+        }
+        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;
@@ -2253,20 +2512,32 @@
     }
 
     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 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 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
@@ -2278,6 +2549,14 @@
         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 {
@@ -2384,7 +2663,15 @@
     /* 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;
@@ -2431,6 +2718,10 @@
             return sb.toString();
         }
 
+        public int getUid() {
+            return mUid;
+        }
+
         private boolean isPreSClient() {
             return mIsPreSClient;
         }
@@ -2573,12 +2864,12 @@
                     && !(request instanceof AdvertiserClientRequest);
         }
 
-        void onDiscoverServicesStarted(int listenerKey, NsdServiceInfo info,
+        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);
             }
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 fc0e11b..c162bcc 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java
@@ -17,11 +17,14 @@
 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;
@@ -30,13 +33,16 @@
 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;
@@ -44,6 +50,7 @@
 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;
@@ -83,6 +90,7 @@
     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.
@@ -146,7 +154,9 @@
                 mSharedLog.wtf("Register succeeded for unknown registration");
                 return;
             }
-            if (mMdnsFeatureFlags.mIsMdnsOffloadFeatureEnabled) {
+            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,
@@ -174,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;
@@ -200,10 +213,22 @@
                 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
@@ -226,23 +251,54 @@
         }
     };
 
-    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) {
@@ -321,17 +377,34 @@
 
         /**
          * 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 (MdnsUtils.equalsIgnoreDnsCase(info.getServiceName(), other.getServiceName())
                         && MdnsUtils.equalsIgnoreDnsCase(info.getServiceType(),
@@ -343,16 +416,41 @@
         }
 
         /**
+         * Get the ID of a conflicting registration due to host, or -1 if none.
+         *
+         * <p>It's valid that multiple registrations from the same user are using the same hostname.
+         *
+         * <p>If there's already another registration with the same hostname requested by another
+         * user, this is considered a conflict.
+         */
+        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();
+                if (clientUid != otherRegistration.mClientUid
+                        && MdnsUtils.equalsIgnoreDnsCase(
+                                info.getHostname(), otherInfo.getHostname())) {
+                    return mPendingRegistrations.keyAt(i);
+                }
+            }
+            return -1;
+        }
+
+        /**
          * Add a service to advertise.
          *
-         * Conflicts must be checked via {@link #getConflictingService} before attempting to add.
+         * <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());
+                            registration.getAdvertisingOptions());
                 } catch (NameConflictException e) {
                     mSharedLog.wtf("Name conflict adding services that should have unique names",
                             e);
@@ -367,7 +465,8 @@
         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.getSubtype());
+                mAdvertisers.valueAt(i).updateService(
+                        id, registration.getServiceInfo().getSubtypes());
             }
         }
 
@@ -417,7 +516,7 @@
                 final Registration registration = mPendingRegistrations.valueAt(i);
                 try {
                     advertiser.addService(mPendingRegistrations.keyAt(i),
-                            registration.getServiceInfo(), registration.getSubtype());
+                            registration.getServiceInfo(), registration.getAdvertisingOptions());
                 } catch (NameConflictException e) {
                     mSharedLog.wtf("Name conflict adding services that should have unique names",
                             e);
@@ -479,38 +578,45 @@
     }
 
     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 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());
         }
 
         /**
-         * Matches between the NsdServiceInfo in the Registration and the provided argument.
+         * Update subTypes for the registration.
          */
-        public boolean matches(@Nullable NsdServiceInfo newInfo) {
-            return Objects.equals(newInfo.getServiceName(), mOriginalName) && Objects.equals(
-                    newInfo.getServiceType(), mServiceInfo.getServiceType()) && Objects.equals(
-                    newInfo.getNetwork(), mServiceInfo.getNetwork());
-        }
-
-        /**
-         * Update subType for the registration.
-         */
-        public void updateSubtype(@Nullable String subtype) {
-            this.mSubtype = subtype;
+        public void updateSubtypes(@NonNull Set<String> subtypes) {
+            mServiceInfo = new NsdServiceInfo(mServiceInfo);
+            mServiceInfo.setSubtypes(subtypes);
         }
 
         /**
@@ -519,8 +625,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;
         }
 
@@ -536,39 +653,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;
         }
     }
 
@@ -637,14 +772,16 @@
 
     public MdnsAdvertiser(@NonNull Looper looper, @NonNull MdnsSocketProvider socketProvider,
             @NonNull AdvertiserCallback cb, @NonNull SharedLog sharedLog,
-            @NonNull MdnsFeatureFlags mDnsFeatureFlags) {
-        this(looper, socketProvider, cb, new Dependencies(), sharedLog, mDnsFeatureFlags);
+            @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 MdnsFeatureFlags mDnsFeatureFlags) {
+            @NonNull SharedLog sharedLog, @NonNull MdnsFeatureFlags mDnsFeatureFlags,
+            @NonNull Context context) {
         mLooper = looper;
         mCb = cb;
         mSocketProvider = socketProvider;
@@ -652,6 +789,31 @@
         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() {
@@ -665,14 +827,15 @@
      *
      * @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 addOrUpdateService(int id, NsdServiceInfo service, @Nullable String subtype,
-            MdnsAdvertisingOptions advertisingOptions) {
+    public void addOrUpdateService(int id, NsdServiceInfo service,
+            MdnsAdvertisingOptions advertisingOptions, int clientUid) {
         checkThread();
         final Registration existingRegistration = mRegistrations.get(id);
         final Network network = service.getNetwork();
+        final Set<String> subtypes = service.getSubtypes();
         Registration registration;
         if (advertisingOptions.isOnlyUpdate()) {
             if (existingRegistration == null) {
@@ -680,17 +843,17 @@
                 mCb.onRegisterServiceFailed(id, NsdManager.FAILURE_INTERNAL_ERROR);
                 return;
             }
-            if (!(existingRegistration.matches(service))) {
+            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;
 
             }
-            mSharedLog.i("Update service " + service + " with ID " + id + " and subtype " + subtype
-                    + " advertisingOptions " + advertisingOptions);
+            mSharedLog.i("Update service " + service + " with ID " + id + " and subtypes "
+                    + subtypes + " advertisingOptions " + advertisingOptions);
             registration = existingRegistration;
-            registration.updateSubtype(subtype);
+            registration.updateSubtypes(subtypes);
         } else {
             if (existingRegistration != null) {
                 mSharedLog.e("Adding duplicate registration for " + service);
@@ -698,9 +861,9 @@
                 mCb.onRegisterServiceFailed(id, NsdManager.FAILURE_INTERNAL_ERROR);
                 return;
             }
-            mSharedLog.i("Adding service " + service + " with ID " + id + " and subtype " + subtype
-                    + " advertisingOptions " + advertisingOptions);
-            registration = new Registration(service, subtype);
+            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
@@ -793,21 +956,17 @@
     private OffloadServiceInfoWrapper createOffloadService(int serviceId,
             @NonNull Registration registration, byte[] rawOffloadPacket) {
         final NsdServiceInfo nsdServiceInfo = registration.getServiceInfo();
-        final List<String> subTypes = new ArrayList<>();
-        String subType = registration.getSubtype();
-        if (subType != null) {
-            subTypes.add(subType);
-        }
+        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()),
-                subTypes,
+                new ArrayList<>(nsdServiceInfo.getSubtypes()),
                 String.join(".", mDeviceHostName),
                 rawOffloadPacket,
-                // TODO: define overlayable resources in
-                // ServiceConnectivityResources that set the priority based on
-                // service type.
-                0 /* priority */,
+                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
index e7a6ca7..a81d1e4 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertisingOptions.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertisingOptions.java
@@ -16,6 +16,11 @@
 
 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.
  *
@@ -27,13 +32,15 @@
 
     private static MdnsAdvertisingOptions sDefaultOptions;
     private final boolean mIsOnlyUpdate;
+    @Nullable
+    private final Duration mTtl;
 
     /**
      * Parcelable constructs for a {@link MdnsAdvertisingOptions}.
      */
-    MdnsAdvertisingOptions(
-            boolean isOnlyUpdate) {
+    MdnsAdvertisingOptions(boolean isOnlyUpdate, @Nullable Duration ttl) {
         this.mIsOnlyUpdate = isOnlyUpdate;
+        this.mTtl = ttl;
     }
 
     /**
@@ -60,9 +67,36 @@
         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 + '}';
+        return "MdnsAdvertisingOptions{" + "mIsOnlyUpdate=" + mIsOnlyUpdate + ", mTtl=" + mTtl
+                + '}';
     }
 
     /**
@@ -70,6 +104,8 @@
      */
     public static final class Builder {
         private boolean mIsOnlyUpdate = false;
+        @Nullable
+        private Duration mTtl;
 
         private Builder() {
         }
@@ -83,10 +119,18 @@
         }
 
         /**
+         * 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);
+            return new MdnsAdvertisingOptions(mIsOnlyUpdate, mTtl);
         }
     }
 }
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 766f999..21b7069 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.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
@@ -50,11 +53,13 @@
     @NonNull private final SharedLog sharedLog;
 
     @NonNull private final PerSocketServiceTypeClients perSocketServiceTypeClients;
-    @NonNull private final Handler handler;
-    @Nullable private final HandlerThread handlerThread;
-    @NonNull private final MdnsServiceCache serviceCache;
+    @NonNull private final DiscoveryExecutor discoveryExecutor;
     @NonNull private final MdnsFeatureFlags mdnsFeatureFlags;
 
+    // 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<>();
@@ -125,33 +130,82 @@
         this.sharedLog = sharedLog;
         this.perSocketServiceTypeClients = new PerSocketServiceTypeClients();
         this.mdnsFeatureFlags = mdnsFeatureFlags;
-        if (socketClient.getLooper() != null) {
-            this.handlerThread = null;
-            this.handler = new Handler(socketClient.getLooper());
-            this.serviceCache = new MdnsServiceCache(socketClient.getLooper(), mdnsFeatureFlags);
-        } else {
-            this.handlerThread = new HandlerThread(MdnsDiscoveryManager.class.getSimpleName());
-            this.handlerThread.start();
-            this.handler = new Handler(handlerThread.getLooper());
-            this.serviceCache = new MdnsServiceCache(handlerThread.getLooper(), 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();
     }
 
     /**
@@ -169,7 +223,7 @@
             @NonNull MdnsServiceBrowserListener listener,
             @NonNull MdnsSearchOptions searchOptions) {
         sharedLog.i("Registering listener for serviceType: " + serviceType);
-        checkAndRunOnHandlerThread(() ->
+        discoveryExecutor.checkAndRunOnHandlerThread(() ->
                 handleRegisterListener(serviceType, listener, searchOptions));
     }
 
@@ -191,7 +245,7 @@
                 new MdnsSocketClientBase.SocketCreationCallback() {
                     @Override
                     public void onSocketCreated(@NonNull SocketKey socketKey) {
-                        ensureRunningOnHandlerThread(handler);
+                        discoveryExecutor.ensureRunningOnHandlerThread();
                         // All listeners of the same service types shares the same
                         // MdnsServiceTypeClient.
                         MdnsServiceTypeClient serviceTypeClient =
@@ -206,7 +260,7 @@
 
                     @Override
                     public void onSocketDestroyed(@NonNull SocketKey socketKey) {
-                        ensureRunningOnHandlerThread(handler);
+                        discoveryExecutor.ensureRunningOnHandlerThread();
                         final MdnsServiceTypeClient serviceTypeClient =
                                 perSocketServiceTypeClients.get(serviceType, socketKey);
                         if (serviceTypeClient == null) return;
@@ -229,7 +283,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(
@@ -260,7 +315,7 @@
 
     @Override
     public void onResponseReceived(@NonNull MdnsPacket packet, @NonNull SocketKey socketKey) {
-        checkAndRunOnHandlerThread(() ->
+        discoveryExecutor.checkAndRunOnHandlerThread(() ->
                 handleOnResponseReceived(packet, socketKey));
     }
 
@@ -282,7 +337,7 @@
     @Override
     public void onFailedToParseMdnsResponse(int receivedPacketNumber, int errorCode,
             @NonNull SocketKey socketKey) {
-        checkAndRunOnHandlerThread(() ->
+        discoveryExecutor.checkAndRunOnHandlerThread(() ->
                 handleOnFailedToParseMdnsResponse(receivedPacketNumber, errorCode, socketKey));
     }
 
@@ -296,12 +351,31 @@
     @VisibleForTesting
     MdnsServiceTypeClient createServiceTypeClient(@NonNull String serviceType,
             @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(), socketKey,
-                sharedLog.forSubComponent(tag), handler.getLooper(), serviceCache);
+                sharedLog.forSubComponent(tag), looper, serviceCache);
+    }
+
+    /**
+     * 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
index 0a6d8c1..56202fd 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsFeatureFlags.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsFeatureFlags.java
@@ -15,6 +15,9 @@
  */
 package com.android.server.connectivity.mdns;
 
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
 /**
  * The class that contains mDNS feature flags;
  */
@@ -41,6 +44,24 @@
      */
     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";
+
     // Flag for offload feature
     public final boolean mIsMdnsOffloadFeatureEnabled;
 
@@ -53,17 +74,76 @@
     // 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;
+
+    @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);
+    }
+
     /**
      * The constructor for {@link MdnsFeatureFlags}.
      */
     public MdnsFeatureFlags(boolean isOffloadFeatureEnabled,
             boolean includeInetAddressRecordsInProbing,
             boolean isExpiredServicesRemovalEnabled,
-            boolean isLabelCountLimitEnabled) {
+            boolean isLabelCountLimitEnabled,
+            boolean isKnownAnswerSuppressionEnabled,
+            boolean isUnicastReplyEnabled,
+            boolean isAggressiveQueryModeEnabled,
+            @Nullable FlagOverrideProvider overrideProvider) {
         mIsMdnsOffloadFeatureEnabled = isOffloadFeatureEnabled;
         mIncludeInetAddressRecordsInProbing = includeInetAddressRecordsInProbing;
         mIsExpiredServicesRemovalEnabled = isExpiredServicesRemovalEnabled;
         mIsLabelCountLimitEnabled = isLabelCountLimitEnabled;
+        mIsKnownAnswerSuppressionEnabled = isKnownAnswerSuppressionEnabled;
+        mIsUnicastReplyEnabled = isUnicastReplyEnabled;
+        mIsAggressiveQueryModeEnabled = isAggressiveQueryModeEnabled;
+        mOverrideProvider = overrideProvider;
     }
 
 
@@ -79,6 +159,10 @@
         private boolean mIncludeInetAddressRecordsInProbing;
         private boolean mIsExpiredServicesRemovalEnabled;
         private boolean mIsLabelCountLimitEnabled;
+        private boolean mIsKnownAnswerSuppressionEnabled;
+        private boolean mIsUnicastReplyEnabled;
+        private boolean mIsAggressiveQueryModeEnabled;
+        private FlagOverrideProvider mOverrideProvider;
 
         /**
          * The constructor for {@link Builder}.
@@ -88,6 +172,10 @@
             mIncludeInetAddressRecordsInProbing = false;
             mIsExpiredServicesRemovalEnabled = false;
             mIsLabelCountLimitEnabled = true; // Default enabled.
+            mIsKnownAnswerSuppressionEnabled = false;
+            mIsUnicastReplyEnabled = true;
+            mIsAggressiveQueryModeEnabled = false;
+            mOverrideProvider = null;
         }
 
         /**
@@ -132,13 +220,58 @@
         }
 
         /**
+         * 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;
+        }
+
+        /**
          * Builds a {@link MdnsFeatureFlags} with the arguments supplied to this builder.
          */
         public MdnsFeatureFlags build() {
             return new MdnsFeatureFlags(mIsMdnsOffloadFeatureEnabled,
                     mIncludeInetAddressRecordsInProbing,
                     mIsExpiredServicesRemovalEnabled,
-                    mIsLabelCountLimitEnabled);
+                    mIsLabelCountLimitEnabled,
+                    mIsKnownAnswerSuppressionEnabled,
+                    mIsUnicastReplyEnabled,
+                    mIsAggressiveQueryModeEnabled,
+                    mOverrideProvider);
         }
     }
 }
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java b/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java
index 463df63..c2363c0 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java
@@ -22,10 +22,12 @@
 import android.annotation.Nullable;
 import android.annotation.RequiresApi;
 import android.net.LinkAddress;
+import android.net.nsd.NsdManager;
 import android.net.nsd.NsdServiceInfo;
 import android.os.Build;
 import android.os.Handler;
 import android.os.Looper;
+import android.util.ArraySet;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.net.module.util.HexDump;
@@ -37,12 +39,17 @@
 import java.io.IOException;
 import java.net.InetSocketAddress;
 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;
@@ -84,10 +91,15 @@
         /**
          * 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.
@@ -161,10 +173,11 @@
         @NonNull
         public MdnsReplySender makeReplySender(@NonNull String interfaceTag, @NonNull Looper looper,
                 @NonNull MdnsInterfaceSocket socket, @NonNull byte[] packetCreationBuffer,
-                @NonNull SharedLog sharedLog) {
+                @NonNull SharedLog sharedLog, @NonNull MdnsFeatureFlags mdnsFeatureFlags) {
             return new MdnsReplySender(looper, socket, packetCreationBuffer,
                     sharedLog.forSubComponent(
-                            MdnsReplySender.class.getSimpleName() + "/" + interfaceTag), DBG);
+                            MdnsReplySender.class.getSimpleName() + "/" + interfaceTag), DBG,
+                    mdnsFeatureFlags);
         }
 
         /** @see MdnsAnnouncer */
@@ -207,7 +220,7 @@
         mCb = cb;
         mCbHandler = new Handler(looper);
         mReplySender = deps.makeReplySender(sharedLog.getTag(), looper, socket,
-                packetCreationBuffer, sharedLog);
+                packetCreationBuffer, sharedLog, mdnsFeatureFlags);
         mPacketCreationBuffer = packetCreationBuffer;
         mAnnouncer = deps.makeMdnsAnnouncer(sharedLog.getTag(), looper, mReplySender,
                 mAnnouncingCallback, sharedLog);
@@ -232,12 +245,12 @@
      * Update an already registered service without sending exit/re-announcement packet.
      *
      * @param id An exiting service id
-     * @param subtype A new subtype
+     * @param subtypes New subtypes
      */
-    public void updateService(int id, @Nullable String subtype) {
+    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, subtype);
+        mRecordRepository.updateService(id, subtypes);
     }
 
     /**
@@ -245,9 +258,10 @@
      *
      * @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) {
@@ -335,6 +349,7 @@
         final MdnsProber.ProbingInfo probingInfo = mRecordRepository.setServiceProbing(serviceId);
         if (probingInfo == null) return false;
 
+        mAnnouncer.stop(serviceId);
         mProber.restartForConflict(probingInfo);
         return true;
     }
@@ -373,23 +388,33 @@
             }
             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) {
             mSharedLog.v("Parsed packet with " + packet.questions.size() + " questions, "
                     + packet.answers.size() + " answers, "
                     + packet.authorityRecords.size() + " authority, "
-                    + packet.additionalRecords.size() + " additional from " + src);
+                    + 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 MdnsReplyInfo answers = mRecordRepository.getReply(packet, src);
+        final MdnsReplyInfo answers = mRecordRepository.getReply(packet, srcCopy);
 
         if (answers == null) return;
         mReplySender.queueReply(answers);
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 e7b0eaa..869ac9b 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClient.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClient.java
@@ -96,7 +96,8 @@
         @Override
         public void onInterfaceDestroyed(@NonNull SocketKey socketKey,
                 @NonNull MdnsInterfaceSocket socket) {
-            notifySocketDestroyed(socketKey);
+            mActiveSockets.remove(socketKey);
+            mSocketCreationCallback.onSocketDestroyed(socketKey);
             maybeCleanupPacketHandler(socketKey);
         }
 
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 28bd1b4..4b43989 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsRecord.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsRecord.java
@@ -176,6 +176,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
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 48ece68..ac64c3a 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java
@@ -19,6 +19,8 @@
 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;
@@ -28,6 +30,8 @@
 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;
 
@@ -41,11 +45,13 @@
 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;
@@ -53,6 +59,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}.
@@ -68,14 +76,12 @@
     // 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 =
@@ -92,6 +98,7 @@
     private final Looper mLooper;
     @NonNull
     private final String[] mDeviceHostname;
+    @NonNull
     private final MdnsFeatureFlags mMdnsFeatureFlags;
 
     public MdnsRecordRepository(@NonNull Looper looper, @NonNull String[] deviceHostname,
@@ -141,6 +148,9 @@
          * Last time (as per SystemClock.elapsedRealtime) when sent via unicast or multicast,
          * 0 if never
          */
+        // FIXME: the `lastSentTimeMs` and `lastAdvertisedTimeMs` should be maintained separately
+        // for IPv4 and IPv6, because neither IPv4 nor and IPv6 clients can receive replies in
+        // different address space.
         public long lastSentTimeMs;
 
         RecordInfo(NsdServiceInfo serviceInfo, T record, boolean sharedName) {
@@ -155,14 +165,14 @@
         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;
         @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.
@@ -184,91 +194,132 @@
          */
         private boolean isProbing;
 
-        /**
-         * Create a ServiceRegistration with only update the subType
-         */
-        ServiceRegistration withSubtype(String newSubType) {
-            return new ServiceRegistration(srvRecord.record.getServiceHost(), serviceInfo,
-                    newSubType, repliedServiceCount, sentPacketCount, exiting, 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,
-                @Nullable String subtype, int repliedServiceCount, int sentPacketCount,
-                boolean exiting, boolean isProbing) {
+                int repliedServiceCount, int sentPacketCount, boolean exiting, boolean isProbing,
+                @Nullable Duration ttl) {
             this.serviceInfo = serviceInfo;
-            this.subtype = subtype;
 
-            final String[] serviceType = splitServiceType(serviceInfo);
-            final String[] serviceName = splitFullyQualifiedName(serviceInfo, serviceType);
+            final long nonNameRecordsTtlMillis;
+            final long nameRecordsTtlMillis;
 
-            // 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 */);
-
-            if (subtype == null) {
-                this.ptrRecords = Collections.singletonList(ptrRecord);
+            // 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 {
-                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 */);
-
-                this.ptrRecords = List.of(ptrRecord, subtypeRecord);
+                nonNameRecordsTtlMillis = DEFAULT_NON_NAME_RECORDS_TTL_MILLIS;
+                nameRecordsTtlMillis = DEFAULT_NAME_RECORDS_TTL_MILLIS;
             }
 
-            srvRecord = new RecordInfo<>(
-                    serviceInfo,
-                    new MdnsServiceRecord(serviceName,
-                            0L /* receiptTimeMillis */,
-                            true /* cacheFlush */,
-                            NAME_RECORDS_TTL_MILLIS, 0 /* servicePriority */, 0 /* serviceWeight */,
-                            serviceInfo.getPort(),
-                            deviceHostname),
-                    false /* sharedName */);
-
-            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 */);
-
+            final boolean hasService = !TextUtils.isEmpty(serviceInfo.getServiceType());
+            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);
-            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 */));
+
+            if (hasService) {
+                final String[] serviceType = splitServiceType(serviceInfo);
+                final String[] serviceName = splitFullyQualifiedName(serviceInfo, serviceType);
+                // 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();
+            }
 
             this.allRecords = Collections.unmodifiableList(allRecords);
             this.repliedServiceCount = repliedServiceCount;
@@ -284,9 +335,9 @@
          * @param serviceInfo Service to advertise
          */
         ServiceRegistration(@NonNull String[] deviceHostname, @NonNull NsdServiceInfo serviceInfo,
-                @Nullable String subtype, int repliedServiceCount, int sentPacketCount) {
-            this(deviceHostname, serviceInfo, subtype, repliedServiceCount, sentPacketCount,
-                    false /* exiting */, true /* isProbing */);
+                int repliedServiceCount, int sentPacketCount, @Nullable Duration ttl) {
+            this(deviceHostname, serviceInfo,repliedServiceCount, sentPacketCount,
+                    false /* exiting */, true /* isProbing */, ttl);
         }
 
         void setProbing(boolean probing) {
@@ -308,7 +359,7 @@
                             revDnsAddr,
                             0L /* receiptTimeMillis */,
                             true /* cacheFlush */,
-                            NAME_RECORDS_TTL_MILLIS,
+                            DEFAULT_NAME_RECORDS_TTL_MILLIS,
                             mDeviceHostname),
                     false /* sharedName */));
 
@@ -318,7 +369,7 @@
                             mDeviceHostname,
                             0L /* receiptTimeMillis */,
                             true /* cacheFlush */,
-                            NAME_RECORDS_TTL_MILLIS,
+                            DEFAULT_NAME_RECORDS_TTL_MILLIS,
                             addr.getAddress()),
                     false /* sharedName */));
         }
@@ -328,17 +379,16 @@
      * Update a service that already registered in the repository.
      *
      * @param serviceId An existing service ID.
-     * @param subtype A new subtype
-     * @return
+     * @param subtypes New subtypes
      */
-    public void updateService(int serviceId, @Nullable String subtype) {
+    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.withSubtype(
-                subtype);
+        final ServiceRegistration updatedRegistration = existingRegistration.withSubtypes(
+                subtypes);
         mServices.put(serviceId, updatedRegistration);
     }
 
@@ -348,26 +398,28 @@
      * 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, NO_PACKET /* repliedServiceCount */,
-                NO_PACKET /* sentPacketCount */);
+                mDeviceHostname, serviceInfo, NO_PACKET /* repliedServiceCount */,
+                NO_PACKET /* sentPacketCount */, ttl);
         mServices.put(serviceId, registration);
 
         // Remove existing exiting service
@@ -376,34 +428,41 @@
     }
 
     /**
-     * @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 (MdnsUtils.equalsIgnoreDnsCase(serviceName,
-                    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,
-            @NonNull List<MdnsInetAddressRecord> inetAddressRecords) {
+    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 : inetAddressRecords) {
+        for (MdnsInetAddressRecord inetAddressRecord :
+                makeProbingInetAddressRecords(registration.serviceInfo)) {
             probingRecords.add(new MdnsInetAddressRecord(inetAddressRecord.getName(),
                     0L /* receiptTimeMillis */,
                     false /* cacheFlush */,
@@ -500,6 +559,16 @@
         return ret;
     }
 
+    private boolean isTruncatedKnownAnswerPacket(MdnsPacket packet) {
+        if (!mMdnsFeatureFlags.isKnownAnswerSuppressionEnabled()
+                // Should ignore the response packet.
+                || (packet.flags & MdnsConstants.FLAGS_RESPONSE) != 0) {
+            return false;
+        }
+        // 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;
+    }
+
     /**
      * Get the reply to send to an incoming packet.
      *
@@ -509,30 +578,83 @@
     @Nullable
     public MdnsReplyInfo 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<>();
+
+        // 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())) {
+                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 || registration.isProbing) continue;
                 if (addReplyFromService(question, registration.allRecords, registration.ptrRecords,
-                        registration.srvRecord, registration.txtRecord, replyUnicast, now,
-                        answerInfo, additionalAnswerRecords)) {
+                        registration.srvRecord, registration.txtRecord,
+                        registration.serviceInfo.getHostname(),
+                        replyUnicastEnabled, now,
+                        answerInfo, additionalAnswerInfo, packet.answers)) {
+                    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
@@ -556,6 +678,12 @@
         // 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_SOCKET_ADDR;
@@ -571,10 +699,23 @@
             if (!replyUnicast) {
                 info.lastAdvertisedTimeMs = 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 MdnsReplyInfo(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;
     }
 
     /**
@@ -585,14 +726,16 @@
             @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 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) {
 
              /* RFC6762 6.: the record name must match the question name, the record rrtype
@@ -615,19 +758,32 @@
             }
 
             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
+            if (!(replyUnicastEnabled && question.isUnicastReplyRequested())
+                    && info.lastAdvertisedTimeMs > 0L
                     && now - info.lastAdvertisedTimeMs < 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);
         }
 
@@ -636,21 +792,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 false;
+        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) {
@@ -663,21 +820,8 @@
 
         // 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;
     }
 
@@ -694,7 +838,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
@@ -710,6 +854,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) {
@@ -727,6 +877,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.
@@ -741,10 +912,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);
@@ -763,29 +934,46 @@
             MdnsProber.ProbingInfo probeSuccessInfo)
             throws IOException {
 
-        final ServiceRegistration registration = mServices.get(probeSuccessInfo.getServiceId());
-        if (registration == null) throw new IOException(
-                "Service is not registered: " + probeSuccessInfo.getServiceId());
+        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<>();
+        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(
+                probeSuccessInfo.getServiceId(), new ArrayList<>(answersSet), additionalAnswers);
     }
 
     /**
@@ -804,8 +992,13 @@
         for (RecordInfo<MdnsPointerRecord> ptrRecord : registration.ptrRecords) {
             answers.add(ptrRecord.record);
         }
-        answers.add(registration.srvRecord.record);
-        answers.add(registration.txtRecord.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);
@@ -820,70 +1013,181 @@
                 Collections.emptyList() /* additionalRecords */);
     }
 
+    /** Check if the record is in any service registration */
+    private boolean hasInetAddressRecord(@NonNull MdnsInetAddressRecord record) {
+        for (int i = 0; i < mServices.size(); i++) {
+            final ServiceRegistration registration = mServices.valueAt(i);
+            if (registration.exiting) continue;
+
+            for (RecordInfo<MdnsInetAddressRecord> localRecord : registration.addressRecords) {
+                if (Objects.equals(localRecord.record, record)) {
+                    return true;
+                }
+            }
+        }
+        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) {
+    public Map<Integer, Integer> getConflictingServices(MdnsPacket packet) {
         // Avoid allocating a new set for each incoming packet: use an empty set by default.
-        Set<Integer> conflicting = Collections.emptySet();
+        Map<Integer, Integer> conflicting = Collections.emptyMap();
         for (MdnsRecord record : packet.answers) {
             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 (!MdnsUtils.equalsDnsLabelIgnoreDnsCase(record.getName(),
-                        srvRecord.record.getName())) {
-                    continue;
+                int conflictType = 0;
+
+                if (conflictForService(record, registration)) {
+                    conflictType |= CONFLICT_SERVICE;
                 }
 
-                // 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;
+                if (conflictForHost(record, registration)) {
+                    conflictType |= CONFLICT_HOST;
+                }
+
+                if (conflictType != 0) {
+                    if (conflicting.isEmpty()) {
+                        // Conflict was found: use a mutable set
+                        conflicting = new ArrayMap<>();
                     }
+                    final int serviceId = mServices.keyAt(i);
+                    conflicting.put(serviceId, conflictType);
                 }
-
-                if (record instanceof MdnsTextRecord) {
-                    final MdnsTextRecord local = registration.txtRecord.record;
-                    final MdnsTextRecord other = (MdnsTextRecord) record;
-                    if (Objects.equals(local, other)) {
-                        continue;
-                    }
-                }
-
-                if (conflicting.size() == 0) {
-                    // Conflict was found: use a mutable set
-                    conflicting = new ArraySet<>();
-                }
-                final int serviceId = mServices.keyAt(i);
-                conflicting.add(serviceId);
             }
         }
 
         return conflicting;
     }
 
-    private List<MdnsInetAddressRecord> makeProbingInetAddressRecords() {
-        final List<MdnsInetAddressRecord> records = new ArrayList<>();
-        if (mMdnsFeatureFlags.mIncludeInetAddressRecordsInProbing) {
-            for (RecordInfo<?> record : mGeneralRecords) {
-                if (record.record instanceof MdnsInetAddressRecord) {
-                    records.add((MdnsInetAddressRecord) record.record);
-                }
+
+    private static boolean conflictForService(
+            @NonNull MdnsRecord record, @NonNull ServiceRegistration registration) {
+        if (registration.srvRecord == null) {
+            return false;
+        }
+
+        final RecordInfo<MdnsServiceRecord> srvRecord = registration.srvRecord;
+        if (!MdnsUtils.equalsDnsLabelIgnoreDnsCase(record.getName(), srvRecord.record.getName())) {
+            return false;
+        }
+
+        // As per RFC6762 9., it's fine if the "conflict" is an identical record with same
+        // data.
+        if (record instanceof MdnsServiceRecord) {
+            final MdnsServiceRecord local = srvRecord.record;
+            final MdnsServiceRecord other = (MdnsServiceRecord) record;
+            // Note "equals" does not consider TTL or receipt time, as intended here
+            if (Objects.equals(local, other)) {
+                return false;
             }
         }
+
+        if (record instanceof MdnsTextRecord) {
+            final MdnsTextRecord local = registration.txtRecord.record;
+            final MdnsTextRecord other = (MdnsTextRecord) record;
+            if (Objects.equals(local, other)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    private boolean conflictForHost(
+            @NonNull MdnsRecord record, @NonNull ServiceRegistration registration) {
+        // Only custom hosts are checked. When using the default host, the hostname is derived from
+        // a UUID and it's supposed to be unique.
+        if (registration.serviceInfo.getHostname() == null) {
+            return false;
+        }
+
+        // The record's name cannot be registered by NsdManager so it's not a conflict.
+        if (record.getName().length != 2 || !record.getName()[1].equals(LOCAL_TLD)) {
+            return false;
+        }
+
+        // Different names. There won't be a conflict.
+        if (!MdnsUtils.equalsIgnoreDnsCase(
+                record.getName()[0], registration.serviceInfo.getHostname())) {
+            return false;
+        }
+
+        // If this registration has any address record and there's no identical record in the
+        // repository, it's a conflict. There will be no conflict if no registration has addresses
+        // for that hostname.
+        if (record instanceof MdnsInetAddressRecord) {
+            if (!registration.addressRecords.isEmpty()) {
+                return !hasInetAddressRecord((MdnsInetAddressRecord) record);
+            }
+        }
+
+        return false;
+    }
+
+    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.
@@ -894,8 +1198,8 @@
         if (registration == null) return null;
 
         registration.setProbing(true);
-        return makeProbingInfo(
-                serviceId, registration.srvRecord.record, makeProbingInetAddressRecords());
+
+        return makeProbingInfo(serviceId, registration);
     }
 
     /**
@@ -929,10 +1233,9 @@
         if (existing == null) return null;
 
         final ServiceRegistration newService = new ServiceRegistration(mDeviceHostname, newInfo,
-                existing.subtype, existing.repliedServiceCount, existing.sentPacketCount);
+                existing.repliedServiceCount, existing.sentPacketCount, existing.ttl);
         mServices.put(serviceId, newService);
-        return makeProbingInfo(
-                serviceId, newService.srvRecord.record, makeProbingInetAddressRecords());
+        return makeProbingInfo(serviceId, newService);
     }
 
     /**
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsReplyInfo.java b/service-t/src/com/android/server/connectivity/mdns/MdnsReplyInfo.java
index ce61b54..8747f67 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsReplyInfo.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsReplyInfo.java
@@ -32,22 +32,32 @@
     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 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 to " + destination + ", answers: " + answers.size()
+        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 ea3af5e..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,6 +16,8 @@
 
 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;
@@ -24,7 +26,10 @@
 import android.os.Handler;
 import android.os.Looper;
 import android.os.Message;
+import android.util.ArrayMap;
+import android.util.ArraySet;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.net.module.util.SharedLog;
 import com.android.server.connectivity.mdns.util.MdnsUtils;
 
@@ -34,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
@@ -57,15 +65,78 @@
     @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;
+
+    /**
+     * 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) {
+            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);
         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;
     }
 
     /**
@@ -73,8 +144,53 @@
      */
     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 (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);
@@ -104,7 +220,7 @@
      */
     public void cancelAll() {
         ensureRunningOnHandlerThread(mHandler);
-        mHandler.removeMessages(MSG_SEND);
+        mDependencies.removeMessages(mHandler, MSG_SEND);
     }
 
     private class SendHandler extends Handler {
@@ -114,7 +230,21 @@
 
         @Override
         public void handleMessage(@NonNull Message msg) {
-            final MdnsReplyInfo replyInfo = (MdnsReplyInfo) msg.obj;
+            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)
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 63835d9..086094b 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsSearchOptions.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsSearchOptions.java
@@ -39,6 +39,15 @@
  * @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 =
@@ -47,7 +56,7 @@
                 public MdnsSearchOptions createFromParcel(Parcel source) {
                     return new MdnsSearchOptions(
                             source.createStringArrayList(),
-                            source.readInt() == 1,
+                            source.readInt(),
                             source.readInt() == 1,
                             source.readParcelable(null),
                             source.readString(),
@@ -64,7 +73,7 @@
     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;
@@ -74,7 +83,7 @@
     /** Parcelable constructs for a {@link MdnsSearchOptions}. */
     MdnsSearchOptions(
             List<String> subtypes,
-            boolean isPassiveMode,
+            int queryMode,
             boolean removeExpiredService,
             @Nullable Network network,
             @Nullable String resolveInstanceName,
@@ -84,7 +93,7 @@
         if (subtypes != null) {
             this.subtypes.addAll(subtypes);
         }
-        this.isPassiveMode = isPassiveMode;
+        this.queryMode = queryMode;
         this.onlyUseIpv6OnIpv6OnlyNetworks = onlyUseIpv6OnIpv6OnlyNetworks;
         this.numOfQueriesBeforeBackoff = numOfQueriesBeforeBackoff;
         this.removeExpiredService = removeExpiredService;
@@ -111,11 +120,10 @@
     }
 
     /**
-     * @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;
     }
 
     /**
@@ -166,7 +174,7 @@
     @Override
     public void writeToParcel(Parcel out, int flags) {
         out.writeStringList(subtypes);
-        out.writeInt(isPassiveMode ? 1 : 0);
+        out.writeInt(queryMode);
         out.writeInt(removeExpiredService ? 1 : 0);
         out.writeParcelable(mNetwork, 0);
         out.writeString(resolveInstanceName);
@@ -177,7 +185,7 @@
     /** 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;
@@ -212,14 +220,12 @@
         }
 
         /**
-         * 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;
         }
 
@@ -276,7 +282,7 @@
         public MdnsSearchOptions build() {
             return new MdnsSearchOptions(
                     new ArrayList<>(subtypes),
-                    isPassiveMode,
+                    queryMode,
                     removeExpiredService,
                     mNetwork,
                     resolveInstanceName,
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..f60a95e 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,6 +91,9 @@
     @Nullable
     private final Network network;
 
+    @NonNull
+    private final Instant expirationTime;
+
     /** Constructs a {@link MdnsServiceInfo} object with default values. */
     public MdnsServiceInfo(
             String serviceInstanceName,
@@ -110,7 +115,8 @@
                 textStrings,
                 /* textEntries= */ null,
                 /* interfaceIndex= */ INTERFACE_INDEX_UNSPECIFIED,
-                /* network= */ null);
+                /* network= */ null,
+                /* expirationTime= */ Instant.MAX);
     }
 
     /** Constructs a {@link MdnsServiceInfo} object with default values. */
@@ -135,7 +141,8 @@
                 textStrings,
                 textEntries,
                 /* interfaceIndex= */ INTERFACE_INDEX_UNSPECIFIED,
-                /* network= */ null);
+                /* network= */ null,
+                /* expirationTime= */ Instant.MAX);
     }
 
     /**
@@ -165,7 +172,8 @@
                 textStrings,
                 textEntries,
                 interfaceIndex,
-                /* network= */ null);
+                /* network= */ null,
+                /* expirationTime= */ Instant.MAX);
     }
 
     /**
@@ -184,7 +192,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 +226,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 +324,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 +385,7 @@
         out.writeTypedList(textEntries);
         out.writeInt(interfaceIndex);
         out.writeParcelable(network, 0);
+        out.writeLong(expirationTime.getEpochSecond());
     }
 
     @Override
@@ -377,7 +399,8 @@
                 + ", interfaceIndex: " + interfaceIndex
                 + ", network: " + network
                 + ", textStrings: " + textStrings
-                + ", textEntries: " + textEntries;
+                + ", textEntries: " + textEntries
+                + ", expirationTime: " + expirationTime;
     }
 
 
@@ -496,4 +519,4 @@
             out.writeByteArray(value);
         }
     }
-}
\ No newline at end of file
+}
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 32f604e..8f41b94 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
@@ -35,10 +35,13 @@
 import com.android.net.module.util.SharedLog;
 import com.android.server.connectivity.mdns.util.MdnsUtils;
 
+import java.io.PrintWriter;
 import java.net.Inet4Address;
 import java.net.Inet6Address;
+import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.Iterator;
 import java.util.List;
@@ -81,7 +84,7 @@
                     notifyRemovedServiceToListeners(previousResponse, "Service record expired");
                 }
             };
-    private final ArrayMap<MdnsServiceBrowserListener, MdnsSearchOptions> listeners =
+    private final ArrayMap<MdnsServiceBrowserListener, ListenerInfo> listeners =
             new ArrayMap<>();
     private final boolean removeServiceAfterTtlExpires =
             MdnsConfigs.removeServiceAfterTtlExpires();
@@ -95,6 +98,32 @@
     private long currentSessionId = 0;
     private long lastSentTime;
 
+    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);
@@ -113,7 +142,7 @@
                     // 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,
-                            servicesToResolve.size() < listeners.size() /* sendDiscoveryQueries */);
+                            getAllDiscoverySubtypes(), needSendDiscoveryQueries(listeners));
                     executor.submit(queryTask);
                     break;
                 }
@@ -281,6 +310,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,
@@ -293,7 +323,8 @@
                 textStrings,
                 textEntries,
                 response.getInterfaceIndex(),
-                response.getNetwork());
+                response.getNetwork(),
+                now.plusMillis(response.getMinRemainingTtl(now.toEpochMilli())));
     }
 
     /**
@@ -311,12 +342,16 @@
         ensureRunningOnHandlerThread(handler);
         this.searchOptions = searchOptions;
         boolean hadReply = false;
-        if (listeners.put(listener, searchOptions) == null) {
+        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);
                 listener.onServiceNameDiscovered(info, true /* isServiceFromCache */);
+                listenerInfo.setServiceDiscovered(info.getServiceInstanceName());
                 if (existingResponse.isComplete()) {
                     listener.onServiceFound(info, true /* isServiceFromCache */);
                     hadReply = true;
@@ -329,8 +364,7 @@
         // Keep tracking the ScheduledFuture for the task so we can cancel it if caller is not
         // interested anymore.
         final QueryTaskConfig taskConfig = new QueryTaskConfig(
-                searchOptions.getSubtypes(),
-                searchOptions.isPassiveMode(),
+                searchOptions.getQueryMode(),
                 searchOptions.onlyUseIpv6OnIpv6OnlyNetworks(),
                 searchOptions.numOfQueriesBeforeBackoff(),
                 socketKey);
@@ -357,13 +391,22 @@
             final QueryTask queryTask = new QueryTask(
                     mdnsQueryScheduler.scheduleFirstRun(taskConfig, now,
                             minRemainingTtl, currentSessionId), servicesToResolve,
-                    servicesToResolve.size() < listeners.size() /* sendDiscoveryQueries */);
+                    getAllDiscoverySubtypes(), needSendDiscoveryQueries(listeners));
             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.
      */
@@ -480,9 +523,10 @@
     private void notifyRemovedServiceToListeners(@NonNull MdnsResponse response,
             @NonNull String message) {
         for (int i = 0; i < listeners.size(); i++) {
-            if (!responseMatchesOptions(response, listeners.valueAt(i))) continue;
+            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);
                 if (response.isComplete()) {
@@ -511,10 +555,9 @@
         final MdnsResponse currentResponse =
                 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) {
                 serviceCache.addOrUpdateService(cacheKey, response);
             }
@@ -525,16 +568,22 @@
             serviceBecomesComplete = !before && after;
         }
         sharedLog.i(String.format(
-                "Handling response from service: %s, newServiceFound: %b, serviceBecomesComplete:"
+                "Handling response from service: %s, newInCache: %b, serviceBecomesComplete:"
                         + " %b, responseIsComplete: %b",
-                serviceInstanceName, newServiceFound, serviceBecomesComplete,
+                serviceInstanceName, newInCache, serviceBecomesComplete,
                 response.isComplete()));
         MdnsServiceInfo serviceInfo =
                 buildMdnsServiceInfoFromResponse(response, serviceTypeLabels);
 
         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, false /* isServiceFromCache */);
@@ -576,10 +625,14 @@
     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;
             }
+            if (CollectionUtils.any(resolveResponses,
+                    r -> MdnsUtils.equalsIgnoreDnsCase(resolveName, r.getServiceInstanceName()))) {
+                continue;
+            }
             MdnsResponse knownResponse =
                     serviceCache.getCachedService(resolveName, cacheKey);
             if (knownResponse == null) {
@@ -596,6 +649,17 @@
         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;
 
@@ -628,11 +692,15 @@
     private class QueryTask implements Runnable {
         private final MdnsQueryScheduler.ScheduledQueryTaskArgs taskArgs;
         private final List<MdnsResponse> servicesToResolve = new ArrayList<>();
+        private final List<String> subtypes = new ArrayList<>();
         private final boolean sendDiscoveryQueries;
         QueryTask(@NonNull MdnsQueryScheduler.ScheduledQueryTaskArgs taskArgs,
-                @NonNull List<MdnsResponse> servicesToResolve, boolean sendDiscoveryQueries) {
+                @NonNull Collection<MdnsResponse> servicesToResolve,
+                @NonNull Collection<String> subtypes,
+                boolean sendDiscoveryQueries) {
             this.taskArgs = taskArgs;
             this.servicesToResolve.addAll(servicesToResolve);
+            this.subtypes.addAll(subtypes);
             this.sendDiscoveryQueries = sendDiscoveryQueries;
         }
 
@@ -645,7 +713,7 @@
                                 socketClient,
                                 createMdnsPacketWriter(),
                                 serviceType,
-                                taskArgs.config.subtypes,
+                                subtypes,
                                 taskArgs.config.expectUnicastResponse,
                                 taskArgs.config.transactionId,
                                 taskArgs.config.socketKey,
@@ -657,7 +725,7 @@
                                 .call();
             } catch (RuntimeException e) {
                 sharedLog.e(String.format("Failed to run EnqueueMdnsQueryCallable for subtype: %s",
-                        TextUtils.join(",", taskArgs.config.subtypes)), e);
+                        TextUtils.join(",", subtypes)), e);
                 result = Pair.create(INVALID_TRANSACTION_ID, new ArrayList<>());
             }
             dependencies.sendMessage(
@@ -692,4 +760,13 @@
                 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/MulticastPacketReader.java b/service-t/src/com/android/server/connectivity/mdns/MulticastPacketReader.java
index 3cd77a4..70451f3 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MulticastPacketReader.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MulticastPacketReader.java
@@ -42,6 +42,12 @@
     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/QueryTaskConfig.java b/service-t/src/com/android/server/connectivity/mdns/QueryTaskConfig.java
index 19282b0..0894166 100644
--- a/service-t/src/com/android/server/connectivity/mdns/QueryTaskConfig.java
+++ b/service-t/src/com/android/server/connectivity/mdns/QueryTaskConfig.java
@@ -16,15 +16,14 @@
 
 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;
 
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.List;
-
 /**
  * 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.
@@ -33,19 +32,26 @@
 
     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 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;
-    // The following fields are used by QueryTask so we need to test them.
     @VisibleForTesting
-    final List<String> subtypes;
+    // 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 boolean usePassiveMode;
+    private final int queryMode;
     final boolean onlyUseIpv6OnIpv6OnlyNetworks;
     private final int numOfQueriesBeforeBackoff;
     @VisibleForTesting
@@ -65,8 +71,7 @@
             boolean expectUnicastResponse, boolean isFirstBurst, int burstCounter,
             int queriesPerBurst, int timeBetweenBurstsInMs,
             long delayUntilNextTaskWithoutBackoffMs) {
-        this.subtypes = new ArrayList<>(other.subtypes);
-        this.usePassiveMode = other.usePassiveMode;
+        this.queryMode = other.queryMode;
         this.onlyUseIpv6OnIpv6OnlyNetworks = other.onlyUseIpv6OnIpv6OnlyNetworks;
         this.numOfQueriesBeforeBackoff = other.numOfQueriesBeforeBackoff;
         this.transactionId = transactionId;
@@ -79,36 +84,72 @@
         this.queryCount = queryCount;
         this.socketKey = other.socketKey;
     }
-    QueryTaskConfig(@NonNull Collection<String> subtypes,
-            boolean usePassiveMode,
+
+    QueryTaskConfig(int queryMode,
             boolean onlyUseIpv6OnIpv6OnlyNetworks,
             int numOfQueriesBeforeBackoff,
             @Nullable SocketKey socketKey) {
-        this.usePassiveMode = usePassiveMode;
+        this.queryMode = queryMode;
         this.onlyUseIpv6OnIpv6OnlyNetworks = onlyUseIpv6OnIpv6OnlyNetworks;
         this.numOfQueriesBeforeBackoff = numOfQueriesBeforeBackoff;
-        this.subtypes = new ArrayList<>(subtypes);
         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.usePassiveMode) {
+        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 = TIME_BETWEEN_BURSTS_MS;
+            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;
-        this.delayUntilNextTaskWithoutBackoffMs = TIME_BETWEEN_QUERIES_IN_BURST_MS;
+    }
+
+    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);
     }
 
     /**
@@ -120,43 +161,26 @@
         if (newTransactionId > UNSIGNED_SHORT_MAX_VALUE) {
             newTransactionId = 1;
         }
-        boolean newExpectUnicastResponse = false;
-        boolean newIsFirstBurst = isFirstBurst;
+
         int newQueriesPerBurst = queriesPerBurst;
         int newBurstCounter = burstCounter + 1;
-        long newDelayUntilNextTaskWithoutBackoffMs = delayUntilNextTaskWithoutBackoffMs;
-        int newTimeBetweenBurstsInMs = timeBetweenBurstsInMs;
-        // Only the first query expects uni-cast response.
-        if (newBurstCounter == queriesPerBurst) {
+        final boolean isFirstQueryInBurst = newBurstCounter == 1;
+        final boolean isLastQueryInBurst = newBurstCounter == queriesPerBurst;
+        boolean newIsFirstBurst = isFirstBurst && !isLastQueryInBurst;
+        if (isLastQueryInBurst) {
             newBurstCounter = 0;
-
-            if (alwaysAskForUnicastResponse) {
-                newExpectUnicastResponse = 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) {
-                newIsFirstBurst = false;
-                if (usePassiveMode) {
-                    newQueriesPerBurst = QUERIES_PER_BURST_PASSIVE_MODE;
-                }
+            if (isFirstBurst && queryMode == PASSIVE_QUERY_MODE) {
+                newQueriesPerBurst = 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.
-            newDelayUntilNextTaskWithoutBackoffMs = timeBetweenBurstsInMs;
-            if (timeBetweenBurstsInMs < TIME_BETWEEN_BURSTS_MS) {
-                newTimeBetweenBurstsInMs = Math.min(timeBetweenBurstsInMs * 2,
-                        TIME_BETWEEN_BURSTS_MS);
-            }
-        } else {
-            newDelayUntilNextTaskWithoutBackoffMs = TIME_BETWEEN_QUERIES_IN_BURST_MS;
         }
+
         return new QueryTaskConfig(this, newQueryCount, newTransactionId,
-                newExpectUnicastResponse, newIsFirstBurst, newBurstCounter, newQueriesPerBurst,
-                newTimeBetweenBurstsInMs, newDelayUntilNextTaskWithoutBackoffMs);
+                getNextExpectUnicastResponse(isLastQueryInBurst), newIsFirstBurst, newBurstCounter,
+                newQueriesPerBurst, getNextTimeBetweenBurstsMs(isLastQueryInBurst),
+                getDelayUntilNextTaskWithoutBackoff(isFirstQueryInBurst, isLastQueryInBurst));
     }
 
     /**
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 1482ebb..d553210 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
@@ -86,7 +86,10 @@
     /**
      * 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))) {
@@ -233,6 +236,20 @@
                 && 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 {
         /**
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 48e86d8..458d64f 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;
@@ -237,7 +238,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);
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 27c0f9f..0000000
--- a/service-t/src/com/android/server/net/BpfInterfaceMapUpdater.java
+++ /dev/null
@@ -1,175 +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> mIndexToIfaceBpfMap;
-    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;
-        mIndexToIfaceBpfMap = 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 (mIndexToIfaceBpfMap == 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 {
-            mIndexToIfaceBpfMap.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 = 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/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 2c9f30c..f7f133c 100644
--- a/service-t/src/com/android/server/net/NetworkStatsService.java
+++ b/service-t/src/com/android/server/net/NetworkStatsService.java
@@ -44,7 +44,6 @@
 import static android.net.NetworkStats.TAG_ALL;
 import static android.net.NetworkStats.TAG_NONE;
 import static android.net.NetworkStats.UID_ALL;
-import static android.net.NetworkStatsCollection.compareStats;
 import static android.net.NetworkStatsHistory.FIELD_ALL;
 import static android.net.NetworkTemplate.MATCH_MOBILE;
 import static android.net.NetworkTemplate.MATCH_TEST;
@@ -295,6 +294,11 @@
     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";
+
     private final Context mContext;
     private final NetworkStatsFactory mStatsFactory;
     private final AlarmManager mAlarmManager;
@@ -318,6 +322,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 =
@@ -470,11 +476,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());
@@ -486,9 +494,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;
         }
@@ -619,8 +628,7 @@
         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();
@@ -695,6 +703,24 @@
         }
 
         /**
+         * Get the count of using FastDataInput target attempts.
+         */
+        public int getUseFastDataInputTargetAttempts() {
+            return DeviceConfigUtils.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)
@@ -771,18 +797,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;
@@ -792,8 +816,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;
@@ -803,8 +827,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;
@@ -814,8 +837,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;
@@ -825,8 +847,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;
@@ -836,8 +858,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);
             }
@@ -856,7 +877,8 @@
         /** 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));
         }
 
         /**
@@ -892,13 +914,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();
 
@@ -963,13 +979,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")
@@ -1068,7 +1177,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)
@@ -1085,11 +1194,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;
         }
@@ -1120,7 +1229,8 @@
 
                 if (runComparison) {
                     final boolean success =
-                            compareImportedToLegacyStats(migration, legacyRecorders[i]);
+                            compareImportedToLegacyStats(migration, legacyRecorders[i],
+                                    true /* allowKeyChange */);
                     if (!success && !dryRunImportOnly) {
                         tryIncrementLegacyFallbacksCounter();
                     }
@@ -1243,7 +1353,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;
@@ -1258,7 +1368,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);
@@ -1319,7 +1430,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);
     }
 
@@ -1348,9 +1463,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) {
@@ -1365,7 +1480,8 @@
         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) {
@@ -1382,6 +1498,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);
@@ -1520,7 +1637,7 @@
     }
 
     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.
@@ -1541,7 +1658,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);
     }
@@ -1832,6 +1949,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();
@@ -1868,36 +1986,56 @@
         if (callingUid != android.os.Process.SYSTEM_UID && callingUid != uid) {
             return UNSUPPORTED;
         }
-        return nativeGetUidStat(uid, type);
+        return getEntryValueForType(nativeGetUidStat(uid), type);
     }
 
     @Override
     public long getIfaceStats(@NonNull String iface, int type) {
         Objects.requireNonNull(iface);
-        long nativeIfaceStats = nativeGetIfaceStat(iface, type);
-        if (nativeIfaceStats == -1) {
-            return nativeIfaceStats;
+        final NetworkStats.Entry entry = nativeGetIfaceStat(iface);
+        final long value = getEntryValueForType(entry, type);
+        if (value == UNSUPPORTED) {
+            return UNSUPPORTED;
         } else {
             // When tethering offload is in use, nativeIfaceStats does not contain usage from
             // offload, add it back here. Note that the included statistics might be stale
             // since polling newest stats from hardware might impact system health and not
             // suitable for TrafficStats API use cases.
-            return nativeIfaceStats + getProviderIfaceStats(iface, type);
+            entry.add(getProviderIfaceStats(iface));
+            return getEntryValueForType(entry, type);
+        }
+    }
+
+    private long getEntryValueForType(@Nullable NetworkStats.Entry entry, int type) {
+        if (entry == null) return UNSUPPORTED;
+        switch (type) {
+            case TrafficStats.TYPE_RX_BYTES:
+                return entry.rxBytes;
+            case TrafficStats.TYPE_TX_BYTES:
+                return entry.txBytes;
+            case TrafficStats.TYPE_RX_PACKETS:
+                return entry.rxPackets;
+            case TrafficStats.TYPE_TX_PACKETS:
+                return entry.txPackets;
+            default:
+                return UNSUPPORTED;
         }
     }
 
     @Override
     public long getTotalStats(int type) {
-        long nativeTotalStats = nativeGetTotalStat(type);
-        if (nativeTotalStats == -1) {
-            return nativeTotalStats;
+        final NetworkStats.Entry entry = nativeGetTotalStat();
+        final long value = getEntryValueForType(entry, type);
+        if (value == UNSUPPORTED) {
+            return UNSUPPORTED;
         } else {
             // Refer to comment in getIfaceStats
-            return nativeTotalStats + getProviderIfaceStats(IFACE_ALL, type);
+            entry.add(getProviderIfaceStats(IFACE_ALL));
+            return getEntryValueForType(entry, type);
         }
     }
 
-    private long getProviderIfaceStats(@Nullable String iface, int type) {
+    private NetworkStats.Entry getProviderIfaceStats(@Nullable String iface) {
         final NetworkStats providerSnapshot = getNetworkStatsFromProviders(STATS_PER_IFACE);
         final HashSet<String> limitIfaces;
         if (iface == IFACE_ALL) {
@@ -1906,19 +2044,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);
     }
 
     /**
@@ -2088,6 +2214,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);
 
@@ -2109,7 +2236,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);
                 }
@@ -2159,6 +2286,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) {
@@ -2177,9 +2305,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();
@@ -2544,7 +2678,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>();
@@ -2639,6 +2773,17 @@
                 }
             }
             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.decreaseIndent();
 
@@ -2753,9 +2898,9 @@
             }
 
             pw.println();
-            pw.println("InterfaceMapUpdater:");
+            pw.println("InterfaceMapHelper:");
             pw.increaseIndent();
-            mInterfaceMapUpdater.dump(pw);
+            mInterfaceMapHelper.dump(pw);
             pw.decreaseIndent();
 
             pw.println();
@@ -2777,6 +2922,12 @@
             dumpStatsMapLocked(mStatsMapB, pw, "mStatsMapB");
             dumpIfaceStatsMapLocked(pw);
             pw.decreaseIndent();
+
+            pw.println();
+            pw.println("SkDestroyListener logs:");
+            pw.increaseIndent();
+            mSkDestroyListener.dump(pw);
+            pw.decreaseIndent();
         }
     }
 
@@ -2896,7 +3047,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) + " "
@@ -2914,7 +3065,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 + " "
@@ -3274,10 +3425,14 @@
         }
     }
 
-    private static native long nativeGetTotalStat(int type);
-    private static native long nativeGetIfaceStat(String iface, int type);
-    private static native long nativeGetIfIndexStat(int ifindex, 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..8598ac4
--- /dev/null
+++ b/service-t/src/com/android/server/net/TrafficStatsRateLimitCache.java
@@ -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.net;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.net.NetworkStats;
+
+import com.android.internal.annotations.GuardedBy;
+
+import java.time.Clock;
+import java.util.HashMap;
+import java.util.Objects;
+
+/**
+ * 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.
+     */
+    TrafficStatsRateLimitCache(@NonNull Clock clock, long expiryDurationMs) {
+        mClock = clock;
+        mExpiryDurationMs = expiryDurationMs;
+    }
+
+    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 HashMap<TrafficStatsCacheKey, TrafficStatsCacheValue> mMap = new HashMap<>();
+
+    /**
+     * 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;
+            }
+        }
+    }
+
+    /**
+     * 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.clear();
+        }
+    }
+
+    private boolean isExpired(long timestamp) {
+        return mClock.millis() > timestamp + mExpiryDurationMs;
+    }
+}
diff --git a/service/Android.bp b/service/Android.bp
index e2dab9e..c35c4f8 100644
--- a/service/Android.bp
+++ b/service/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"],
 }
@@ -178,6 +179,8 @@
         "unsupportedappusage",
         "ServiceConnectivityResources",
         "framework-statsd",
+        "framework-permission",
+        "framework-permission-s",
     ],
     static_libs: [
         // Do not add libs here if they are already included
@@ -185,7 +188,7 @@
         "androidx.annotation_annotation",
         "connectivity-net-module-utils-bpf",
         "connectivity_native_aidl_interface-lateststable-java",
-        "dnsresolver_aidl_interface-V13-java",
+        "dnsresolver_aidl_interface-V14-java",
         "modules-utils-shell-command-handler",
         "net-utils-device-common",
         "net-utils-device-common-ip",
@@ -196,11 +199,15 @@
         "PlatformProperties",
         "service-connectivity-protos",
         "service-connectivity-stats-protos",
+        "net-utils-multicast-forwarding-structs",
     ],
     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__",
@@ -225,6 +232,7 @@
     ],
     lint: {
         strict_updatability_linting: true,
+
     },
 }
 
@@ -260,6 +268,8 @@
         "framework-tethering.impl",
         "framework-wifi",
         "libprotobuf-java-nano",
+        "framework-permission",
+        "framework-permission-s",
     ],
     jarjar_rules: ":connectivity-jarjar-rules",
     apex_available: [
@@ -268,9 +278,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
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-de/strings.xml b/service/ServiceConnectivityResources/res/values-de/strings.xml
index 536ebda..f58efb0 100644
--- a/service/ServiceConnectivityResources/res/values-de/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-de/strings.xml
@@ -29,7 +29,7 @@
     <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/config.xml b/service/ServiceConnectivityResources/res/values/config.xml
index f30abc6..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>
@@ -194,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..f7e47f5
--- /dev/null
+++ b/service/ServiceConnectivityResources/res/values/config_thread.xml
@@ -0,0 +1,34 @@
+<?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>
+</resources>
diff --git a/service/ServiceConnectivityResources/res/values/overlayable.xml b/service/ServiceConnectivityResources/res/values/overlayable.xml
index 4c85e8c..d9af5a3 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,10 @@
             <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" />
         </policy>
     </overlayable>
 </resources>
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/lint-baseline.xml b/service/lint-baseline.xml
index 5149e6d..3e11d52 100644
--- a/service/lint-baseline.xml
+++ b/service/lint-baseline.xml
@@ -1,5 +1,82 @@
 <?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 33 (current min is 30): `getUidRule`"
+        errorLine1="        return BpfNetMapsReader.getUidRule(sUidOwnerMap, childChain, uid);"
+        errorLine2="                                ~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/BpfNetMaps.java"
+            line="643"
+            column="33"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `BpfBitmap`"
+        errorLine1="                return new BpfBitmap(BLOCKED_PORTS_MAP_PATH);"
+        errorLine2="                       ~~~~~~~~~~~~~">
+        <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 +85,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 +674,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 +712,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/libs/libclat/Android.bp b/service/native/libs/libclat/Android.bp
index 5c6b123..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,7 +39,10 @@
 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",
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/server/BpfNetMaps.java b/service/src/com/android/server/BpfNetMaps.java
index ad9cfbe..a7fddd0 100644
--- a/service/src/com/android/server/BpfNetMaps.java
+++ b/service/src/com/android/server/BpfNetMaps.java
@@ -31,7 +31,6 @@
 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.PRE_T;
 import static android.net.BpfNetMapsUtils.getMatchByFirewallChain;
 import static android.net.BpfNetMapsUtils.isFirewallAllowList;
 import static android.net.BpfNetMapsUtils.matchToString;
@@ -68,6 +67,7 @@
 
 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.IBpfMap;
@@ -95,7 +95,7 @@
  */
 public class BpfNetMaps {
     static {
-        if (!PRE_T) {
+        if (SdkLevel.isAtLeastT()) {
             System.loadLibrary("service-connectivity");
         }
     }
@@ -184,60 +184,67 @@
         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);
+                    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);
+                    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);
+                    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,
+            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 new BpfMap<>(
-                    DATA_SAVER_ENABLED_MAP_PATH, BpfMap.BPF_F_RDWR, S32.class, U8.class);
+                    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 new BpfMap<>(INGRESS_DISCARD_MAP_PATH, BpfMap.BPF_F_RDWR,
+            return new BpfMap<>(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();
@@ -295,6 +302,7 @@
      * 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;
         initBpfMaps();
@@ -348,7 +356,7 @@
     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) {
@@ -357,7 +365,7 @@
 
     @VisibleForTesting
     public BpfNetMaps(final Context context, final INetd netd, final Dependencies deps) {
-        if (!PRE_T) {
+        if (SdkLevel.isAtLeastT()) {
             ensureInitialized(context);
         }
         mNetd = netd;
@@ -371,7 +379,7 @@
     }
 
     private void throwIfPreT(final String msg) {
-        if (PRE_T) {
+        if (!SdkLevel.isAtLeastT()) {
             throw new UnsupportedOperationException(msg);
         }
     }
@@ -712,7 +720,7 @@
      *                                  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;
         }
@@ -750,7 +758,7 @@
      *                                  cause of the failure.
      */
     public void removeUidInterfaceRules(final int[] uids) throws RemoteException {
-        if (PRE_T) {
+        if (!SdkLevel.isAtLeastT()) {
             mNetd.firewallRemoveUidInterfaceRules(uids);
             return;
         }
@@ -829,7 +837,7 @@
      * @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;
         }
@@ -1019,7 +1027,7 @@
     @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.");
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index 0ec0f13..6839c22 100755
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -38,6 +38,7 @@
 import static android.net.ConnectivityManager.BLOCKED_REASON_NONE;
 import static android.net.ConnectivityManager.CALLBACK_IP_CHANGED;
 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;
@@ -65,6 +66,7 @@
 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;
@@ -107,15 +109,15 @@
 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.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 com.android.net.module.util.PermissionUtils.hasAnyPermissionOf;
 import static com.android.server.ConnectivityStatsLog.CONNECTIVITY_STATE_SAMPLE;
-
-import static java.util.Map.Entry;
+import static com.android.server.connectivity.ConnectivityFlags.REQUEST_RESTRICTED_WIFI;
 
 import android.Manifest;
+import android.annotation.CheckResult;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.SuppressLint;
@@ -171,6 +173,7 @@
 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;
@@ -253,6 +256,7 @@
 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;
@@ -290,6 +294,7 @@
 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;
@@ -314,12 +319,12 @@
 import com.android.server.connectivity.DnsManager.PrivateDnsValidationUpdate;
 import com.android.server.connectivity.DscpPolicyTracker;
 import com.android.server.connectivity.FullScore;
-import com.android.server.connectivity.HandlerUtils;
 import com.android.server.connectivity.InvalidTagException;
 import com.android.server.connectivity.KeepaliveResourceUtil;
 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;
@@ -328,11 +333,13 @@
 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.RoutingCoordinatorService;
+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;
@@ -361,6 +368,7 @@
 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;
@@ -369,6 +377,8 @@
 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
@@ -463,6 +473,8 @@
 
     private volatile boolean mLockdownEnabled;
 
+    private final boolean mRequestRestrictedWifiEnabled;
+
     /**
      * 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.
@@ -496,6 +508,7 @@
     @GuardedBy("mTNSLock")
     private TestNetworkService mTNS;
     private final CompanionDeviceManagerProxyService mCdmps;
+    private final MulticastRoutingCoordinatorService mMulticastRoutingCoordinatorService;
     private final RoutingCoordinatorService mRoutingCoordinatorService;
 
     private final Object mTNSLock = new Object();
@@ -557,6 +570,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
@@ -827,6 +844,11 @@
     private static final int EVENT_UID_FROZEN_STATE_CHANGED = 61;
 
     /**
+     * Event to inform the ConnectivityService handler when a uid has lost carrier privileges.
+     */
+    private static final int EVENT_UID_CARRIER_PRIVILEGES_LOST = 62;
+
+    /**
      * Argument for {@link #EVENT_PROVISIONING_NOTIFICATION} to indicate that the notification
      * should be shown.
      */
@@ -915,6 +937,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;
@@ -941,6 +964,8 @@
 
     private final IpConnectivityLog mMetricsLog;
 
+    @Nullable private final NetworkRequestStateStatsMetrics mNetworkRequestStateStatsMetrics;
+
     @GuardedBy("mBandwidthRequests")
     private final SparseArray<Integer> mBandwidthRequests = new SparseArray<>(10);
 
@@ -1263,6 +1288,14 @@
     }
     private final LegacyTypeTracker mLegacyTypeTracker = new LegacyTypeTracker(this);
 
+    @VisibleForTesting
+    void onCarrierPrivilegesLost(Integer uid, Integer subId) {
+        if (mRequestRestrictedWifiEnabled) {
+            mHandler.sendMessage(mHandler.obtainMessage(
+                    EVENT_UID_CARRIER_PRIVILEGES_LOST, uid, subId));
+        }
+    }
+
     final LocalPriorityDump mPriorityDumper = new LocalPriorityDump();
     /**
      * Helper class which parses out priority arguments and dumps sections according to their
@@ -1277,7 +1310,7 @@
         LocalPriorityDump() {}
 
         private void dumpHigh(FileDescriptor fd, PrintWriter pw) {
-            if (!HandlerUtils.runWithScissors(mHandler, () -> {
+            if (!HandlerUtils.runWithScissorsForDump(mHandler, () -> {
                 doDump(fd, pw, new String[]{DIAG_ARG});
                 doDump(fd, pw, new String[]{SHORT_ARG});
             }, DUMPSYS_DEFAULT_TIMEOUT_MS)) {
@@ -1286,7 +1319,7 @@
         }
 
         private void dumpNormal(FileDescriptor fd, PrintWriter pw, String[] args) {
-            if (!HandlerUtils.runWithScissors(mHandler, () -> doDump(fd, pw, args),
+            if (!HandlerUtils.runWithScissorsForDump(mHandler, () -> doDump(fd, pw, args),
                     DUMPSYS_DEFAULT_TIMEOUT_MS)) {
                 pw.println("dumpNormal timeout");
             }
@@ -1421,6 +1454,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
          */
@@ -1457,15 +1514,31 @@
          */
         @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) {
             if (isAtLeastT()) {
-                return new CarrierPrivilegeAuthenticator(context, tm);
+                return new CarrierPrivilegeAuthenticator(
+                        context, tm, requestRestrictedWifiEnabled, listener);
             } else {
                 return null;
             }
         }
 
         /**
+         * @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) {
@@ -1654,6 +1727,7 @@
                 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,
@@ -1727,8 +1801,20 @@
         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);
+        mCarrierPrivilegeAuthenticator = mDeps.makeCarrierPrivilegeAuthenticator(
+                mContext, mTelephonyManager, mRequestRestrictedWifiEnabled,
+                this::onCarrierPrivilegesLost);
+
+        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
@@ -1859,9 +1945,11 @@
         }
 
         mRoutingCoordinatorService = new RoutingCoordinatorService(netd);
+        mMulticastRoutingCoordinatorService =
+                mDeps.makeMulticastRoutingCoordinatorService(mHandler);
 
-        mDestroyFrozenSockets = mDeps.isAtLeastU()
-                && mDeps.isFeatureEnabled(context, KEY_DESTROY_FROZEN_SOCKETS_VERSION);
+        mDestroyFrozenSockets = mDeps.isAtLeastV() || (mDeps.isAtLeastU()
+                && mDeps.isFeatureEnabled(context, KEY_DESTROY_FROZEN_SOCKETS_VERSION));
         mDelayDestroyFrozenSockets = mDeps.isAtLeastU()
                 && mDeps.isFeatureEnabled(context, DELAY_DESTROY_FROZEN_SOCKETS_VERSION);
         mAllowSysUiConnectivityReports = mDeps.isFeatureNotChickenedOut(
@@ -1996,6 +2084,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(
@@ -2615,7 +2715,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);
@@ -2651,7 +2751,7 @@
         Objects.requireNonNull(nc);
         Objects.requireNonNull(packageName);
         enforceNetworkStackOrSettingsPermission();
-        if (!checkAccessPermission(-1 /* pid */, uid)) {
+        if (!hasAccessPermission(-1 /* pid */, uid)) {
             return null;
         }
         return createWithLocationInfoSanitizedIfNecessaryWhenParceled(
@@ -2662,14 +2762,14 @@
 
     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
-                || checkAnyPermissionOf(mContext, pid, uid,
+                || hasAnyPermissionOf(mContext, pid, uid,
                         android.Manifest.permission.NETWORK_FACTORY);
     }
 
@@ -2682,14 +2782,14 @@
         // 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]);
@@ -2757,11 +2857,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;
         }
@@ -2770,10 +2871,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;
         }
@@ -2877,7 +2980,7 @@
             return new LinkProperties(lp);
         }
 
-        if (checkSettingsPermission(callerPid, callerUid)) {
+        if (hasSettingsPermission(callerPid, callerUid)) {
             return new LinkProperties(lp, true /* parcelSensitiveFields */);
         }
 
@@ -2893,7 +2996,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
@@ -3004,26 +3107,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.
@@ -3039,7 +3122,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 */);
@@ -3354,6 +3438,9 @@
 
     public static final String LOG_BPF_RC = "log_bpf_rc_force_disable";
 
+    public static final String ALLOW_SATALLITE_NETWORK_FALLBACK =
+            "allow_satallite_network_fallback";
+
     private void enforceInternetPermission() {
         mContext.enforceCallingOrSelfPermission(
                 android.Manifest.permission.INTERNET,
@@ -3366,7 +3453,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;
     }
@@ -3452,7 +3540,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(
@@ -3462,13 +3551,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(
@@ -3505,33 +3595,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 checkSystemBarServicePermission(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);
     }
 
-    private boolean checkNetworkSignalStrengthWakeupPermission(int pid, int uid) {
-        return checkAnyPermissionOf(mContext, pid, uid,
+    @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;
         }
@@ -3539,8 +3632,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;
         }
 
@@ -3554,7 +3646,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.");
         }
@@ -3564,7 +3656,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);
     }
@@ -3714,6 +3807,10 @@
             updateMobileDataPreferredUids();
         }
 
+        if (mSatelliteAccessController != null) {
+            mSatelliteAccessController.start();
+        }
+
         // On T+ devices, register callback for statsd to pull NETWORK_BPF_MAP_INFO atom
         if (mDeps.isAtLeastT()) {
             mBpfNetMaps.setPullAtomCallback(mContext);
@@ -3858,12 +3955,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="
@@ -4034,6 +4132,10 @@
         pw.increaseIndent();
         mNetworkActivityTracker.dump(pw);
         pw.decreaseIndent();
+
+        pw.println();
+        pw.println("Multicast routing supported: " +
+                (mMulticastRoutingCoordinatorService != null));
     }
 
     private void dumpNetworks(IndentingPrintWriter pw) {
@@ -5175,9 +5277,12 @@
     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(
-                    localAgent.linkProperties.getInterfaceName(),
-                    upstream.linkProperties.getInterfaceName());
+                    localNetworkInterfaceName,
+                    upstreamNetworkInterfaceName);
+            disableMulticastRouting(localNetworkInterfaceName, upstreamNetworkInterfaceName);
         } catch (RemoteException e) {
             loge("Couldn't remove interface forward for "
                     + localAgent.linkProperties.getInterfaceName() + " to "
@@ -5288,6 +5393,13 @@
         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.
@@ -5324,6 +5436,8 @@
                             updateSignalStrengthThresholds(network, "REGISTER", req);
                         }
                     }
+                } else if (req.isRequest() && mNetworkRequestStateStatsMetrics != null) {
+                    mNetworkRequestStateStatsMetrics.onNetworkRequestReceived(req);
                 }
             }
 
@@ -5541,6 +5655,8 @@
             }
             if (req.isListen()) {
                 removeListenRequestFromNetworks(req);
+            } else if (req.isRequest() && mNetworkRequestStateStatsMetrics != null) {
+                mNetworkRequestStateStatsMetrics.onNetworkRequestRemoved(req);
             }
         }
         nri.unlinkDeathRecipient();
@@ -5669,7 +5785,7 @@
     }
 
     private RequestInfoPerUidCounter getRequestCounter(NetworkRequestInfo nri) {
-        return checkAnyPermissionOf(mContext,
+        return hasAnyPermissionOf(mContext,
                 nri.mPid, nri.mUid, NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK)
                 ? mSystemNetworkRequestCounter : mNetworkRequestCounter;
     }
@@ -5893,7 +6009,7 @@
             if (nm == null) return;
 
             if (request == CaptivePortal.APP_REQUEST_REEVALUATION_REQUIRED) {
-                checkNetworkStackPermission();
+                enforceNetworkStackPermission(mContext);
                 nm.forceReevaluation(mDeps.getCallingUid());
             }
         }
@@ -5923,7 +6039,7 @@
      * @see MultinetworkPolicyTracker#getAvoidBadWifi()
      */
     public boolean shouldAvoidBadWifi() {
-        if (!checkNetworkStackPermission()) {
+        if (!hasNetworkStackPermission()) {
             throw new SecurityException("avoidBadWifi requires NETWORK_STACK permission");
         }
         return avoidBadWifi();
@@ -6292,10 +6408,8 @@
                     if (!networkFound) return;
 
                     if (underpinnedNetworkFound) {
-                        final NetworkCapabilities underpinnedNc =
-                                getNetworkCapabilitiesInternal(underpinnedNetwork);
                         mKeepaliveTracker.handleMonitorAutomaticKeepalive(ki,
-                                underpinnedNetwork.netId, underpinnedNc.getUids());
+                                underpinnedNetwork.netId);
                     } else {
                         // If no underpinned network, then make sure the keepalive is running.
                         mKeepaliveTracker.handleMaybeResumeKeepalive(ki);
@@ -6376,6 +6490,9 @@
                     UidFrozenStateChangedArgs args = (UidFrozenStateChangedArgs) msg.obj;
                     handleFrozenUids(args.mUids, args.mFrozenStates);
                     break;
+                case EVENT_UID_CARRIER_PRIVILEGES_LOST:
+                    handleUidCarrierPrivilegesLost(msg.arg1, msg.arg2);
+                    break;
             }
         }
     }
@@ -7445,20 +7562,25 @@
     // 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();
+        if (nc.getSubscriptionIds().isEmpty()) {
+            return;
         }
+        if (mRequestRestrictedWifiEnabled
+                && canRequestRestrictedNetworkDueToCarrierPrivileges(nc, callerUid)) {
+            return;
+        }
+        enforceNetworkFactoryPermission();
     }
 
     private int[] getSignalStrengthThresholds(@NonNull final NetworkAgentInfo nai) {
@@ -7548,7 +7670,7 @@
             int reqTypeInt, Messenger messenger, int timeoutMs, final IBinder binder,
             int legacyType, int callbackFlags, @NonNull String callingPackageName,
             @Nullable String callingAttributionTag) {
-        if (legacyType != TYPE_NONE && !checkNetworkStackPermission()) {
+        if (legacyType != TYPE_NONE && !hasNetworkStackPermission()) {
             if (isTargetSdkAtleast(Build.VERSION_CODES.M, mDeps.getCallingUid(),
                     callingPackageName)) {
                 throw new SecurityException("Insufficient permissions to specify legacy type");
@@ -7738,6 +7860,22 @@
         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,
@@ -7745,13 +7883,11 @@
             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);
         }
@@ -9018,6 +9154,40 @@
         }
     }
 
+    private void handleUidCarrierPrivilegesLost(int uid, int subId) {
+        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.isListen())
+                    && !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:
      *
@@ -9095,6 +9265,71 @@
         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,
@@ -9108,7 +9343,6 @@
             Log.v(TAG, "Update local network config " + nai.network.netId + " : " + newConfig);
         }
         final LocalNetworkConfig.Builder configBuilder = new LocalNetworkConfig.Builder();
-        // TODO : apply the diff for multicast routing.
         configBuilder.setUpstreamMulticastRoutingConfig(
                 newConfig.getUpstreamMulticastRoutingConfig());
         configBuilder.setDownstreamMulticastRoutingConfig(
@@ -9167,6 +9401,7 @@
             configBuilder.setUpstreamSelector(oldRequest);
             nai.localNetworkConfig = configBuilder.build();
         }
+        maybeApplyMulticastRoutingConfig(nai, oldConfig, newConfig);
     }
 
     /**
@@ -9400,7 +9635,6 @@
         final ArraySet<Integer> toAdd = new ArraySet<>(newUids);
         toRemove.removeAll(newUids);
         toAdd.removeAll(prevUids);
-
         try {
             if (!toAdd.isEmpty()) {
                 mNetd.networkAddUidRangesParcel(new NativeUidRangeConfig(
@@ -10166,6 +10400,8 @@
                     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 !)
@@ -10174,6 +10410,9 @@
                     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);
@@ -11041,17 +11280,28 @@
                 err.getFileDescriptor(), args);
     }
 
-    private Boolean parseBooleanArgument(final String arg) {
-        if ("true".equals(arg)) {
-            return true;
-        } else if ("false".equals(arg)) {
-            return false;
-        } else {
-            return null;
-        }
-    }
-
     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) {
@@ -11128,6 +11378,38 @@
                         }
                         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": {
+                        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
@@ -11188,6 +11470,10 @@
                     + "    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.");
         }
     }
 
@@ -11228,7 +11514,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
@@ -11488,7 +11774,7 @@
             if (report == null) {
                 continue;
             }
-            if (!checkConnectivityDiagnosticsPermissions(
+            if (!hasConnectivityDiagnosticsPermissions(
                     nri.mPid, nri.mUid, nai, cbInfo.mCallingPackageName)) {
                 continue;
             }
@@ -11651,7 +11937,7 @@
                 continue;
             }
 
-            if (!checkConnectivityDiagnosticsPermissions(
+            if (!hasConnectivityDiagnosticsPermissions(
                     nri.mPid, nri.mUid, nai, cbInfo.mCallingPackageName)) {
                 continue;
             }
@@ -11695,14 +11981,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
-                && checkSystemBarServicePermission(callbackPid, callbackUid)) {
+                && hasSystemBarServicePermission(callbackPid, callbackUid)) {
             return true;
         }
 
@@ -12568,16 +12855,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.
@@ -12586,14 +12884,28 @@
                 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: restricted Satellite internet
+        final NetworkCapabilities cap = new NetworkCapabilities.Builder()
+                .addCapability(NET_CAPABILITY_INTERNET)
+                .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+                .removeCapability(NET_CAPABILITY_NOT_RESTRICTED)
+                .addTransportType(NetworkCapabilities.TRANSPORT_SATELLITE)
+                .build();
+        requests.add(createNetworkRequest(NetworkRequest.Type.REQUEST, cap));
+
+        return createNrisForPreferenceOrder(uids, requests, PREFERENCE_ORDER_SATELLITE_FALLBACK);
     }
 
     private void handleMobileDataPreferredUidsChanged() {
@@ -12605,6 +12917,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(
diff --git a/service/src/com/android/server/connectivity/AutomaticOnOffKeepaliveTracker.java b/service/src/com/android/server/connectivity/AutomaticOnOffKeepaliveTracker.java
index 8036ae9..31108fc 100644
--- a/service/src/com/android/server/connectivity/AutomaticOnOffKeepaliveTracker.java
+++ b/service/src/com/android/server/connectivity/AutomaticOnOffKeepaliveTracker.java
@@ -25,9 +25,6 @@
 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;
@@ -53,7 +50,6 @@
 import android.util.LocalLog;
 import android.util.Log;
 import android.util.Pair;
-import android.util.Range;
 import android.util.SparseArray;
 
 import com.android.internal.annotations.VisibleForTesting;
@@ -78,7 +74,6 @@
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Objects;
-import java.util.Set;
 
 /**
  * Manages automatic on/off socket keepalive requests.
@@ -90,6 +85,7 @@
  */
 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;
@@ -375,27 +371,26 @@
      * Determine if any state transition is needed for the specific automatic keepalive.
      */
     public void handleMonitorAutomaticKeepalive(@NonNull final AutomaticOnOffKeepalive ki,
-            final int vpnNetId, @NonNull Set<Range<Integer>> vpnUidRanges) {
+            final int vpnNetId) {
         // Might happen if the automatic keepalive was removed by the app just as the alarm fires.
         if (!mAutomaticOnOffKeepalives.contains(ki)) return;
         if (STATE_ALWAYS_ON == ki.mAutomaticOnOffState) {
             throw new IllegalStateException("Should not monitor non-auto keepalive");
         }
 
-        handleMonitorTcpConnections(ki, vpnNetId, vpnUidRanges);
+        handleMonitorTcpConnections(ki, vpnNetId);
     }
 
     /**
      * Determine if disable or re-enable keepalive is needed or not based on TCP sockets status.
      */
-    private void handleMonitorTcpConnections(@NonNull AutomaticOnOffKeepalive ki, int vpnNetId,
-            @NonNull Set<Range<Integer>> vpnUidRanges) {
+    private void handleMonitorTcpConnections(@NonNull AutomaticOnOffKeepalive ki, int vpnNetId) {
         // Might happen if the automatic keepalive was removed by the app just as the alarm fires.
         if (!mAutomaticOnOffKeepalives.contains(ki)) return;
         if (STATE_ALWAYS_ON == ki.mAutomaticOnOffState) {
             throw new IllegalStateException("Should not monitor non-auto keepalive");
         }
-        if (!isAnyTcpSocketConnected(vpnNetId, vpnUidRanges)) {
+        if (!isAnyTcpSocketConnected(vpnNetId)) {
             // No TCP socket exists. Stop keepalive if ENABLED, and remain SUSPENDED if currently
             // SUSPENDED.
             if (ki.mAutomaticOnOffState == STATE_ENABLED) {
@@ -747,7 +742,7 @@
     }
 
     @VisibleForTesting
-    boolean isAnyTcpSocketConnected(int netId, @NonNull Set<Range<Integer>> vpnUidRanges) {
+    boolean isAnyTcpSocketConnected(int netId) {
         FileDescriptor fd = null;
 
         try {
@@ -760,8 +755,7 @@
 
             // Send request for each IP family
             for (final int family : ADDRESS_FAMILIES) {
-                if (isAnyTcpSocketConnectedForFamily(
-                        fd, family, networkMark, networkMask, vpnUidRanges)) {
+                if (isAnyTcpSocketConnectedForFamily(fd, family, networkMark, networkMask)) {
                     return true;
                 }
             }
@@ -775,7 +769,7 @@
     }
 
     private boolean isAnyTcpSocketConnectedForFamily(FileDescriptor fd, int family, int networkMark,
-            int networkMask, @NonNull Set<Range<Integer>> vpnUidRanges)
+            int networkMask)
             throws ErrnoException, InterruptedIOException {
         ensureRunningOnHandlerThread();
         // Build SocketDiag messages and cache it.
@@ -794,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,
@@ -834,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;
diff --git a/service/src/com/android/server/connectivity/CarrierPrivilegeAuthenticator.java b/service/src/com/android/server/connectivity/CarrierPrivilegeAuthenticator.java
index 5705ebe..04d0fc1 100644
--- a/service/src/com/android/server/connectivity/CarrierPrivilegeAuthenticator.java
+++ b/service/src/com/android/server/connectivity/CarrierPrivilegeAuthenticator.java
@@ -40,12 +40,13 @@
 import android.telephony.SubscriptionManager;
 import android.telephony.TelephonyManager;
 import android.util.Log;
-import android.util.SparseIntArray;
+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;
@@ -55,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.
@@ -71,7 +73,8 @@
     private final TelephonyManagerShim mTelephonyManagerShim;
     private final TelephonyManager mTelephonyManager;
     @GuardedBy("mLock")
-    private final SparseIntArray mCarrierServiceUid = new SparseIntArray(2 /* initialCapacity */);
+    private final SparseArray<CarrierServiceUidWithSubId> mCarrierServiceUidWithSubId =
+            new SparseArray<>(2 /* initialCapacity */);
     @GuardedBy("mLock")
     private int mModemCount = 0;
     private final Object mLock = new Object();
@@ -79,11 +82,16 @@
     @NonNull
     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 TelephonyManagerShim telephonyManagerShim) {
+            @NonNull final TelephonyManagerShim telephonyManagerShim,
+            final boolean requestRestrictedWifiEnabled,
+            @NonNull BiConsumer<Integer, Integer> listener) {
         mContext = c;
         mTelephonyManager = t;
         mTelephonyManagerShim = telephonyManagerShim;
@@ -92,6 +100,8 @@
         mHandler = new Handler(thread.getLooper());
         mUseCallbacksForServiceChanged = deps.isFeatureEnabled(
                 c, CARRIER_SERVICE_CHANGED_USE_CALLBACK);
+        mRequestRestrictedWifiEnabled = requestRestrictedWifiEnabled;
+        mListener = listener;
         final IntentFilter filter = new IntentFilter();
         filter.addAction(TelephonyManager.ACTION_MULTI_SIM_CONFIG_CHANGED);
         synchronized (mLock) {
@@ -113,8 +123,10 @@
     }
 
     public CarrierPrivilegeAuthenticator(@NonNull final Context c,
-            @NonNull final TelephonyManager t) {
-        this(c, new Dependencies(), t, TelephonyManagerShimImpl.newInstance(t));
+            @NonNull final TelephonyManager t, final boolean requestRestrictedWifiEnabled,
+            @NonNull BiConsumer<Integer, Integer> listener) {
+        this(c, new Dependencies(), t, TelephonyManagerShimImpl.newInstance(t),
+                requestRestrictedWifiEnabled, listener);
     }
 
     public static class Dependencies {
@@ -142,6 +154,29 @@
         }
     }
 
+    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;
 
@@ -171,7 +206,18 @@
                 return;
             }
             synchronized (mLock) {
-                mCarrierServiceUid.put(mLogicalSlot, carrierServiceUid);
+                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);
+                }
             }
         }
     }
@@ -193,7 +239,14 @@
     private void unregisterCarrierPrivilegesListeners() {
         for (PrivilegeListener carrierPrivilegesListener : mCarrierPrivilegesChangedListeners) {
             removeCarrierPrivilegesListener(carrierPrivilegesListener);
-            mCarrierServiceUid.delete(carrierPrivilegesListener.mLogicalSlot);
+            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);
+            }
         }
         mCarrierPrivilegesChangedListeners.clear();
     }
@@ -230,8 +283,24 @@
      */
     public boolean isCarrierServiceUidForNetworkCapabilities(int callingUid,
             @NonNull NetworkCapabilities networkCapabilities) {
-        if (callingUid == Process.INVALID_UID) return false;
-        final int subId;
+        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)) {
@@ -239,6 +308,12 @@
         } 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()
@@ -250,34 +325,60 @@
             Log.wtf(TAG, "NetworkCapabilities subIds are inconsistent between "
                     + "specifier/transportInfo and mSubIds : " + networkCapabilities);
         }
-        if (SubscriptionManager.INVALID_SUBSCRIPTION_ID == subId) return false;
-        return callingUid == getCarrierServiceUidForSubId(subId);
+        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.clear();
+            SparseArray<CarrierServiceUidWithSubId> copy = mCarrierServiceUidWithSubId.clone();
+            mCarrierServiceUidWithSubId.clear();
             for (int i = 0; i < mModemCount; i++) {
-                mCarrierServiceUid.put(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) {
-            return mCarrierServiceUid.get(slotId, Process.INVALID_UID);
+            for (int i = 0; i < mCarrierServiceUidWithSubId.size(); ++i) {
+                if (mCarrierServiceUidWithSubId.valueAt(i).mSubId == subId) {
+                    return mCarrierServiceUidWithSubId.valueAt(i).mUid;
+                }
+            }
+            return Process.INVALID_UID;
         }
     }
 
     @VisibleForTesting
-    protected int getSlotIndex(int subId) {
-        return SubscriptionManager.getSlotIndex(subId);
-    }
-
-    @VisibleForTesting
     int getUidForPackage(String pkgName) {
         if (pkgName == null) {
             return Process.INVALID_UID;
@@ -340,12 +441,14 @@
 
     public void dump(IndentingPrintWriter pw) {
         pw.println("CarrierPrivilegeAuthenticator:");
+        pw.println("mRequestRestrictedWifiEnabled = " + mRequestRestrictedWifiEnabled);
         synchronized (mLock) {
-            final int size = mCarrierServiceUid.size();
-            for (int i = 0; i < size; ++i) {
-                final int logicalSlot = mCarrierServiceUid.keyAt(i);
-                final int serviceUid = mCarrierServiceUid.valueAt(i);
-                pw.println("Logical slot = " + logicalSlot + " : uid = " + serviceUid);
+            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 17de146..daaf91d 100644
--- a/service/src/com/android/server/connectivity/ClatCoordinator.java
+++ b/service/src/com/android/server/connectivity/ClatCoordinator.java
@@ -256,7 +256,7 @@
         public IBpfMap<ClatIngress6Key, ClatIngress6Value> getBpfIngress6Map() {
             try {
                 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;
@@ -268,7 +268,7 @@
         public IBpfMap<ClatEgress4Key, ClatEgress4Value> getBpfEgress4Map() {
             try {
                 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;
@@ -280,7 +280,7 @@
         public IBpfMap<CookieTagMapKey, CookieTagMapValue> getBpfCookieTagMap() {
             try {
                 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;
diff --git a/service/src/com/android/server/connectivity/ConnectivityFlags.java b/service/src/com/android/server/connectivity/ConnectivityFlags.java
index f8f76ef..bf09160 100644
--- a/service/src/com/android/server/connectivity/ConnectivityFlags.java
+++ b/service/src/com/android/server/connectivity/ConnectivityFlags.java
@@ -36,6 +36,8 @@
     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";
     private boolean mNoRematchAllRequestsOnRegister;
 
     /**
diff --git a/service/src/com/android/server/connectivity/DscpPolicyTracker.java b/service/src/com/android/server/connectivity/DscpPolicyTracker.java
index 8d566b6..15d6adb 100644
--- a/service/src/com/android/server/connectivity/DscpPolicyTracker.java
+++ b/service/src/com/android/server/connectivity/DscpPolicyTracker.java
@@ -85,10 +85,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) {
diff --git a/service/src/com/android/server/connectivity/HandlerUtils.java b/service/src/com/android/server/connectivity/HandlerUtils.java
deleted file mode 100644
index 997ecbf..0000000
--- a/service/src/com/android/server/connectivity/HandlerUtils.java
+++ /dev/null
@@ -1,139 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.connectivity;
-
-import android.annotation.NonNull;
-import android.os.Handler;
-import android.os.Looper;
-import android.os.SystemClock;
-
-/**
- * Helper class for Handler related utilities.
- *
- * @hide
- */
-public class HandlerUtils {
-    // Note: @hide methods copied from android.os.Handler
-    /**
-     * Runs the specified task synchronously.
-     * <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 occasionally useful in situations where a background thread
-     * must synchronously await completion of a task that must run on the
-     * handler's thread.  However, this problem is often a symptom of bad design.
-     * Consider improving the design (if possible) before resorting to this method.
-     * </p><p>
-     * One example of where you might want to use this method is when you just
-     * set up a Handler thread and need to perform some initialization steps on
-     * it before continuing execution.
-     * </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 #runWithScissors} 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 wait indefinitely.
-     *
-     * @return Returns true if the Runnable was successfully executed.
-     *         Returns false on failure, usually because the
-     *         looper processing the message queue is exiting.
-     *
-     * @hide This method is prone to abuse and should probably not be in the API.
-     * If we ever do make it part of the API, we might want to rename it to something
-     * less funny like runUnsafe().
-     */
-    public static boolean runWithScissors(@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;
-        }
-
-        BlockingRunnable br = new BlockingRunnable(r);
-        return br.postAndWait(h, timeout);
-    }
-
-    private static final class BlockingRunnable implements Runnable {
-        private final Runnable mTask;
-        private boolean mDone;
-
-        BlockingRunnable(Runnable task) {
-            mTask = task;
-        }
-
-        @Override
-        public void run() {
-            try {
-                mTask.run();
-            } finally {
-                synchronized (this) {
-                    mDone = true;
-                    notifyAll();
-                }
-            }
-        }
-
-        public boolean postAndWait(Handler handler, long timeout) {
-            if (!handler.post(this)) {
-                return false;
-            }
-
-            synchronized (this) {
-                if (timeout > 0) {
-                    final long expirationTime = SystemClock.uptimeMillis() + timeout;
-                    while (!mDone) {
-                        long delay = expirationTime - SystemClock.uptimeMillis();
-                        if (delay <= 0) {
-                            return false; // timeout
-                        }
-                        try {
-                            wait(delay);
-                        } catch (InterruptedException ex) {
-                        }
-                    }
-                } else {
-                    while (!mDone) {
-                        try {
-                            wait();
-                        } catch (InterruptedException ex) {
-                        }
-                    }
-                }
-            }
-            return true;
-        }
-    }
-}
diff --git a/service/src/com/android/server/connectivity/KeepaliveStatsTracker.java b/service/src/com/android/server/connectivity/KeepaliveStatsTracker.java
index 7a8b41b..48af9fa 100644
--- a/service/src/com/android/server/connectivity/KeepaliveStatsTracker.java
+++ b/service/src/com/android/server/connectivity/KeepaliveStatsTracker.java
@@ -74,9 +74,10 @@
 public class KeepaliveStatsTracker {
     private static final String TAG = KeepaliveStatsTracker.class.getSimpleName();
     private static final int INVALID_KEEPALIVE_ID = -1;
-    // 1 hour acceptable deviation in metrics collection duration time.
+    // 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 + 1 * 60 * 60 * 1_000L;
+            AutomaticOnOffKeepaliveTracker.METRICS_COLLECTION_DURATION_MS + 2 * 60 * 60 * 1_000L;
 
     @NonNull private final Handler mConnectivityServiceHandler;
     @NonNull private final Dependencies mDependencies;
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..4d5001b
--- /dev/null
+++ b/service/src/com/android/server/connectivity/MulticastRoutingCoordinatorService.java
@@ -0,0 +1,820 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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 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 = mInterfaces.indexOfValue(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();
+
+        if (newConfig.getForwardingMode() != FORWARD_NONE) {
+            // Make sure iif and oif are added as multicast forwarding interfaces
+            try {
+                maybeAddAndTrackInterface(iifName);
+                maybeAddAndTrackInterface(oifName);
+            } catch (IllegalStateException e) {
+                Log.e(TAG, "Failed to apply multicast routing config, ", e);
+                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(mInterfaces.indexOfValue(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);
+        }
+    }
+
+    private int getNextAvailableVirtualIndex() {
+        if (mVirtualInterfaces.size() >= MAX_NUM_OF_MULTICAST_VIRTUAL_INTERFACES) {
+            throw new IllegalStateException("Can't allocate new multicast virtual interface");
+        }
+        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 = mVirtualInterfaces.indexOfValue(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);
+    }
+
+    private void maybeAddAndTrackInterface(String ifName) {
+        checkOnHandlerThread();
+        if (mVirtualInterfaces.indexOfValue(ifName) >= 0) return;
+
+        int nextVirtualIndex = getNextAvailableVirtualIndex();
+        int ifIndex = mDependencies.getInterfaceIndex(ifName);
+        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;
+        }
+        mVirtualInterfaces.put(nextVirtualIndex, ifName);
+        mInterfaces.put(ifIndex, ifName);
+    }
+
+    @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);
+        }
+
+        public Integer getInterfaceIndex(String ifName) {
+            try {
+                NetworkInterface ni = NetworkInterface.getByName(ifName);
+                return ni.getIndex();
+            } catch (NullPointerException | SocketException e) {
+                return null;
+            }
+        }
+
+        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/NetworkAgentInfo.java b/service/src/com/android/server/connectivity/NetworkAgentInfo.java
index 50cad45..76993a6 100644
--- a/service/src/com/android/server/connectivity/NetworkAgentInfo.java
+++ b/service/src/com/android/server/connectivity/NetworkAgentInfo.java
@@ -1551,7 +1551,7 @@
      * @param hasAutomotiveFeature true if this device has the automotive feature, false otherwise
      * @param authenticator the carrier privilege authenticator to check for telephony constraints
      */
-    public void restrictCapabilitiesFromNetworkAgent(@NonNull final NetworkCapabilities nc,
+    public static void restrictCapabilitiesFromNetworkAgent(@NonNull final NetworkCapabilities nc,
             final int creatorUid, final boolean hasAutomotiveFeature,
             @NonNull final ConnectivityService.Dependencies deps,
             @Nullable final CarrierPrivilegeAuthenticator authenticator) {
@@ -1564,7 +1564,7 @@
         }
     }
 
-    private boolean areAllowedUidsAcceptableFromNetworkAgent(
+    private static boolean areAllowedUidsAcceptableFromNetworkAgent(
             @NonNull final NetworkCapabilities nc, final boolean hasAutomotiveFeature,
             @NonNull final ConnectivityService.Dependencies deps,
             @Nullable final CarrierPrivilegeAuthenticator carrierPrivilegeAuthenticator) {
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/RoutingCoordinatorService.java b/service/src/com/android/server/connectivity/RoutingCoordinatorService.java
index 3350d2d..742a2cc 100644
--- a/service/src/com/android/server/connectivity/RoutingCoordinatorService.java
+++ b/service/src/com/android/server/connectivity/RoutingCoordinatorService.java
@@ -171,7 +171,8 @@
             }
             final ForwardingPair fwp = new ForwardingPair(fromIface, toIface);
             if (mForwardedInterfaces.contains(fwp)) {
-                throw new IllegalStateException("Forward already exists between ifaces "
+                // TODO: remove if no reports are observed from the below log
+                Log.wtf(TAG, "Forward already exists between ifaces "
                         + fromIface + " → " + toIface);
             }
             mForwardedInterfaces.add(fwp);
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..b53abce
--- /dev/null
+++ b/service/src/com/android/server/connectivity/SatelliteAccessController.java
@@ -0,0 +1,236 @@
+/*
+ * 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.Context;
+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 UserManager mUserManager;
+    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;
+        mUserManager = mContext.getSystemService(UserManager.class);
+        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()
+    // TODO(b/326373613): using UserLifecycleListener, callback to be received when user removed for
+    // user delete scenario. This to be used to update uid list and ML Layer request can also be
+    // updated.
+    private void onRoleSmsChanged(@NonNull UserHandle userHandle) {
+        int userId = userHandle.getIdentifier();
+        if (userId == Process.INVALID_UID) {
+            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 = !packageNames.isEmpty()
+                ? updateSatelliteNetworkFallbackUidListCache(packageNames, userHandle)
+                : new ArraySet<>();
+        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);
+
+        // 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);
+    }
+
+    private List<String> getRoleSmsChangedPackageName(UserHandle userHandle) {
+        try {
+            return mDeps.getRoleHoldersAsUser(RoleManager.ROLE_SMS, userHandle);
+        } catch (RuntimeException e) {
+            Log.wtf(TAG, "Could not get package name at role sms change update due to: " + e);
+            return null;
+        }
+    }
+
+    /** Register OnRoleHoldersChangedListener */
+    public void start() {
+        mConnectivityServiceHandler.post(this::updateAllUserRoleSmsUids);
+        mDefaultMessageRoleListener.register();
+    }
+
+    private void updateAllUserRoleSmsUids() {
+        List<UserHandle> existingUsers = mUserManager.getUserHandles(true /* excludeDying */);
+        // Iterate through the user handles and obtain their uids with role sms and satellite
+        // communication permission
+        for (UserHandle userHandle : existingUsers) {
+            onRoleSmsChanged(userHandle);
+        }
+    }
+}
diff --git a/staticlibs/Android.bp b/staticlibs/Android.bp
index 9f1debc..f7b42a6 100644
--- a/staticlibs/Android.bp
+++ b/staticlibs/Android.bp
@@ -28,33 +28,35 @@
 // 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/NetworkMonitorUtils.java",
-      "device/com/android/net/module/util/PacketReader.java",
-      "device/com/android/net/module/util/SharedLog.java",
-      "device/com/android/net/module/util/SocketUtils.java",
-      "device/com/android/net/module/util/FeatureVersions.java",
-      // 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: [
+    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/NetworkMonitorUtils.java",
+        "device/com/android/net/module/util/PacketReader.java",
+        "device/com/android/net/module/util/SharedLog.java",
+        "device/com/android/net/module/util/SocketUtils.java",
+        "device/com/android/net/module/util/FeatureVersions.java",
+        "device/com/android/net/module/util/HandlerUtils.java",
+        // 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__",
@@ -64,26 +66,26 @@
         "//frameworks/opt/net/telephony",
         "//packages/modules/NetworkStack:__subpackages__",
         "//packages/modules/CaptivePortalLogin",
-  ],
-  static_libs: [
-      "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"],
-  },
+    ],
+    static_libs: [
+        "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"
+        "mockito-target-extended-minus-junit4",
     ],
     jni_libs: [
         "libdexmakerjvmtiagent",
@@ -94,12 +96,12 @@
 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",
+        "framework/**/DnsPacket.java",
+        "framework/**/DnsPacketUtils.java",
+        "framework/**/DnsSvcbPacket.java",
+        "framework/**/DnsSvcbRecord.java",
+        "framework/**/HexDump.java",
+        "framework/**/NetworkStackConstants.java",
     ],
     sdk_version: "module_current",
     visibility: [
@@ -186,6 +188,33 @@
     },
 }
 
+// The net-utils-multicast-forwarding-structs library requires the callers to
+// contain net-utils-device-common-bpf.
+java_library {
+    name: "net-utils-multicast-forwarding-structs",
+    srcs: [
+        "device/com/android/net/module/util/structs/StructMf6cctl.java",
+        "device/com/android/net/module/util/structs/StructMif6ctl.java",
+        "device/com/android/net/module/util/structs/StructMrt6Msg.java",
+    ],
+    sdk_version: "module_current",
+    min_sdk_version: "30",
+    visibility: [
+        "//packages/modules/Connectivity:__subpackages__",
+    ],
+    libs: [
+        // Only Struct.java is needed from "net-utils-device-common-bpf"
+        "net-utils-device-common-bpf",
+    ],
+    apex_available: [
+        "com.android.tethering",
+    ],
+    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.
 java_library {
@@ -246,7 +275,7 @@
         "//apex_available:platform",
     ],
     lint: {
-        strict_updatability_linting: true,
+        baseline_filename: "lint-baseline.xml",
         error_checks: ["NewApi"],
     },
 }
@@ -270,13 +299,11 @@
         "//cts/tests/tests/net",
         "//cts/tests/tests/wifi",
         "//packages/modules/Connectivity/tests/cts/net",
-        "//frameworks/base/packages/Tethering",
         "//packages/modules/Connectivity/Tethering",
         "//frameworks/base/tests:__subpackages__",
         "//frameworks/opt/net/ike",
         "//frameworks/opt/telephony",
         "//frameworks/base/wifi:__subpackages__",
-        "//frameworks/base/packages/Connectivity:__subpackages__",
         "//packages/modules/Connectivity:__subpackages__",
         "//packages/modules/NetworkStack:__subpackages__",
         "//packages/modules/CaptivePortalLogin",
@@ -295,6 +322,10 @@
             "-Xep:NullablePrimitive:ERROR",
         ],
     },
+    apex_available: [
+        "//apex_available:platform",
+        "com.android.tethering",
+    ],
 }
 
 java_library {
@@ -459,10 +490,10 @@
 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",
+        "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",
diff --git a/staticlibs/client-libs/Android.bp b/staticlibs/client-libs/Android.bp
index c938dd6..f665584 100644
--- a/staticlibs/client-libs/Android.bp
+++ b/staticlibs/client-libs/Android.bp
@@ -1,4 +1,5 @@
 package {
+    default_team: "trendy_team_fwk_core_networking",
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
@@ -10,17 +11,17 @@
     apex_available: [
         "//apex_available:platform",
         "com.android.tethering",
-        "com.android.wifi"
+        "com.android.wifi",
     ],
     visibility: [
         "//packages/modules/Connectivity:__subpackages__",
         "//frameworks/base/services:__subpackages__",
         "//frameworks/base/packages:__subpackages__",
-        "//packages/modules/Wifi/service:__subpackages__"
+        "//packages/modules/Wifi/service:__subpackages__",
     ],
     libs: ["androidx.annotation_annotation"],
     static_libs: [
         "netd_aidl_interface-lateststable-java",
-        "netd_event_listener_interface-lateststable-java"
-    ]
+        "netd_event_listener_interface-lateststable-java",
+    ],
 }
diff --git a/staticlibs/client-libs/tests/unit/Android.bp b/staticlibs/client-libs/tests/unit/Android.bp
index 03e3e70..7aafd69 100644
--- a/staticlibs/client-libs/tests/unit/Android.bp
+++ b/staticlibs/client-libs/tests/unit/Android.bp
@@ -1,4 +1,5 @@
 package {
+    default_team: "trendy_team_fwk_core_networking",
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
@@ -26,7 +27,7 @@
         "//packages/modules/Connectivity/tests:__subpackages__",
         "//packages/modules/Connectivity/Tethering/tests:__subpackages__",
         "//packages/modules/NetworkStack/tests/integration",
-    ]
+    ],
 }
 
 android_test {
diff --git a/staticlibs/device/com/android/net/module/util/BpfBitmap.java b/staticlibs/device/com/android/net/module/util/BpfBitmap.java
index acb3ca5..b62a430 100644
--- a/staticlibs/device/com/android/net/module/util/BpfBitmap.java
+++ b/staticlibs/device/com/android/net/module/util/BpfBitmap.java
@@ -38,8 +38,7 @@
      * @param path The path of the BPF map.
      */
     public BpfBitmap(@NonNull String path) throws ErrnoException {
-        mBpfMap = new BpfMap<Struct.S32, Struct.S64>(path, BpfMap.BPF_F_RDWR,
-                Struct.S32.class, Struct.S64.class);
+        mBpfMap = new BpfMap<>(path, Struct.S32.class, Struct.S64.class);
     }
 
     /**
diff --git a/staticlibs/device/com/android/net/module/util/BpfMap.java b/staticlibs/device/com/android/net/module/util/BpfMap.java
index e3ef0f0..da77ae8 100644
--- a/staticlibs/device/com/android/net/module/util/BpfMap.java
+++ b/staticlibs/device/com/android/net/module/util/BpfMap.java
@@ -27,7 +27,6 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.RequiresApi;
 
-import java.io.IOException;
 import java.nio.ByteBuffer;
 import java.nio.ByteOrder;
 import java.util.NoSuchElementException;
@@ -110,6 +109,17 @@
     }
 
     /**
+     * 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)
      */
diff --git a/staticlibs/device/com/android/net/module/util/DeviceConfigUtils.java b/staticlibs/device/com/android/net/module/util/DeviceConfigUtils.java
index 42f26f4..5b7cbb8 100644
--- a/staticlibs/device/com/android/net/module/util/DeviceConfigUtils.java
+++ b/staticlibs/device/com/android/net/module/util/DeviceConfigUtils.java
@@ -64,9 +64,6 @@
     @VisibleForTesting
     public static final long DEFAULT_PACKAGE_VERSION = 1000;
 
-    private static final String CORE_NETWORKING_TRUNK_STABLE_NAMESPACE = "android_core_networking";
-    private static final String CORE_NETWORKING_TRUNK_STABLE_FLAG_PACKAGE = "com.android.net.flags";
-
     @VisibleForTesting
     public static void resetPackageVersionCacheForTest() {
         sPackageVersion = -1;
@@ -409,31 +406,4 @@
 
         return pkgs.get(0).activityInfo.applicationInfo.packageName;
     }
-
-    /**
-     * Check whether one specific trunk stable flag in android_core_networking namespace is enabled.
-     * This method reads trunk stable feature flag value from DeviceConfig directly since
-     * java_aconfig_library soong module is not available in the mainline branch.
-     * After the mainline branch support the aconfig soong module, this function must be removed and
-     * java_aconfig_library must be used instead to check if the feature is enabled.
-     *
-     * @param flagName The name of the trunk stable flag
-     * @return true if this feature is enabled, or false if disabled.
-     */
-    public static boolean isTrunkStableFeatureEnabled(final String flagName) {
-        return isTrunkStableFeatureEnabled(
-                CORE_NETWORKING_TRUNK_STABLE_NAMESPACE,
-                CORE_NETWORKING_TRUNK_STABLE_FLAG_PACKAGE,
-                flagName
-        );
-    }
-
-    private static boolean isTrunkStableFeatureEnabled(final String namespace,
-            final String packageName, final String flagName) {
-        return DeviceConfig.getBoolean(
-                namespace,
-                packageName + "." + flagName,
-                false /* defaultValue */
-        );
-    }
 }
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/Ipv6Utils.java b/staticlibs/device/com/android/net/module/util/Ipv6Utils.java
index d538221..497b8cb 100644
--- a/staticlibs/device/com/android/net/module/util/Ipv6Utils.java
+++ b/staticlibs/device/com/android/net/module/util/Ipv6Utils.java
@@ -166,6 +166,24 @@
     }
 
     /**
+     * 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,
@@ -176,11 +194,21 @@
     }
 
     /**
-     * Build an ICMPv6 Echo Reply packet without ethernet header.
+     * Build an ICMPv6 Echo Request packet from the required specified parameters without ethernet
+     * header.
      */
-    public static ByteBuffer buildEchoReplyPacket(final Inet6Address srcIp,
+    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/arp/ArpPacket.java b/staticlibs/device/com/android/net/module/util/arp/ArpPacket.java
index dab9694..bf447d3 100644
--- a/staticlibs/device/com/android/net/module/util/arp/ArpPacket.java
+++ b/staticlibs/device/com/android/net/module/util/arp/ArpPacket.java
@@ -45,14 +45,18 @@
 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(short opCode, MacAddress senderHwAddress, Inet4Address senderIp,
-            MacAddress targetHwAddress, Inet4Address targetIp) {
+    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;
@@ -145,7 +149,9 @@
             buffer.get(targetHwAddress);
             buffer.get(targetIp);
 
-            return new ArpPacket(opCode, MacAddress.fromBytes(senderHwAddress),
+            return new ArpPacket(MacAddress.fromBytes(l2dst),
+                    MacAddress.fromBytes(l2src), opCode,
+                    MacAddress.fromBytes(senderHwAddress),
                     (Inet4Address) InetAddress.getByAddress(senderIp),
                     MacAddress.fromBytes(targetHwAddress),
                     (Inet4Address) InetAddress.getByAddress(targetIp));
diff --git a/staticlibs/device/com/android/net/module/util/netlink/InetDiagMessage.java b/staticlibs/device/com/android/net/module/util/netlink/InetDiagMessage.java
index 4f76577..fecaa09 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/InetDiagMessage.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/InetDiagMessage.java
@@ -24,14 +24,14 @@
 import static android.system.OsConstants.IPPROTO_UDP;
 import static android.system.OsConstants.NETLINK_INET_DIAG;
 
-import static com.android.net.module.util.netlink.NetlinkConstants.NLMSG_DONE;
 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.hexify;
+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;
@@ -59,8 +59,11 @@
 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;
 
 /**
@@ -154,7 +157,8 @@
     }
 
     public StructInetDiagMsg inetDiagMsg;
-
+    // The netlink attributes.
+    public List<StructNlAttr> nlAttrs = new ArrayList<>();
     @VisibleForTesting
     public InetDiagMessage(@NonNull StructNlMsgHdr header) {
         super(header);
@@ -172,6 +176,19 @@
         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;
     }
 
@@ -265,7 +282,7 @@
         int uid = INVALID_UID;
         FileDescriptor fd = null;
         try {
-            fd = NetlinkUtils.netlinkSocketForProto(NETLINK_INET_DIAG);
+            fd = NetlinkUtils.netlinkSocketForProto(NETLINK_INET_DIAG, SOCKET_RECV_BUFSIZE);
             connectToKernel(fd);
             uid = lookupUid(protocol, local, remote, fd);
         } catch (ErrnoException | SocketException | IllegalArgumentException
@@ -307,9 +324,8 @@
         NetlinkUtils.receiveNetlinkAck(fd);
     }
 
-    private static void sendNetlinkDumpRequest(FileDescriptor fd, int proto, int states, int family)
-            throws InterruptedIOException, ErrnoException {
-        final byte[] dumpMsg = InetDiagMessage.inetDiagReqV2(
+    private static byte [] makeNetlinkDumpRequest(int proto, int states, int family) {
+        return InetDiagMessage.inetDiagReqV2(
                 proto,
                 null /* id */,
                 family,
@@ -318,51 +334,29 @@
                 0 /* pad */,
                 0 /* idiagExt */,
                 states);
-        NetlinkUtils.sendMessage(fd, dumpMsg, 0, dumpMsg.length, IO_TIMEOUT_MS);
     }
 
-    private static int processNetlinkDumpAndDestroySockets(FileDescriptor dumpFd,
+    private static int processNetlinkDumpAndDestroySockets(byte[] dumpReq,
             FileDescriptor destroyFd, int proto, Predicate<InetDiagMessage> filter)
-            throws InterruptedIOException, ErrnoException {
-        int destroyedSockets = 0;
-
-        while (true) {
-            final ByteBuffer buf = NetlinkUtils.recvMessage(
-                    dumpFd, DEFAULT_RECV_BUFSIZE, IO_TIMEOUT_MS);
-
-            while (buf.remaining() > 0) {
-                final int position = buf.position();
-                final NetlinkMessage nlMsg = NetlinkMessage.parse(buf, NETLINK_INET_DIAG);
-                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 destroyedSockets;
-                }
-
-                if (!(nlMsg instanceof InetDiagMessage)) {
-                    Log.wtf(TAG, "Received unexpected netlink message: " + nlMsg);
-                    continue;
-                }
-
-                final InetDiagMessage diagMsg = (InetDiagMessage) nlMsg;
-                if (filter.test(diagMsg)) {
-                    try {
-                        sendNetlinkDestroyRequest(destroyFd, proto, diagMsg);
-                        destroyedSockets++;
-                    } catch (InterruptedIOException | ErrnoException e) {
-                        if (!(e instanceof ErrnoException
-                                && ((ErrnoException) e).errno == ENOENT)) {
-                            Log.e(TAG, "Failed to destroy socket: diagMsg=" + diagMsg + ", " + e);
-                        }
+            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();
     }
 
     /**
@@ -420,31 +414,28 @@
 
     private static void destroySockets(int proto, int states, Predicate<InetDiagMessage> filter)
             throws ErrnoException, SocketException, InterruptedIOException {
-        FileDescriptor dumpFd = null;
         FileDescriptor destroyFd = null;
 
         try {
-            dumpFd = NetlinkUtils.createNetLinkInetDiagSocket();
             destroyFd = NetlinkUtils.createNetLinkInetDiagSocket();
-            connectToKernel(dumpFd);
             connectToKernel(destroyFd);
 
             for (int family : List.of(AF_INET, AF_INET6)) {
+                byte[] req = makeNetlinkDumpRequest(proto, states, family);
+
                 try {
-                    sendNetlinkDumpRequest(dumpFd, proto, states, family);
-                } catch (InterruptedIOException | ErrnoException e) {
-                    Log.e(TAG, "Failed to send netlink dump request: " + e);
-                    continue;
-                }
-                final int destroyedSockets = processNetlinkDumpAndDestroySockets(
-                        dumpFd, destroyFd, proto, filter);
-                Log.d(TAG, "Destroyed " + destroyedSockets + " sockets"
+                    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(dumpFd);
             closeSocketQuietly(destroyFd);
         }
     }
diff --git a/staticlibs/device/com/android/net/module/util/netlink/NduseroptMessage.java b/staticlibs/device/com/android/net/module/util/netlink/NduseroptMessage.java
index bdf574d..2e9a99b 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/NduseroptMessage.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/NduseroptMessage.java
@@ -20,6 +20,7 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
 
 import java.net.Inet6Address;
 import java.net.InetAddress;
@@ -63,6 +64,20 @@
     /** 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);
diff --git a/staticlibs/device/com/android/net/module/util/netlink/NetlinkConstants.java b/staticlibs/device/com/android/net/module/util/netlink/NetlinkConstants.java
index 44c51d8..ad7a4d7 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/NetlinkConstants.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/NetlinkConstants.java
@@ -151,6 +151,9 @@
     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;
diff --git a/staticlibs/device/com/android/net/module/util/netlink/NetlinkUtils.java b/staticlibs/device/com/android/net/module/util/netlink/NetlinkUtils.java
index f1f30d3..541a375 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/NetlinkUtils.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/NetlinkUtils.java
@@ -30,6 +30,12 @@
 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;
@@ -44,10 +50,14 @@
 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.
@@ -76,6 +86,7 @@
 
     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
@@ -150,7 +161,7 @@
      */
     public static void sendOneShotKernelMessage(int nlProto, byte[] msg) throws ErrnoException {
         final String errPrefix = "Error in NetlinkSocket.sendOneShotKernelMessage";
-        final FileDescriptor fd = netlinkSocketForProto(nlProto);
+        final FileDescriptor fd = netlinkSocketForProto(nlProto, SOCKET_RECV_BUFSIZE);
 
         try {
             connectToKernel(fd);
@@ -163,28 +174,24 @@
             Log.e(TAG, errPrefix, e);
             throw new ErrnoException(errPrefix, EIO, e);
         } finally {
-            try {
-                SocketUtils.closeSocket(fd);
-            } catch (IOException e) {
-                // Nothing we can do here
-            }
+            closeSocketQuietly(fd);
         }
     }
 
     /**
-     * Send an RTM_NEWADDR message to kernel to add or update an IPv6 address.
+     * Send an RTM_NEWADDR message to kernel to add or update an IP address.
      *
      * @param ifIndex interface index.
-     * @param ip IPv6 address to be added.
-     * @param prefixlen IPv6 address prefix length.
-     * @param flags IPv6 address flags.
-     * @param scope IPv6 address scope.
-     * @param preferred The preferred lifetime of IPv6 address.
-     * @param valid The valid lifetime of IPv6 address.
+     * @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 Inet6Address ip,
+    public static boolean sendRtmNewAddressRequest(int ifIndex, @NonNull final InetAddress ip,
             short prefixlen, int flags, byte scope, long preferred, long valid) {
-        Objects.requireNonNull(ip, "IPv6 address to be added should not be null.");
+        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 {
@@ -218,22 +225,41 @@
     }
 
     /**
-     * Create netlink socket with the given netlink protocol type.
+     * 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) throws ErrnoException {
-        final FileDescriptor fd = Os.socket(AF_NETLINK, SOCK_DGRAM, nlProto);
-        Os.setsockoptInt(fd, SOL_SOCKET, SO_RCVBUF, SOCKET_RECV_BUFSIZE);
+    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 Os.socket(AF_NETLINK, SOCK_DGRAM | SOCK_CLOEXEC, NETLINK_INET_DIAG);
+        return netlinkSocketForProto(NETLINK_INET_DIAG);
     }
 
     /**
@@ -308,4 +334,139 @@
     }
 
     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
index cbe0ab0..4846df7 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkAddressMessage.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkAddressMessage.java
@@ -16,6 +16,7 @@
 
 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;
@@ -28,6 +29,7 @@
 
 import com.android.net.module.util.HexDump;
 
+import java.net.Inet4Address;
 import java.net.Inet6Address;
 import java.net.InetAddress;
 import java.nio.ByteBuffer;
@@ -48,6 +50,8 @@
  */
 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;
 
@@ -71,6 +75,7 @@
         mIfacacheInfo = structIfacacheInfo;
         mFlags = flags;
     }
+
     private RtNetlinkAddressMessage(@NonNull StructNlMsgHdr header) {
         this(header, null, null, null, 0);
     }
@@ -158,6 +163,24 @@
         // 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);
+        }
     }
 
     /**
@@ -184,7 +207,7 @@
                 0 /* tstamp */);
         msg.mFlags = flags;
 
-        final byte[] bytes = new byte[msg.getRequiredSpace()];
+        final byte[] bytes = new byte[msg.getRequiredSpace(family)];
         nlmsghdr.nlmsg_len = bytes.length;
         final ByteBuffer byteBuffer = ByteBuffer.wrap(bytes);
         byteBuffer.order(ByteOrder.nativeOrder());
@@ -237,7 +260,7 @@
     // 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() {
+    private int getRequiredSpace(int family) {
         int spaceRequired = StructNlMsgHdr.STRUCT_SIZE + StructIfaddrMsg.STRUCT_SIZE;
         // IFA_ADDRESS attr
         spaceRequired += NetlinkConstants.alignedLengthOf(
@@ -247,6 +270,14 @@
                 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;
     }
 
diff --git a/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkRouteMessage.java b/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkRouteMessage.java
index 9acac69..545afea 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkRouteMessage.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkRouteMessage.java
@@ -21,9 +21,11 @@
 
 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;
@@ -49,31 +51,78 @@
  */
 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;
 
-    private int mIfindex;
+    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;
-    @NonNull
-    private IpPrefix mDestination;
+    @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
 
-    private RtNetlinkRouteMessage(StructNlMsgHdr header) {
+
+    @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 = null;
-        mDestination = null;
-        mGateway = null;
-        mIfindex = 0;
-        mRtaCacheInfo = null;
+        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 mIfindex;
+        return mOifIndex;
     }
 
     @NonNull
@@ -86,6 +135,14 @@
         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;
@@ -97,6 +154,18 @@
     }
 
     /**
+     * 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.
      *
@@ -107,7 +176,8 @@
     private static boolean matchRouteAddressFamily(@NonNull final InetAddress address,
             int family) {
         return ((address instanceof Inet4Address) && (family == AF_INET))
-                || ((address instanceof Inet6Address) && (family == AF_INET6));
+                || ((address instanceof Inet6Address) &&
+                        (family == AF_INET6 || family == RTNL_FAMILY_IP6MR));
     }
 
     /**
@@ -121,11 +191,11 @@
     @Nullable
     public static RtNetlinkRouteMessage parse(@NonNull final StructNlMsgHdr header,
             @NonNull final ByteBuffer byteBuffer) {
-        final RtNetlinkRouteMessage routeMsg = new RtNetlinkRouteMessage(header);
-
-        routeMsg.mRtmsg = StructRtMsg.parse(byteBuffer);
-        if (routeMsg.mRtmsg == null) return null;
+        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();
@@ -139,12 +209,24 @@
             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) {
+        } 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);
@@ -156,6 +238,17 @@
             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);
@@ -164,7 +257,7 @@
             // 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.mIfindex = nlAttr.getValueAsInt(0 /* 0 isn't a valid ifindex */);
+            routeMsg.mOifIndex = nlAttr.getValueAsInt(0 /* 0 isn't a valid ifindex */);
         }
 
         // RTA_CACHEINFO
@@ -174,33 +267,59 @@
             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}.
      */
-    @VisibleForTesting
-    protected void pack(ByteBuffer byteBuffer) {
+    public void pack(ByteBuffer byteBuffer) {
         getHeader().pack(byteBuffer);
         mRtmsg.pack(byteBuffer);
 
-        final StructNlAttr destination = new StructNlAttr(RTA_DST, mDestination.getAddress());
-        destination.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 (mIfindex != 0) {
-            final StructNlAttr ifindex = new StructNlAttr(RTA_OIF, mIfindex);
-            ifindex.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
@@ -208,10 +327,14 @@
         return "RtNetlinkRouteMessage{ "
                 + "nlmsghdr{" + mHeader.toString(OsConstants.NETLINK_ROUTE) + "}, "
                 + "Rtmsg{" + mRtmsg.toString() + "}, "
-                + "destination{" + mDestination.getAddress().getHostAddress() + "}, "
+                + (mSource == null ? "" : "source{" + mSource.getAddress().getHostAddress() + "}, ")
+                + (mDestination == null ?
+                        "" : "destination{" + mDestination.getAddress().getHostAddress() + "}, ")
                 + "gateway{" + (mGateway == null ? "" : mGateway.getHostAddress()) + "}, "
-                + "ifindex{" + mIfindex + "}, "
+                + (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/StructNlAttr.java b/staticlibs/device/com/android/net/module/util/netlink/StructNlAttr.java
index a9b6495..43e8312 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/StructNlAttr.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/StructNlAttr.java
@@ -21,6 +21,8 @@
 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;
@@ -152,12 +154,12 @@
         nla_type = type;
         setValue(new byte[Short.BYTES]);
         final ByteBuffer buf = getValueAsByteBuffer();
-        final ByteOrder originalOrder = buf.order();
+        // ByteBuffer returned by getValueAsByteBuffer is always in native byte order.
         try {
             buf.order(order);
             buf.putShort(value);
         } finally {
-            buf.order(originalOrder);
+            buf.order(nativeOrder());
         }
     }
 
@@ -169,12 +171,29 @@
         nla_type = type;
         setValue(new byte[Integer.BYTES]);
         final ByteBuffer buf = getValueAsByteBuffer();
-        final ByteOrder originalOrder = buf.order();
+        // ByteBuffer returned by getValueAsByteBuffer is always in native byte order.
         try {
             buf.order(order);
             buf.putInt(value);
         } finally {
-            buf.order(originalOrder);
+            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());
         }
     }
 
@@ -288,6 +307,7 @@
 
     /**
      * 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();
@@ -298,6 +318,18 @@
     }
 
     /**
+     * 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) {
diff --git a/staticlibs/device/com/android/net/module/util/netlink/StructRtMsg.java b/staticlibs/device/com/android/net/module/util/netlink/StructRtMsg.java
index 3cd7292..6d9318c 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/StructRtMsg.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/StructRtMsg.java
@@ -18,6 +18,7 @@
 
 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;
@@ -57,8 +58,9 @@
     @Field(order = 8, type = Type.U32)
     public final long flags;
 
-    StructRtMsg(short family, short dstLen, short srcLen, short tos, short table, short protocol,
-            short scope, short type, 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;
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/framework/com/android/net/module/util/LocationPermissionChecker.java b/staticlibs/framework/com/android/net/module/util/LocationPermissionChecker.java
index cd1f31c..f6bee69 100644
--- a/staticlibs/framework/com/android/net/module/util/LocationPermissionChecker.java
+++ b/staticlibs/framework/com/android/net/module/util/LocationPermissionChecker.java
@@ -189,8 +189,9 @@
      * @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(String pkgName, @Nullable String featureId,
-            int uid, boolean coarseForTargetSdkLessThanQ, @Nullable String message) {
+    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);
 
diff --git a/staticlibs/framework/com/android/net/module/util/PermissionUtils.java b/staticlibs/framework/com/android/net/module/util/PermissionUtils.java
index 8315b8f..0d7d96f 100644
--- a/staticlibs/framework/com/android/net/module/util/PermissionUtils.java
+++ b/staticlibs/framework/com/android/net/module/util/PermissionUtils.java
@@ -23,11 +23,14 @@
 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;
@@ -43,8 +46,9 @@
     /**
      * Return true if the context has one of given permission.
      */
-    public static boolean checkAnyPermissionOf(@NonNull Context context,
-            @NonNull String... permissions) {
+    @CheckResult
+    public static boolean hasAnyPermissionOf(@NonNull Context context,
+                                             @NonNull String... permissions) {
         for (String permission : permissions) {
             if (context.checkCallingOrSelfPermission(permission) == PERMISSION_GRANTED) {
                 return true;
@@ -54,11 +58,12 @@
     }
 
     /**
-     * Return true if the context has one of give permission that is allowed
+     * Return true if the context has one of given permission that is allowed
      * for a particular process and user ID running in the system.
      */
-    public static boolean checkAnyPermissionOf(@NonNull Context context,
-            int pid, int uid, @NonNull String... permissions) {
+    @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;
@@ -72,7 +77,7 @@
      */
     public static void enforceAnyPermissionOf(@NonNull Context context,
             @NonNull String... permissions) {
-        if (!checkAnyPermissionOf(context, permissions)) {
+        if (!hasAnyPermissionOf(context, permissions)) {
             throw new SecurityException("Requires one of the following permissions: "
                     + String.join(", ", permissions) + ".");
         }
@@ -131,7 +136,8 @@
     /**
      * Return true if the context has DUMP permission.
      */
-    public static boolean checkDumpPermission(Context context, String tag, PrintWriter pw) {
+    @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="
@@ -183,4 +189,33 @@
         }
         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/lint-baseline.xml b/staticlibs/lint-baseline.xml
new file mode 100644
index 0000000..2ee3a43
--- /dev/null
+++ b/staticlibs/lint-baseline.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<issues format="6" by="lint 8.4.0-alpha04" type="baseline" client="" dependencies="true" name="" variant="all" version="8.4.0-alpha04">
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `makeNetlinkSocketAddress`"
+        errorLine1="            Os.bind(fd, makeNetlinkSocketAddress(0, mBindGroups));"
+        errorLine2="                        ~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/staticlibs/device/com/android/net/module/util/ip/NetlinkMonitor.java"
+            line="111"
+            column="25"/>
+    </issue>
+
+</issues>
diff --git a/staticlibs/native/bpf_headers/Android.bp b/staticlibs/native/bpf_headers/Android.bp
index 41184ea..d55584a 100644
--- a/staticlibs/native/bpf_headers/Android.bp
+++ b/staticlibs/native/bpf_headers/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/staticlibs/native/bpf_headers/include/bpf/BpfMap.h b/staticlibs/native/bpf_headers/include/bpf/BpfMap.h
index 3fede3c..1037beb 100644
--- a/staticlibs/native/bpf_headers/include/bpf/BpfMap.h
+++ b/staticlibs/native/bpf_headers/include/bpf/BpfMap.h
@@ -26,6 +26,8 @@
 #include "BpfSyscallWrappers.h"
 #include "bpf/BpfUtils.h"
 
+#include <functional>
+
 namespace android {
 namespace bpf {
 
diff --git a/staticlibs/native/bpf_syscall_wrappers/Android.bp b/staticlibs/native/bpf_syscall_wrappers/Android.bp
index b3efc21..1e0cb22 100644
--- a/staticlibs/native/bpf_syscall_wrappers/Android.bp
+++ b/staticlibs/native/bpf_syscall_wrappers/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/staticlibs/native/bpfmapjni/Android.bp b/staticlibs/native/bpfmapjni/Android.bp
index 8babcce..7e6b4ec 100644
--- a/staticlibs/native/bpfmapjni/Android.bp
+++ b/staticlibs/native/bpfmapjni/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/staticlibs/native/bpfutiljni/Android.bp b/staticlibs/native/bpfutiljni/Android.bp
index 39a2795..1ef01a6 100644
--- a/staticlibs/native/bpfutiljni/Android.bp
+++ b/staticlibs/native/bpfutiljni/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/staticlibs/native/ip_checksum/Android.bp b/staticlibs/native/ip_checksum/Android.bp
index 9878d73..e2e118e 100644
--- a/staticlibs/native/ip_checksum/Android.bp
+++ b/staticlibs/native/ip_checksum/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/staticlibs/native/netjniutils/Android.bp b/staticlibs/native/netjniutils/Android.bp
index ca3bbbc..4cab459 100644
--- a/staticlibs/native/netjniutils/Android.bp
+++ b/staticlibs/native/netjniutils/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/staticlibs/native/nettestutils/Android.bp b/staticlibs/native/nettestutils/Android.bp
index df3bb42..ef87f04 100644
--- a/staticlibs/native/nettestutils/Android.bp
+++ b/staticlibs/native/nettestutils/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/staticlibs/native/tcutils/Android.bp b/staticlibs/native/tcutils/Android.bp
index 9a38745..926590d 100644
--- a/staticlibs/native/tcutils/Android.bp
+++ b/staticlibs/native/tcutils/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/staticlibs/netd/Android.bp b/staticlibs/netd/Android.bp
index 637a938..59ef20d 100644
--- a/staticlibs/netd/Android.bp
+++ b/staticlibs/netd/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"],
 }
 
@@ -241,5 +242,17 @@
             min_sdk_version: "30",
         },
     },
-    versions: ["1"],
+    versions_with_info: [
+        {
+            version: "1",
+            imports: [],
+        },
+        {
+            version: "2",
+            imports: [],
+        },
+
+    ],
+    frozen: true,
+
 }
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/IMDns.aidl b/staticlibs/netd/aidl_api/mdns_aidl_interface/current/android/net/mdns/aidl/IMDns.aidl
index ecbe966..d84742b 100644
--- 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
@@ -34,13 +34,40 @@
 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
index 4625cac..187a3d2 100644
--- 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
@@ -34,9 +34,21 @@
 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;
diff --git a/staticlibs/netd/binder/android/net/mdns/aidl/IMDns.aidl b/staticlibs/netd/binder/android/net/mdns/aidl/IMDns.aidl
index 255d70f..3bf1da8 100644
--- a/staticlibs/netd/binder/android/net/mdns/aidl/IMDns.aidl
+++ b/staticlibs/netd/binder/android/net/mdns/aidl/IMDns.aidl
@@ -28,6 +28,8 @@
      * 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();
 
@@ -35,6 +37,8 @@
      * 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();
 
@@ -49,6 +53,8 @@
      * @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);
 
@@ -63,6 +69,8 @@
      * @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);
 
@@ -77,6 +85,8 @@
      * @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);
 
@@ -92,6 +102,8 @@
      * @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);
 
@@ -101,6 +113,8 @@
      * @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);
 
@@ -112,6 +126,8 @@
      * @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);
 
@@ -121,6 +137,8 @@
      * @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
index a202a26..f7f028b 100644
--- a/staticlibs/netd/binder/android/net/mdns/aidl/IMDnsEventListener.aidl
+++ b/staticlibs/netd/binder/android/net/mdns/aidl/IMDnsEventListener.aidl
@@ -31,8 +31,8 @@
 oneway interface IMDnsEventListener {
     /**
      * Types for MDNS operation result.
-     * These are in sync with frameworks/libs/net/common/netd/libnetdutils/include/netdutils/\
-     * ResponseCode.h
+     * 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;
@@ -46,21 +46,29 @@
 
     /**
      * 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/libnetdutils/Android.bp b/staticlibs/netd/libnetdutils/Android.bp
index fdb9380..2ae5911 100644
--- a/staticlibs/netd/libnetdutils/Android.bp
+++ b/staticlibs/netd/libnetdutils/Android.bp
@@ -1,4 +1,5 @@
 package {
+    default_team: "trendy_team_fwk_core_networking",
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
@@ -22,7 +23,10 @@
         "Utils.cpp",
     ],
     defaults: ["netd_defaults"],
-    cflags: ["-Wall", "-Werror"],
+    cflags: [
+        "-Wall",
+        "-Werror",
+    ],
     shared_libs: [
         "libbase",
         "liblog",
diff --git a/staticlibs/netd/libnetdutils/include/netdutils/InternetAddresses.h b/staticlibs/netd/libnetdutils/include/netdutils/InternetAddresses.h
index d10cec7..d662739 100644
--- a/staticlibs/netd/libnetdutils/include/netdutils/InternetAddresses.h
+++ b/staticlibs/netd/libnetdutils/include/netdutils/InternetAddresses.h
@@ -21,6 +21,7 @@
 #include <stdint.h>
 #include <cstring>
 #include <limits>
+#include <memory>
 #include <string>
 
 #include "netdutils/NetworkConstants.h"
diff --git a/staticlibs/netd/libnetdutils/include/netdutils/Log.h b/staticlibs/netd/libnetdutils/include/netdutils/Log.h
index 77ae649..d266cbc 100644
--- a/staticlibs/netd/libnetdutils/include/netdutils/Log.h
+++ b/staticlibs/netd/libnetdutils/include/netdutils/Log.h
@@ -19,6 +19,7 @@
 
 #include <chrono>
 #include <deque>
+#include <functional>
 #include <shared_mutex>
 #include <string>
 #include <type_traits>
diff --git a/staticlibs/netd/libnetdutils/include/netdutils/Slice.h b/staticlibs/netd/libnetdutils/include/netdutils/Slice.h
index 717fbd1..aa12927 100644
--- a/staticlibs/netd/libnetdutils/include/netdutils/Slice.h
+++ b/staticlibs/netd/libnetdutils/include/netdutils/Slice.h
@@ -22,6 +22,7 @@
 #include <cstring>
 #include <ostream>
 #include <tuple>
+#include <type_traits>
 #include <vector>
 
 namespace android {
diff --git a/staticlibs/tests/unit/Android.bp b/staticlibs/tests/unit/Android.bp
index 0dfca57..4c226cc 100644
--- a/staticlibs/tests/unit/Android.bp
+++ b/staticlibs/tests/unit/Android.bp
@@ -3,12 +3,16 @@
 //########################################################################
 
 package {
+    default_team: "trendy_team_fwk_core_networking",
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
 android_library {
     name: "NetworkStaticLibTestsLib",
-    srcs: ["src/**/*.java","src/**/*.kt"],
+    srcs: [
+        "src/**/*.java",
+        "src/**/*.kt",
+    ],
     min_sdk_version: "30",
     defaults: ["framework-connectivity-test-defaults"],
     static_libs: [
@@ -34,8 +38,7 @@
         "//packages/modules/NetworkStack/tests/integration",
     ],
     lint: {
-        strict_updatability_linting: true,
-        test: true
+        test: true,
     },
 }
 
@@ -52,5 +55,4 @@
     ],
     jarjar_rules: "jarjar-rules.txt",
     test_suites: ["device-tests"],
-    lint: { strict_updatability_linting: true },
 }
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
index e25d554..29e84c9 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/ArpPacketTest.java
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/ArpPacketTest.java
@@ -50,6 +50,8 @@
             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,
@@ -163,6 +165,8 @@
     @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));
@@ -174,6 +178,8 @@
     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));
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/DeviceConfigUtilsTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/DeviceConfigUtilsTest.java
index 06b3e2f..f32337d 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/DeviceConfigUtilsTest.java
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/DeviceConfigUtilsTest.java
@@ -71,10 +71,6 @@
 public class DeviceConfigUtilsTest {
     private static final String TEST_NAME_SPACE = "connectivity";
     private static final String TEST_EXPERIMENT_FLAG = "experiment_flag";
-    private static final String CORE_NETWORKING_TRUNK_STABLE_NAMESPACE = "android_core_networking";
-    private static final String TEST_TRUNK_STABLE_FLAG = "trunk_stable_feature";
-    private static final String TEST_CORE_NETWORKING_TRUNK_STABLE_FLAG_PROPERTY =
-            "com.android.net.flags.trunk_stable_feature";
     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;
@@ -507,25 +503,4 @@
         verify(mContext, never()).getPackageName();
         verify(mPm, never()).getPackageInfo(anyString(), anyInt());
     }
-
-    @Test
-    public void testIsCoreNetworkingTrunkStableFeatureEnabled() {
-        doReturn(null).when(() -> DeviceConfig.getProperty(
-                CORE_NETWORKING_TRUNK_STABLE_NAMESPACE,
-                TEST_CORE_NETWORKING_TRUNK_STABLE_FLAG_PROPERTY));
-        assertFalse(DeviceConfigUtils.isTrunkStableFeatureEnabled(
-                TEST_TRUNK_STABLE_FLAG));
-
-        doReturn("false").when(() -> DeviceConfig.getProperty(
-                CORE_NETWORKING_TRUNK_STABLE_NAMESPACE,
-                TEST_CORE_NETWORKING_TRUNK_STABLE_FLAG_PROPERTY));
-        assertFalse(DeviceConfigUtils.isTrunkStableFeatureEnabled(
-                TEST_TRUNK_STABLE_FLAG));
-
-        doReturn("true").when(() -> DeviceConfig.getProperty(
-                CORE_NETWORKING_TRUNK_STABLE_NAMESPACE,
-                TEST_CORE_NETWORKING_TRUNK_STABLE_FLAG_PROPERTY));
-        assertTrue(DeviceConfigUtils.isTrunkStableFeatureEnabled(
-                TEST_TRUNK_STABLE_FLAG));
-    }
 }
diff --git a/tests/unit/java/com/android/server/HandlerUtilsTest.kt b/staticlibs/tests/unit/src/com/android/net/module/util/HandlerUtilsTest.kt
similarity index 90%
rename from tests/unit/java/com/android/server/HandlerUtilsTest.kt
rename to staticlibs/tests/unit/src/com/android/net/module/util/HandlerUtilsTest.kt
index 62bb651..f2c902f 100644
--- a/tests/unit/java/com/android/server/HandlerUtilsTest.kt
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/HandlerUtilsTest.kt
@@ -14,11 +14,11 @@
  * limitations under the License.
  */
 
-package com.android.server
+package com.android.net.module.util
 
 import android.os.HandlerThread
-import com.android.server.connectivity.HandlerUtils
 import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.DevSdkIgnoreRunner.MonitorThreadLeak
 import kotlin.test.assertEquals
 import kotlin.test.assertTrue
 import org.junit.After
@@ -27,6 +27,8 @@
 
 const val THREAD_BLOCK_TIMEOUT_MS = 1000L
 const val TEST_REPEAT_COUNT = 100
+
+@MonitorThreadLeak
 @RunWith(DevSdkIgnoreRunner::class)
 class HandlerUtilsTest {
     val handlerThread = HandlerThread("HandlerUtilsTestHandlerThread").also {
@@ -39,7 +41,7 @@
         // 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.runWithScissors(handler, {
+            HandlerUtils.runWithScissorsForDump(handler, {
                 assertEquals(Thread.currentThread(), handlerThread)
                 result = true
             }, THREAD_BLOCK_TIMEOUT_MS)
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
index c5a91a4..8586e82 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/PermissionUtilsTest.kt
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/PermissionUtilsTest.kt
@@ -19,16 +19,18 @@
 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.checkAnyPermissionOf
 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
@@ -42,7 +44,10 @@
 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 */
@@ -53,6 +58,9 @@
     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)
 
@@ -61,6 +69,7 @@
     @Before
     fun setup() {
         doReturn(mockPackageManager).`when`(mockContext).packageManager
+        doReturn(mockContext).`when`(mockContext).createContextAsUser(any(), anyInt())
     }
 
     @Test
@@ -69,18 +78,18 @@
             .checkCallingOrSelfPermission(TEST_PERMISSION1)
         doReturn(PERMISSION_DENIED).`when`(mockContext)
             .checkCallingOrSelfPermission(TEST_PERMISSION2)
-        assertTrue(checkAnyPermissionOf(mockContext, TEST_PERMISSION1, 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(checkAnyPermissionOf(mockContext, TEST_PERMISSION1, TEST_PERMISSION2))
+        assertTrue(hasAnyPermissionOf(mockContext, TEST_PERMISSION1, TEST_PERMISSION2))
         enforceAnyPermissionOf(mockContext, TEST_PERMISSION1, TEST_PERMISSION2)
 
         doReturn(PERMISSION_DENIED).`when`(mockContext).checkCallingOrSelfPermission(any())
-        assertFalse(checkAnyPermissionOf(mockContext, TEST_PERMISSION1, TEST_PERMISSION2))
+        assertFalse(hasAnyPermissionOf(mockContext, TEST_PERMISSION1, TEST_PERMISSION2))
         assertFailsWith<SecurityException>("Expect fail but permission granted.") {
             enforceAnyPermissionOf(mockContext, TEST_PERMISSION1, TEST_PERMISSION2)
         }
@@ -141,4 +150,24 @@
             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/netlink/InetDiagSocketTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/InetDiagSocketTest.java
index 65e99f8..b44e428 100644
--- 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
@@ -32,6 +32,7 @@
 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;
 
@@ -345,28 +346,28 @@
     // 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
+            "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
+            + "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
+                + "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);
@@ -394,33 +395,45 @@
         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
-            "58000000" +     // length = 88
-            "1400" +         // type = SOCK_DIAG_BY_FAMILY
-            "0200" +         // flags = NLM_F_MULTI
-            "00000000" +     // seqno
-            "f5220000" +     // pid
+            "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
+            + "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
+                + "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);
@@ -448,6 +461,104 @@
         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 =
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
index 5a231fc..f64adb8 100644
--- 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
@@ -21,7 +21,9 @@
 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;
@@ -33,6 +35,8 @@
 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;
@@ -43,6 +47,7 @@
 
 import com.android.modules.utils.build.SdkLevel;
 import com.android.net.module.util.Struct;
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
 
 import libcore.io.IoUtils;
 
@@ -55,6 +60,9 @@
 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
@@ -65,19 +73,14 @@
 
     @Test
     public void testGetNeighborsQuery() throws Exception {
-        final FileDescriptor fd = NetlinkUtils.netlinkSocketForProto(NETLINK_ROUTE);
-        assertNotNull(fd);
-
-        NetlinkUtils.connectToKernel(fd);
-
-        final NetlinkSocketAddress localAddr = (NetlinkSocketAddress) Os.getsockname(fd);
-        assertNotNull(localAddr);
-        assertEquals(0, localAddr.getGroupsMask());
-        assertTrue(0 != localAddr.getPortId());
-
         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()
@@ -94,7 +97,8 @@
             assumeFalse("network_stack context is expected to have permission to send RTM_GETNEIGH",
                     ctxt.startsWith("u:r:network_stack:s0"));
             try {
-                NetlinkUtils.sendMessage(fd, req, 0, req.length, TEST_TIMEOUT_MS);
+                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) {
@@ -105,106 +109,70 @@
         }
 
         // Check that apps targeting lower API levels / running on older platforms succeed
-        assertEquals(req.length,
-                NetlinkUtils.sendMessage(fd, req, 0, req.length, TEST_TIMEOUT_MS));
+        NetlinkUtils.<RtNetlinkNeighborMessage>getAndProcessNetlinkDumpMessages(req,
+                NETLINK_ROUTE, RtNetlinkNeighborMessage.class, handleNlDumpMsg);
 
-        int neighMessageCount = 0;
-        int doneMessageCount = 0;
-
-        while (doneMessageCount == 0) {
-            ByteBuffer response =
-                    NetlinkUtils.recvMessage(fd, DEFAULT_RECV_BUFSIZE, TEST_TIMEOUT_MS);
-            assertNotNull(response);
-            assertTrue(StructNlMsgHdr.STRUCT_SIZE <= response.limit());
-            assertEquals(0, response.position());
-            assertEquals(ByteOrder.nativeOrder(), response.order());
-
-            // Verify the messages at least appears minimally reasonable.
-            while (response.remaining() > 0) {
-                final NetlinkMessage msg = NetlinkMessage.parse(response, NETLINK_ROUTE);
-                assertNotNull(msg);
-                final StructNlMsgHdr hdr = msg.getHeader();
-                assertNotNull(hdr);
-
-                if (hdr.nlmsg_type == NetlinkConstants.NLMSG_DONE) {
-                    doneMessageCount++;
-                    continue;
-                }
-
-                assertEquals(NetlinkConstants.RTM_NEWNEIGH, hdr.nlmsg_type);
-                assertTrue(msg instanceof RtNetlinkNeighborMessage);
-                assertTrue((hdr.nlmsg_flags & StructNlMsgHdr.NLM_F_MULTI) != 0);
-                assertEquals(TEST_SEQNO, hdr.nlmsg_seq);
-                assertEquals(localAddr.getPortId(), hdr.nlmsg_pid);
-
-                neighMessageCount++;
-            }
+        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);
         }
 
-        assertEquals(1, doneMessageCount);
         // TODO: make sure this test passes sanely in airplane mode.
-        assertTrue(neighMessageCount > 0);
-
-        IoUtils.closeQuietly(fd);
+        assertTrue(msgs.size() > 0);
     }
 
     @Test
     public void testBasicWorkingGetAddrQuery() throws Exception {
-        final FileDescriptor fd = NetlinkUtils.netlinkSocketForProto(NETLINK_ROUTE);
-        assertNotNull(fd);
-
-        NetlinkUtils.connectToKernel(fd);
-
-        final NetlinkSocketAddress localAddr = (NetlinkSocketAddress) Os.getsockname(fd);
-        assertNotNull(localAddr);
-        assertEquals(0, localAddr.getGroupsMask());
-        assertTrue(0 != localAddr.getPortId());
-
         final int testSeqno = 8;
         final byte[] req = newGetAddrRequest(testSeqno);
         assertNotNull(req);
 
-        final long timeout = 500;
-        assertEquals(req.length, NetlinkUtils.sendMessage(fd, req, 0, req.length, timeout));
+        List<RtNetlinkAddressMessage> msgs = new ArrayList<>();
+        Consumer<RtNetlinkAddressMessage> handleNlDumpMsg = (msg) -> {
+            msgs.add(msg);
+        };
+        NetlinkUtils.<RtNetlinkAddressMessage>getAndProcessNetlinkDumpMessages(req, NETLINK_ROUTE,
+                RtNetlinkAddressMessage.class, handleNlDumpMsg);
 
-        int addrMessageCount = 0;
+        boolean ipv4LoopbackAddressFound = false;
+        boolean ipv6LoopbackAddressFound = false;
+        final InetAddress loopbackIpv4 = InetAddress.getByName("127.0.0.1");
+        final InetAddress loopbackIpv6 = InetAddress.getByName("::1");
 
-        while (true) {
-            ByteBuffer response = NetlinkUtils.recvMessage(fd, DEFAULT_RECV_BUFSIZE, timeout);
-            assertNotNull(response);
-            assertTrue(StructNlMsgHdr.STRUCT_SIZE <= response.limit());
-            assertEquals(0, response.position());
-            assertEquals(ByteOrder.nativeOrder(), response.order());
-
-            final NetlinkMessage msg = NetlinkMessage.parse(response, NETLINK_ROUTE);
+        for (var msg : msgs) {
             assertNotNull(msg);
             final StructNlMsgHdr nlmsghdr = msg.getHeader();
             assertNotNull(nlmsghdr);
-
-            if (nlmsghdr.nlmsg_type == NetlinkConstants.NLMSG_DONE) {
-                break;
-            }
-
             assertEquals(NetlinkConstants.RTM_NEWADDR, nlmsghdr.nlmsg_type);
             assertTrue((nlmsghdr.nlmsg_flags & StructNlMsgHdr.NLM_F_MULTI) != 0);
             assertEquals(testSeqno, nlmsghdr.nlmsg_seq);
-            assertEquals(localAddr.getPortId(), nlmsghdr.nlmsg_pid);
             assertTrue(msg instanceof RtNetlinkAddressMessage);
-            addrMessageCount++;
-
-            // From the query response we can see the RTM_NEWADDR messages representing for IPv4
-            // and IPv6 loopback address: 127.0.0.1 and ::1.
+            // 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);
-            assertTrue(ipAddress.isLoopbackAddress());
+            assertNotNull(ipAddress);
+
+            if (ipAddress.equals(loopbackIpv4)) {
+                ipv4LoopbackAddressFound = true;
+                assertTrue(ipAddress.isLoopbackAddress());
+            }
+            if (ipAddress.equals(loopbackIpv6)) {
+                ipv6LoopbackAddressFound = true;
+                assertTrue(ipAddress.isLoopbackAddress());
+            }
         }
 
-        assertTrue(addrMessageCount > 0);
-
-        IoUtils.closeQuietly(fd);
+        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. */
@@ -228,4 +196,36 @@
 
         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
index 01126d2..1d08525 100644
--- 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
@@ -42,6 +42,7 @@
 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;
@@ -179,6 +180,57 @@
     }
 
     @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 =
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
index 9881653..50b8278 100644
--- 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
@@ -16,7 +16,9 @@
 
 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;
@@ -38,6 +40,7 @@
 import java.net.Inet6Address;
 import java.nio.ByteBuffer;
 import java.nio.ByteOrder;
+import java.util.Arrays;
 
 @RunWith(AndroidJUnit4.class)
 @SmallTest
@@ -127,6 +130,72 @@
         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
@@ -220,10 +289,79 @@
                 + "scope: 0, type: 1, flags: 0}, "
                 + "destination{2001:db8:1::}, "
                 + "gateway{fe80::1}, "
-                + "ifindex{735}, "
+                + "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/StructNlAttrTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/StructNlAttrTest.java
index af3fac2..4c3fde6 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/StructNlAttrTest.java
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/StructNlAttrTest.java
@@ -92,4 +92,26 @@
         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/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/testutils/Android.bp b/staticlibs/testutils/Android.bp
index a5e5afb..a8e5a69 100644
--- a/staticlibs/testutils/Android.bp
+++ b/staticlibs/testutils/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,11 +25,11 @@
     ],
     defaults: [
         "framework-connectivity-test-defaults",
-        "lib_mockito_extended"
+        "lib_mockito_extended",
     ],
     libs: [
         "androidx.annotation_annotation",
-        "net-utils-device-common-bpf",  // TestBpfMap extends IBpfMap.
+        "net-utils-device-common-bpf", // TestBpfMap extends IBpfMap.
     ],
     static_libs: [
         "androidx.test.ext.junit",
@@ -42,7 +43,9 @@
         "net-utils-device-common-wear",
         "modules-utils-build_system",
     ],
-    lint: { strict_updatability_linting: true },
+    lint: {
+        strict_updatability_linting: true,
+    },
 }
 
 java_library {
@@ -72,9 +75,11 @@
         "jsr305",
     ],
     static_libs: [
-        "kotlin-test"
+        "kotlin-test",
     ],
-    lint: { strict_updatability_linting: true },
+    lint: {
+        strict_updatability_linting: true,
+    },
 }
 
 java_test_host {
@@ -84,6 +89,15 @@
         "host/**/*.kt",
     ],
     libs: ["tradefed"],
-    test_suites: ["ats", "device-tests", "general-tests", "cts", "mts-networking"],
+    test_suites: [
+        "ats",
+        "device-tests",
+        "general-tests",
+        "cts",
+        "mts-networking",
+        "mcts-networking",
+        "mts-tethering",
+        "mcts-tethering",
+    ],
     data: [":ConnectivityTestPreparer"],
 }
diff --git a/staticlibs/testutils/app/connectivitychecker/Android.bp b/staticlibs/testutils/app/connectivitychecker/Android.bp
index 049ec9e..5af8c14 100644
--- a/staticlibs/testutils/app/connectivitychecker/Android.bp
+++ b/staticlibs/testutils/app/connectivitychecker/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"],
 }
 
@@ -30,5 +31,7 @@
         "net-tests-utils",
     ],
     host_required: ["net-tests-utils-host-common"],
-    lint: { strict_updatability_linting: true },
+    lint: {
+        strict_updatability_linting: true,
+    },
 }
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/ConnectUtil.kt b/staticlibs/testutils/devicetests/com/android/testutils/ConnectUtil.kt
index b1d64f8..8090d5b 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/ConnectUtil.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/ConnectUtil.kt
@@ -116,7 +116,10 @@
         }
     }
 
-    private fun connectToWifiConfig(config: WifiConfiguration) {
+    // 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()
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/DevSdkIgnoreRunner.kt b/staticlibs/testutils/devicetests/com/android/testutils/DevSdkIgnoreRunner.kt
index 10accd4..69fdbf8 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/DevSdkIgnoreRunner.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/DevSdkIgnoreRunner.kt
@@ -31,6 +31,7 @@
 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].
@@ -124,6 +125,9 @@
             notifier.fireTestFailure(Failure(leakMonitorDesc,
                     IllegalStateException("Unexpected thread changes: $threadsDiff")))
         }
+        // Clears up internal state of all inline mocks.
+        // TODO: Call clearInlineMocks() at the end of each test.
+        Mockito.framework().clearInlineMocks()
         notifier.fireTestFinished(leakMonitorDesc)
     }
 
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/DeviceConfigRule.kt b/staticlibs/testutils/devicetests/com/android/testutils/DeviceConfigRule.kt
index 3d98cc3..68248ca 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/DeviceConfigRule.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/DeviceConfigRule.kt
@@ -22,12 +22,12 @@
 import android.util.Log
 import com.android.modules.utils.build.SdkLevel
 import com.android.testutils.FunctionalUtils.ThrowingRunnable
-import org.junit.rules.TestRule
-import org.junit.runner.Description
-import org.junit.runners.model.Statement
 import java.util.concurrent.CompletableFuture
 import java.util.concurrent.Executor
 import java.util.concurrent.TimeUnit
+import org.junit.rules.TestRule
+import org.junit.runner.Description
+import org.junit.runners.model.Statement
 
 private val TAG = DeviceConfigRule::class.simpleName
 
@@ -147,11 +147,11 @@
         return tryTest {
             runAsShell(*readWritePermissions) {
                 DeviceConfig.addOnPropertiesChangedListener(
-                        DeviceConfig.NAMESPACE_CONNECTIVITY,
+                        namespace,
                         inlineExecutor,
                         listener)
                 DeviceConfig.setProperty(
-                        DeviceConfig.NAMESPACE_CONNECTIVITY,
+                        namespace,
                         key,
                         value,
                         false /* makeDefault */)
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/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/NatExternalPacketForwarder.kt b/staticlibs/testutils/devicetests/com/android/testutils/NatExternalPacketForwarder.kt
deleted file mode 100644
index d7961a0..0000000
--- a/staticlibs/testutils/devicetests/com/android/testutils/NatExternalPacketForwarder.kt
+++ /dev/null
@@ -1,81 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.testutils
-
-import java.io.FileDescriptor
-import java.net.InetAddress
-
-/**
- * A class that forwards packets from the external {@link TestNetworkInterface} to the internal
- * {@link TestNetworkInterface} with NAT. See {@link NatPacketForwarderBase} for detail.
- */
-class NatExternalPacketForwarder(
-    srcFd: FileDescriptor,
-    mtu: Int,
-    dstFd: FileDescriptor,
-    extAddr: InetAddress,
-    natMap: PacketBridge.NatMap
-) : NatPacketForwarderBase(srcFd, mtu, dstFd, extAddr, natMap) {
-
-    /**
-     * Rewrite addresses, ports and fix up checksums for packets received on the external
-     * interface.
-     *
-     * Incoming response from external interface which is being forwarded to the internal
-     * interface with translated address, e.g. 1.2.3.4:80 -> 8.8.8.8:1234
-     * will be translated into 8.8.8.8:80 -> 192.168.1.1:5678.
-     *
-     * For packets that are not an incoming response, do not forward them to the
-     * internal interface.
-     */
-    override fun preparePacketForForwarding(buf: ByteArray, len: Int, version: Int, proto: Int) {
-        val (addrPos, addrLen) = getAddressPositionAndLength(version)
-
-        // TODO: support one external address per ip version.
-        val extAddrBuf = mExtAddr.address
-        if (addrLen != extAddrBuf.size) throw IllegalStateException("Packet IP version mismatch")
-
-        // Get internal address by port.
-        val transportOffset =
-            if (version == 4) PacketReflector.IPV4_HEADER_LENGTH
-            else PacketReflector.IPV6_HEADER_LENGTH
-        val dstPort = getPortAt(buf, transportOffset + DESTINATION_PORT_OFFSET)
-        val intAddrInfo = synchronized(mNatMap) { mNatMap.fromExternalPort(dstPort) }
-        // No mapping, skip. This usually happens if the connection is initiated directly on
-        // the external interface, e.g. DNS64 resolution, network validation, etc.
-        if (intAddrInfo == null) return
-
-        val intAddrBuf = intAddrInfo.address.address
-        val intPort = intAddrInfo.port
-
-        // Copy the original destination to into the source address.
-        for (i in 0 until addrLen) {
-            buf[addrPos + i] = buf[addrPos + addrLen + i]
-        }
-
-        // Copy the internal address into the destination address.
-        for (i in 0 until addrLen) {
-            buf[addrPos + addrLen + i] = intAddrBuf[i]
-        }
-
-        // Copy the internal port into the destination port.
-        setPortAt(intPort, buf, transportOffset + DESTINATION_PORT_OFFSET)
-
-        // Fix IP and Transport layer checksum.
-        fixPacketChecksum(buf, len, version, proto.toByte())
-    }
-}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/NatInternalPacketForwarder.kt b/staticlibs/testutils/devicetests/com/android/testutils/NatInternalPacketForwarder.kt
deleted file mode 100644
index fa39d19..0000000
--- a/staticlibs/testutils/devicetests/com/android/testutils/NatInternalPacketForwarder.kt
+++ /dev/null
@@ -1,78 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.testutils
-
-import java.io.FileDescriptor
-import java.net.InetAddress
-
-/**
- * A class that forwards packets from the internal {@link TestNetworkInterface} to the external
- * {@link TestNetworkInterface} with NAT. See {@link NatPacketForwarderBase} for detail.
- */
-class NatInternalPacketForwarder(
-    srcFd: FileDescriptor,
-    mtu: Int,
-    dstFd: FileDescriptor,
-    extAddr: InetAddress,
-    natMap: PacketBridge.NatMap
-) : NatPacketForwarderBase(srcFd, mtu, dstFd, extAddr, natMap) {
-
-    /**
-     * Rewrite addresses, ports and fix up checksums for packets received on the internal
-     * interface.
-     *
-     * Outgoing packet from the internal interface which is being forwarded to the
-     * external interface with translated address, e.g. 192.168.1.1:5678 -> 8.8.8.8:80
-     * will be translated into 8.8.8.8:1234 -> 1.2.3.4:80.
-     *
-     * The external port, e.g. 1234 in the above example, is the port number assigned by
-     * the forwarder when creating the mapping to identify the source address and port when
-     * the response is coming from the external interface. See {@link PacketBridge.NatMap}
-     * for detail.
-     */
-    override fun preparePacketForForwarding(buf: ByteArray, len: Int, version: Int, proto: Int) {
-        val (addrPos, addrLen) = getAddressPositionAndLength(version)
-
-        // TODO: support one external address per ip version.
-        val extAddrBuf = mExtAddr.address
-        if (addrLen != extAddrBuf.size) throw IllegalStateException("Packet IP version mismatch")
-
-        val srcAddr = getInetAddressAt(buf, addrPos, addrLen)
-
-        // Copy the original destination to into the source address.
-        for (i in 0 until addrLen) {
-            buf[addrPos + i] = buf[addrPos + addrLen + i]
-        }
-
-        // Copy the external address into the destination address.
-        for (i in 0 until addrLen) {
-            buf[addrPos + addrLen + i] = extAddrBuf[i]
-        }
-
-        // Add an entry to NAT mapping table.
-        val transportOffset =
-            if (version == 4) PacketReflector.IPV4_HEADER_LENGTH
-            else PacketReflector.IPV6_HEADER_LENGTH
-        val srcPort = getPortAt(buf, transportOffset)
-        val extPort = synchronized(mNatMap) { mNatMap.toExternalPort(srcAddr, srcPort, proto) }
-        // Copy the external port to into the source port.
-        setPortAt(extPort, buf, transportOffset)
-
-        // Fix IP and Transport layer checksum.
-        fixPacketChecksum(buf, len, version, proto.toByte())
-    }
-}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/PacketBridge.kt b/staticlibs/testutils/devicetests/com/android/testutils/PacketBridge.kt
index d50f78a..0b736d1 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/PacketBridge.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/PacketBridge.kt
@@ -16,6 +16,7 @@
 
 package com.android.testutils
 
+import android.annotation.SuppressLint
 import android.content.Context
 import android.net.ConnectivityManager
 import android.net.LinkAddress
@@ -31,61 +32,65 @@
 import java.net.InetAddress
 import libcore.io.IoUtils
 
-private const val MIN_PORT_NUMBER = 1025
-private const val MAX_PORT_NUMBER = 65535
-
 /**
- * A class that set up two {@link TestNetworkInterface} with NAT, and forward packets between them.
+ * A class that set up two {@link TestNetworkInterface}, and forward packets between them.
  *
- * See {@link NatPacketForwarder} for more detailed information.
+ * See {@link PacketForwarder} for more detailed information.
  */
 class PacketBridge(
     context: Context,
-    internalAddr: LinkAddress,
-    externalAddr: LinkAddress,
-    dnsAddr: InetAddress
+    addresses: List<LinkAddress>,
+    dnsAddr: InetAddress,
+    portMapping: List<Pair<Int, Int>>
 ) {
-    private val natMap = NatMap()
     private val binder = Binder()
 
     private val cm = context.getSystemService(ConnectivityManager::class.java)!!
     private val tnm = context.getSystemService(TestNetworkManager::class.java)!!
 
-    // Create test networks.
-    private val internalIface = tnm.createTunInterface(listOf(internalAddr))
-    private val externalIface = tnm.createTunInterface(listOf(externalAddr))
+    // 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, internalAddr, dnsAddr)
-        val (exCb, exNet) = createTestNetwork(externalIface, externalAddr, dnsAddr)
+        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
+        }
     }
 
-    // Setup the packet bridge.
+    // Set up the packet bridge.
     private val internalFd = internalIface.fileDescriptor.fileDescriptor
     private val externalFd = externalIface.fileDescriptor.fileDescriptor
 
-    private val pr1 = NatInternalPacketForwarder(
+    private val pr1 = InternalPacketForwarder(
         internalFd,
         1500,
         externalFd,
-        externalAddr.address,
-        natMap
+        internalForwardMap
     )
-    private val pr2 = NatExternalPacketForwarder(
+    private val pr2 = ExternalPacketForwarder(
         externalFd,
         1500,
         internalFd,
-        externalAddr.address,
-        natMap
+        externalForwardMap
     )
 
     fun start() {
@@ -107,7 +112,7 @@
      */
     private fun createTestNetwork(
         testIface: TestNetworkInterface,
-        addr: LinkAddress,
+        addresses: List<LinkAddress>,
         dnsAddr: InetAddress
     ): Pair<TestableNetworkCallback, Network> {
         // Make a network request to hold the test network
@@ -120,7 +125,7 @@
         cm.requestNetwork(nr, testCb)
 
         val lp = LinkProperties().apply {
-            addLinkAddress(addr)
+            setLinkAddresses(addresses)
             interfaceName = testIface.interfaceName
             addDnsServer(dnsAddr)
         }
@@ -130,44 +135,4 @@
         val network = testCb.expect<Available>().network
         return testCb to network
     }
-
-    /**
-     * A helper class to maintain the mappings between internal addresses/ports and external
-     * ports.
-     *
-     * This class assigns an unused external port number if the mapping between
-     * srcaddress:srcport:protocol and the external port does not exist yet.
-     *
-     * Note that this class is not thread-safe. The instance of the class needs to be
-     * synchronized in the callers when being used in multiple threads.
-     */
-    class NatMap {
-        data class AddressInfo(val address: InetAddress, val port: Int, val protocol: Int)
-
-        private val mToExternalPort = HashMap<AddressInfo, Int>()
-        private val mFromExternalPort = HashMap<Int, AddressInfo>()
-
-        // Skip well-known port 0~1024.
-        private var nextExternalPort = MIN_PORT_NUMBER
-
-        fun toExternalPort(addr: InetAddress, port: Int, protocol: Int): Int {
-            val info = AddressInfo(addr, port, protocol)
-            val extPort: Int
-            if (!mToExternalPort.containsKey(info)) {
-                extPort = nextExternalPort++
-                if (nextExternalPort > MAX_PORT_NUMBER) {
-                    throw IllegalStateException("Available ports are exhausted")
-                }
-                mToExternalPort[info] = extPort
-                mFromExternalPort[extPort] = info
-            } else {
-                extPort = mToExternalPort[info]!!
-            }
-            return extPort
-        }
-
-        fun fromExternalPort(port: Int): AddressInfo? {
-            return mFromExternalPort[port]
-        }
-    }
 }
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/NatPacketForwarderBase.java b/staticlibs/testutils/devicetests/com/android/testutils/PacketForwarderBase.java
similarity index 63%
rename from staticlibs/testutils/devicetests/com/android/testutils/NatPacketForwarderBase.java
rename to staticlibs/testutils/devicetests/com/android/testutils/PacketForwarderBase.java
index 0a2b5d4..5c79eb0 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/NatPacketForwarderBase.java
+++ b/staticlibs/testutils/devicetests/com/android/testutils/PacketForwarderBase.java
@@ -30,16 +30,14 @@
 import android.system.Os;
 import android.util.Log;
 
-import androidx.annotation.GuardedBy;
-
 import java.io.FileDescriptor;
 import java.io.IOException;
-import java.net.InetAddress;
+import java.util.Map;
 import java.util.Objects;
 
 /**
  * A class that forwards packets from a {@link TestNetworkInterface} to another
- * {@link TestNetworkInterface} with NAT.
+ * {@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
@@ -54,29 +52,14 @@
  *
  * To make it work, an internal interface and an external interface are defined, where
  * the client might send packets from the internal interface which are originated from
- * multiple addresses to a server that listens on the external address.
- *
- * When forwarding the outgoing packet on the internal interface, a simple NAT mechanism
- * is implemented during forwarding, which will swap the source and destination,
- * but replacing the source address with the external address,
- * e.g. 192.168.1.1:1234 -> 8.8.8.8:80 will be translated into 8.8.8.8:1234 -> 1.2.3.4:80.
- *
- * For the above example, a client who sends http request will have a hallucination that
- * it is talking to a remote server at 8.8.8.8. Also, the server listens on 1.2.3.4 will
- * have a different hallucination that the request is sent from a remote client at 8.8.8.8,
- * to a local address 1.2.3.4.
- *
- * And a NAT mapping is created at the time when the outgoing packet is forwarded.
- * With a different internal source port, the instance learned that when a response with the
- * destination port 1234, it should forward the packet to the internal address 192.168.1.1.
+ * 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, and the source address will be replaced
- * with the internal address, which is obtained from the NAT mapping described above.
+ * where the source and destination will be swapped.
  */
-public abstract class NatPacketForwarderBase extends Thread {
-    private static final String TAG = "NatPacketForwarder";
+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.
@@ -88,27 +71,14 @@
     // The destination fd to write packets to.
     @NonNull
     final FileDescriptor mDstFd;
-    // The NAT mapping table shared between two NatPacketForwarder instances to map from
-    // the source port to the associated internal address. The map can be read/write from two
-    // different threads on any given time whenever receiving packets on the
-    // {@link TestNetworkInterface}. Thus, synchronize on the object when reading/writing is needed.
-    @GuardedBy("mNatMap")
-    @NonNull
-    final PacketBridge.NatMap mNatMap;
-    // The address of the external interface. See {@link NatPacketForwarder}.
-    @NonNull
-    final InetAddress mExtAddr;
 
+    @NonNull
+    final Map<Integer, Integer> mPortRemapRules;
     /**
-     * Construct a {@link NatPacketForwarderBase}.
+     * 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} with
-     * NAT applied. See {@link NatPacketForwarderBase}.
-     *
-     * To apply NAT, the address of the external interface needs to be supplied through
-     * {@code extAddr} to identify the external interface. And a shared NAT mapping table,
-     * {@code natMap} is needed to be shared between these two instances.
+     * 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.
@@ -116,28 +86,50 @@
      * @param srcFd   {@link FileDescriptor} to read packets from.
      * @param mtu     MTU of the test network.
      * @param dstFd   {@link FileDescriptor} to write packets to.
-     * @param extAddr the external address, which is the address of the external interface.
-     *                See {@link NatPacketForwarderBase}.
-     * @param natMap  the NAT mapping table shared between two {@link NatPacketForwarderBase}
-     *                instance.
+     * @param portRemapRules    port remap rules
      */
-    public NatPacketForwarderBase(@NonNull FileDescriptor srcFd, int mtu,
-            @NonNull FileDescriptor dstFd, @NonNull InetAddress extAddr,
-            @NonNull PacketBridge.NatMap natMap) {
+    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);
-        mExtAddr = Objects.requireNonNull(extAddr);
-        mNatMap = Objects.requireNonNull(natMap);
+        mPortRemapRules = Objects.requireNonNull(portRemapRules);
     }
 
     /**
      * A method to prepare forwarding packets between two instances of {@link TestNetworkInterface},
-     * which includes re-write addresses, ports and fix up checksums.
-     * Subclasses should override this method to implement a simple NAT.
+     * 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 preparePacketForForwarding(@NonNull byte[] buf, int len, int version, int proto);
+    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 {
@@ -147,7 +139,13 @@
         }
     }
 
-    // Reads one packet from mSrcFd, and writes the packet to the mDstFd for supported protocols.
+    /**
+     * 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) {
@@ -190,12 +188,19 @@
         if (len < ipHdrLen + transportHdrLen) {
             throw new IllegalStateException("Unexpected buffer length: " + len);
         }
-        // Re-write addresses, ports and fix up checksums.
-        preparePacketForForwarding(mBuf, len, version, proto);
+
+        // 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());
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/PacketReflector.java b/staticlibs/testutils/devicetests/com/android/testutils/PacketReflector.java
index 69392d4..ce20d67 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/PacketReflector.java
+++ b/staticlibs/testutils/devicetests/com/android/testutils/PacketReflector.java
@@ -87,31 +87,6 @@
         mBuf = new byte[mtu];
     }
 
-    private static void swapBytes(@NonNull byte[] buf, int pos1, int pos2, int len) {
-        for (int i = 0; i < len; i++) {
-            byte b = buf[pos1 + i];
-            buf[pos1 + i] = buf[pos2 + i];
-            buf[pos2 + i] = b;
-        }
-    }
-
-    private static void swapAddresses(@NonNull byte[] buf, int version) {
-        int addrPos, addrLen;
-        switch (version) {
-            case 4:
-                addrPos = IPV4_ADDR_OFFSET;
-                addrLen = IPV4_ADDR_LENGTH;
-                break;
-            case 6:
-                addrPos = IPV6_ADDR_OFFSET;
-                addrLen = IPV6_ADDR_LENGTH;
-                break;
-            default:
-                throw new IllegalArgumentException();
-        }
-        swapBytes(buf, addrPos, addrPos + addrLen, addrLen);
-    }
-
     // Reflect TCP packets: swap the source and destination addresses, but don't change the ports.
     // This is used by the test to "connect to itself" through the VPN.
     private void processTcpPacket(@NonNull byte[] buf, int version, int len, int hdrLen) {
@@ -120,7 +95,7 @@
         }
 
         // Swap src and dst IP addresses.
-        swapAddresses(buf, version);
+        PacketReflectorUtil.swapAddresses(buf, version);
 
         // Send the packet back.
         writePacket(buf, len);
@@ -134,11 +109,11 @@
         }
 
         // Swap src and dst IP addresses.
-        swapAddresses(buf, version);
+        PacketReflectorUtil.swapAddresses(buf, version);
 
         // Swap dst and src ports.
         int portOffset = hdrLen;
-        swapBytes(buf, portOffset, portOffset + 2, 2);
+        PacketReflectorUtil.swapBytes(buf, portOffset, portOffset + 2, 2);
 
         // Send the packet back.
         writePacket(buf, len);
@@ -160,7 +135,7 @@
 
         // Swap src and dst IP addresses, and send the packet back.
         // This effectively pings the device to see if it replies.
-        swapAddresses(buf, version);
+        PacketReflectorUtil.swapAddresses(buf, version);
         writePacket(buf, len);
 
         // The device should have replied, and buf should now contain a ping response.
@@ -202,7 +177,7 @@
         }
 
         // Now swap the addresses again and reflect the packet. This sends a ping reply.
-        swapAddresses(buf, version);
+        PacketReflectorUtil.swapAddresses(buf, version);
         writePacket(buf, len);
     }
 
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/PacketReflectorUtil.kt b/staticlibs/testutils/devicetests/com/android/testutils/PacketReflectorUtil.kt
index 498b1a3..ad259c5 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/PacketReflectorUtil.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/PacketReflectorUtil.kt
@@ -112,3 +112,28 @@
         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/TestHttpServer.kt b/staticlibs/testutils/devicetests/com/android/testutils/TestHttpServer.kt
index 740bf63..f1f0c1c 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/TestHttpServer.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/TestHttpServer.kt
@@ -25,8 +25,10 @@
  * 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(host: String? = null) : NanoHTTPD(host, 0 /* auto-select the port */) {
+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>()
 
diff --git a/staticlibs/testutils/hostdevice/com/android/testutils/PacketFilter.kt b/staticlibs/testutils/hostdevice/com/android/testutils/PacketFilter.kt
index 1bb6d68..a73a58a 100644
--- a/staticlibs/testutils/hostdevice/com/android/testutils/PacketFilter.kt
+++ b/staticlibs/testutils/hostdevice/com/android/testutils/PacketFilter.kt
@@ -110,6 +110,12 @@
     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.
  */
diff --git a/tests/benchmark/Android.bp b/tests/benchmark/Android.bp
index 6ea5347..7854bb5 100644
--- a/tests/benchmark/Android.bp
+++ b/tests/benchmark/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"],
 }
@@ -40,4 +41,3 @@
     test_suites: ["device-tests"],
     jarjar_rules: ":connectivity-jarjar-rules",
 }
-
diff --git a/tests/benchmark/src/android/net/netstats/benchmarktests/NetworkStatsTest.kt b/tests/benchmark/src/android/net/netstats/benchmarktests/NetworkStatsTest.kt
index 585157f..57602f1 100644
--- a/tests/benchmark/src/android/net/netstats/benchmarktests/NetworkStatsTest.kt
+++ b/tests/benchmark/src/android/net/netstats/benchmarktests/NetworkStatsTest.kt
@@ -133,7 +133,16 @@
     }
 
     @Test
-    fun testReadFromRecorder_manyUids() {
+    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 {
@@ -146,7 +155,9 @@
                 PREFIX_UID,
                 UID_COLLECTION_BUCKET_DURATION_MS,
                 false /* includeTags */,
-                false /* wipeOnError */
+                false /* wipeOnError */,
+                useFastDataInput /* useFastDataInput */,
+                it
             )
             recorder.orLoadCompleteLocked
         }
diff --git a/tests/common/Android.bp b/tests/common/Android.bp
index 7b5c298..6e9d614 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"],
 }
@@ -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 {
diff --git a/tests/common/java/android/net/NetworkCapabilitiesTest.java b/tests/common/java/android/net/NetworkCapabilitiesTest.java
index 3a3459b..6eb56c7 100644
--- a/tests/common/java/android/net/NetworkCapabilitiesTest.java
+++ b/tests/common/java/android/net/NetworkCapabilitiesTest.java
@@ -54,13 +54,13 @@
 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;
 import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
 import static android.net.NetworkCapabilities.TRANSPORT_WIFI_AWARE;
 import static android.os.Process.INVALID_UID;
-
 import static com.android.modules.utils.build.SdkLevel.isAtLeastS;
 import static com.android.modules.utils.build.SdkLevel.isAtLeastT;
 import static com.android.modules.utils.build.SdkLevel.isAtLeastV;
@@ -68,7 +68,6 @@
 import static com.android.testutils.MiscAsserts.assertEmpty;
 import static com.android.testutils.MiscAsserts.assertThrows;
 import static com.android.testutils.ParcelUtils.assertParcelingIsLossless;
-
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -82,25 +81,22 @@
 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;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
 import com.android.testutils.DevSdkIgnoreRunner;
-
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mockito;
-
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
 import java.util.Set;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
 
 @SmallTest
 @RunWith(DevSdkIgnoreRunner.class)
@@ -761,6 +757,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()
diff --git a/tests/common/java/android/net/nsd/NsdServiceInfoTest.java b/tests/common/java/android/net/nsd/NsdServiceInfoTest.java
index ffe0e91..8e89037 100644
--- a/tests/common/java/android/net/nsd/NsdServiceInfoTest.java
+++ b/tests/common/java/android/net/nsd/NsdServiceInfoTest.java
@@ -18,6 +18,7 @@
 
 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;
 
@@ -40,6 +41,7 @@
 import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 @RunWith(DevSdkIgnoreRunner.class)
 @SmallTest
@@ -114,8 +116,10 @@
         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.setNetwork(new Network(123));
         fullInfo.setInterfaceIndex(456);
         checkParcelable(fullInfo);
@@ -131,6 +135,7 @@
         attributedInfo.setServiceType("_kitten._tcp");
         attributedInfo.setPort(4242);
         attributedInfo.setHostAddresses(List.of(IPV6_ADDRESS, IPV4_ADDRESS));
+        attributedInfo.setHostname("home");
         attributedInfo.setAttribute("color", "pink");
         attributedInfo.setAttribute("sound", (new String("にゃあ")).getBytes("UTF-8"));
         attributedInfo.setAttribute("adorable", (String) null);
@@ -149,7 +154,7 @@
         assertFalse(attributedInfo.getAttributes().keySet().contains("sticky"));
     }
 
-    public void checkParcelable(NsdServiceInfo original) {
+    private static void checkParcelable(NsdServiceInfo original) {
         // Write to parcel.
         Parcel p = Parcel.obtain();
         Bundle writer = new Bundle();
@@ -166,6 +171,7 @@
         assertEquals(original.getServiceName(), result.getServiceName());
         assertEquals(original.getServiceType(), result.getServiceType());
         assertEquals(original.getHost(), result.getHost());
+        assertEquals(original.getHostname(), result.getHostname());
         assertTrue(original.getPort() == result.getPort());
         assertEquals(original.getNetwork(), result.getNetwork());
         assertEquals(original.getInterfaceIndex(), result.getInterfaceIndex());
@@ -179,11 +185,20 @@
         }
     }
 
-    public void assertEmptyServiceInfo(NsdServiceInfo shouldBeEmpty) {
+    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/cts/hostside/Android.bp b/tests/cts/hostside/Android.bp
index e55ba63..f6c0430 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,7 +27,10 @@
     name: "CtsHostsideNetworkTests",
     defaults: ["cts_defaults"],
     // Only compile source java files in this apk.
-    srcs: ["src/**/*.java"],
+    srcs: [
+        "src/**/*.java",
+        ":ArgumentConstants",
+    ],
     libs: [
         "net-tests-utils-host-device-common",
         "cts-tradefed",
@@ -43,7 +43,9 @@
     test_suites: [
         "cts",
         "general-tests",
-        "sts"
+        "mcts-tethering",
+        "mts-tethering",
+        "sts",
     ],
     data: [
         ":CtsHostsideNetworkTestsApp",
diff --git a/tests/cts/hostside/aidl/Android.bp b/tests/cts/hostside/aidl/Android.bp
index 2751f6f..18a5897 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"],
 }
 
diff --git a/tests/cts/hostside/app/Android.bp b/tests/cts/hostside/app/Android.bp
index 470bb17..cf4afa9 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"],
 }
 
@@ -35,7 +36,10 @@
         "android.test.runner",
         "android.test.base",
     ],
-    srcs: ["src/**/*.java"],
+    srcs: [
+        "src/**/*.java",
+         ":ArgumentConstants",
+    ],
     // Tag this module as a cts test artifact
     test_suites: [
         "general-tests",
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
index 04d054d..0d7365f 100644
--- 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
@@ -59,13 +59,13 @@
         setBatterySaverMode(false);
         launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_ACTIVTIY);
         setBatterySaverMode(true);
-        assertForegroundNetworkAccess();
+        assertTopNetworkAccess(true);
 
         // Although it should not have access while the screen is off.
         turnScreenOff();
         assertBackgroundNetworkAccess(false);
         turnScreenOn();
-        assertForegroundNetworkAccess();
+        assertTopNetworkAccess(true);
 
         // Goes back to background state.
         finishActivity();
@@ -75,7 +75,7 @@
         setBatterySaverMode(false);
         launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_FOREGROUND_SERVICE);
         setBatterySaverMode(true);
-        assertForegroundNetworkAccess();
+        assertForegroundServiceNetworkAccess();
         stopForegroundService();
         assertBackgroundNetworkAccess(false);
     }
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractDefaultRestrictionsTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractDefaultRestrictionsTest.java
new file mode 100644
index 0000000..8a3e790
--- /dev/null
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractDefaultRestrictionsTest.java
@@ -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.android.cts.net.hostside;
+
+import static android.app.ActivityManager.PROCESS_STATE_TOP_SLEEPING;
+
+import static org.junit.Assume.assumeTrue;
+
+import android.os.SystemClock;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Base class for default, always-on network restrictions.
+ */
+abstract class AbstractDefaultRestrictionsTest extends AbstractRestrictBackgroundNetworkTestCase {
+
+    @Before
+    public final void setUp() throws Exception {
+        super.setUp();
+
+        removePowerSaveModeWhitelist(TEST_APP2_PKG);
+        removePowerSaveModeExceptIdleWhitelist(TEST_APP2_PKG);
+
+        registerBroadcastReceiver();
+        assumeTrue("Feature not enabled", isNetworkBlockedForTopSleepingAndAbove());
+    }
+
+    @After
+    public final void tearDown() throws Exception {
+        super.tearDown();
+
+        removePowerSaveModeWhitelist(TEST_APP2_PKG);
+        removePowerSaveModeExceptIdleWhitelist(TEST_APP2_PKG);
+    }
+
+    @Test
+    public void testFgsNetworkAccess() throws Exception {
+        assertProcessStateBelow(PROCESS_STATE_TOP_SLEEPING);
+        SystemClock.sleep(PROCESS_STATE_TRANSITION_DELAY_MS);
+        assertNetworkAccess(false, null);
+
+        launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_FOREGROUND_SERVICE);
+    }
+
+    @Test
+    public void testActivityNetworkAccess() throws Exception {
+        assertProcessStateBelow(PROCESS_STATE_TOP_SLEEPING);
+        SystemClock.sleep(PROCESS_STATE_TRANSITION_DELAY_MS);
+        assertNetworkAccess(false, null);
+
+        launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_ACTIVTIY);
+    }
+
+    @Test
+    public void testBackgroundNetworkAccess_inFullAllowlist() throws Exception {
+        assertProcessStateBelow(PROCESS_STATE_TOP_SLEEPING);
+        SystemClock.sleep(PROCESS_STATE_TRANSITION_DELAY_MS);
+        assertNetworkAccess(false, null);
+
+        addPowerSaveModeWhitelist(TEST_APP2_PKG);
+        assertProcessStateBelow(PROCESS_STATE_TOP_SLEEPING);
+        assertNetworkAccess(true, null);
+    }
+
+    @Test
+    public void testBackgroundNetworkAccess_inExceptIdleAllowlist() throws Exception {
+        assertProcessStateBelow(PROCESS_STATE_TOP_SLEEPING);
+        SystemClock.sleep(PROCESS_STATE_TRANSITION_DELAY_MS);
+        assertNetworkAccess(false, null);
+
+        addPowerSaveModeExceptIdleWhitelist(TEST_APP2_PKG);
+        assertProcessStateBelow(PROCESS_STATE_TOP_SLEEPING);
+        assertNetworkAccess(true, null);
+    }
+}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractDozeModeTestCase.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractDozeModeTestCase.java
index e0ce4ea..b037953 100644
--- 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
@@ -16,6 +16,8 @@
 
 package com.android.cts.net.hostside;
 
+import static android.app.ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE;
+
 import static com.android.cts.net.hostside.Property.DOZE_MODE;
 import static com.android.cts.net.hostside.Property.NOT_LOW_RAM_DEVICE;
 
@@ -62,9 +64,9 @@
         setDozeMode(false);
         launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_FOREGROUND_SERVICE);
         setDozeMode(true);
-        assertForegroundNetworkAccess();
+        assertForegroundServiceNetworkAccess();
         stopForegroundService();
-        assertBackgroundState();
+        assertProcessStateBelow(PROCESS_STATE_BOUND_FOREGROUND_SERVICE);
         assertBackgroundNetworkAccess(false);
     }
 
@@ -136,6 +138,6 @@
     protected void assertsForegroundAlwaysHasNetworkAccess() throws Exception {
         launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_FOREGROUND_SERVICE);
         stopForegroundService();
-        assertBackgroundState();
+        assertProcessStateBelow(PROCESS_STATE_BOUND_FOREGROUND_SERVICE);
     }
 }
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
index 198b009..2ca8832 100644
--- 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
@@ -16,10 +16,14 @@
 
 package com.android.cts.net.hostside;
 
+import static android.app.ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE;
+import static android.app.ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE;
+import static android.app.ActivityManager.PROCESS_STATE_TOP;
 import static android.app.job.JobScheduler.RESULT_SUCCESS;
 import static android.net.ConnectivityManager.ACTION_RESTRICT_BACKGROUND_CHANGED;
 import static android.os.BatteryManager.BATTERY_PLUGGED_ANY;
 
+import static com.android.cts.net.arguments.InstrumentationArguments.ARG_WAIVE_BIND_PRIORITY;
 import static com.android.cts.net.hostside.NetworkPolicyTestUtils.executeShellCommand;
 import static com.android.cts.net.hostside.NetworkPolicyTestUtils.forceRunJob;
 import static com.android.cts.net.hostside.NetworkPolicyTestUtils.getConnectivityManager;
@@ -38,7 +42,6 @@
 import static org.junit.Assert.fail;
 
 import android.annotation.NonNull;
-import android.app.ActivityManager;
 import android.app.Instrumentation;
 import android.app.NotificationManager;
 import android.app.job.JobInfo;
@@ -63,10 +66,13 @@
 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;
@@ -76,6 +82,7 @@
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.LinkedBlockingQueue;
 import java.util.concurrent.TimeUnit;
+import java.util.function.Predicate;
 
 /**
  * Superclass for tests related to background network restrictions.
@@ -86,6 +93,8 @@
 
     protected static final String TEST_PKG = "com.android.cts.net.hostside";
     protected static final String TEST_APP2_PKG = "com.android.cts.net.hostside.app2";
+    // TODO(b/321797685): Configure it via device-config once it is available.
+    protected static final long PROCESS_STATE_TRANSITION_DELAY_MS = TimeUnit.SECONDS.toMillis(5);
 
     private static final String TEST_APP2_ACTIVITY_CLASS = TEST_APP2_PKG + ".MyActivity";
     private static final String TEST_APP2_SERVICE_CLASS = TEST_APP2_PKG + ".MyForegroundService";
@@ -93,7 +102,6 @@
 
     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;
@@ -126,8 +134,6 @@
     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";
 
@@ -150,8 +156,6 @@
     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;
@@ -171,9 +175,6 @@
             .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();
@@ -182,7 +183,16 @@
         mUid = getUid(TEST_APP2_PKG);
         mMyUid = getUid(mContext.getPackageName());
         mServiceClient = new MyServiceClient(mContext);
-        mServiceClient.bind();
+
+        final Bundle args = InstrumentationRegistry.getArguments();
+        final int bindPriorityFlags;
+        if (Boolean.valueOf(args.getString(ARG_WAIVE_BIND_PRIORITY, "false"))) {
+            bindPriorityFlags = Context.BIND_WAIVE_PRIORITY;
+        } else {
+            bindPriorityFlags = Context.BIND_NOT_FOREGROUND;
+        }
+        mServiceClient.bind(bindPriorityFlags);
+
         mPowerManager = mContext.getSystemService(PowerManager.class);
         executeShellCommand("cmd netpolicy start-watching " + mUid);
         // Some of the test cases assume that Data saver mode is initially disabled, which might not
@@ -206,6 +216,22 @@
         if (null != lock && lock.isHeld()) lock.release();
     }
 
+    /**
+     * Check if the feature blocking network for top_sleeping and lower priority proc-states is
+     * enabled. This is a manual check because the feature flag infrastructure may not be available
+     * in all the branches that will get this code.
+     * TODO: b/322115994 - Use @RequiresFlagsEnabled with
+     * Flags.FLAG_NETWORK_BLOCKED_FOR_TOP_SLEEPING_AND_ABOVE once the tests are moved to cts.
+     */
+    protected boolean isNetworkBlockedForTopSleepingAndAbove() {
+        if (!SdkLevel.isAtLeastV()) {
+            return false;
+        }
+        final String output = executeShellCommand("device_config get backstage_power"
+                + " com.android.server.net.network_blocked_for_top_sleeping_and_above");
+        return Boolean.parseBoolean(output);
+    }
+
     protected int getUid(String packageName) throws Exception {
         return mContext.getPackageManager().getPackageUid(packageName, 0);
     }
@@ -284,44 +310,20 @@
                 restrictBackgroundValueToString(Integer.parseInt(status)));
     }
 
-    protected void assertBackgroundNetworkAccess(boolean expectAllowed) throws Exception {
-        assertBackgroundNetworkAccess(expectAllowed, null);
-    }
-
     /**
-     * Asserts whether the active network is available or not for the background app. 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.
+     * @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.
      */
-    protected void assertBackgroundNetworkAccess(boolean expectAllowed,
-            @Nullable final String expectedUnavailableError) throws Exception {
-        assertBackgroundState();
-        if (expectAllowed && expectedUnavailableError != null) {
-            throw new IllegalArgumentException("expectedUnavailableError is not null");
-        }
-        assertNetworkAccess(expectAllowed /* expectAvailable */, false /* needScreenOn */,
-                expectedUnavailableError);
+    @Deprecated
+    protected void assertBackgroundNetworkAccess(boolean expectAllowed) throws Exception {
+        assertProcessStateBelow(PROCESS_STATE_BOUND_FOREGROUND_SERVICE);
+        assertNetworkAccess(expectAllowed, false, null);
     }
 
-    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 assertTopNetworkAccess(boolean expectAllowed) throws Exception {
+        assertTopState();
+        assertNetworkAccess(expectAllowed, true /* needScreenOn */);
     }
 
     protected void assertForegroundServiceNetworkAccess() throws Exception {
@@ -355,75 +357,65 @@
         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);
+    /**
+     * 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 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 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, "assertForegroundServiceState(): status for app2 (" + mUid + ") on attempt #"
-                    + i + ": " + state);
-            if (state.state == PROCESS_STATE_FOREGROUND_SERVICE) {
+            Log.v(TAG, "assertProcessState(): status for app2 (" + mUid + ") on attempt #" + i
+                    + ": " + state);
+            if (statePredicate.test(state)) {
                 return;
             }
-            Log.d(TAG, "App not on foreground service state on attempt #" + i
+            Log.i(TAG, "App not in desired process 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);
+        fail("App2 (" + mUid + ") is not in the desired process state after " + maxTries
+                + " attempts: " + state);
     }
 
     /**
-     * Returns whether an app state should be considered "background" for restriction purposes.
+     * 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 boolean isBackground(int state) {
-        return state > PROCESS_STATE_FOREGROUND_SERVICE;
+    protected void assertNetworkAccess(boolean expectAllowed, String expectedUnavailableError)
+            throws Exception {
+        if (expectAllowed && expectedUnavailableError != null) {
+            throw new IllegalArgumentException("expectedUnavailableError is not null");
+        }
+        assertNetworkAccess(expectAllowed, false, expectedUnavailableError);
     }
 
     /**
@@ -958,7 +950,7 @@
                 } 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();
+                    assertTopNetworkAccess(true);
                 } else {
                     fail("Unexpected resultCode=" + resultCode + "; received=[" + resultData + "]");
                 }
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
index 10775d0..c1d576d 100644
--- 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
@@ -17,6 +17,9 @@
 package com.android.cts.net.hostside;
 
 
+import static android.app.ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE;
+import static android.app.ActivityManager.PROCESS_STATE_TOP_SLEEPING;
+
 import static com.android.cts.net.hostside.NetworkPolicyTestUtils.getUiDevice;
 import static com.android.cts.net.hostside.NetworkPolicyTestUtils.setRestrictBackground;
 import static com.android.cts.net.hostside.Property.APP_STANDBY_MODE;
@@ -26,8 +29,13 @@
 import static com.android.cts.net.hostside.Property.METERED_NETWORK;
 import static com.android.cts.net.hostside.Property.NON_METERED_NETWORK;
 
+import static org.junit.Assume.assumeTrue;
+
+import android.os.SystemClock;
 import android.util.Log;
 
+import com.android.compatibility.common.util.ThrowingRunnable;
+
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
@@ -61,14 +69,14 @@
     @RequiredProperties({BATTERY_SAVER_MODE})
     public void testStartActivity_batterySaver() throws Exception {
         setBatterySaverMode(true);
-        assertLaunchedActivityHasNetworkAccess("testStartActivity_batterySaver");
+        assertLaunchedActivityHasNetworkAccess("testStartActivity_batterySaver", null);
     }
 
     @Test
     @RequiredProperties({DATA_SAVER_MODE, METERED_NETWORK})
     public void testStartActivity_dataSaver() throws Exception {
         setRestrictBackground(true);
-        assertLaunchedActivityHasNetworkAccess("testStartActivity_dataSaver");
+        assertLaunchedActivityHasNetworkAccess("testStartActivity_dataSaver", null);
     }
 
     @Test
@@ -77,7 +85,7 @@
         setDozeMode(true);
         // TODO (235284115): We need to turn on Doze every time before starting
         // the activity.
-        assertLaunchedActivityHasNetworkAccess("testStartActivity_doze");
+        assertLaunchedActivityHasNetworkAccess("testStartActivity_doze", null);
     }
 
     @Test
@@ -87,15 +95,28 @@
         setAppIdle(true);
         // TODO (235284115): We need to put the app into app standby mode every
         // time before starting the activity.
-        assertLaunchedActivityHasNetworkAccess("testStartActivity_appStandby");
+        assertLaunchedActivityHasNetworkAccess("testStartActivity_appStandby", null);
     }
 
-    private void assertLaunchedActivityHasNetworkAccess(String testName) throws Exception {
+    @Test
+    public void testStartActivity_default() throws Exception {
+        assumeTrue("Feature not enabled", isNetworkBlockedForTopSleepingAndAbove());
+        assertLaunchedActivityHasNetworkAccess("testStartActivity_default", () -> {
+            assertProcessStateBelow(PROCESS_STATE_TOP_SLEEPING);
+            SystemClock.sleep(PROCESS_STATE_TRANSITION_DELAY_MS);
+        });
+    }
+
+    private void assertLaunchedActivityHasNetworkAccess(String testName,
+            ThrowingRunnable onBeginIteration) throws Exception {
         for (int i = 0; i < TEST_ITERATION_COUNT; ++i) {
+            if (onBeginIteration != null) {
+                onBeginIteration.run();
+            }
             Log.i(TAG, testName + " start #" + i);
             launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_ACTIVTIY);
             getUiDevice().pressHome();
-            assertBackgroundState();
+            assertProcessStateBelow(PROCESS_STATE_BOUND_FOREGROUND_SERVICE);
             Log.i(TAG, testName + " end #" + i);
         }
     }
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/DataSaverModeTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/DataSaverModeTest.java
index 2f30536..790e031 100644
--- 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
@@ -108,7 +108,7 @@
         setRestrictBackground(false);
         launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_ACTIVTIY);
         setRestrictBackground(true);
-        assertForegroundNetworkAccess();
+        assertTopNetworkAccess(true);
 
         // Although it should not have access while the screen is off.
         turnScreenOff();
@@ -119,7 +119,7 @@
         if (isTV()) {
             startActivity();
         }
-        assertForegroundNetworkAccess();
+        assertTopNetworkAccess(true);
 
         // Goes back to background state.
         finishActivity();
@@ -129,7 +129,7 @@
         setRestrictBackground(false);
         launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_FOREGROUND_SERVICE);
         setRestrictBackground(true);
-        assertForegroundNetworkAccess();
+        assertForegroundServiceNetworkAccess();
         stopForegroundService();
         assertBackgroundNetworkAccess(false);
     }
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/DefaultRestrictionsMeteredTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/DefaultRestrictionsMeteredTest.java
new file mode 100644
index 0000000..f3a1026
--- /dev/null
+++ b/tests/cts/hostside/app/src/com/android/cts/net/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.net.hostside;
+
+import static com.android.cts.net.hostside.Property.METERED_NETWORK;
+
+@RequiredProperties({METERED_NETWORK})
+public class DefaultRestrictionsMeteredTest extends AbstractDefaultRestrictionsTest {
+}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/DefaultRestrictionsNonMeteredTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/DefaultRestrictionsNonMeteredTest.java
new file mode 100644
index 0000000..5651dd0
--- /dev/null
+++ b/tests/cts/hostside/app/src/com/android/cts/net/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.net.hostside;
+
+import static com.android.cts.net.hostside.Property.NON_METERED_NETWORK;
+
+@RequiredProperties({NON_METERED_NETWORK})
+public class DefaultRestrictionsNonMeteredTest extends AbstractDefaultRestrictionsTest {
+}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyServiceClient.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyServiceClient.java
index 93cc911..980ecd5 100644
--- 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
@@ -34,26 +34,30 @@
 
     private Context mContext;
     private ServiceConnection mServiceConnection;
-    private IMyService mService;
+    private volatile IMyService mService;
+    private final ConditionVariable mServiceCondition = new ConditionVariable();
 
     public MyServiceClient(Context context) {
         mContext = context;
     }
 
-    public void bind() {
+    /**
+     * 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");
         }
-
-        final ConditionVariable cv = new ConditionVariable();
         mServiceConnection = new ServiceConnection() {
             @Override
             public void onServiceConnected(ComponentName name, IBinder service) {
                 mService = IMyService.Stub.asInterface(service);
-                cv.open();
+                mServiceCondition.open();
             }
             @Override
             public void onServiceDisconnected(ComponentName name) {
+                mServiceCondition.close();
                 mService = null;
             }
         };
@@ -63,12 +67,8 @@
         // 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");
-        }
+                | bindPriorityFlags);
+        ensureServiceConnection();
     }
 
     public void unbind() {
@@ -77,37 +77,56 @@
         }
     }
 
+    private void ensureServiceConnection() {
+        if (mService != null) {
+            return;
+        }
+        mServiceCondition.block(TIMEOUT_MS);
+        if (mService == null) {
+            throw new IllegalStateException(
+                    "Could not bind to MyService service after " + TIMEOUT_MS + "ms");
+        }
+    }
+
     public void registerBroadcastReceiver() throws RemoteException {
+        ensureServiceConnection();
         mService.registerBroadcastReceiver();
     }
 
     public int getCounters(String receiverName, String action) throws RemoteException {
+        ensureServiceConnection();
         return mService.getCounters(receiverName, action);
     }
 
     public String checkNetworkStatus() throws RemoteException {
+        ensureServiceConnection();
         return mService.checkNetworkStatus();
     }
 
     public String getRestrictBackgroundStatus() throws RemoteException {
+        ensureServiceConnection();
         return mService.getRestrictBackgroundStatus();
     }
 
     public void sendNotification(int notificationId, String notificationType)
             throws RemoteException {
+        ensureServiceConnection();
         mService.sendNotification(notificationId, notificationType);
     }
 
     public void registerNetworkCallback(final NetworkRequest request, INetworkCallback cb)
             throws RemoteException {
+        ensureServiceConnection();
         mService.registerNetworkCallback(request, cb);
     }
 
     public void unregisterNetworkCallback() throws RemoteException {
+        ensureServiceConnection();
         mService.unregisterNetworkCallback();
     }
 
     public int scheduleJob(JobInfo jobInfo) throws RemoteException {
+        ensureServiceConnection();
         return mService.scheduleJob(jobInfo);
     }
 }
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkCallbackTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkCallbackTest.java
index 82f4a65..5552b8f 100644
--- 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
@@ -16,6 +16,8 @@
 
 package com.android.cts.net.hostside;
 
+import static android.app.ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE;
+import static android.app.ActivityManager.PROCESS_STATE_TOP_SLEEPING;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED;
 import static android.net.NetworkCapabilities.SIGNAL_STRENGTH_UNSPECIFIED;
 
@@ -33,6 +35,7 @@
 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;
@@ -42,6 +45,7 @@
 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;
@@ -144,12 +148,22 @@
         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);
+                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);
         }
@@ -203,6 +217,7 @@
         // 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.
@@ -223,7 +238,7 @@
         // Check that the network is metered.
         mTestNetworkCallback.expectCapabilitiesCallbackEventually(mNetwork,
                 false /* hasCapability */, NET_CAPABILITY_NOT_METERED);
-        mTestNetworkCallback.expectBlockedStatusCallback(mNetwork, false);
+        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
@@ -312,7 +327,8 @@
             // Enable Power Saver
             setBatterySaverMode(true);
             if (SdkLevel.isAtLeastT()) {
-                assertBackgroundNetworkAccess(false, "java.net.UnknownHostException");
+                assertProcessStateBelow(PROCESS_STATE_BOUND_FOREGROUND_SERVICE);
+                assertNetworkAccess(false, "java.net.UnknownHostException");
             } else {
                 assertBackgroundNetworkAccess(false);
             }
@@ -336,7 +352,8 @@
             // Enable Power Saver
             setBatterySaverMode(true);
             if (SdkLevel.isAtLeastT()) {
-                assertBackgroundNetworkAccess(false, "java.net.UnknownHostException");
+                assertProcessStateBelow(PROCESS_STATE_BOUND_FOREGROUND_SERVICE);
+                assertNetworkAccess(false, "java.net.UnknownHostException");
             } else {
                 assertBackgroundNetworkAccess(false);
             }
@@ -353,6 +370,58 @@
         }
     }
 
+    @Test
+    public void testOnBlockedStatusChanged_default() throws Exception {
+        assumeTrue("Feature not enabled", isNetworkBlockedForTopSleepingAndAbove());
+
+        try {
+            assertProcessStateBelow(PROCESS_STATE_TOP_SLEEPING);
+            assertNetworkAccess(false, null);
+            assertNetworkAccessBlockedByBpf(true, mUid, true /* metered */);
+
+            launchActivity();
+            assertTopState();
+            assertNetworkAccess(true, null);
+            mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, false);
+            assertNetworkAccessBlockedByBpf(false, mUid, true /* metered */);
+
+            finishActivity();
+            assertProcessStateBelow(PROCESS_STATE_TOP_SLEEPING);
+            SystemClock.sleep(PROCESS_STATE_TRANSITION_DELAY_MS);
+            assertNetworkAccess(false, null);
+            mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, true);
+            assertNetworkAccessBlockedByBpf(true, mUid, true /* metered */);
+
+        } finally {
+            mMeterednessConfiguration.resetNetworkMeteredness();
+        }
+
+        // Set to non-metered network
+        mMeterednessConfiguration.configureNetworkMeteredness(false);
+        mTestNetworkCallback.expectCapabilitiesCallbackEventually(mNetwork,
+                true /* hasCapability */, NET_CAPABILITY_NOT_METERED);
+        try {
+            assertProcessStateBelow(PROCESS_STATE_TOP_SLEEPING);
+            assertNetworkAccess(false, null);
+            assertNetworkAccessBlockedByBpf(true, mUid, false /* metered */);
+
+            launchActivity();
+            assertTopState();
+            assertNetworkAccess(true, null);
+            mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, false);
+            assertNetworkAccessBlockedByBpf(false, mUid, false /* metered */);
+
+            finishActivity();
+            assertProcessStateBelow(PROCESS_STATE_TOP_SLEEPING);
+            SystemClock.sleep(PROCESS_STATE_TRANSITION_DELAY_MS);
+            assertNetworkAccess(false, null);
+            mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, true);
+            assertNetworkAccessBlockedByBpf(true, mUid, false /* metered */);
+        } finally {
+            mMeterednessConfiguration.resetNetworkMeteredness();
+        }
+    }
+
     // TODO: 1. test against VPN lockdown.
     //       2. test against multiple networks.
 }
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkPolicyManagerTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkPolicyManagerTest.java
index a0d88c9..968e270 100644
--- 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
@@ -16,6 +16,8 @@
 
 package com.android.cts.net.hostside;
 
+import static android.app.ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE;
+import static android.app.ActivityManager.PROCESS_STATE_TOP_SLEEPING;
 import static android.os.Process.SYSTEM_UID;
 
 import static com.android.cts.net.hostside.NetworkPolicyTestUtils.assertIsUidRestrictedOnMeteredNetworks;
@@ -27,6 +29,9 @@
 
 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;
@@ -137,13 +142,13 @@
 
             // Make TEST_APP2_PKG go to foreground and mUid will be allowed temporarily.
             launchActivity();
-            assertForegroundState();
+            assertTopState();
             assertNetworkingBlockedStatusForUid(mUid, METERED,
                     false /* expectedResult */); // Match NTWK_ALLOWED_TMP_ALLOWLIST
 
             // Back to background.
             finishActivity();
-            assertBackgroundState();
+            assertProcessStateBelow(PROCESS_STATE_BOUND_FOREGROUND_SERVICE);
             assertNetworkingBlockedStatusForUid(mUid, METERED,
                     true /* expectedResult */); // Match NTWK_BLOCKED_BG_RESTRICT
         } finally {
@@ -219,11 +224,11 @@
             // Make TEST_APP2_PKG go to foreground and isUidRestrictedOnMeteredNetworks() will
             // return false.
             launchActivity();
-            assertForegroundState();
+            assertTopState();
             assertIsUidRestrictedOnMeteredNetworks(mUid, false /* expectedResult */);
             // Back to background.
             finishActivity();
-            assertBackgroundState();
+            assertProcessStateBelow(PROCESS_STATE_BOUND_FOREGROUND_SERVICE);
 
             // Add mUid into restrict background whitelist and isUidRestrictedOnMeteredNetworks()
             // will return false.
@@ -237,4 +242,33 @@
             assertIsUidRestrictedOnMeteredNetworks(mUid, false /* expectedResult */);
         }
     }
+
+    @Test
+    public void testIsUidNetworkingBlocked_whenInBackground() throws Exception {
+        assumeTrue("Feature not enabled", isNetworkBlockedForTopSleepingAndAbove());
+
+        try {
+            assertProcessStateBelow(PROCESS_STATE_TOP_SLEEPING);
+            SystemClock.sleep(PROCESS_STATE_TRANSITION_DELAY_MS);
+            assertNetworkingBlockedStatusForUid(mUid, METERED, true /* expectedResult */);
+            assertTrue(isUidNetworkingBlocked(mUid, NON_METERED));
+
+            launchActivity();
+            assertTopState();
+            assertNetworkingBlockedStatusForUid(mUid, METERED, false /* expectedResult */);
+            assertFalse(isUidNetworkingBlocked(mUid, NON_METERED));
+
+            finishActivity();
+            assertProcessStateBelow(PROCESS_STATE_TOP_SLEEPING);
+            SystemClock.sleep(PROCESS_STATE_TRANSITION_DELAY_MS);
+            assertNetworkingBlockedStatusForUid(mUid, METERED, true /* expectedResult */);
+            assertTrue(isUidNetworkingBlocked(mUid, NON_METERED));
+
+            addPowerSaveModeWhitelist(TEST_APP2_PKG);
+            assertNetworkingBlockedStatusForUid(mUid, METERED, false /* expectedResult */);
+            assertFalse(isUidNetworkingBlocked(mUid, NON_METERED));
+        } finally {
+            removePowerSaveModeWhitelist(TEST_APP2_PKG);
+        }
+    }
 }
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/RestrictedModeTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/RestrictedModeTest.java
index 35f1f1c..4777bf4 100644
--- 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
@@ -38,7 +38,7 @@
         // go to foreground state and enable restricted mode
         launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_ACTIVTIY);
         setRestrictedNetworkingMode(true);
-        assertForegroundNetworkAccess(false);
+        assertTopNetworkAccess(false);
 
         // go to background state
         finishActivity();
@@ -47,7 +47,7 @@
         // disable restricted mode and assert network access in foreground and background states
         setRestrictedNetworkingMode(false);
         launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_ACTIVTIY);
-        assertForegroundNetworkAccess(true);
+        assertTopNetworkAccess(true);
 
         // go to background state
         finishActivity();
diff --git a/tests/cts/hostside/app2/Android.bp b/tests/cts/hostside/app2/Android.bp
index db92f5c..c526172 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"],
 }
 
diff --git a/tests/cts/hostside/certs/Android.bp b/tests/cts/hostside/certs/Android.bp
index 60b5476..301973e 100644
--- a/tests/cts/hostside/certs/Android.bp
+++ b/tests/cts/hostside/certs/Android.bp
@@ -1,4 +1,5 @@
 package {
+    default_team: "trendy_team_fwk_core_networking",
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
diff --git a/common/TrunkStable.bp b/tests/cts/hostside/instrumentation_arguments/Android.bp
similarity index 73%
rename from common/TrunkStable.bp
rename to tests/cts/hostside/instrumentation_arguments/Android.bp
index 59874c2..cdede36 100644
--- a/common/TrunkStable.bp
+++ b/tests/cts/hostside/instrumentation_arguments/Android.bp
@@ -1,5 +1,4 @@
-//
-// Copyright (C) 2023 The Android Open Source Project
+// Copyright (C) 2024 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -12,5 +11,12 @@
 // WITHOUT WARRANTIES 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/instrumentation_arguments/src/com/android/cts/net/arguments/InstrumentationArguments.java b/tests/cts/hostside/instrumentation_arguments/src/com/android/cts/net/arguments/InstrumentationArguments.java
new file mode 100644
index 0000000..472e347
--- /dev/null
+++ b/tests/cts/hostside/instrumentation_arguments/src/com/android/cts/net/arguments/InstrumentationArguments.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.cts.net.arguments;
+
+public interface InstrumentationArguments {
+    String ARG_WAIVE_BIND_PRIORITY = "waive_bind_priority";
+}
diff --git a/tests/cts/hostside/networkslicingtestapp/Android.bp b/tests/cts/hostside/networkslicingtestapp/Android.bp
index 2aa3f69..100b6e4 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,8 +40,8 @@
 android_test_helper_app {
     name: "CtsHostsideNetworkCapTestsAppWithoutProperty",
     defaults: [
-           "cts_support_defaults",
-           "CtsHostsideNetworkCapTestsAppDefaults"
+        "cts_support_defaults",
+        "CtsHostsideNetworkCapTestsAppDefaults",
     ],
     manifest: "AndroidManifestWithoutProperty.xml",
 }
@@ -48,8 +49,8 @@
 android_test_helper_app {
     name: "CtsHostsideNetworkCapTestsAppWithProperty",
     defaults: [
-           "cts_support_defaults",
-           "CtsHostsideNetworkCapTestsAppDefaults"
+        "cts_support_defaults",
+        "CtsHostsideNetworkCapTestsAppDefaults",
     ],
     manifest: "AndroidManifestWithProperty.xml",
 }
@@ -57,8 +58,8 @@
 android_test_helper_app {
     name: "CtsHostsideNetworkCapTestsAppSdk33",
     defaults: [
-           "cts_support_defaults",
-           "CtsHostsideNetworkCapTestsAppDefaults"
+        "cts_support_defaults",
+        "CtsHostsideNetworkCapTestsAppDefaults",
     ],
     target_sdk_version: "33",
     manifest: "AndroidManifestWithoutProperty.xml",
diff --git a/tests/cts/hostside/src/com/android/cts/net/HostsideConnOnActivityStartTest.java b/tests/cts/hostside/src/com/android/cts/net/HostsideConnOnActivityStartTest.java
index 849ac7c..880e826 100644
--- a/tests/cts/hostside/src/com/android/cts/net/HostsideConnOnActivityStartTest.java
+++ b/tests/cts/hostside/src/com/android/cts/net/HostsideConnOnActivityStartTest.java
@@ -16,6 +16,8 @@
 
 package com.android.cts.net;
 
+import static com.android.cts.net.arguments.InstrumentationArguments.ARG_WAIVE_BIND_PRIORITY;
+
 import android.platform.test.annotations.FlakyTest;
 
 import com.android.testutils.SkipPresubmit;
@@ -26,9 +28,12 @@
 
 import org.junit.Test;
 
+import java.util.Map;
+
 @SkipPresubmit(reason = "Out of SLO flakiness")
 public class HostsideConnOnActivityStartTest extends HostsideNetworkTestCase {
     private static final String TEST_CLASS = TEST_PKG + ".ConnOnActivityStartTest";
+
     @BeforeClassWithInfo
     public static void setUpOnce(TestInformation testInfo) throws Exception {
         uninstallPackage(testInfo, TEST_APP2_PKG, false);
@@ -60,4 +65,11 @@
     public void testStartActivity_appStandby() throws Exception {
         runDeviceTests(TEST_PKG, TEST_CLASS, "testStartActivity_appStandby");
     }
+
+    // TODO(b/321848487): Annotate with @RequiresFlagsEnabled to mirror the device-side test.
+    @Test
+    public void testStartActivity_default() throws Exception {
+        runDeviceTestsWithArgs(TEST_PKG, TEST_CLASS, "testStartActivity_default",
+                Map.of(ARG_WAIVE_BIND_PRIORITY, "true"));
+    }
 }
diff --git a/tests/cts/hostside/src/com/android/cts/net/HostsideDefaultNetworkRestrictionsTests.java b/tests/cts/hostside/src/com/android/cts/net/HostsideDefaultNetworkRestrictionsTests.java
new file mode 100644
index 0000000..0d01fc1
--- /dev/null
+++ b/tests/cts/hostside/src/com/android/cts/net/HostsideDefaultNetworkRestrictionsTests.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.cts.net;
+
+import static com.android.cts.net.arguments.InstrumentationArguments.ARG_WAIVE_BIND_PRIORITY;
+
+import com.android.testutils.SkipPresubmit;
+import com.android.tradefed.device.DeviceNotAvailableException;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.Map;
+
+// TODO(b/321848487): Annotate with @RequiresFlagsEnabled to mirror the device-side tests.
+@SkipPresubmit(reason = "Monitoring for flakiness")
+public class HostsideDefaultNetworkRestrictionsTests extends HostsideNetworkTestCase {
+    private static final String METERED_TEST_CLASS = TEST_PKG + ".DefaultRestrictionsMeteredTest";
+    private static final String NON_METERED_TEST_CLASS =
+            TEST_PKG + ".DefaultRestrictionsNonMeteredTest";
+
+    @Before
+    public void setUp() throws Exception {
+        uninstallPackage(TEST_APP2_PKG, false);
+        installPackage(TEST_APP2_APK);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        uninstallPackage(TEST_APP2_PKG, true);
+    }
+
+    private void runMeteredTest(String methodName) throws DeviceNotAvailableException {
+        runDeviceTestsWithArgs(TEST_PKG, METERED_TEST_CLASS, methodName,
+                Map.of(ARG_WAIVE_BIND_PRIORITY, "true"));
+    }
+
+    private void runNonMeteredTest(String methodName) throws DeviceNotAvailableException {
+        runDeviceTestsWithArgs(TEST_PKG, NON_METERED_TEST_CLASS, methodName,
+                Map.of(ARG_WAIVE_BIND_PRIORITY, "true"));
+    }
+
+    @Test
+    public void testMeteredNetworkAccess_defaultRestrictions_testActivityNetworkAccess()
+            throws Exception {
+        runMeteredTest("testActivityNetworkAccess");
+    }
+
+    @Test
+    public void testMeteredNetworkAccess_defaultRestrictions_testFgsNetworkAccess()
+            throws Exception {
+        runMeteredTest("testFgsNetworkAccess");
+    }
+
+    @Test
+    public void testMeteredNetworkAccess_defaultRestrictions_inFullAllowlist() throws Exception {
+        runMeteredTest("testBackgroundNetworkAccess_inFullAllowlist");
+    }
+
+    @Test
+    public void testMeteredNetworkAccess_defaultRestrictions_inExceptIdleAllowlist()
+            throws Exception {
+        runMeteredTest("testBackgroundNetworkAccess_inExceptIdleAllowlist");
+    }
+
+    @Test
+    public void testNonMeteredNetworkAccess_defaultRestrictions_testActivityNetworkAccess()
+            throws Exception {
+        runNonMeteredTest("testActivityNetworkAccess");
+    }
+
+    @Test
+    public void testNonMeteredNetworkAccess_defaultRestrictions_testFgsNetworkAccess()
+            throws Exception {
+        runNonMeteredTest("testFgsNetworkAccess");
+    }
+
+    @Test
+    public void testNonMeteredNetworkAccess_defaultRestrictions_inFullAllowlist() throws Exception {
+        runNonMeteredTest("testBackgroundNetworkAccess_inFullAllowlist");
+    }
+
+    @Test
+    public void testNonMeteredNetworkAccess_defaultRestrictions_inExceptIdleAllowlist()
+            throws Exception {
+        runNonMeteredTest("testBackgroundNetworkAccess_inExceptIdleAllowlist");
+    }
+}
diff --git a/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkCallbackTests.java b/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkCallbackTests.java
index 04bd1ad..361f7c7 100644
--- a/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkCallbackTests.java
+++ b/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkCallbackTests.java
@@ -15,12 +15,16 @@
  */
 package com.android.cts.net;
 
+import static com.android.cts.net.arguments.InstrumentationArguments.ARG_WAIVE_BIND_PRIORITY;
+
 import com.android.testutils.SkipPresubmit;
 
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 
+import java.util.Map;
+
 @SkipPresubmit(reason = "Out of SLO flakiness")
 public class HostsideNetworkCallbackTests extends HostsideNetworkTestCase {
 
@@ -46,5 +50,12 @@
         runDeviceTests(TEST_PKG,
                 TEST_PKG + ".NetworkCallbackTest", "testOnBlockedStatusChanged_powerSaver");
     }
+
+    // TODO(b/321848487): Annotate with @RequiresFlagsEnabled to mirror the device-side test.
+    @Test
+    public void testOnBlockedStatusChanged_default() throws Exception {
+        runDeviceTestsWithArgs(TEST_PKG, TEST_PKG + ".NetworkCallbackTest",
+                "testOnBlockedStatusChanged_default", Map.of(ARG_WAIVE_BIND_PRIORITY, "true"));
+    }
 }
 
diff --git a/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkPolicyManagerTests.java b/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkPolicyManagerTests.java
index 3ddb88b..e97db58 100644
--- a/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkPolicyManagerTests.java
+++ b/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkPolicyManagerTests.java
@@ -16,10 +16,14 @@
 
 package com.android.cts.net;
 
+import static com.android.cts.net.arguments.InstrumentationArguments.ARG_WAIVE_BIND_PRIORITY;
+
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 
+import java.util.Map;
+
 public class HostsideNetworkPolicyManagerTests extends HostsideNetworkTestCase {
     @Before
     public void setUp() throws Exception {
@@ -71,4 +75,12 @@
         runDeviceTests(TEST_PKG,
                 TEST_PKG + ".NetworkPolicyManagerTest", "testIsUidRestrictedOnMeteredNetworks");
     }
+
+    // TODO(b/321848487): Annotate with @RequiresFlagsEnabled to mirror the device-side test.
+    @Test
+    public void testIsUidNetworkingBlocked_whenInBackground() throws Exception {
+        runDeviceTestsWithArgs(TEST_PKG, TEST_PKG + ".NetworkPolicyManagerTest",
+                "testIsUidNetworkingBlocked_whenInBackground",
+                Map.of(ARG_WAIVE_BIND_PRIORITY, "true"));
+    }
 }
diff --git a/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkTestCase.java b/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkTestCase.java
index 3358fd7..ca95ed6 100644
--- a/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkTestCase.java
+++ b/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkTestCase.java
@@ -31,10 +31,13 @@
 import com.android.tradefed.testtype.junit4.AfterClassWithInfo;
 import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
 import com.android.tradefed.testtype.junit4.BeforeClassWithInfo;
+import com.android.tradefed.testtype.junit4.DeviceTestRunOptions;
 import com.android.tradefed.util.RunUtil;
 
 import org.junit.runner.RunWith;
 
+import java.util.Map;
+
 @RunWith(DeviceJUnit4ClassRunner.class)
 abstract class HostsideNetworkTestCase extends BaseHostJUnit4Test {
     protected static final boolean DEBUG = false;
@@ -146,6 +149,17 @@
                 + packageName + ", u=" + currentUser);
     }
 
+    protected boolean runDeviceTestsWithArgs(String packageName, String className,
+            String methodName, Map<String, String> args) throws DeviceNotAvailableException {
+        final DeviceTestRunOptions deviceTestRunOptions = new DeviceTestRunOptions(packageName)
+                .setTestClassName(className)
+                .setTestMethodName(methodName);
+        for (Map.Entry<String, String> arg : args.entrySet()) {
+            deviceTestRunOptions.addInstrumentationArg(arg.getKey(), arg.getValue());
+        }
+        return runDeviceTests(deviceTestRunOptions);
+    }
+
     protected String runCommand(String command) throws DeviceNotAvailableException {
         Log.d(TAG, "Command: '" + command + "'");
         final String output = getDevice().executeShellCommand(command);
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 aa90f5f..fa68e3e 100644
--- a/tests/cts/hostside/src/com/android/cts/net/ProcNetTest.java
+++ b/tests/cts/hostside/src/com/android/cts/net/ProcNetTest.java
@@ -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..5ac4229
--- /dev/null
+++ b/tests/cts/multidevices/Android.bp
@@ -0,0 +1,42 @@
+// 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"],
+}
+
+python_test_host {
+    name: "CtsConnectivityMultiDevicesTestCases",
+    main: "connectivity_multi_devices_test.py",
+    srcs: ["connectivity_multi_devices_test.py"],
+    libs: [
+        "mobly",
+    ],
+    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..ab88504
--- /dev/null
+++ b/tests/cts/multidevices/connectivity_multi_devices_test.py
@@ -0,0 +1,110 @@
+# Lint as: python3
+"""Connectivity multi devices tests."""
+import base64
+import sys
+import uuid
+
+from mobly import asserts
+from mobly import base_test
+from mobly import test_runner
+from mobly import utils
+from mobly.controllers import android_device
+
+CONNECTIVITY_MULTI_DEVICES_SNIPPET_PACKAGE = "com.google.snippet.connectivity"
+
+
+class UpstreamType:
+  CELLULAR = 1
+  WIFI = 2
+
+
+class ConnectivityMultiDevicesTest(base_test.BaseTestClass):
+
+  def setup_class(self):
+    # Declare that two Android devices are needed.
+    self.clientDevice, self.serverDevice = self.register_controller(
+        android_device, min_number=2
+    )
+
+    def setup_device(device):
+      device.load_snippet(
+          "connectivity_multi_devices_snippet",
+          CONNECTIVITY_MULTI_DEVICES_SNIPPET_PACKAGE,
+      )
+
+    # Set up devices in parallel to save time.
+    utils.concurrent_exec(
+        setup_device,
+        ((self.clientDevice,), (self.serverDevice,)),
+        max_workers=2,
+        raise_on_exception=True,
+    )
+
+  @staticmethod
+  def generate_uuid32_base64():
+    """Generates a UUID32 and encodes it in Base64.
+
+    Returns:
+        str: The Base64-encoded UUID32 string. Which is 22 characters.
+    """
+    return base64.b64encode(uuid.uuid1().bytes).decode("utf-8").strip("=")
+
+  def _do_test_hotspot_for_upstream_type(self, upstream_type):
+    """Test hotspot with the specified upstream type.
+
+    This test create a hotspot, make the client connect
+    to it, and verify the packet is forwarded by the hotspot.
+    """
+    server = self.serverDevice.connectivity_multi_devices_snippet
+    client = self.clientDevice.connectivity_multi_devices_snippet
+
+    # Assert pre-conditions specific to each upstream type.
+    asserts.skip_if(not client.hasWifiFeature(), "Client requires Wifi feature")
+    asserts.skip_if(
+      not server.hasHotspotFeature(), "Server requires hotspot feature"
+    )
+    if upstream_type == UpstreamType.CELLULAR:
+      asserts.skip_if(
+          not server.hasTelephonyFeature(), "Server requires Telephony feature"
+      )
+      server.requestCellularAndEnsureDefault()
+    elif upstream_type == UpstreamType.WIFI:
+      asserts.skip_if(
+          not server.isStaApConcurrencySupported(),
+          "Server requires Wifi AP + STA concurrency",
+      )
+      server.ensureWifiIsDefault()
+    else:
+      raise ValueError(f"Invalid upstream type: {upstream_type}")
+
+    # Generate ssid/passphrase with random characters to make sure nearby devices won't
+    # connect unexpectedly. Note that total length of ssid cannot go over 32.
+    testSsid = "HOTSPOT-" + self.generate_uuid32_base64()
+    testPassphrase = self.generate_uuid32_base64()
+
+    try:
+      # Create a hotspot with fixed SSID and password.
+      server.startHotspot(testSsid, testPassphrase)
+
+      # Make the client connects to the hotspot.
+      client.connectToWifi(testSsid, testPassphrase, True)
+
+    finally:
+      if upstream_type == UpstreamType.CELLULAR:
+        server.unrequestCellular()
+      # Teardown the hotspot.
+      server.stopAllTethering()
+
+  def test_hotspot_upstream_wifi(self):
+    self._do_test_hotspot_for_upstream_type(UpstreamType.WIFI)
+
+  def test_hotspot_upstream_cellular(self):
+    self._do_test_hotspot_for_upstream_type(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..5940cbb
--- /dev/null
+++ b/tests/cts/multidevices/snippet/Android.bp
@@ -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 {
+    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",
+    ],
+    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..9ed8146
--- /dev/null
+++ b/tests/cts/multidevices/snippet/AndroidManifest.xml
@@ -0,0 +1,39 @@
+<?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" />
+  <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" />
+  </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..115210b
--- /dev/null
+++ b/tests/cts/multidevices/snippet/ConnectivityMultiDevicesSnippet.kt
@@ -0,0 +1,170 @@
+/*
+ * 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.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.WifiManager
+import android.net.wifi.WifiNetworkSpecifier
+import android.net.wifi.WifiSsid
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.testutils.ConnectUtil
+import com.android.testutils.RecorderCallback.CallbackEntry.Available
+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
+
+class ConnectivityMultiDevicesSnippet : Snippet {
+    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 ctsTetheringUtils = CtsTetheringUtils(context)
+    private var oldSoftApConfig: SoftApConfiguration? = null
+
+    @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 = "Request cellular connection and ensure it is the default network.")
+    fun requestCellularAndEnsureDefault() {
+        ctsNetUtils.disableWifi()
+        val network = ctsNetUtils.connectToCell()
+        ctsNetUtils.expectNetworkIsSystemDefault(network)
+    }
+
+    @Rpc(description = "Unrequest cellular connection.")
+    fun unrequestCellular() {
+        ctsNetUtils.disconnectFromCell()
+    }
+
+    @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, requireValidation: Boolean): Network {
+        val specifier = WifiNetworkSpecifier.Builder()
+            .setSsid(ssid)
+            .setWpa2Passphrase(passphrase)
+            .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)
+
+        // Register network callback for the specific wifi.
+        val networkCallback = TestableNetworkCallback()
+        val wifiRequest = NetworkRequest.Builder().addTransportType(TRANSPORT_WIFI)
+            .setNetworkSpecifier(specifier)
+            .build()
+        cm.registerNetworkCallback(wifiRequest, networkCallback)
+
+        try {
+            // Add the test configuration and connect to it.
+            val connectUtil = ConnectUtil(context)
+            connectUtil.connectToWifiConfig(wifiConfig)
+
+            val event = networkCallback.expect<Available>()
+            if (requireValidation) {
+                networkCallback.eventuallyExpect<CapabilitiesChanged> {
+                    it.caps.hasCapability(NET_CAPABILITY_VALIDATED)
+                }
+            }
+            return event.network
+        } finally {
+            cm.unregisterNetworkCallback(networkCallback)
+        }
+    }
+
+    @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) {
+        // 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()
+            ctsTetheringUtils.startWifiTethering(tetheringCallback)
+        } 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/net/Android.bp b/tests/cts/net/Android.bp
index 9310888..074c587 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"],
 }
 
@@ -54,27 +55,26 @@
         "junit",
         "junit-params",
         "modules-utils-build",
+        "net-tests-utils",
         "net-utils-framework-common",
         "truth",
         "TetheringIntegrationTestsBaseLib",
     ],
 
-    // uncomment when b/13249961 is fixed
-    // sdk_version: "current",
-    platform_apis: true,
+    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: [
@@ -87,6 +87,14 @@
     ],
     test_suites: [
         "cts",
+        "mts-dnsresolver",
+        "mts-networking",
+        "mts-tethering",
+        "mts-wifi",
+        "mcts-dnsresolver",
+        "mcts-networking",
+        "mcts-tethering",
+        "mcts-wifi",
         "general-tests",
     ],
 }
@@ -131,6 +139,7 @@
         "cts",
         "general-tests",
         "mts-tethering",
+        "mcts-tethering",
     ],
 }
 
diff --git a/tests/cts/net/api23Test/Android.bp b/tests/cts/net/api23Test/Android.bp
index 1f1dd5d..2ec3a70 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"],
 }
 
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/jni/Android.bp b/tests/cts/net/jni/Android.bp
index a421349..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"],
 }
 
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 da4fe28..8e24fba 100644
--- a/tests/cts/net/native/dns/Android.bp
+++ b/tests/cts/net/native/dns/Android.bp
@@ -1,4 +1,5 @@
 package {
+    default_team: "trendy_team_fwk_core_networking",
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
@@ -49,5 +50,7 @@
         "general-tests",
         "mts-dnsresolver",
         "mts-networking",
+        "mcts-dnsresolver",
+        "mcts-networking",
     ],
 }
diff --git a/tests/cts/net/src/android/net/cts/BatteryStatsManagerTest.java b/tests/cts/net/src/android/net/cts/BatteryStatsManagerTest.java
index 466514c..3e5d0ba 100644
--- a/tests/cts/net/src/android/net/cts/BatteryStatsManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/BatteryStatsManagerTest.java
@@ -199,7 +199,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,
diff --git a/tests/cts/net/src/android/net/cts/ConnectivityDiagnosticsManagerTest.java b/tests/cts/net/src/android/net/cts/ConnectivityDiagnosticsManagerTest.java
index e0fe929..ceb48d4 100644
--- a/tests/cts/net/src/android/net/cts/ConnectivityDiagnosticsManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/ConnectivityDiagnosticsManagerTest.java
@@ -298,17 +298,6 @@
                 },
                 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.
-        runWithShellPermissionIdentity(
-                () -> mConnectivityManager.requestNetwork(
-                        CELLULAR_NETWORK_REQUEST, testNetworkCallback),
-                android.Manifest.permission.CONNECTIVITY_INTERNAL);
-
-        final Network network = testNetworkCallback.waitForAvailable();
-        assertNotNull(network);
-
         assertTrue("Didn't receive broadcast for ACTION_CARRIER_CONFIG_CHANGED for subId=" + subId,
                 carrierConfigReceiver.waitForCarrierConfigChanged());
 
@@ -324,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
diff --git a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
index 544f300..cdf8340 100644
--- a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
@@ -802,7 +802,9 @@
             assertNull(redactedNormal.getUids());
             assertNull(redactedNormal.getSsid());
             assertNull(redactedNormal.getUnderlyingNetworks());
-            assertEquals(0, redactedNormal.getSubscriptionIds().size());
+            // TODO: Make subIds public and update to verify the size is 2
+            final int subIdsSize = redactedNormal.getSubscriptionIds().size();
+            assertTrue(subIdsSize == 0 || subIdsSize == 2);
             assertEquals(WifiInfo.DEFAULT_MAC_ADDRESS,
                     ((WifiInfo) redactedNormal.getTransportInfo()).getBSSID());
             assertEquals(rssi, ((WifiInfo) redactedNormal.getTransportInfo()).getRssi());
@@ -1353,9 +1355,7 @@
     public void testToggleWifiConnectivityAction() throws Exception {
         assumeTrue(mPackageManager.hasSystemFeature(FEATURE_WIFI));
 
-        // toggleWifi calls connectToWifi and disconnectFromWifi, which both wait for
-        // CONNECTIVITY_ACTION broadcasts.
-        mCtsNetUtils.toggleWifi();
+        mCtsNetUtils.reconnectWifiAndWaitForConnectivityAction();
     }
 
     /** Verify restricted networks cannot be requested. */
@@ -1554,6 +1554,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.
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 9ff0f2f..752891f 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,11 +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;
@@ -80,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();
 
@@ -123,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();
@@ -144,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) {
diff --git a/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java b/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java
index 6b7954a..f6a025a 100644
--- a/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java
+++ b/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java
@@ -648,7 +648,7 @@
                         testIpv6Only, requiresValidation, testSessionKey , testIkeTunConnParams)));
     }
 
-    @Test
+    @Test @IgnoreUpTo(SC_V2)
     public void testStartStopVpnProfileV4() throws Exception {
         doTestStartStopVpnProfile(false /* testIpv6Only */, false /* requiresValidation */,
                 false /* testSessionKey */, false /* testIkeTunConnParams */);
@@ -660,7 +660,7 @@
                 false /* testSessionKey */, false /* testIkeTunConnParams */);
     }
 
-    @Test
+    @Test @IgnoreUpTo(SC_V2)
     public void testStartStopVpnProfileV6() throws Exception {
         doTestStartStopVpnProfile(true /* testIpv6Only */, false /* requiresValidation */,
                 false /* testSessionKey */, false /* testIkeTunConnParams */);
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/MdnsTestUtils.kt b/tests/cts/net/src/android/net/cts/MdnsTestUtils.kt
index eef3f87..5ba6c4c 100644
--- a/tests/cts/net/src/android/net/cts/MdnsTestUtils.kt
+++ b/tests/cts/net/src/android/net/cts/MdnsTestUtils.kt
@@ -23,11 +23,15 @@
 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
@@ -236,19 +240,28 @@
 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))
+            predicate(TestDnsPacket(mdnsPayload, dst))
         } catch (e: DnsPacket.ParseException) {
             false
         }
     }
-    return poll(timeoutMs, mdnsProbeFilter)?.let { TestDnsPacket(getMdnsPayload(it)) }
+    return poll(timeoutMs, mdnsProbeFilter)?.let {
+        TestDnsPacket(getMdnsPayload(it), getDstAddr(it))
+    }
 }
 
 fun TapPacketReader.pollForProbe(
@@ -281,7 +294,7 @@
     it.isReplyFor("$serviceName.$serviceType.local")
 }
 
-class TestDnsPacket(data: ByteArray) : DnsPacket(data) {
+class TestDnsPacket(data: ByteArray, val dstAddr: InetAddress) : DnsPacket(data) {
     val header: DnsHeader
         get() = mHeader
     val records: Array<List<DnsRecord>>
@@ -290,9 +303,10 @@
         it.dName == name && it.nsType == DnsResolver.TYPE_ANY
     }
 
-    fun isReplyFor(name: String): Boolean = mRecords[ANSECTION].any {
-        it.dName == name && it.nsType == DnsResolver.TYPE_SRV
-    }
+    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 {
diff --git a/tests/cts/net/src/android/net/cts/MultinetworkApiTest.java b/tests/cts/net/src/android/net/cts/MultinetworkApiTest.java
index 17a9ca2..7ab73c2 100644
--- a/tests/cts/net/src/android/net/cts/MultinetworkApiTest.java
+++ b/tests/cts/net/src/android/net/cts/MultinetworkApiTest.java
@@ -16,7 +16,17 @@
 
 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 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;
@@ -28,9 +38,25 @@
 import android.platform.test.annotations.AppModeFull;
 import android.system.ErrnoException;
 import android.system.OsConstants;
-import android.test.AndroidTestCase;
+import android.util.ArraySet;
 
-public class MultinetworkApiTest extends AndroidTestCase {
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.testutils.DeviceConfigRule;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Set;
+
+@RunWith(AndroidJUnit4.class)
+public class MultinetworkApiTest {
+    @Rule
+    public final DeviceConfigRule mDeviceConfigRule = new DeviceConfigRule();
 
     static {
         System.loadLibrary("nativemultinetwork_jni");
@@ -56,24 +82,27 @@
     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();
+    @After
+    public void tearDown() {
+        if (mCtsNetUtils.cellConnectAttempted()) {
+            mCtsNetUtils.disconnectFromCell();
+        }
     }
 
-    public void testGetaddrinfo() throws ErrnoException {
-        for (Network network : mCtsNetUtils.getTestableNetworks()) {
+    @Test
+    public void testGetaddrinfo() throws Exception {
+        for (Network network : getTestableNetworks()) {
             int errno = runGetaddrinfoCheck(network.getNetworkHandle());
             if (errno != 0) {
                 throw new ErrnoException(
@@ -82,13 +111,14 @@
         }
     }
 
+    @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());
 
-        for (Network network : mCtsNetUtils.getTestableNetworks()) {
+        for (Network network : getTestableNetworks()) {
             mCM.setProcessDefaultNetwork(null);
             assertNull(mCM.getProcessDefaultNetwork());
 
@@ -107,7 +137,7 @@
             mCM.setProcessDefaultNetwork(null);
         }
 
-        for (Network network : mCtsNetUtils.getTestableNetworks()) {
+        for (Network network : getTestableNetworks()) {
             NetworkUtils.bindProcessToNetwork(0);
             assertNull(mCM.getBoundNetworkForProcess());
 
@@ -125,9 +155,10 @@
         }
     }
 
+    @Test
     @AppModeFull(reason = "CHANGE_NETWORK_STATE permission can't be granted to instant apps")
-    public void testSetsocknetwork() throws ErrnoException {
-        for (Network network : mCtsNetUtils.getTestableNetworks()) {
+    public void testSetsocknetwork() throws Exception {
+        for (Network network : getTestableNetworks()) {
             int errno = runSetsocknetwork(network.getNetworkHandle());
             if (errno != 0) {
                 throw new ErrnoException(
@@ -136,8 +167,9 @@
         }
     }
 
-    public void testNativeDatagramTransmission() throws ErrnoException {
-        for (Network network : mCtsNetUtils.getTestableNetworks()) {
+    @Test
+    public void testNativeDatagramTransmission() throws Exception {
+        for (Network network : getTestableNetworks()) {
             int errno = runDatagramCheck(network.getNetworkHandle());
             if (errno != 0) {
                 throw new ErrnoException(
@@ -146,7 +178,8 @@
         }
     }
 
-    public void testNoSuchNetwork() {
+    @Test
+    public void testNoSuchNetwork() throws Exception {
         final Network eNoNet = new Network(54321);
         assertNull(mCM.getNetworkInfo(eNoNet));
 
@@ -158,9 +191,10 @@
         // 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 : mCtsNetUtils.getTestableNetworks()) {
+        for (Network network : getTestableNetworks()) {
             long networkHandle = network.getNetworkHandle();
             Network newNetwork = Network.fromNetworkHandle(networkHandle);
             assertEquals(newNetwork, network);
@@ -181,10 +215,9 @@
         } catch (IllegalArgumentException e) {}
     }
 
+    @Test
     public void testResNApi() throws Exception {
-        final Network[] testNetworks = mCtsNetUtils.getTestableNetworks();
-
-        for (Network network : testNetworks) {
+        for (Network network : getTestableNetworks()) {
             // Throws AssertionError directly in jni function if test fail.
             runResNqueryCheck(network.getNetworkHandle());
             runResNsendCheck(network.getNetworkHandle());
@@ -201,14 +234,26 @@
         }
     }
 
+    @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 {
             mCtsNetUtils.setPrivateDnsStrictMode(GOOGLE_PRIVATE_DNS_SERVER);
-            for (Network network : mCtsNetUtils.getTestableNetworks()) {
+            for (Network network : getTestableNetworks()) {
               // Wait for private DNS setting to propagate.
               mCtsNetUtils.awaitPrivateDnsSetting("NxDomain test wait private DNS setting timeout",
                         network, GOOGLE_PRIVATE_DNS_SERVER, true);
@@ -218,4 +263,44 @@
             mCtsNetUtils.restorePrivateDnsSetting();
         }
     }
+
+    /**
+     * Get all testable Networks with internet capability.
+     */
+    private Set<Network> getTestableNetworks() throws InterruptedException {
+        // Obtain cell and Wi-Fi through CtsNetUtils (which uses NetworkCallbacks), as they may have
+        // just been reconnected by the test using NetworkCallbacks, so synchronous calls may not
+        // yet return them (synchronous calls and callbacks should not be mixed for a given
+        // Network).
+        final Set<Network> testableNetworks = new ArraySet<>();
+        if (mContext.getPackageManager().hasSystemFeature(FEATURE_TELEPHONY)) {
+            if (!mCtsNetUtils.cellConnectAttempted()) {
+                mRequestedCellNetwork = mCtsNetUtils.connectToCell();
+            }
+            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 fe2f813..beb9274 100644
--- a/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
+++ b/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
@@ -21,6 +21,7 @@
 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
@@ -70,6 +71,7 @@
 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
@@ -150,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
@@ -574,27 +577,13 @@
     }
 
     private fun doTestAllowedUids(
-            subId: Int,
-            transport: Int,
-            uid: Int,
-            expectUidsPresent: Boolean
-    ) {
-        doTestAllowedUids(subId, intArrayOf(transport), uid, expectUidsPresent)
-    }
-
-    private fun doTestAllowedUids(
-            subId: Int,
             transports: IntArray,
             uid: Int,
-            expectUidsPresent: Boolean
+            expectUidsPresent: Boolean,
+            specifier: NetworkSpecifier?,
+            transportInfo: TransportInfo?
     ) {
         val callback = TestableNetworkCallback(DEFAULT_TIMEOUT_MS)
-        val specifier = when {
-            transports.size != 1 -> null
-            TRANSPORT_ETHERNET in transports -> EthernetNetworkSpecifier("testInterface")
-            TRANSPORT_CELLULAR in transports -> TelephonyNetworkSpecifier(subId)
-            else -> null
-        }
         val agent = createNetworkAgent(initialNc = NetworkCapabilities.Builder().run {
             addTransportType(TRANSPORT_TEST)
             transports.forEach { addTransportType(it) }
@@ -602,10 +591,7 @@
             addCapability(NET_CAPABILITY_NOT_SUSPENDED)
             removeCapability(NET_CAPABILITY_NOT_RESTRICTED)
             setNetworkSpecifier(specifier)
-            if (TRANSPORT_WIFI in transports && SdkLevel.isAtLeastV()) {
-                // setSubscriptionId only exists in V+
-                setTransportInfo(WifiInfo.Builder().setSubscriptionId(subId).build())
-            }
+            setTransportInfo(transportInfo)
             setAllowedUids(setOf(uid))
             setOwnerUid(Process.myUid())
             setAdministratorUids(intArrayOf(Process.myUid()))
@@ -626,9 +612,49 @@
             }
         }
         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,
@@ -722,6 +748,19 @@
     @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"
@@ -734,12 +773,17 @@
 
         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.
-            doTestAllowedUids(defaultSubId, TRANSPORT_CELLULAR, uid, expectUidsPresent = false)
-            doTestAllowedUids(defaultSubId, TRANSPORT_WIFI, uid, expectUidsPresent = false)
-            doTestAllowedUids(defaultSubId, TRANSPORT_BLUETOOTH, uid, expectUidsPresent = false)
+            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.
@@ -782,9 +826,10 @@
                 // TODO(b/315136340): Allow ownerUid to see allowedUids and enable below test case
                 // doTestAllowedUids(defaultSubId, TRANSPORT_WIFI, uid, expectUidsPresent = true)
             }
-            doTestAllowedUids(defaultSubId, TRANSPORT_BLUETOOTH, uid, expectUidsPresent = false)
-            doTestAllowedUids(defaultSubId, intArrayOf(TRANSPORT_CELLULAR, TRANSPORT_WIFI), uid,
+            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 {
diff --git a/tests/cts/net/src/android/net/cts/NetworkRequestTest.java b/tests/cts/net/src/android/net/cts/NetworkRequestTest.java
index 594f3fb..6ec4e62 100644
--- a/tests/cts/net/src/android/net/cts/NetworkRequestTest.java
+++ b/tests/cts/net/src/android/net/cts/NetworkRequestTest.java
@@ -32,6 +32,8 @@
 
 import static com.android.testutils.DevSdkIgnoreRuleKt.VANILLA_ICE_CREAM;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import static junit.framework.Assert.fail;
 
 import static org.junit.Assert.assertArrayEquals;
@@ -62,6 +64,7 @@
 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.IgnoreUpTo;
 
@@ -72,6 +75,7 @@
 import java.util.Set;
 
 @RunWith(AndroidJUnit4.class)
+@ConnectivityModuleTest
 public class NetworkRequestTest {
     @Rule
     public final DevSdkIgnoreRule ignoreRule = new DevSdkIgnoreRule();
@@ -173,6 +177,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());
diff --git a/tests/cts/net/src/android/net/cts/NsdManagerDownstreamTetheringTest.kt b/tests/cts/net/src/android/net/cts/NsdManagerDownstreamTetheringTest.kt
index f374181..1b1f367 100644
--- a/tests/cts/net/src/android/net/cts/NsdManagerDownstreamTetheringTest.kt
+++ b/tests/cts/net/src/android/net/cts/NsdManagerDownstreamTetheringTest.kt
@@ -62,7 +62,7 @@
 
     @Test
     fun testMdnsDiscoveryCanSendPacketOnLocalOnlyDownstreamTetheringInterface() {
-        assumeFalse(isInterfaceForTetheringAvailable)
+        assumeFalse(isInterfaceForTetheringAvailable())
 
         var downstreamIface: TestNetworkInterface? = null
         var tetheringEventCallback: MyTetheringEventCallback? = null
@@ -104,7 +104,7 @@
 
     @Test
     fun testMdnsDiscoveryWorkOnTetheringInterface() {
-        assumeFalse(isInterfaceForTetheringAvailable)
+        assumeFalse(isInterfaceForTetheringAvailable())
         setIncludeTestInterfaces(true)
 
         var downstreamIface: TestNetworkInterface? = null
diff --git a/tests/cts/net/src/android/net/cts/NsdManagerTest.kt b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
index e1ea2b9..ce2c2c1 100644
--- a/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
+++ b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
@@ -43,6 +43,7 @@
 import android.net.cts.NsdDiscoveryRecord.DiscoveryEvent.DiscoveryStopped
 import android.net.cts.NsdDiscoveryRecord.DiscoveryEvent.ServiceFound
 import android.net.cts.NsdDiscoveryRecord.DiscoveryEvent.ServiceLost
+import android.net.cts.NsdRegistrationRecord.RegistrationEvent.RegistrationFailed
 import android.net.cts.NsdRegistrationRecord.RegistrationEvent.ServiceRegistered
 import android.net.cts.NsdRegistrationRecord.RegistrationEvent.ServiceUnregistered
 import android.net.cts.NsdResolveRecord.ResolveEvent.ResolutionStopped
@@ -52,6 +53,7 @@
 import android.net.cts.NsdServiceInfoCallbackRecord.ServiceInfoCallbackEvent.ServiceUpdatedLost
 import android.net.cts.NsdServiceInfoCallbackRecord.ServiceInfoCallbackEvent.UnregisterCallbackSucceeded
 import android.net.cts.util.CtsNetUtils
+import android.net.nsd.DiscoveryRequest
 import android.net.nsd.NsdManager
 import android.net.nsd.NsdServiceInfo
 import android.net.nsd.OffloadEngine
@@ -60,6 +62,7 @@
 import android.os.Handler
 import android.os.HandlerThread
 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
@@ -68,6 +71,7 @@
 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
@@ -77,12 +81,14 @@
 import com.android.modules.utils.build.SdkLevel.isAtLeastU
 import com.android.net.module.util.DnsPacket
 import com.android.net.module.util.HexDump
+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.IgnoreAfter
 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.RecorderCallback.CallbackEntry.CapabilitiesChanged
 import com.android.testutils.RecorderCallback.CallbackEntry.LinkPropertiesChanged
 import com.android.testutils.TapPacketReader
@@ -132,6 +138,7 @@
 private const val TEST_PORT = 12345
 private const val MDNS_PORT = 5353.toShort()
 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)
@@ -143,6 +150,9 @@
     @get:Rule
     val ignoreRule = DevSdkIgnoreRule()
 
+    @get:Rule
+    val deviceConfigRule = DeviceConfigRule()
+
     private val context by lazy { InstrumentationRegistry.getInstrumentation().context }
     private val nsdManager by lazy {
         context.getSystemService(NsdManager::class.java) ?: fail("Could not get NsdManager service")
@@ -151,7 +161,11 @@
     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 handlerThread = HandlerThread(NsdManagerTest::class.java.simpleName)
     private val ctsNetUtils by lazy{ CtsNetUtils(context) }
 
@@ -670,6 +684,48 @@
         }
     }
 
+    @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)
@@ -677,11 +733,12 @@
         assertEquals(listOf("_subtype"), serviceInfo.subtypes)
         assertTrue(serviceInfo.hostname.startsWith("Android_"))
         assertTrue(serviceInfo.hostname.endsWith("local"))
-        assertEquals(0, serviceInfo.priority)
+        // 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)
+        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)
@@ -992,6 +1049,199 @@
     }
 
     @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())
@@ -1029,6 +1279,83 @@
     }
 
     @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())
@@ -1103,6 +1430,121 @@
         }
     }
 
+    @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 {
+            repeat(3) {
+                assertNotNull(packetReader.pollForAdvertisement(serviceName, serviceType),
+                        "Expect 3 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.
@@ -1161,7 +1603,8 @@
         // Resolve service on testNetwork1
         val resolveRecord = NsdResolveRecord()
         val packetReader = TapPacketReader(Handler(handlerThread.looper),
-                testNetwork1.iface.fileDescriptor.fileDescriptor, 1500 /* maxPacketSize */)
+                testNetwork1.iface.fileDescriptor.fileDescriptor, 1500 /* maxPacketSize */
+        )
         packetReader.startAsyncForTest()
         handlerThread.waitForIdle(TIMEOUT_MS)
 
@@ -1224,6 +1667,445 @@
                 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>()
+        }
+    }
+
+    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_unionOfAddressesFound() {
+        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 hostAddresses3 = listOf(
+                parseNumericAddress("2001:db8::3"),
+                parseNumericAddress("2001:db8::5"))
+        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 si3 = NsdServiceInfo().also {
+            it.network = testNetwork1.network
+            it.serviceName = serviceName3
+            it.serviceType = serviceType
+            it.port = TEST_PORT + 1
+            it.hostname = customHostname
+            it.hostAddresses = hostAddresses3
+        }
+
+        val registrationRecord1 = NsdRegistrationRecord()
+        val registrationRecord2 = NsdRegistrationRecord()
+        val registrationRecord3 = NsdRegistrationRecord()
+
+        val discoveryRecord = NsdDiscoveryRecord()
+        tryTest {
+            registerService(registrationRecord1, si1)
+            registerService(registrationRecord2, si2)
+
+            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(TEST_PORT, resolvedInfo1.port)
+            assertEquals(si1.hostname, resolvedInfo1.hostname)
+            assertAddressEquals(
+                    hostAddresses1 + hostAddresses2,
+                    resolvedInfo1.hostAddresses)
+
+            registerService(registrationRecord3, si3)
+
+            val discoveredInfo2 = discoveryRecord.waitForServiceDiscovered(
+                    serviceName3, serviceType, testNetwork1.network)
+            val resolvedInfo2 = resolveService(discoveredInfo2)
+
+            assertEquals(TEST_PORT + 1, resolvedInfo2.port)
+            assertEquals(si2.hostname, resolvedInfo2.hostname)
+            assertAddressEquals(
+                    hostAddresses1 + hostAddresses2 + hostAddresses3,
+                    resolvedInfo2.hostAddresses)
+        } cleanupStep {
+            nsdManager.stopServiceDiscovery(discoveryRecord)
+
+            discoveryRecord.expectCallbackEventually<DiscoveryStopped>()
+        } cleanup {
+            nsdManager.unregisterService(registrationRecord1)
+            nsdManager.unregisterService(registrationRecord2)
+            nsdManager.unregisterService(registrationRecord3)
+        }
+    }
+
+    @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)
+        }
+    }
+
     private fun buildConflictingAnnouncement(): ByteBuffer {
         /*
         Generated with:
@@ -1240,6 +2122,22 @@
         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.
@@ -1256,6 +2154,19 @@
         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)
@@ -1268,7 +2179,10 @@
         replaceAll(buffer, source, replacement)
     }
 
-    private fun buildMdnsPacket(mdnsPayload: ByteArray): ByteBuffer {
+    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)
@@ -1283,7 +2197,7 @@
                 0x60000000, // version=6, traffic class=0x0, flowlabel=0x0
                 IPPROTO_UDP.toByte(),
                 64 /* hop limit */,
-                parseNumericAddress("2001:db8::123") as Inet6Address /* srcIp */,
+                srcAddr,
                 multicastIpv6Addr /* dstIp */)
         packetBuilder.writeUdpHeader(MDNS_PORT /* srcPort */, MDNS_PORT /* dstPort */)
         packetBuffer.put(mdnsPayload)
@@ -1305,6 +2219,18 @@
         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()
         nsdManager.resolveService(discoveredInfo, Executor { it.run() }, record)
@@ -1339,3 +2265,9 @@
     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())
+}
\ No newline at end of file
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/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 96330e2..3d828a4 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
@@ -173,21 +173,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)
diff --git a/tests/cts/netpermission/internetpermission/Android.bp b/tests/cts/netpermission/internetpermission/Android.bp
index 5314396..7d5ca2f 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"],
 }
 
diff --git a/tests/cts/netpermission/updatestatspermission/Android.bp b/tests/cts/netpermission/updatestatspermission/Android.bp
index 40474db..2fde1ce 100644
--- a/tests/cts/netpermission/updatestatspermission/Android.bp
+++ b/tests/cts/netpermission/updatestatspermission/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/tethering/Android.bp b/tests/cts/tethering/Android.bp
index 4284f56..3928961 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"],
 }
 
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 f705e34..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"],
 }
@@ -74,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 cea83c7..1821329 100644
--- a/tests/integration/AndroidManifest.xml
+++ b/tests/integration/AndroidManifest.xml
@@ -42,6 +42,9 @@
     <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/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt b/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt
index 496d163..361d68c 100644
--- a/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt
+++ b/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt
@@ -37,28 +37,35 @@
 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 kotlin.test.assertEquals
 import kotlin.test.assertNotNull
 import kotlin.test.assertTrue
@@ -74,14 +81,14 @@
 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
+import java.util.function.Consumer
+import java.util.function.BiConsumer
 
 const val SERVICE_BIND_TIMEOUT_MS = 5_000L
 const val TEST_TIMEOUT_MS = 10_000L
@@ -90,7 +97,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.
@@ -117,6 +125,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.
@@ -197,7 +207,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)
@@ -208,31 +218,61 @@
     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)
+
+        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>
+        ): CarrierPrivilegeAuthenticator {
+            return CarrierPrivilegeAuthenticator(context,
+                object : CarrierPrivilegeAuthenticator.Dependencies() {
+                    override fun makeHandlerThread(): HandlerThread =
+                        super.makeHandlerThread().also { handlerThreads.add(it) }
+                },
+                tm, TelephonyManagerShimImpl.newInstance(tm),
+                requestRestrictedWifiEnabled, listener)
+        }
+
+        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
@@ -254,13 +294,18 @@
         na.addCapability(NET_CAPABILITY_INTERNET)
         na.connect()
 
-        testCallback.expectAvailableThenValidatedCallbacks(na.network, TEST_TIMEOUT_MS)
-        val requestedSize = nsInstrumentation.getRequestUrls().size
-        if (requestedSize == 2 || (requestedSize == 1 &&
-                nsInstrumentation.getRequestUrls()[0] == httpsProbeUrl)) {
-            return
+        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()
         }
-        fail("Unexpected request urls: ${nsInstrumentation.getRequestUrls()}")
     }
 
     @Test
@@ -292,24 +337,32 @@
         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
-        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.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()
         }
     }
 
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..765e56e
--- /dev/null
+++ b/tests/integration/src/com/android/server/net/integrationtests/NetworkStatsIntegrationTest.kt
@@ -0,0 +1,597 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.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.
+        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.
+        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/util/com/android/server/NetworkAgentWrapper.java b/tests/integration/util/com/android/server/NetworkAgentWrapper.java
index ec09f9e..960c6ca 100644
--- a/tests/integration/util/com/android/server/NetworkAgentWrapper.java
+++ b/tests/integration/util/com/android/server/NetworkAgentWrapper.java
@@ -36,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;
@@ -51,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;
 
@@ -65,11 +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;
@@ -157,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" : "";
@@ -359,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();
diff --git a/tests/mts/Android.bp b/tests/mts/Android.bp
index 6425223..336be2e 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"],
 }
 
@@ -38,5 +39,5 @@
         "bpf_existence_test.cpp",
     ],
     compile_multilib: "first",
-    min_sdk_version: "30",  // Ensure test runs on R and above.
+    min_sdk_version: "30", // Ensure test runs on R and above.
 }
diff --git a/tests/native/connectivity_native_test/Android.bp b/tests/native/connectivity_native_test/Android.bp
index 8825aa4..2f66d17 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"],
 }
 
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 669b76a..34b4f07 100644
--- a/tests/native/utilities/firewall.cpp
+++ b/tests/native/utilities/firewall.cpp
@@ -60,10 +60,10 @@
     // 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",
-                      static_cast<int>(match));
+                      static_cast<uint32_t>(match));
     } else if (match != IIF_MATCH && iif != 0) {
         return Errorf("Non-interface match {} must have zero interface index",
-                      static_cast<int>(match));
+                      static_cast<uint32_t>(match));
     }
 
     std::lock_guard guard(mMutex);
@@ -71,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());
@@ -93,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);
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..20d457f 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"
@@ -65,15 +66,13 @@
         "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.
@@ -115,7 +114,7 @@
         "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/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/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/NetworkUtilsTest.java b/tests/unit/java/android/net/NetworkUtilsTest.java
index 5d789b4..e453c02 100644
--- a/tests/unit/java/android/net/NetworkUtilsTest.java
+++ b/tests/unit/java/android/net/NetworkUtilsTest.java
@@ -21,8 +21,14 @@
 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;
@@ -38,7 +44,6 @@
 import org.junit.runner.RunWith;
 
 import java.io.FileDescriptor;
-import java.io.IOException;
 import java.math.BigInteger;
 import java.nio.ByteBuffer;
 import java.nio.ByteOrder;
@@ -154,10 +159,9 @@
         return timeval;
     }
 
-    @Test
-    public void testSetSockOptBytes() throws ErrnoException {
-        final FileDescriptor sock = Os.socket(AF_INET6, SOCK_DGRAM, IPPROTO_ICMPV6);
-        final StructTimeval writeTimeval = StructTimeval.fromMillis(1200);
+    private void testSetSockOptBytes(FileDescriptor sock, long timeValMillis)
+            throws ErrnoException {
+        final StructTimeval writeTimeval = StructTimeval.fromMillis(timeValMillis);
         byte[] timeval = getTimevalBytes(writeTimeval);
         final StructTimeval readTimeval;
 
@@ -165,6 +169,22 @@
         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/nsd/AdvertisingRequestTest.kt b/tests/unit/java/android/net/nsd/AdvertisingRequestTest.kt
new file mode 100644
index 0000000..332f2a3
--- /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 550a9ee..76a649e 100644
--- a/tests/unit/java/android/net/nsd/NsdManagerTest.java
+++ b/tests/unit/java/android/net/nsd/NsdManagerTest.java
@@ -23,6 +23,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 +39,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 +53,10 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
+import java.net.InetAddress;
+import java.util.List;
+import java.time.Duration;
+
 @DevSdkIgnoreRunner.MonitorThreadLeak
 @RunWith(DevSdkIgnoreRunner.class)
 @SmallTest
@@ -86,73 +92,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;
 
@@ -196,6 +210,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, capturedRequest);
+    }
+
     private void doTestRegisterService() throws Exception {
         NsdManager manager = mManager;
 
@@ -260,6 +307,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");
@@ -280,7 +328,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");
 
 
@@ -320,7 +368,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
@@ -344,10 +392,52 @@
         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);
 
         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");
 
         // Service registration
         //  - invalid arguments
@@ -357,13 +447,38 @@
         mustFail(() -> { manager.registerService(invalidService, PROTOCOL, listener1); });
         mustFail(() -> { manager.registerService(validService, -1, listener1); });
         mustFail(() -> { manager.registerService(validService, PROTOCOL, null); });
+        mustFail(() -> {
+            manager.registerService(invalidMissingHostnameWithAddresses, 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);
 
         // Discover service
         //  - invalid arguments
@@ -405,4 +520,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/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/metrics/ConnectivitySampleMetricsTest.kt b/tests/unit/java/com/android/metrics/ConnectivitySampleMetricsTest.kt
index 3043d50..53baee1 100644
--- a/tests/unit/java/com/android/metrics/ConnectivitySampleMetricsTest.kt
+++ b/tests/unit/java/com/android/metrics/ConnectivitySampleMetricsTest.kt
@@ -16,6 +16,7 @@
 import android.net.NetworkCapabilities.TRANSPORT_CELLULAR
 import android.net.NetworkCapabilities.TRANSPORT_WIFI
 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
@@ -86,7 +87,10 @@
                 .addCapability(NET_CAPABILITY_NOT_SUSPENDED)
                 .addCapability(NET_CAPABILITY_NOT_ROAMING)
                 .build()
-        val wifi1Score = NetworkScore.Builder().setExiting(true).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()
@@ -96,7 +100,10 @@
                 .addCapability(NET_CAPABILITY_NOT_ROAMING)
                 .addEnterpriseId(NET_ENTERPRISE_ID_3)
                 .build()
-        val wifi2Score = NetworkScore.Builder().setTransportPrimary(true).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()
@@ -107,7 +114,9 @@
                 .addCapability(NET_CAPABILITY_NOT_ROAMING)
                 .addEnterpriseId(NET_ENTERPRISE_ID_1)
                 .build()
-        val cellScore = NetworkScore.Builder().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() }
diff --git a/tests/unit/java/com/android/server/ConnectivityServiceTest.java b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
index 8f5fd7c..c534025 100755
--- a/tests/unit/java/com/android/server/ConnectivityServiceTest.java
+++ b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
@@ -157,6 +157,7 @@
 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.server.ConnectivityService.DELAY_DESTROY_FROZEN_SOCKETS_VERSION;
 import static com.android.net.module.util.DeviceConfigUtils.TETHERING_MODULE_NAME;
 import static com.android.server.ConnectivityService.ALLOW_SYSUI_CONNECTIVITY_REPORTS;
@@ -420,6 +421,7 @@
 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.VpnProfileStore;
@@ -485,6 +487,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;
@@ -502,6 +505,7 @@
 // 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";
@@ -522,7 +526,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.
@@ -561,6 +565,7 @@
     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;
 
@@ -589,6 +594,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;
@@ -639,6 +645,7 @@
     @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.
@@ -1092,6 +1099,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.
@@ -2050,11 +2058,21 @@
         @Override
         public CarrierPrivilegeAuthenticator makeCarrierPrivilegeAuthenticator(
                 @NonNull final Context context,
-                @NonNull final TelephonyManager tm) {
+                @NonNull final TelephonyManager tm,
+                final boolean requestRestrictedWifiEnabled,
+                BiConsumer<Integer, Integer> 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));
         }
@@ -2144,6 +2162,8 @@
                 case ConnectivityFlags.NO_REMATCH_ALL_REQUESTS_ON_REGISTER:
                 case ConnectivityFlags.CARRIER_SERVICE_CHANGED_USE_CALLBACK:
                     return true;
+                case ConnectivityFlags.REQUEST_RESTRICTED_WIFI:
+                    return true;
                 case KEY_DESTROY_FROZEN_SOCKETS_VERSION:
                     return true;
                 case DELAY_DESTROY_FROZEN_SOCKETS_VERSION:
@@ -2160,6 +2180,8 @@
                     return true;
                 case LOG_BPF_RC:
                     return true;
+                case ALLOW_SATALLITE_NETWORK_FALLBACK:
+                    return true;
                 default:
                     return super.isFeatureNotChickenedOut(context, name);
             }
@@ -2404,6 +2426,11 @@
         FakeSettingsProvider.clearSettingsProvider();
         ConnectivityResources.setResourcesContextForTest(null);
 
+        for (TestNetworkAgentWrapper agent : mCreatedAgents) {
+            agent.destroy();
+        }
+        mCreatedAgents.clear();
+
         mCsHandlerThread.quitSafely();
         mCsHandlerThread.join();
         mAlarmManagerThread.quitSafely();
@@ -11460,7 +11487,7 @@
         doTestInterfaceClassActivityChanged(TRANSPORT_CELLULAR);
     }
 
-    private void doTestOnNetworkActive_NewNetworkConnects(int transportType, boolean expectCallback)
+    private void doTestOnNetworkActive_NewNetworkConnects(int transportType, boolean expectCapChanged)
             throws Exception {
         final ConditionVariable onNetworkActiveCv = new ConditionVariable();
         final ConnectivityManager.OnNetworkActiveListener listener = onNetworkActiveCv::open;
@@ -11472,7 +11499,7 @@
         testAndCleanup(() -> {
             mCm.addDefaultNetworkActiveListener(listener);
             agent.connect(true);
-            if (expectCallback) {
+            if (expectCapChanged) {
                 assertTrue(onNetworkActiveCv.block(TEST_CALLBACK_TIMEOUT_MS));
             } else {
                 assertFalse(onNetworkActiveCv.block(TEST_CALLBACK_TIMEOUT_MS));
@@ -11487,7 +11514,7 @@
 
     @Test
     public void testOnNetworkActive_NewCellConnects_CallbackCalled() throws Exception {
-        doTestOnNetworkActive_NewNetworkConnects(TRANSPORT_CELLULAR, true /* expectCallback */);
+        doTestOnNetworkActive_NewNetworkConnects(TRANSPORT_CELLULAR, true /* expectCapChanged */);
     }
 
     @Test
@@ -11496,8 +11523,8 @@
         // 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 expectCallback = mDeps.isAtLeastV();
-        doTestOnNetworkActive_NewNetworkConnects(TRANSPORT_ETHERNET, expectCallback);
+        final boolean expectCapChanged = mDeps.isAtLeastV();
+        doTestOnNetworkActive_NewNetworkConnects(TRANSPORT_ETHERNET, expectCapChanged);
     }
 
     @Test
@@ -12921,7 +12948,7 @@
         mServiceContext.setPermission(NETWORK_STACK, PERMISSION_GRANTED);
         assertTrue(
                 "NetworkStack permission not applied",
-                mService.checkConnectivityDiagnosticsPermissions(
+                mService.hasConnectivityDiagnosticsPermissions(
                         Process.myPid(), Process.myUid(), naiWithoutUid,
                         mContext.getOpPackageName()));
     }
@@ -12933,7 +12960,7 @@
         mServiceContext.setPermission(STATUS_BAR_SERVICE, PERMISSION_GRANTED);
         assertTrue(
                 "SysUi permission (STATUS_BAR_SERVICE) not applied",
-                mService.checkConnectivityDiagnosticsPermissions(
+                mService.hasConnectivityDiagnosticsPermissions(
                         Process.myPid(), Process.myUid(), naiWithoutUid,
                         mContext.getOpPackageName()));
     }
@@ -12950,7 +12977,7 @@
 
         assertFalse(
                 "Mismatched uid/package name should not pass the location permission check",
-                mService.checkConnectivityDiagnosticsPermissions(
+                mService.hasConnectivityDiagnosticsPermissions(
                         Process.myPid() + 1, wrongUid, naiWithUid, mContext.getOpPackageName()));
     }
 
@@ -12961,7 +12988,7 @@
         assertEquals(
                 "Unexpected ConnDiags permission",
                 expectPermission,
-                mService.checkConnectivityDiagnosticsPermissions(
+                mService.hasConnectivityDiagnosticsPermissions(
                         Process.myPid(), Process.myUid(), info, mContext.getOpPackageName()));
     }
 
@@ -13003,7 +13030,7 @@
         waitForIdle();
         assertTrue(
                 "Active VPN permission not applied",
-                mService.checkConnectivityDiagnosticsPermissions(
+                mService.hasConnectivityDiagnosticsPermissions(
                         Process.myPid(), Process.myUid(), naiWithoutUid,
                         mContext.getOpPackageName()));
 
@@ -13011,7 +13038,7 @@
         waitForIdle();
         assertFalse(
                 "VPN shouldn't receive callback on non-underlying network",
-                mService.checkConnectivityDiagnosticsPermissions(
+                mService.hasConnectivityDiagnosticsPermissions(
                         Process.myPid(), Process.myUid(), naiWithoutUid,
                         mContext.getOpPackageName()));
     }
@@ -13028,7 +13055,7 @@
 
         assertTrue(
                 "NetworkCapabilities administrator uid permission not applied",
-                mService.checkConnectivityDiagnosticsPermissions(
+                mService.hasConnectivityDiagnosticsPermissions(
                         Process.myPid(), Process.myUid(), naiWithUid, mContext.getOpPackageName()));
     }
 
@@ -13046,7 +13073,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()));
     }
@@ -17345,6 +17372,14 @@
                 .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);
@@ -17378,6 +17413,141 @@
     }
 
     @Test
+    @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();
+
+        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());
+        mService.onCarrierPrivilegesLost(lostPrivilegeUid, lostPrivilegeSubId);
+        waitForIdle();
+
+        if (expectCapChanged) {
+            networkCallback.expect(NETWORK_CAPS_UPDATED);
+        }
+        if (expectUnavailable) {
+            networkCallback.expect(UNAVAILABLE);
+        }
+        if (!expectCapChanged && !expectUnavailable) {
+            networkCallback.assertNoCallback();
+        }
+
+        mWiFiAgent.disconnect();
+        waitForIdle();
+
+        if (expectUnavailable) {
+            testFactory.assertRequestCountEquals(0);
+        } else {
+            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 testAllowedUids() throws Exception {
         final int preferenceOrder =
                 ConnectivityService.PREFERENCE_ORDER_IRRELEVANT_BECAUSE_NOT_DEFAULT;
diff --git a/tests/unit/java/com/android/server/NsdServiceTest.java b/tests/unit/java/com/android/server/NsdServiceTest.java
index 32014c2..881de56 100644
--- a/tests/unit/java/com/android/server/NsdServiceTest.java
+++ b/tests/unit/java/com/android/server/NsdServiceTest.java
@@ -34,6 +34,7 @@
 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;
@@ -82,6 +83,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;
@@ -100,6 +102,7 @@
 import android.os.IBinder;
 import android.os.Looper;
 import android.os.Message;
+import android.os.Process;
 import android.os.RemoteException;
 import android.util.Pair;
 
@@ -109,6 +112,7 @@
 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;
@@ -131,15 +135,20 @@
 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
@@ -149,6 +158,9 @@
 @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;
@@ -230,7 +242,8 @@
         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());
+        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();
@@ -252,8 +265,14 @@
             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,
@@ -286,6 +305,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);
@@ -321,6 +341,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();
@@ -365,6 +386,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);
 
@@ -401,6 +423,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;
@@ -499,6 +522,7 @@
 
     @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);
@@ -531,6 +555,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);
@@ -585,6 +610,7 @@
 
     @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);
@@ -617,6 +643,7 @@
 
     @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);
@@ -652,6 +679,7 @@
 
     @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);
@@ -701,8 +729,89 @@
                 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();
@@ -723,6 +832,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);
@@ -749,6 +859,7 @@
 
     @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);
@@ -774,6 +885,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);
@@ -864,7 +976,8 @@
                 List.of() /* textStrings */,
                 List.of() /* textEntries */,
                 1234,
-                network);
+                network,
+                Instant.MAX /* expirationTime */);
 
         // Callbacks for query sent.
         listener.onDiscoveryQuerySent(Collections.emptyList(), 1 /* transactionId */);
@@ -894,7 +1007,8 @@
                 List.of() /* textStrings */,
                 List.of() /* textEntries */,
                 1234,
-                network);
+                network,
+                Instant.MAX /* expirationTime */);
 
         // Verify onServiceUpdated callback.
         listener.onServiceUpdated(updatedServiceInfo);
@@ -955,6 +1069,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);
@@ -1025,7 +1140,8 @@
                 List.of(), /* textStrings */
                 List.of(), /* textEntries */
                 1234, /* interfaceIndex */
-                network);
+                network,
+                Instant.MAX /* expirationTime */);
 
         // Verify onServiceNameDiscovered callback
         listener.onServiceNameDiscovered(foundInfo, false /* isServiceFromCache */);
@@ -1046,7 +1162,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 ->
@@ -1117,7 +1234,8 @@
         waitForIdle();
         verify(mAdvertiser).addOrUpdateService(anyInt(), argThat(s ->
                 "Instance".equals(s.getServiceName())
-                        && SERVICE_TYPE.equals(s.getServiceType())), eq("_subtype"), any());
+                        && 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);
@@ -1167,7 +1285,8 @@
                 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
         doReturn(TEST_TIME_MS + 10L).when(mClock).elapsedRealtime();
@@ -1192,6 +1311,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))
@@ -1201,6 +1321,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);
@@ -1223,7 +1344,7 @@
 
         final ArgumentCaptor<Integer> serviceIdCaptor = ArgumentCaptor.forClass(Integer.class);
         verify(mAdvertiser).addOrUpdateService(serviceIdCaptor.capture(),
-                argThat(info -> matches(info, regInfo)), eq(null) /* subtype */, any());
+                argThat(info -> matches(info, regInfo)), any(), anyInt());
 
         client.unregisterService(regListenerWithoutFeature);
         waitForIdle();
@@ -1239,6 +1360,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(),
@@ -1251,7 +1373,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(),
@@ -1283,9 +1405,9 @@
 
         // The advertiser is enabled for _type2 but not _type1
         verify(mAdvertiser, never()).addOrUpdateService(anyInt(),
-                argThat(info -> matches(info, service1)), eq(null) /* subtype */, any());
+                argThat(info -> matches(info, service1)), any(), anyInt());
         verify(mAdvertiser).addOrUpdateService(anyInt(), argThat(info -> matches(info, service2)),
-                eq(null) /* subtype */, any());
+                any(), anyInt());
     }
 
     @Test
@@ -1297,7 +1419,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(), 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"));
@@ -1310,7 +1432,7 @@
         verify(mSocketProvider).startMonitoringSockets();
         final ArgumentCaptor<Integer> idCaptor = ArgumentCaptor.forClass(Integer.class);
         verify(mAdvertiser).addOrUpdateService(idCaptor.capture(), argThat(info ->
-                matches(info, regInfo)), eq(null) /* subtype */, any());
+                matches(info, regInfo)), any(), anyInt());
 
         // Verify onServiceRegistered callback
         final MdnsAdvertiser.AdvertiserCallback cb = cbCaptor.getValue();
@@ -1348,7 +1470,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(), 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"));
@@ -1358,7 +1480,7 @@
 
         client.registerService(regInfo, NsdManager.PROTOCOL_DNS_SD, Runnable::run, regListener);
         waitForIdle();
-        verify(mAdvertiser, never()).addOrUpdateService(anyInt(), any(), 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));
@@ -1375,7 +1497,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(), 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"));
@@ -1387,9 +1509,12 @@
         waitForIdle();
         final ArgumentCaptor<Integer> idCaptor = ArgumentCaptor.forClass(Integer.class);
         // Service name is truncated to 63 characters
-        verify(mAdvertiser).addOrUpdateService(idCaptor.capture(),
-                argThat(info -> info.getServiceName().equals("a".repeat(63))),
-                eq(null) /* subtype */, any());
+        verify(mAdvertiser)
+                .addOrUpdateService(
+                        idCaptor.capture(),
+                        argThat(info -> info.getServiceName().equals("a".repeat(63))),
+                        any(),
+                        anyInt());
 
         // Verify onServiceRegistered callback
         final MdnsAdvertiser.AdvertiserCallback cb = cbCaptor.getValue();
@@ -1404,6 +1529,82 @@
     }
 
     @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
     public void testStopServiceResolutionWithMdnsDiscoveryManager() {
         setMdnsDiscoveryManagerEnabled();
 
@@ -1453,14 +1654,22 @@
         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
@@ -1479,7 +1688,7 @@
         client.registerService(regInfo, NsdManager.PROTOCOL_DNS_SD, Runnable::run, regListener);
         waitForIdle();
         verify(mSocketProvider).startMonitoringSockets();
-        verify(mAdvertiser).addOrUpdateService(anyInt(), any(), any(), any());
+        verify(mAdvertiser).addOrUpdateService(anyInt(), any(), any(), anyInt());
 
         // Verify the discovery uses MdnsDiscoveryManager
         final DiscoveryListener discListener = mock(DiscoveryListener.class);
@@ -1512,7 +1721,7 @@
         client.registerService(regInfo, NsdManager.PROTOCOL_DNS_SD, Runnable::run, regListener);
         waitForIdle();
         verify(mSocketProvider).startMonitoringSockets();
-        verify(mAdvertiser).addOrUpdateService(anyInt(), any(), any(), any());
+        verify(mAdvertiser).addOrUpdateService(anyInt(), any(), any(), anyInt());
 
         final Network wifiNetwork1 = new Network(123);
         final Network wifiNetwork2 = new Network(124);
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 10a0982..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;
@@ -71,31 +69,16 @@
 import android.os.Message;
 import android.os.SystemClock;
 import android.telephony.SubscriptionManager;
-import android.test.suitebuilder.annotation.SmallTest;
-import android.util.ArraySet;
 import android.util.Log;
-import android.util.Range;
-
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
-
+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 libcore.util.HexEncoding;
-
-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.StringWriter;
 import java.net.Inet4Address;
@@ -104,9 +87,15 @@
 import java.nio.ByteBuffer;
 import java.nio.ByteOrder;
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.List;
-import java.util.Set;
+import libcore.util.HexEncoding;
+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;
 
 @RunWith(DevSdkIgnoreRunner.class)
 @SmallTest
@@ -142,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
@@ -236,9 +225,6 @@
     private static final byte[] TEST_RESPONSE_BYTES =
             HexEncoding.decode(TEST_RESPONSE_HEX.toCharArray(), false);
 
-    private static final Set<Range<Integer>> TEST_UID_RANGES =
-            new ArraySet<>(Arrays.asList(new Range<>(10000, 99999)));
-
     private static class TestKeepaliveInfo {
         private static List<Socket> sOpenSockets = new ArrayList<>();
 
@@ -416,28 +402,28 @@
     public void testIsAnyTcpSocketConnected_runOnNonHandlerThread() throws Exception {
         setupResponseWithSocketExisting();
         assertThrows(IllegalStateException.class,
-                () -> mAOOKeepaliveTracker.isAnyTcpSocketConnected(TEST_NETID, TEST_UID_RANGES));
+                () -> mAOOKeepaliveTracker.isAnyTcpSocketConnected(TEST_NETID));
     }
 
     @Test
     public void testIsAnyTcpSocketConnected_withTargetNetId() throws Exception {
         setupResponseWithSocketExisting();
         assertTrue(visibleOnHandlerThread(mTestHandler,
-                () -> mAOOKeepaliveTracker.isAnyTcpSocketConnected(TEST_NETID, TEST_UID_RANGES)));
+                () -> mAOOKeepaliveTracker.isAnyTcpSocketConnected(TEST_NETID)));
     }
 
     @Test
     public void testIsAnyTcpSocketConnected_withIncorrectNetId() throws Exception {
         setupResponseWithSocketExisting();
         assertFalse(visibleOnHandlerThread(mTestHandler,
-                () -> mAOOKeepaliveTracker.isAnyTcpSocketConnected(OTHER_NETID, TEST_UID_RANGES)));
+                () -> mAOOKeepaliveTracker.isAnyTcpSocketConnected(OTHER_NETID)));
     }
 
     @Test
     public void testIsAnyTcpSocketConnected_noSocketExists() throws Exception {
         setupResponseWithoutSocketExisting();
         assertFalse(visibleOnHandlerThread(mTestHandler,
-                () -> mAOOKeepaliveTracker.isAnyTcpSocketConnected(TEST_NETID, TEST_UID_RANGES)));
+                () -> mAOOKeepaliveTracker.isAnyTcpSocketConnected(TEST_NETID)));
     }
 
     private void triggerEventKeepalive(int slot, int reason) {
@@ -481,16 +467,14 @@
         setupResponseWithoutSocketExisting();
         visibleOnHandlerThread(
                 mTestHandler,
-                () -> mAOOKeepaliveTracker.handleMonitorAutomaticKeepalive(
-                        autoKi, TEST_NETID, TEST_UID_RANGES));
+                () -> mAOOKeepaliveTracker.handleMonitorAutomaticKeepalive(autoKi, TEST_NETID));
     }
 
     private void doResumeKeepalive(AutomaticOnOffKeepalive autoKi) throws Exception {
         setupResponseWithSocketExisting();
         visibleOnHandlerThread(
                 mTestHandler,
-                () -> mAOOKeepaliveTracker.handleMonitorAutomaticKeepalive(
-                        autoKi, TEST_NETID, TEST_UID_RANGES));
+                () -> mAOOKeepaliveTracker.handleMonitorAutomaticKeepalive(autoKi, TEST_NETID));
     }
 
     private void doStopKeepalive(AutomaticOnOffKeepalive autoKi) throws Exception {
diff --git a/tests/unit/java/com/android/server/connectivity/CarrierPrivilegeAuthenticatorTest.java b/tests/unit/java/com/android/server/connectivity/CarrierPrivilegeAuthenticatorTest.java
index f07593e..7bd2b56 100644
--- a/tests/unit/java/com/android/server/connectivity/CarrierPrivilegeAuthenticatorTest.java
+++ b/tests/unit/java/com/android/server/connectivity/CarrierPrivilegeAuthenticatorTest.java
@@ -46,7 +46,6 @@
 import android.net.TelephonyNetworkSpecifier;
 import android.os.Build;
 import android.os.HandlerThread;
-import android.telephony.SubscriptionManager;
 import android.telephony.TelephonyManager;
 
 import com.android.net.module.util.CollectionUtils;
@@ -54,10 +53,12 @@
 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 org.junit.After;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -67,6 +68,8 @@
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Map;
+import java.util.Set;
+import java.util.function.BiConsumer;
 
 /**
  * Tests for CarrierPrivilegeAuthenticatorTest.
@@ -77,6 +80,9 @@
 @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;
 
@@ -85,7 +91,9 @@
     @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;
@@ -94,12 +102,12 @@
         TestCarrierPrivilegeAuthenticator(@NonNull final Context c,
                 @NonNull final Dependencies deps,
                 @NonNull final TelephonyManager t) {
-            super(c, deps, t, mTelephonyManagerShim);
+            super(c, deps, t, mTelephonyManagerShim, true /* requestRestrictedWifiEnabled */,
+                    mListener);
         }
         @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;
         }
     }
 
@@ -119,7 +127,9 @@
         mTelephonyManager = mock(TelephonyManager.class);
         mTelephonyManagerShim = mock(TelephonyManagerShimImpl.class);
         mPackageManager = mock(PackageManager.class);
+        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));
@@ -172,7 +182,7 @@
 
         final NetworkCapabilities.Builder ncBuilder = new NetworkCapabilities.Builder()
                 .addTransportType(TRANSPORT_CELLULAR)
-                .setNetworkSpecifier(new TelephonyNetworkSpecifier(0));
+                .setNetworkSpecifier(new TelephonyNetworkSpecifier(TEST_SUBSCRIPTION_ID));
 
         assertTrue(mCarrierPrivilegeAuthenticator.isCarrierServiceUidForNetworkCapabilities(
                 mCarrierConfigPkgUid, ncBuilder.build()));
@@ -208,7 +218,8 @@
 
         newListeners.get(0).onCarrierServiceChanged(null, mCarrierConfigPkgUid);
 
-        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)
@@ -220,10 +231,27 @@
     }
 
     @Test
+    @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
+    public void testCarrierPrivilegesLostDueToCarrierServiceUpdate() throws Exception {
+        final CarrierPrivilegesListenerShim l = getCarrierPrivilegesListeners().get(0);
+
+        l.onCarrierServiceChanged(null, mCarrierConfigPkgUid);
+        l.onCarrierServiceChanged(null, mCarrierConfigPkgUid + 1);
+        if (mUseCallbacks) {
+            verify(mListener).accept(eq(mCarrierConfigPkgUid), eq(TEST_SUBSCRIPTION_ID));
+        }
+        l.onCarrierServiceChanged(null, mCarrierConfigPkgUid + 2);
+        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)
@@ -251,7 +279,7 @@
         assertFalse(mCarrierPrivilegeAuthenticator.isCarrierServiceUidForNetworkCapabilities(
                 mCarrierConfigPkgUid, ncBuilder.build()));
 
-        ncBuilder.setNetworkSpecifier(new TelephonyNetworkSpecifier(0));
+        ncBuilder.setNetworkSpecifier(new TelephonyNetworkSpecifier(TEST_SUBSCRIPTION_ID));
         assertTrue(mCarrierPrivilegeAuthenticator.isCarrierServiceUidForNetworkCapabilities(
                 mCarrierConfigPkgUid, ncBuilder.build()));
 
@@ -260,7 +288,35 @@
         ncBuilder.setNetworkSpecifier(null);
         ncBuilder.removeTransportType(TRANSPORT_CELLULAR);
         ncBuilder.addTransportType(TRANSPORT_WIFI);
-        ncBuilder.setNetworkSpecifier(new TelephonyNetworkSpecifier(0));
+        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);
+        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);
+        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/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 1b964e2..294dacb 100644
--- a/tests/unit/java/com/android/server/connectivity/KeepaliveStatsTrackerTest.java
+++ b/tests/unit/java/com/android/server/connectivity/KeepaliveStatsTrackerTest.java
@@ -1297,8 +1297,8 @@
 
         assertTrue(mKeepaliveStatsTracker.allMetricsExpected(dailyKeepaliveInfoReported));
 
-        // Write time after 26 hours.
-        final int writeTime2 = 26 * 60 * 60 * 1000;
+        // Write time after 27 hours.
+        final int writeTime2 = 27 * 60 * 60 * 1000;
         setElapsedRealtime(writeTime2);
 
         visibleOnHandlerThread(mTestHandler, () -> mKeepaliveStatsTracker.writeAndResetMetrics());
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..6c2c256
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivity/MulticastRoutingCoordinatorServiceTest.kt
@@ -0,0 +1,499 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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)
+
+        verify(mDeps).setsockoptMrt6AddMfc(eq(mFd), eq(mf6cctlAdd))
+
+        applyMulticastForwardNone(mIfName1, mIfName2)
+        mLooper.dispatchAll()
+
+        verify(mDeps).setsockoptMrt6AddMfc(eq(mFd), eq(mf6cctlUpdate))
+
+        applyMulticastForwardNone(mIfName1, mIfName3)
+        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/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/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/RoutingCoordinatorServiceTest.kt b/tests/unit/java/com/android/server/connectivity/RoutingCoordinatorServiceTest.kt
index 12758c6..4e15d5f 100644
--- a/tests/unit/java/com/android/server/connectivity/RoutingCoordinatorServiceTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/RoutingCoordinatorServiceTest.kt
@@ -18,14 +18,17 @@
 
 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
-import kotlin.test.assertFailsWith
 
 @RunWith(DevSdkIgnoreRunner::class)
 @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
@@ -46,9 +49,15 @@
         inOrder.verify(mNetd).tetherAddForward("from2", "to1")
         inOrder.verify(mNetd).ipfwdAddInterfaceForward("from2", "to1")
 
-        assertFailsWith<IllegalStateException> {
-            // Can't add the same pair again
+        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")
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..193078b
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivity/SatelliteAccessControllerTest.kt
@@ -0,0 +1,222 @@
+/*
+ * 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.Context
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageManager
+import android.content.pm.UserInfo
+import android.os.Build
+import android.os.Handler
+import android.os.UserHandle
+import android.util.ArraySet
+import com.android.server.makeMockUserManager
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.DevSdkIgnoreRunner
+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.Mockito.doReturn
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import java.util.concurrent.Executor
+import java.util.function.Consumer
+
+private const val USER = 0
+val USER_INFO = UserInfo(USER, "" /* name */, UserInfo.FLAG_PRIMARY)
+val USER_HANDLE = UserHandle(USER)
+private const val PRIMARY_USER = 0
+private const val SECONDARY_USER = 10
+private val PRIMARY_USER_HANDLE = UserHandle.of(PRIMARY_USER)
+private val SECONDARY_USER_HANDLE = UserHandle.of(SECONDARY_USER)
+// sms app names
+private const val SMS_APP1 = "sms_app_1"
+private const val SMS_APP2 = "sms_app_2"
+// sms app ids
+private const val SMS_APP_ID1 = 100
+private const val SMS_APP_ID2 = 101
+// UID for app1 and app2 on primary user
+// These app could become default sms app for user1
+private val PRIMARY_USER_SMS_APP_UID1 = UserHandle.getUid(PRIMARY_USER, SMS_APP_ID1)
+private val PRIMARY_USER_SMS_APP_UID2 = UserHandle.getUid(PRIMARY_USER, SMS_APP_ID2)
+// UID for app1 and app2 on secondary user
+// These app could become default sms app for user2
+private val SECONDARY_USER_SMS_APP_UID1 = UserHandle.getUid(SECONDARY_USER, SMS_APP_ID1)
+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 mPackageManager = mock(PackageManager::class.java)
+    private val mHandler = mock(Handler::class.java)
+    private val mRoleManager =
+        mock(SatelliteAccessController.Dependencies::class.java)
+    private val mCallback = mock(Consumer::class.java) as Consumer<Set<Int>>
+    private val mSatelliteAccessController =
+        SatelliteAccessController(context, mRoleManager, mCallback, mHandler)
+    private lateinit var mRoleHolderChangedListener: OnRoleHoldersChangedListener
+    @Before
+    @Throws(PackageManager.NameNotFoundException::class)
+    fun setup() {
+        makeMockUserManager(USER_INFO, USER_HANDLE)
+        doReturn(context).`when`(context).createContextAsUser(any(), anyInt())
+        doReturn(mPackageManager).`when`(context).packageManager
+
+        doReturn(PackageManager.PERMISSION_GRANTED)
+            .`when`(mPackageManager)
+            .checkPermission(Manifest.permission.SATELLITE_COMMUNICATION, SMS_APP1)
+        doReturn(PackageManager.PERMISSION_GRANTED)
+            .`when`(mPackageManager)
+            .checkPermission(Manifest.permission.SATELLITE_COMMUNICATION, SMS_APP2)
+
+        // Initialise default message application primary user package1
+        val applicationInfo1 = ApplicationInfo()
+        applicationInfo1.uid = PRIMARY_USER_SMS_APP_UID1
+        doReturn(applicationInfo1)
+            .`when`(mPackageManager)
+            .getApplicationInfo(eq(SMS_APP1), anyInt())
+
+        // Initialise default message application primary user package2
+        val applicationInfo2 = ApplicationInfo()
+        applicationInfo2.uid = PRIMARY_USER_SMS_APP_UID2
+        doReturn(applicationInfo2)
+            .`when`(mPackageManager)
+            .getApplicationInfo(eq(SMS_APP2), anyInt())
+
+        // Get registered listener using captor
+        val listenerCaptor = ArgumentCaptor.forClass(
+            OnRoleHoldersChangedListener::class.java
+        )
+        mSatelliteAccessController.start()
+        verify(mRoleManager).addOnRoleHoldersChangedListenerAsUser(
+            any(Executor::class.java), listenerCaptor.capture(), any(UserHandle::class.java))
+        mRoleHolderChangedListener = listenerCaptor.value
+    }
+
+    @Test
+    fun test_onRoleHoldersChanged_SatelliteFallbackUid_Changed_SingleUser() {
+        doReturn(listOf<String>()).`when`(mRoleManager).getRoleHoldersAsUser(RoleManager.ROLE_SMS,
+            PRIMARY_USER_HANDLE)
+        mRoleHolderChangedListener.onRoleHoldersChanged(RoleManager.ROLE_SMS, PRIMARY_USER_HANDLE)
+        verify(mCallback, never()).accept(any())
+
+        // check DEFAULT_MESSAGING_APP1 is available as satellite network fallback uid
+        doReturn(listOf(SMS_APP1))
+            .`when`(mRoleManager).getRoleHoldersAsUser(RoleManager.ROLE_SMS, PRIMARY_USER_HANDLE)
+        mRoleHolderChangedListener.onRoleHoldersChanged(RoleManager.ROLE_SMS, PRIMARY_USER_HANDLE)
+        verify(mCallback).accept(setOf(PRIMARY_USER_SMS_APP_UID1))
+
+        // check SMS_APP2 is available as satellite network Fallback uid
+        doReturn(listOf(SMS_APP2)).`when`(mRoleManager).getRoleHoldersAsUser(RoleManager.ROLE_SMS,
+            PRIMARY_USER_HANDLE)
+        mRoleHolderChangedListener.onRoleHoldersChanged(RoleManager.ROLE_SMS, PRIMARY_USER_HANDLE)
+        verify(mCallback).accept(setOf(PRIMARY_USER_SMS_APP_UID2))
+
+        // check no uid is available as satellite network fallback uid
+        doReturn(listOf<String>()).`when`(mRoleManager).getRoleHoldersAsUser(RoleManager.ROLE_SMS,
+            PRIMARY_USER_HANDLE)
+        mRoleHolderChangedListener.onRoleHoldersChanged(RoleManager.ROLE_SMS, PRIMARY_USER_HANDLE)
+        verify(mCallback).accept(ArraySet())
+    }
+
+    @Test
+    fun test_onRoleHoldersChanged_NoSatelliteCommunicationPermission() {
+        doReturn(listOf<Any>()).`when`(mRoleManager).getRoleHoldersAsUser(RoleManager.ROLE_SMS,
+            PRIMARY_USER_HANDLE)
+        mRoleHolderChangedListener.onRoleHoldersChanged(RoleManager.ROLE_SMS, PRIMARY_USER_HANDLE)
+        verify(mCallback, never()).accept(any())
+
+        // check DEFAULT_MESSAGING_APP1 is not available as satellite network fallback uid
+        // since satellite communication permission not available.
+        doReturn(PackageManager.PERMISSION_DENIED)
+            .`when`(mPackageManager)
+            .checkPermission(Manifest.permission.SATELLITE_COMMUNICATION, SMS_APP1)
+        doReturn(listOf(SMS_APP1))
+            .`when`(mRoleManager).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() {
+        doReturn(listOf(SMS_APP1))
+            .`when`(mRoleManager).getRoleHoldersAsUser(RoleManager.ROLE_SMS, PRIMARY_USER_HANDLE)
+        mRoleHolderChangedListener.onRoleHoldersChanged(RoleManager.ROLE_BROWSER,
+            PRIMARY_USER_HANDLE)
+        verify(mCallback, never()).accept(any())
+    }
+
+    @Test
+    fun test_onRoleHoldersChanged_SatelliteNetworkFallbackUid_Changed_multiUser() {
+        doReturn(listOf<String>()).`when`(mRoleManager).getRoleHoldersAsUser(RoleManager.ROLE_SMS,
+            PRIMARY_USER_HANDLE)
+        mRoleHolderChangedListener.onRoleHoldersChanged(RoleManager.ROLE_SMS, PRIMARY_USER_HANDLE)
+        verify(mCallback, never()).accept(any())
+
+        // check SMS_APP1 is available as satellite network fallback uid at primary user
+        doReturn(listOf(SMS_APP1))
+            .`when`(mRoleManager).getRoleHoldersAsUser(RoleManager.ROLE_SMS, PRIMARY_USER_HANDLE)
+        mRoleHolderChangedListener.onRoleHoldersChanged(RoleManager.ROLE_SMS, PRIMARY_USER_HANDLE)
+        verify(mCallback).accept(setOf(PRIMARY_USER_SMS_APP_UID1))
+
+        // check SMS_APP2 is available as satellite network fallback uid at primary user
+        doReturn(listOf(SMS_APP2)).`when`(mRoleManager).getRoleHoldersAsUser(RoleManager.ROLE_SMS,
+            PRIMARY_USER_HANDLE)
+        mRoleHolderChangedListener.onRoleHoldersChanged(RoleManager.ROLE_SMS, PRIMARY_USER_HANDLE)
+        verify(mCallback).accept(setOf(PRIMARY_USER_SMS_APP_UID2))
+
+        // check SMS_APP1 is available as satellite network fallback uid at secondary user
+        val applicationInfo1 = ApplicationInfo()
+        applicationInfo1.uid = SECONDARY_USER_SMS_APP_UID1
+        doReturn(applicationInfo1).`when`(mPackageManager)
+            .getApplicationInfo(eq(SMS_APP1), anyInt())
+        doReturn(listOf(SMS_APP1)).`when`(mRoleManager).getRoleHoldersAsUser(RoleManager.ROLE_SMS,
+            SECONDARY_USER_HANDLE)
+        mRoleHolderChangedListener.onRoleHoldersChanged(RoleManager.ROLE_SMS, SECONDARY_USER_HANDLE)
+        verify(mCallback).accept(setOf(PRIMARY_USER_SMS_APP_UID2, SECONDARY_USER_SMS_APP_UID1))
+
+        // check no uid is available as satellite network fallback uid at primary user
+        doReturn(listOf<String>()).`when`(mRoleManager).getRoleHoldersAsUser(RoleManager.ROLE_SMS,
+            PRIMARY_USER_HANDLE)
+        mRoleHolderChangedListener.onRoleHoldersChanged(RoleManager.ROLE_SMS,
+            PRIMARY_USER_HANDLE)
+        verify(mCallback).accept(setOf(SECONDARY_USER_SMS_APP_UID1))
+
+        // check SMS_APP2 is available as satellite network fallback uid at secondary user
+        applicationInfo1.uid = SECONDARY_USER_SMS_APP_UID2
+        doReturn(applicationInfo1).`when`(mPackageManager)
+            .getApplicationInfo(eq(SMS_APP2), anyInt())
+        doReturn(listOf(SMS_APP2))
+            .`when`(mRoleManager).getRoleHoldersAsUser(RoleManager.ROLE_SMS, SECONDARY_USER_HANDLE)
+        mRoleHolderChangedListener.onRoleHoldersChanged(RoleManager.ROLE_SMS, SECONDARY_USER_HANDLE)
+        verify(mCallback).accept(setOf(SECONDARY_USER_SMS_APP_UID2))
+
+        // check no uid is available as satellite network fallback uid at secondary user
+        doReturn(listOf<String>()).`when`(mRoleManager).getRoleHoldersAsUser(RoleManager.ROLE_SMS,
+            SECONDARY_USER_HANDLE)
+        mRoleHolderChangedListener.onRoleHoldersChanged(RoleManager.ROLE_SMS, SECONDARY_USER_HANDLE)
+        verify(mCallback).accept(ArraySet())
+    }
+}
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 c9cece0..0000000
--- a/tests/unit/java/com/android/server/connectivity/VpnTest.java
+++ /dev/null
@@ -1,3298 +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.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.HandlerUtils.waitForIdleSerialExecutor;
-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.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.atLeast;
-import static org.mockito.Mockito.atLeastOnce;
-import static org.mockito.Mockito.clearInvocations;
-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.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;
-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.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.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.ext.junit.runners.AndroidJUnit4;
-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 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.Captor;
-import org.mockito.InOrder;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-import java.io.FileDescriptor;
-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.Collection;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.SortedSet;
-import java.util.TreeSet;
-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;
-
-/**
- * Tests for {@link Vpn}.
- *
- * Build, install and run with:
- *  runtest frameworks-net -c com.android.server.connectivity.VpnTest
- */
-@RunWith(AndroidJUnit4.class)
-@SmallTest
-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;
-
-    @Captor private ArgumentCaptor<Collection<Range<Integer>>> mUidRangesCaptor;
-
-    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 testAlwaysOnWithoutLockdown() throws Exception {
-        final Vpn vpn = createVpn(PRIMARY_USER.id);
-        assertTrue(vpn.setAlwaysOnPackage(
-                PKGS[1], false /* lockdown */, null /* lockdownAllowlist */));
-        verify(mConnectivityManager, never()).setRequireVpnForUids(anyBoolean(), any());
-
-        assertTrue(vpn.setAlwaysOnPackage(
-                null /* packageName */, false /* lockdown */, null /* lockdownAllowlist */));
-        verify(mConnectivityManager, never()).setRequireVpnForUids(anyBoolean(), any());
-    }
-
-    @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 testLockdownSystemUser() throws Exception {
-        final Vpn vpn = createVpn(SYSTEM_USER_ID);
-
-        // Uid 0 is always excluded and PKG_UIDS[1] is the uid of the VPN.
-        final List<Integer> excludedUids = new ArrayList<>(List.of(0, PKG_UIDS[1]));
-        final List<Range<Integer>> ranges = makeVpnUidRange(SYSTEM_USER_ID, excludedUids);
-
-        // Set always-on with lockdown.
-        assertTrue(vpn.setAlwaysOnPackage(
-                PKGS[1], true /* lockdown */, null /* lockdownAllowlist */));
-        verify(mConnectivityManager).setRequireVpnForUids(true, ranges);
-
-        // Disable always-on with lockdown.
-        assertTrue(vpn.setAlwaysOnPackage(
-                null /* packageName */, false /* lockdown */, null /* lockdownAllowlist */));
-        verify(mConnectivityManager).setRequireVpnForUids(false, ranges);
-
-        // Set always-on with lockdown and allow the app PKGS[2].
-        excludedUids.add(PKG_UIDS[2]);
-        final List<Range<Integer>> ranges2 = makeVpnUidRange(SYSTEM_USER_ID, excludedUids);
-        assertTrue(vpn.setAlwaysOnPackage(
-                PKGS[1], true /* lockdown */, Collections.singletonList(PKGS[2])));
-        verify(mConnectivityManager).setRequireVpnForUids(true, ranges2);
-
-        // Disable always-on with lockdown.
-        assertTrue(vpn.setAlwaysOnPackage(
-                null /* packageName */, false /* lockdown */, null /* lockdownAllowlist */));
-        verify(mConnectivityManager).setRequireVpnForUids(false, ranges2);
-    }
-
-    @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 testOnUserAddedAndRemoved_restrictedUser() throws Exception {
-        final InOrder order = inOrder(mMockNetworkAgent);
-        final Vpn vpn = createVpn(PRIMARY_USER.id);
-        final Set<Range<Integer>> initialRange = rangeSet(PRIMARY_USER_RANGE);
-        // Note since mVpnProfile is a Ikev2VpnProfile, this starts an IkeV2VpnRunner.
-        startLegacyVpn(vpn, mVpnProfile);
-        // Set an initial Uid range and mock the network agent
-        vpn.mNetworkCapabilities.setUids(initialRange);
-        vpn.mNetworkAgent = mMockNetworkAgent;
-
-        // Add the restricted user
-        setMockedUsers(PRIMARY_USER, RESTRICTED_PROFILE_A);
-        vpn.onUserAdded(RESTRICTED_PROFILE_A.id);
-        // Expect restricted user range to be added to the NetworkCapabilities.
-        final Set<Range<Integer>> expectRestrictedRange =
-                rangeSet(PRIMARY_USER_RANGE, uidRangeForUser(RESTRICTED_PROFILE_A.id));
-        assertEquals(expectRestrictedRange, vpn.mNetworkCapabilities.getUids());
-        order.verify(mMockNetworkAgent).doSendNetworkCapabilities(
-                argThat(nc -> expectRestrictedRange.equals(nc.getUids())));
-
-        // Remove the restricted user
-        vpn.onUserRemoved(RESTRICTED_PROFILE_A.id);
-        // Expect restricted user range to be removed from the NetworkCapabilities.
-        assertEquals(initialRange, vpn.mNetworkCapabilities.getUids());
-        order.verify(mMockNetworkAgent).doSendNetworkCapabilities(
-                argThat(nc -> initialRange.equals(nc.getUids())));
-    }
-
-    @Test
-    public void testOnUserAddedAndRemoved_restrictedUserLockdown() throws Exception {
-        final UidRangeParcel[] primaryUserRangeParcel = new UidRangeParcel[] {
-                new UidRangeParcel(PRIMARY_USER_RANGE.getLower(), PRIMARY_USER_RANGE.getUpper())};
-        final Range<Integer> restrictedUserRange = uidRangeForUser(RESTRICTED_PROFILE_A.id);
-        final UidRangeParcel[] restrictedUserRangeParcel = new UidRangeParcel[] {
-                new UidRangeParcel(restrictedUserRange.getLower(), restrictedUserRange.getUpper())};
-        final Vpn vpn = createVpn(PRIMARY_USER.id);
-
-        // Set lockdown calls setRequireVpnForUids
-        vpn.setLockdown(true);
-        verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(primaryUserRangeParcel));
-
-        // Add the restricted user
-        doReturn(true).when(mUserManager).canHaveRestrictedProfile();
-        setMockedUsers(PRIMARY_USER, RESTRICTED_PROFILE_A);
-        vpn.onUserAdded(RESTRICTED_PROFILE_A.id);
-
-        // Expect restricted user range to be added.
-        verify(mConnectivityManager).setRequireVpnForUids(true,
-                toRanges(restrictedUserRangeParcel));
-
-        // Mark as partial indicates that the user is removed, mUserManager.getAliveUsers() does not
-        // return the restricted user but it is still returned in mUserManager.getUserInfo().
-        RESTRICTED_PROFILE_A.partial = true;
-        // Remove the restricted user
-        vpn.onUserRemoved(RESTRICTED_PROFILE_A.id);
-        verify(mConnectivityManager).setRequireVpnForUids(false,
-                toRanges(restrictedUserRangeParcel));
-        // reset to avoid affecting other tests since RESTRICTED_PROFILE_A is static.
-        RESTRICTED_PROFILE_A.partial = false;
-    }
-
-    @Test
-    public void testOnUserAddedAndRemoved_restrictedUserAlwaysOn() throws Exception {
-        final Vpn vpn = createVpn(PRIMARY_USER.id);
-
-        // setAlwaysOnPackage() calls setRequireVpnForUids()
-        assertTrue(vpn.setAlwaysOnPackage(
-                PKGS[0], true /* lockdown */, null /* lockdownAllowlist */));
-        final List<Integer> excludedUids = List.of(PKG_UIDS[0]);
-        final List<Range<Integer>> primaryRanges =
-                makeVpnUidRange(PRIMARY_USER.id, excludedUids);
-        verify(mConnectivityManager).setRequireVpnForUids(true, primaryRanges);
-
-        // Add the restricted user
-        doReturn(true).when(mUserManager).canHaveRestrictedProfile();
-        setMockedUsers(PRIMARY_USER, RESTRICTED_PROFILE_A);
-        vpn.onUserAdded(RESTRICTED_PROFILE_A.id);
-
-        final List<Range<Integer>> restrictedRanges =
-                makeVpnUidRange(RESTRICTED_PROFILE_A.id, excludedUids);
-        // Expect restricted user range to be added.
-        verify(mConnectivityManager).setRequireVpnForUids(true, restrictedRanges);
-
-        // Mark as partial indicates that the user is removed, mUserManager.getAliveUsers() does not
-        // return the restricted user but it is still returned in mUserManager.getUserInfo().
-        RESTRICTED_PROFILE_A.partial = true;
-        // Remove the restricted user
-        vpn.onUserRemoved(RESTRICTED_PROFILE_A.id);
-        verify(mConnectivityManager).setRequireVpnForUids(false, restrictedRanges);
-
-        // reset to avoid affecting other tests since RESTRICTED_PROFILE_A is static.
-        RESTRICTED_PROFILE_A.partial = false;
-    }
-
-    @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 testPrepare_legacyVpnWithoutControlVpn()
-            throws Exception {
-        doThrow(new SecurityException("no CONTROL_VPN")).when(mContext)
-                .enforceCallingOrSelfPermission(eq(CONTROL_VPN), any());
-        final Vpn vpn = createVpn();
-        assertThrows(SecurityException.class,
-                () -> vpn.prepare(null, VpnConfig.LEGACY_VPN, VpnManager.TYPE_VPN_SERVICE));
-
-        // CONTROL_VPN can be held by the caller or another system server process - both are
-        // allowed. Just checking for `enforceCallingPermission` may not be sufficient.
-        verify(mContext, never()).enforceCallingPermission(eq(CONTROL_VPN), any());
-    }
-
-    @Test
-    public void testPrepare_legacyVpnWithControlVpn()
-            throws Exception {
-        doNothing().when(mContext).enforceCallingOrSelfPermission(eq(CONTROL_VPN), any());
-        final Vpn vpn = createVpn();
-        assertTrue(vpn.prepare(null, VpnConfig.LEGACY_VPN, VpnManager.TYPE_VPN_SERVICE));
-
-        // CONTROL_VPN can be held by the caller or another system server process - both are
-        // allowed. Just checking for `enforceCallingPermission` may not be sufficient.
-        verify(mContext, never()).enforceCallingPermission(eq(CONTROL_VPN), any());
-    }
-
-    @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 String startVpnForVerifyAppExclusionList(Vpn vpn) throws Exception {
-        when(mVpnProfileStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG)))
-                .thenReturn(mVpnProfile.encode());
-        when(mVpnProfileStore.get(PRIMARY_USER_APP_EXCLUDE_KEY))
-                .thenReturn(HexDump.hexStringToByteArray(PKGS_BYTES));
-        final String sessionKey = vpn.startVpnProfile(TEST_VPN_PKG);
-        final Set<Range<Integer>> uidRanges = vpn.createUserAndRestrictedProfilesRanges(
-                PRIMARY_USER.id, null /* allowedApplications */, Arrays.asList(PKGS));
-        verify(mConnectivityManager).setVpnDefaultForUids(eq(sessionKey), eq(uidRanges));
-        clearInvocations(mConnectivityManager);
-        verify(mVpnProfileStore).get(eq(vpn.getProfileNameForPackage(TEST_VPN_PKG)));
-        vpn.mNetworkAgent = mMockNetworkAgent;
-
-        return sessionKey;
-    }
-
-    private Vpn prepareVpnForVerifyAppExclusionList() throws Exception {
-        final Vpn vpn = createVpn(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
-        startVpnForVerifyAppExclusionList(vpn);
-
-        return vpn;
-    }
-
-    @Test
-    public void testSetAndGetAppExclusionList() throws Exception {
-        final Vpn vpn = createVpn(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
-        final String sessionKey = startVpnForVerifyAppExclusionList(vpn);
-        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)));
-        final Set<Range<Integer>> uidRanges = vpn.createUserAndRestrictedProfilesRanges(
-                PRIMARY_USER.id, null /* allowedApplications */, Arrays.asList(PKGS));
-        verify(mConnectivityManager).setVpnDefaultForUids(eq(sessionKey), eq(uidRanges));
-        assertEquals(uidRanges, vpn.mNetworkCapabilities.getUids());
-        assertEquals(Arrays.asList(PKGS), vpn.getAppExclusionList(TEST_VPN_PKG));
-    }
-
-    @Test
-    public void testRefreshPlatformVpnAppExclusionList_updatesExcludedUids() throws Exception {
-        final Vpn vpn = createVpn(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
-        final String sessionKey = startVpnForVerifyAppExclusionList(vpn);
-        vpn.setAppExclusionList(TEST_VPN_PKG, Arrays.asList(PKGS));
-        final Set<Range<Integer>> uidRanges = vpn.createUserAndRestrictedProfilesRanges(
-                PRIMARY_USER.id, null /* allowedApplications */, Arrays.asList(PKGS));
-        verify(mConnectivityManager).setVpnDefaultForUids(eq(sessionKey), eq(uidRanges));
-        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]);
-        Set<Range<Integer>> newUidRanges = makeVpnUidRangeSet(PRIMARY_USER.id, newExcludedUids);
-        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(newUidRanges, vpn.mNetworkCapabilities.getUids());
-        ArgumentCaptor<NetworkCapabilities> ncCaptor =
-                ArgumentCaptor.forClass(NetworkCapabilities.class);
-        verify(mMockNetworkAgent).doSendNetworkCapabilities(ncCaptor.capture());
-        assertEquals(newUidRanges, ncCaptor.getValue().getUids());
-        verify(mConnectivityManager).setVpnDefaultForUids(eq(sessionKey), eq(newUidRanges));
-
-        reset(mMockNetworkAgent);
-
-        // Add the package back
-        newExcludedUids.add(PKG_UIDS[0]);
-        newUidRanges = makeVpnUidRangeSet(PRIMARY_USER.id, newExcludedUids);
-        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(newUidRanges, vpn.mNetworkCapabilities.getUids());
-        verify(mMockNetworkAgent).doSendNetworkCapabilities(ncCaptor.capture());
-        assertEquals(newUidRanges, ncCaptor.getValue().getUids());
-
-        // The uidRange is the same as the original setAppExclusionList so this is the second call
-        verify(mConnectivityManager, times(2))
-                .setVpnDefaultForUids(eq(sessionKey), eq(newUidRanges));
-    }
-
-    private List<Range<Integer>> makeVpnUidRange(int userId, List<Integer> excludedAppIdList) {
-        final SortedSet<Integer> list = new TreeSet<>();
-
-        final int userBase = userId * UserHandle.PER_USER_RANGE;
-        for (int appId : excludedAppIdList) {
-            final int uid = UserHandle.getUid(userId, appId);
-            list.add(uid);
-            if (Process.isApplicationUid(uid)) {
-                list.add(Process.toSdkSandboxUid(uid)); // Add Sdk Sandbox UID
-            }
-        }
-
-        final int minUid = userBase;
-        final int maxUid = userBase + UserHandle.PER_USER_RANGE - 1;
-        final List<Range<Integer>> ranges = new ArrayList<>();
-
-        // 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;
-    }
-
-    private Set<Range<Integer>> makeVpnUidRangeSet(int userId, List<Integer> excludedAppIdList) {
-        return new ArraySet<>(makeVpnUidRange(userId, excludedAppIdList));
-    }
-
-    @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, timeout(TEST_TIMEOUT_MS).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 Set<Range<Integer>> uidRanges = rangeSet(PRIMARY_USER_RANGE);
-        // This is triggered by Ikev2VpnRunner constructor.
-        verify(mConnectivityManager, times(1)).setVpnDefaultForUids(eq(sessionKey), eq(uidRanges));
-        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());
-        // This is triggered by Vpn#startOrMigrateIkeSession().
-        verify(mConnectivityManager, times(2)).setVpnDefaultForUids(eq(sessionKey), eq(uidRanges));
-        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();
-            mExecutor.execute(() -> 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).setVpnDefaultForUids(eq(sessionKey),
-                    eq(Collections.EMPTY_LIST));
-            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;
-            // First failure occurred above.
-            final IkeSessionCallback retryCb = verifyRetryAndGetNewIkeCb(retryIndex++);
-            // Trigger 2 more failures to let the retry delay increase to 5s.
-            mExecutor.execute(() -> retryCb.onClosedWithException(exception));
-            final IkeSessionCallback retryCb2 = verifyRetryAndGetNewIkeCb(retryIndex++);
-            mExecutor.execute(() -> retryCb2.onClosedWithException(exception));
-            final IkeSessionCallback retryCb3 = verifyRetryAndGetNewIkeCb(retryIndex++);
-
-            // setVpnDefaultForUids may be called again but the uidRanges should not change.
-            verify(mConnectivityManager, atLeast(2)).setVpnDefaultForUids(eq(sessionKey),
-                    mUidRangesCaptor.capture());
-            final List<Collection<Range<Integer>>> capturedUidRanges =
-                    mUidRangesCaptor.getAllValues();
-            for (int i = 2; i < capturedUidRanges.size(); i++) {
-                // Assert equals no order.
-                assertTrue(
-                        "uid ranges should not be modified. Expected: " + uidRanges
-                                + ", actual: " + capturedUidRanges.get(i),
-                        capturedUidRanges.get(i).containsAll(uidRanges)
-                                && capturedUidRanges.get(i).size() == uidRanges.size());
-            }
-
-            // A fourth failure will cause the retry delay to be greater than 5s.
-            mExecutor.execute(() -> retryCb3.onClosedWithException(exception));
-            verifyRetryAndGetNewIkeCb(retryIndex++);
-
-            // The VPN network preference will be cleared when the retry delay is greater than 5s.
-            verify(mConnectivityManager).setVpnDefaultForUids(eq(sessionKey),
-                    eq(Collections.EMPTY_LIST));
-        }
-    }
-
-    private IkeSessionCallback verifyRetryAndGetNewIkeCb(int retryIndex) {
-        final ArgumentCaptor<IkeSessionCallback> ikeCbCaptor =
-                ArgumentCaptor.forClass(IkeSessionCallback.class);
-
-        // Verify retry is scheduled
-        final long expectedDelayMs = mTestDeps.getNextRetryDelayMs(retryIndex);
-        verify(mExecutor, timeout(TEST_TIMEOUT_MS)).schedule(any(Runnable.class),
-                eq(expectedDelayMs), eq(TimeUnit.MILLISECONDS));
-
-        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);
-        vpn.startLegacyVpn(vpnProfile);
-        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());
-
-        final String sessionKey = vpn.startVpnProfile(TEST_VPN_PKG);
-        final Set<Range<Integer>> uidRanges = Collections.singleton(PRIMARY_USER_RANGE);
-        verify(mConnectivityManager).setVpnDefaultForUids(eq(sessionKey), eq(uidRanges));
-        final NetworkCallback nwCb = triggerOnAvailableAndGetCallback(underlyingNetworkCaps);
-        // There are 4 interactions with the executor.
-        // - Network available
-        // - LP change
-        // - NC change
-        // - schedule() calls in scheduleStartIkeSession()
-        // The first 3 calls are triggered from Executor.execute(). The execute() will also call to
-        // schedule() with 0 delay. Verify the exact interaction here so that it won't cause flakes
-        // in the follow-up flow.
-        verify(mExecutor, timeout(TEST_TIMEOUT_MS).times(4))
-                .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());
-        // Check if extra info for VPN is set.
-        assertTrue(nacCaptor.getValue().getLegacyExtraInfo().contains(TEST_VPN_PKG));
-        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();
-        verify(mConnectivityManager).setVpnDefaultForUids(anyString(), eq(Collections.EMPTY_LIST));
-    }
-
-    @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, timeout(TEST_TIMEOUT_MS))
-                    .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 testStartPlatformVpn_underlyingNetworkNotChange() throws Exception {
-        final PlatformVpnSnapshot vpnSnapShot = verifySetupPlatformVpn(
-                createIkeConfig(createIkeConnectInfo(), true /* isMobikeEnabled */));
-        // Trigger update on the same network should not cause underlying network change in NC of
-        // the VPN network
-        vpnSnapShot.nwCb.onAvailable(TEST_NETWORK);
-        vpnSnapShot.nwCb.onCapabilitiesChanged(TEST_NETWORK,
-                new NetworkCapabilities.Builder()
-                        .setSubscriptionIds(Set.of(TEST_SUB_ID))
-                        .build());
-        // Verify setNetwork() called but no underlying network update
-        verify(mIkeSessionWrapper, timeout(TEST_TIMEOUT_MS)).setNetwork(eq(TEST_NETWORK),
-                eq(ESP_IP_VERSION_AUTO) /* ipVersion */,
-                eq(ESP_ENCAP_TYPE_AUTO) /* encapType */,
-                eq(DEFAULT_UDP_PORT_4500_NAT_TIMEOUT_SEC_INT) /* keepaliveDelay */);
-        verify(mMockNetworkAgent, never())
-                .doSetUnderlyingNetworks(any());
-
-        vpnSnapShot.nwCb.onAvailable(TEST_NETWORK_2);
-        vpnSnapShot.nwCb.onCapabilitiesChanged(TEST_NETWORK_2,
-                new NetworkCapabilities.Builder().build());
-
-        // A new network should trigger both setNetwork() and a underlying network update.
-        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 */);
-        verify(mMockNetworkAgent).doSetUnderlyingNetworks(
-                Collections.singletonList(TEST_NETWORK_2));
-
-        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 */);
-        // Verify mNetworkCapabilities is updated
-        assertEquals(
-                Collections.singletonList(TEST_NETWORK_2),
-                vpnSnapShot.vpn.mNetworkCapabilities.getUnderlyingNetworks());
-        verify(mMockNetworkAgent)
-                .doSetUnderlyingNetworks(Collections.singletonList(TEST_NETWORK_2));
-
-        // 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 mNetworkAgent is updated
-        verify(mMockNetworkAgent).doSendLinkProperties(argThat(lp -> lp.getMtu() == newMtu));
-        verify(mMockNetworkAgent, never()).unregister();
-        // No further doSetUnderlyingNetworks interaction. The interaction count should stay one.
-        verify(mMockNetworkAgent, times(1)).doSetUnderlyingNetworks(any());
-        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.nwCb.onCapabilitiesChanged(TEST_NETWORK_2,
-                new NetworkCapabilities.Builder().build());
-        // Verify mNetworkCapabilities is updated
-        verify(mMockNetworkAgent, timeout(TEST_TIMEOUT_MS))
-                .doSetUnderlyingNetworks(Collections.singletonList(TEST_NETWORK_2));
-        assertEquals(
-                Collections.singletonList(TEST_NETWORK_2),
-                vpnSnapShot.vpn.mNetworkCapabilities.getUnderlyingNetworks());
-
-        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, timeout(TEST_TIMEOUT_MS)).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, int retryIndex) {
-        // Verify retry is scheduled
-        final long expectedDelayMs = mTestDeps.getValidationFailRecoveryMs(retryIndex);
-        final ArgumentCaptor<Long> delayCaptor = ArgumentCaptor.forClass(Long.class);
-        verify(mExecutor, times(retryIndex + 1)).schedule(
-                any(Runnable.class), delayCaptor.capture(), eq(TimeUnit.MILLISECONDS));
-        final List<Long> delays = delayCaptor.getAllValues();
-        assertEquals(expectedDelayMs, (long) delays.get(delays.size() - 1));
-
-        final ArgumentCaptor<Network> networkCaptor = ArgumentCaptor.forClass(Network.class);
-        verify(mIkeSessionWrapper, timeout(TEST_TIMEOUT_MS + expectedDelayMs))
-                .setNetwork(networkCaptor.capture(), anyInt() /* ipVersion */,
-                        anyInt() /* encapType */, anyInt() /* keepaliveDelay */);
-        assertEquals(expected, Collections.singletonList(networkCaptor.getValue()));
-    }
-
-    @Test
-    public void testDataStallInIkev2VpnMobikeDisabled() throws Exception {
-        final PlatformVpnSnapshot vpnSnapShot = verifySetupPlatformVpn(
-                createIkeConfig(createIkeConnectInfo(), false /* isMobikeEnabled */));
-
-        doReturn(TEST_NETWORK).when(mMockNetworkAgent).getNetwork();
-        ((Vpn.IkeV2VpnRunner) vpnSnapShot.vpn.mVpnRunner).onValidationStatus(
-                NetworkAgent.VALIDATION_STATUS_NOT_VALID);
-
-        // 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();
-        ((Vpn.IkeV2VpnRunner) vpnSnapShot.vpn.mVpnRunner).onValidationStatus(
-                NetworkAgent.VALIDATION_STATUS_NOT_VALID);
-        // Verify MOBIKE is triggered
-        verifyMobikeTriggered(vpnSnapShot.vpn.mNetworkCapabilities.getUnderlyingNetworks(),
-                0 /* retryIndex */);
-        // Validation failure on VPN network should trigger a re-evaluation request for the
-        // underlying network.
-        verify(mConnectivityManager).reportNetworkConnectivity(TEST_NETWORK, false);
-
-        reset(mIkev2SessionCreator);
-        reset(mExecutor);
-
-        // 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 that the retry count is reset. The mValidationFailRetryCount will not be reset
-        // until the executor finishes the execute() call, so wait until the all tasks are executed.
-        waitForIdleSerialExecutor(mExecutor, TEST_TIMEOUT_MS);
-        assertEquals(0,
-                ((Vpn.IkeV2VpnRunner) vpnSnapShot.vpn.mVpnRunner).mValidationFailRetryCount);
-        verify(mIkev2SessionCreator, never()).createIkeSession(
-                any(), any(), any(), any(), any(), any());
-
-        reset(mIkeSessionWrapper);
-        reset(mExecutor);
-
-        // Another validation fail should trigger another reportNetworkConnectivity
-        ((Vpn.IkeV2VpnRunner) vpnSnapShot.vpn.mVpnRunner).onValidationStatus(
-                NetworkAgent.VALIDATION_STATUS_NOT_VALID);
-        verifyMobikeTriggered(vpnSnapShot.vpn.mNetworkCapabilities.getUnderlyingNetworks(),
-                0 /* retryIndex */);
-        verify(mConnectivityManager, times(2)).reportNetworkConnectivity(TEST_NETWORK, false);
-    }
-
-    @Test
-    public void testDataStallInIkev2VpnNotRecoveredByMobike() throws Exception {
-        final PlatformVpnSnapshot vpnSnapShot = verifySetupPlatformVpn(
-                createIkeConfig(createIkeConnectInfo(), true /* isMobikeEnabled */));
-
-        int retry = 0;
-        doReturn(TEST_NETWORK).when(mMockNetworkAgent).getNetwork();
-        ((Vpn.IkeV2VpnRunner) vpnSnapShot.vpn.mVpnRunner).onValidationStatus(
-                NetworkAgent.VALIDATION_STATUS_NOT_VALID);
-        verifyMobikeTriggered(vpnSnapShot.vpn.mNetworkCapabilities.getUnderlyingNetworks(),
-                retry++);
-        // Validation failure on VPN network should trigger a re-evaluation request for the
-        // underlying network.
-        verify(mConnectivityManager).reportNetworkConnectivity(TEST_NETWORK, false);
-        reset(mIkev2SessionCreator);
-
-        // Second validation status update.
-        ((Vpn.IkeV2VpnRunner) vpnSnapShot.vpn.mVpnRunner).onValidationStatus(
-                NetworkAgent.VALIDATION_STATUS_NOT_VALID);
-        verifyMobikeTriggered(vpnSnapShot.vpn.mNetworkCapabilities.getUnderlyingNetworks(),
-                retry++);
-        // Call to reportNetworkConnectivity should only happen once. No further interaction.
-        verify(mConnectivityManager, times(1)).reportNetworkConnectivity(TEST_NETWORK, false);
-
-        // Use real delay to verify reset session will not be performed if there is an existing
-        // recovery for resetting the session.
-        mExecutor.delayMs = TestExecutor.REAL_DELAY;
-        mExecutor.executeDirect = true;
-        // Send validation status update should result in ike session reset.
-        ((Vpn.IkeV2VpnRunner) vpnSnapShot.vpn.mVpnRunner).onValidationStatus(
-                NetworkAgent.VALIDATION_STATUS_NOT_VALID);
-
-        // Verify session reset is scheduled
-        long expectedDelay = mTestDeps.getValidationFailRecoveryMs(retry++);
-        final ArgumentCaptor<Long> delayCaptor = ArgumentCaptor.forClass(Long.class);
-        verify(mExecutor, times(retry)).schedule(any(Runnable.class), delayCaptor.capture(),
-                eq(TimeUnit.MILLISECONDS));
-        final List<Long> delays = delayCaptor.getAllValues();
-        assertEquals(expectedDelay, (long) delays.get(delays.size() - 1));
-        // Call to reportNetworkConnectivity should only happen once. No further interaction.
-        verify(mConnectivityManager, times(1)).reportNetworkConnectivity(TEST_NETWORK, false);
-
-        // Another invalid status reported should not trigger other scheduled recovery.
-        expectedDelay = mTestDeps.getValidationFailRecoveryMs(retry++);
-        ((Vpn.IkeV2VpnRunner) vpnSnapShot.vpn.mVpnRunner).onValidationStatus(
-                NetworkAgent.VALIDATION_STATUS_NOT_VALID);
-        verify(mExecutor, never()).schedule(
-                any(Runnable.class), eq(expectedDelay), eq(TimeUnit.MILLISECONDS));
-
-        // Verify that session being reset
-        verify(mIkev2SessionCreator, timeout(TEST_TIMEOUT_MS + expectedDelay))
-                .createIkeSession(any(), any(), any(), any(), any(), any());
-        // Call to reportNetworkConnectivity should only happen once. No further interaction.
-        verify(mConnectivityManager, times(1)).reportNetworkConnectivity(TEST_NETWORK, false);
-    }
-
-    @Test
-    public void testStartLegacyVpnType() throws Exception {
-        setMockedUsers(PRIMARY_USER);
-        final Vpn vpn = createVpn(PRIMARY_USER.id);
-        final VpnProfile profile = new VpnProfile("testProfile" /* key */);
-
-        profile.type = VpnProfile.TYPE_PPTP;
-        assertThrows(UnsupportedOperationException.class, () -> startLegacyVpn(vpn, profile));
-        profile.type = VpnProfile.TYPE_L2TP_IPSEC_PSK;
-        assertThrows(UnsupportedOperationException.class, () -> startLegacyVpn(vpn, profile));
-    }
-
-    @Test
-    public void testStartLegacyVpnModifyProfile_TypePSK() throws Exception {
-        setMockedUsers(PRIMARY_USER);
-        final Vpn vpn = createVpn(PRIMARY_USER.id);
-        final Ikev2VpnProfile ikev2VpnProfile =
-                new Ikev2VpnProfile.Builder(TEST_VPN_SERVER, TEST_VPN_IDENTITY)
-                        .setAuthPsk(TEST_VPN_PSK)
-                        .build();
-        final VpnProfile profile = ikev2VpnProfile.toVpnProfile();
-
-        startLegacyVpn(vpn, profile);
-        assertEquals(profile, ikev2VpnProfile.toVpnProfile());
-    }
-
-    private void assertTransportInfoMatches(NetworkCapabilities nc, int type) {
-        assertNotNull(nc);
-        VpnTransportInfo ti = (VpnTransportInfo) nc.getTransportInfo();
-        assertNotNull(ti);
-        assertEquals(type, ti.getType());
-    }
-
-    // Make it public and un-final so as to spy it
-    public class TestDeps extends Vpn.Dependencies {
-        TestDeps() {}
-
-        @Override
-        public boolean isCallerSystem() {
-            return true;
-        }
-
-        @Override
-        public PendingIntent getIntentForStatusPanel(Context context) {
-            return null;
-        }
-
-        @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 getValidationFailRecoveryMs(int retryCount) {
-            // Simply return retryCount as the delay seconds for retrying.
-            return retryCount * 100L;
-        }
-
-        @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) {
-        }
-    }
-}
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 f0cb6df..b8ebf0f 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsAdvertiserTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsAdvertiserTest.kt
@@ -16,6 +16,8 @@
 
 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
@@ -26,13 +28,17 @@
 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
@@ -55,9 +61,10 @@
 
 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 CASE_INSENSITIVE_TEST_SERVICE_ID = 5
+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")
@@ -68,8 +75,10 @@
 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()
@@ -80,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
@@ -93,6 +109,14 @@
     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
@@ -134,6 +158,12 @@
     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 {
@@ -148,6 +178,8 @@
     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
@@ -168,12 +200,21 @@
         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()
     }
@@ -187,9 +228,9 @@
     @Test
     fun testAddService_OneNetwork() {
         val advertiser =
-            MdnsAdvertiser(thread.looper, socketProvider, cb, mockDeps, sharedlog, flags)
+            MdnsAdvertiser(thread.looper, socketProvider, cb, mockDeps, sharedlog, flags, context)
         postSync { advertiser.addOrUpdateService(SERVICE_ID_1, SERVICE_1,
-                null /* subtype */, DEFAULT_ADVERTISING_OPTION) }
+                DEFAULT_ADVERTISING_OPTION, TEST_CLIENT_UID_1) }
 
         val socketCbCaptor = ArgumentCaptor.forClass(SocketCallback::class.java)
         verify(socketProvider).requestSocket(eq(TEST_NETWORK_1), socketCbCaptor.capture())
@@ -216,7 +257,10 @@
         verify(cb).onOffloadStartOrUpdate(eq(TEST_INTERFACE1), eq(OFFLOAD_SERVICEINFO_NO_SUBTYPE))
 
         // Service is conflicted.
-        postSync { intAdvCbCaptor.value.onServiceConflict(mockInterfaceAdvertiser1, SERVICE_ID_1) }
+        postSync {
+            intAdvCbCaptor.value
+                    .onServiceConflict(mockInterfaceAdvertiser1, SERVICE_ID_1, CONFLICT_SERVICE)
+        }
 
         // Verify the metrics data
         doReturn(25).`when`(mockInterfaceAdvertiser1).getServiceRepliedRequestsCount(SERVICE_ID_1)
@@ -247,14 +291,14 @@
     }
 
     @Test
-    fun testAddService_AllNetworks() {
+    fun testAddService_AllNetworksWithSubType() {
         val advertiser =
-            MdnsAdvertiser(thread.looper, socketProvider, cb, mockDeps, sharedlog, flags)
-        postSync { advertiser.addOrUpdateService(SERVICE_ID_1, ALL_NETWORKS_SERVICE,
-                TEST_SUBTYPE, DEFAULT_ADVERTISING_OPTION) }
+            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
@@ -270,9 +314,9 @@
                 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.onServiceProbingSucceeded(
@@ -286,12 +330,21 @@
                 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) }
-        postSync { intAdvCbCaptor1.value.onServiceConflict(mockInterfaceAdvertiser1, SERVICE_ID_1) }
-        postSync { intAdvCbCaptor2.value.onServiceConflict(mockInterfaceAdvertiser2, SERVICE_ID_1) }
+        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)
@@ -319,11 +372,70 @@
     }
 
     @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, flags)
+            MdnsAdvertiser(thread.looper, socketProvider, cb, mockDeps, sharedlog, flags, context)
         postSync { advertiser.addOrUpdateService(SERVICE_ID_1, SERVICE_1,
-                null /* subtype */, DEFAULT_ADVERTISING_OPTION) }
+                DEFAULT_ADVERTISING_OPTION, TEST_CLIENT_UID_1) }
 
         val oneNetSocketCbCaptor = ArgumentCaptor.forClass(SocketCallback::class.java)
         verify(socketProvider).requestSocket(eq(TEST_NETWORK_1), oneNetSocketCbCaptor.capture())
@@ -331,18 +443,18 @@
 
         // Register a service with the same name on all networks (name conflict)
         postSync { advertiser.addOrUpdateService(SERVICE_ID_2, ALL_NETWORKS_SERVICE,
-                null /* subtype */, DEFAULT_ADVERTISING_OPTION) }
+                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.addOrUpdateService(LONG_SERVICE_ID_1, LONG_SERVICE_1,
-                null /* subtype */, DEFAULT_ADVERTISING_OPTION) }
+                DEFAULT_ADVERTISING_OPTION, TEST_CLIENT_UID_1) }
         postSync { advertiser.addOrUpdateService(LONG_SERVICE_ID_2, LONG_ALL_NETWORKS_SERVICE,
-                null /* subtype */, DEFAULT_ADVERTISING_OPTION) }
+                DEFAULT_ADVERTISING_OPTION, TEST_CLIENT_UID_1) }
 
         postSync { advertiser.addOrUpdateService(CASE_INSENSITIVE_TEST_SERVICE_ID,
-                ALL_NETWORKS_SERVICE_2, null /* subtype */, DEFAULT_ADVERTISING_OPTION) }
+                ALL_NETWORKS_SERVICE_2, DEFAULT_ADVERTISING_OPTION, TEST_CLIENT_UID_1) }
 
         // Callbacks for matching network and all networks both get the socket
         postSync {
@@ -378,15 +490,15 @@
                 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) }, eq(null))
+            argThat { it.matches(expectedCaseInsensitiveRenamed) }, any())
 
         doReturn(false).`when`(mockInterfaceAdvertiser1).isProbing(SERVICE_ID_1)
         postSync { intAdvCbCaptor.value.onServiceProbingSucceeded(
@@ -409,9 +521,10 @@
     @Test
     fun testAddOrUpdateService_Updates() {
         val advertiser =
-                MdnsAdvertiser(thread.looper, socketProvider, cb, mockDeps, sharedlog, flags)
+                MdnsAdvertiser(thread.looper, socketProvider, cb, mockDeps, sharedlog, flags,
+                    context)
         postSync { advertiser.addOrUpdateService(SERVICE_ID_1, ALL_NETWORKS_SERVICE,
-                null /* subtype */, DEFAULT_ADVERTISING_OPTION) }
+                DEFAULT_ADVERTISING_OPTION, TEST_CLIENT_UID_1) }
 
         val socketCbCaptor = ArgumentCaptor.forClass(SocketCallback::class.java)
         verify(socketProvider).requestSocket(eq(null), socketCbCaptor.capture())
@@ -420,38 +533,55 @@
         postSync { socketCb.onSocketCreated(TEST_SOCKETKEY_1, mockSocket1, listOf(TEST_LINKADDR)) }
 
         verify(mockInterfaceAdvertiser1).addService(eq(SERVICE_ID_1),
-                argThat { it.matches(ALL_NETWORKS_SERVICE) }, eq(null))
+                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, TEST_SUBTYPE,
-                updateOptions) }
+        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, TEST_SUBTYPE,
-                updateOptions) }
+        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, TEST_SUBTYPE,
-                updateOptions) }
-        verify(mockInterfaceAdvertiser1).updateService(eq(SERVICE_ID_1), eq(TEST_SUBTYPE))
+        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) }, eq(TEST_SUBTYPE))
+                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, flags)
+            MdnsAdvertiser(thread.looper, socketProvider, cb, mockDeps, sharedlog, flags, context)
         verify(mockDeps, times(1)).generateHostname()
         postSync { advertiser.addOrUpdateService(SERVICE_ID_1, SERVICE_1,
-                null /* subtype */, DEFAULT_ADVERTISING_OPTION) }
+                DEFAULT_ADVERTISING_OPTION, TEST_CLIENT_UID_1) }
         postSync { advertiser.removeService(SERVICE_ID_1) }
         verify(mockDeps, times(2)).generateHostname()
     }
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 2797462..27242f1 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsAnnouncerTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsAnnouncerTest.kt
@@ -55,6 +55,7 @@
     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() {
@@ -83,7 +84,7 @@
     @Test
     fun testAnnounce() {
         val replySender = MdnsReplySender(
-                thread.looper, socket, buffer, sharedLog, true /* enableDebugLog */)
+                thread.looper, socket, buffer, sharedLog, true /* enableDebugLog */, flags)
         @Suppress("UNCHECKED_CAST")
         val cb = mock(MdnsPacketRepeater.PacketRepeaterCallback::class.java)
                 as MdnsPacketRepeater.PacketRepeaterCallback<BaseAnnouncementInfo>
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 f85d71d..28608bb 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiserTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiserTest.kt
@@ -18,6 +18,7 @@
 
 import android.net.InetAddresses.parseNumericAddress
 import android.net.LinkAddress
+import android.net.nsd.NsdManager
 import android.net.nsd.NsdServiceInfo
 import android.os.Build
 import android.os.HandlerThread
@@ -26,6 +27,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 +37,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,6 +47,7 @@
 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
@@ -67,6 +71,13 @@
     port = 12345
 }
 
+private val TEST_SERVICE_1_SUBTYPE = NsdServiceInfo().apply {
+    subtypes = setOf("_sub")
+    serviceType = "_testservice._tcp"
+    serviceName = "MyTestService"
+    port = 12345
+}
+
 @RunWith(DevSdkIgnoreRunner::class)
 @IgnoreUpTo(Build.VERSION_CODES.S_V2)
 class MdnsInterfaceAdvertiserTest {
@@ -79,7 +90,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().build()
+    private val flags = MdnsFeatureFlags.newBuilder()
+            .setIsKnownAnswerSuppressionEnabled(true).build()
     @Suppress("UNCHECKED_CAST")
     private val probeCbCaptor = ArgumentCaptor.forClass(PacketRepeaterCallback::class.java)
             as ArgumentCaptor<PacketRepeaterCallback<ProbingInfo>>
@@ -110,7 +122,8 @@
     @Before
     fun setUp() {
         doReturn(repository).`when`(deps).makeRecordRepository(any(), eq(TEST_HOSTNAME), any())
-        doReturn(replySender).`when`(deps).makeReplySender(anyString(), any(), any(), any(), 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())
 
@@ -192,7 +205,8 @@
     fun testReplyToQuery() {
         addServiceAndFinishProbing(TEST_SERVICE_ID_1, TEST_SERVICE_1)
 
-        val testReply = MdnsReplyInfo(emptyList(), emptyList(), 0, InetSocketAddress(0))
+        val testReply = MdnsReplyInfo(emptyList(), emptyList(), 0, InetSocketAddress(0),
+                InetSocketAddress(0), emptyList())
         doReturn(testReply).`when`(repository).getReply(any(), any())
 
         // Query obtained with:
@@ -206,7 +220,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)
@@ -222,9 +241,116 @@
     }
 
     @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(
@@ -250,7 +376,7 @@
         }
 
         thread.waitForIdle(TIMEOUT_MS)
-        verify(cb).onServiceConflict(advertiser, TEST_SERVICE_ID_1)
+        verify(cb).onServiceConflict(advertiser, TEST_SERVICE_ID_1, CONFLICT_SERVICE)
     }
 
     @Test
@@ -278,8 +404,8 @@
     fun testReplaceExitingService() {
         doReturn(TEST_SERVICE_ID_DUPLICATE).`when`(repository)
                 .addService(eq(TEST_SERVICE_ID_DUPLICATE), any(), any())
-        val subType = "_sub"
-        advertiser.addService(TEST_SERVICE_ID_DUPLICATE, TEST_SERVICE_1, subType)
+        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())
@@ -289,8 +415,8 @@
     fun testUpdateExistingService() {
         doReturn(TEST_SERVICE_ID_DUPLICATE).`when`(repository)
                 .addService(eq(TEST_SERVICE_ID_DUPLICATE), any(), any())
-        val subType = "_sub"
-        advertiser.updateService(TEST_SERVICE_ID_DUPLICATE, subType)
+        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())
@@ -302,8 +428,8 @@
         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)
 
         // Simulate probing success: continues to announcing
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 ad30ce0..9474464 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClientTest.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClientTest.java
@@ -101,11 +101,17 @@
 
     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();
     }
@@ -365,4 +371,40 @@
         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);
+    }
 }
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 5b7c0ba..9befbc1 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsProberTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsProberTest.kt
@@ -61,6 +61,7 @@
     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() {
@@ -120,7 +121,7 @@
     @Test
     fun testProbe() {
         val replySender = MdnsReplySender(
-                thread.looper, socket, buffer, sharedLog, true /* enableDebugLog */)
+                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)))
@@ -145,7 +146,7 @@
     @Test
     fun testProbeMultipleRecords() {
         val replySender = MdnsReplySender(
-                thread.looper, socket, buffer, sharedLog, true /* enableDebugLog */)
+                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),
@@ -184,7 +185,7 @@
     @Test
     fun testStopProbing() {
         val replySender = MdnsReplySender(
-                thread.looper, socket, buffer, sharedLog, true /* enableDebugLog */)
+                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)),
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 c74e330..8d1dff6 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt
@@ -22,11 +22,19 @@
 import android.os.Build
 import android.os.HandlerThread
 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_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
@@ -35,7 +43,9 @@
 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
@@ -44,8 +54,19 @@
 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),
@@ -70,6 +91,26 @@
     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
+}
+
 @RunWith(DevSdkIgnoreRunner::class)
 @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
 class MdnsRecordRepositoryTest {
@@ -78,7 +119,6 @@
         override fun getInterfaceInetAddresses(iface: NetworkInterface) =
                 Collections.enumeration(TEST_ADDRESSES.map { it.address })
     }
-    private val flags = MdnsFeatureFlags.newBuilder().build()
 
     @Before
     fun setUp() {
@@ -91,12 +131,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, flags)
+        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, null /* ttl */))
         assertEquals(1, repository.servicesCount)
 
         val probingInfo = repository.setServiceProbing(TEST_SERVICE_ID_1)
@@ -119,7 +169,7 @@
         assertEquals(MdnsServiceRecord(expectedName,
                 0L /* receiptTimeMillis */,
                 false /* cacheFlush */,
-                120_000L /* ttlMillis */,
+                SHORT_TTL /* ttlMillis */,
                 0 /* servicePriority */, 0 /* serviceWeight */,
                 TEST_PORT, TEST_HOSTNAME), packet.authorityRecords[0])
 
@@ -128,31 +178,31 @@
 
     @Test
     fun testAddAndConflicts() {
-        val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
+        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 /* subtype */)
+            repository.addService(TEST_SERVICE_ID_3, TEST_SERVICE_3, null /* ttl */)
         }
     }
 
     @Test
     fun testAddAndUpdates() {
-        val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
+        val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
         repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
 
         assertFailsWith(IllegalArgumentException::class) {
-            repository.updateService(TEST_SERVICE_ID_2, null /* subtype */)
+            repository.updateService(TEST_SERVICE_ID_2, emptySet() /* subtype */)
         }
 
-        repository.updateService(TEST_SERVICE_ID_1, TEST_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, listOf() /* answers */,
-                listOf() /* authorityRecords */, listOf() /* additionalRecords */)
+        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)
 
@@ -174,19 +224,19 @@
 
     @Test
     fun testInvalidReuseOfServiceId() {
-        val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
-        repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* subtype */)
+        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 /* subtype */)
+            repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_2, null /* ttl */)
         }
     }
 
     @Test
     fun testHasActiveService() {
-        val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
+        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)
@@ -200,7 +250,7 @@
 
     @Test
     fun testExitAnnouncements() {
-        val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
+        val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
         repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
         repository.onAdvertisementSent(TEST_SERVICE_ID_1, 2 /* sentPacketCount */)
 
@@ -229,9 +279,10 @@
     }
 
     @Test
-    fun testExitAnnouncements_WithSubtype() {
-        val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
-        repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1, TEST_SUBTYPE)
+    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)
@@ -245,7 +296,7 @@
         assertEquals(0, packet.authorityRecords.size)
         assertEquals(0, packet.additionalRecords.size)
 
-        assertContentEquals(listOf(
+        assertThat(packet.answers).containsExactly(
                 MdnsPointerRecord(
                         arrayOf("_testservice", "_tcp", "local"),
                         0L /* receiptTimeMillis */,
@@ -258,7 +309,12 @@
                         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)
@@ -266,13 +322,13 @@
 
     @Test
     fun testExitingServiceReAdded() {
-        val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
+        val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
         repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_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)
@@ -281,9 +337,9 @@
 
     @Test
     fun testOnProbingSucceeded() {
-        val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
+        val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
         val announcementInfo = repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1,
-                TEST_SUBTYPE)
+                setOf(TEST_SUBTYPE, TEST_SUBTYPE2))
         repository.onAdvertisementSent(TEST_SERVICE_ID_1, 2 /* sentPacketCount */)
         val packet = announcementInfo.getPacket(0)
 
@@ -294,12 +350,13 @@
 
         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 */,
@@ -346,6 +403,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 */,
@@ -367,8 +431,7 @@
                         0L /* receiptTimeMillis */,
                         false /* cacheFlush */,
                         4500000L /* ttlMillis */,
-                        serviceType)
-        ), packet.answers)
+                        serviceType))
 
         assertContentEquals(listOf(
                 MdnsNsecRecord(v4AddrRev,
@@ -376,37 +439,37 @@
                         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, flags)
+        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")
@@ -468,13 +531,13 @@
 
     @Test
     fun testGetReplyCaseInsensitive() {
-        val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
+        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,
-            listOf() /* answers */, listOf() /* authorityRecords */,
-            listOf() /* additionalRecords */)
+            emptyList() /* answers */, emptyList() /* authorityRecords */,
+            emptyList() /* additionalRecords */)
         val src = InetSocketAddress(parseNumericAddress("192.0.2.123"), 5353)
         val replyCaseInsensitive = repository.getReply(queryCaseInsensitive, src)
         assertNotNull(replyCaseInsensitive)
@@ -482,103 +545,454 @@
         assertEquals(7, replyCaseInsensitive.additionalAnswers.size)
     }
 
-    @Test
-    fun testGetReply() {
-        doGetReplyTest(subtype = null)
+    /**
+     * 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_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, flags)
-        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, false /* isUnicast */))
-        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_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 testGetConflictingServices() {
-        val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
-        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 */,
@@ -598,15 +1012,18 @@
                 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, flags)
-        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 */,
@@ -626,15 +1043,138 @@
             emptyList() /* authorityRecords */,
             emptyList() /* additionalRecords */)
 
-        assertEquals(setOf(TEST_SERVICE_ID_1, TEST_SERVICE_ID_2),
-            repository.getConflictingServices(packet))
+        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.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::2")),
+                ) /* answers */,
+                emptyList() /* authorityRecords */,
+                emptyList() /* additionalRecords */)
+
+        assertEquals(emptyMap(),
+                repository.getConflictingServices(packet))
+    }
+
+    @Test
+    fun testGetConflictingServices_customHostsReplyHasIdenticalHosts_noConflict() {
+        val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+        repository.addService(TEST_CUSTOM_HOST_ID_1, TEST_CUSTOM_HOST_1, null /* ttl */)
+        repository.addService(TEST_CUSTOM_HOST_ID_2, TEST_CUSTOM_HOST_2, null /* ttl */)
+
+        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.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")),
+                ) /* answers */,
+                emptyList() /* authorityRecords */,
+                emptyList() /* additionalRecords */)
+
+        assertEquals(emptyMap(),
+                repository.getConflictingServices(packet))
     }
 
     @Test
     fun testGetConflictingServices_IdenticalService() {
-        val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
-        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(
@@ -656,14 +1196,14 @@
                 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, flags)
-        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(
@@ -685,12 +1225,12 @@
                 emptyList() /* additionalRecords */)
 
         // Above records are identical to the actual registrations: no conflict
-        assertEquals(emptySet(), repository.getConflictingServices(packet))
+        assertEquals(emptyMap(), repository.getConflictingServices(packet))
     }
 
     @Test
     fun testGetServiceRepliedRequestsCount() {
-        val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
+        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,
@@ -698,8 +1238,8 @@
 
         val questions = listOf(
                 MdnsPointerRecord(arrayOf("_testservice", "_tcp", "local"), false /* isUnicast */))
-        val query = MdnsPacket(0 /* flags */, questions, listOf() /* answers */,
-                listOf() /* authorityRecords */, listOf() /* additionalRecords */)
+        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.
@@ -715,11 +1255,11 @@
     @Test
     fun testIncludeInetAddressRecordsInProbing() {
         val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME,
-                MdnsFeatureFlags.newBuilder().setIncludeInetAddressRecordsInProbing(true).build())
+            makeFlags(includeInetAddressesInProbing = true))
         repository.updateAddresses(TEST_ADDRESSES)
         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, null /* ttl */))
         assertEquals(1, repository.servicesCount)
 
         val probingInfo = repository.setServiceProbing(TEST_SERVICE_ID_1)
@@ -746,7 +1286,7 @@
                 expectedName,
                 0L /* receiptTimeMillis */,
                 false /* cacheFlush */,
-                120_000L /* ttlMillis */,
+                SHORT_TTL /* ttlMillis */,
                 0 /* servicePriority */,
                 0 /* serviceWeight */,
                 TEST_PORT,
@@ -755,33 +1295,404 @@
                 TEST_HOSTNAME,
                 0L /* receiptTimeMillis */,
                 false /* cacheFlush */,
-                120_000L /* ttlMillis */,
+                SHORT_TTL /* ttlMillis */,
                 TEST_ADDRESSES[0].address),
             MdnsInetAddressRecord(
                 TEST_HOSTNAME,
                 0L /* receiptTimeMillis */,
                 false /* cacheFlush */,
-                120_000L /* ttlMillis */,
+                SHORT_TTL /* ttlMillis */,
                 TEST_ADDRESSES[1].address),
             MdnsInetAddressRecord(
                 TEST_HOSTNAME,
                 0L /* receiptTimeMillis */,
                 false /* cacheFlush */,
-                120_000L /* ttlMillis */,
+                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)
+    }
 }
 
 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/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/MdnsServiceInfoTest.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceInfoTest.java
index e7d7a98..8740e80 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;
 
@@ -202,7 +203,8 @@
                         List.of(),
                         /* textEntries= */ null,
                         /* interfaceIndex= */ 20,
-                        network);
+                        network,
+                        Instant.MAX /* expirationTime */);
 
         assertEquals(network, info2.getNetwork());
     }
@@ -225,7 +227,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 7a2e4bf..09236b1 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java
@@ -16,7 +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;
@@ -37,6 +43,7 @@
 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;
@@ -145,8 +152,8 @@
         MockitoAnnotations.initMocks(this);
         doReturn(TEST_ELAPSED_REALTIME).when(mockDecoderClock).elapsedRealtime();
 
-        expectedIPv4Packets = new DatagramPacket[16];
-        expectedIPv6Packets = new DatagramPacket[16];
+        expectedIPv4Packets = new DatagramPacket[24];
+        expectedIPv6Packets = new DatagramPacket[24];
         socketKey = new SocketKey(mockNetwork, INTERFACE_INDEX);
 
         for (int i = 0; i < expectedIPv4Packets.length; ++i) {
@@ -171,7 +178,15 @@
                 .thenReturn(expectedIPv4Packets[12])
                 .thenReturn(expectedIPv4Packets[13])
                 .thenReturn(expectedIPv4Packets[14])
-                .thenReturn(expectedIPv4Packets[15]);
+                .thenReturn(expectedIPv4Packets[15])
+                .thenReturn(expectedIPv4Packets[16])
+                .thenReturn(expectedIPv4Packets[17])
+                .thenReturn(expectedIPv4Packets[18])
+                .thenReturn(expectedIPv4Packets[19])
+                .thenReturn(expectedIPv4Packets[20])
+                .thenReturn(expectedIPv4Packets[21])
+                .thenReturn(expectedIPv4Packets[22])
+                .thenReturn(expectedIPv4Packets[23]);
 
         when(mockPacketWriter.getPacket(IPV6_ADDRESS))
                 .thenReturn(expectedIPv6Packets[0])
@@ -189,7 +204,15 @@
                 .thenReturn(expectedIPv6Packets[12])
                 .thenReturn(expectedIPv6Packets[13])
                 .thenReturn(expectedIPv6Packets[14])
-                .thenReturn(expectedIPv6Packets[15]);
+                .thenReturn(expectedIPv6Packets[15])
+                .thenReturn(expectedIPv6Packets[16])
+                .thenReturn(expectedIPv6Packets[17])
+                .thenReturn(expectedIPv6Packets[18])
+                .thenReturn(expectedIPv6Packets[19])
+                .thenReturn(expectedIPv6Packets[20])
+                .thenReturn(expectedIPv6Packets[21])
+                .thenReturn(expectedIPv6Packets[22])
+                .thenReturn(expectedIPv6Packets[23]);
 
         thread = new HandlerThread("MdnsServiceTypeClientTests");
         thread.start();
@@ -219,15 +242,22 @@
             return true;
         }).when(mockDeps).sendMessage(any(Handler.class), any(Message.class));
 
-        client =
-                new MdnsServiceTypeClient(SERVICE_TYPE, mockSocketClient, currentThreadExecutor,
-                        mockDecoderClock, socketKey, mockSharedLog, thread.getLooper(), mockDeps,
-                        serviceCache) {
-                    @Override
-                    MdnsPacketWriter createMdnsPacketWriter() {
-                        return mockPacketWriter;
-                    }
-                };
+        client = makeMdnsServiceTypeClient(mockPacketWriter);
+    }
+
+    private MdnsServiceTypeClient makeMdnsServiceTypeClient(
+            @Nullable MdnsPacketWriter packetWriter) {
+        return new MdnsServiceTypeClient(SERVICE_TYPE, mockSocketClient, currentThreadExecutor,
+                mockDecoderClock, socketKey, mockSharedLog, thread.getLooper(), mockDeps,
+                serviceCache) {
+            @Override
+            MdnsPacketWriter createMdnsPacketWriter() {
+                if (packetWriter == null) {
+                    return super.createMdnsPacketWriter();
+                }
+                return packetWriter;
+            }
+        };
     }
 
     @After
@@ -267,8 +297,8 @@
 
     @Test
     public void sendQueries_activeScanMode() {
-        MdnsSearchOptions searchOptions =
-                MdnsSearchOptions.newBuilder().addSubtype(SUBTYPE).setIsPassiveMode(false).build();
+        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));
@@ -319,8 +349,8 @@
 
     @Test
     public void sendQueries_reentry_activeScanMode() {
-        MdnsSearchOptions searchOptions =
-                MdnsSearchOptions.newBuilder().addSubtype(SUBTYPE).setIsPassiveMode(false).build();
+        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));
@@ -333,7 +363,7 @@
                 MdnsSearchOptions.newBuilder()
                         .addSubtype(SUBTYPE)
                         .addSubtype("_subtype2")
-                        .setIsPassiveMode(false)
+                        .setQueryMode(ACTIVE_QUERY_MODE)
                         .build();
         startSendAndReceive(mockListenerOne, searchOptions);
         // The previous scheduled task should be canceled.
@@ -353,8 +383,8 @@
 
     @Test
     public void sendQueries_passiveScanMode() {
-        MdnsSearchOptions searchOptions =
-                MdnsSearchOptions.newBuilder().addSubtype(SUBTYPE).setIsPassiveMode(true).build();
+        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));
@@ -380,8 +410,10 @@
     @Test
     public void sendQueries_activeScanWithQueryBackoff() {
         MdnsSearchOptions searchOptions =
-                MdnsSearchOptions.newBuilder().addSubtype(SUBTYPE).setIsPassiveMode(
-                        false).setNumOfQueriesBeforeBackoff(11).build();
+                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));
@@ -439,8 +471,10 @@
     @Test
     public void sendQueries_passiveScanWithQueryBackoff() {
         MdnsSearchOptions searchOptions =
-                MdnsSearchOptions.newBuilder().addSubtype(SUBTYPE).setIsPassiveMode(
-                        true).setNumOfQueriesBeforeBackoff(3).build();
+                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));
@@ -497,8 +531,8 @@
 
     @Test
     public void sendQueries_reentry_passiveScanMode() {
-        MdnsSearchOptions searchOptions =
-                MdnsSearchOptions.newBuilder().addSubtype(SUBTYPE).setIsPassiveMode(true).build();
+        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));
@@ -511,7 +545,7 @@
                 MdnsSearchOptions.newBuilder()
                         .addSubtype(SUBTYPE)
                         .addSubtype("_subtype2")
-                        .setIsPassiveMode(true)
+                        .setQueryMode(PASSIVE_QUERY_MODE)
                         .build();
         startSendAndReceive(mockListenerOne, searchOptions);
         // The previous scheduled task should be canceled.
@@ -533,16 +567,15 @@
     @Ignore("MdnsConfigs is not configurable currently.")
     public void testQueryTaskConfig_alwaysAskForUnicastResponse() {
         //MdnsConfigsFlagsImpl.alwaysAskForUnicastResponseInEachBurst.override(true);
-        MdnsSearchOptions searchOptions =
-                MdnsSearchOptions.newBuilder().addSubtype(SUBTYPE).setIsPassiveMode(false).build();
+        MdnsSearchOptions searchOptions = MdnsSearchOptions.newBuilder()
+                .addSubtype(SUBTYPE).setQueryMode(ACTIVE_QUERY_MODE).build();
         QueryTaskConfig config = new QueryTaskConfig(
-                searchOptions.getSubtypes(), searchOptions.isPassiveMode(),
+                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.
@@ -550,7 +583,6 @@
             int oldTransactionId = config.transactionId;
             config = config.getConfigForNextRun();
             assertFalse(config.expectUnicastResponse);
-            assertEquals(config.subtypes, searchOptions.getSubtypes());
             assertEquals(config.transactionId, oldTransactionId + 1);
         }
 
@@ -558,22 +590,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(SUBTYPE).setIsPassiveMode(false).build();
+        MdnsSearchOptions searchOptions = MdnsSearchOptions.newBuilder()
+                .addSubtype(SUBTYPE).setQueryMode(ACTIVE_QUERY_MODE).build();
         QueryTaskConfig config = new QueryTaskConfig(
-                searchOptions.getSubtypes(), searchOptions.isPassiveMode(),
+                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.
@@ -581,7 +611,6 @@
             int oldTransactionId = config.transactionId;
             config = config.getConfigForNextRun();
             assertFalse(config.expectUnicastResponse);
-            assertEquals(config.subtypes, searchOptions.getSubtypes());
             assertEquals(config.transactionId, oldTransactionId + 1);
         }
 
@@ -589,14 +618,13 @@
         int oldTransactionId = config.transactionId;
         config = config.getConfigForNextRun();
         assertFalse(config.expectUnicastResponse);
-        assertEquals(config.subtypes, searchOptions.getSubtypes());
         assertEquals(config.transactionId, oldTransactionId + 1);
     }
 
     @Test
     public void testIfPreviousTaskIsCanceledWhenNewSessionStarts() {
-        MdnsSearchOptions searchOptions =
-                MdnsSearchOptions.newBuilder().addSubtype(SUBTYPE).setIsPassiveMode(true).build();
+        MdnsSearchOptions searchOptions = MdnsSearchOptions.newBuilder()
+                .addSubtype(SUBTYPE).setQueryMode(PASSIVE_QUERY_MODE).build();
         startSendAndReceive(mockListenerOne, searchOptions);
         Runnable firstMdnsTask = currentThreadExecutor.getAndClearSubmittedRunnable();
 
@@ -605,7 +633,7 @@
                 MdnsSearchOptions.newBuilder()
                         .addSubtype(SUBTYPE)
                         .addSubtype("_subtype2")
-                        .setIsPassiveMode(true)
+                        .setQueryMode(PASSIVE_QUERY_MODE)
                         .build();
         startSendAndReceive(mockListenerOne, searchOptions);
 
@@ -624,8 +652,8 @@
     @Ignore("MdnsConfigs is not configurable currently.")
     public void testIfPreviousTaskIsCanceledWhenSessionStops() {
         //MdnsConfigsFlagsImpl.shouldCancelScanTaskWhenFutureIsNull.override(true);
-        MdnsSearchOptions searchOptions =
-                MdnsSearchOptions.newBuilder().addSubtype(SUBTYPE).setIsPassiveMode(true).build();
+        MdnsSearchOptions searchOptions = MdnsSearchOptions.newBuilder()
+                .addSubtype(SUBTYPE).setQueryMode(PASSIVE_QUERY_MODE).build();
         startSendAndReceive(mockListenerOne, searchOptions);
         // Change the sutypes and start a new session.
         stopSendAndReceive(mockListenerOne);
@@ -667,6 +695,81 @@
                 any(), any(), eq(MdnsConfigs.timeBetweenQueriesInBurstMs()));
     }
 
+    @Test
+    public void testCombinedSubtypesQueriedWithMultipleListeners() throws Exception {
+        client = makeMdnsServiceTypeClient(/* packetWriter= */ null);
+        final MdnsSearchOptions searchOptions1 = MdnsSearchOptions.newBuilder()
+                .addSubtype("subtype1").build();
+        final MdnsSearchOptions searchOptions2 = MdnsSearchOptions.newBuilder()
+                .addSubtype("subtype2").build();
+        startSendAndReceive(mockListenerOne, searchOptions1);
+        currentThreadExecutor.getAndClearSubmittedRunnable().run();
+
+        InOrder inOrder = inOrder(mockListenerOne, mockSocketClient, mockDeps);
+
+        // Verify the query asks for subtype1
+        final ArgumentCaptor<DatagramPacket> subtype1QueryCaptor =
+                ArgumentCaptor.forClass(DatagramPacket.class);
+        currentThreadExecutor.getAndClearLastScheduledRunnable().run();
+        // 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()));
+
+        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<DatagramPacket> combinedSubtypesQueryCaptor =
+                ArgumentCaptor.forClass(DatagramPacket.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()));
+
+        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<DatagramPacket> subtype2QueryCaptor =
+                ArgumentCaptor.forClass(DatagramPacket.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()));
+
+        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, SocketKey socketKey) {
@@ -919,15 +1022,7 @@
     public void processResponse_searchOptionsEnableServiceRemoval_shouldRemove()
             throws Exception {
         final String serviceInstanceName = "service-instance-1";
-        client =
-                new MdnsServiceTypeClient(SERVICE_TYPE, mockSocketClient, currentThreadExecutor,
-                        mockDecoderClock, socketKey, mockSharedLog, thread.getLooper(), mockDeps,
-                        serviceCache) {
-                    @Override
-                    MdnsPacketWriter createMdnsPacketWriter() {
-                        return mockPacketWriter;
-                    }
-                };
+        client = makeMdnsServiceTypeClient(mockPacketWriter);
         MdnsSearchOptions searchOptions = MdnsSearchOptions.newBuilder()
                 .setRemoveExpiredService(true)
                 .setNumOfQueriesBeforeBackoff(Integer.MAX_VALUE)
@@ -965,15 +1060,7 @@
     public void processResponse_searchOptionsNotEnableServiceRemoval_shouldNotRemove()
             throws Exception {
         final String serviceInstanceName = "service-instance-1";
-        client =
-                new MdnsServiceTypeClient(SERVICE_TYPE, mockSocketClient, currentThreadExecutor,
-                        mockDecoderClock, socketKey, mockSharedLog, thread.getLooper(), mockDeps,
-                        serviceCache) {
-                    @Override
-                    MdnsPacketWriter createMdnsPacketWriter() {
-                        return mockPacketWriter;
-                    }
-                };
+        client = makeMdnsServiceTypeClient(mockPacketWriter);
         startSendAndReceive(mockListenerOne, MdnsSearchOptions.getDefaultOptions());
         Runnable firstMdnsTask = currentThreadExecutor.getAndClearSubmittedRunnable();
 
@@ -999,15 +1086,7 @@
             throws Exception {
         //MdnsConfigsFlagsImpl.removeServiceAfterTtlExpires.override(true);
         final String serviceInstanceName = "service-instance-1";
-        client =
-                new MdnsServiceTypeClient(SERVICE_TYPE, mockSocketClient, currentThreadExecutor,
-                        mockDecoderClock, socketKey, mockSharedLog, thread.getLooper(), mockDeps,
-                        serviceCache) {
-                    @Override
-                    MdnsPacketWriter createMdnsPacketWriter() {
-                        return mockPacketWriter;
-                    }
-                };
+        client = makeMdnsServiceTypeClient(mockPacketWriter);
         startSendAndReceive(mockListenerOne, MdnsSearchOptions.getDefaultOptions());
         Runnable firstMdnsTask = currentThreadExecutor.getAndClearSubmittedRunnable();
 
@@ -1122,19 +1201,21 @@
 
     @Test
     public void testProcessResponse_Resolve() throws Exception {
-        client = new MdnsServiceTypeClient(SERVICE_TYPE, mockSocketClient, currentThreadExecutor,
-                mockDecoderClock, socketKey, mockSharedLog, thread.getLooper(), mockDeps,
-                serviceCache);
+        client = makeMdnsServiceTypeClient(/* packetWriter= */ null);
 
         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();
 
-        startSendAndReceive(mockListenerOne, resolveOptions);
+        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
@@ -1147,13 +1228,19 @@
                 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()));
 
         final String[] serviceName = getTestServiceName(instanceName);
+        assertEquals(1, srvTxtQueryPacket.questions.size());
         assertFalse(hasQuestion(srvTxtQueryPacket, MdnsRecord.TYPE_PTR));
         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(
@@ -1170,6 +1257,10 @@
                 Collections.emptyList() /* additionalRecords */);
 
         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
         dispatchMessage();
@@ -1179,11 +1270,18 @@
         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()));
+        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(
@@ -1200,10 +1298,12 @@
                 Collections.emptyList() /* additionalRecords */);
 
         inOrder.verify(mockListenerOne, never()).onServiceNameDiscovered(any(), anyBoolean());
+        verifyNoMoreInteractions(mockListenerTwo);
         processResponse(addressResponse, socketKey);
 
         inOrder.verify(mockListenerOne).onServiceFound(
                 serviceInfoCaptor.capture(), eq(false) /* isServiceFromCache */);
+        verify(mockListenerTwo).onServiceFound(any(), anyBoolean());
         verifyServiceInfo(serviceInfoCaptor.getValue(),
                 instanceName,
                 SERVICE_TYPE_LABELS,
@@ -1217,9 +1317,7 @@
 
     @Test
     public void testRenewTxtSrvInResolve() throws Exception {
-        client = new MdnsServiceTypeClient(SERVICE_TYPE, mockSocketClient, currentThreadExecutor,
-                mockDecoderClock, socketKey, mockSharedLog, thread.getLooper(), mockDeps,
-                serviceCache);
+        client = makeMdnsServiceTypeClient(/* packetWriter= */ null);
 
         final String instanceName = "service-instance";
         final String[] hostname = new String[] { "testhost "};
@@ -1333,9 +1431,7 @@
 
     @Test
     public void testProcessResponse_ResolveExcludesOtherServices() {
-        client = new MdnsServiceTypeClient(SERVICE_TYPE, mockSocketClient, currentThreadExecutor,
-                mockDecoderClock, socketKey, mockSharedLog, thread.getLooper(), mockDeps,
-                serviceCache);
+        client = makeMdnsServiceTypeClient(/* packetWriter= */ null);
 
         final String requestedInstance = "instance1";
         final String otherInstance = "instance2";
@@ -1403,9 +1499,7 @@
 
     @Test
     public void testProcessResponse_SubtypeDiscoveryLimitedToSubtype() {
-        client = new MdnsServiceTypeClient(SERVICE_TYPE, mockSocketClient, currentThreadExecutor,
-                mockDecoderClock, socketKey, mockSharedLog, thread.getLooper(), mockDeps,
-                serviceCache);
+        client = makeMdnsServiceTypeClient(/* packetWriter= */ null);
 
         final String matchingInstance = "instance1";
         final String subtype = "_subtype";
@@ -1492,10 +1586,91 @@
     }
 
     @Test
+    public void testProcessResponse_SubtypeChange() {
+        client = makeMdnsServiceTypeClient(/* packetWriter= */ null);
+
+        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 {
-        client = new MdnsServiceTypeClient(SERVICE_TYPE, mockSocketClient, currentThreadExecutor,
-                mockDecoderClock, socketKey, mockSharedLog, thread.getLooper(), mockDeps,
-                serviceCache);
+        client = makeMdnsServiceTypeClient(/* packetWriter= */ null);
 
         final String requestedInstance = "instance1";
         final String otherInstance = "instance2";
@@ -1666,6 +1841,111 @@
                 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 */);
+    }
+
     private static MdnsServiceInfo matchServiceName(String name) {
         return argThat(info -> info.getServiceInstanceName().equals(name));
     }
@@ -1712,6 +1992,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);
     }
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSBasicMethodsTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSBasicMethodsTest.kt
index 58f20a9..a5d5297 100644
--- a/tests/unit/java/com/android/server/connectivityservice/CSBasicMethodsTest.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/CSBasicMethodsTest.kt
@@ -23,11 +23,12 @@
 import androidx.test.filters.SmallTest
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
 import com.android.testutils.DevSdkIgnoreRunner
-import org.junit.Test
-import org.junit.runner.RunWith
 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)
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSBpfNetMapsTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSBpfNetMapsTest.kt
index c26ec53..8155fd0 100644
--- a/tests/unit/java/com/android/server/connectivityservice/CSBpfNetMapsTest.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/CSBpfNetMapsTest.kt
@@ -38,6 +38,7 @@
 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+.
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..be2b29c
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivityservice/CSCaptivePortalAppTest.kt
@@ -0,0 +1,127 @@
+/*
+ * 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.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.NetworkStack
+import android.net.CaptivePortal
+import android.net.NetworkRequest
+import android.net.NetworkScore
+import android.net.NetworkScore.KEEP_CONNECTED_FOR_TEST
+import android.net.RouteInfo
+import android.os.Build
+import android.os.Bundle
+import androidx.test.filters.SmallTest
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.assertThrows
+import com.android.testutils.TestableNetworkCallback
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.verify
+import kotlin.test.assertEquals
+
+// 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)
+        assertThrows(SecurityException::class.java, { captivePortal?.reevaluateNetwork() })
+    }
+
+    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/CSDestroyedNetworkTests.kt b/tests/unit/java/com/android/server/connectivityservice/CSDestroyedNetworkTests.kt
index 572c7bb..5c29e3a 100644
--- a/tests/unit/java/com/android/server/connectivityservice/CSDestroyedNetworkTests.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/CSDestroyedNetworkTests.kt
@@ -30,6 +30,7 @@
 
 private const val LONG_TIMEOUT_MS = 5_000
 
+@DevSdkIgnoreRunner.MonitorThreadLeak
 @RunWith(DevSdkIgnoreRunner::class)
 @SmallTest
 @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSKeepConnectedTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSKeepConnectedTest.kt
index a753922..94c68c0 100644
--- a/tests/unit/java/com/android/server/connectivityservice/CSKeepConnectedTest.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/CSKeepConnectedTest.kt
@@ -22,8 +22,8 @@
 import android.net.NetworkCapabilities.TRANSPORT_WIFI
 import android.net.NetworkRequest
 import android.net.NetworkScore
-import android.net.NetworkScore.KEEP_CONNECTED_LOCAL_NETWORK
 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
@@ -33,6 +33,7 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 
+@DevSdkIgnoreRunner.MonitorThreadLeak
 @RunWith(DevSdkIgnoreRunner::class)
 @SmallTest
 @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentCreationTests.kt b/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentCreationTests.kt
index 6add6b9..cb98454 100644
--- a/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentCreationTests.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentCreationTests.kt
@@ -33,6 +33,7 @@
 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
@@ -41,7 +42,6 @@
 import org.mockito.Mockito.inOrder
 import org.mockito.Mockito.never
 import org.mockito.Mockito.timeout
-import kotlin.test.assertFailsWith
 
 private const val TIMEOUT_MS = 2_000L
 private const val NO_CALLBACK_TIMEOUT_MS = 200L
@@ -51,6 +51,7 @@
 
 private fun defaultLnc() = FromS(LocalNetworkConfig.Builder().build())
 
+@DevSdkIgnoreRunner.MonitorThreadLeak
 @RunWith(DevSdkIgnoreRunner::class)
 @SmallTest
 @IgnoreUpTo(Build.VERSION_CODES.R)
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentTests.kt b/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentTests.kt
index dd0706b..c1730a4 100644
--- a/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentTests.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentTests.kt
@@ -20,6 +20,8 @@
 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
@@ -42,14 +44,15 @@
 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.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.verify
-import kotlin.test.assertFailsWith
 
 private const val TIMEOUT_MS = 200L
 private const val MEDIUM_TIMEOUT_MS = 1_000L
@@ -79,9 +82,28 @@
         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)
@@ -177,6 +199,266 @@
         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,
+        )
+        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)
@@ -196,11 +478,10 @@
         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()),
+                        .setUpstreamSelector(upstreamSelectorWifi)
+                        .setUpstreamMulticastRoutingConfig(multicastRoutingConfigMinScope)
+                        .setDownstreamMulticastRoutingConfig(multicastRoutingConfigSelected)
+                        .build()),
                 score = FromS(NetworkScore.Builder()
                         .setKeepConnectedReason(KEEP_CONNECTED_LOCAL_NETWORK)
                         .build())
@@ -219,10 +500,15 @@
         }
 
         clearInvocations(netd)
-        val inOrder = inOrder(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"
@@ -235,9 +521,16 @@
         cb.expect<Lost> { it.network == wifiAgent.network }
 
         inOrder.verify(netd).ipfwdAddInterfaceForward("local0", wifiIface2)
-        if (sameIfaceName) {
-            inOrder.verify(netd, never()).ipfwdRemoveInterfaceForward(any(), any())
-        }
+        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
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSNetworkActivityTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSNetworkActivityTest.kt
index 526ec9d..df0a2cc 100644
--- a/tests/unit/java/com/android/server/connectivityservice/CSNetworkActivityTest.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/CSNetworkActivityTest.kt
@@ -63,6 +63,7 @@
 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)
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/CSSatelliteNetworkFallbackTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSSatelliteNetworkFallbackTest.kt
new file mode 100644
index 0000000..9024641
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivityservice/CSSatelliteNetworkFallbackTest.kt
@@ -0,0 +1,202 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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_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.IgnoreUpTo
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.visibleOnHandlerThread
+import org.junit.Assert
+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 CSSatelliteNetworkPreferredTest : CSTest() {
+    /**
+     * 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_PREFERENCE_UIDS changes will send correct net id and uid ranges
+     * to netd.
+     */
+    @Test
+    fun testSatelliteNetworkPreferredUidsChanged() {
+        val netdInOrder = inOrder(netd)
+
+        val satelliteAgent = createSatelliteAgent("satellite0")
+        satelliteAgent.connect()
+
+        val satelliteNetId = satelliteAgent.network.netId
+        netdInOrder.verify(netd).networkCreate(
+            nativeNetworkConfigPhysical(satelliteNetId, INetd.PERMISSION_NONE))
+
+        val uid1 = PRIMARY_USER_HANDLE.getUid(TEST_PACKAGE_UID)
+        val uid2 = PRIMARY_USER_HANDLE.getUid(TEST_PACKAGE_UID2)
+        val uid3 = SECONDARY_USER_HANDLE.getUid(TEST_PACKAGE_UID)
+
+        // Initial satellite network preferred uids status.
+        setAndUpdateSatelliteNetworkPreferredUids(setOf())
+        netdInOrder.verify(netd, never()).networkAddUidRangesParcel(any())
+        netdInOrder.verify(netd, never()).networkRemoveUidRangesParcel(any())
+
+        // Set SATELLITE_NETWORK_PREFERENCE_UIDS setting and verify that net id and uid ranges
+        // send to netd
+        var uids = mutableSetOf(uid1, uid2, uid3)
+        val uidRanges1 = toUidRangeStableParcels(uidRangesForUids(uids))
+        val config1 = NativeUidRangeConfig(
+            satelliteNetId, uidRanges1,
+            PREFERENCE_ORDER_SATELLITE_FALLBACK
+        )
+        setAndUpdateSatelliteNetworkPreferredUids(uids)
+        netdInOrder.verify(netd).networkAddUidRangesParcel(config1)
+        netdInOrder.verify(netd, never()).networkRemoveUidRangesParcel(any())
+
+        // Set SATELLITE_NETWORK_PREFERENCE_UIDS setting again and verify that old rules are removed
+        // and new rules are added.
+        uids = mutableSetOf(uid1)
+        val uidRanges2: Array<UidRangeParcel?> = toUidRangeStableParcels(uidRangesForUids(uids))
+        val config2 = NativeUidRangeConfig(
+            satelliteNetId, uidRanges2,
+            PREFERENCE_ORDER_SATELLITE_FALLBACK
+        )
+        setAndUpdateSatelliteNetworkPreferredUids(uids)
+        netdInOrder.verify(netd).networkRemoveUidRangesParcel(config1)
+        netdInOrder.verify(netd).networkAddUidRangesParcel(config2)
+    }
+
+    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 setAndUpdateSatelliteNetworkPreferredUids(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): CSAgentWrapper {
+        return Agent(score = keepScore(), lp = lp(name),
+            nc = nc(TRANSPORT_SATELLITE, NET_CAPABILITY_INTERNET)
+        )
+    }
+
+    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 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()
+    )
+}
diff --git a/tests/unit/java/com/android/server/connectivityservice/base/CSAgentWrapper.kt b/tests/unit/java/com/android/server/connectivityservice/base/CSAgentWrapper.kt
index d41c742..d7343b1 100644
--- a/tests/unit/java/com/android/server/connectivityservice/base/CSAgentWrapper.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/base/CSAgentWrapper.kt
@@ -19,6 +19,8 @@
 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
@@ -75,10 +77,15 @@
 ) : 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!!
 
@@ -120,10 +127,10 @@
         }
         nmCallbacks.notifyProbeStatusChanged(0 /* completed */, 0 /* succeeded */)
         val p = NetworkTestResultParcelable()
-        p.result = VALIDATION_RESULT_INVALID
-        p.probesAttempted = 0
-        p.probesSucceeded = 0
-        p.redirectUrl = null
+        p.result = nmValidationResult
+        p.probesAttempted = nmProbesCompleted
+        p.probesSucceeded = nmProbesSucceeded
+        p.redirectUrl = nmValidationRedirectUrl
         p.timestampMillis = VALIDATION_TIMESTAMP
         nmCallbacks.notifyNetworkTestedWithExtras(p)
     }
@@ -171,4 +178,26 @@
 
     fun sendLocalNetworkConfig(lnc: LocalNetworkConfig) = agent.sendLocalNetworkConfig(lnc)
     fun sendNetworkCapabilities(nc: NetworkCapabilities) = agent.sendNetworkCapabilities(nc)
+
+    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
index 958c4f2..595ca47 100644
--- a/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt
@@ -16,6 +16,7 @@
 
 package com.android.server
 
+import android.app.AlarmManager
 import android.content.BroadcastReceiver
 import android.content.Context
 import android.content.Intent
@@ -46,8 +47,10 @@
 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.TelephonyManager
 import android.testing.TestableContext
 import android.util.ArraySet
@@ -61,20 +64,30 @@
 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.Consumer
+import java.util.function.BiConsumer
+import kotlin.test.assertNotNull
 import kotlin.test.assertNull
 import kotlin.test.fail
+import org.junit.After
+import org.junit.Before
 import org.mockito.AdditionalAnswers.delegatesTo
 import org.mockito.Mockito.doAnswer
 import org.mockito.Mockito.doReturn
 import org.mockito.Mockito.mock
 
-internal const val HANDLER_TIMEOUT_MS = 2_000
+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"
@@ -129,10 +142,12 @@
     // 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.DELAY_DESTROY_FROZEN_SOCKETS_VERSION] = true
         it[ConnectivityService.ALLOW_SYSUI_CONNECTIVITY_REPORTS] = true
         it[ConnectivityService.LOG_BPF_RC] = true
+        it[ConnectivityService.ALLOW_SATALLITE_NETWORK_FALLBACK] = true
     }
     fun enableFeature(f: String) = enabledFeatures.set(f, true)
     fun disableFeature(f: String) = enabledFeatures.set(f, false)
@@ -157,8 +172,8 @@
     val netd = mock<INetd>()
     val bpfNetMaps = mock<BpfNetMaps>()
     val clatCoordinator = mock<ClatCoordinator>()
+    val networkRequestStateStatsMetrics = mock<NetworkRequestStateStatsMetrics>()
     val proxyTracker = ProxyTracker(context, mock<Handler>(), 16 /* EVENT_PROXY_HAS_CHANGED */)
-    val alarmManager = makeMockAlarmManager()
     val systemConfigManager = makeMockSystemConfigManager()
     val batteryStats = mock<IBatteryStats>()
     val batteryManager = BatteryStatsManager(batteryStats)
@@ -166,10 +181,36 @@
         doReturn(true).`when`(it).isDataCapable()
     }
 
+    val multicastRoutingCoordinatorService = mock<MulticastRoutingCoordinatorService>()
+    val satelliteAccessController = mock<SatelliteAccessController>()
+
     val deps = CSDeps()
-    val service = makeConnectivityService(context, netd, deps).also { it.systemReadyInternal() }
-    val cm = ConnectivityManager(context, service)
-    val csHandler = Handler(csHandlerThread.looper)
+
+    // 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
+
+    @Before
+    fun setUp() {
+        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()
+    }
 
     inner class CSDeps : ConnectivityService.Dependencies() {
         override fun getResources(ctx: Context) = connResources
@@ -179,12 +220,26 @@
 
         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
+                tm: TelephonyManager,
+                requestRestrictedWifiEnabled: Boolean,
+                listener: BiConsumer<Int, Int>
         ) = 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)
@@ -197,6 +252,9 @@
                 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.
@@ -247,13 +305,65 @@
         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
 
-        // TODO : buff up the capabilities of this permission scheme to allow checking for
-        // permission rejections
-        override fun checkPermission(permission: String, pid: Int, uid: Int) = PERMISSION_GRANTED
-        override fun checkCallingOrSelfPermission(permission: String) = PERMISSION_GRANTED
+        // 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.
@@ -311,6 +421,16 @@
         ) {
             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
diff --git a/tests/unit/java/com/android/server/connectivityservice/base/CSTestHelpers.kt b/tests/unit/java/com/android/server/connectivityservice/base/CSTestHelpers.kt
index c1828b2..8ff790c 100644
--- a/tests/unit/java/com/android/server/connectivityservice/base/CSTestHelpers.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/base/CSTestHelpers.kt
@@ -53,6 +53,7 @@
 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
@@ -64,7 +65,6 @@
 import org.mockito.Mockito.doAnswer
 import org.mockito.Mockito.doNothing
 import org.mockito.Mockito.doReturn
-import kotlin.test.fail
 
 internal inline fun <reified T> mock() = Mockito.mock(T::class.java)
 internal inline fun <reified T> any() = any(T::class.java)
@@ -128,8 +128,8 @@
 }
 
 private val UNREASONABLY_LONG_ALARM_WAIT_MS = 1000
-internal fun makeMockAlarmManager() = mock<AlarmManager>().also { am ->
-    val alrmHdlr = HandlerThread("TestAlarmManager").also { it.start() }.threadHandler
+internal fun makeMockAlarmManager(handlerThread: HandlerThread) = mock<AlarmManager>().also { am ->
+    val alrmHdlr = handlerThread.threadHandler
     doAnswer {
         val (_, date, _, wakeupMsg, handler) = it.arguments
         wakeupMsg as WakeupMessage
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/NetworkStatsServiceTest.java b/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
index 7a4dfed..2be74db 100644
--- a/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
+++ b/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
@@ -64,9 +64,12 @@
 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.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;
@@ -83,6 +86,7 @@
 import static org.mockito.ArgumentMatchers.anyInt;
 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;
@@ -95,6 +99,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;
@@ -122,12 +127,15 @@
 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;
@@ -169,7 +177,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;
@@ -242,6 +249,7 @@
     private static @Mock WifiInfo sWifiInfo;
     private @Mock INetd mNetd;
     private @Mock TetheringManager mTetheringManager;
+    private @Mock PackageManager mPm;
     private @Mock NetworkStatsFactory mStatsFactory;
     @NonNull
     private final TestNetworkStatsSettings mSettings =
@@ -251,7 +259,7 @@
     private @Mock AlarmManager mAlarmManager;
     @Mock
     private NetworkStatsSubscriptionsMonitor mNetworkStatsSubscriptionsMonitor;
-    private @Mock BpfInterfaceMapUpdater mBpfInterfaceMapUpdater;
+    private @Mock BpfInterfaceMapHelper mBpfInterfaceMapHelper;
     private HandlerThread mHandlerThread;
     @Mock
     private LocationPermissionChecker mLocationPermissionChecker;
@@ -284,9 +292,14 @@
     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();
 
     private class MockContext extends BroadcastInterceptingContext {
         private final Context mBaseContext;
@@ -297,6 +310,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;
@@ -369,7 +392,6 @@
                 powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
 
         mHandlerThread = new HandlerThread("NetworkStatsServiceTest-HandlerThread");
-        final NetworkStatsService.Dependencies deps = makeDependencies();
         // 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
@@ -384,7 +406,7 @@
             }
         };
         mService = new NetworkStatsService(mServiceContext, mNetd, mAlarmManager, wakeLock,
-                mClock, mSettings, mStatsFactory, statsObservers, deps);
+                mClock, mSettings, mStatsFactory, statsObservers, mDeps);
 
         mElapsedRealtime = 0L;
 
@@ -420,135 +442,155 @@
                 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;
 
-            @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 boolean supportEventLogger(@NonNull Context cts) {
-                return true;
-            }
-        };
+        @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;
+        }
     }
 
     @After
@@ -1568,7 +1610,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
@@ -2166,6 +2208,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();
@@ -2230,7 +2337,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) {
@@ -2673,13 +2781,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");
     }
 
@@ -2694,13 +2802,13 @@
 
     @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");
     }
 
@@ -2713,4 +2821,48 @@
         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..27e6f96
--- /dev/null
+++ b/tests/unit/java/com/android/server/net/TrafficStatsRateLimitCacheTest.kt
@@ -0,0 +1,89 @@
+/*
+ * 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
+import com.android.testutils.DevSdkIgnoreRunner
+import java.time.Clock
+import kotlin.test.assertEquals
+import kotlin.test.assertNull
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.`when`
+
+@RunWith(DevSdkIgnoreRunner::class)
+class TrafficStatsRateLimitCacheTest {
+    companion object {
+        private const val expiryDurationMs = 1000L
+    }
+
+    private val clock = mock(Clock::class.java)
+    private val entry = mock(NetworkStats.Entry::class.java)
+    private val cache = TrafficStatsRateLimitCache(clock, expiryDurationMs)
+
+    @Test
+    fun testGet_returnsEntryIfNotExpired() {
+        cache.put("iface", 2, entry)
+        `when`(clock.millis()).thenReturn(500L) // Set clock to before expiry
+        val result = cache.get("iface", 2)
+        assertEquals(entry, result)
+    }
+
+    @Test
+    fun testGet_returnsNullIfExpired() {
+        cache.put("iface", 2, entry)
+        `when`(clock.millis()).thenReturn(2000L) // 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(NetworkStats.Entry::class.java)
+        val entry2 = mock(NetworkStats.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(NetworkStats.Entry::class.java)
+        val entry2 = mock(NetworkStats.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 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
index 1a6bddc..f74eab8 100644
--- a/tests/unit/vpn-jarjar-rules.txt
+++ b/tests/unit/vpn-jarjar-rules.txt
@@ -1,4 +1,2 @@
 # Only keep classes imported by ConnectivityServiceTest
-keep com.android.server.connectivity.Vpn
 keep com.android.server.connectivity.VpnProfileStore
-keep com.android.server.net.LockdownVpnTracker
diff --git a/thread/TEST_MAPPING b/thread/TEST_MAPPING
index 30aeca5..ebbb9af 100644
--- a/thread/TEST_MAPPING
+++ b/thread/TEST_MAPPING
@@ -2,11 +2,14 @@
   "presubmit": [
     {
       "name": "CtsThreadNetworkTestCases"
+    },
+    {
+      "name": "ThreadNetworkUnitTests"
+    },
+    {
+      "name": "ThreadNetworkIntegrationTests"
     }
   ],
   "postsubmit": [
-    {
-      "name": "ThreadNetworkUnitTests"
-    }
   ]
 }
diff --git a/thread/apex/Android.bp b/thread/apex/Android.bp
index 28854f2..edf000a 100644
--- a/thread/apex/Android.bp
+++ b/thread/apex/Android.bp
@@ -15,6 +15,7 @@
 //
 
 package {
+    default_team: "trendy_team_fwk_thread_network",
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
diff --git a/thread/apex/ot-daemon.34rc b/thread/apex/ot-daemon.34rc
index 1eb1294..25060d1 100644
--- a/thread/apex/ot-daemon.34rc
+++ b/thread/apex/ot-daemon.34rc
@@ -21,4 +21,5 @@
     user thread_network
     group thread_network inet system
     seclabel u:r:ot_daemon:s0
+    socket ot-daemon/thread-wpan.sock stream 0666 thread_network thread_network
     override
diff --git a/thread/demoapp/Android.bp b/thread/demoapp/Android.bp
index da7a5f8..fcfd469 100644
--- a/thread/demoapp/Android.bp
+++ b/thread/demoapp/Android.bp
@@ -13,6 +13,7 @@
 // limitations under the License.
 
 package {
+    default_team: "trendy_team_fwk_thread_network",
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
diff --git a/thread/framework/Android.bp b/thread/framework/Android.bp
index cc598d8..846253c 100644
--- a/thread/framework/Android.bp
+++ b/thread/framework/Android.bp
@@ -15,6 +15,7 @@
 //
 
 package {
+    default_team: "trendy_team_fwk_thread_network",
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
diff --git a/thread/framework/java/android/net/thread/IStateCallback.aidl b/thread/framework/java/android/net/thread/IStateCallback.aidl
index d7cbda9..9d0a571 100644
--- a/thread/framework/java/android/net/thread/IStateCallback.aidl
+++ b/thread/framework/java/android/net/thread/IStateCallback.aidl
@@ -22,4 +22,5 @@
 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
index 89dcd39..485e25d 100644
--- a/thread/framework/java/android/net/thread/IThreadNetworkController.aidl
+++ b/thread/framework/java/android/net/thread/IThreadNetworkController.aidl
@@ -38,6 +38,10 @@
     void scheduleMigration(in PendingOperationalDataset pendingOpDataset, in IOperationReceiver receiver);
     void leave(in IOperationReceiver receiver);
 
+    void setTestNetworkAsUpstream(in String testNetworkInterfaceName, in IOperationReceiver receiver);
+
     int getThreadVersion();
     void createRandomizedDataset(String networkName, IActiveOperationalDatasetReceiver receiver);
+
+    void setEnabled(boolean enabled, in IOperationReceiver receiver);
 }
diff --git a/thread/framework/java/android/net/thread/ThreadNetworkController.java b/thread/framework/java/android/net/thread/ThreadNetworkController.java
index 34b0b06..db761a3 100644
--- a/thread/framework/java/android/net/thread/ThreadNetworkController.java
+++ b/thread/framework/java/android/net/thread/ThreadNetworkController.java
@@ -31,6 +31,7 @@
 import android.os.RemoteException;
 
 import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -67,6 +68,15 @@
     /** 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({
@@ -78,6 +88,13 @@
     })
     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;
 
@@ -105,6 +122,40 @@
         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() {
@@ -169,6 +220,16 @@
          * @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 {
@@ -199,6 +260,16 @@
                 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);
+            }
+        }
     }
 
     /**
@@ -499,6 +570,32 @@
         }
     }
 
+    /**
+     * 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();
+        }
+    }
+
     private static <T> void propagateError(
             Executor executor,
             OutcomeReceiver<T, ThreadNetworkException> receiver,
diff --git a/thread/framework/java/android/net/thread/ThreadNetworkException.java b/thread/framework/java/android/net/thread/ThreadNetworkException.java
index c5e1e97..4def0fb 100644
--- a/thread/framework/java/android/net/thread/ThreadNetworkException.java
+++ b/thread/framework/java/android/net/thread/ThreadNetworkException.java
@@ -47,6 +47,8 @@
         ERROR_REJECTED_BY_PEER,
         ERROR_RESPONSE_BAD_FORMAT,
         ERROR_RESOURCE_EXHAUSTED,
+        ERROR_UNKNOWN,
+        ERROR_THREAD_DISABLED,
     })
     public @interface ErrorCode {}
 
@@ -87,8 +89,9 @@
 
     /**
      * 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. The
-     * caller should not retry the same operation before the precondition is satisfied.
+     * 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;
 
@@ -122,11 +125,42 @@
      */
     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;
+
+    private static final int ERROR_MIN = ERROR_INTERNAL_ERROR;
+    private static final int ERROR_MAX = ERROR_THREAD_DISABLED;
+
     private final int mErrorCode;
 
-    /** Creates a new {@link ThreadNetworkException} object with given error code and message. */
-    public ThreadNetworkException(@ErrorCode int errorCode, @NonNull String errorMessage) {
-        super(requireNonNull(errorMessage, "errorMessage cannot be null"));
+    /**
+     * 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;
     }
 
diff --git a/thread/framework/java/android/net/thread/ThreadNetworkManager.java b/thread/framework/java/android/net/thread/ThreadNetworkManager.java
index 28012a7..150b759 100644
--- a/thread/framework/java/android/net/thread/ThreadNetworkManager.java
+++ b/thread/framework/java/android/net/thread/ThreadNetworkManager.java
@@ -79,6 +79,17 @@
     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;
 
diff --git a/thread/scripts/make-pretty.sh b/thread/scripts/make-pretty.sh
index e4bd459..c176bfa 100755
--- a/thread/scripts/make-pretty.sh
+++ b/thread/scripts/make-pretty.sh
@@ -3,5 +3,7 @@
 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
index 35ae3c2..6e2fac1 100644
--- a/thread/service/Android.bp
+++ b/thread/service/Android.bp
@@ -15,6 +15,7 @@
 //
 
 package {
+    default_team: "trendy_team_fwk_thread_network",
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
@@ -35,17 +36,18 @@
     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",
         "ot-daemon-aidl-java",
     ],
     apex_available: ["com.android.tethering"],
-    optimize: {
-        proguard_flags_files: ["proguard.flags"],
-    },
 }
 
 cc_library_shared {
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..e3b4e1a
--- /dev/null
+++ b/thread/service/java/com/android/server/thread/ActiveOperationalDatasetReceiverWrapper.java
@@ -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.
+ */
+
+package com.android.server.thread;
+
+import static android.net.thread.ThreadNetworkException.ERROR_UNAVAILABLE;
+
+import android.net.thread.ActiveOperationalDataset;
+import android.net.thread.IActiveOperationalDatasetReceiver;
+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(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
index d7c49a0..be54cbc 100644
--- a/thread/service/java/com/android/server/thread/InfraInterfaceController.java
+++ b/thread/service/java/com/android/server/thread/InfraInterfaceController.java
@@ -36,8 +36,7 @@
      * @return an ICMPv6 socket file descriptor on the Infrastructure network interface.
      * @throws IOException when fails to create the socket.
      */
-    public static ParcelFileDescriptor createIcmp6Socket(String infraInterfaceName)
-            throws IOException {
+    public ParcelFileDescriptor createIcmp6Socket(String infraInterfaceName) throws IOException {
         return ParcelFileDescriptor.adoptFd(nativeCreateIcmp6Socket(infraInterfaceName));
     }
 
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..c74c023
--- /dev/null
+++ b/thread/service/java/com/android/server/thread/NsdPublisher.java
@@ -0,0 +1,335 @@
+/*
+ * 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.content.Context;
+import android.net.nsd.NsdManager;
+import android.net.nsd.NsdServiceInfo;
+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.INsdPublisher;
+import com.android.server.thread.openthread.INsdStatusReceiver;
+
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Deque;
+import java.util.HashSet;
+import java.util.List;
+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.
+ *
+ * <p>TODO: b/323300118 - Remove the following mechanism when the race condition in NsdManager is
+ * fixed.
+ *
+ * <p>There's always only one running registration job at any timepoint. All other pending jobs are
+ * queued in {@code mRegistrationJobs}. When a registration job is complete (i.e. the according
+ * method in {@link NsdManager.RegistrationListener} is called), it will start the next registration
+ * job in the queue.
+ */
+public final class NsdPublisher extends INsdPublisher.Stub {
+    // TODO: b/321883491 - specify network for mDNS operations
+    private static final String TAG = NsdPublisher.class.getSimpleName();
+    private final NsdManager mNsdManager;
+    private final Handler mHandler;
+    private final Executor mExecutor;
+    private final SparseArray<RegistrationListener> mRegistrationListeners = new SparseArray<>(0);
+    private final Deque<Runnable> mRegistrationJobs = new ArrayDeque<>();
+
+    @VisibleForTesting
+    public NsdPublisher(NsdManager nsdManager, Handler handler) {
+        mNsdManager = nsdManager;
+        mHandler = handler;
+        mExecutor = runnable -> mHandler.post(runnable);
+    }
+
+    public static NsdPublisher newInstance(Context context, Handler handler) {
+        return new NsdPublisher(context.getSystemService(NsdManager.class), handler);
+    }
+
+    @Override
+    public void registerService(
+            String hostname,
+            String name,
+            String type,
+            List<String> subTypeList,
+            int port,
+            List<DnsTxtAttribute> txt,
+            INsdStatusReceiver receiver,
+            int listenerId) {
+        postRegistrationJob(
+                () -> {
+                    NsdServiceInfo serviceInfo =
+                            buildServiceInfoForService(
+                                    hostname, name, type, subTypeList, port, txt);
+                    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;
+    }
+
+    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) {
+        postRegistrationJob(() -> 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);
+    }
+
+    private void checkOnHandlerThread() {
+        if (mHandler.getLooper().getThread() != Thread.currentThread()) {
+            throw new IllegalStateException(
+                    "Not running on handler Thread: " + Thread.currentThread().getName());
+        }
+    }
+
+    /** On ot-daemon died, unregister all registrations. */
+    public void onOtDaemonDied() {
+        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();
+    }
+
+    // TODO: b/323300118 - Remove this mechanism when the race condition in NsdManager is fixed.
+    /** Fetch the first job from the queue and run it. See the class doc for more details. */
+    private void peekAndRun() {
+        if (mRegistrationJobs.isEmpty()) {
+            return;
+        }
+        Runnable job = mRegistrationJobs.getFirst();
+        job.run();
+    }
+
+    // TODO: b/323300118 - Remove this mechanism when the race condition in NsdManager is fixed.
+    /**
+     * Pop the first job from the queue and run the next job. See the class doc for more details.
+     */
+    private void popAndRunNext() {
+        if (mRegistrationJobs.isEmpty()) {
+            Log.i(TAG, "No registration jobs when trying to pop and run next.");
+            return;
+        }
+        mRegistrationJobs.removeFirst();
+        peekAndRun();
+    }
+
+    private void postRegistrationJob(Runnable registrationJob) {
+        mHandler.post(
+                () -> {
+                    mRegistrationJobs.addLast(registrationJob);
+                    if (mRegistrationJobs.size() == 1) {
+                        peekAndRun();
+                    }
+                });
+    }
+
+    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
+            }
+            popAndRunNext();
+        }
+
+        @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
+                }
+            }
+            popAndRunNext();
+        }
+
+        @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
+            }
+            popAndRunNext();
+        }
+
+        @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);
+            popAndRunNext();
+        }
+    }
+}
diff --git a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
index 60c97bf..0623b87 100644
--- a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
+++ b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
@@ -14,8 +14,8 @@
 
 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_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;
@@ -26,6 +26,9 @@
 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;
@@ -34,33 +37,44 @@
 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.ErrorCode;
+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_DETACHED;
+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_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 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.net.ConnectivityManager;
-import android.net.IpPrefix;
+import android.net.InetAddresses;
 import android.net.LinkAddress;
 import android.net.LinkProperties;
 import android.net.LocalNetworkConfig;
-import android.net.MulticastRoutingConfig;
 import android.net.LocalNetworkInfo;
+import android.net.MulticastRoutingConfig;
 import android.net.Network;
 import android.net.NetworkAgent;
 import android.net.NetworkAgentConfig;
@@ -68,7 +82,7 @@
 import android.net.NetworkProvider;
 import android.net.NetworkRequest;
 import android.net.NetworkScore;
-import android.net.RouteInfo;
+import android.net.TestNetworkSpecifier;
 import android.net.thread.ActiveOperationalDataset;
 import android.net.thread.ActiveOperationalDataset.SecurityPolicy;
 import android.net.thread.IActiveOperationalDatasetReceiver;
@@ -80,23 +94,28 @@
 import android.net.thread.PendingOperationalDataset;
 import android.net.thread.ThreadNetworkController;
 import android.net.thread.ThreadNetworkController.DeviceRole;
+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.internal.annotations.VisibleForTesting;
 import com.android.server.ServiceManagerWrapper;
+import com.android.server.thread.openthread.BackboneRouterState;
+import com.android.server.thread.openthread.BorderRouterConfigurationParcel;
+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.OtDaemonState;
-import com.android.server.thread.openthread.BorderRouterConfigurationParcel;
 
 import java.io.IOException;
 import java.net.Inet6Address;
@@ -105,6 +124,7 @@
 import java.security.SecureRandom;
 import java.time.Instant;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Random;
@@ -115,10 +135,11 @@
  *
  * <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
- * `mHandlerThread` 2. In the @Override methods, the actual work MUST be dispatched to the
+ * 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";
 
@@ -127,82 +148,81 @@
     private final Context mContext;
     private final Handler mHandler;
 
-    // Below member fields can only be accessed from the handler thread (`mHandlerThread`). In
+    // 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 HandlerThread mHandlerThread;
     private final NetworkProvider mNetworkProvider;
     private final Supplier<IOtDaemon> mOtDaemonSupplier;
     private final ConnectivityManager mConnectivityManager;
     private final TunInterfaceController mTunIfController;
-    private final LinkProperties mLinkProperties = new LinkProperties();
+    private final InfraInterfaceController mInfraIfController;
+    private final NsdPublisher mNsdPublisher;
     private final OtDaemonCallbackProxy mOtDaemonCallbackProxy = new OtDaemonCallbackProxy();
 
-    // TODO(b/308310823): read supported channel from Thread dameon
-    private final int mSupportedChannelMask = 0x07FFF800; // from channel 11 to 26
+    @Nullable private IOtDaemon mOtDaemon;
+    @Nullable private NetworkAgent mNetworkAgent;
+    @Nullable private NetworkAgent mTestNetworkAgent;
 
-    private IOtDaemon mOtDaemon;
-    private NetworkAgent mNetworkAgent;
     private MulticastRoutingConfig mUpstreamMulticastRoutingConfig = CONFIG_FORWARD_NONE;
     private MulticastRoutingConfig mDownstreamMulticastRoutingConfig = CONFIG_FORWARD_NONE;
     private Network mUpstreamNetwork;
-    private final NetworkRequest mUpstreamNetworkRequest;
+    private NetworkRequest mUpstreamNetworkRequest;
+    private UpstreamNetworkCallback mUpstreamNetworkCallback;
+    private TestNetworkSpecifier mUpstreamTestNetworkSpecifier;
     private final HashMap<Network, String> mNetworkToInterface;
-    private final LocalNetworkConfig mLocalNetworkConfig;
+    private final ThreadPersistentSettings mPersistentSettings;
+    private final UserManager mUserManager;
+    private boolean mUserRestricted;
 
     private BorderRouterConfigurationParcel mBorderRouterConfig;
 
     @VisibleForTesting
     ThreadNetworkControllerService(
             Context context,
-            HandlerThread handlerThread,
+            Handler handler,
             NetworkProvider networkProvider,
             Supplier<IOtDaemon> otDaemonSupplier,
             ConnectivityManager connectivityManager,
-            TunInterfaceController tunIfController) {
+            TunInterfaceController tunIfController,
+            InfraInterfaceController infraIfController,
+            ThreadPersistentSettings persistentSettings,
+            NsdPublisher nsdPublisher,
+            UserManager userManager) {
         mContext = context;
-        mHandlerThread = handlerThread;
-        mHandler = new Handler(handlerThread.getLooper());
+        mHandler = handler;
         mNetworkProvider = networkProvider;
         mOtDaemonSupplier = otDaemonSupplier;
         mConnectivityManager = connectivityManager;
         mTunIfController = tunIfController;
-        mUpstreamNetworkRequest =
-                new NetworkRequest.Builder()
-                        .clearCapabilities()
-                        .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
-                        .addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET)
-                        .build();
-        mLocalNetworkConfig =
-                new LocalNetworkConfig.Builder()
-                        .setUpstreamSelector(mUpstreamNetworkRequest)
-                        .build();
+        mInfraIfController = infraIfController;
+        mUpstreamNetworkRequest = newUpstreamNetworkRequest();
         mNetworkToInterface = new HashMap<Network, String>();
         mBorderRouterConfig = new BorderRouterConfigurationParcel();
+        mPersistentSettings = persistentSettings;
+        mNsdPublisher = nsdPublisher;
+        mUserManager = userManager;
     }
 
-    public static ThreadNetworkControllerService newInstance(Context context) {
+    public static ThreadNetworkControllerService newInstance(
+            Context context, ThreadPersistentSettings persistentSettings) {
         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,
-                handlerThread,
+                handler,
                 networkProvider,
                 () -> IOtDaemon.Stub.asInterface(ServiceManagerWrapper.waitForService("ot_daemon")),
                 context.getSystemService(ConnectivityManager.class),
-                new TunInterfaceController(TUN_IF_NAME));
-    }
-
-    private static NetworkCapabilities newNetworkCapabilities() {
-        return new NetworkCapabilities.Builder()
-                .addTransportType(NetworkCapabilities.TRANSPORT_THREAD)
-                .addCapability(NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK)
-                .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED)
-                .build();
+                new TunInterfaceController(TUN_IF_NAME),
+                new InfraInterfaceController(),
+                persistentSettings,
+                NsdPublisher.newInstance(context, handler),
+                context.getSystemService(UserManager.class));
     }
 
     private static Inet6Address bytesToInet6Address(byte[] addressBytes) {
@@ -237,6 +257,28 @@
                 LinkAddress.LIFETIME_PERMANENT /* expirationTime */);
     }
 
+    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 initializeOtDaemon() {
         try {
             getOtDaemon();
@@ -246,6 +288,8 @@
     }
 
     private IOtDaemon getOtDaemon() throws RemoteException {
+        checkOnHandlerThread();
+
         if (mOtDaemon != null) {
             return mOtDaemon;
         }
@@ -254,19 +298,23 @@
         if (otDaemon == null) {
             throw new RemoteException("Internal error: failed to start OT daemon");
         }
-        otDaemon.asBinder().linkToDeath(() -> mHandler.post(this::onOtDaemonDied), 0);
-        otDaemon.initialize(mTunIfController.getTunFd());
+        otDaemon.initialize(mTunIfController.getTunFd(), isEnabled(), mNsdPublisher);
         otDaemon.registerStateCallback(mOtDaemonCallbackProxy, -1);
+        otDaemon.asBinder().linkToDeath(() -> mHandler.post(this::onOtDaemonDied), 0);
         mOtDaemon = otDaemon;
         return mOtDaemon;
     }
 
-    // TODO(b/309792480): restarts the OT daemon service
     private void onOtDaemonDied() {
-        Log.w(TAG, "OT daemon became dead, clean up...");
+        checkOnHandlerThread();
+        Log.w(TAG, "OT daemon is dead, clean up and restart it...");
+
         OperationReceiverWrapper.onOtDaemonDied();
         mOtDaemonCallbackProxy.onOtDaemonDied();
+        mTunIfController.onOtDaemonDied();
+        mNsdPublisher.onOtDaemonDied();
         mOtDaemon = null;
+        initializeOtDaemon();
     }
 
     public void initialize() {
@@ -279,56 +327,182 @@
                         throw new IllegalStateException(
                                 "Failed to create Thread tunnel interface", e);
                     }
-                    mLinkProperties.setInterfaceName(TUN_IF_NAME);
-                    mLinkProperties.setMtu(TunInterfaceController.MTU);
                     mConnectivityManager.registerNetworkProvider(mNetworkProvider);
                     requestUpstreamNetwork();
-
+                    requestThreadNetwork();
+                    mUserRestricted = isThreadUserRestricted();
+                    registerUserRestrictionsReceiver();
                     initializeOtDaemon();
                 });
     }
 
-    private void requestUpstreamNetwork() {
-        mConnectivityManager.registerNetworkCallback(
-                mUpstreamNetworkRequest,
-                new ConnectivityManager.NetworkCallback() {
-                    @Override
-                    public void onAvailable(@NonNull Network network) {
-                        Log.i(TAG, "onAvailable: " + network);
-                    }
+    public void setEnabled(boolean isEnabled, @NonNull IOperationReceiver receiver) {
+        enforceAllPermissionsGranted(PERMISSION_THREAD_NETWORK_PRIVILEGED);
 
-                    @Override
-                    public void onLost(@NonNull Network network) {
-                        Log.i(TAG, "onLost: " + network);
-                    }
+        mHandler.post(
+                () ->
+                        setEnabledInternal(
+                                isEnabled,
+                                true /* persist */,
+                                new OperationReceiverWrapper(receiver)));
+    }
 
+    private void setEnabledInternal(
+            boolean isEnabled, boolean persist, @NonNull OperationReceiverWrapper receiver) {
+        if (isEnabled && isThreadUserRestricted()) {
+            receiver.onError(
+                    ERROR_FAILED_PRECONDITION,
+                    "Cannot enable Thread: forbidden by user restriction");
+            return;
+        }
+
+        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 e) {
+            Log.e(TAG, "otDaemon.setThreadEnabled failed", e);
+            receiver.onError(ERROR_INTERNAL_ERROR, "Thread stack error");
+        }
+    }
+
+    private void registerUserRestrictionsReceiver() {
+        mContext.registerReceiver(
+                new BroadcastReceiver() {
                     @Override
-                    public void onLinkPropertiesChanged(
-                            @NonNull Network network, @NonNull LinkProperties linkProperties) {
-                        Log.i(
-                                TAG,
-                                String.format(
-                                        "onLinkPropertiesChanged: {network: %s, interface: %s}",
-                                        network, linkProperties.getInterfaceName()));
-                        mNetworkToInterface.put(network, linkProperties.getInterfaceName());
-                        if (network.equals(mUpstreamNetwork)) {
-                            enableBorderRouting(mNetworkToInterface.get(mUpstreamNetwork));
-                        }
+                    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 isEnabled = isEnabled();
+        final IOperationReceiver receiver =
+                new IOperationReceiver.Stub() {
+                    @Override
+                    public void onSuccess() {
+                        Log.d(
+                                TAG,
+                                (isEnabled ? "Enabled" : "Disabled")
+                                        + " Thread due to user restriction change");
+                    }
+
+                    @Override
+                    public void onError(int otError, String messages) {
+                        Log.e(
+                                TAG,
+                                "Failed to "
+                                        + (isEnabled ? "enable" : "disable")
+                                        + " Thread for user restriction change");
+                    }
+                };
+        // Do not save the user restriction state to persistent settings so that the user
+        // configuration won't be overwritten
+        setEnabledInternal(isEnabled, false /* persist */, new OperationReceiverWrapper(receiver));
+    }
+
+    /** Returns {@code true} if Thread is set enabled. */
+    private boolean isEnabled() {
+        return !mUserRestricted && mPersistentSettings.get(ThreadPersistentSettings.THREAD_ENABLED);
+    }
+
+    /** Returns {@code true} if Thread has been restricted for the user. */
+    private boolean isThreadUserRestricted() {
+        return mUserManager.hasUserRestriction(DISALLOW_THREAD_NETWORK);
+    }
+
+    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) {
-            Log.i(TAG, "onAvailable: Thread network Available");
+            checkOnHandlerThread();
+            Log.i(TAG, "Thread network available: " + network);
         }
 
         @Override
         public void onLocalNetworkInfoChanged(
                 @NonNull Network network, @NonNull LocalNetworkInfo localNetworkInfo) {
-            Log.i(TAG, "onLocalNetworkInfoChanged: " + localNetworkInfo);
+            checkOnHandlerThread();
+            Log.i(
+                    TAG,
+                    "LocalNetworkInfo of Thread network changed: {threadNetwork: "
+                            + network
+                            + ", localNetworkInfo: "
+                            + localNetworkInfo
+                            + "}");
             if (localNetworkInfo.getUpstreamNetwork() == null) {
                 mUpstreamNetwork = null;
                 return;
@@ -345,35 +519,54 @@
     private void requestThreadNetwork() {
         mConnectivityManager.registerNetworkCallback(
                 new NetworkRequest.Builder()
+                        // clearCapabilities() is needed to remove forbidden capabilities and UID
+                        // requirement.
                         .clearCapabilities()
                         .addTransportType(NetworkCapabilities.TRANSPORT_THREAD)
-                        .removeForbiddenCapability(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_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;
         }
-        NetworkCapabilities netCaps = newNetworkCapabilities();
-        NetworkScore score =
-                new NetworkScore.Builder()
-                        .setKeepConnectedReason(NetworkScore.KEEP_CONNECTED_LOCAL_NETWORK)
-                        .build();
-        requestThreadNetwork();
-        mNetworkAgent =
-                new NetworkAgent(
-                        mContext,
-                        mHandlerThread.getLooper(),
-                        TAG,
-                        netCaps,
-                        mLinkProperties,
-                        mLocalNetworkConfig,
-                        score,
-                        new NetworkAgentConfig.Builder().build(),
-                        mNetworkProvider) {};
+
+        mNetworkAgent = newNetworkAgent();
         mNetworkAgent.register();
         mNetworkAgent.markConnected();
         Log.i(TAG, "Registered Thread network");
@@ -392,46 +585,6 @@
         mNetworkAgent = null;
     }
 
-    private void updateTunInterfaceAddress(LinkAddress linkAddress, boolean isAdded) {
-        try {
-            if (isAdded) {
-                mTunIfController.addAddress(linkAddress);
-            } else {
-                mTunIfController.removeAddress(linkAddress);
-            }
-        } catch (IOException e) {
-            Log.e(
-                    TAG,
-                    String.format(
-                            "Failed to %s Thread tun interface address %s",
-                            (isAdded ? "add" : "remove"), linkAddress),
-                    e);
-        }
-    }
-
-    private void updateNetworkLinkProperties(LinkAddress linkAddress, boolean isAdded) {
-        RouteInfo routeInfo =
-                new RouteInfo(
-                        new IpPrefix(linkAddress.getAddress(), 64),
-                        null,
-                        TUN_IF_NAME,
-                        RouteInfo.RTN_UNICAST,
-                        TunInterfaceController.MTU);
-        if (isAdded) {
-            mLinkProperties.addLinkAddress(linkAddress);
-            mLinkProperties.addRoute(routeInfo);
-        } else {
-            mLinkProperties.removeLinkAddress(linkAddress);
-            mLinkProperties.removeRoute(routeInfo);
-        }
-
-        // The Thread daemon can send link property updates before the networkAgent is
-        // registered
-        if (mNetworkAgent != null) {
-            mNetworkAgent.sendLinkProperties(mLinkProperties);
-        }
-    }
-
     @Override
     public int getThreadVersion() {
         return THREAD_VERSION_1_3;
@@ -440,26 +593,51 @@
     @Override
     public void createRandomizedDataset(
             String networkName, IActiveOperationalDatasetReceiver receiver) {
-        mHandler.post(
-                () -> {
-                    ActiveOperationalDataset dataset =
-                            createRandomizedDatasetInternal(
-                                    networkName,
-                                    mSupportedChannelMask,
-                                    Instant.now(),
-                                    new Random(),
-                                    new SecureRandom());
-                    try {
-                        receiver.onSuccess(dataset);
-                    } catch (RemoteException e) {
-                        // The client is dead, do nothing
-                    }
-                });
+        ActiveOperationalDatasetReceiverWrapper receiverWrapper =
+                new ActiveOperationalDatasetReceiverWrapper(receiver);
+        mHandler.post(() -> createRandomizedDatasetInternal(networkName, receiverWrapper));
     }
 
-    private static ActiveOperationalDataset createRandomizedDatasetInternal(
+    private void createRandomizedDatasetInternal(
+            String networkName, @NonNull ActiveOperationalDatasetReceiverWrapper receiver) {
+        checkOnHandlerThread();
+
+        try {
+            getOtDaemon().getChannelMasks(newChannelMasksReceiver(networkName, receiver));
+        } catch (RemoteException e) {
+            Log.e(TAG, "otDaemon.getChannelMasks failed", e);
+            receiver.onError(ERROR_INTERNAL_ERROR, "Thread stack error");
+        }
+    }
+
+    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,
+                                Instant.now(),
+                                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,
             Instant now,
             Random random,
             SecureRandom secureRandom) {
@@ -469,6 +647,7 @@
 
         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};
 
@@ -479,7 +658,7 @@
                 .setExtendedPanId(newRandomBytes(random, LENGTH_EXTENDED_PAN_ID))
                 .setPanId(panId)
                 .setNetworkName(networkName)
-                .setChannel(CHANNEL_PAGE_24_GHZ, selectRandomChannel(supportedChannelMask, random))
+                .setChannel(CHANNEL_PAGE_24_GHZ, channel)
                 .setChannelMask(channelMask)
                 .setPskc(newRandomBytes(secureRandom, LENGTH_PSKC))
                 .setNetworkKey(newRandomBytes(secureRandom, LENGTH_NETWORK_KEY))
@@ -488,6 +667,18 @@
                 .build();
     }
 
+    private static int selectChannel(
+            int supportedChannelMask, int preferredChannelMask, Random random) {
+        // 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);
@@ -524,29 +715,29 @@
         return -1;
     }
 
-    private void enforceAllCallingPermissionsGranted(String... permissions) {
+    private void enforceAllPermissionsGranted(String... permissions) {
         for (String permission : permissions) {
-            mContext.enforceCallingPermission(
+            mContext.enforceCallingOrSelfPermission(
                     permission, "Permission " + permission + " is missing");
         }
     }
 
     @Override
     public void registerStateCallback(IStateCallback stateCallback) throws RemoteException {
-        enforceAllCallingPermissionsGranted(permission.ACCESS_NETWORK_STATE);
+        enforceAllPermissionsGranted(permission.ACCESS_NETWORK_STATE);
         mHandler.post(() -> mOtDaemonCallbackProxy.registerStateCallback(stateCallback));
     }
 
     @Override
     public void unregisterStateCallback(IStateCallback stateCallback) throws RemoteException {
-        enforceAllCallingPermissionsGranted(permission.ACCESS_NETWORK_STATE);
+        enforceAllPermissionsGranted(permission.ACCESS_NETWORK_STATE);
         mHandler.post(() -> mOtDaemonCallbackProxy.unregisterStateCallback(stateCallback));
     }
 
     @Override
     public void registerOperationalDatasetCallback(IOperationalDatasetCallback callback)
             throws RemoteException {
-        enforceAllCallingPermissionsGranted(
+        enforceAllPermissionsGranted(
                 permission.ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED);
         mHandler.post(() -> mOtDaemonCallbackProxy.registerDatasetCallback(callback));
     }
@@ -554,13 +745,13 @@
     @Override
     public void unregisterOperationalDatasetCallback(IOperationalDatasetCallback callback)
             throws RemoteException {
-        enforceAllCallingPermissionsGranted(
+        enforceAllPermissionsGranted(
                 permission.ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED);
         mHandler.post(() -> mOtDaemonCallbackProxy.unregisterDatasetCallback(callback));
     }
 
     private void checkOnHandlerThread() {
-        if (Looper.myLooper() != mHandlerThread.getLooper()) {
+        if (Looper.myLooper() != mHandler.getLooper()) {
             Log.wtf(TAG, "Must be on the handler thread!");
         }
     }
@@ -587,9 +778,6 @@
                 return ERROR_ABORTED;
             case OT_ERROR_BUSY:
                 return ERROR_BUSY;
-            case OT_ERROR_DETACHED:
-            case OT_ERROR_INVALID_STATE:
-                return ERROR_FAILED_PRECONDITION;
             case OT_ERROR_NO_BUFS:
                 return ERROR_RESOURCE_EXHAUSTED;
             case OT_ERROR_PARSE:
@@ -601,6 +789,11 @@
                 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;
         }
@@ -609,7 +802,7 @@
     @Override
     public void join(
             @NonNull ActiveOperationalDataset activeDataset, @NonNull IOperationReceiver receiver) {
-        enforceAllCallingPermissionsGranted(PERMISSION_THREAD_NETWORK_PRIVILEGED);
+        enforceAllPermissionsGranted(PERMISSION_THREAD_NETWORK_PRIVILEGED);
 
         OperationReceiverWrapper receiverWrapper = new OperationReceiverWrapper(receiver);
         mHandler.post(() -> joinInternal(activeDataset, receiverWrapper));
@@ -633,7 +826,7 @@
     public void scheduleMigration(
             @NonNull PendingOperationalDataset pendingDataset,
             @NonNull IOperationReceiver receiver) {
-        enforceAllCallingPermissionsGranted(PERMISSION_THREAD_NETWORK_PRIVILEGED);
+        enforceAllPermissionsGranted(PERMISSION_THREAD_NETWORK_PRIVILEGED);
 
         OperationReceiverWrapper receiverWrapper = new OperationReceiverWrapper(receiver);
         mHandler.post(() -> scheduleMigrationInternal(pendingDataset, receiverWrapper));
@@ -656,7 +849,7 @@
 
     @Override
     public void leave(@NonNull IOperationReceiver receiver) throws RemoteException {
-        enforceAllCallingPermissionsGranted(PERMISSION_THREAD_NETWORK_PRIVILEGED);
+        enforceAllPermissionsGranted(PERMISSION_THREAD_NETWORK_PRIVILEGED);
 
         mHandler.post(() -> leaveInternal(new OperationReceiverWrapper(receiver)));
     }
@@ -672,16 +865,74 @@
         }
     }
 
+    /**
+     * 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();
+
+        try {
+            getOtDaemon().setCountryCode(countryCode, newOtStatusReceiver(receiver));
+        } catch (RemoteException e) {
+            Log.e(TAG, "otDaemon.setCountryCode failed", e);
+            receiver.onError(ERROR_INTERNAL_ERROR, "Thread stack error");
+        }
+    }
+
+    @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
+        }
+    }
+
     private void enableBorderRouting(String infraIfName) {
         if (mBorderRouterConfig.isBorderRoutingEnabled
                 && infraIfName.equals(mBorderRouterConfig.infraInterfaceName)) {
             return;
         }
-        Log.i(TAG, "enableBorderRouting on AIL: " + infraIfName);
+        Log.i(TAG, "Enable border routing on AIL: " + infraIfName);
         try {
             mBorderRouterConfig.infraInterfaceName = infraIfName;
             mBorderRouterConfig.infraInterfaceIcmp6Socket =
-                    InfraInterfaceController.createIcmp6Socket(infraIfName);
+                    mInfraIfController.createIcmp6Socket(infraIfName);
             mBorderRouterConfig.isBorderRoutingEnabled = true;
 
             mOtDaemon.configureBorderRouter(
@@ -708,7 +959,7 @@
     private void handleThreadInterfaceStateChanged(boolean isUp) {
         try {
             mTunIfController.setInterfaceUp(isUp);
-            Log.d(TAG, "Thread network interface becomes " + (isUp ? "up" : "down"));
+            Log.i(TAG, "Thread TUN interface becomes " + (isUp ? "up" : "down"));
         } catch (IOException e) {
             Log.e(TAG, "Failed to handle Thread interface state changes", e);
         }
@@ -716,13 +967,13 @@
 
     private void handleDeviceRoleChanged(@DeviceRole int deviceRole) {
         if (ThreadNetworkController.isAttached(deviceRole)) {
-            Log.d(TAG, "Attached to the Thread network");
+            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.d(TAG, "Detached from the Thread network");
+            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
@@ -739,104 +990,66 @@
         }
 
         LinkAddress linkAddress = newLinkAddress(addressInfo);
-        Log.d(TAG, (isAdded ? "Adding" : "Removing") + " address " + linkAddress);
+        if (isAdded) {
+            mTunIfController.addAddress(linkAddress);
+        } else {
+            mTunIfController.removeAddress(linkAddress);
+        }
 
-        updateTunInterfaceAddress(linkAddress, isAdded);
-        updateNetworkLinkProperties(linkAddress, isAdded);
-    }
-
-    private boolean isMulticastForwardingEnabled() {
-        return !(mUpstreamMulticastRoutingConfig.getForwardingMode() == FORWARD_NONE
-                && mDownstreamMulticastRoutingConfig.getForwardingMode() == FORWARD_NONE);
+        // 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.Builder configBuilder = new LocalNetworkConfig.Builder();
-        LocalNetworkConfig localNetworkConfig =
-                configBuilder
-                        .setUpstreamMulticastRoutingConfig(mUpstreamMulticastRoutingConfig)
-                        .setDownstreamMulticastRoutingConfig(mDownstreamMulticastRoutingConfig)
-                        .setUpstreamSelector(mUpstreamNetworkRequest)
-                        .build();
+        final LocalNetworkConfig localNetworkConfig = newLocalNetworkConfig();
         mNetworkAgent.sendLocalNetworkConfig(localNetworkConfig);
-        Log.d(
-                TAG,
-                "Sent localNetworkConfig with upstreamConfig "
-                        + mUpstreamMulticastRoutingConfig
-                        + " downstreamConfig"
-                        + mDownstreamMulticastRoutingConfig);
+        Log.d(TAG, "Sent localNetworkConfig: " + localNetworkConfig);
     }
 
-    private void handleMulticastForwardingStateChanged(boolean isEnabled) {
-        if (isMulticastForwardingEnabled() == isEnabled) {
-            return;
-        }
-        if (isEnabled) {
+    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
-            mUpstreamMulticastRoutingConfig =
+            upstreamMulticastRoutingConfig =
                     new MulticastRoutingConfig.Builder(FORWARD_WITH_MIN_SCOPE, 4).build();
-            mDownstreamMulticastRoutingConfig =
-                    new MulticastRoutingConfig.Builder(FORWARD_SELECTED).build();
+            downstreamMulticastRoutingConfig =
+                    buildDownstreamMulticastRoutingConfigSelected(state.listeningAddresses);
         } else {
             // When multicast forwarding is disabled, set both upstream and downstream
             // forwarding config to FORWARD_NONE.
-            mUpstreamMulticastRoutingConfig = CONFIG_FORWARD_NONE;
-            mDownstreamMulticastRoutingConfig = CONFIG_FORWARD_NONE;
+            upstreamMulticastRoutingConfig = CONFIG_FORWARD_NONE;
+            downstreamMulticastRoutingConfig = CONFIG_FORWARD_NONE;
         }
+
+        if (upstreamMulticastRoutingConfig.equals(mUpstreamMulticastRoutingConfig)
+                && downstreamMulticastRoutingConfig.equals(mDownstreamMulticastRoutingConfig)) {
+            return;
+        }
+
+        mUpstreamMulticastRoutingConfig = upstreamMulticastRoutingConfig;
+        mDownstreamMulticastRoutingConfig = downstreamMulticastRoutingConfig;
         sendLocalNetworkConfig();
-        Log.d(
-                TAG,
-                "Sent updated localNetworkConfig with multicast forwarding "
-                        + (isEnabled ? "enabled" : "disabled"));
     }
 
-    private void handleMulticastForwardingAddressChanged(byte[] addressBytes, boolean isAdded) {
-        Inet6Address address = bytesToInet6Address(addressBytes);
-        MulticastRoutingConfig newDownstreamConfig;
-        MulticastRoutingConfig.Builder builder;
-
-        if (mDownstreamMulticastRoutingConfig.getForwardingMode() !=
-                MulticastRoutingConfig.FORWARD_SELECTED) {
-            Log.e(
-                    TAG,
-                    "Ignore multicast listening address updates when downstream multicast "
-                            + "forwarding mode is not FORWARD_SELECTED");
-            // Don't update the address set if downstream multicast forwarding is disabled.
-            return;
-        }
-        if (isAdded ==
-                mDownstreamMulticastRoutingConfig.getListeningAddresses().contains(address)) {
-            return;
-        }
-
-        builder = new MulticastRoutingConfig.Builder(FORWARD_SELECTED);
-        for (Inet6Address listeningAddress :
-                mDownstreamMulticastRoutingConfig.getListeningAddresses()) {
-            builder.addListeningAddress(listeningAddress);
-        }
-
-        if (isAdded) {
+    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);
-        } else {
-            builder.clearListeningAddress(address);
         }
-
-        newDownstreamConfig = builder.build();
-        if (!newDownstreamConfig.equals(mDownstreamMulticastRoutingConfig)) {
-            Log.d(
-                    TAG,
-                    "Multicast listening address "
-                            + address.getHostAddress()
-                            + " is "
-                            + (isAdded ? "added" : "removed"));
-            mDownstreamMulticastRoutingConfig = newDownstreamConfig;
-            sendLocalNetworkConfig();
-        }
+        return builder.build();
     }
 
     private static final class CallbackMetadata {
@@ -861,8 +1074,8 @@
     }
 
     /**
-     * Handles and forwards Thread daemon callbacks. This class must be accessed from the {@code
-     * mHandlerThread}.
+     * 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<>();
@@ -897,6 +1110,15 @@
             }
         }
 
+        private void notifyThreadEnabledUpdated(IStateCallback callback, int enabledState) {
+            try {
+                callback.onThreadEnableStateChanged(enabledState);
+                Log.i(TAG, "onThreadEnableStateChanged " + enabledState);
+            } catch (RemoteException ignored) {
+                // do nothing if the client is dead
+            }
+        }
+
         public void unregisterStateCallback(IStateCallback callback) {
             checkOnHandlerThread();
             if (!mStateCallbacks.containsKey(callback)) {
@@ -961,6 +1183,31 @@
         }
 
         @Override
+        public void onThreadEnabledChanged(int state) {
+            mHandler.post(() -> onThreadEnabledChangedInternal(state));
+        }
+
+        private void onThreadEnabledChangedInternal(int state) {
+            checkOnHandlerThread();
+            for (IStateCallback callback : mStateCallbacks.keySet()) {
+                notifyThreadEnabledUpdated(callback, otStateToAndroidState(state));
+            }
+        }
+
+        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));
         }
@@ -970,7 +1217,6 @@
             onInterfaceStateChanged(newState.isInterfaceUp);
             onDeviceRoleChanged(newState.deviceRole, listenerId);
             onPartitionIdChanged(newState.partitionId, listenerId);
-            onMulticastForwardingStateChanged(newState.multicastForwardingEnabled);
             mState = newState;
 
             ActiveOperationalDataset newActiveDataset;
@@ -1079,19 +1325,14 @@
             }
         }
 
-        private void onMulticastForwardingStateChanged(boolean isEnabled) {
-            checkOnHandlerThread();
-            handleMulticastForwardingStateChanged(isEnabled);
-        }
-
         @Override
         public void onAddressChanged(Ipv6AddressInfo addressInfo, boolean isAdded) {
             mHandler.post(() -> handleAddressChanged(addressInfo, isAdded));
         }
 
         @Override
-        public void onMulticastForwardingAddressChanged(byte[] address, boolean isAdded) {
-            mHandler.post(() -> handleMulticastForwardingAddressChanged(address, isAdded));
+        public void onBackboneRouterStateChanged(BackboneRouterState state) {
+            mHandler.post(() -> handleMulticastForwardingChanged(state));
         }
     }
 }
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..ffa7b44
--- /dev/null
+++ b/thread/service/java/com/android/server/thread/ThreadNetworkCountryCode.java
@@ -0,0 +1,590 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.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,
+            })
+    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 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();
+
+    @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) {
+        mLocationManager = locationManager;
+        mThreadNetworkControllerService = threadNetworkControllerService;
+        mGeocoder = geocoder;
+        mResources = resources;
+        mWifiManager = wifiManager;
+        mContext = context;
+        mTelephonyManager = telephonyManager;
+        mSubscriptionManager = subscriptionManager;
+
+        if (oemCountryCode != null) {
+            mOemCountryCodeInfo = new CountryCodeInfo(oemCountryCode, COUNTRY_CODE_SOURCE_OEM);
+        }
+    }
+
+    public static ThreadNetworkCountryCode newInstance(
+            Context context, ThreadNetworkControllerService controllerService) {
+        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));
+    }
+
+    /** 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;
+        }
+
+        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;
+                }
+            }
+
+            @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 or {@code null} if no country code is set. */
+    @Nullable
+    public synchronized String getCountryCode() {
+        return (mCurrentCountryCodeInfo != null) ? mCurrentCountryCodeInfo.getCountryCode() : null;
+    }
+
+    /**
+     * 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
index cc694a1..5664922 100644
--- a/thread/service/java/com/android/server/thread/ThreadNetworkService.java
+++ b/thread/service/java/com/android/server/thread/ThreadNetworkService.java
@@ -16,13 +16,20 @@
 
 package com.android.server.thread;
 
+import static android.content.pm.PackageManager.PERMISSION_GRANTED;
+
+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;
 
@@ -31,22 +38,35 @@
  */
 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 the service initializer.
+     * Called by {@link com.android.server.ConnectivityServiceInitializer}.
      *
      * @see com.android.server.SystemService#onBootPhase
      */
     public void onBootPhase(int phase) {
-        if (phase == SystemService.PHASE_BOOT_COMPLETED) {
-            mControllerService = ThreadNetworkControllerService.newInstance(mContext);
+        if (phase == SystemService.PHASE_SYSTEM_SERVICES_READY) {
+            mPersistentSettings.initialize();
+            mControllerService =
+                    ThreadNetworkControllerService.newInstance(mContext, mPersistentSettings);
             mControllerService.initialize();
+        } else if (phase == SystemService.PHASE_BOOT_COMPLETED) {
+            // Country code initialization is delayed to the BOOT_COMPLETED phase because it will
+            // call into Wi-Fi and Telephony service whose country code module is ready after
+            // PHASE_ACTIVITY_MANAGER_READY and PHASE_THIRD_PARTY_APPS_CAN_START
+            mCountryCode = ThreadNetworkCountryCode.newInstance(mContext, mControllerService);
+            mCountryCode.initialize();
+            mShellCommand = new ThreadNetworkShellCommand(mCountryCode);
         }
     }
 
@@ -57,4 +77,40 @@
         }
         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..c17c5a7
--- /dev/null
+++ b/thread/service/java/com/android/server/thread/ThreadNetworkShellCommand.java
@@ -0,0 +1,183 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.os.Binder;
+import android.os.Process;
+import android.text.TextUtils;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.modules.utils.BasicShellCommandHandler;
+
+import java.io.PrintWriter;
+import java.util.List;
+
+/**
+ * Interprets and executes 'adb shell cmd thread_network [args]'.
+ *
+ * <p>To add new commands: - onCommand: Add a case "<command>" execute. Return a 0 if command
+ * executed successfully. - onHelp: add a description string.
+ *
+ * <p>Permissions: currently root permission is required for some commands. Others will enforce the
+ * corresponding API permissions.
+ */
+public class ThreadNetworkShellCommand extends BasicShellCommandHandler {
+    private static final String TAG = "ThreadNetworkShellCommand";
+
+    // These don't require root access.
+    private static final List<String> NON_PRIVILEGED_COMMANDS = List.of("help", "get-country-code");
+
+    @Nullable private final ThreadNetworkCountryCode mCountryCode;
+    @Nullable private PrintWriter mOutputWriter;
+    @Nullable private PrintWriter mErrorWriter;
+
+    ThreadNetworkShellCommand(@Nullable ThreadNetworkCountryCode countryCode) {
+        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 int onCommand(String cmd) {
+        // Treat no command as help command.
+        if (TextUtils.isEmpty(cmd)) {
+            cmd = "help";
+        }
+
+        final PrintWriter pw = getOutputWriter();
+        final PrintWriter perr = getErrorWriter();
+
+        // Explicit exclusion from root permission
+        if (!NON_PRIVILEGED_COMMANDS.contains(cmd)) {
+            final int uid = Binder.getCallingUid();
+
+            if (uid != Process.ROOT_UID) {
+                perr.println(
+                        "Uid "
+                                + uid
+                                + " does not have access to "
+                                + cmd
+                                + " thread command "
+                                + "(or such command doesn't exist)");
+                return -1;
+            }
+        }
+
+        switch (cmd) {
+            case "force-country-code":
+                boolean enabled;
+
+                if (mCountryCode == null) {
+                    perr.println("Thread country code operations are not supported");
+                    return -1;
+                }
+
+                try {
+                    enabled = getNextArgRequiredTrueOrFalse("enabled", "disabled");
+                } catch (IllegalArgumentException e) {
+                    perr.println("Invalid argument: " + e.getMessage());
+                    return -1;
+                }
+
+                if (enabled) {
+                    String countryCode = getNextArgRequired();
+                    if (!ThreadNetworkCountryCode.isValidCountryCode(countryCode)) {
+                        perr.println(
+                                "Invalid argument: Country code must be a 2-Character"
+                                        + " string. But got country code "
+                                        + countryCode
+                                        + " instead");
+                        return -1;
+                    }
+                    mCountryCode.setOverrideCountryCode(countryCode);
+                    pw.println("Set Thread country code: " + countryCode);
+
+                } else {
+                    mCountryCode.clearOverrideCountryCode();
+                }
+                return 0;
+            case "get-country-code":
+                if (mCountryCode == null) {
+                    perr.println("Thread country code operations are not supported");
+                    return -1;
+                }
+
+                pw.println("Thread country code = " + mCountryCode.getCountryCode());
+                return 0;
+            default:
+                return handleDefaultCommands(cmd);
+        }
+    }
+
+    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);
+    }
+
+    private void onHelpNonPrivileged(PrintWriter pw) {
+        pw.println("  get-country-code");
+        pw.println("    Gets country code as a two-letter string");
+    }
+
+    private void onHelpPrivileged(PrintWriter pw) {
+        pw.println("  force-country-code enabled <two-letter code> | disabled ");
+        pw.println("    Sets country code to <two-letter code> or left for normal value");
+    }
+
+    @Override
+    public void onHelp() {
+        final PrintWriter pw = getOutputWriter();
+        pw.println("Thread network commands:");
+        pw.println("  help or -h");
+        pw.println("    Print this help text.");
+        onHelpNonPrivileged(pw);
+        if (Binder.getCallingUid() == Process.ROOT_UID) {
+            onHelpPrivileged(pw);
+        }
+        pw.println();
+    }
+}
diff --git a/thread/service/java/com/android/server/thread/ThreadPersistentSettings.java b/thread/service/java/com/android/server/thread/ThreadPersistentSettings.java
new file mode 100644
index 0000000..aba4193
--- /dev/null
+++ b/thread/service/java/com/android/server/thread/ThreadPersistentSettings.java
@@ -0,0 +1,279 @@
+/*
+ * 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.Nullable;
+import android.content.ApexEnvironment;
+import android.content.Context;
+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);
+
+    /******** 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 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);
+    }
+
+    /**
+     * 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.e(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
index 7223b2a..b29a54f 100644
--- a/thread/service/java/com/android/server/thread/TunInterfaceController.java
+++ b/thread/service/java/com/android/server/thread/TunInterfaceController.java
@@ -17,7 +17,10 @@
 package com.android.server.thread;
 
 import android.annotation.Nullable;
+import android.net.IpPrefix;
 import android.net.LinkAddress;
+import android.net.LinkProperties;
+import android.net.RouteInfo;
 import android.net.util.SocketUtils;
 import android.os.ParcelFileDescriptor;
 import android.os.SystemClock;
@@ -31,6 +34,7 @@
 
 import java.io.FileDescriptor;
 import java.io.IOException;
+import java.io.InterruptedIOException;
 
 /** Controller for virtual/tunnel network interfaces. */
 public class TunInterfaceController {
@@ -43,13 +47,21 @@
     }
 
     private final String mIfName;
+    private final LinkProperties mLinkProperties = new LinkProperties();
     private ParcelFileDescriptor mParcelTunFd;
     private FileDescriptor mNetlinkSocket;
     private static int sNetlinkSeqNo = 0;
 
     /** Creates a new {@link TunInterfaceController} instance for given interface. */
     public TunInterfaceController(String interfaceName) {
-        this.mIfName = interfaceName;
+        mIfName = interfaceName;
+        mLinkProperties.setInterfaceName(mIfName);
+        mLinkProperties.setMtu(MTU);
+    }
+
+    /** Returns link properties of the Thread TUN interface. */
+    public LinkProperties getLinkProperties() {
+        return mLinkProperties;
     }
 
     /**
@@ -87,13 +99,18 @@
 
     /** 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);
+            }
+        }
         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) throws IOException {
+    public void addAddress(LinkAddress address) {
         Log.d(TAG, "Adding address " + address + " with flags: " + address.getFlags());
 
         long validLifetimeSeconds;
@@ -121,7 +138,7 @@
 
         byte[] message =
                 RtNetlinkAddressMessage.newRtmNewAddressMessage(
-                        sNetlinkSeqNo,
+                        sNetlinkSeqNo++,
                         address.getAddress(),
                         (short) address.getPrefixLength(),
                         address.getFlags(),
@@ -131,13 +148,51 @@
                         preferredLifetimeSeconds);
         try {
             Os.write(mNetlinkSocket, message, 0, message.length);
-        } catch (ErrnoException e) {
-            throw new IOException("Failed to send netlink message", e);
+        } catch (ErrnoException | InterruptedIOException e) {
+            Log.e(TAG, "Failed to add address " + address, e);
+            return;
         }
+        mLinkProperties.addLinkAddress(address);
+        mLinkProperties.addRoute(getRouteForAddress(address));
     }
 
     /** Removes an address from the interface. */
-    public void removeAddress(LinkAddress address) throws IOException {
-        // TODO(b/263222068): remove address with netlink
+    public void removeAddress(LinkAddress address) {
+        Log.d(TAG, "Removing address " + address);
+        byte[] message =
+                RtNetlinkAddressMessage.newRtmDelAddressMessage(
+                        sNetlinkSeqNo++,
+                        address.getAddress(),
+                        (short) address.getPrefixLength(),
+                        Os.if_nametoindex(mIfName));
+
+        // Intentionally update the mLinkProperties before send netlink message because the
+        // address is already removed from ot-daemon and apps can't reach to the address even
+        // when the netlink request below fails
+        mLinkProperties.removeLinkAddress(address);
+        mLinkProperties.removeRoute(getRouteForAddress(address));
+        try {
+            Os.write(mNetlinkSocket, message, 0, message.length);
+        } catch (ErrnoException | InterruptedIOException e) {
+            Log.e(TAG, "Failed to remove address " + address, e);
+        }
+    }
+
+    private RouteInfo getRouteForAddress(LinkAddress linkAddress) {
+        return new RouteInfo(
+                new IpPrefix(linkAddress.getAddress(), linkAddress.getPrefixLength()),
+                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");
+        }
     }
 }
diff --git a/thread/service/proguard.flags b/thread/service/proguard.flags
deleted file mode 100644
index 5028982..0000000
--- a/thread/service/proguard.flags
+++ /dev/null
@@ -1,4 +0,0 @@
-# Ensure the callback methods are not stripped
--keepclassmembers class **.ThreadNetworkControllerService$ThreadNetworkCallback {
-    *;
-}
diff --git a/thread/tests/cts/Android.bp b/thread/tests/cts/Android.bp
index d91c9db..c1cf0a0 100644
--- a/thread/tests/cts/Android.bp
+++ b/thread/tests/cts/Android.bp
@@ -15,12 +15,13 @@
 //
 
 package {
+    default_team: "trendy_team_fwk_thread_network",
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
-// TODO: add this test to the CTS test suite
 android_test {
     name: "CtsThreadNetworkTestCases",
+    defaults: ["cts_defaults"],
     min_sdk_version: "33",
     sdk_version: "test_current",
     manifest: "AndroidManifest.xml",
@@ -29,7 +30,9 @@
         "src/**/*.java",
     ],
     test_suites: [
+        "cts",
         "general-tests",
+        "mcts-tethering",
         "mts-tethering",
     ],
     static_libs: [
@@ -39,14 +42,16 @@
         "guava",
         "guava-android-testlib",
         "net-tests-utils",
+        "ThreadNetworkTestUtils",
         "truth",
     ],
     libs: [
         "android.test.base",
         "android.test.runner",
-        "framework-connectivity-module-api-stubs-including-flagged"
+        "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
index 4370fe3..1541bf5 100644
--- a/thread/tests/cts/AndroidManifest.xml
+++ b/thread/tests/cts/AndroidManifest.xml
@@ -19,6 +19,9 @@
     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>
diff --git a/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java b/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java
index 362ff39..36ce4d5 100644
--- a/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java
+++ b/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java
@@ -16,24 +16,41 @@
 
 package android.net.thread.cts;
 
+import static android.Manifest.permission.ACCESS_NETWORK_STATE;
+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.assertNotNull;
 import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.fail;
 import static org.junit.Assume.assumeNotNull;
 
 import static java.util.concurrent.TimeUnit.MILLISECONDS;
 
-import android.Manifest.permission;
 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;
@@ -42,17 +59,20 @@
 import android.net.thread.ThreadNetworkController.StateCallback;
 import android.net.thread.ThreadNetworkException;
 import android.net.thread.ThreadNetworkManager;
+import android.net.thread.utils.TapTestNetworkTracker;
 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.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
 import com.android.testutils.DevSdkIgnoreRunner;
-
-import com.google.common.util.concurrent.SettableFuture;
+import com.android.testutils.FunctionalUtils.ThrowingRunnable;
 
 import org.junit.After;
 import org.junit.Before;
@@ -60,65 +80,744 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+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
 @RunWith(DevSdkIgnoreRunner.class)
 @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU) // Thread is available on only U+
 public class ThreadNetworkControllerTest {
-    private static final int CALLBACK_TIMEOUT_MILLIS = 1000;
-    private static final String PERMISSION_THREAD_NETWORK_PRIVILEGED =
+    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 String MESHCOP_SERVICE_TYPE = "_meshcop._udp";
+    private static final String THREAD_NETWORK_PRIVILEGED =
             "android.permission.THREAD_NETWORK_PRIVILEGED";
 
     @Rule public DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule();
 
     private final Context mContext = ApplicationProvider.getApplicationContext();
     private ExecutorService mExecutor;
-    private ThreadNetworkManager mManager;
+    private ThreadNetworkController mController;
+    private NsdManager mNsdManager;
 
     private Set<String> mGrantedPermissions;
+    private HandlerThread mHandlerThread;
+    private TapTestNetworkTracker mTestNetworkTracker;
 
     @Before
-    public void setUp() {
-        mExecutor = Executors.newSingleThreadExecutor();
-        mManager = mContext.getSystemService(ThreadNetworkManager.class);
-        mGrantedPermissions = new HashSet<String>();
+    public void setUp() throws Exception {
+        ThreadNetworkManager manager = mContext.getSystemService(ThreadNetworkManager.class);
+        if (manager != null) {
+            mController = manager.getAllThreadNetworkControllers().get(0);
+        }
 
         // TODO: we will also need it in tearDown(), it's better to have a Rule to skip
         // tests if a feature is not available.
-        assumeNotNull(mManager);
+        assumeNotNull(mController);
+
+        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 {
-        if (mManager != null) {
-            leaveAndWait();
+        if (mController == null) {
+            return;
+        }
+        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 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));
         }
     }
 
-    private List<ThreadNetworkController> getAllControllers() {
-        return mManager.getAllThreadNetworkControllers();
+    @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);
     }
 
-    private void leaveAndWait() throws Exception {
-        grantPermissions(PERMISSION_THREAD_NETWORK_PRIVILEGED);
+    @Test
+    public void unregisterStateCallback_callbackNotRegistered_throwsIllegalArgumentException()
+            throws Exception {
+        CompletableFuture<Integer> deviceRole = new CompletableFuture<>();
+        StateCallback callback = role -> deviceRole.complete(role);
 
-        for (ThreadNetworkController controller : getAllControllers()) {
-            SettableFuture<Void> future = SettableFuture.create();
-            controller.leave(mExecutor, future::set);
-            future.get();
+        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_toggleAfterJoin_joinsThreadNetworkAgain() throws Exception {
+        joinRandomizedDatasetAndWait(mController);
+
+        setEnabledAndWait(mController, false);
+        assertThat(getDeviceRole(mController)).isEqualTo(DEVICE_ROLE_STOPPED);
+        setEnabledAndWait(mController, true);
+
+        runAsShell(ACCESS_NETWORK_STATE, () -> waitForAttachedState(mController));
+    }
+
+    @Test
+    public void setEnabled_enableFollowedByDisable_allSucceed() throws Exception {
+        joinRandomizedDatasetAndWait(mController);
+        CompletableFuture<Void> setFuture1 = new CompletableFuture<>();
+        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 networkRequest =
+                new NetworkRequest.Builder()
+                        .addTransportType(NetworkCapabilities.TRANSPORT_THREAD)
+                        .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();
+    }
+
     private void grantPermissions(String... permissions) {
         for (String permission : permissions) {
             mGrantedPermissions.add(permission);
@@ -128,14 +827,79 @@
         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
+    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_DISCOVERY_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));
+    }
+
     private static void dropAllPermissions() {
         getInstrumentation().getUiAutomation().dropShellPermissionIdentity();
     }
 
     private static ActiveOperationalDataset newRandomizedDataset(
             String networkName, ThreadNetworkController controller) throws Exception {
-        SettableFuture<ActiveOperationalDataset> future = SettableFuture.create();
-        controller.createRandomizedDataset(networkName, directExecutor(), future::set);
+        CompletableFuture<ActiveOperationalDataset> future = new CompletableFuture<>();
+        controller.createRandomizedDataset(networkName, directExecutor(), future::complete);
         return future.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS);
     }
 
@@ -144,567 +908,302 @@
     }
 
     private static int getDeviceRole(ThreadNetworkController controller) throws Exception {
-        SettableFuture<Integer> future = SettableFuture.create();
-        StateCallback callback = future::set;
-        controller.registerStateCallback(directExecutor(), callback);
-        int role = future.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS);
-        controller.unregisterStateCallback(callback);
-        return role;
+        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 {
-        SettableFuture<Integer> future = SettableFuture.create();
+        CompletableFuture<Integer> future = new CompletableFuture<>();
         StateCallback callback =
                 newRole -> {
                     if (deviceRoles.contains(newRole)) {
-                        future.set(newRole);
+                        future.complete(newRole);
                     }
                 };
         controller.registerStateCallback(directExecutor(), callback);
-        int role = future.get();
+        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) {
+            assertNotNull(mReadHead.poll(ENABLED_TIMEOUT_MILLIS, e -> (e == enabled)));
+        }
+
+        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 {
-        SettableFuture<ActiveOperationalDataset> future = SettableFuture.create();
-        OperationalDatasetCallback callback = future::set;
-        controller.registerOperationalDatasetCallback(directExecutor(), callback);
-        ActiveOperationalDataset dataset = future.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS);
-        controller.unregisterOperationalDatasetCallback(callback);
-        return dataset;
+        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 {
-        SettableFuture<ActiveOperationalDataset> activeFuture = SettableFuture.create();
-        SettableFuture<PendingOperationalDataset> pendingFuture = SettableFuture.create();
+        CompletableFuture<ActiveOperationalDataset> activeFuture = new CompletableFuture<>();
+        CompletableFuture<PendingOperationalDataset> pendingFuture = new CompletableFuture<>();
         controller.registerOperationalDatasetCallback(
                 directExecutor(), newDatasetCallback(activeFuture, pendingFuture));
-        return pendingFuture.get();
+        return pendingFuture.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS);
     }
 
     private static OperationalDatasetCallback newDatasetCallback(
-            SettableFuture<ActiveOperationalDataset> activeFuture,
-            SettableFuture<PendingOperationalDataset> pendingFuture) {
+            CompletableFuture<ActiveOperationalDataset> activeFuture,
+            CompletableFuture<PendingOperationalDataset> pendingFuture) {
         return new OperationalDatasetCallback() {
             @Override
             public void onActiveOperationalDatasetChanged(
                     ActiveOperationalDataset activeOpDataset) {
-                activeFuture.set(activeOpDataset);
+                activeFuture.complete(activeOpDataset);
             }
 
             @Override
             public void onPendingOperationalDatasetChanged(
                     PendingOperationalDataset pendingOpDataset) {
-                pendingFuture.set(pendingOpDataset);
+                pendingFuture.complete(pendingOpDataset);
             }
         };
     }
 
-    @Test
-    public void getThreadVersion_returnsAtLeastThreadVersion1P3() {
-        for (ThreadNetworkController controller : getAllControllers()) {
-            assertThat(controller.getThreadVersion()).isAtLeast(THREAD_VERSION_1_3);
+    private static void assertDoesNotThrow(ThrowingRunnable runnable) {
+        try {
+            runnable.run();
+        } catch (Throwable e) {
+            fail("Should not have thrown " + e);
         }
     }
 
-    @Test
-    public void registerStateCallback_permissionsGranted_returnsCurrentStates() throws Exception {
-        grantPermissions(permission.ACCESS_NETWORK_STATE);
-
-        for (ThreadNetworkController controller : getAllControllers()) {
-            SettableFuture<Integer> deviceRole = SettableFuture.create();
-            StateCallback callback = deviceRole::set;
-
-            try {
-                controller.registerStateCallback(mExecutor, callback);
-
-                assertThat(deviceRole.get()).isEqualTo(DEVICE_ROLE_STOPPED);
-            } finally {
-                controller.unregisterStateCallback(callback);
-            }
+    // Return the first discovered service instance.
+    private NsdServiceInfo discoverService(String serviceType) 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, listener);
+        try {
+            serviceInfoFuture.get(SERVICE_DISCOVERY_TIMEOUT_MILLIS, MILLISECONDS);
+        } finally {
+            mNsdManager.stopServiceDiscovery(listener);
         }
+
+        return serviceInfoFuture.get();
     }
 
-    @Test
-    public void registerStateCallback_noPermissions_throwsSecurityException() throws Exception {
-        dropAllPermissions();
-
-        for (ThreadNetworkController controller : getAllControllers()) {
-            assertThrows(
-                    SecurityException.class,
-                    () -> controller.registerStateCallback(mExecutor, role -> {}));
-        }
+    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, listener);
+        return listener;
     }
 
-    @Test
-    public void registerStateCallback_alreadyRegistered_throwsIllegalArgumentException()
+    private NsdServiceInfo expectServiceResolved(
+            String serviceType, int timeoutMilliseconds, Predicate<NsdServiceInfo> predicate)
             throws Exception {
-        grantPermissions(permission.ACCESS_NETWORK_STATE);
-
-        for (ThreadNetworkController controller : getAllControllers()) {
-            SettableFuture<Integer> deviceRole = SettableFuture.create();
-            StateCallback callback = role -> deviceRole.set(role);
-            controller.registerStateCallback(mExecutor, callback);
-
-            assertThrows(
-                    IllegalArgumentException.class,
-                    () -> controller.registerStateCallback(mExecutor, callback));
+        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);
         }
     }
 
-    @Test
-    public void unregisterStateCallback_noPermissions_throwsSecurityException() throws Exception {
-        for (ThreadNetworkController controller : getAllControllers()) {
-            SettableFuture<Integer> deviceRole = SettableFuture.create();
-            StateCallback callback = role -> deviceRole.set(role);
-            grantPermissions(permission.ACCESS_NETWORK_STATE);
-            controller.registerStateCallback(mExecutor, callback);
-
-            try {
-                dropAllPermissions();
-                assertThrows(
-                        SecurityException.class,
-                        () -> controller.unregisterStateCallback(callback));
-            } finally {
-                grantPermissions(permission.ACCESS_NETWORK_STATE);
-                controller.unregisterStateCallback(callback);
-            }
-        }
+    private void setUpTestNetwork() {
+        assertThat(mTestNetworkTracker).isNull();
+        mTestNetworkTracker = new TapTestNetworkTracker(mContext, mHandlerThread.getLooper());
     }
 
-    @Test
-    public void unregisterStateCallback_callbackRegistered_success() throws Exception {
-        grantPermissions(permission.ACCESS_NETWORK_STATE);
-        for (ThreadNetworkController controller : getAllControllers()) {
-            SettableFuture<Integer> deviceRole = SettableFuture.create();
-            StateCallback callback = role -> deviceRole.set(role);
-            controller.registerStateCallback(mExecutor, callback);
-
-            controller.unregisterStateCallback(callback);
+    private void tearDownTestNetwork() throws InterruptedException {
+        if (mTestNetworkTracker != null) {
+            mTestNetworkTracker.tearDown();
         }
+        mHandlerThread.quitSafely();
+        mHandlerThread.join();
     }
 
-    @Test
-    public void unregisterStateCallback_callbackNotRegistered_throwsIllegalArgumentException()
-            throws Exception {
-        for (ThreadNetworkController controller : getAllControllers()) {
-            SettableFuture<Integer> deviceRole = SettableFuture.create();
-            StateCallback callback = role -> deviceRole.set(role);
+    private static class DefaultDiscoveryListener implements NsdManager.DiscoveryListener {
+        @Override
+        public void onStartDiscoveryFailed(String serviceType, int errorCode) {}
 
-            assertThrows(
-                    IllegalArgumentException.class,
-                    () -> controller.unregisterStateCallback(callback));
-        }
+        @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) {}
     }
 
-    @Test
-    public void unregisterStateCallback_alreadyUnregistered_throwsIllegalArgumentException()
-            throws Exception {
-        grantPermissions(permission.ACCESS_NETWORK_STATE);
-        for (ThreadNetworkController controller : getAllControllers()) {
-            SettableFuture<Integer> deviceRole = SettableFuture.create();
-            StateCallback callback = deviceRole::set;
-            controller.registerStateCallback(mExecutor, callback);
-            controller.unregisterStateCallback(callback);
+    private static class DefaultServiceInfoCallback implements NsdManager.ServiceInfoCallback {
+        @Override
+        public void onServiceInfoCallbackRegistrationFailed(int errorCode) {}
 
-            assertThrows(
-                    IllegalArgumentException.class,
-                    () -> controller.unregisterStateCallback(callback));
-        }
-    }
+        @Override
+        public void onServiceUpdated(@NonNull NsdServiceInfo serviceInfo) {}
 
-    @Test
-    public void registerOperationalDatasetCallback_permissionsGranted_returnsCurrentStates()
-            throws Exception {
-        grantPermissions(permission.ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED);
+        @Override
+        public void onServiceLost() {}
 
-        for (ThreadNetworkController controller : getAllControllers()) {
-            SettableFuture<ActiveOperationalDataset> activeFuture = SettableFuture.create();
-            SettableFuture<PendingOperationalDataset> pendingFuture = SettableFuture.create();
-            var callback = newDatasetCallback(activeFuture, pendingFuture);
-
-            try {
-                controller.registerOperationalDatasetCallback(mExecutor, callback);
-
-                assertThat(activeFuture.get()).isNull();
-                assertThat(pendingFuture.get()).isNull();
-            } finally {
-                controller.unregisterOperationalDatasetCallback(callback);
-            }
-        }
-    }
-
-    @Test
-    public void registerOperationalDatasetCallback_noPermissions_throwsSecurityException()
-            throws Exception {
-        dropAllPermissions();
-
-        for (ThreadNetworkController controller : getAllControllers()) {
-            SettableFuture<ActiveOperationalDataset> activeFuture = SettableFuture.create();
-            SettableFuture<PendingOperationalDataset> pendingFuture = SettableFuture.create();
-            var callback = newDatasetCallback(activeFuture, pendingFuture);
-
-            assertThrows(
-                    SecurityException.class,
-                    () -> controller.registerOperationalDatasetCallback(mExecutor, callback));
-        }
-    }
-
-    @Test
-    public void unregisterOperationalDatasetCallback_callbackRegistered_success() throws Exception {
-        grantPermissions(permission.ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED);
-        for (ThreadNetworkController controller : getAllControllers()) {
-            SettableFuture<ActiveOperationalDataset> activeFuture = SettableFuture.create();
-            SettableFuture<PendingOperationalDataset> pendingFuture = SettableFuture.create();
-            var callback = newDatasetCallback(activeFuture, pendingFuture);
-            controller.registerOperationalDatasetCallback(mExecutor, callback);
-
-            controller.unregisterOperationalDatasetCallback(callback);
-        }
-    }
-
-    @Test
-    public void unregisterOperationalDatasetCallback_noPermissions_throwsSecurityException()
-            throws Exception {
-        dropAllPermissions();
-
-        for (ThreadNetworkController controller : getAllControllers()) {
-            SettableFuture<ActiveOperationalDataset> activeFuture = SettableFuture.create();
-            SettableFuture<PendingOperationalDataset> pendingFuture = SettableFuture.create();
-            var callback = newDatasetCallback(activeFuture, pendingFuture);
-            grantPermissions(permission.ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED);
-            controller.registerOperationalDatasetCallback(mExecutor, callback);
-
-            try {
-                dropAllPermissions();
-                assertThrows(
-                        SecurityException.class,
-                        () -> controller.unregisterOperationalDatasetCallback(callback));
-            } finally {
-                grantPermissions(
-                        permission.ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED);
-                controller.unregisterOperationalDatasetCallback(callback);
-            }
-        }
-    }
-
-    private static <V> OutcomeReceiver<V, ThreadNetworkException> newOutcomeReceiver(
-            SettableFuture<V> future) {
-        return new OutcomeReceiver<V, ThreadNetworkException>() {
-            @Override
-            public void onResult(V result) {
-                future.set(result);
-            }
-
-            @Override
-            public void onError(ThreadNetworkException e) {
-                future.setException(e);
-            }
-        };
-    }
-
-    @Test
-    public void join_withPrivilegedPermission_success() throws Exception {
-        grantPermissions(PERMISSION_THREAD_NETWORK_PRIVILEGED);
-
-        for (ThreadNetworkController controller : getAllControllers()) {
-            ActiveOperationalDataset activeDataset = newRandomizedDataset("TestNet", controller);
-            SettableFuture<Void> joinFuture = SettableFuture.create();
-
-            controller.join(activeDataset, mExecutor, newOutcomeReceiver(joinFuture));
-            joinFuture.get();
-
-            grantPermissions(permission.ACCESS_NETWORK_STATE);
-            assertThat(isAttached(controller)).isTrue();
-            assertThat(getActiveOperationalDataset(controller)).isEqualTo(activeDataset);
-        }
-    }
-
-    @Test
-    public void join_withoutPrivilegedPermission_throwsSecurityException() throws Exception {
-        dropAllPermissions();
-
-        for (ThreadNetworkController controller : getAllControllers()) {
-            ActiveOperationalDataset activeDataset = newRandomizedDataset("TestNet", controller);
-
-            assertThrows(
-                    SecurityException.class,
-                    () -> controller.join(activeDataset, mExecutor, v -> {}));
-        }
-    }
-
-    @Test
-    public void join_concurrentRequests_firstOneIsAborted() throws Exception {
-        grantPermissions(PERMISSION_THREAD_NETWORK_PRIVILEGED);
-
-        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};
-        for (ThreadNetworkController controller : getAllControllers()) {
-            ActiveOperationalDataset activeDataset1 =
-                    new ActiveOperationalDataset.Builder(
-                                    newRandomizedDataset("TestNet", controller))
-                            .setNetworkKey(KEY_1)
-                            .build();
-            ActiveOperationalDataset activeDataset2 =
-                    new ActiveOperationalDataset.Builder(activeDataset1)
-                            .setNetworkKey(KEY_2)
-                            .build();
-            SettableFuture<Void> joinFuture1 = SettableFuture.create();
-            SettableFuture<Void> joinFuture2 = SettableFuture.create();
-
-            controller.join(activeDataset1, mExecutor, newOutcomeReceiver(joinFuture1));
-            controller.join(activeDataset2, mExecutor, newOutcomeReceiver(joinFuture2));
-
-            ThreadNetworkException thrown =
-                    (ThreadNetworkException)
-                            assertThrows(ExecutionException.class, joinFuture1::get).getCause();
-            assertThat(thrown.getErrorCode()).isEqualTo(ERROR_ABORTED);
-            joinFuture2.get();
-            grantPermissions(permission.ACCESS_NETWORK_STATE);
-            assertThat(isAttached(controller)).isTrue();
-            assertThat(getActiveOperationalDataset(controller)).isEqualTo(activeDataset2);
-        }
-    }
-
-    @Test
-    public void leave_withPrivilegedPermission_success() throws Exception {
-        grantPermissions(PERMISSION_THREAD_NETWORK_PRIVILEGED);
-
-        for (ThreadNetworkController controller : getAllControllers()) {
-            ActiveOperationalDataset activeDataset = newRandomizedDataset("TestNet", controller);
-            SettableFuture<Void> joinFuture = SettableFuture.create();
-            SettableFuture<Void> leaveFuture = SettableFuture.create();
-            controller.join(activeDataset, mExecutor, newOutcomeReceiver(joinFuture));
-            joinFuture.get();
-
-            controller.leave(mExecutor, newOutcomeReceiver(leaveFuture));
-            leaveFuture.get();
-
-            grantPermissions(permission.ACCESS_NETWORK_STATE);
-            assertThat(getDeviceRole(controller)).isEqualTo(DEVICE_ROLE_STOPPED);
-        }
-    }
-
-    @Test
-    public void leave_withoutPrivilegedPermission_throwsSecurityException() {
-        dropAllPermissions();
-
-        for (ThreadNetworkController controller : getAllControllers()) {
-            assertThrows(SecurityException.class, () -> controller.leave(mExecutor, v -> {}));
-        }
-    }
-
-    @Test
-    public void leave_concurrentRequests_bothSuccess() throws Exception {
-        grantPermissions(PERMISSION_THREAD_NETWORK_PRIVILEGED);
-
-        for (ThreadNetworkController controller : getAllControllers()) {
-            ActiveOperationalDataset activeDataset = newRandomizedDataset("TestNet", controller);
-            SettableFuture<Void> joinFuture = SettableFuture.create();
-            SettableFuture<Void> leaveFuture1 = SettableFuture.create();
-            SettableFuture<Void> leaveFuture2 = SettableFuture.create();
-            controller.join(activeDataset, mExecutor, newOutcomeReceiver(joinFuture));
-            joinFuture.get();
-
-            controller.leave(mExecutor, newOutcomeReceiver(leaveFuture1));
-            controller.leave(mExecutor, newOutcomeReceiver(leaveFuture2));
-
-            leaveFuture1.get();
-            leaveFuture2.get();
-            grantPermissions(permission.ACCESS_NETWORK_STATE);
-            assertThat(getDeviceRole(controller)).isEqualTo(DEVICE_ROLE_STOPPED);
-        }
-    }
-
-    @Test
-    public void scheduleMigration_withPrivilegedPermission_success() throws Exception {
-        grantPermissions(permission.ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED);
-
-        for (ThreadNetworkController controller : getAllControllers()) {
-            ActiveOperationalDataset activeDataset1 =
-                    new ActiveOperationalDataset.Builder(
-                                    newRandomizedDataset("TestNet", controller))
-                            .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));
-            SettableFuture<Void> joinFuture = SettableFuture.create();
-            SettableFuture<Void> migrateFuture = SettableFuture.create();
-            controller.join(activeDataset1, mExecutor, newOutcomeReceiver(joinFuture));
-            joinFuture.get();
-
-            controller.scheduleMigration(
-                    pendingDataset2, mExecutor, newOutcomeReceiver(migrateFuture));
-
-            migrateFuture.get();
-            Thread.sleep(35 * 1000);
-            assertThat(getActiveOperationalDataset(controller)).isEqualTo(activeDataset2);
-            assertThat(getPendingOperationalDataset(controller)).isNull();
-        }
-    }
-
-    @Test
-    public void scheduleMigration_whenNotAttached_failWithPreconditionError() throws Exception {
-        grantPermissions(permission.ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED);
-
-        for (ThreadNetworkController controller : getAllControllers()) {
-            PendingOperationalDataset pendingDataset =
-                    new PendingOperationalDataset(
-                            newRandomizedDataset("TestNet", controller),
-                            OperationalDatasetTimestamp.fromInstant(Instant.now()),
-                            Duration.ofSeconds(30));
-            SettableFuture<Void> migrateFuture = SettableFuture.create();
-
-            controller.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(permission.ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED);
-
-        for (ThreadNetworkController controller : getAllControllers()) {
-            final ActiveOperationalDataset activeDataset =
-                    new ActiveOperationalDataset.Builder(
-                                    newRandomizedDataset("testNet", controller))
-                            .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));
-            SettableFuture<Void> joinFuture = SettableFuture.create();
-            SettableFuture<Void> migrateFuture1 = SettableFuture.create();
-            SettableFuture<Void> migrateFuture2 = SettableFuture.create();
-            controller.join(activeDataset, mExecutor, newOutcomeReceiver(joinFuture));
-            joinFuture.get();
-
-            controller.scheduleMigration(
-                    pendingDataset1, mExecutor, newOutcomeReceiver(migrateFuture1));
-            migrateFuture1.get();
-            controller.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_success() throws Exception {
-        grantPermissions(permission.ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED);
-
-        for (ThreadNetworkController controller : getAllControllers()) {
-            final ActiveOperationalDataset activeDataset =
-                    new ActiveOperationalDataset.Builder(
-                                    newRandomizedDataset("validName", controller))
-                            .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));
-            SettableFuture<Void> joinFuture = SettableFuture.create();
-            SettableFuture<Void> migrateFuture1 = SettableFuture.create();
-            SettableFuture<Void> migrateFuture2 = SettableFuture.create();
-            controller.join(activeDataset, mExecutor, newOutcomeReceiver(joinFuture));
-            joinFuture.get();
-
-            controller.scheduleMigration(
-                    pendingDataset1, mExecutor, newOutcomeReceiver(migrateFuture1));
-            migrateFuture1.get();
-            controller.scheduleMigration(
-                    pendingDataset2, mExecutor, newOutcomeReceiver(migrateFuture2));
-
-            migrateFuture2.get();
-            Thread.sleep(35 * 1000);
-            assertThat(getActiveOperationalDataset(controller)).isEqualTo(activeDataset2);
-            assertThat(getPendingOperationalDataset(controller)).isNull();
-        }
-    }
-
-    @Test
-    public void createRandomizedDataset_wrongNetworkNameLength_throwsIllegalArgumentException() {
-        for (ThreadNetworkController controller : getAllControllers()) {
-            assertThrows(
-                    IllegalArgumentException.class,
-                    () -> controller.createRandomizedDataset("", mExecutor, dataset -> {}));
-
-            assertThrows(
-                    IllegalArgumentException.class,
-                    () ->
-                            controller.createRandomizedDataset(
-                                    "ANetNameIs17Bytes", mExecutor, dataset -> {}));
-        }
-    }
-
-    @Test
-    public void createRandomizedDataset_validNetworkName_success() throws Exception {
-        for (ThreadNetworkController controller : getAllControllers()) {
-            ActiveOperationalDataset dataset = newRandomizedDataset("validName", controller);
-
-            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);
-        }
+        @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..7d9ae81
--- /dev/null
+++ b/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkExceptionTest.java
@@ -0,0 +1,65 @@
+/*
+ * 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 androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** CTS tests for {@link ThreadNetworkException}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public final class ThreadNetworkExceptionTest {
+    @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/integration/Android.bp b/thread/tests/integration/Android.bp
new file mode 100644
index 0000000..6ba192d
--- /dev/null
+++ b/thread/tests/integration/Android.bp
@@ -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 {
+    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",
+        "testables",
+        "truth",
+    ],
+    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..152c1c3
--- /dev/null
+++ b/thread/tests/integration/AndroidTest.xml
@@ -0,0 +1,46 @@
+<?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>
+
+    <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..e8ef346
--- /dev/null
+++ b/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java
@@ -0,0 +1,666 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.Manifest.permission.NETWORK_SETTINGS;
+import static android.net.thread.ThreadNetworkManager.PERMISSION_THREAD_NETWORK_PRIVILEGED;
+import static android.net.thread.utils.IntegrationTestUtils.JOIN_TIMEOUT;
+import static android.net.thread.utils.IntegrationTestUtils.RESTART_JOIN_TIMEOUT;
+import static android.net.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.isSimulatedThreadRadioSupported;
+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.DeviceInfoUtils.isKernelVersionAtLeast;
+import static com.android.testutils.TestNetworkTrackerKt.initTestNetwork;
+import static com.android.testutils.TestPermissionUtil.runAsShell;
+
+import static com.google.common.io.BaseEncoding.base16;
+import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assume.assumeNotNull;
+import static org.junit.Assume.assumeTrue;
+
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+
+import android.content.Context;
+import android.net.InetAddresses;
+import android.net.LinkProperties;
+import android.net.MacAddress;
+import android.net.thread.utils.FullThreadDevice;
+import android.net.thread.utils.InfraNetworkDevice;
+import android.os.Handler;
+import android.os.HandlerThread;
+
+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.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.concurrent.CompletableFuture;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Predicate;
+
+/** Integration test cases for Thread Border Routing feature. */
+@RunWith(AndroidJUnit4.class)
+@LargeTest
+public class BorderRoutingTest {
+    private static final String TAG = BorderRoutingTest.class.getSimpleName();
+    private final Context mContext = ApplicationProvider.getApplicationContext();
+    private ThreadNetworkController mController;
+    private HandlerThread mHandlerThread;
+    private Handler mHandler;
+    private TestNetworkTracker mInfraNetworkTracker;
+    private List<FullThreadDevice> mFtds;
+    private TapPacketReader mInfraNetworkReader;
+    private InfraNetworkDevice mInfraDevice;
+
+    private static final int NUM_FTD = 2;
+    private static final String KERNEL_VERSION_MULTICAST_ROUTING_SUPPORTED = "5.15.0";
+    private static final Inet6Address GROUP_ADDR_SCOPE_5 =
+            (Inet6Address) InetAddresses.parseNumericAddress("ff05::1234");
+    private static final Inet6Address GROUP_ADDR_SCOPE_4 =
+            (Inet6Address) InetAddresses.parseNumericAddress("ff04::1234");
+    private static final Inet6Address GROUP_ADDR_SCOPE_3 =
+            (Inet6Address) InetAddresses.parseNumericAddress("ff03::1234");
+
+    // A valid Thread Active Operational Dataset generated from OpenThread CLI "dataset init new".
+    private static final byte[] DEFAULT_DATASET_TLVS =
+            base16().decode(
+                            "0E080000000000010000000300001335060004001FFFE002"
+                                    + "08ACC214689BC40BDF0708FD64DB1225F47E0B0510F26B31"
+                                    + "53760F519A63BAFDDFFC80D2AF030F4F70656E5468726561"
+                                    + "642D643961300102D9A00410A245479C836D551B9CA557F7"
+                                    + "B9D351B40C0402A0FFF8");
+    private static final ActiveOperationalDataset DEFAULT_DATASET =
+            ActiveOperationalDataset.fromThreadTlvs(DEFAULT_DATASET_TLVS);
+
+    @Before
+    public void setUp() throws Exception {
+        assumeTrue(isSimulatedThreadRadioSupported());
+        final ThreadNetworkManager manager = mContext.getSystemService(ThreadNetworkManager.class);
+        if (manager != null) {
+            mController = manager.getAllThreadNetworkControllers().get(0);
+        }
+
+        // Run the tests on only devices where the Thread feature is available
+        assumeNotNull(mController);
+
+        mHandlerThread = new HandlerThread(getClass().getSimpleName());
+        mHandlerThread.start();
+        mHandler = new Handler(mHandlerThread.getLooper());
+        mFtds = new ArrayList<>();
+
+        setUpInfraNetwork();
+
+        // BR forms a network.
+        startBrLeader();
+
+        // Creates a infra network device.
+        mInfraNetworkReader = newPacketReader(mInfraNetworkTracker.getTestIface(), mHandler);
+        startInfraDevice();
+
+        // Create Ftds
+        for (int i = 0; i < NUM_FTD; ++i) {
+            mFtds.add(new FullThreadDevice(15 + i /* node ID */));
+        }
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        if (mController == null) {
+            return;
+        }
+
+        runAsShell(
+                PERMISSION_THREAD_NETWORK_PRIVILEGED,
+                NETWORK_SETTINGS,
+                () -> {
+                    CountDownLatch latch = new CountDownLatch(2);
+                    mController.setTestNetworkAsUpstream(
+                            null, directExecutor(), v -> latch.countDown());
+                    mController.leave(directExecutor(), v -> latch.countDown());
+                    latch.await(10, TimeUnit.SECONDS);
+                });
+        tearDownInfraNetwork();
+
+        mHandlerThread.quitSafely();
+        mHandlerThread.join();
+
+        for (var ftd : mFtds) {
+            ftd.destroy();
+        }
+        mFtds.clear();
+    }
+
+    @Test
+    public void unicastRouting_infraDevicePingTheadDeviceOmr_replyReceived() throws Exception {
+        /*
+         * <pre>
+         * Topology:
+         *                 infra network                       Thread
+         * infra device -------------------- Border Router -------------- Full Thread device
+         *                                   (Cuttlefish)
+         * </pre>
+         */
+
+        // Let ftd join the network.
+        FullThreadDevice ftd = mFtds.get(0);
+        startFtdChild(ftd);
+
+        // Infra device sends an echo request to FTD's OMR.
+        mInfraDevice.sendEchoRequest(ftd.getOmrAddress());
+
+        // Infra device receives an echo reply sent by FTD.
+        assertNotNull(pollForPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, null /* srcAddress */));
+    }
+
+    @Test
+    public void unicastRouting_borderRouterSendsUdpToThreadDevice_datagramReceived()
+            throws Exception {
+        assumeTrue(isSimulatedThreadRadioSupported());
+
+        /*
+         * <pre>
+         * Topology:
+         *                   Thread
+         * Border Router -------------- Full Thread device
+         *  (Cuttlefish)
+         * </pre>
+         */
+
+        // BR forms a network.
+        CompletableFuture<Void> joinFuture = new CompletableFuture<>();
+        runAsShell(
+                PERMISSION_THREAD_NETWORK_PRIVILEGED,
+                () -> mController.join(DEFAULT_DATASET, directExecutor(), joinFuture::complete));
+        joinFuture.get(RESTART_JOIN_TIMEOUT.toMillis(), MILLISECONDS);
+
+        // Creates a Full Thread Device (FTD) and lets it join the network.
+        FullThreadDevice ftd = mFtds.get(0);
+        startFtdChild(ftd);
+        Inet6Address ftdOmr = ftd.getOmrAddress();
+        Inet6Address ftdMlEid = ftd.getMlEid();
+        assertNotNull(ftdMlEid);
+
+        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 multicastRouting_ftdSubscribedMulticastAddress_infraLinkJoinsMulticastGroup()
+            throws Exception {
+        assumeTrue(isKernelVersionAtLeast(KERNEL_VERSION_MULTICAST_ROUTING_SUPPORTED));
+        /*
+         * <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
+    public void
+            multicastRouting_ftdSubscribedScope3MulticastAddress_infraLinkNotJoinMulticastGroup()
+                    throws Exception {
+        assumeTrue(isKernelVersionAtLeast(KERNEL_VERSION_MULTICAST_ROUTING_SUPPORTED));
+        /*
+         * <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
+    public void multicastRouting_ftdSubscribedMulticastAddress_canPingfromInfraLink()
+            throws Exception {
+        assumeTrue(isKernelVersionAtLeast(KERNEL_VERSION_MULTICAST_ROUTING_SUPPORTED));
+        /*
+         * <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
+    public void multicastRouting_inboundForwarding_afterBrRejoinFtdRepliesSubscribedAddress()
+            throws Exception {
+        assumeTrue(isKernelVersionAtLeast(KERNEL_VERSION_MULTICAST_ROUTING_SUPPORTED));
+
+        // TODO (b/327311034): Testing bbr state switch from primary mode to secondary mode and back
+        // to primary mode requires an additional BR in the Thread network. This is not currently
+        // supported, to be implemented when possible.
+    }
+
+    @Test
+    public void multicastRouting_ftdSubscribedScope3MulticastAddress_cannotPingfromInfraLink()
+            throws Exception {
+        assumeTrue(isKernelVersionAtLeast(KERNEL_VERSION_MULTICAST_ROUTING_SUPPORTED));
+        /*
+         * <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
+    public void multicastRouting_ftdNotSubscribedMulticastAddress_cannotPingFromInfraDevice()
+            throws Exception {
+        assumeTrue(isKernelVersionAtLeast(KERNEL_VERSION_MULTICAST_ROUTING_SUPPORTED));
+        /*
+         * <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
+    public void multicastRouting_multipleFtdsSubscribedDifferentAddresses_canPingFromInfraDevice()
+            throws Exception {
+        assumeTrue(isKernelVersionAtLeast(KERNEL_VERSION_MULTICAST_ROUTING_SUPPORTED));
+        /*
+         * <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
+    public void multicastRouting_multipleFtdsSubscribedSameAddress_canPingFromInfraDevice()
+            throws Exception {
+        assumeTrue(isKernelVersionAtLeast(KERNEL_VERSION_MULTICAST_ROUTING_SUPPORTED));
+        /*
+         * <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
+    public void multicastRouting_outboundForwarding_scopeLargerThan3IsForwarded() throws Exception {
+        assumeTrue(isKernelVersionAtLeast(KERNEL_VERSION_MULTICAST_ROUTING_SUPPORTED));
+        /*
+         * <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
+    public void multicastRouting_outboundForwarding_scopeSmallerThan4IsNotForwarded()
+            throws Exception {
+        assumeTrue(isKernelVersionAtLeast(KERNEL_VERSION_MULTICAST_ROUTING_SUPPORTED));
+        /*
+         * <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
+    public void multicastRouting_outboundForwarding_llaToScope4IsNotForwarded() throws Exception {
+        assumeTrue(isKernelVersionAtLeast(KERNEL_VERSION_MULTICAST_ROUTING_SUPPORTED));
+        /*
+         * <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, 100 /* size */, 1 /* count */);
+
+        assertNull(
+                pollForPacketOnInfraNetwork(ICMPV6_ECHO_REQUEST_TYPE, ftdLla, GROUP_ADDR_SCOPE_4));
+    }
+
+    @Test
+    public void multicastRouting_outboundForwarding_mlaToScope4IsNotForwarded() throws Exception {
+        assumeTrue(isKernelVersionAtLeast(KERNEL_VERSION_MULTICAST_ROUTING_SUPPORTED));
+        /*
+         * <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, 100 /* size */, 1 /* count */);
+
+            assertNull(
+                    pollForPacketOnInfraNetwork(
+                            ICMPV6_ECHO_REQUEST_TYPE, ftdMla, GROUP_ADDR_SCOPE_4));
+        }
+    }
+
+    @Test
+    public void multicastRouting_infraNetworkSwitch_ftdRepliesToSubscribedAddress()
+            throws Exception {
+        assumeTrue(isKernelVersionAtLeast(KERNEL_VERSION_MULTICAST_ROUTING_SUPPORTED));
+        /*
+         * <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);
+        startInfraDevice();
+
+        mInfraDevice.sendEchoRequest(GROUP_ADDR_SCOPE_5);
+
+        assertNotNull(pollForPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftdOmr));
+    }
+
+    @Test
+    public void multicastRouting_infraNetworkSwitch_outboundPacketIsForwarded() throws Exception {
+        assumeTrue(isKernelVersionAtLeast(KERNEL_VERSION_MULTICAST_ROUTING_SUPPORTED));
+        /*
+         * <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);
+        startInfraDevice();
+
+        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));
+    }
+
+    private void setUpInfraNetwork() {
+        mInfraNetworkTracker =
+                runAsShell(
+                        MANAGE_TEST_NETWORKS,
+                        () ->
+                                initTestNetwork(
+                                        mContext, new LinkProperties(), 5000 /* timeoutMs */));
+        runAsShell(
+                PERMISSION_THREAD_NETWORK_PRIVILEGED,
+                NETWORK_SETTINGS,
+                () -> {
+                    CompletableFuture<Void> future = new CompletableFuture<>();
+                    mController.setTestNetworkAsUpstream(
+                            mInfraNetworkTracker.getTestIface().getInterfaceName(),
+                            directExecutor(),
+                            future::complete);
+                    future.get(5, TimeUnit.SECONDS);
+                });
+    }
+
+    private void tearDownInfraNetwork() {
+        runAsShell(MANAGE_TEST_NETWORKS, () -> mInfraNetworkTracker.teardown());
+    }
+
+    private void startBrLeader() throws Exception {
+        CompletableFuture<Void> joinFuture = new CompletableFuture<>();
+        runAsShell(
+                PERMISSION_THREAD_NETWORK_PRIVILEGED,
+                () -> mController.join(DEFAULT_DATASET, directExecutor(), joinFuture::complete));
+        joinFuture.get(RESTART_JOIN_TIMEOUT.toSeconds(), TimeUnit.SECONDS);
+    }
+
+    private void startFtdChild(FullThreadDevice ftd) throws Exception {
+        ftd.factoryReset();
+        ftd.joinNetwork(DEFAULT_DATASET);
+        ftd.waitForStateAnyOf(List.of("router", "child"), JOIN_TIMEOUT);
+        waitFor(() -> ftd.getOmrAddress() != null, Duration.ofSeconds(60));
+        Inet6Address ftdOmr = ftd.getOmrAddress();
+        assertNotNull(ftdOmr);
+    }
+
+    private void startInfraDevice() throws Exception {
+        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/ThreadIntegrationTest.java b/thread/tests/integration/src/android/net/thread/ThreadIntegrationTest.java
new file mode 100644
index 0000000..70897f0
--- /dev/null
+++ b/thread/tests/integration/src/android/net/thread/ThreadIntegrationTest.java
@@ -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 android.net.thread;
+
+import static android.Manifest.permission.NETWORK_SETTINGS;
+import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_DETACHED;
+import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_LEADER;
+import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_STOPPED;
+import static android.net.thread.ThreadNetworkManager.PERMISSION_THREAD_NETWORK_PRIVILEGED;
+import static android.net.thread.utils.IntegrationTestUtils.CALLBACK_TIMEOUT;
+import static android.net.thread.utils.IntegrationTestUtils.LEAVE_TIMEOUT;
+import static android.net.thread.utils.IntegrationTestUtils.RESTART_JOIN_TIMEOUT;
+import static android.net.thread.utils.IntegrationTestUtils.waitForStateAnyOf;
+
+import static com.android.compatibility.common.util.SystemUtil.runShellCommand;
+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 com.google.common.util.concurrent.MoreExecutors.directExecutor;
+
+import static org.junit.Assume.assumeNotNull;
+
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.net.thread.ThreadNetworkController.StateCallback;
+import android.net.thread.utils.OtDaemonController;
+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.Test;
+import org.junit.runner.RunWith;
+
+import java.net.Inet6Address;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+
+/** Tests for E2E Android Thread integration with ot-daemon, ConnectivityService, etc.. */
+@LargeTest
+@RunWith(AndroidJUnit4.class)
+public class ThreadIntegrationTest {
+    private final Context mContext = ApplicationProvider.getApplicationContext();
+    private ThreadNetworkController mController;
+    private OtDaemonController mOtCtl;
+
+    // A valid Thread Active Operational Dataset generated from OpenThread CLI "dataset init new".
+    private static final byte[] DEFAULT_DATASET_TLVS =
+            base16().decode(
+                            "0E080000000000010000000300001335060004001FFFE002"
+                                    + "08ACC214689BC40BDF0708FD64DB1225F47E0B0510F26B31"
+                                    + "53760F519A63BAFDDFFC80D2AF030F4F70656E5468726561"
+                                    + "642D643961300102D9A00410A245479C836D551B9CA557F7"
+                                    + "B9D351B40C0402A0FFF8");
+    private static final ActiveOperationalDataset DEFAULT_DATASET =
+            ActiveOperationalDataset.fromThreadTlvs(DEFAULT_DATASET_TLVS);
+
+    @Before
+    public void setUp() throws Exception {
+        final ThreadNetworkManager manager = mContext.getSystemService(ThreadNetworkManager.class);
+        if (manager != null) {
+            mController = manager.getAllThreadNetworkControllers().get(0);
+        }
+
+        // Run the tests on only devices where the Thread feature is available
+        assumeNotNull(mController);
+
+        mOtCtl = new OtDaemonController();
+        leaveAndWait(mController);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        if (mController == null) {
+            return;
+        }
+
+        setTestUpStreamNetworkAndWait(mController, null);
+        leaveAndWait(mController);
+    }
+
+    @Test
+    public void otDaemonRestart_notJoinedAndStopped_deviceRoleIsStopped() throws Exception {
+        leaveAndWait(mController);
+
+        runShellCommand("stop ot-daemon");
+        // TODO(b/323331973): the sleep is needed to workaround the race conditions
+        SystemClock.sleep(200);
+
+        waitForStateAnyOf(mController, List.of(DEVICE_ROLE_STOPPED), CALLBACK_TIMEOUT);
+    }
+
+    @Test
+    public void otDaemonRestart_JoinedNetworkAndStopped_autoRejoined() throws Exception {
+        joinAndWait(mController, DEFAULT_DATASET);
+
+        runShellCommand("stop ot-daemon");
+
+        waitForStateAnyOf(mController, List.of(DEVICE_ROLE_DETACHED), CALLBACK_TIMEOUT);
+        waitForStateAnyOf(mController, List.of(DEVICE_ROLE_LEADER), RESTART_JOIN_TIMEOUT);
+    }
+
+    @Test
+    public void otDaemonFactoryReset_deviceRoleIsStopped() throws Exception {
+        joinAndWait(mController, DEFAULT_DATASET);
+
+        mOtCtl.factoryReset();
+
+        assertThat(getDeviceRole(mController)).isEqualTo(DEVICE_ROLE_STOPPED);
+    }
+
+    @Test
+    public void otDaemonFactoryReset_addressesRemoved() throws Exception {
+        joinAndWait(mController, DEFAULT_DATASET);
+
+        mOtCtl.factoryReset();
+        String ifconfig = runShellCommand("ifconfig thread-wpan");
+
+        assertThat(ifconfig).doesNotContain("inet6 addr");
+    }
+
+    @Test
+    public void tunInterface_joinedNetwork_otAddressesAddedToTunInterface() throws Exception {
+        joinAndWait(mController, DEFAULT_DATASET);
+
+        String ifconfig = runShellCommand("ifconfig thread-wpan");
+        List<Inet6Address> otAddresses = mOtCtl.getAddresses();
+        assertThat(otAddresses).isNotEmpty();
+        for (Inet6Address otAddress : otAddresses) {
+            assertThat(ifconfig).contains(otAddress.getHostAddress());
+        }
+    }
+
+    // TODO (b/323300829): add more tests for integration with linux platform and
+    // ConnectivityService
+
+    private static int getDeviceRole(ThreadNetworkController controller) throws Exception {
+        CompletableFuture<Integer> future = new CompletableFuture<>();
+        StateCallback callback = future::complete;
+        controller.registerStateCallback(directExecutor(), callback);
+        try {
+            return future.get(CALLBACK_TIMEOUT.toMillis(), MILLISECONDS);
+        } finally {
+            controller.unregisterStateCallback(callback);
+        }
+    }
+
+    private static void joinAndWait(
+            ThreadNetworkController controller, ActiveOperationalDataset activeDataset)
+            throws Exception {
+        runAsShell(
+                PERMISSION_THREAD_NETWORK_PRIVILEGED,
+                () -> controller.join(activeDataset, directExecutor(), result -> {}));
+        waitForStateAnyOf(controller, List.of(DEVICE_ROLE_LEADER), RESTART_JOIN_TIMEOUT);
+    }
+
+    private static void leaveAndWait(ThreadNetworkController controller) throws Exception {
+        CompletableFuture<Void> future = new CompletableFuture<>();
+        runAsShell(
+                PERMISSION_THREAD_NETWORK_PRIVILEGED,
+                () -> controller.leave(directExecutor(), future::complete));
+        future.get(LEAVE_TIMEOUT.toMillis(), MILLISECONDS);
+    }
+
+    private static void setTestUpStreamNetworkAndWait(
+            ThreadNetworkController controller, @Nullable String networkInterfaceName)
+            throws Exception {
+        CompletableFuture<Void> future = new CompletableFuture<>();
+        runAsShell(
+                PERMISSION_THREAD_NETWORK_PRIVILEGED,
+                NETWORK_SETTINGS,
+                () -> {
+                    controller.setTestNetworkAsUpstream(
+                            networkInterfaceName, directExecutor(), future::complete);
+                });
+        future.get(CALLBACK_TIMEOUT.toMillis(), MILLISECONDS);
+    }
+}
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..6cb1675
--- /dev/null
+++ b/thread/tests/integration/src/android/net/thread/utils/FullThreadDevice.java
@@ -0,0 +1,275 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.waitFor;
+
+import static com.google.common.io.BaseEncoding.base16;
+
+import static org.junit.Assert.fail;
+
+import android.net.InetAddresses;
+import android.net.IpPrefix;
+import android.net.thread.ActiveOperationalDataset;
+
+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.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
+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 final Process mProcess;
+    private final BufferedReader mReader;
+    private final BufferedWriter mWriter;
+
+    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 " + nodeId);
+        } catch (IOException e) {
+            throw new IllegalStateException("Failed to start ot-cli-ftd (id=" + nodeId + ")", e);
+        }
+        mReader = new BufferedReader(new InputStreamReader(mProcess.getInputStream()));
+        mWriter = new BufferedWriter(new OutputStreamWriter(mProcess.getOutputStream()));
+        mActiveOperationalDataset = null;
+    }
+
+    public void destroy() {
+        mProcess.destroy();
+    }
+
+    /**
+     * 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(String.format("udp bind %s %d", address.getHostAddress(), port));
+    }
+
+    /** Returns the message received on the UDP socket. */
+    public String udpReceive() throws IOException {
+        Pattern pattern =
+                Pattern.compile("> (\\d+) bytes from ([\\da-f:]+) (\\d+) ([\\x00-\\x7F]+)");
+        Matcher matcher = pattern.matcher(mReader.readLine());
+        matcher.matches();
+
+        return matcher.group(4);
+    }
+
+    /** 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, int size, int count) {
+        String cmd =
+                "ping"
+                        + ((source == null) ? "" : (" -I " + source.getHostAddress()))
+                        + " "
+                        + address.getHostAddress()
+                        + " "
+                        + size
+                        + " "
+                        + count;
+        executeCommand(cmd);
+    }
+
+    public void ping(Inet6Address address) {
+        ping(address, null, 100 /* size */, 1 /* count */);
+    }
+
+    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 List<String> readUntilDone() throws IOException {
+        ArrayList<String> result = new ArrayList<>();
+        String line;
+        while ((line = mReader.readLine()) != null) {
+            if (line.equals("Done")) {
+                break;
+            }
+            if (line.startsWith("Error:")) {
+                fail("ot-cli-ftd reported an error: " + line);
+            }
+            if (!line.startsWith("> ")) {
+                result.add(line);
+            }
+        }
+        return result;
+    }
+}
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..74251a6
--- /dev/null
+++ b/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.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 android.net.thread.utils;
+
+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 android.net.TestNetworkInterface;
+import android.net.thread.ThreadNetworkController;
+import android.os.Handler;
+import android.os.SystemClock;
+import android.os.SystemProperties;
+
+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.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);
+
+    private IntegrationTestUtils() {}
+
+    /** Returns whether the device supports simulated Thread radio. */
+    public static boolean isSimulatedThreadRadioSupported() {
+        // The integration test uses SIMULATION Thread radio so that it only supports CuttleFish.
+        return SystemProperties.get("ro.product.model").startsWith("Cuttlefish");
+    }
+
+    /**
+     * Waits for the given {@link Supplier} to be true until given timeout.
+     *
+     * @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 = 1000;
+        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;
+    }
+}
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..4a06fe8
--- /dev/null
+++ b/thread/tests/integration/src/android/net/thread/utils/OtDaemonController.java
@@ -0,0 +1,68 @@
+/*
+ * 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.net.InetAddresses;
+import android.os.SystemClock;
+
+import com.android.compatibility.common.util.SystemUtil;
+
+import java.net.Inet6Address;
+import java.util.Arrays;
+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() {
+        String output = executeCommand("ipaddr");
+        return Arrays.asList(output.split("\n")).stream()
+                .map(String::trim)
+                .filter(str -> !str.equals("Done"))
+                .map(addr -> InetAddresses.parseNumericAddress(addr))
+                .map(inetAddr -> (Inet6Address) inetAddr)
+                .toList();
+    }
+
+    public String executeCommand(String cmd) {
+        return SystemUtil.runShellCommand(OT_CTL + " " + cmd);
+    }
+}
diff --git a/thread/tests/unit/Android.bp b/thread/tests/unit/Android.bp
index 8092693..3365cd0 100644
--- a/thread/tests/unit/Android.bp
+++ b/thread/tests/unit/Android.bp
@@ -15,6 +15,7 @@
 //
 
 package {
+    default_team: "trendy_team_fwk_thread_network",
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
@@ -29,22 +30,38 @@
     ],
     test_suites: [
         "general-tests",
+        "mts-tethering",
     ],
     static_libs: [
-        "androidx.test.ext.junit",
-        "compatibility-device-util-axt",
+        "frameworks-base-testutils",
         "framework-connectivity-pre-jarjar",
         "framework-connectivity-t-pre-jarjar",
+        "framework-location.stubs.module_lib",
         "guava",
         "guava-android-testlib",
-        "mockito-target-minus-junit4",
+        "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.
diff --git a/thread/tests/unit/AndroidTest.xml b/thread/tests/unit/AndroidTest.xml
index 597c6a8..d16e423 100644
--- a/thread/tests/unit/AndroidTest.xml
+++ b/thread/tests/unit/AndroidTest.xml
@@ -19,6 +19,18 @@
     <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>
+
     <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
         <option name="test-file-name" value="ThreadNetworkUnitTests.apk" />
         <option name="check-min-sdk" value="true" />
@@ -30,5 +42,8 @@
         <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
index 7284968..e92dcb9 100644
--- a/thread/tests/unit/src/android/net/thread/ActiveOperationalDatasetTest.java
+++ b/thread/tests/unit/src/android/net/thread/ActiveOperationalDatasetTest.java
@@ -33,12 +33,8 @@
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
-import java.security.SecureRandom;
-import java.util.Random;
-
 /** Unit tests for {@link ActiveOperationalDataset}. */
 @SmallTest
 @RunWith(AndroidJUnit4.class)
@@ -62,9 +58,6 @@
                                     + "642D643961300102D9A00410A245479C836D551B9CA557F7"
                                     + "B9D351B40C0402A0FFF8");
 
-    @Mock private Random mockRandom;
-    @Mock private SecureRandom mockSecureRandom;
-
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
diff --git a/thread/tests/unit/src/android/net/thread/ThreadNetworkControllerTest.java b/thread/tests/unit/src/android/net/thread/ThreadNetworkControllerTest.java
index 2f120b2..75eb043 100644
--- a/thread/tests/unit/src/android/net/thread/ThreadNetworkControllerTest.java
+++ b/thread/tests/unit/src/android/net/thread/ThreadNetworkControllerTest.java
@@ -28,11 +28,6 @@
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.Mockito.doAnswer;
 
-import android.net.thread.IActiveOperationalDatasetReceiver;
-import android.net.thread.IOperationReceiver;
-import android.net.thread.IOperationalDatasetCallback;
-import android.net.thread.IStateCallback;
-import android.net.thread.IThreadNetworkController;
 import android.net.thread.ThreadNetworkController.OperationalDatasetCallback;
 import android.net.thread.ThreadNetworkController.StateCallback;
 import android.os.Binder;
@@ -111,6 +106,11 @@
         return (IOperationReceiver) invocation.getArguments()[1];
     }
 
+    private static IOperationReceiver getSetTestNetworkAsUpstreamReceiver(
+            InvocationOnMock invocation) {
+        return (IOperationReceiver) invocation.getArguments()[1];
+    }
+
     private static IActiveOperationalDatasetReceiver getCreateDatasetReceiver(
             InvocationOnMock invocation) {
         return (IActiveOperationalDatasetReceiver) invocation.getArguments()[1];
@@ -359,4 +359,27 @@
         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..f62b437
--- /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(13, "13"));
+    }
+}
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..8aea0a3
--- /dev/null
+++ b/thread/tests/unit/src/com/android/server/thread/NsdPublisherTest.java
@@ -0,0 +1,367 @@
+/*
+ * 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.FAILURE_INTERNAL_ERROR;
+import static android.net.nsd.NsdManager.PROTOCOL_DNS_SD;
+
+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.doThrow;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.net.nsd.NsdManager;
+import android.net.nsd.NsdServiceInfo;
+import android.os.Handler;
+import android.os.test.TestLooper;
+
+import com.android.server.thread.openthread.DnsTxtAttribute;
+import com.android.server.thread.openthread.INsdStatusReceiver;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.Executor;
+
+/** Unit tests for {@link NsdPublisher}. */
+public final class NsdPublisherTest {
+    @Mock private NsdManager mMockNsdManager;
+
+    @Mock private INsdStatusReceiver mRegistrationReceiver;
+    @Mock private INsdStatusReceiver mUnregistrationReceiver;
+
+    private TestLooper mTestLooper;
+    private NsdPublisher mNsdPublisher;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+    }
+
+    @Test
+    public void registerService_nsdManagerSucceeds_serviceRegistrationSucceeds() throws Exception {
+        prepareTest();
+
+        DnsTxtAttribute txt1 = makeTxtAttribute("key1", List.of(0x01, 0x02));
+        DnsTxtAttribute txt2 = makeTxtAttribute("key2", List.of(0x03));
+
+        mNsdPublisher.registerService(
+                null,
+                "MyService",
+                "_test._tcp",
+                List.of("_subtype1", "_subtype2"),
+                12345,
+                List.of(txt1, txt2),
+                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("key1"))
+                .isEqualTo(new byte[] {(byte) 0x01, (byte) 0x02});
+        assertThat(actualServiceInfo.getAttributes().get("key2"))
+                .isEqualTo(new byte[] {(byte) 0x03});
+
+        verify(mRegistrationReceiver, times(1)).onSuccess();
+    }
+
+    @Test
+    public void registerService_nsdManagerFails_serviceRegistrationFails() throws Exception {
+        prepareTest();
+
+        DnsTxtAttribute txt1 = makeTxtAttribute("key1", List.of(0x01, 0x02));
+        DnsTxtAttribute txt2 = makeTxtAttribute("key2", List.of(0x03));
+
+        mNsdPublisher.registerService(
+                null,
+                "MyService",
+                "_test._tcp",
+                List.of("_subtype1", "_subtype2"),
+                12345,
+                List.of(txt1, txt2),
+                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("key1"))
+                .isEqualTo(new byte[] {(byte) 0x01, (byte) 0x02});
+        assertThat(actualServiceInfo.getAttributes().get("key2"))
+                .isEqualTo(new byte[] {(byte) 0x03});
+
+        verify(mRegistrationReceiver, times(1)).onError(FAILURE_INTERNAL_ERROR);
+    }
+
+    @Test
+    public void registerService_nsdManagerThrows_serviceRegistrationFails() throws Exception {
+        prepareTest();
+
+        DnsTxtAttribute txt1 = makeTxtAttribute("key1", List.of(0x01, 0x02));
+        DnsTxtAttribute txt2 = makeTxtAttribute("key2", List.of(0x03));
+
+        doThrow(new IllegalArgumentException("NsdManager fails"))
+                .when(mMockNsdManager)
+                .registerService(any(), anyInt(), any(Executor.class), any());
+
+        mNsdPublisher.registerService(
+                null,
+                "MyService",
+                "_test._tcp",
+                List.of("_subtype1", "_subtype2"),
+                12345,
+                List.of(txt1, txt2),
+                mRegistrationReceiver,
+                16 /* listenerId */);
+        mTestLooper.dispatchAll();
+
+        verify(mRegistrationReceiver, times(1)).onError(FAILURE_INTERNAL_ERROR);
+    }
+
+    @Test
+    public void unregisterService_nsdManagerSucceeds_serviceUnregistrationSucceeds()
+            throws Exception {
+        prepareTest();
+
+        DnsTxtAttribute txt1 = makeTxtAttribute("key1", List.of(0x01, 0x02));
+        DnsTxtAttribute txt2 = makeTxtAttribute("key2", List.of(0x03));
+
+        mNsdPublisher.registerService(
+                null,
+                "MyService",
+                "_test._tcp",
+                List.of("_subtype1", "_subtype2"),
+                12345,
+                List.of(txt1, txt2),
+                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();
+
+        DnsTxtAttribute txt1 = makeTxtAttribute("key1", List.of(0x01, 0x02));
+        DnsTxtAttribute txt2 = makeTxtAttribute("key2", List.of(0x03));
+
+        mNsdPublisher.registerService(
+                null,
+                "MyService",
+                "_test._tcp",
+                List.of("_subtype1", "_subtype2"),
+                12345,
+                List.of(txt1, txt2),
+                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 onOtDaemonDied_unregisterAll() {
+        prepareTest();
+
+        DnsTxtAttribute txt1 = makeTxtAttribute("key1", List.of(0x01, 0x02));
+        DnsTxtAttribute txt2 = makeTxtAttribute("key2", List.of(0x03));
+
+        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(txt1, txt2),
+                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.onOtDaemonDied();
+        mTestLooper.dispatchAll();
+
+        verify(mMockNsdManager, times(1)).unregisterService(actualListener1);
+        verify(mMockNsdManager, times(1)).unregisterService(actualListener2);
+    }
+
+    private static DnsTxtAttribute makeTxtAttribute(String name, List<Integer> value) {
+        DnsTxtAttribute txtAttribute = new DnsTxtAttribute();
+
+        txtAttribute.name = name;
+        txtAttribute.value = new byte[value.size()];
+
+        for (int i = 0; i < value.size(); ++i) {
+            txtAttribute.value[i] = value.get(i).byteValue();
+        }
+
+        return txtAttribute;
+    }
+
+    // @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, handler);
+    }
+}
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..60a5f2b
--- /dev/null
+++ b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.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 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.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_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.eq;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doNothing;
+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 android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+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.ThreadNetworkException;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.ParcelFileDescriptor;
+import android.os.RemoteException;
+import android.os.UserManager;
+import android.os.test.TestLooper;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.server.thread.openthread.testing.FakeOtDaemon;
+
+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.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.atomic.AtomicReference;
+
+/** Unit tests for {@link ThreadNetworkControllerService}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+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
+    private static final int DEFAULT_PREFERRED_CHANNEL_MASK = 0x00000800; // channel 11
+    private static final int DEFAULT_SELECTED_CHANNEL = 11;
+    private static final byte[] DEFAULT_SUPPORTED_CHANNEL_MASK_ARRAY = base16().decode("001FFFE0");
+
+    @Mock private ConnectivityManager mMockConnectivityManager;
+    @Mock private NetworkAgent mMockNetworkAgent;
+    @Mock private TunInterfaceController mMockTunIfController;
+    @Mock private ParcelFileDescriptor mMockTunFd;
+    @Mock private InfraInterfaceController mMockInfraIfController;
+    @Mock private ThreadPersistentSettings mMockPersistentSettings;
+    @Mock private NsdPublisher mMockNsdPublisher;
+    @Mock private UserManager mMockUserManager;
+    @Mock private IBinder mIBinder;
+    private Context mContext;
+    private TestLooper mTestLooper;
+    private FakeOtDaemon mFakeOtDaemon;
+    private ThreadNetworkControllerService mService;
+    @Captor private ArgumentCaptor<ActiveOperationalDataset> mActiveDatasetCaptor;
+
+    @Before
+    public void setUp() {
+        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(mMockPersistentSettings.get(any())).thenReturn(true);
+        when(mMockUserManager.hasUserRestriction(eq(DISALLOW_THREAD_NETWORK))).thenReturn(false);
+
+        mService =
+                new ThreadNetworkControllerService(
+                        mContext,
+                        handler,
+                        networkProvider,
+                        () -> mFakeOtDaemon,
+                        mMockConnectivityManager,
+                        mMockTunIfController,
+                        mMockInfraIfController,
+                        mMockPersistentSettings,
+                        mMockNsdPublisher,
+                        mMockUserManager);
+        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 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_threadIsDisabled() {
+        when(mMockUserManager.hasUserRestriction(eq(DISALLOW_THREAD_NETWORK))).thenReturn(true);
+
+        mService.initialize();
+        mTestLooper.dispatchAll();
+
+        assertThat(mFakeOtDaemon.getEnabledState()).isEqualTo(STATE_DISABLED);
+    }
+
+    @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() {
+        AtomicReference<BroadcastReceiver> receiverRef = new AtomicReference<>();
+        when(mMockUserManager.hasUserRestriction(eq(DISALLOW_THREAD_NETWORK))).thenReturn(false);
+        doAnswer(
+                        invocation -> {
+                            receiverRef.set((BroadcastReceiver) invocation.getArguments()[0]);
+                            return null;
+                        })
+                .when(mContext)
+                .registerReceiver(any(BroadcastReceiver.class), any(), any(), any());
+        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);
+        verify(mMockPersistentSettings, never())
+                .put(eq(ThreadPersistentSettings.THREAD_ENABLED.key), eq(false));
+    }
+
+    @Test
+    public void userRestriction_userBecomesNotRestricted_stateIsEnabledButNotPersisted() {
+        AtomicReference<BroadcastReceiver> receiverRef = new AtomicReference<>();
+        when(mMockUserManager.hasUserRestriction(eq(DISALLOW_THREAD_NETWORK))).thenReturn(true);
+        doAnswer(
+                        invocation -> {
+                            receiverRef.set((BroadcastReceiver) invocation.getArguments()[0]);
+                            return null;
+                        })
+                .when(mContext)
+                .registerReceiver(any(BroadcastReceiver.class), any(), any(), any());
+        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);
+        verify(mMockPersistentSettings, never())
+                .put(eq(ThreadPersistentSettings.THREAD_ENABLED.key), eq(true));
+    }
+
+    @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 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_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());
+    }
+}
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..5ca6511
--- /dev/null
+++ b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkCountryCodeTest.java
@@ -0,0 +1,469 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.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;
+
+    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);
+    }
+
+    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 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..c7e0eca
--- /dev/null
+++ b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkShellCommandTest.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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 org.mockito.Mockito.any;
+import static org.mockito.Mockito.contains;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.validateMockitoUsage;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.os.Binder;
+import android.os.Process;
+
+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 java.io.FileDescriptor;
+import java.io.PrintWriter;
+
+/** Unit tests for {@link ThreadNetworkShellCommand}. */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class ThreadNetworkShellCommandTest {
+    private static final String TAG = "ThreadNetworkShellCommandTTest";
+    @Mock ThreadNetworkService mThreadNetworkService;
+    @Mock ThreadNetworkCountryCode mThreadNetworkCountryCode;
+    @Mock PrintWriter mErrorWriter;
+    @Mock PrintWriter mOutputWriter;
+
+    ThreadNetworkShellCommand mThreadNetworkShellCommand;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+
+        mThreadNetworkShellCommand = new ThreadNetworkShellCommand(mThreadNetworkCountryCode);
+        mThreadNetworkShellCommand.setPrintWriters(mOutputWriter, mErrorWriter);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        validateMockitoUsage();
+    }
+
+    @Test
+    public void getCountryCode_executeInUnrootedShell_allowed() {
+        BinderUtil.setUid(Process.SHELL_UID);
+        when(mThreadNetworkCountryCode.getCountryCode()).thenReturn("US");
+
+        mThreadNetworkShellCommand.exec(
+                new Binder(),
+                new FileDescriptor(),
+                new FileDescriptor(),
+                new FileDescriptor(),
+                new String[] {"get-country-code"});
+
+        verify(mOutputWriter).println(contains("US"));
+    }
+
+    @Test
+    public void forceSetCountryCodeEnabled_executeInUnrootedShell_notAllowed() {
+        BinderUtil.setUid(Process.SHELL_UID);
+
+        mThreadNetworkShellCommand.exec(
+                new Binder(),
+                new FileDescriptor(),
+                new FileDescriptor(),
+                new FileDescriptor(),
+                new String[] {"force-country-code", "enabled", "US"});
+
+        verify(mThreadNetworkCountryCode, never()).setOverrideCountryCode(eq("US"));
+        verify(mErrorWriter).println(contains("force-country-code"));
+    }
+
+    @Test
+    public void forceSetCountryCodeEnabled_executeInRootedShell_allowed() {
+        BinderUtil.setUid(Process.ROOT_UID);
+
+        mThreadNetworkShellCommand.exec(
+                new Binder(),
+                new FileDescriptor(),
+                new FileDescriptor(),
+                new FileDescriptor(),
+                new String[] {"force-country-code", "enabled", "US"});
+
+        verify(mThreadNetworkCountryCode).setOverrideCountryCode(eq("US"));
+    }
+
+    @Test
+    public void forceSetCountryCodeDisabled_executeInUnrootedShell_notAllowed() {
+        BinderUtil.setUid(Process.SHELL_UID);
+
+        mThreadNetworkShellCommand.exec(
+                new Binder(),
+                new FileDescriptor(),
+                new FileDescriptor(),
+                new FileDescriptor(),
+                new String[] {"force-country-code", "disabled"});
+
+        verify(mThreadNetworkCountryCode, never()).setOverrideCountryCode(any());
+        verify(mErrorWriter).println(contains("force-country-code"));
+    }
+
+    @Test
+    public void forceSetCountryCodeDisabled_executeInRootedShell_allowed() {
+        BinderUtil.setUid(Process.ROOT_UID);
+
+        mThreadNetworkShellCommand.exec(
+                new Binder(),
+                new FileDescriptor(),
+                new FileDescriptor(),
+                new FileDescriptor(),
+                new String[] {"force-country-code", "disabled"});
+
+        verify(mThreadNetworkCountryCode).clearOverrideCountryCode();
+    }
+}
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..49b002a
--- /dev/null
+++ b/thread/tests/unit/src/com/android/server/thread/ThreadPersistentSettingsTest.java
@@ -0,0 +1,151 @@
+/*
+ * 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_ENABLED;
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.validateMockitoUsage;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.res.Resources;
+import android.os.PersistableBundle;
+import android.util.AtomicFile;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+import com.android.connectivity.resources.R;
+import com.android.server.connectivity.ConnectivityResources;
+import java.io.ByteArrayOutputStream;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/** Unit tests for {@link ThreadPersistentSettings}. */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class ThreadPersistentSettingsTest {
+    @Mock private AtomicFile mAtomicFile;
+    @Mock Resources mResources;
+    @Mock ConnectivityResources mConnectivityResources;
+
+    private ThreadPersistentSettings mThreadPersistentSettings;
+
+    @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);
+
+        FileOutputStream fos = mock(FileOutputStream.class);
+        when(mAtomicFile.startWrite()).thenReturn(fos);
+        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);
+        setupAtomicFileMockForRead(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);
+        setupAtomicFileMockForRead(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);
+        setupAtomicFileMockForRead(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();
+        // Confirm that file writes have been triggered.
+        verify(mAtomicFile).startWrite();
+        verify(mAtomicFile).finishWrite(any());
+    }
+
+    @Test
+    public void put_ThreadFeatureEnabledFalse_returnsFalse() throws Exception {
+        mThreadPersistentSettings.put(THREAD_ENABLED.key, false);
+
+        assertThat(mThreadPersistentSettings.get(THREAD_ENABLED)).isFalse();
+        // Confirm that file writes have been triggered.
+        verify(mAtomicFile).startWrite();
+        verify(mAtomicFile).finishWrite(any());
+    }
+
+    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 setupAtomicFileMockForRead(byte[] dataToRead) throws Exception {
+        FileInputStream is = mock(FileInputStream.class);
+        when(mAtomicFile.openRead()).thenReturn(is);
+        when(is.available()).thenReturn(dataToRead.length).thenReturn(0);
+        doAnswer(
+                        invocation -> {
+                            byte[] data = invocation.getArgument(0);
+                            int pos = invocation.getArgument(1);
+                            if (pos == dataToRead.length) return 0; // read complete.
+                            System.arraycopy(dataToRead, 0, data, 0, dataToRead.length);
+                            return dataToRead.length;
+                        })
+                .when(is)
+                .read(any(), anyInt(), anyInt());
+    }
+}
diff --git a/thread/tests/utils/Android.bp b/thread/tests/utils/Android.bp
new file mode 100644
index 0000000..24e9bb9
--- /dev/null
+++ b/thread/tests/utils/Android.bp
@@ -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.
+//
+
+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",
+    ],
+    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..43f177d
--- /dev/null
+++ b/thread/tests/utils/src/android/net/thread/utils/TapTestNetworkTracker.java
@@ -0,0 +1,185 @@
+/*
+ * 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 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();
+    }
+
+    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());
+        final Network network = mAgent.register();
+        mAgent.markConnected();
+
+        PollingCheck.check(
+                "No usable address on interface",
+                TIMEOUT.toMillis(),
+                () -> hasUsableAddress(network, 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/tools/Android.bp b/tools/Android.bp
index 3ce76f6..9216b5b 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,