Merge "netbpfload: enable mainline on API V/35+" into main
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/DnsBpfHelper.cpp b/DnsResolver/DnsBpfHelper.cpp
index 37c46ca..de8bef5 100644
--- a/DnsResolver/DnsBpfHelper.cpp
+++ b/DnsResolver/DnsBpfHelper.cpp
@@ -62,16 +62,24 @@
   if (isBlockedByUidRules(enabledRules.value(), uidRules)) return true;
 
   // For data saver.
-  if (!metered) return false;
+  // DataSaverEnabled map on V+ platforms is the only reliable source of information about the
+  // current data saver status. While ConnectivityService offers two ways to update this map for U
+  // and V+, the U- platform implementation can have delays, potentially leading to inaccurate
+  // results. Conversely, the V+ platform implementation is synchronized with the actual data saver
+  // state, making it a trustworthy source. Since this library primarily serves DNS resolvers,
+  // relying solely on V+ data prevents erroneous blocking of DNS queries.
+  if (android::modules::sdklevel::IsAtLeastV() && metered) {
+    // The background data setting (PENALTY_BOX_MATCH) and unrestricted data usage setting
+    // (HAPPY_BOX_MATCH) for individual apps override the system wide Data Saver setting.
+    if (uidRules & PENALTY_BOX_MATCH) return true;
+    if (uidRules & HAPPY_BOX_MATCH) return false;
 
-  // The background data setting (PENALTY_BOX_MATCH) and unrestricted data usage setting
-  // (HAPPY_BOX_MATCH) for individual apps override the system wide Data Saver setting.
-  if (uidRules & PENALTY_BOX_MATCH) return true;
-  if (uidRules & HAPPY_BOX_MATCH) return false;
+    auto dataSaverSetting = mDataSaverEnabledMap.readValue(DATA_SAVER_ENABLED_KEY);
+    RETURN_IF_RESULT_NOT_OK(dataSaverSetting);
+    return dataSaverSetting.value();
+  }
 
-  auto dataSaverSetting = mDataSaverEnabledMap.readValue(DATA_SAVER_ENABLED_KEY);
-  RETURN_IF_RESULT_NOT_OK(dataSaverSetting);
-  return dataSaverSetting.value();
+  return false;
 }
 
 }  // namespace net
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 dd60be7..fba7d69 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"],
 }
 
@@ -81,7 +82,10 @@
         "framework-tethering.impl",
     ],
     manifest: "AndroidManifestBase.xml",
-    lint: { strict_updatability_linting: true },
+    lint: {
+        strict_updatability_linting: true,
+        error_checks: ["NewApi"],
+    },
 }
 
 // build tethering static library, used to compile both variants of the tethering.
@@ -91,14 +95,18 @@
         "ConnectivityNextEnableDefaults",
         "TetheringAndroidLibraryDefaults",
         "TetheringApiLevel",
-        "TetheringReleaseTargetSdk"
+        "TetheringReleaseTargetSdk",
     ],
     static_libs: [
         "NetworkStackApiCurrentShims",
         "net-utils-device-common-struct",
     ],
     apex_available: ["com.android.tethering"],
-    lint: { strict_updatability_linting: true },
+    lint: {
+        strict_updatability_linting: true,
+        baseline_filename: "lint-baseline.xml",
+
+    },
 }
 
 android_library {
@@ -106,14 +114,18 @@
     defaults: [
         "TetheringAndroidLibraryDefaults",
         "TetheringApiLevel",
-        "TetheringReleaseTargetSdk"
+        "TetheringReleaseTargetSdk",
     ],
     static_libs: [
         "NetworkStackApiStableShims",
         "net-utils-device-common-struct",
     ],
     apex_available: ["com.android.tethering"],
-    lint: { strict_updatability_linting: true },
+    lint: {
+        strict_updatability_linting: true,
+        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).
@@ -186,20 +198,28 @@
     optimize: {
         proguard_flags_files: ["proguard.flags"],
     },
-    lint: { strict_updatability_linting: true },
+    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 },
+    lint: {
+        strict_updatability_linting: true,
+
+    },
 }
 
 android_app {
@@ -215,7 +235,11 @@
     use_embedded_native_libs: true,
     privapp_allowlist: ":privapp_allowlist_com.android.tethering",
     apex_available: ["com.android.tethering"],
-    lint: { strict_updatability_linting: true },
+    lint: {
+        strict_updatability_linting: true,
+        error_checks: ["NewApi"],
+
+    },
 }
 
 sdk {
@@ -233,7 +257,9 @@
 
 java_library_static {
     name: "tetheringstatsprotos",
-    proto: {type: "lite"},
+    proto: {
+        type: "lite",
+    },
     srcs: [
         "src/com/android/networkstack/tethering/metrics/stats.proto",
     ],
@@ -246,6 +272,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 c5d3a94..d79be20 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
@@ -92,7 +96,7 @@
         both: {
             jni_libs: [
                 "libframework-connectivity-jni",
-                "libframework-connectivity-tiramisu-jni"
+                "libframework-connectivity-tiramisu-jni",
             ],
         },
     },
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 c26c32f..9c2a59d 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"],
 }
 
@@ -21,7 +22,6 @@
     name: "framework-tethering",
     defaults: [
         "framework-tethering-defaults",
-        "FlaggedApiDefaults",
     ],
     impl_library_visibility: [
         "//packages/modules/Connectivity/Tethering:__subpackages__",
@@ -44,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",
@@ -55,14 +56,16 @@
 
     hostdex: true, // for hiddenapi check
     permitted_packages: ["android.net"],
-    lint: { strict_updatability_linting: true },
+    lint: {
+        strict_updatability_linting: true,
+    },
 }
 
 java_library {
-  name: "framework-tethering-pre-jarjar",
-  defaults: [
-    "framework-tethering-defaults",
-  ],
+    name: "framework-tethering-pre-jarjar",
+    defaults: [
+        "framework-tethering-defaults",
+    ],
 }
 
 java_genrule {
@@ -88,7 +91,7 @@
     name: "framework-tethering-defaults",
     defaults: ["framework-module-defaults"],
     srcs: [
-      ":framework-tethering-srcs"
+        ":framework-tethering-srcs",
     ],
     libs: ["framework-connectivity.stubs.module_lib"],
     aidl: {
@@ -107,5 +110,5 @@
         "src/**/*.aidl",
         "src/**/*.java",
     ],
-    path: "src"
+    path: "src",
 }
diff --git a/Tethering/common/TetheringLib/udc-extended-api/current.txt b/Tethering/common/TetheringLib/udc-extended-api/current.txt
deleted file mode 100644
index d802177..0000000
--- a/Tethering/common/TetheringLib/udc-extended-api/current.txt
+++ /dev/null
@@ -1 +0,0 @@
-// Signature format: 2.0
diff --git a/Tethering/common/TetheringLib/udc-extended-api/module-lib-current.txt b/Tethering/common/TetheringLib/udc-extended-api/module-lib-current.txt
deleted file mode 100644
index 460c216..0000000
--- a/Tethering/common/TetheringLib/udc-extended-api/module-lib-current.txt
+++ /dev/null
@@ -1,50 +0,0 @@
-// Signature format: 2.0
-package android.net {
-
-  public final class TetheringConstants {
-    field public static final String EXTRA_ADD_TETHER_TYPE = "extraAddTetherType";
-    field public static final String EXTRA_PROVISION_CALLBACK = "extraProvisionCallback";
-    field public static final String EXTRA_REM_TETHER_TYPE = "extraRemTetherType";
-    field public static final String EXTRA_RUN_PROVISION = "extraRunProvision";
-    field public static final String EXTRA_SET_ALARM = "extraSetAlarm";
-  }
-
-  public class TetheringManager {
-    ctor public TetheringManager(@NonNull android.content.Context, @NonNull java.util.function.Supplier<android.os.IBinder>);
-    method public int getLastTetherError(@NonNull String);
-    method @NonNull public String[] getTetherableBluetoothRegexs();
-    method @NonNull public String[] getTetherableIfaces();
-    method @NonNull public String[] getTetherableUsbRegexs();
-    method @NonNull public String[] getTetherableWifiRegexs();
-    method @NonNull public String[] getTetheredIfaces();
-    method @NonNull public String[] getTetheringErroredIfaces();
-    method public boolean isTetheringSupported();
-    method public boolean isTetheringSupported(@NonNull String);
-    method public void requestLatestTetheringEntitlementResult(int, @NonNull android.os.ResultReceiver, boolean);
-    method @Deprecated public int setUsbTethering(boolean);
-    method @RequiresPermission(anyOf={android.Manifest.permission.TETHER_PRIVILEGED, android.Manifest.permission.WRITE_SETTINGS}) public void startTethering(int, @NonNull java.util.concurrent.Executor, @NonNull android.net.TetheringManager.StartTetheringCallback);
-    method @Deprecated public int tether(@NonNull String);
-    method @Deprecated public int untether(@NonNull String);
-  }
-
-  public static interface TetheringManager.TetheredInterfaceCallback {
-    method public void onAvailable(@NonNull String);
-    method public void onUnavailable();
-  }
-
-  public static interface TetheringManager.TetheredInterfaceRequest {
-    method public void release();
-  }
-
-  public static interface TetheringManager.TetheringEventCallback {
-    method @Deprecated public default void onTetherableInterfaceRegexpsChanged(@NonNull android.net.TetheringManager.TetheringInterfaceRegexps);
-  }
-
-  @Deprecated public static class TetheringManager.TetheringInterfaceRegexps {
-    method @Deprecated @NonNull public java.util.List<java.lang.String> getTetherableBluetoothRegexs();
-    method @Deprecated @NonNull public java.util.List<java.lang.String> getTetherableUsbRegexs();
-    method @Deprecated @NonNull public java.util.List<java.lang.String> getTetherableWifiRegexs();
-  }
-
-}
-
diff --git a/Tethering/common/TetheringLib/udc-extended-api/module-lib-removed.txt b/Tethering/common/TetheringLib/udc-extended-api/module-lib-removed.txt
deleted file mode 100644
index d802177..0000000
--- a/Tethering/common/TetheringLib/udc-extended-api/module-lib-removed.txt
+++ /dev/null
@@ -1 +0,0 @@
-// Signature format: 2.0
diff --git a/Tethering/common/TetheringLib/udc-extended-api/removed.txt b/Tethering/common/TetheringLib/udc-extended-api/removed.txt
deleted file mode 100644
index d802177..0000000
--- a/Tethering/common/TetheringLib/udc-extended-api/removed.txt
+++ /dev/null
@@ -1 +0,0 @@
-// Signature format: 2.0
diff --git a/Tethering/common/TetheringLib/udc-extended-api/system-current.txt b/Tethering/common/TetheringLib/udc-extended-api/system-current.txt
deleted file mode 100644
index 844ff64..0000000
--- a/Tethering/common/TetheringLib/udc-extended-api/system-current.txt
+++ /dev/null
@@ -1,117 +0,0 @@
-// Signature format: 2.0
-package android.net {
-
-  public final class TetheredClient implements android.os.Parcelable {
-    ctor public TetheredClient(@NonNull android.net.MacAddress, @NonNull java.util.Collection<android.net.TetheredClient.AddressInfo>, int);
-    method public int describeContents();
-    method @NonNull public java.util.List<android.net.TetheredClient.AddressInfo> getAddresses();
-    method @NonNull public android.net.MacAddress getMacAddress();
-    method public int getTetheringType();
-    method public void writeToParcel(@NonNull android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.net.TetheredClient> CREATOR;
-  }
-
-  public static final class TetheredClient.AddressInfo implements android.os.Parcelable {
-    method public int describeContents();
-    method @NonNull public android.net.LinkAddress getAddress();
-    method @Nullable public String getHostname();
-    method public void writeToParcel(@NonNull android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.net.TetheredClient.AddressInfo> CREATOR;
-  }
-
-  public final class TetheringInterface implements android.os.Parcelable {
-    ctor public TetheringInterface(int, @NonNull String);
-    method public int describeContents();
-    method @NonNull public String getInterface();
-    method public int getType();
-    method public void writeToParcel(@NonNull android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.net.TetheringInterface> CREATOR;
-  }
-
-  public class TetheringManager {
-    method @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public void registerTetheringEventCallback(@NonNull java.util.concurrent.Executor, @NonNull android.net.TetheringManager.TetheringEventCallback);
-    method @RequiresPermission(anyOf={android.Manifest.permission.TETHER_PRIVILEGED, android.Manifest.permission.WRITE_SETTINGS}) public void requestLatestTetheringEntitlementResult(int, boolean, @NonNull java.util.concurrent.Executor, @NonNull android.net.TetheringManager.OnTetheringEntitlementResultListener);
-    method @RequiresPermission(anyOf={android.Manifest.permission.TETHER_PRIVILEGED, android.Manifest.permission.WRITE_SETTINGS}) public void startTethering(@NonNull android.net.TetheringManager.TetheringRequest, @NonNull java.util.concurrent.Executor, @NonNull android.net.TetheringManager.StartTetheringCallback);
-    method @RequiresPermission(anyOf={android.Manifest.permission.TETHER_PRIVILEGED, android.Manifest.permission.WRITE_SETTINGS}) public void stopAllTethering();
-    method @RequiresPermission(anyOf={android.Manifest.permission.TETHER_PRIVILEGED, android.Manifest.permission.WRITE_SETTINGS}) public void stopTethering(int);
-    method @RequiresPermission(anyOf={android.Manifest.permission.TETHER_PRIVILEGED, android.Manifest.permission.ACCESS_NETWORK_STATE}) public void unregisterTetheringEventCallback(@NonNull android.net.TetheringManager.TetheringEventCallback);
-    field @Deprecated public static final String ACTION_TETHER_STATE_CHANGED = "android.net.conn.TETHER_STATE_CHANGED";
-    field public static final int CONNECTIVITY_SCOPE_GLOBAL = 1; // 0x1
-    field public static final int CONNECTIVITY_SCOPE_LOCAL = 2; // 0x2
-    field public static final String EXTRA_ACTIVE_LOCAL_ONLY = "android.net.extra.ACTIVE_LOCAL_ONLY";
-    field public static final String EXTRA_ACTIVE_TETHER = "tetherArray";
-    field public static final String EXTRA_AVAILABLE_TETHER = "availableArray";
-    field public static final String EXTRA_ERRORED_TETHER = "erroredArray";
-    field public static final int TETHERING_BLUETOOTH = 2; // 0x2
-    field public static final int TETHERING_ETHERNET = 5; // 0x5
-    field public static final int TETHERING_INVALID = -1; // 0xffffffff
-    field public static final int TETHERING_NCM = 4; // 0x4
-    field public static final int TETHERING_USB = 1; // 0x1
-    field public static final int TETHERING_WIFI = 0; // 0x0
-    field public static final int TETHERING_WIFI_P2P = 3; // 0x3
-    field public static final int TETHER_ERROR_DHCPSERVER_ERROR = 12; // 0xc
-    field public static final int TETHER_ERROR_DISABLE_FORWARDING_ERROR = 9; // 0x9
-    field public static final int TETHER_ERROR_ENABLE_FORWARDING_ERROR = 8; // 0x8
-    field public static final int TETHER_ERROR_ENTITLEMENT_UNKNOWN = 13; // 0xd
-    field public static final int TETHER_ERROR_IFACE_CFG_ERROR = 10; // 0xa
-    field public static final int TETHER_ERROR_INTERNAL_ERROR = 5; // 0x5
-    field public static final int TETHER_ERROR_NO_ACCESS_TETHERING_PERMISSION = 15; // 0xf
-    field public static final int TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION = 14; // 0xe
-    field public static final int TETHER_ERROR_NO_ERROR = 0; // 0x0
-    field public static final int TETHER_ERROR_PROVISIONING_FAILED = 11; // 0xb
-    field public static final int TETHER_ERROR_SERVICE_UNAVAIL = 2; // 0x2
-    field public static final int TETHER_ERROR_TETHER_IFACE_ERROR = 6; // 0x6
-    field public static final int TETHER_ERROR_UNAVAIL_IFACE = 4; // 0x4
-    field public static final int TETHER_ERROR_UNKNOWN_IFACE = 1; // 0x1
-    field public static final int TETHER_ERROR_UNKNOWN_TYPE = 16; // 0x10
-    field public static final int TETHER_ERROR_UNSUPPORTED = 3; // 0x3
-    field public static final int TETHER_ERROR_UNTETHER_IFACE_ERROR = 7; // 0x7
-    field public static final int TETHER_HARDWARE_OFFLOAD_FAILED = 2; // 0x2
-    field public static final int TETHER_HARDWARE_OFFLOAD_STARTED = 1; // 0x1
-    field public static final int TETHER_HARDWARE_OFFLOAD_STOPPED = 0; // 0x0
-  }
-
-  public static interface TetheringManager.OnTetheringEntitlementResultListener {
-    method public void onTetheringEntitlementResult(int);
-  }
-
-  public static interface TetheringManager.StartTetheringCallback {
-    method public default void onTetheringFailed(int);
-    method public default void onTetheringStarted();
-  }
-
-  public static interface TetheringManager.TetheringEventCallback {
-    method public default void onClientsChanged(@NonNull java.util.Collection<android.net.TetheredClient>);
-    method public default void onError(@NonNull String, int);
-    method public default void onError(@NonNull android.net.TetheringInterface, int);
-    method public default void onLocalOnlyInterfacesChanged(@NonNull java.util.List<java.lang.String>);
-    method public default void onLocalOnlyInterfacesChanged(@NonNull java.util.Set<android.net.TetheringInterface>);
-    method public default void onOffloadStatusChanged(int);
-    method public default void onTetherableInterfacesChanged(@NonNull java.util.List<java.lang.String>);
-    method public default void onTetherableInterfacesChanged(@NonNull java.util.Set<android.net.TetheringInterface>);
-    method public default void onTetheredInterfacesChanged(@NonNull java.util.List<java.lang.String>);
-    method public default void onTetheredInterfacesChanged(@NonNull java.util.Set<android.net.TetheringInterface>);
-    method public default void onTetheringSupported(boolean);
-    method public default void onUpstreamChanged(@Nullable android.net.Network);
-  }
-
-  public static class TetheringManager.TetheringRequest {
-    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();
-  }
-
-  public static class TetheringManager.TetheringRequest.Builder {
-    ctor public TetheringManager.TetheringRequest.Builder(int);
-    method @NonNull public android.net.TetheringManager.TetheringRequest build();
-    method @NonNull @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED) public android.net.TetheringManager.TetheringRequest.Builder setConnectivityScope(int);
-    method @NonNull @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED) public android.net.TetheringManager.TetheringRequest.Builder setExemptFromEntitlementCheck(boolean);
-    method @NonNull @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED) public android.net.TetheringManager.TetheringRequest.Builder setShouldShowEntitlementUi(boolean);
-    method @NonNull @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED) public android.net.TetheringManager.TetheringRequest.Builder setStaticIpv4Addresses(@NonNull android.net.LinkAddress, @NonNull android.net.LinkAddress);
-  }
-
-}
-
diff --git a/Tethering/common/TetheringLib/udc-extended-api/system-removed.txt b/Tethering/common/TetheringLib/udc-extended-api/system-removed.txt
deleted file mode 100644
index d802177..0000000
--- a/Tethering/common/TetheringLib/udc-extended-api/system-removed.txt
+++ /dev/null
@@ -1 +0,0 @@
-// Signature format: 2.0
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-af/strings.xml b/Tethering/res/values-af/strings.xml
index 3790142..056168b 100644
--- a/Tethering/res/values-af/strings.xml
+++ b/Tethering/res/values-af/strings.xml
@@ -16,14 +16,14 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="tethered_notification_title" msgid="5350162111436634622">"Verbinding of warmkol is aktief"</string>
-    <string name="tethered_notification_message" msgid="2338023450330652098">"Tik om op te stel."</string>
-    <string name="disable_tether_notification_title" msgid="3183576627492925522">"Verbinding is gedeaktiveer"</string>
-    <string name="disable_tether_notification_message" msgid="6655882039707534929">"Kontak jou admin vir besonderhede"</string>
-    <string name="notification_channel_tethering_status" msgid="7030733422705019001">"Warmkol- en verbindingstatus"</string>
-    <string name="no_upstream_notification_title" msgid="2052743091868702475"></string>
-    <string name="no_upstream_notification_message" msgid="6932020551635470134"></string>
-    <string name="no_upstream_notification_disable_button" msgid="8836277213343697023"></string>
-    <string name="upstream_roaming_notification_title" msgid="8614262557406849762"></string>
-    <string name="upstream_roaming_notification_message" msgid="5999740876323106599"></string>
+    <string name="tethered_notification_title" msgid="6426563586025792944">"Verbinding of warmkol is aktief"</string>
+    <string name="tethered_notification_message" msgid="64800879503420696">"Tik om op te stel."</string>
+    <string name="disable_tether_notification_title" msgid="3004509127903564191">"Verbinding is gedeaktiveer"</string>
+    <string name="disable_tether_notification_message" msgid="6717523799293901476">"Kontak jou administrateur vir besonderhede"</string>
+    <string name="notification_channel_tethering_status" msgid="2663463891530932727">"Warmkol- en verbindingstatus"</string>
+    <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+    <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+    <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+    <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+    <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
 </resources>
diff --git a/Tethering/res/values-am/strings.xml b/Tethering/res/values-am/strings.xml
index bb89d6e..ac468dd 100644
--- a/Tethering/res/values-am/strings.xml
+++ b/Tethering/res/values-am/strings.xml
@@ -16,14 +16,14 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="tethered_notification_title" msgid="5350162111436634622">"እንደ ሞደም መሰካት ወይም መገናኛ ነጥብ ገባሪ"</string>
-    <string name="tethered_notification_message" msgid="2338023450330652098">"ለማዋቀር መታ ያድርጉ።"</string>
-    <string name="disable_tether_notification_title" msgid="3183576627492925522">"እንደ ሞደም መሰካት ተሰናክሏል"</string>
-    <string name="disable_tether_notification_message" msgid="6655882039707534929">"ለዝርዝሮች የእርስዎን አስተዳዳሪ ያነጋግሩ"</string>
-    <string name="notification_channel_tethering_status" msgid="7030733422705019001">"መገናኛ ነጥብ እና እንደ ሞደም የመሰካት ሁኔታ"</string>
-    <string name="no_upstream_notification_title" msgid="2052743091868702475"></string>
-    <string name="no_upstream_notification_message" msgid="6932020551635470134"></string>
-    <string name="no_upstream_notification_disable_button" msgid="8836277213343697023"></string>
-    <string name="upstream_roaming_notification_title" msgid="8614262557406849762"></string>
-    <string name="upstream_roaming_notification_message" msgid="5999740876323106599"></string>
+    <string name="tethered_notification_title" msgid="6426563586025792944">"እንደ ሞደም መሰካት ወይም መገናኛ ነጥብ ገባሪ"</string>
+    <string name="tethered_notification_message" msgid="64800879503420696">"ለማዋቀር መታ ያድርጉ።"</string>
+    <string name="disable_tether_notification_title" msgid="3004509127903564191">"እንደ ሞደም መሰካት ተሰናክሏል"</string>
+    <string name="disable_tether_notification_message" msgid="6717523799293901476">"ለዝርዝሮች የእርስዎን አስተዳዳሪ ያነጋግሩ"</string>
+    <string name="notification_channel_tethering_status" msgid="2663463891530932727">"መገናኛ ነጥብ እና እንደ ሞደም የመሰካት ሁኔታ"</string>
+    <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+    <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+    <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+    <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+    <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
 </resources>
diff --git a/Tethering/res/values-ar/strings.xml b/Tethering/res/values-ar/strings.xml
index ef98a01..7d5bad3 100644
--- a/Tethering/res/values-ar/strings.xml
+++ b/Tethering/res/values-ar/strings.xml
@@ -16,14 +16,14 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="tethered_notification_title" msgid="5350162111436634622">"نقطة التوصيل نشطة أو الاتصال نشط"</string>
-    <string name="tethered_notification_message" msgid="2338023450330652098">"انقر لإعداد التوصيل."</string>
-    <string name="disable_tether_notification_title" msgid="3183576627492925522">"التوصيل غير مفعَّل"</string>
-    <string name="disable_tether_notification_message" msgid="6655882039707534929">"تواصَل مع المشرف للحصول على التفاصيل."</string>
-    <string name="notification_channel_tethering_status" msgid="7030733422705019001">"حالة نقطة الاتصال والتوصيل"</string>
-    <string name="no_upstream_notification_title" msgid="2052743091868702475"></string>
-    <string name="no_upstream_notification_message" msgid="6932020551635470134"></string>
-    <string name="no_upstream_notification_disable_button" msgid="8836277213343697023"></string>
-    <string name="upstream_roaming_notification_title" msgid="8614262557406849762"></string>
-    <string name="upstream_roaming_notification_message" msgid="5999740876323106599"></string>
+    <string name="tethered_notification_title" msgid="6426563586025792944">"النطاق نشط أو نقطة الاتصال نشطة"</string>
+    <string name="tethered_notification_message" msgid="64800879503420696">"انقر للإعداد."</string>
+    <string name="disable_tether_notification_title" msgid="3004509127903564191">"التوصيل متوقف."</string>
+    <string name="disable_tether_notification_message" msgid="6717523799293901476">"تواصَل مع المشرف للحصول على التفاصيل."</string>
+    <string name="notification_channel_tethering_status" msgid="2663463891530932727">"حالة نقطة الاتصال والتوصيل"</string>
+    <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+    <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+    <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+    <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+    <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
 </resources>
diff --git a/Tethering/res/values-as/strings.xml b/Tethering/res/values-as/strings.xml
index 9b9e8d6..0913504 100644
--- a/Tethering/res/values-as/strings.xml
+++ b/Tethering/res/values-as/strings.xml
@@ -16,14 +16,14 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="tethered_notification_title" msgid="5350162111436634622">"টে’ডাৰিং অথবা হ’টস্প’ট সক্ৰিয় অৱস্থাত আছে"</string>
-    <string name="tethered_notification_message" msgid="2338023450330652098">"ছেট আপ কৰিবলৈ টিপক।"</string>
-    <string name="disable_tether_notification_title" msgid="3183576627492925522">"টে’ডাৰিঙৰ সুবিধাটো অক্ষম কৰি থোৱা হৈছে"</string>
-    <string name="disable_tether_notification_message" msgid="6655882039707534929">"সবিশেষ জানিবলৈ আপোনাৰ প্ৰশাসকৰ সৈতে যোগাযোগ কৰক"</string>
-    <string name="notification_channel_tethering_status" msgid="7030733422705019001">"হ’টস্প’ট আৰু টে’ডাৰিঙৰ স্থিতি"</string>
-    <string name="no_upstream_notification_title" msgid="2052743091868702475"></string>
-    <string name="no_upstream_notification_message" msgid="6932020551635470134"></string>
-    <string name="no_upstream_notification_disable_button" msgid="8836277213343697023"></string>
-    <string name="upstream_roaming_notification_title" msgid="8614262557406849762"></string>
-    <string name="upstream_roaming_notification_message" msgid="5999740876323106599"></string>
+    <string name="tethered_notification_title" msgid="6426563586025792944">"টে\'ডাৰিং অথবা হ\'টস্প\'ট সক্ৰিয় অৱস্থাত আছে"</string>
+    <string name="tethered_notification_message" msgid="64800879503420696">"ছেট আপ কৰিবলৈ টিপক।"</string>
+    <string name="disable_tether_notification_title" msgid="3004509127903564191">"টে\'ডাৰিঙৰ সুবিধাটো অক্ষম কৰি থোৱা হৈছে"</string>
+    <string name="disable_tether_notification_message" msgid="6717523799293901476">"সবিশেষ জানিবলৈ আপোনাৰ প্ৰশাসকৰ সৈতে যোগাযোগ কৰক"</string>
+    <string name="notification_channel_tethering_status" msgid="2663463891530932727">"হ’টস্প\'ট আৰু টে\'ডাৰিঙৰ স্থিতি"</string>
+    <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+    <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+    <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+    <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+    <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
 </resources>
diff --git a/Tethering/res/values-az/strings.xml b/Tethering/res/values-az/strings.xml
index b091f15..dce70da 100644
--- a/Tethering/res/values-az/strings.xml
+++ b/Tethering/res/values-az/strings.xml
@@ -16,14 +16,14 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="tethered_notification_title" msgid="5350162111436634622">"Modem rejimi və ya hotspot aktivdir"</string>
-    <string name="tethered_notification_message" msgid="2338023450330652098">"Ayarlamaq üçün toxunun."</string>
-    <string name="disable_tether_notification_title" msgid="3183576627492925522">"Modem rejimi deaktivdir"</string>
-    <string name="disable_tether_notification_message" msgid="6655882039707534929">"Detallar üçün adminlə əlaqə saxlayın"</string>
-    <string name="notification_channel_tethering_status" msgid="7030733422705019001">"Hotspot və modem rejimi statusu"</string>
-    <string name="no_upstream_notification_title" msgid="2052743091868702475"></string>
-    <string name="no_upstream_notification_message" msgid="6932020551635470134"></string>
-    <string name="no_upstream_notification_disable_button" msgid="8836277213343697023"></string>
-    <string name="upstream_roaming_notification_title" msgid="8614262557406849762"></string>
-    <string name="upstream_roaming_notification_message" msgid="5999740876323106599"></string>
+    <string name="tethered_notification_title" msgid="6426563586025792944">"Birləşmə və ya hotspot aktivdir"</string>
+    <string name="tethered_notification_message" msgid="64800879503420696">"Ayarlamaq üçün toxunun."</string>
+    <string name="disable_tether_notification_title" msgid="3004509127903564191">"Birləşmə deaktivdir"</string>
+    <string name="disable_tether_notification_message" msgid="6717523799293901476">"Detallar üçün adminlə əlaqə saxlayın"</string>
+    <string name="notification_channel_tethering_status" msgid="2663463891530932727">"Hotspot &amp; birləşmə statusu"</string>
+    <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+    <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+    <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+    <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+    <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
 </resources>
diff --git a/Tethering/res/values-b+sr+Latn/strings.xml b/Tethering/res/values-b+sr+Latn/strings.xml
index aa6c6fd..b0774ec 100644
--- a/Tethering/res/values-b+sr+Latn/strings.xml
+++ b/Tethering/res/values-b+sr+Latn/strings.xml
@@ -16,14 +16,14 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="tethered_notification_title" msgid="5350162111436634622">"Aktivno je privezivanje ili hotspot"</string>
-    <string name="tethered_notification_message" msgid="2338023450330652098">"Dodirnite da biste podesili."</string>
-    <string name="disable_tether_notification_title" msgid="3183576627492925522">"Privezivanje je onemogućeno"</string>
-    <string name="disable_tether_notification_message" msgid="6655882039707534929">"Potražite detalje od administratora"</string>
-    <string name="notification_channel_tethering_status" msgid="7030733422705019001">"Status hotspota i privezivanja"</string>
-    <string name="no_upstream_notification_title" msgid="2052743091868702475"></string>
-    <string name="no_upstream_notification_message" msgid="6932020551635470134"></string>
-    <string name="no_upstream_notification_disable_button" msgid="8836277213343697023"></string>
-    <string name="upstream_roaming_notification_title" msgid="8614262557406849762"></string>
-    <string name="upstream_roaming_notification_message" msgid="5999740876323106599"></string>
+    <string name="tethered_notification_title" msgid="6426563586025792944">"Privezivanje ili hotspot je aktivan"</string>
+    <string name="tethered_notification_message" msgid="64800879503420696">"Dodirnite da biste podesili."</string>
+    <string name="disable_tether_notification_title" msgid="3004509127903564191">"Privezivanje je onemogućeno"</string>
+    <string name="disable_tether_notification_message" msgid="6717523799293901476">"Potražite detalje od administratora"</string>
+    <string name="notification_channel_tethering_status" msgid="2663463891530932727">"Status hotspota i privezivanja"</string>
+    <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+    <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+    <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+    <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+    <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
 </resources>
diff --git a/Tethering/res/values-be/strings.xml b/Tethering/res/values-be/strings.xml
index 5da8828..a8acebe 100644
--- a/Tethering/res/values-be/strings.xml
+++ b/Tethering/res/values-be/strings.xml
@@ -16,14 +16,14 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="tethered_notification_title" msgid="5350162111436634622">"Мадэм або хот-спот актыўныя"</string>
-    <string name="tethered_notification_message" msgid="2338023450330652098">"Націсніце, каб наладзіць."</string>
-    <string name="disable_tether_notification_title" msgid="3183576627492925522">"Рэжым мадэма выключаны"</string>
-    <string name="disable_tether_notification_message" msgid="6655882039707534929">"Звярніцеся да адміністратара па падрабязную інфармацыю"</string>
-    <string name="notification_channel_tethering_status" msgid="7030733422705019001">"Стан \"Хот-спот і мадэм\""</string>
-    <string name="no_upstream_notification_title" msgid="2052743091868702475"></string>
-    <string name="no_upstream_notification_message" msgid="6932020551635470134"></string>
-    <string name="no_upstream_notification_disable_button" msgid="8836277213343697023"></string>
-    <string name="upstream_roaming_notification_title" msgid="8614262557406849762"></string>
-    <string name="upstream_roaming_notification_message" msgid="5999740876323106599"></string>
+    <string name="tethered_notification_title" msgid="6426563586025792944">"Мадэм або хот-спот актыўныя"</string>
+    <string name="tethered_notification_message" msgid="64800879503420696">"Дакраніцеся, каб наладзіць."</string>
+    <string name="disable_tether_notification_title" msgid="3004509127903564191">"Рэжым мадэма выключаны"</string>
+    <string name="disable_tether_notification_message" msgid="6717523799293901476">"Звярніцеся да адміністратара па падрабязную інфармацыю"</string>
+    <string name="notification_channel_tethering_status" msgid="2663463891530932727">"Стан \"Хот-спот і мадэм\""</string>
+    <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+    <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+    <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+    <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+    <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
 </resources>
diff --git a/Tethering/res/values-bg/strings.xml b/Tethering/res/values-bg/strings.xml
index 0ce2ac7..94fb2d8 100644
--- a/Tethering/res/values-bg/strings.xml
+++ b/Tethering/res/values-bg/strings.xml
@@ -16,14 +16,14 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="tethered_notification_title" msgid="5350162111436634622">"Има активна споделена връзка или точка за достъп"</string>
-    <string name="tethered_notification_message" msgid="2338023450330652098">"Докоснете, за да настроите."</string>
-    <string name="disable_tether_notification_title" msgid="3183576627492925522">"Функцията за тетъринг е деактивирана"</string>
-    <string name="disable_tether_notification_message" msgid="6655882039707534929">"Свържете се с администратора си за подробности"</string>
-    <string name="notification_channel_tethering_status" msgid="7030733422705019001">"Състояние на функцията за точка за достъп и тетъринг"</string>
-    <string name="no_upstream_notification_title" msgid="2052743091868702475"></string>
-    <string name="no_upstream_notification_message" msgid="6932020551635470134"></string>
-    <string name="no_upstream_notification_disable_button" msgid="8836277213343697023"></string>
-    <string name="upstream_roaming_notification_title" msgid="8614262557406849762"></string>
-    <string name="upstream_roaming_notification_message" msgid="5999740876323106599"></string>
+    <string name="tethered_notification_title" msgid="6426563586025792944">"Има активна споделена връзка или точка за достъп"</string>
+    <string name="tethered_notification_message" msgid="64800879503420696">"Докоснете, за да настроите."</string>
+    <string name="disable_tether_notification_title" msgid="3004509127903564191">"Функцията за тетъринг е деактивирана"</string>
+    <string name="disable_tether_notification_message" msgid="6717523799293901476">"Свържете се с администратора си за подробности"</string>
+    <string name="notification_channel_tethering_status" msgid="2663463891530932727">"Състояние на функцията за точка за достъп и тетъринг"</string>
+    <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+    <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+    <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+    <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+    <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
 </resources>
diff --git a/Tethering/res/values-bn/strings.xml b/Tethering/res/values-bn/strings.xml
index 787a65c..aea02b9 100644
--- a/Tethering/res/values-bn/strings.xml
+++ b/Tethering/res/values-bn/strings.xml
@@ -16,14 +16,14 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="tethered_notification_title" msgid="5350162111436634622">"টেথারিং বা হটস্পট অ্যাক্টিভ আছে"</string>
-    <string name="tethered_notification_message" msgid="2338023450330652098">"সেট-আপ করতে ট্যাপ করুন।"</string>
-    <string name="disable_tether_notification_title" msgid="3183576627492925522">"টেথারিং বন্ধ করা আছে"</string>
-    <string name="disable_tether_notification_message" msgid="6655882039707534929">"বিস্তারিত বিবরণ পেতে, অ্যাডমিনের সাথে যোগাযোগ করুন"</string>
-    <string name="notification_channel_tethering_status" msgid="7030733422705019001">"হটস্পট ও টেথারিং সংক্রান্ত স্ট্যাটাস"</string>
-    <string name="no_upstream_notification_title" msgid="2052743091868702475"></string>
-    <string name="no_upstream_notification_message" msgid="6932020551635470134"></string>
-    <string name="no_upstream_notification_disable_button" msgid="8836277213343697023"></string>
-    <string name="upstream_roaming_notification_title" msgid="8614262557406849762"></string>
-    <string name="upstream_roaming_notification_message" msgid="5999740876323106599"></string>
+    <string name="tethered_notification_title" msgid="6426563586025792944">"টিথারিং বা হটস্পট চালু আছে"</string>
+    <string name="tethered_notification_message" msgid="64800879503420696">"সেট-আপ করতে ট্যাপ করুন।"</string>
+    <string name="disable_tether_notification_title" msgid="3004509127903564191">"টিথারিং বন্ধ করা আছে"</string>
+    <string name="disable_tether_notification_message" msgid="6717523799293901476">"বিশদে জানতে অ্যাডমিনের সাথে যোগাযোগ করুন"</string>
+    <string name="notification_channel_tethering_status" msgid="2663463891530932727">"হটস্পট ও টিথারিং স্ট্যাটাস"</string>
+    <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+    <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+    <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+    <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+    <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
 </resources>
diff --git a/Tethering/res/values-bs/strings.xml b/Tethering/res/values-bs/strings.xml
index b6073fd..de23272 100644
--- a/Tethering/res/values-bs/strings.xml
+++ b/Tethering/res/values-bs/strings.xml
@@ -16,14 +16,14 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="tethered_notification_title" msgid="5350162111436634622">"Dijeljenje internetske veze ili pristupna tačka su aktivni"</string>
-    <string name="tethered_notification_message" msgid="2338023450330652098">"Dodirnite da postavite."</string>
-    <string name="disable_tether_notification_title" msgid="3183576627492925522">"Dijeljenje internetske veze je onemogućeno"</string>
-    <string name="disable_tether_notification_message" msgid="6655882039707534929">"Kontaktirajte administratora za detalje"</string>
-    <string name="notification_channel_tethering_status" msgid="7030733422705019001">"Status pristupne tačke i dijeljenja internetske veze"</string>
-    <string name="no_upstream_notification_title" msgid="2052743091868702475"></string>
-    <string name="no_upstream_notification_message" msgid="6932020551635470134"></string>
-    <string name="no_upstream_notification_disable_button" msgid="8836277213343697023"></string>
-    <string name="upstream_roaming_notification_title" msgid="8614262557406849762"></string>
-    <string name="upstream_roaming_notification_message" msgid="5999740876323106599"></string>
+    <string name="tethered_notification_title" msgid="6426563586025792944">"Aktivno je povezivanje putem mobitela ili pristupna tačka"</string>
+    <string name="tethered_notification_message" msgid="64800879503420696">"Dodirnite da postavite."</string>
+    <string name="disable_tether_notification_title" msgid="3004509127903564191">"Povezivanje putem mobitela je onemogućeno"</string>
+    <string name="disable_tether_notification_message" msgid="6717523799293901476">"Kontaktirajte svog administratora za detalje"</string>
+    <string name="notification_channel_tethering_status" msgid="2663463891530932727">"Status pristupne tačke i povezivanja putem mobitela"</string>
+    <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+    <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+    <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+    <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+    <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
 </resources>
diff --git a/Tethering/res/values-ca/strings.xml b/Tethering/res/values-ca/strings.xml
index 2513989..88b795c 100644
--- a/Tethering/res/values-ca/strings.xml
+++ b/Tethering/res/values-ca/strings.xml
@@ -16,14 +16,14 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="tethered_notification_title" msgid="5350162111436634622">"Compartició de xarxa o punt d\'accés Wi‑Fi actius"</string>
-    <string name="tethered_notification_message" msgid="2338023450330652098">"Toca per configurar."</string>
-    <string name="disable_tether_notification_title" msgid="3183576627492925522">"La compartició de xarxa està desactivada"</string>
-    <string name="disable_tether_notification_message" msgid="6655882039707534929">"Contacta amb el teu administrador per obtenir més informació"</string>
-    <string name="notification_channel_tethering_status" msgid="7030733422705019001">"Estat del punt d\'accés Wi‑Fi i de la compartició de xarxa"</string>
-    <string name="no_upstream_notification_title" msgid="2052743091868702475"></string>
-    <string name="no_upstream_notification_message" msgid="6932020551635470134"></string>
-    <string name="no_upstream_notification_disable_button" msgid="8836277213343697023"></string>
-    <string name="upstream_roaming_notification_title" msgid="8614262557406849762"></string>
-    <string name="upstream_roaming_notification_message" msgid="5999740876323106599"></string>
+    <string name="tethered_notification_title" msgid="6426563586025792944">"Compartició de xarxa o punt d\'accés Wi‑Fi actius"</string>
+    <string name="tethered_notification_message" msgid="64800879503420696">"Toca per configurar."</string>
+    <string name="disable_tether_notification_title" msgid="3004509127903564191">"La compartició de xarxa està desactivada"</string>
+    <string name="disable_tether_notification_message" msgid="6717523799293901476">"Contacta amb el teu administrador per obtenir més informació"</string>
+    <string name="notification_channel_tethering_status" msgid="2663463891530932727">"Estat del punt d\'accés Wi‑Fi i de la compartició de xarxa"</string>
+    <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+    <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+    <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+    <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+    <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
 </resources>
diff --git a/Tethering/res/values-cs/strings.xml b/Tethering/res/values-cs/strings.xml
index a749915..8c1b83b 100644
--- a/Tethering/res/values-cs/strings.xml
+++ b/Tethering/res/values-cs/strings.xml
@@ -16,14 +16,14 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="tethered_notification_title" msgid="5350162111436634622">"Tethering nebo hotspot je aktivní"</string>
-    <string name="tethered_notification_message" msgid="2338023450330652098">"Klepnutím ho nastavíte."</string>
-    <string name="disable_tether_notification_title" msgid="3183576627492925522">"Tethering je zakázán"</string>
-    <string name="disable_tether_notification_message" msgid="6655882039707534929">"O podrobnosti požádejte administrátora"</string>
-    <string name="notification_channel_tethering_status" msgid="7030733422705019001">"Stav hotspotu a tetheringu"</string>
-    <string name="no_upstream_notification_title" msgid="2052743091868702475"></string>
-    <string name="no_upstream_notification_message" msgid="6932020551635470134"></string>
-    <string name="no_upstream_notification_disable_button" msgid="8836277213343697023"></string>
-    <string name="upstream_roaming_notification_title" msgid="8614262557406849762"></string>
-    <string name="upstream_roaming_notification_message" msgid="5999740876323106599"></string>
+    <string name="tethered_notification_title" msgid="6426563586025792944">"Tethering nebo hotspot je aktivní"</string>
+    <string name="tethered_notification_message" msgid="64800879503420696">"Klepnutím zahájíte nastavení."</string>
+    <string name="disable_tether_notification_title" msgid="3004509127903564191">"Tethering je zakázán"</string>
+    <string name="disable_tether_notification_message" msgid="6717523799293901476">"O podrobnosti požádejte administrátora"</string>
+    <string name="notification_channel_tethering_status" msgid="2663463891530932727">"Stav hotspotu a tetheringu"</string>
+    <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+    <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+    <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+    <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+    <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
 </resources>
diff --git a/Tethering/res/values-da/strings.xml b/Tethering/res/values-da/strings.xml
index dddf097..f413e70 100644
--- a/Tethering/res/values-da/strings.xml
+++ b/Tethering/res/values-da/strings.xml
@@ -16,14 +16,14 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="tethered_notification_title" msgid="5350162111436634622">"Netdeling eller hotspot er aktiveret"</string>
-    <string name="tethered_notification_message" msgid="2338023450330652098">"Tryk for at konfigurere."</string>
-    <string name="disable_tether_notification_title" msgid="3183576627492925522">"Netdeling er deaktiveret"</string>
-    <string name="disable_tether_notification_message" msgid="6655882039707534929">"Kontakt din administrator for at få oplysninger"</string>
-    <string name="notification_channel_tethering_status" msgid="7030733422705019001">"Status for hotspot og netdeling"</string>
-    <string name="no_upstream_notification_title" msgid="2052743091868702475"></string>
-    <string name="no_upstream_notification_message" msgid="6932020551635470134"></string>
-    <string name="no_upstream_notification_disable_button" msgid="8836277213343697023"></string>
-    <string name="upstream_roaming_notification_title" msgid="8614262557406849762"></string>
-    <string name="upstream_roaming_notification_message" msgid="5999740876323106599"></string>
+    <string name="tethered_notification_title" msgid="6426563586025792944">"Netdeling eller hotspot er aktivt"</string>
+    <string name="tethered_notification_message" msgid="64800879503420696">"Tryk for at konfigurere."</string>
+    <string name="disable_tether_notification_title" msgid="3004509127903564191">"Netdeling er deaktiveret"</string>
+    <string name="disable_tether_notification_message" msgid="6717523799293901476">"Kontakt din administrator for at få oplysninger"</string>
+    <string name="notification_channel_tethering_status" msgid="2663463891530932727">"Status for hotspot og netdeling"</string>
+    <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+    <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+    <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+    <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+    <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
 </resources>
diff --git a/Tethering/res/values-de/strings.xml b/Tethering/res/values-de/strings.xml
index ab7b8c9..f057d78 100644
--- a/Tethering/res/values-de/strings.xml
+++ b/Tethering/res/values-de/strings.xml
@@ -16,14 +16,14 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="tethered_notification_title" msgid="5350162111436634622">"Tethering oder Hotspot aktiv"</string>
-    <string name="tethered_notification_message" msgid="2338023450330652098">"Zum Einrichten tippen."</string>
-    <string name="disable_tether_notification_title" msgid="3183576627492925522">"Tethering ist deaktiviert"</string>
-    <string name="disable_tether_notification_message" msgid="6655882039707534929">"Bitte wende dich für weitere Informationen an den Administrator"</string>
-    <string name="notification_channel_tethering_status" msgid="7030733422705019001">"Hotspot- und Tethering-Status"</string>
-    <string name="no_upstream_notification_title" msgid="2052743091868702475"></string>
-    <string name="no_upstream_notification_message" msgid="6932020551635470134"></string>
-    <string name="no_upstream_notification_disable_button" msgid="8836277213343697023"></string>
-    <string name="upstream_roaming_notification_title" msgid="8614262557406849762"></string>
-    <string name="upstream_roaming_notification_message" msgid="5999740876323106599"></string>
+    <string name="tethered_notification_title" msgid="6426563586025792944">"Tethering oder Hotspot aktiv"</string>
+    <string name="tethered_notification_message" msgid="64800879503420696">"Zum Einrichten tippen."</string>
+    <string name="disable_tether_notification_title" msgid="3004509127903564191">"Tethering ist deaktiviert"</string>
+    <string name="disable_tether_notification_message" msgid="6717523799293901476">"Bitte wende dich für weitere Informationen an den Administrator"</string>
+    <string name="notification_channel_tethering_status" msgid="2663463891530932727">"Hotspot- und Tethering-Status"</string>
+    <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+    <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+    <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+    <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+    <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
 </resources>
diff --git a/Tethering/res/values-el/strings.xml b/Tethering/res/values-el/strings.xml
index 4ed3ec5..b3c986b 100644
--- a/Tethering/res/values-el/strings.xml
+++ b/Tethering/res/values-el/strings.xml
@@ -16,14 +16,14 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="tethered_notification_title" msgid="5350162111436634622">"Ενεργή σύνδεση ή ενεργό σημείο πρόσβασης Wi-Fi"</string>
-    <string name="tethered_notification_message" msgid="2338023450330652098">"Πατήστε για ρύθμιση."</string>
-    <string name="disable_tether_notification_title" msgid="3183576627492925522">"Η σύνδεση είναι απενεργοποιημένη"</string>
-    <string name="disable_tether_notification_message" msgid="6655882039707534929">"Επικοινωνήστε με τον διαχειριστή για λεπτομέρειες"</string>
-    <string name="notification_channel_tethering_status" msgid="7030733422705019001">"Κατάσταση σημείου πρόσβασης Wi-Fi και σύνδεσης"</string>
-    <string name="no_upstream_notification_title" msgid="2052743091868702475"></string>
-    <string name="no_upstream_notification_message" msgid="6932020551635470134"></string>
-    <string name="no_upstream_notification_disable_button" msgid="8836277213343697023"></string>
-    <string name="upstream_roaming_notification_title" msgid="8614262557406849762"></string>
-    <string name="upstream_roaming_notification_message" msgid="5999740876323106599"></string>
+    <string name="tethered_notification_title" msgid="6426563586025792944">"Πρόσδεση ή σύνδεση σημείου πρόσβασης ενεργή"</string>
+    <string name="tethered_notification_message" msgid="64800879503420696">"Πατήστε για ρύθμιση."</string>
+    <string name="disable_tether_notification_title" msgid="3004509127903564191">"Η σύνδεση είναι απενεργοποιημένη"</string>
+    <string name="disable_tether_notification_message" msgid="6717523799293901476">"Επικοινωνήστε με τον διαχειριστή σας για λεπτομέρειες"</string>
+    <string name="notification_channel_tethering_status" msgid="2663463891530932727">"Κατάσταση σημείου πρόσβασης Wi-Fi και σύνδεσης"</string>
+    <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+    <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+    <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+    <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+    <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
 </resources>
diff --git a/Tethering/res/values-en-rAU/strings.xml b/Tethering/res/values-en-rAU/strings.xml
index 2dc7689..769e012 100644
--- a/Tethering/res/values-en-rAU/strings.xml
+++ b/Tethering/res/values-en-rAU/strings.xml
@@ -16,14 +16,14 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="tethered_notification_title" msgid="5350162111436634622">"Tethering or hotspot active"</string>
-    <string name="tethered_notification_message" msgid="2338023450330652098">"Tap to set up."</string>
-    <string name="disable_tether_notification_title" msgid="3183576627492925522">"Tethering is disabled"</string>
-    <string name="disable_tether_notification_message" msgid="6655882039707534929">"Contact your admin for details"</string>
-    <string name="notification_channel_tethering_status" msgid="7030733422705019001">"Hotspot and tethering status"</string>
-    <string name="no_upstream_notification_title" msgid="2052743091868702475"></string>
-    <string name="no_upstream_notification_message" msgid="6932020551635470134"></string>
-    <string name="no_upstream_notification_disable_button" msgid="8836277213343697023"></string>
-    <string name="upstream_roaming_notification_title" msgid="8614262557406849762"></string>
-    <string name="upstream_roaming_notification_message" msgid="5999740876323106599"></string>
+    <string name="tethered_notification_title" msgid="6426563586025792944">"Tethering or hotspot active"</string>
+    <string name="tethered_notification_message" msgid="64800879503420696">"Tap to set up."</string>
+    <string name="disable_tether_notification_title" msgid="3004509127903564191">"Tethering is disabled"</string>
+    <string name="disable_tether_notification_message" msgid="6717523799293901476">"Contact your admin for details"</string>
+    <string name="notification_channel_tethering_status" msgid="2663463891530932727">"Hotspot and tethering status"</string>
+    <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+    <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+    <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+    <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+    <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
 </resources>
diff --git a/Tethering/res/values-en-rCA/strings.xml b/Tethering/res/values-en-rCA/strings.xml
index 066cd82..769e012 100644
--- a/Tethering/res/values-en-rCA/strings.xml
+++ b/Tethering/res/values-en-rCA/strings.xml
@@ -16,14 +16,14 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="tethered_notification_title" msgid="5350162111436634622">"Tethering or hotspot active"</string>
-    <string name="tethered_notification_message" msgid="2338023450330652098">"Tap to set up."</string>
-    <string name="disable_tether_notification_title" msgid="3183576627492925522">"Tethering is disabled"</string>
-    <string name="disable_tether_notification_message" msgid="6655882039707534929">"Contact your admin for details"</string>
-    <string name="notification_channel_tethering_status" msgid="7030733422705019001">"Hotspot &amp; tethering status"</string>
-    <string name="no_upstream_notification_title" msgid="2052743091868702475"></string>
-    <string name="no_upstream_notification_message" msgid="6932020551635470134"></string>
-    <string name="no_upstream_notification_disable_button" msgid="8836277213343697023"></string>
-    <string name="upstream_roaming_notification_title" msgid="8614262557406849762"></string>
-    <string name="upstream_roaming_notification_message" msgid="5999740876323106599"></string>
+    <string name="tethered_notification_title" msgid="6426563586025792944">"Tethering or hotspot active"</string>
+    <string name="tethered_notification_message" msgid="64800879503420696">"Tap to set up."</string>
+    <string name="disable_tether_notification_title" msgid="3004509127903564191">"Tethering is disabled"</string>
+    <string name="disable_tether_notification_message" msgid="6717523799293901476">"Contact your admin for details"</string>
+    <string name="notification_channel_tethering_status" msgid="2663463891530932727">"Hotspot and tethering status"</string>
+    <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+    <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+    <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+    <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+    <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
 </resources>
diff --git a/Tethering/res/values-en-rGB/strings.xml b/Tethering/res/values-en-rGB/strings.xml
index 2dc7689..769e012 100644
--- a/Tethering/res/values-en-rGB/strings.xml
+++ b/Tethering/res/values-en-rGB/strings.xml
@@ -16,14 +16,14 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="tethered_notification_title" msgid="5350162111436634622">"Tethering or hotspot active"</string>
-    <string name="tethered_notification_message" msgid="2338023450330652098">"Tap to set up."</string>
-    <string name="disable_tether_notification_title" msgid="3183576627492925522">"Tethering is disabled"</string>
-    <string name="disable_tether_notification_message" msgid="6655882039707534929">"Contact your admin for details"</string>
-    <string name="notification_channel_tethering_status" msgid="7030733422705019001">"Hotspot and tethering status"</string>
-    <string name="no_upstream_notification_title" msgid="2052743091868702475"></string>
-    <string name="no_upstream_notification_message" msgid="6932020551635470134"></string>
-    <string name="no_upstream_notification_disable_button" msgid="8836277213343697023"></string>
-    <string name="upstream_roaming_notification_title" msgid="8614262557406849762"></string>
-    <string name="upstream_roaming_notification_message" msgid="5999740876323106599"></string>
+    <string name="tethered_notification_title" msgid="6426563586025792944">"Tethering or hotspot active"</string>
+    <string name="tethered_notification_message" msgid="64800879503420696">"Tap to set up."</string>
+    <string name="disable_tether_notification_title" msgid="3004509127903564191">"Tethering is disabled"</string>
+    <string name="disable_tether_notification_message" msgid="6717523799293901476">"Contact your admin for details"</string>
+    <string name="notification_channel_tethering_status" msgid="2663463891530932727">"Hotspot and tethering status"</string>
+    <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+    <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+    <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+    <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+    <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
 </resources>
diff --git a/Tethering/res/values-en-rIN/strings.xml b/Tethering/res/values-en-rIN/strings.xml
index 2dc7689..769e012 100644
--- a/Tethering/res/values-en-rIN/strings.xml
+++ b/Tethering/res/values-en-rIN/strings.xml
@@ -16,14 +16,14 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="tethered_notification_title" msgid="5350162111436634622">"Tethering or hotspot active"</string>
-    <string name="tethered_notification_message" msgid="2338023450330652098">"Tap to set up."</string>
-    <string name="disable_tether_notification_title" msgid="3183576627492925522">"Tethering is disabled"</string>
-    <string name="disable_tether_notification_message" msgid="6655882039707534929">"Contact your admin for details"</string>
-    <string name="notification_channel_tethering_status" msgid="7030733422705019001">"Hotspot and tethering status"</string>
-    <string name="no_upstream_notification_title" msgid="2052743091868702475"></string>
-    <string name="no_upstream_notification_message" msgid="6932020551635470134"></string>
-    <string name="no_upstream_notification_disable_button" msgid="8836277213343697023"></string>
-    <string name="upstream_roaming_notification_title" msgid="8614262557406849762"></string>
-    <string name="upstream_roaming_notification_message" msgid="5999740876323106599"></string>
+    <string name="tethered_notification_title" msgid="6426563586025792944">"Tethering or hotspot active"</string>
+    <string name="tethered_notification_message" msgid="64800879503420696">"Tap to set up."</string>
+    <string name="disable_tether_notification_title" msgid="3004509127903564191">"Tethering is disabled"</string>
+    <string name="disable_tether_notification_message" msgid="6717523799293901476">"Contact your admin for details"</string>
+    <string name="notification_channel_tethering_status" msgid="2663463891530932727">"Hotspot and tethering status"</string>
+    <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+    <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+    <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+    <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+    <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
 </resources>
diff --git a/Tethering/res/values-en-rXC/strings.xml b/Tethering/res/values-en-rXC/strings.xml
index a83caac..f1674be 100644
--- a/Tethering/res/values-en-rXC/strings.xml
+++ b/Tethering/res/values-en-rXC/strings.xml
@@ -16,14 +16,14 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="tethered_notification_title" msgid="5350162111436634622">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‎‏‎‏‎‎‎‏‏‏‏‏‏‏‎‎‏‏‎‎‎‎‎‎‏‏‎‎‎‎‎‎‎‏‏‏‎‏‏‏‎‏‏‏‏‎‎‏‏‏‎‎‏‏‏‏‏‏‏‏‎‎Tethering or hotspot active‎‏‎‎‏‎"</string>
-    <string name="tethered_notification_message" msgid="2338023450330652098">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‎‎‎‎‎‎‏‏‏‎‎‏‎‎‏‎‏‎‎‏‏‏‏‏‏‎‎‎‎‏‏‎‏‎‎‎‏‎‏‏‏‏‏‎‏‏‏‎‏‏‏‎‏‏‏‎‎‎‎‏‎‎Tap to set up.‎‏‎‎‏‎"</string>
-    <string name="disable_tether_notification_title" msgid="3183576627492925522">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‎‏‏‎‎‎‎‏‎‏‏‏‎‎‏‎‏‎‏‏‎‎‎‎‏‎‎‏‏‎‏‎‎‏‏‎‎‏‏‏‏‎‎‏‏‏‎‏‏‎‎‎‎‎‏‎‏‎‎‏‎‎Tethering is disabled‎‏‎‎‏‎"</string>
-    <string name="disable_tether_notification_message" msgid="6655882039707534929">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‏‏‏‎‎‎‏‎‏‏‏‏‎‎‏‏‏‎‎‎‏‎‏‏‏‎‎‏‏‎‏‏‎‎‎‏‏‏‎‏‎‏‎‏‏‏‎‎‎‎‏‏‎‎‏‎‏‎‎‎‏‎Contact your admin for details‎‏‎‎‏‎"</string>
-    <string name="notification_channel_tethering_status" msgid="7030733422705019001">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‎‎‎‎‏‏‎‎‏‎‎‏‎‎‎‏‎‏‏‏‎‏‏‎‎‏‏‎‎‎‎‏‎‏‎‏‎‏‏‎‎‏‏‎‎‏‎‎‎‏‎‎‎‎‏‏‏‏‎‎‏‎Hotspot &amp; tethering status‎‏‎‎‏‎"</string>
-    <string name="no_upstream_notification_title" msgid="2052743091868702475"></string>
-    <string name="no_upstream_notification_message" msgid="6932020551635470134"></string>
-    <string name="no_upstream_notification_disable_button" msgid="8836277213343697023"></string>
-    <string name="upstream_roaming_notification_title" msgid="8614262557406849762"></string>
-    <string name="upstream_roaming_notification_message" msgid="5999740876323106599"></string>
+    <string name="tethered_notification_title" msgid="6426563586025792944">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‏‏‎‎‏‎‎‏‎‏‏‏‏‏‎‏‏‏‏‎‏‏‎‎‎‏‎‎‎‎‎‏‏‎‏‏‏‏‎‎‎‎‏‏‏‎‎‏‎‎‏‎‏‏‎‏‏‎‎‎‎‎Tethering or hotspot active‎‏‎‎‏‎"</string>
+    <string name="tethered_notification_message" msgid="64800879503420696">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‎‎‎‏‏‏‎‎‏‏‎‎‎‏‏‏‎‎‎‎‎‎‎‏‏‏‎‎‏‏‏‏‎‏‏‏‏‏‎‏‏‏‏‎‎‏‎‏‏‎‏‎‎‎‏‏‎‎‎‎Tap to set up.‎‏‎‎‏‎"</string>
+    <string name="disable_tether_notification_title" msgid="3004509127903564191">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‎‏‎‎‏‏‎‏‏‎‎‏‎‎‎‏‎‏‎‎‏‎‎‏‎‎‎‏‎‎‎‎‏‏‏‎‏‎‎‎‏‏‏‎‎‎‏‎‏‎‎‎‏‏‎‎‏‏‏‏‏‎Tethering is disabled‎‏‎‎‏‎"</string>
+    <string name="disable_tether_notification_message" msgid="6717523799293901476">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‏‏‏‎‏‎‎‏‏‏‎‎‏‎‏‏‏‎‎‎‎‎‏‎‎‏‏‎‏‏‏‏‏‎‎‏‏‎‎‏‎‎‏‎‏‎‎‏‏‏‎‏‎‏‎‏‎‎‏‎‎‎Contact your admin for details‎‏‎‎‏‎"</string>
+    <string name="notification_channel_tethering_status" msgid="2663463891530932727">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‎‎‏‎‎‏‏‏‏‎‏‏‎‏‎‎‎‎‏‏‎‎‏‎‎‏‏‎‎‎‎‏‏‎‏‏‎‏‏‎‎‎‏‏‏‏‏‏‎‎‎‎‏‏‏‏‏‎‏‏‏‎Hotspot &amp; tethering status‎‏‎‎‏‎"</string>
+    <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+    <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+    <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+    <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+    <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
 </resources>
diff --git a/Tethering/res/values-es-rUS/strings.xml b/Tethering/res/values-es-rUS/strings.xml
index 69bd4e7..63689f4 100644
--- a/Tethering/res/values-es-rUS/strings.xml
+++ b/Tethering/res/values-es-rUS/strings.xml
@@ -16,14 +16,14 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="tethered_notification_title" msgid="5350162111436634622">"Conexión mediante dispositivo móvil o hotspot activos"</string>
-    <string name="tethered_notification_message" msgid="2338023450330652098">"Presiona para configurar esta opción."</string>
-    <string name="disable_tether_notification_title" msgid="3183576627492925522">"Se inhabilitó la conexión mediante dispositivo móvil"</string>
-    <string name="disable_tether_notification_message" msgid="6655882039707534929">"Para obtener más información, comunícate con el administrador"</string>
-    <string name="notification_channel_tethering_status" msgid="7030733422705019001">"Estado del hotspot y de la conexión mediante dispositivo portátil"</string>
-    <string name="no_upstream_notification_title" msgid="2052743091868702475"></string>
-    <string name="no_upstream_notification_message" msgid="6932020551635470134"></string>
-    <string name="no_upstream_notification_disable_button" msgid="8836277213343697023"></string>
-    <string name="upstream_roaming_notification_title" msgid="8614262557406849762"></string>
-    <string name="upstream_roaming_notification_message" msgid="5999740876323106599"></string>
+    <string name="tethered_notification_title" msgid="6426563586025792944">"Conexión a red o hotspot conectados"</string>
+    <string name="tethered_notification_message" msgid="64800879503420696">"Presiona para configurar esta opción."</string>
+    <string name="disable_tether_notification_title" msgid="3004509127903564191">"Se inhabilitó la conexión mediante dispositivo portátil"</string>
+    <string name="disable_tether_notification_message" msgid="6717523799293901476">"Para obtener más información, comunícate con el administrador"</string>
+    <string name="notification_channel_tethering_status" msgid="2663463891530932727">"Estado del hotspot y la conexión mediante dispositivo portátil"</string>
+    <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+    <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+    <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+    <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+    <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
 </resources>
diff --git a/Tethering/res/values-es/strings.xml b/Tethering/res/values-es/strings.xml
index 6bef387..9a34ed5 100644
--- a/Tethering/res/values-es/strings.xml
+++ b/Tethering/res/values-es/strings.xml
@@ -16,14 +16,14 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="tethered_notification_title" msgid="5350162111436634622">"Conexión compartida o punto de acceso activos"</string>
-    <string name="tethered_notification_message" msgid="2338023450330652098">"Toca para configurarla."</string>
-    <string name="disable_tether_notification_title" msgid="3183576627492925522">"La conexión compartida está inhabilitada"</string>
-    <string name="disable_tether_notification_message" msgid="6655882039707534929">"Ponte en contacto con el administrador para obtener más información"</string>
-    <string name="notification_channel_tethering_status" msgid="7030733422705019001">"Estado del punto de acceso y la conexión compartida"</string>
-    <string name="no_upstream_notification_title" msgid="2052743091868702475"></string>
-    <string name="no_upstream_notification_message" msgid="6932020551635470134"></string>
-    <string name="no_upstream_notification_disable_button" msgid="8836277213343697023"></string>
-    <string name="upstream_roaming_notification_title" msgid="8614262557406849762"></string>
-    <string name="upstream_roaming_notification_message" msgid="5999740876323106599"></string>
+    <string name="tethered_notification_title" msgid="6426563586025792944">"Conexión compartida o punto de acceso activos"</string>
+    <string name="tethered_notification_message" msgid="64800879503420696">"Toca para configurar."</string>
+    <string name="disable_tether_notification_title" msgid="3004509127903564191">"La conexión compartida está inhabilitada"</string>
+    <string name="disable_tether_notification_message" msgid="6717523799293901476">"Solicita más información a tu administrador"</string>
+    <string name="notification_channel_tethering_status" msgid="2663463891530932727">"Estado del punto de acceso y de la conexión compartida"</string>
+    <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+    <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+    <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+    <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+    <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
 </resources>
diff --git a/Tethering/res/values-et/strings.xml b/Tethering/res/values-et/strings.xml
index 68088ce..0970341 100644
--- a/Tethering/res/values-et/strings.xml
+++ b/Tethering/res/values-et/strings.xml
@@ -16,14 +16,14 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="tethered_notification_title" msgid="5350162111436634622">"Jagamine või kuumkoht on aktiivne"</string>
-    <string name="tethered_notification_message" msgid="2338023450330652098">"Puudutage seadistamiseks."</string>
-    <string name="disable_tether_notification_title" msgid="3183576627492925522">"Jagamine on keelatud"</string>
-    <string name="disable_tether_notification_message" msgid="6655882039707534929">"Lisateabe saamiseks võtke ühendust oma administraatoriga"</string>
-    <string name="notification_channel_tethering_status" msgid="7030733422705019001">"Kuumkoha ja jagamise olek"</string>
-    <string name="no_upstream_notification_title" msgid="2052743091868702475"></string>
-    <string name="no_upstream_notification_message" msgid="6932020551635470134"></string>
-    <string name="no_upstream_notification_disable_button" msgid="8836277213343697023"></string>
-    <string name="upstream_roaming_notification_title" msgid="8614262557406849762"></string>
-    <string name="upstream_roaming_notification_message" msgid="5999740876323106599"></string>
+    <string name="tethered_notification_title" msgid="6426563586025792944">"Jagamine või kuumkoht on aktiivne"</string>
+    <string name="tethered_notification_message" msgid="64800879503420696">"Puudutage seadistamiseks."</string>
+    <string name="disable_tether_notification_title" msgid="3004509127903564191">"Jagamine on keelatud"</string>
+    <string name="disable_tether_notification_message" msgid="6717523799293901476">"Lisateabe saamiseks võtke ühendust oma administraatoriga"</string>
+    <string name="notification_channel_tethering_status" msgid="2663463891530932727">"Kuumkoha ja jagamise olek"</string>
+    <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+    <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+    <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+    <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+    <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
 </resources>
diff --git a/Tethering/res/values-eu/strings.xml b/Tethering/res/values-eu/strings.xml
index 37b35a8..632019e 100644
--- a/Tethering/res/values-eu/strings.xml
+++ b/Tethering/res/values-eu/strings.xml
@@ -16,14 +16,14 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="tethered_notification_title" msgid="5350162111436634622">"Konexioa partekatzeko aukera edo wifi-gunea aktibo dago"</string>
-    <string name="tethered_notification_message" msgid="2338023450330652098">"Konfiguratzeko, sakatu hau."</string>
-    <string name="disable_tether_notification_title" msgid="3183576627492925522">"Konexioa partekatzeko aukera desgaituta dago"</string>
-    <string name="disable_tether_notification_message" msgid="6655882039707534929">"Xehetasunak lortzeko, jarri administratzailearekin harremanetan"</string>
-    <string name="notification_channel_tethering_status" msgid="7030733422705019001">"Wifi-gunearen eta konexioa partekatzeko aukeraren egoera"</string>
-    <string name="no_upstream_notification_title" msgid="2052743091868702475"></string>
-    <string name="no_upstream_notification_message" msgid="6932020551635470134"></string>
-    <string name="no_upstream_notification_disable_button" msgid="8836277213343697023"></string>
-    <string name="upstream_roaming_notification_title" msgid="8614262557406849762"></string>
-    <string name="upstream_roaming_notification_message" msgid="5999740876323106599"></string>
+    <string name="tethered_notification_title" msgid="6426563586025792944">"Konexioa partekatzea edo wifi-gunea aktibo dago"</string>
+    <string name="tethered_notification_message" msgid="64800879503420696">"Sakatu konfiguratzeko."</string>
+    <string name="disable_tether_notification_title" msgid="3004509127903564191">"Desgaituta dago konexioa partekatzeko aukera"</string>
+    <string name="disable_tether_notification_message" msgid="6717523799293901476">"Xehetasunak lortzeko, jarri administratzailearekin harremanetan"</string>
+    <string name="notification_channel_tethering_status" msgid="2663463891530932727">"Wifi-gunearen eta konexioa partekatzeko eginbidearen egoera"</string>
+    <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+    <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+    <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+    <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+    <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
 </resources>
diff --git a/Tethering/res/values-fa/strings.xml b/Tethering/res/values-fa/strings.xml
index d7f2543..2e21c85 100644
--- a/Tethering/res/values-fa/strings.xml
+++ b/Tethering/res/values-fa/strings.xml
@@ -16,14 +16,14 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="tethered_notification_title" msgid="5350162111436634622">"اشتراک‌گذاری اینترنت یا نقطه اتصال فعال است"</string>
-    <string name="tethered_notification_message" msgid="2338023450330652098">"برای راه‌اندازی، ضربه بزنید."</string>
-    <string name="disable_tether_notification_title" msgid="3183576627492925522">"اشتراک‌گذاری اینترنت غیرفعال است"</string>
-    <string name="disable_tether_notification_message" msgid="6655882039707534929">"برای جزئیات، با سرپرستتان تماس بگیرید"</string>
-    <string name="notification_channel_tethering_status" msgid="7030733422705019001">"وضعیت نقطه اتصال و اشتراک‌گذاری اینترنت"</string>
-    <string name="no_upstream_notification_title" msgid="2052743091868702475"></string>
-    <string name="no_upstream_notification_message" msgid="6932020551635470134"></string>
-    <string name="no_upstream_notification_disable_button" msgid="8836277213343697023"></string>
-    <string name="upstream_roaming_notification_title" msgid="8614262557406849762"></string>
-    <string name="upstream_roaming_notification_message" msgid="5999740876323106599"></string>
+    <string name="tethered_notification_title" msgid="6426563586025792944">"اشتراک‌گذاری اینترنت یا نقطه اتصال فعال"</string>
+    <string name="tethered_notification_message" msgid="64800879503420696">"برای راه‌اندازی ضربه بزنید."</string>
+    <string name="disable_tether_notification_title" msgid="3004509127903564191">"اشتراک‌گذاری اینترنت غیرفعال است"</string>
+    <string name="disable_tether_notification_message" msgid="6717523799293901476">"برای جزئیات، با سرپرستتان تماس بگیرید"</string>
+    <string name="notification_channel_tethering_status" msgid="2663463891530932727">"وضعیت نقطه اتصال و اشتراک‌گذاری اینترنت"</string>
+    <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+    <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+    <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+    <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+    <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
 </resources>
diff --git a/Tethering/res/values-fi/strings.xml b/Tethering/res/values-fi/strings.xml
index 4bf09fec..413db3f 100644
--- a/Tethering/res/values-fi/strings.xml
+++ b/Tethering/res/values-fi/strings.xml
@@ -16,14 +16,14 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="tethered_notification_title" msgid="5350162111436634622">"Puhelimen käyttäminen modeemina tai hotspot käytössä"</string>
-    <string name="tethered_notification_message" msgid="2338023450330652098">"Ota käyttöön napauttamalla."</string>
-    <string name="disable_tether_notification_title" msgid="3183576627492925522">"Puhelimen käyttäminen modeemina on poistettu käytöstä"</string>
-    <string name="disable_tether_notification_message" msgid="6655882039707534929">"Pyydä lisätietoa järjestelmänvalvojalta"</string>
-    <string name="notification_channel_tethering_status" msgid="7030733422705019001">"Hotspotin ja modeemina toimivan puhelimen tila"</string>
-    <string name="no_upstream_notification_title" msgid="2052743091868702475"></string>
-    <string name="no_upstream_notification_message" msgid="6932020551635470134"></string>
-    <string name="no_upstream_notification_disable_button" msgid="8836277213343697023"></string>
-    <string name="upstream_roaming_notification_title" msgid="8614262557406849762"></string>
-    <string name="upstream_roaming_notification_message" msgid="5999740876323106599"></string>
+    <string name="tethered_notification_title" msgid="6426563586025792944">"Yhteyden jakaminen tai hotspot käytössä"</string>
+    <string name="tethered_notification_message" msgid="64800879503420696">"Ota käyttöön napauttamalla."</string>
+    <string name="disable_tether_notification_title" msgid="3004509127903564191">"Yhteyden jakaminen on poistettu käytöstä"</string>
+    <string name="disable_tether_notification_message" msgid="6717523799293901476">"Pyydä lisätietoja järjestelmänvalvojalta"</string>
+    <string name="notification_channel_tethering_status" msgid="2663463891530932727">"Hotspotin ja yhteyden jakamisen tila"</string>
+    <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+    <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+    <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+    <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+    <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
 </resources>
diff --git a/Tethering/res/values-fr-rCA/strings.xml b/Tethering/res/values-fr-rCA/strings.xml
index 66b4684..eb2e4ba 100644
--- a/Tethering/res/values-fr-rCA/strings.xml
+++ b/Tethering/res/values-fr-rCA/strings.xml
@@ -16,14 +16,14 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="tethered_notification_title" msgid="5350162111436634622">"Partage de connexion ou point d\'accès sans fil activé"</string>
-    <string name="tethered_notification_message" msgid="2338023450330652098">"Touchez pour configurer."</string>
-    <string name="disable_tether_notification_title" msgid="3183576627492925522">"Le partage de connexion est désactivé"</string>
-    <string name="disable_tether_notification_message" msgid="6655882039707534929">"Communiquez avec votre administrateur pour obtenir plus de détails"</string>
-    <string name="notification_channel_tethering_status" msgid="7030733422705019001">"État du point d\'accès sans fil et du partage de connexion"</string>
-    <string name="no_upstream_notification_title" msgid="2052743091868702475"></string>
-    <string name="no_upstream_notification_message" msgid="6932020551635470134"></string>
-    <string name="no_upstream_notification_disable_button" msgid="8836277213343697023"></string>
-    <string name="upstream_roaming_notification_title" msgid="8614262557406849762"></string>
-    <string name="upstream_roaming_notification_message" msgid="5999740876323106599"></string>
+    <string name="tethered_notification_title" msgid="6426563586025792944">"Partage de connexion ou point d\'accès sans fil activé"</string>
+    <string name="tethered_notification_message" msgid="64800879503420696">"Touchez pour configurer."</string>
+    <string name="disable_tether_notification_title" msgid="3004509127903564191">"Le partage de connexion est désactivé"</string>
+    <string name="disable_tether_notification_message" msgid="6717523799293901476">"Communiquez avec votre administrateur pour obtenir plus de détails"</string>
+    <string name="notification_channel_tethering_status" msgid="2663463891530932727">"Point d\'accès et partage de connexion"</string>
+    <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+    <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+    <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+    <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+    <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
 </resources>
diff --git a/Tethering/res/values-fr/strings.xml b/Tethering/res/values-fr/strings.xml
index 9440d95..22259c5 100644
--- a/Tethering/res/values-fr/strings.xml
+++ b/Tethering/res/values-fr/strings.xml
@@ -16,14 +16,14 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="tethered_notification_title" msgid="5350162111436634622">"Partage de connexion ou point d\'accès activé"</string>
-    <string name="tethered_notification_message" msgid="2338023450330652098">"Appuyez pour configurer."</string>
-    <string name="disable_tether_notification_title" msgid="3183576627492925522">"Le partage de connexion est désactivé"</string>
-    <string name="disable_tether_notification_message" msgid="6655882039707534929">"Pour en savoir plus, contactez votre administrateur"</string>
-    <string name="notification_channel_tethering_status" msgid="7030733422705019001">"État du point d\'accès et du partage de connexion"</string>
-    <string name="no_upstream_notification_title" msgid="2052743091868702475"></string>
-    <string name="no_upstream_notification_message" msgid="6932020551635470134"></string>
-    <string name="no_upstream_notification_disable_button" msgid="8836277213343697023"></string>
-    <string name="upstream_roaming_notification_title" msgid="8614262557406849762"></string>
-    <string name="upstream_roaming_notification_message" msgid="5999740876323106599"></string>
+    <string name="tethered_notification_title" msgid="6426563586025792944">"Partage de connexion ou point d\'accès activé"</string>
+    <string name="tethered_notification_message" msgid="64800879503420696">"Appuyez pour effectuer la configuration."</string>
+    <string name="disable_tether_notification_title" msgid="3004509127903564191">"Le partage de connexion est désactivé"</string>
+    <string name="disable_tether_notification_message" msgid="6717523799293901476">"Pour en savoir plus, contactez votre administrateur"</string>
+    <string name="notification_channel_tethering_status" msgid="2663463891530932727">"État du point d\'accès et du partage de connexion"</string>
+    <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+    <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+    <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+    <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+    <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
 </resources>
diff --git a/Tethering/res/values-gl/strings.xml b/Tethering/res/values-gl/strings.xml
index 74bb7f2..ded82fc 100644
--- a/Tethering/res/values-gl/strings.xml
+++ b/Tethering/res/values-gl/strings.xml
@@ -16,14 +16,14 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="tethered_notification_title" msgid="5350162111436634622">"Conexión compartida ou zona wifi activada"</string>
-    <string name="tethered_notification_message" msgid="2338023450330652098">"Toca para configurar."</string>
-    <string name="disable_tether_notification_title" msgid="3183576627492925522">"A conexión compartida está desactivada"</string>
-    <string name="disable_tether_notification_message" msgid="6655882039707534929">"Contacta co administrador para obter información"</string>
-    <string name="notification_channel_tethering_status" msgid="7030733422705019001">"Estado da zona wifi e da conexión compartida"</string>
-    <string name="no_upstream_notification_title" msgid="2052743091868702475"></string>
-    <string name="no_upstream_notification_message" msgid="6932020551635470134"></string>
-    <string name="no_upstream_notification_disable_button" msgid="8836277213343697023"></string>
-    <string name="upstream_roaming_notification_title" msgid="8614262557406849762"></string>
-    <string name="upstream_roaming_notification_message" msgid="5999740876323106599"></string>
+    <string name="tethered_notification_title" msgid="6426563586025792944">"Conexión compartida ou zona wifi activada"</string>
+    <string name="tethered_notification_message" msgid="64800879503420696">"Toca para configurar."</string>
+    <string name="disable_tether_notification_title" msgid="3004509127903564191">"A conexión compartida está desactivada"</string>
+    <string name="disable_tether_notification_message" msgid="6717523799293901476">"Contacta co administrador para obter información"</string>
+    <string name="notification_channel_tethering_status" msgid="2663463891530932727">"Estado da zona wifi e da conexión compartida"</string>
+    <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+    <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+    <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+    <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+    <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
 </resources>
diff --git a/Tethering/res/values-gu/strings.xml b/Tethering/res/values-gu/strings.xml
index c463499..7cbbc2d 100644
--- a/Tethering/res/values-gu/strings.xml
+++ b/Tethering/res/values-gu/strings.xml
@@ -16,14 +16,14 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="tethered_notification_title" msgid="5350162111436634622">"ઇન્ટરનેટ શેર કરવાની સુવિધા અથવા હૉટસ્પૉટ સક્રિય છે"</string>
-    <string name="tethered_notification_message" msgid="2338023450330652098">"સેટઅપ કરવા માટે ટૅપ કરો."</string>
-    <string name="disable_tether_notification_title" msgid="3183576627492925522">"ઇન્ટરનેટ શેર કરવાની સુવિધા બંધ કરી છે"</string>
-    <string name="disable_tether_notification_message" msgid="6655882039707534929">"વિગતો માટે તમારા ઍડમિનનો સંપર્ક કરો"</string>
-    <string name="notification_channel_tethering_status" msgid="7030733422705019001">"હૉટસ્પૉટ અને ઇન્ટરનેટ શેર કરવાની સુવિધાનું સ્ટેટસ"</string>
-    <string name="no_upstream_notification_title" msgid="2052743091868702475"></string>
-    <string name="no_upstream_notification_message" msgid="6932020551635470134"></string>
-    <string name="no_upstream_notification_disable_button" msgid="8836277213343697023"></string>
-    <string name="upstream_roaming_notification_title" msgid="8614262557406849762"></string>
-    <string name="upstream_roaming_notification_message" msgid="5999740876323106599"></string>
+    <string name="tethered_notification_title" msgid="6426563586025792944">"ઇન્ટરનેટ શેર કરવાની સુવિધા અથવા હૉટસ્પૉટ સક્રિય છે"</string>
+    <string name="tethered_notification_message" msgid="64800879503420696">"સેટઅપ કરવા માટે ટૅપ કરો."</string>
+    <string name="disable_tether_notification_title" msgid="3004509127903564191">"ઇન્ટરનેટ શેર કરવાની સુવિધા બંધ કરી છે"</string>
+    <string name="disable_tether_notification_message" msgid="6717523799293901476">"વિગતો માટે તમારા વ્યવસ્થાપકનો સંપર્ક કરો"</string>
+    <string name="notification_channel_tethering_status" msgid="2663463891530932727">"હૉટસ્પૉટ અને ઇન્ટરનેટ શેર કરવાની સુવિધાનું સ્ટેટસ"</string>
+    <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+    <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+    <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+    <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+    <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
 </resources>
diff --git a/Tethering/res/values-hi/strings.xml b/Tethering/res/values-hi/strings.xml
index 12f7961..08af81b 100644
--- a/Tethering/res/values-hi/strings.xml
+++ b/Tethering/res/values-hi/strings.xml
@@ -16,14 +16,14 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="tethered_notification_title" msgid="5350162111436634622">"टेदरिंग या हॉटस्पॉट चालू है"</string>
-    <string name="tethered_notification_message" msgid="2338023450330652098">"सेट अप करने के लिए टैप करें."</string>
-    <string name="disable_tether_notification_title" msgid="3183576627492925522">"टेदरिंग बंद है"</string>
-    <string name="disable_tether_notification_message" msgid="6655882039707534929">"जानकारी के लिए अपने एडमिन से संपर्क करें"</string>
-    <string name="notification_channel_tethering_status" msgid="7030733422705019001">"हॉटस्पॉट और टेदरिंग की स्थिति"</string>
-    <string name="no_upstream_notification_title" msgid="2052743091868702475"></string>
-    <string name="no_upstream_notification_message" msgid="6932020551635470134"></string>
-    <string name="no_upstream_notification_disable_button" msgid="8836277213343697023"></string>
-    <string name="upstream_roaming_notification_title" msgid="8614262557406849762"></string>
-    <string name="upstream_roaming_notification_message" msgid="5999740876323106599"></string>
+    <string name="tethered_notification_title" msgid="6426563586025792944">"टेदरिंग या हॉटस्पॉट चालू है"</string>
+    <string name="tethered_notification_message" msgid="64800879503420696">"सेट अप करने के लिए टैप करें."</string>
+    <string name="disable_tether_notification_title" msgid="3004509127903564191">"टेदरिंग बंद है"</string>
+    <string name="disable_tether_notification_message" msgid="6717523799293901476">"जानकारी के लिए अपने एडमिन से संपर्क करें"</string>
+    <string name="notification_channel_tethering_status" msgid="2663463891530932727">"हॉटस्पॉट और टेदरिंग की स्थिति"</string>
+    <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+    <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+    <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+    <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+    <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
 </resources>
diff --git a/Tethering/res/values-hr/strings.xml b/Tethering/res/values-hr/strings.xml
index 19b7b45..827c135 100644
--- a/Tethering/res/values-hr/strings.xml
+++ b/Tethering/res/values-hr/strings.xml
@@ -16,14 +16,14 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="tethered_notification_title" msgid="5350162111436634622">"Modemsko povezivanje ili žarišna točka aktivni"</string>
-    <string name="tethered_notification_message" msgid="2338023450330652098">"Dodirnite da biste ih postavili."</string>
-    <string name="disable_tether_notification_title" msgid="3183576627492925522">"Modemsko je povezivanje onemogućeno"</string>
-    <string name="disable_tether_notification_message" msgid="6655882039707534929">"Obratite se administratoru da biste saznali pojedinosti"</string>
-    <string name="notification_channel_tethering_status" msgid="7030733422705019001">"Status žarišne točke i modemskog povezivanja"</string>
-    <string name="no_upstream_notification_title" msgid="2052743091868702475"></string>
-    <string name="no_upstream_notification_message" msgid="6932020551635470134"></string>
-    <string name="no_upstream_notification_disable_button" msgid="8836277213343697023"></string>
-    <string name="upstream_roaming_notification_title" msgid="8614262557406849762"></string>
-    <string name="upstream_roaming_notification_message" msgid="5999740876323106599"></string>
+    <string name="tethered_notification_title" msgid="6426563586025792944">"Modemsko povezivanje ili žarišna točka aktivni"</string>
+    <string name="tethered_notification_message" msgid="64800879503420696">"Dodirnite da biste postavili."</string>
+    <string name="disable_tether_notification_title" msgid="3004509127903564191">"Modemsko je povezivanje onemogućeno"</string>
+    <string name="disable_tether_notification_message" msgid="6717523799293901476">"Obratite se administratoru da biste saznali pojedinosti"</string>
+    <string name="notification_channel_tethering_status" msgid="2663463891530932727">"Status žarišne točke i modemskog povezivanja"</string>
+    <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+    <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+    <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+    <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+    <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
 </resources>
diff --git a/Tethering/res/values-hu/strings.xml b/Tethering/res/values-hu/strings.xml
index 419f434..eb68d6b 100644
--- a/Tethering/res/values-hu/strings.xml
+++ b/Tethering/res/values-hu/strings.xml
@@ -16,14 +16,14 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="tethered_notification_title" msgid="5350162111436634622">"Az internetmegosztás vagy a hotspot aktív"</string>
-    <string name="tethered_notification_message" msgid="2338023450330652098">"Koppintson a beállításhoz."</string>
-    <string name="disable_tether_notification_title" msgid="3183576627492925522">"Az internetmegosztás le van tiltva"</string>
-    <string name="disable_tether_notification_message" msgid="6655882039707534929">"A részletekért forduljon rendszergazdájához"</string>
-    <string name="notification_channel_tethering_status" msgid="7030733422705019001">"Hotspot és internetmegosztás állapota"</string>
-    <string name="no_upstream_notification_title" msgid="2052743091868702475"></string>
-    <string name="no_upstream_notification_message" msgid="6932020551635470134"></string>
-    <string name="no_upstream_notification_disable_button" msgid="8836277213343697023"></string>
-    <string name="upstream_roaming_notification_title" msgid="8614262557406849762"></string>
-    <string name="upstream_roaming_notification_message" msgid="5999740876323106599"></string>
+    <string name="tethered_notification_title" msgid="6426563586025792944">"Megosztás vagy aktív hotspot"</string>
+    <string name="tethered_notification_message" msgid="64800879503420696">"Koppintson a beállításhoz."</string>
+    <string name="disable_tether_notification_title" msgid="3004509127903564191">"Az internetmegosztás le van tiltva"</string>
+    <string name="disable_tether_notification_message" msgid="6717523799293901476">"A részletekért forduljon rendszergazdájához"</string>
+    <string name="notification_channel_tethering_status" msgid="2663463891530932727">"Hotspot és internetmegosztás állapota"</string>
+    <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+    <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+    <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+    <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+    <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
 </resources>
diff --git a/Tethering/res/values-hy/strings.xml b/Tethering/res/values-hy/strings.xml
index c8842b6..912941e 100644
--- a/Tethering/res/values-hy/strings.xml
+++ b/Tethering/res/values-hy/strings.xml
@@ -16,14 +16,14 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="tethered_notification_title" msgid="5350162111436634622">"Մոդեմի ռեժիմը միացված է"</string>
-    <string name="tethered_notification_message" msgid="2338023450330652098">"Հպեք՝ կարգավորելու համար։"</string>
-    <string name="disable_tether_notification_title" msgid="3183576627492925522">"Մոդեմի ռեժիմն անջատված է"</string>
-    <string name="disable_tether_notification_message" msgid="6655882039707534929">"Մանրամասների համար դիմեք ձեր ադմինիստրատորին"</string>
-    <string name="notification_channel_tethering_status" msgid="7030733422705019001">"Թեժ կետի և մոդեմի ռեժիմի կարգավիճակը"</string>
-    <string name="no_upstream_notification_title" msgid="2052743091868702475"></string>
-    <string name="no_upstream_notification_message" msgid="6932020551635470134"></string>
-    <string name="no_upstream_notification_disable_button" msgid="8836277213343697023"></string>
-    <string name="upstream_roaming_notification_title" msgid="8614262557406849762"></string>
-    <string name="upstream_roaming_notification_message" msgid="5999740876323106599"></string>
+    <string name="tethered_notification_title" msgid="6426563586025792944">"Մոդեմի ռեժիմը միացված է"</string>
+    <string name="tethered_notification_message" msgid="64800879503420696">"Հպեք՝ կարգավորելու համար։"</string>
+    <string name="disable_tether_notification_title" msgid="3004509127903564191">"Մոդեմի ռեժիմն անջատված է"</string>
+    <string name="disable_tether_notification_message" msgid="6717523799293901476">"Մանրամասների համար դիմեք ձեր ադմինիստրատորին"</string>
+    <string name="notification_channel_tethering_status" msgid="2663463891530932727">"Թեժ կետի և մոդեմի ռեժիմի կարգավիճակը"</string>
+    <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+    <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+    <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+    <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+    <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
 </resources>
diff --git a/Tethering/res/values-in/strings.xml b/Tethering/res/values-in/strings.xml
index 4ae35d4..a4e175a 100644
--- a/Tethering/res/values-in/strings.xml
+++ b/Tethering/res/values-in/strings.xml
@@ -16,14 +16,14 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="tethered_notification_title" msgid="5350162111436634622">"Tethering atau hotspot aktif"</string>
-    <string name="tethered_notification_message" msgid="2338023450330652098">"Ketuk untuk menyiapkan."</string>
-    <string name="disable_tether_notification_title" msgid="3183576627492925522">"Tethering dinonaktifkan"</string>
-    <string name="disable_tether_notification_message" msgid="6655882039707534929">"Hubungi admin untuk mengetahui detailnya"</string>
-    <string name="notification_channel_tethering_status" msgid="7030733422705019001">"Status hotspot &amp; tethering"</string>
-    <string name="no_upstream_notification_title" msgid="2052743091868702475"></string>
-    <string name="no_upstream_notification_message" msgid="6932020551635470134"></string>
-    <string name="no_upstream_notification_disable_button" msgid="8836277213343697023"></string>
-    <string name="upstream_roaming_notification_title" msgid="8614262557406849762"></string>
-    <string name="upstream_roaming_notification_message" msgid="5999740876323106599"></string>
+    <string name="tethered_notification_title" msgid="6426563586025792944">"Tethering atau hotspot aktif"</string>
+    <string name="tethered_notification_message" msgid="64800879503420696">"Ketuk untuk menyiapkan."</string>
+    <string name="disable_tether_notification_title" msgid="3004509127903564191">"Tethering dinonaktifkan"</string>
+    <string name="disable_tether_notification_message" msgid="6717523799293901476">"Hubungi admin untuk mengetahui detailnya"</string>
+    <string name="notification_channel_tethering_status" msgid="2663463891530932727">"Status hotspot &amp; tethering"</string>
+    <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+    <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+    <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+    <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+    <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
 </resources>
diff --git a/Tethering/res/values-is/strings.xml b/Tethering/res/values-is/strings.xml
index df69fb4..e9f6670 100644
--- a/Tethering/res/values-is/strings.xml
+++ b/Tethering/res/values-is/strings.xml
@@ -16,14 +16,14 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="tethered_notification_title" msgid="5350162111436634622">"Kveikt á tjóðrun eða heitum reit"</string>
-    <string name="tethered_notification_message" msgid="2338023450330652098">"Ýttu til að setja upp."</string>
-    <string name="disable_tether_notification_title" msgid="3183576627492925522">"Slökkt er á tjóðrun"</string>
-    <string name="disable_tether_notification_message" msgid="6655882039707534929">"Hafðu samband við stjórnanda til að fá upplýsingar"</string>
-    <string name="notification_channel_tethering_status" msgid="7030733422705019001">"Staða heits reits og tjóðrunar"</string>
-    <string name="no_upstream_notification_title" msgid="2052743091868702475"></string>
-    <string name="no_upstream_notification_message" msgid="6932020551635470134"></string>
-    <string name="no_upstream_notification_disable_button" msgid="8836277213343697023"></string>
-    <string name="upstream_roaming_notification_title" msgid="8614262557406849762"></string>
-    <string name="upstream_roaming_notification_message" msgid="5999740876323106599"></string>
+    <string name="tethered_notification_title" msgid="6426563586025792944">"Kveikt á tjóðrun eða aðgangsstað"</string>
+    <string name="tethered_notification_message" msgid="64800879503420696">"Ýttu til að setja upp."</string>
+    <string name="disable_tether_notification_title" msgid="3004509127903564191">"Slökkt er á tjóðrun"</string>
+    <string name="disable_tether_notification_message" msgid="6717523799293901476">"Hafðu samband við kerfisstjórann til að fá upplýsingar"</string>
+    <string name="notification_channel_tethering_status" msgid="2663463891530932727">"Staða heits reits og tjóðrunar"</string>
+    <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+    <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+    <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+    <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+    <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
 </resources>
diff --git a/Tethering/res/values-it/strings.xml b/Tethering/res/values-it/strings.xml
index b13ee92..ffb9196 100644
--- a/Tethering/res/values-it/strings.xml
+++ b/Tethering/res/values-it/strings.xml
@@ -16,14 +16,14 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="tethered_notification_title" msgid="5350162111436634622">"Hotspot o tethering attivo"</string>
-    <string name="tethered_notification_message" msgid="2338023450330652098">"Tocca per impostare."</string>
-    <string name="disable_tether_notification_title" msgid="3183576627492925522">"Tethering disattivato"</string>
-    <string name="disable_tether_notification_message" msgid="6655882039707534929">"Contatta il tuo amministratore per avere informazioni dettagliate"</string>
-    <string name="notification_channel_tethering_status" msgid="7030733422705019001">"Stato hotspot e tethering"</string>
-    <string name="no_upstream_notification_title" msgid="2052743091868702475"></string>
-    <string name="no_upstream_notification_message" msgid="6932020551635470134"></string>
-    <string name="no_upstream_notification_disable_button" msgid="8836277213343697023"></string>
-    <string name="upstream_roaming_notification_title" msgid="8614262557406849762"></string>
-    <string name="upstream_roaming_notification_message" msgid="5999740876323106599"></string>
+    <string name="tethered_notification_title" msgid="6426563586025792944">"Hotspot o tethering attivo"</string>
+    <string name="tethered_notification_message" msgid="64800879503420696">"Tocca per impostare."</string>
+    <string name="disable_tether_notification_title" msgid="3004509127903564191">"Tethering disattivato"</string>
+    <string name="disable_tether_notification_message" msgid="6717523799293901476">"Contatta il tuo amministratore per avere informazioni dettagliate"</string>
+    <string name="notification_channel_tethering_status" msgid="2663463891530932727">"Stato hotspot e tethering"</string>
+    <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+    <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+    <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+    <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+    <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
 </resources>
diff --git a/Tethering/res/values-iw/strings.xml b/Tethering/res/values-iw/strings.xml
index f7fb4d5..7adcb47 100644
--- a/Tethering/res/values-iw/strings.xml
+++ b/Tethering/res/values-iw/strings.xml
@@ -16,14 +16,14 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="tethered_notification_title" msgid="5350162111436634622">"שיתוף האינטרנט או הנקודה לשיתוף אינטרנט פעילים"</string>
-    <string name="tethered_notification_message" msgid="2338023450330652098">"יש להקיש כדי להגדיר."</string>
-    <string name="disable_tether_notification_title" msgid="3183576627492925522">"שיתוף האינטרנט בין מכשירים מושבת"</string>
-    <string name="disable_tether_notification_message" msgid="6655882039707534929">"לפרטים, יש לפנות לאדמין"</string>
-    <string name="notification_channel_tethering_status" msgid="7030733422705019001">"סטטוס של נקודה לשיתוף אינטרנט ושיתוף אינטרנט בין מכשירים"</string>
-    <string name="no_upstream_notification_title" msgid="2052743091868702475"></string>
-    <string name="no_upstream_notification_message" msgid="6932020551635470134"></string>
-    <string name="no_upstream_notification_disable_button" msgid="8836277213343697023"></string>
-    <string name="upstream_roaming_notification_title" msgid="8614262557406849762"></string>
-    <string name="upstream_roaming_notification_message" msgid="5999740876323106599"></string>
+    <string name="tethered_notification_title" msgid="6426563586025792944">"נקודה לשיתוף אינטרנט או שיתוף אינטרנט בין מכשירים: בסטטוס פעיל"</string>
+    <string name="tethered_notification_message" msgid="64800879503420696">"יש להקיש כדי להגדיר."</string>
+    <string name="disable_tether_notification_title" msgid="3004509127903564191">"שיתוף האינטרנט בין מכשירים מושבת"</string>
+    <string name="disable_tether_notification_message" msgid="6717523799293901476">"לפרטים, יש לפנות למנהל המערכת"</string>
+    <string name="notification_channel_tethering_status" msgid="2663463891530932727">"סטטוס של נקודה לשיתוף אינטרנט ושיתוף אינטרנט בין מכשירים"</string>
+    <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+    <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+    <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+    <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+    <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
 </resources>
diff --git a/Tethering/res/values-ja/strings.xml b/Tethering/res/values-ja/strings.xml
index 172e771..f68a730 100644
--- a/Tethering/res/values-ja/strings.xml
+++ b/Tethering/res/values-ja/strings.xml
@@ -16,14 +16,14 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="tethered_notification_title" msgid="5350162111436634622">"テザリングまたはアクセス ポイントが有効です"</string>
-    <string name="tethered_notification_message" msgid="2338023450330652098">"タップしてセットアップしてください。"</string>
-    <string name="disable_tether_notification_title" msgid="3183576627492925522">"テザリングは無効に設定されています"</string>
-    <string name="disable_tether_notification_message" msgid="6655882039707534929">"詳しくは、管理者にお問い合わせください"</string>
-    <string name="notification_channel_tethering_status" msgid="7030733422705019001">"アクセス ポイントとテザリングのステータス"</string>
-    <string name="no_upstream_notification_title" msgid="2052743091868702475"></string>
-    <string name="no_upstream_notification_message" msgid="6932020551635470134"></string>
-    <string name="no_upstream_notification_disable_button" msgid="8836277213343697023"></string>
-    <string name="upstream_roaming_notification_title" msgid="8614262557406849762"></string>
-    <string name="upstream_roaming_notification_message" msgid="5999740876323106599"></string>
+    <string name="tethered_notification_title" msgid="6426563586025792944">"テザリングまたはアクセス ポイントが有効です"</string>
+    <string name="tethered_notification_message" msgid="64800879503420696">"タップしてセットアップします。"</string>
+    <string name="disable_tether_notification_title" msgid="3004509127903564191">"テザリングは無効に設定されています"</string>
+    <string name="disable_tether_notification_message" msgid="6717523799293901476">"詳しくは、管理者にお問い合わせください"</string>
+    <string name="notification_channel_tethering_status" msgid="2663463891530932727">"アクセス ポイントとテザリングのステータス"</string>
+    <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+    <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+    <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+    <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+    <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
 </resources>
diff --git a/Tethering/res/values-ka/strings.xml b/Tethering/res/values-ka/strings.xml
index b4e1191..7c22e82 100644
--- a/Tethering/res/values-ka/strings.xml
+++ b/Tethering/res/values-ka/strings.xml
@@ -16,14 +16,14 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="tethered_notification_title" msgid="5350162111436634622">"ტეტერინგი ან უსადენო ქსელი აქტიურია"</string>
-    <string name="tethered_notification_message" msgid="2338023450330652098">"შეეხეთ დასაყენებლად."</string>
-    <string name="disable_tether_notification_title" msgid="3183576627492925522">"ტეტერინგი გათიშულია"</string>
-    <string name="disable_tether_notification_message" msgid="6655882039707534929">"დამატებითი ინფორმაციისთვის დაუკავშირდით თქვენს ადმინისტრატორს"</string>
-    <string name="notification_channel_tethering_status" msgid="7030733422705019001">"უსადენო ქსელის და ტეტერინგის სტატუსი"</string>
-    <string name="no_upstream_notification_title" msgid="2052743091868702475"></string>
-    <string name="no_upstream_notification_message" msgid="6932020551635470134"></string>
-    <string name="no_upstream_notification_disable_button" msgid="8836277213343697023"></string>
-    <string name="upstream_roaming_notification_title" msgid="8614262557406849762"></string>
-    <string name="upstream_roaming_notification_message" msgid="5999740876323106599"></string>
+    <string name="tethered_notification_title" msgid="6426563586025792944">"ტეტერინგი ან უსადენო ქსელი აქტიურია"</string>
+    <string name="tethered_notification_message" msgid="64800879503420696">"შეეხეთ დასაყენებლად."</string>
+    <string name="disable_tether_notification_title" msgid="3004509127903564191">"ტეტერინგი გათიშულია"</string>
+    <string name="disable_tether_notification_message" msgid="6717523799293901476">"დამატებითი ინფორმაციისთვის დაუკავშირდით თქვენს ადმინისტრატორს"</string>
+    <string name="notification_channel_tethering_status" msgid="2663463891530932727">"უსადენო ქსელის და ტეტერინგის სტატუსი"</string>
+    <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+    <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+    <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+    <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+    <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
 </resources>
diff --git a/Tethering/res/values-kk/strings.xml b/Tethering/res/values-kk/strings.xml
index 0116381..0857d06 100644
--- a/Tethering/res/values-kk/strings.xml
+++ b/Tethering/res/values-kk/strings.xml
@@ -16,14 +16,14 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="tethered_notification_title" msgid="5350162111436634622">"Тетеринг немесе хотспот іске қосылған"</string>
-    <string name="tethered_notification_message" msgid="2338023450330652098">"Реттеу үшін түртіңіз."</string>
-    <string name="disable_tether_notification_title" msgid="3183576627492925522">"Тетеринг өшірілді."</string>
-    <string name="disable_tether_notification_message" msgid="6655882039707534929">"Мәлімет алу үшін әкімшіге хабарласыңыз."</string>
-    <string name="notification_channel_tethering_status" msgid="7030733422705019001">"Хотспот және тетеринг күйі"</string>
-    <string name="no_upstream_notification_title" msgid="2052743091868702475"></string>
-    <string name="no_upstream_notification_message" msgid="6932020551635470134"></string>
-    <string name="no_upstream_notification_disable_button" msgid="8836277213343697023"></string>
-    <string name="upstream_roaming_notification_title" msgid="8614262557406849762"></string>
-    <string name="upstream_roaming_notification_message" msgid="5999740876323106599"></string>
+    <string name="tethered_notification_title" msgid="6426563586025792944">"Тетеринг немесе хотспот қосулы"</string>
+    <string name="tethered_notification_message" msgid="64800879503420696">"Реттеу үшін түртіңіз."</string>
+    <string name="disable_tether_notification_title" msgid="3004509127903564191">"Тетеринг өшірілді."</string>
+    <string name="disable_tether_notification_message" msgid="6717523799293901476">"Мәліметтерді әкімшіден алыңыз."</string>
+    <string name="notification_channel_tethering_status" msgid="2663463891530932727">"Хотспот және тетеринг күйі"</string>
+    <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+    <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+    <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+    <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+    <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
 </resources>
diff --git a/Tethering/res/values-km/strings.xml b/Tethering/res/values-km/strings.xml
index 52667e8..536e3d1 100644
--- a/Tethering/res/values-km/strings.xml
+++ b/Tethering/res/values-km/strings.xml
@@ -16,14 +16,14 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="tethered_notification_title" msgid="5350162111436634622">"ការភ្ជាប់ ឬហតស្ប៉ត​កំពុងដំណើរការ"</string>
-    <string name="tethered_notification_message" msgid="2338023450330652098">"ចុច​ដើម្បី​រៀបចំ។"</string>
-    <string name="disable_tether_notification_title" msgid="3183576627492925522">"ការភ្ជាប់​ត្រូវបានបិទ"</string>
-    <string name="disable_tether_notification_message" msgid="6655882039707534929">"ទាក់ទងអ្នកគ្រប់គ្រង​របស់អ្នក ដើម្បីទទួលបានព័ត៌មានលម្អិត"</string>
-    <string name="notification_channel_tethering_status" msgid="7030733422705019001">"ស្ថានភាពនៃការភ្ជាប់ និងហតស្ប៉ត"</string>
-    <string name="no_upstream_notification_title" msgid="2052743091868702475"></string>
-    <string name="no_upstream_notification_message" msgid="6932020551635470134"></string>
-    <string name="no_upstream_notification_disable_button" msgid="8836277213343697023"></string>
-    <string name="upstream_roaming_notification_title" msgid="8614262557406849762"></string>
-    <string name="upstream_roaming_notification_message" msgid="5999740876323106599"></string>
+    <string name="tethered_notification_title" msgid="6426563586025792944">"ការភ្ជាប់ ឬហតស្ប៉ត​កំពុងដំណើរការ"</string>
+    <string name="tethered_notification_message" msgid="64800879503420696">"ចុច​ដើម្បី​រៀបចំ។"</string>
+    <string name="disable_tether_notification_title" msgid="3004509127903564191">"ការភ្ជាប់​ត្រូវបានបិទ"</string>
+    <string name="disable_tether_notification_message" msgid="6717523799293901476">"ទាក់ទងអ្នកគ្រប់គ្រង​របស់អ្នក ដើម្បីទទួលបានព័ត៌មានលម្អិត"</string>
+    <string name="notification_channel_tethering_status" msgid="2663463891530932727">"ស្ថានភាពនៃការភ្ជាប់ និងហតស្ប៉ត"</string>
+    <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+    <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+    <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+    <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+    <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
 </resources>
diff --git a/Tethering/res/values-kn/strings.xml b/Tethering/res/values-kn/strings.xml
index a0a3607..32f5492 100644
--- a/Tethering/res/values-kn/strings.xml
+++ b/Tethering/res/values-kn/strings.xml
@@ -16,14 +16,14 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="tethered_notification_title" msgid="5350162111436634622">"ಟೆಥರಿಂಗ್ ಅಥವಾ ಹಾಟ್‌ಸ್ಪಾಟ್ ಸಕ್ರಿಯವಾಗಿದೆ"</string>
-    <string name="tethered_notification_message" msgid="2338023450330652098">"ಸೆಟಪ್ ಮಾಡಲು ಟ್ಯಾಪ್ ಮಾಡಿ."</string>
-    <string name="disable_tether_notification_title" msgid="3183576627492925522">"ಟೆಥರಿಂಗ್ ಅನ್ನು ನಿಷ್ಕ್ರಿಯಗೊಳಿಸಲಾಗಿದೆ"</string>
-    <string name="disable_tether_notification_message" msgid="6655882039707534929">"ವಿವರಗಳಿಗಾಗಿ ನಿಮ್ಮ ನಿರ್ವಾಹಕರನ್ನು ಸಂಪರ್ಕಿಸಿ"</string>
-    <string name="notification_channel_tethering_status" msgid="7030733422705019001">"ಹಾಟ್‌ಸ್ಪಾಟ್ ಮತ್ತು ಟೆಥರಿಂಗ್‌ ಸ್ಥಿತಿ"</string>
-    <string name="no_upstream_notification_title" msgid="2052743091868702475"></string>
-    <string name="no_upstream_notification_message" msgid="6932020551635470134"></string>
-    <string name="no_upstream_notification_disable_button" msgid="8836277213343697023"></string>
-    <string name="upstream_roaming_notification_title" msgid="8614262557406849762"></string>
-    <string name="upstream_roaming_notification_message" msgid="5999740876323106599"></string>
+    <string name="tethered_notification_title" msgid="6426563586025792944">"ಟೆಥರಿಂಗ್ ಅಥವಾ ಹಾಟ್‌ಸ್ಪಾಟ್ ಸಕ್ರಿಯವಾಗಿದೆ"</string>
+    <string name="tethered_notification_message" msgid="64800879503420696">"ಸೆಟಪ್ ಮಾಡಲು ಟ್ಯಾಪ್ ಮಾಡಿ."</string>
+    <string name="disable_tether_notification_title" msgid="3004509127903564191">"ಟೆಥರಿಂಗ್ ಅನ್ನು ನಿಷ್ಕ್ರಿಯಗೊಳಿಸಲಾಗಿದೆ"</string>
+    <string name="disable_tether_notification_message" msgid="6717523799293901476">"ವಿವರಗಳಿಗಾಗಿ ನಿಮ್ಮ ನಿರ್ವಾಹಕರನ್ನು ಸಂಪರ್ಕಿಸಿ"</string>
+    <string name="notification_channel_tethering_status" msgid="2663463891530932727">"ಹಾಟ್‌ಸ್ಪಾಟ್ ಮತ್ತು ಟೆಥರಿಂಗ್‌ ಸ್ಥಿತಿ"</string>
+    <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+    <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+    <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+    <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+    <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
 </resources>
diff --git a/Tethering/res/values-ko/strings.xml b/Tethering/res/values-ko/strings.xml
index f7b8da0..156b247 100644
--- a/Tethering/res/values-ko/strings.xml
+++ b/Tethering/res/values-ko/strings.xml
@@ -16,14 +16,14 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="tethered_notification_title" msgid="5350162111436634622">"테더링 또는 핫스팟 사용 중"</string>
-    <string name="tethered_notification_message" msgid="2338023450330652098">"설정하려면 탭하세요."</string>
-    <string name="disable_tether_notification_title" msgid="3183576627492925522">"테더링이 사용 중지됨"</string>
-    <string name="disable_tether_notification_message" msgid="6655882039707534929">"자세한 정보는 관리자에게 문의하세요."</string>
-    <string name="notification_channel_tethering_status" msgid="7030733422705019001">"핫스팟 및 테더링 상태"</string>
-    <string name="no_upstream_notification_title" msgid="2052743091868702475"></string>
-    <string name="no_upstream_notification_message" msgid="6932020551635470134"></string>
-    <string name="no_upstream_notification_disable_button" msgid="8836277213343697023"></string>
-    <string name="upstream_roaming_notification_title" msgid="8614262557406849762"></string>
-    <string name="upstream_roaming_notification_message" msgid="5999740876323106599"></string>
+    <string name="tethered_notification_title" msgid="6426563586025792944">"테더링 또는 핫스팟 사용"</string>
+    <string name="tethered_notification_message" msgid="64800879503420696">"설정하려면 탭하세요."</string>
+    <string name="disable_tether_notification_title" msgid="3004509127903564191">"테더링이 사용 중지됨"</string>
+    <string name="disable_tether_notification_message" msgid="6717523799293901476">"자세한 정보는 관리자에게 문의하세요."</string>
+    <string name="notification_channel_tethering_status" msgid="2663463891530932727">"핫스팟 및 테더링 상태"</string>
+    <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+    <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+    <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+    <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+    <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
 </resources>
diff --git a/Tethering/res/values-ky/strings.xml b/Tethering/res/values-ky/strings.xml
index 35e6453..18ee5fd 100644
--- a/Tethering/res/values-ky/strings.xml
+++ b/Tethering/res/values-ky/strings.xml
@@ -16,14 +16,14 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="tethered_notification_title" msgid="5350162111436634622">"Модем режими күйүп турат"</string>
-    <string name="tethered_notification_message" msgid="2338023450330652098">"Тууралоо үчүн басыңыз."</string>
-    <string name="disable_tether_notification_title" msgid="3183576627492925522">"Модем режими өчүк"</string>
-    <string name="disable_tether_notification_message" msgid="6655882039707534929">"Кеңири маалымат үчүн администраторуңузга кайрылыңыз"</string>
-    <string name="notification_channel_tethering_status" msgid="7030733422705019001">"Хотспот жана байланыш түйүнүүн статусу"</string>
-    <string name="no_upstream_notification_title" msgid="2052743091868702475"></string>
-    <string name="no_upstream_notification_message" msgid="6932020551635470134"></string>
-    <string name="no_upstream_notification_disable_button" msgid="8836277213343697023"></string>
-    <string name="upstream_roaming_notification_title" msgid="8614262557406849762"></string>
-    <string name="upstream_roaming_notification_message" msgid="5999740876323106599"></string>
+    <string name="tethered_notification_title" msgid="6426563586025792944">"Модем режими күйүп турат"</string>
+    <string name="tethered_notification_message" msgid="64800879503420696">"Жөндөө үчүн таптап коюңуз."</string>
+    <string name="disable_tether_notification_title" msgid="3004509127903564191">"Телефонду модем катары колдонууга болбойт"</string>
+    <string name="disable_tether_notification_message" msgid="6717523799293901476">"Кеңири маалымат үчүн администраторуңузга кайрылыңыз"</string>
+    <string name="notification_channel_tethering_status" msgid="2663463891530932727">"Байланыш түйүнүнүн жана модем режиминин статусу"</string>
+    <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+    <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+    <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+    <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+    <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
 </resources>
diff --git a/Tethering/res/values-lo/strings.xml b/Tethering/res/values-lo/strings.xml
index 046551d..b127670 100644
--- a/Tethering/res/values-lo/strings.xml
+++ b/Tethering/res/values-lo/strings.xml
@@ -16,14 +16,14 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="tethered_notification_title" msgid="5350162111436634622">"ການປ່ອຍສັນຍານ ຫຼື ຮັອດສະປອດເປີດນຳໃຊ້ຢູ່"</string>
-    <string name="tethered_notification_message" msgid="2338023450330652098">"ແຕະເພື່ອຕັ້ງຄ່າ."</string>
-    <string name="disable_tether_notification_title" msgid="3183576627492925522">"ການປ່ອຍສັນຍານຖືກປິດໄວ້"</string>
-    <string name="disable_tether_notification_message" msgid="6655882039707534929">"ຕິດຕໍ່ຜູ້ເບິ່ງແຍງລະບົບຂອງທ່ານສຳລັບລາຍລະອຽດ"</string>
-    <string name="notification_channel_tethering_status" msgid="7030733422705019001">"ສະຖານະຮັອດສະປອດ ແລະ ການປ່ອຍສັນຍານ"</string>
-    <string name="no_upstream_notification_title" msgid="2052743091868702475"></string>
-    <string name="no_upstream_notification_message" msgid="6932020551635470134"></string>
-    <string name="no_upstream_notification_disable_button" msgid="8836277213343697023"></string>
-    <string name="upstream_roaming_notification_title" msgid="8614262557406849762"></string>
-    <string name="upstream_roaming_notification_message" msgid="5999740876323106599"></string>
+    <string name="tethered_notification_title" msgid="6426563586025792944">"ເປີດການປ່ອຍສັນຍານ ຫຼື ຮັອດສະປອດແລ້ວ"</string>
+    <string name="tethered_notification_message" msgid="64800879503420696">"ແຕະເພື່ອຕັ້ງຄ່າ."</string>
+    <string name="disable_tether_notification_title" msgid="3004509127903564191">"ການປ່ອຍສັນຍານຖືກປິດໄວ້"</string>
+    <string name="disable_tether_notification_message" msgid="6717523799293901476">"ຕິດຕໍ່ຜູ້ເບິ່ງແຍງລະບົບສຳລັບລາຍລະອຽດ"</string>
+    <string name="notification_channel_tethering_status" msgid="2663463891530932727">"ສະຖານະຮັອດສະປອດ ແລະ ການປ່ອຍສັນຍານ"</string>
+    <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+    <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+    <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+    <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+    <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
 </resources>
diff --git a/Tethering/res/values-lt/strings.xml b/Tethering/res/values-lt/strings.xml
index c685318..8427baf 100644
--- a/Tethering/res/values-lt/strings.xml
+++ b/Tethering/res/values-lt/strings.xml
@@ -16,14 +16,14 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="tethered_notification_title" msgid="5350162111436634622">"Įrenginys naudojamas kaip modemas arba įjungtas viešosios interneto prieigos taškas"</string>
-    <string name="tethered_notification_message" msgid="2338023450330652098">"Palieskite, kad nustatytumėte."</string>
-    <string name="disable_tether_notification_title" msgid="3183576627492925522">"Įrenginio kaip modemo naudojimas išjungtas"</string>
-    <string name="disable_tether_notification_message" msgid="6655882039707534929">"Jei reikia išsamios informacijos, susisiekite su administratoriumi"</string>
-    <string name="notification_channel_tethering_status" msgid="7030733422705019001">"Viešosios interneto prieigos taško ir įrenginio kaip modemo naudojimo būsena"</string>
-    <string name="no_upstream_notification_title" msgid="2052743091868702475"></string>
-    <string name="no_upstream_notification_message" msgid="6932020551635470134"></string>
-    <string name="no_upstream_notification_disable_button" msgid="8836277213343697023"></string>
-    <string name="upstream_roaming_notification_title" msgid="8614262557406849762"></string>
-    <string name="upstream_roaming_notification_message" msgid="5999740876323106599"></string>
+    <string name="tethered_notification_title" msgid="6426563586025792944">"Įrenginys naudojamas kaip modemas arba įjungtas viešosios interneto prieigos taškas"</string>
+    <string name="tethered_notification_message" msgid="64800879503420696">"Palieskite, kad nustatytumėte."</string>
+    <string name="disable_tether_notification_title" msgid="3004509127903564191">"Įrenginio kaip modemo naudojimas išjungtas"</string>
+    <string name="disable_tether_notification_message" msgid="6717523799293901476">"Jei reikia išsamios informacijos, susisiekite su administratoriumi"</string>
+    <string name="notification_channel_tethering_status" msgid="2663463891530932727">"Viešosios interneto prieigos taško ir įrenginio kaip modemo naudojimo būsena"</string>
+    <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+    <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+    <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+    <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+    <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
 </resources>
diff --git a/Tethering/res/values-lv/strings.xml b/Tethering/res/values-lv/strings.xml
index fd8751c..aa2d699 100644
--- a/Tethering/res/values-lv/strings.xml
+++ b/Tethering/res/values-lv/strings.xml
@@ -16,14 +16,14 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="tethered_notification_title" msgid="5350162111436634622">"Piesaiste vai tīklājs ir aktīvs"</string>
-    <string name="tethered_notification_message" msgid="2338023450330652098">"Pieskarieties, lai iestatītu."</string>
-    <string name="disable_tether_notification_title" msgid="3183576627492925522">"Piesaiste ir atspējota"</string>
-    <string name="disable_tether_notification_message" msgid="6655882039707534929">"Lai iegūtu detalizētu informāciju, sazinieties ar savu administratoru."</string>
-    <string name="notification_channel_tethering_status" msgid="7030733422705019001">"Tīklāja un piesaistes statuss"</string>
-    <string name="no_upstream_notification_title" msgid="2052743091868702475"></string>
-    <string name="no_upstream_notification_message" msgid="6932020551635470134"></string>
-    <string name="no_upstream_notification_disable_button" msgid="8836277213343697023"></string>
-    <string name="upstream_roaming_notification_title" msgid="8614262557406849762"></string>
-    <string name="upstream_roaming_notification_message" msgid="5999740876323106599"></string>
+    <string name="tethered_notification_title" msgid="6426563586025792944">"Piesaiste vai tīklājs ir aktīvs."</string>
+    <string name="tethered_notification_message" msgid="64800879503420696">"Pieskarieties, lai to iestatītu."</string>
+    <string name="disable_tether_notification_title" msgid="3004509127903564191">"Piesaiste ir atspējota"</string>
+    <string name="disable_tether_notification_message" msgid="6717523799293901476">"Lai iegūtu detalizētu informāciju, sazinieties ar savu administratoru."</string>
+    <string name="notification_channel_tethering_status" msgid="2663463891530932727">"Tīklāja un piesaistes statuss"</string>
+    <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+    <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+    <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+    <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+    <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
 </resources>
diff --git a/Tethering/res/values-mcc310-mnc004-af/strings.xml b/Tethering/res/values-mcc310-mnc004-af/strings.xml
index 216c02c..19d659c 100644
--- a/Tethering/res/values-mcc310-mnc004-af/strings.xml
+++ b/Tethering/res/values-mcc310-mnc004-af/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="3584617491053416666">"Verbinding het nie internet nie"</string>
-    <string name="no_upstream_notification_message" msgid="5626323795587558017">"Toestelle kan nie koppel nie"</string>
-    <string name="no_upstream_notification_disable_button" msgid="868677179945695858">"Skakel verbinding af"</string>
-    <string name="upstream_roaming_notification_title" msgid="2870229486619751829">"Warmkol of verbinding is aan"</string>
-    <string name="upstream_roaming_notification_message" msgid="5229740963392849544">"Bykomende heffings kan geld terwyl jy swerf"</string>
+    <string name="no_upstream_notification_title" msgid="5030042590486713460">"Verbinding het nie internet nie"</string>
+    <string name="no_upstream_notification_message" msgid="3843613362272973447">"Toestelle kan nie koppel nie"</string>
+    <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"Skakel verbinding af"</string>
+    <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"Warmkol of verbinding is aan"</string>
+    <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"Bykomende heffings kan geld terwyl jy swerf"</string>
 </resources>
diff --git a/Tethering/res/values-mcc310-mnc004-am/strings.xml b/Tethering/res/values-mcc310-mnc004-am/strings.xml
index 666630a..8995430 100644
--- a/Tethering/res/values-mcc310-mnc004-am/strings.xml
+++ b/Tethering/res/values-mcc310-mnc004-am/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="3584617491053416666">"እንደ ሞደም መሰካት ምንም በይነመረብ የለውም"</string>
-    <string name="no_upstream_notification_message" msgid="5626323795587558017">"መሣሪያዎችን ማገናኘት አልተቻልም"</string>
-    <string name="no_upstream_notification_disable_button" msgid="868677179945695858">"እንደ ሞደም መሰካትን አጥፋ"</string>
-    <string name="upstream_roaming_notification_title" msgid="2870229486619751829">"መገናኛ ነጥብ ወይም እንደ ሞደም መሰካት በርቷል"</string>
-    <string name="upstream_roaming_notification_message" msgid="5229740963392849544">"በሚያንዣብብበት ጊዜ ተጨማሪ ክፍያዎች ተፈጻሚ ሊሆኑ ይችላሉ"</string>
+    <string name="no_upstream_notification_title" msgid="5030042590486713460">"ማስተሳሰር ምንም በይነመረብ የለውም"</string>
+    <string name="no_upstream_notification_message" msgid="3843613362272973447">"መሣሪያዎችን ማገናኘት አይቻልም"</string>
+    <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"ማስተሳሰርን አጥፋ"</string>
+    <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"መገናኛ ነጥብ ወይም ማስተሳሰር በርቷል"</string>
+    <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"በሚያንዣብብበት ጊዜ ተጨማሪ ክፍያዎች ተፈጻሚ ሊሆኑ ይችላሉ"</string>
 </resources>
diff --git a/Tethering/res/values-mcc310-mnc004-ar/strings.xml b/Tethering/res/values-mcc310-mnc004-ar/strings.xml
index 2859803..54f3b53 100644
--- a/Tethering/res/values-mcc310-mnc004-ar/strings.xml
+++ b/Tethering/res/values-mcc310-mnc004-ar/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="3584617491053416666">"ما مِن اتصال بالإنترنت خلال التوصيل"</string>
-    <string name="no_upstream_notification_message" msgid="5626323795587558017">"يتعذّر اتصال الأجهزة"</string>
-    <string name="no_upstream_notification_disable_button" msgid="868677179945695858">"إيقاف التوصيل"</string>
-    <string name="upstream_roaming_notification_title" msgid="2870229486619751829">"نقطة الاتصال مفعَّلة أو التوصيل مفعَّل"</string>
-    <string name="upstream_roaming_notification_message" msgid="5229740963392849544">"قد يتم تحصيل رسوم إضافية أثناء التجوال."</string>
+    <string name="no_upstream_notification_title" msgid="5030042590486713460">"ما مِن اتصال بالإنترنت خلال التوصيل"</string>
+    <string name="no_upstream_notification_message" msgid="3843613362272973447">"تعذّر اتصال الأجهزة"</string>
+    <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"إيقاف التوصيل"</string>
+    <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"نقطة الاتصال أو التوصيل مفعّلان"</string>
+    <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"قد يتم تطبيق رسوم إضافية أثناء التجوال."</string>
 </resources>
diff --git a/Tethering/res/values-mcc310-mnc004-as/strings.xml b/Tethering/res/values-mcc310-mnc004-as/strings.xml
index 360c8ca..e215141 100644
--- a/Tethering/res/values-mcc310-mnc004-as/strings.xml
+++ b/Tethering/res/values-mcc310-mnc004-as/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="3584617491053416666">"টে’ডাৰিঙৰ ইণ্টাৰনেট নাই"</string>
-    <string name="no_upstream_notification_message" msgid="5626323795587558017">"ডিভাইচসমূহ সংযোগ কৰিব নোৱাৰি"</string>
-    <string name="no_upstream_notification_disable_button" msgid="868677179945695858">"টে’ডাৰিং অফ কৰক"</string>
-    <string name="upstream_roaming_notification_title" msgid="2870229486619751829">"হ’টস্প’ট অথবা টে’ডাৰিং অন আছে"</string>
-    <string name="upstream_roaming_notification_message" msgid="5229740963392849544">"ৰ’মিঙত থাকিলে অতিৰিক্ত মাচুল প্ৰযোজ্য হ’ব পাৰে"</string>
+    <string name="no_upstream_notification_title" msgid="5030042590486713460">"টে\'ডাৰিঙৰ ইণ্টাৰনেট নাই"</string>
+    <string name="no_upstream_notification_message" msgid="3843613362272973447">"ডিভাইচসমূহ সংযোগ কৰিব নোৱাৰি"</string>
+    <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"টে\'ডাৰিং অফ কৰক"</string>
+    <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"হটস্পট অথবা টে\'ডাৰিং অন আছে"</string>
+    <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"ৰ\'মিঙত থাকিলে অতিৰিক্ত মাচুল প্ৰযোজ্য হ’ব পাৰে"</string>
 </resources>
diff --git a/Tethering/res/values-mcc310-mnc004-az/strings.xml b/Tethering/res/values-mcc310-mnc004-az/strings.xml
index b7fdd70..1fd8e4c 100644
--- a/Tethering/res/values-mcc310-mnc004-az/strings.xml
+++ b/Tethering/res/values-mcc310-mnc004-az/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="3584617491053416666">"Modem rejimi internetə qoşulmayıb"</string>
-    <string name="no_upstream_notification_message" msgid="5626323795587558017">"Cihazları qoşmaq olmur"</string>
-    <string name="no_upstream_notification_disable_button" msgid="868677179945695858">"Modem rejimini deaktiv edin"</string>
-    <string name="upstream_roaming_notification_title" msgid="2870229486619751829">"Hotspot və ya modem rejimi aktivdir"</string>
-    <string name="upstream_roaming_notification_message" msgid="5229740963392849544">"Rouminq zamanı əlavə ödəniş çıxıla bilər"</string>
+    <string name="no_upstream_notification_title" msgid="5030042590486713460">"Modemin internetə girişi yoxdur"</string>
+    <string name="no_upstream_notification_message" msgid="3843613362272973447">"Cihazları qoşmaq mümkün deyil"</string>
+    <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"Modemi deaktiv edin"</string>
+    <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"Hotspot və ya modem aktivdir"</string>
+    <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"Rouminq zamanı əlavə ödənişlər tətbiq edilə bilər"</string>
 </resources>
diff --git a/Tethering/res/values-mcc310-mnc004-b+sr+Latn/strings.xml b/Tethering/res/values-mcc310-mnc004-b+sr+Latn/strings.xml
index a214f93..1abe4f3 100644
--- a/Tethering/res/values-mcc310-mnc004-b+sr+Latn/strings.xml
+++ b/Tethering/res/values-mcc310-mnc004-b+sr+Latn/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="3584617491053416666">"Privezivanje nema pristup internetu"</string>
-    <string name="no_upstream_notification_message" msgid="5626323795587558017">"Povezivanje uređaja nije uspelo"</string>
-    <string name="no_upstream_notification_disable_button" msgid="868677179945695858">"Isključi privezivanje"</string>
-    <string name="upstream_roaming_notification_title" msgid="2870229486619751829">"Uključen je hotspot ili privezivanje"</string>
-    <string name="upstream_roaming_notification_message" msgid="5229740963392849544">"Možda važe dodatni troškovi u romingu"</string>
+    <string name="no_upstream_notification_title" msgid="5030042590486713460">"Privezivanje nema pristup internetu"</string>
+    <string name="no_upstream_notification_message" msgid="3843613362272973447">"Povezivanje uređaja nije uspelo"</string>
+    <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"Isključi privezivanje"</string>
+    <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"Uključen je hotspot ili privezivanje"</string>
+    <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"Možda važe dodatni troškovi u romingu"</string>
 </resources>
diff --git a/Tethering/res/values-mcc310-mnc004-be/strings.xml b/Tethering/res/values-mcc310-mnc004-be/strings.xml
index 316e856..38dbd1e 100644
--- a/Tethering/res/values-mcc310-mnc004-be/strings.xml
+++ b/Tethering/res/values-mcc310-mnc004-be/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="3584617491053416666">"Рэжым мадэма выкарыстоўваецца без доступу да інтэрнэту"</string>
-    <string name="no_upstream_notification_message" msgid="5626323795587558017">"Не ўдалося падключыць прылады"</string>
-    <string name="no_upstream_notification_disable_button" msgid="868677179945695858">"Выключыць рэжым мадэма"</string>
-    <string name="upstream_roaming_notification_title" msgid="2870229486619751829">"Хот-спот або рэжым мадэма ўключаны"</string>
-    <string name="upstream_roaming_notification_message" msgid="5229740963392849544">"Пры выкарыстанні роўмінгу можа спаганяцца дадатковая плата"</string>
+    <string name="no_upstream_notification_title" msgid="5030042590486713460">"Рэжым мадэма выкарыстоўваецца без доступу да інтэрнэту"</string>
+    <string name="no_upstream_notification_message" msgid="3843613362272973447">"Не ўдалося падключыць прылады"</string>
+    <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"Выключыць рэжым мадэма"</string>
+    <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"Хот-спот або рэжым мадэма ўключаны"</string>
+    <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"Пры выкарыстанні роўмінгу можа спаганяцца дадатковая плата"</string>
 </resources>
diff --git a/Tethering/res/values-mcc310-mnc004-bg/strings.xml b/Tethering/res/values-mcc310-mnc004-bg/strings.xml
index a31c06a..04b44db 100644
--- a/Tethering/res/values-mcc310-mnc004-bg/strings.xml
+++ b/Tethering/res/values-mcc310-mnc004-bg/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="3584617491053416666">"Няма връзка с интернет за тетъринг"</string>
-    <string name="no_upstream_notification_message" msgid="5626323795587558017">"Устройствата не могат да установят връзка"</string>
-    <string name="no_upstream_notification_disable_button" msgid="868677179945695858">"Изключване на функцията за тетъринг"</string>
-    <string name="upstream_roaming_notification_title" msgid="2870229486619751829">"Точката за достъп или функцията за тетъринг са включени"</string>
-    <string name="upstream_roaming_notification_message" msgid="5229740963392849544">"Възможно е да ви бъдат начислени допълнителни такси при роуминг"</string>
+    <string name="no_upstream_notification_title" msgid="5030042590486713460">"Тетърингът няма връзка с интернет"</string>
+    <string name="no_upstream_notification_message" msgid="3843613362272973447">"Устройствата не могат да установят връзка"</string>
+    <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"Изключване на тетъринга"</string>
+    <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"Точката за достъп или тетърингът са включени"</string>
+    <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"Възможно е да ви бъдат начислени допълнителни такси при роуминг"</string>
 </resources>
diff --git a/Tethering/res/values-mcc310-mnc004-bn/strings.xml b/Tethering/res/values-mcc310-mnc004-bn/strings.xml
index f49b14c..579d1be 100644
--- a/Tethering/res/values-mcc310-mnc004-bn/strings.xml
+++ b/Tethering/res/values-mcc310-mnc004-bn/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="3584617491053416666">"টেথারিং করার জন্য কোনও ইন্টারনেট কানেকশন লাগে না"</string>
-    <string name="no_upstream_notification_message" msgid="5626323795587558017">"ডিভাইস কানেক্ট করা যাচ্ছে না"</string>
-    <string name="no_upstream_notification_disable_button" msgid="868677179945695858">"টেথারিং বন্ধ করুন"</string>
-    <string name="upstream_roaming_notification_title" msgid="2870229486619751829">"হটস্পট বা টেথারিং চালু আছে"</string>
-    <string name="upstream_roaming_notification_message" msgid="5229740963392849544">"রোমিংয়ে থাকার সময় অতিরিক্ত চার্জ লাগতে পারে"</string>
+    <string name="no_upstream_notification_title" msgid="5030042590486713460">"টিথারিং করার জন্য কোনও ইন্টারনেট কানেকশন নেই"</string>
+    <string name="no_upstream_notification_message" msgid="3843613362272973447">"ডিভাইস কানেক্ট করতে পারছে না"</string>
+    <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"টিথারিং বন্ধ করুন"</string>
+    <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"হটস্পট বা টিথারিং চালু আছে"</string>
+    <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"রোমিংয়ের সময় অতিরিক্ত চার্জ করা হতে পারে"</string>
 </resources>
diff --git a/Tethering/res/values-mcc310-mnc004-bs/strings.xml b/Tethering/res/values-mcc310-mnc004-bs/strings.xml
index ed269c6..9ce3efe 100644
--- a/Tethering/res/values-mcc310-mnc004-bs/strings.xml
+++ b/Tethering/res/values-mcc310-mnc004-bs/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="3584617491053416666">"Dijeljenje internetske veze nema internet"</string>
-    <string name="no_upstream_notification_message" msgid="5626323795587558017">"Nije moguće povezati uređaje"</string>
-    <string name="no_upstream_notification_disable_button" msgid="868677179945695858">"Isključi dijeljenje internetske veze"</string>
-    <string name="upstream_roaming_notification_title" msgid="2870229486619751829">"Pristupna tačka ili dijeljenje internetske veze su uključeni"</string>
-    <string name="upstream_roaming_notification_message" msgid="5229740963392849544">"Mogu nastati dodatni troškovi u romingu"</string>
+    <string name="no_upstream_notification_title" msgid="5030042590486713460">"Povezivanje putem mobitela nema internet"</string>
+    <string name="no_upstream_notification_message" msgid="3843613362272973447">"Uređaji se ne mogu povezati"</string>
+    <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"Isključi povezivanje putem mobitela"</string>
+    <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"Pristupna tačka ili povezivanje putem mobitela je uključeno"</string>
+    <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"Mogu nastati dodatni troškovi u romingu"</string>
 </resources>
diff --git a/Tethering/res/values-mcc310-mnc004-ca/strings.xml b/Tethering/res/values-mcc310-mnc004-ca/strings.xml
index 0826f4e..46d4c35 100644
--- a/Tethering/res/values-mcc310-mnc004-ca/strings.xml
+++ b/Tethering/res/values-mcc310-mnc004-ca/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="3584617491053416666">"La compartició de xarxa no té accés a Internet"</string>
-    <string name="no_upstream_notification_message" msgid="5626323795587558017">"No es poden connectar els dispositius"</string>
-    <string name="no_upstream_notification_disable_button" msgid="868677179945695858">"Desactiva la compartició de xarxa"</string>
-    <string name="upstream_roaming_notification_title" msgid="2870229486619751829">"El punt d\'accés Wi‑Fi o la compartició de xarxa estan activats"</string>
-    <string name="upstream_roaming_notification_message" msgid="5229740963392849544">"És possible que s\'apliquin costos addicionals en itinerància"</string>
+    <string name="no_upstream_notification_title" msgid="5030042590486713460">"La compartició de xarxa no té accés a Internet"</string>
+    <string name="no_upstream_notification_message" msgid="3843613362272973447">"No es poden connectar els dispositius"</string>
+    <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"Desactiva la compartició de xarxa"</string>
+    <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"S\'ha activat el punt d\'accés Wi‑Fi o la compartició de xarxa"</string>
+    <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"És possible que s\'apliquin costos addicionals en itinerància"</string>
 </resources>
diff --git a/Tethering/res/values-mcc310-mnc004-cs/strings.xml b/Tethering/res/values-mcc310-mnc004-cs/strings.xml
index 6899f71..cc13860 100644
--- a/Tethering/res/values-mcc310-mnc004-cs/strings.xml
+++ b/Tethering/res/values-mcc310-mnc004-cs/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="3584617491053416666">"Tethering nemá připojení k internetu"</string>
-    <string name="no_upstream_notification_message" msgid="5626323795587558017">"Zařízení se nemůžou připojit"</string>
-    <string name="no_upstream_notification_disable_button" msgid="868677179945695858">"Vypnout tethering"</string>
-    <string name="upstream_roaming_notification_title" msgid="2870229486619751829">"Je zapnutý hotspot nebo tethering"</string>
-    <string name="upstream_roaming_notification_message" msgid="5229740963392849544">"Při roamingu mohou být účtovány dodatečné poplatky"</string>
+    <string name="no_upstream_notification_title" msgid="5030042590486713460">"Tethering nemá připojení k internetu"</string>
+    <string name="no_upstream_notification_message" msgid="3843613362272973447">"Zařízení se nemůžou připojit"</string>
+    <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"Vypnout tethering"</string>
+    <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"Je zapnutý hotspot nebo tethering"</string>
+    <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"Při roamingu mohou být účtovány dodatečné poplatky"</string>
 </resources>
diff --git a/Tethering/res/values-mcc310-mnc004-da/strings.xml b/Tethering/res/values-mcc310-mnc004-da/strings.xml
index dbca93b..92c3ae1 100644
--- a/Tethering/res/values-mcc310-mnc004-da/strings.xml
+++ b/Tethering/res/values-mcc310-mnc004-da/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="3584617491053416666">"Netdeling har ingen internetforbindelse"</string>
-    <string name="no_upstream_notification_message" msgid="5626323795587558017">"Enheder kan ikke oprette forbindelse"</string>
-    <string name="no_upstream_notification_disable_button" msgid="868677179945695858">"Deaktiver netdeling"</string>
-    <string name="upstream_roaming_notification_title" msgid="2870229486619751829">"Hotspot eller netdeling er aktiveret"</string>
-    <string name="upstream_roaming_notification_message" msgid="5229740963392849544">"Der opkræves muligvis yderligere gebyrer ved roaming"</string>
+    <string name="no_upstream_notification_title" msgid="5030042590486713460">"Netdeling har ingen internetforbindelse"</string>
+    <string name="no_upstream_notification_message" msgid="3843613362272973447">"Enheder kan ikke oprette forbindelse"</string>
+    <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"Deaktiver netdeling"</string>
+    <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"Hotspot eller netdeling er aktiveret"</string>
+    <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"Der opkræves muligvis yderligere gebyrer ved roaming"</string>
 </resources>
diff --git a/Tethering/res/values-mcc310-mnc004-de/strings.xml b/Tethering/res/values-mcc310-mnc004-de/strings.xml
index 139b4e0..967eb4d 100644
--- a/Tethering/res/values-mcc310-mnc004-de/strings.xml
+++ b/Tethering/res/values-mcc310-mnc004-de/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="3584617491053416666">"Tethering hat keinen Internetzugriff"</string>
-    <string name="no_upstream_notification_message" msgid="5626323795587558017">"Geräte können sich nicht verbinden"</string>
-    <string name="no_upstream_notification_disable_button" msgid="868677179945695858">"Tethering deaktivieren"</string>
-    <string name="upstream_roaming_notification_title" msgid="2870229486619751829">"Hotspot oder Tethering ist aktiviert"</string>
-    <string name="upstream_roaming_notification_message" msgid="5229740963392849544">"Für das Roaming können zusätzliche Gebühren anfallen"</string>
+    <string name="no_upstream_notification_title" msgid="5030042590486713460">"Tethering hat keinen Internetzugriff"</string>
+    <string name="no_upstream_notification_message" msgid="3843613362272973447">"Geräte können sich nicht verbinden"</string>
+    <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"Tethering deaktivieren"</string>
+    <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"Hotspot oder Tethering ist aktiviert"</string>
+    <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"Für das Roaming können zusätzliche Gebühren anfallen"</string>
 </resources>
diff --git a/Tethering/res/values-mcc310-mnc004-el/strings.xml b/Tethering/res/values-mcc310-mnc004-el/strings.xml
index d778b03..5fb4974 100644
--- a/Tethering/res/values-mcc310-mnc004-el/strings.xml
+++ b/Tethering/res/values-mcc310-mnc004-el/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="3584617491053416666">"Η σύνδεση δεν έχει πρόσβαση στο διαδίκτυο"</string>
-    <string name="no_upstream_notification_message" msgid="5626323795587558017">"Δεν είναι δυνατή η σύνδεση των συσκευών"</string>
-    <string name="no_upstream_notification_disable_button" msgid="868677179945695858">"Απενεργοποίηση σύνδεσης"</string>
-    <string name="upstream_roaming_notification_title" msgid="2870229486619751829">"Ενεργό σημείο πρόσβασης Wi-Fi ή ενεργή σύνδεση"</string>
-    <string name="upstream_roaming_notification_message" msgid="5229740963392849544">"Ενδέχεται να ισχύουν επιπλέον χρεώσεις κατά την περιαγωγή."</string>
+    <string name="no_upstream_notification_title" msgid="5030042590486713460">"Η σύνδεση δεν έχει πρόσβαση στο διαδίκτυο"</string>
+    <string name="no_upstream_notification_message" msgid="3843613362272973447">"Δεν είναι δυνατή η σύνδεση των συσκευών"</string>
+    <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"Απενεργοποιήστε τη σύνδεση"</string>
+    <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"Ενεργό σημείο πρόσβασης Wi-Fi ή ενεργή σύνδεση"</string>
+    <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"Ενδέχεται να ισχύουν επιπλέον χρεώσεις κατά την περιαγωγή."</string>
 </resources>
diff --git a/Tethering/res/values-mcc310-mnc004-en-rAU/strings.xml b/Tethering/res/values-mcc310-mnc004-en-rAU/strings.xml
index bc68d00..45647f9 100644
--- a/Tethering/res/values-mcc310-mnc004-en-rAU/strings.xml
+++ b/Tethering/res/values-mcc310-mnc004-en-rAU/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="3584617491053416666">"Tethering has no Internet"</string>
-    <string name="no_upstream_notification_message" msgid="5626323795587558017">"Devices can\'t connect"</string>
-    <string name="no_upstream_notification_disable_button" msgid="868677179945695858">"Turn off tethering"</string>
-    <string name="upstream_roaming_notification_title" msgid="2870229486619751829">"Hotspot or tethering is on"</string>
-    <string name="upstream_roaming_notification_message" msgid="5229740963392849544">"Additional charges may apply while roaming"</string>
+    <string name="no_upstream_notification_title" msgid="5030042590486713460">"Tethering has no Internet"</string>
+    <string name="no_upstream_notification_message" msgid="3843613362272973447">"Devices can’t connect"</string>
+    <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"Turn off tethering"</string>
+    <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"Hotspot or tethering is on"</string>
+    <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"Additional charges may apply while roaming"</string>
 </resources>
diff --git a/Tethering/res/values-mcc310-mnc004-en-rCA/strings.xml b/Tethering/res/values-mcc310-mnc004-en-rCA/strings.xml
index 4f39489..45647f9 100644
--- a/Tethering/res/values-mcc310-mnc004-en-rCA/strings.xml
+++ b/Tethering/res/values-mcc310-mnc004-en-rCA/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="3584617491053416666">"Tethering has no internet"</string>
-    <string name="no_upstream_notification_message" msgid="5626323795587558017">"Devices can’t connect"</string>
-    <string name="no_upstream_notification_disable_button" msgid="868677179945695858">"Turn off tethering"</string>
-    <string name="upstream_roaming_notification_title" msgid="2870229486619751829">"Hotspot or tethering is on"</string>
-    <string name="upstream_roaming_notification_message" msgid="5229740963392849544">"Additional charges may apply while roaming"</string>
+    <string name="no_upstream_notification_title" msgid="5030042590486713460">"Tethering has no Internet"</string>
+    <string name="no_upstream_notification_message" msgid="3843613362272973447">"Devices can’t connect"</string>
+    <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"Turn off tethering"</string>
+    <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"Hotspot or tethering is on"</string>
+    <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"Additional charges may apply while roaming"</string>
 </resources>
diff --git a/Tethering/res/values-mcc310-mnc004-en-rGB/strings.xml b/Tethering/res/values-mcc310-mnc004-en-rGB/strings.xml
index bc68d00..45647f9 100644
--- a/Tethering/res/values-mcc310-mnc004-en-rGB/strings.xml
+++ b/Tethering/res/values-mcc310-mnc004-en-rGB/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="3584617491053416666">"Tethering has no Internet"</string>
-    <string name="no_upstream_notification_message" msgid="5626323795587558017">"Devices can\'t connect"</string>
-    <string name="no_upstream_notification_disable_button" msgid="868677179945695858">"Turn off tethering"</string>
-    <string name="upstream_roaming_notification_title" msgid="2870229486619751829">"Hotspot or tethering is on"</string>
-    <string name="upstream_roaming_notification_message" msgid="5229740963392849544">"Additional charges may apply while roaming"</string>
+    <string name="no_upstream_notification_title" msgid="5030042590486713460">"Tethering has no Internet"</string>
+    <string name="no_upstream_notification_message" msgid="3843613362272973447">"Devices can’t connect"</string>
+    <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"Turn off tethering"</string>
+    <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"Hotspot or tethering is on"</string>
+    <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"Additional charges may apply while roaming"</string>
 </resources>
diff --git a/Tethering/res/values-mcc310-mnc004-en-rIN/strings.xml b/Tethering/res/values-mcc310-mnc004-en-rIN/strings.xml
index bc68d00..45647f9 100644
--- a/Tethering/res/values-mcc310-mnc004-en-rIN/strings.xml
+++ b/Tethering/res/values-mcc310-mnc004-en-rIN/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="3584617491053416666">"Tethering has no Internet"</string>
-    <string name="no_upstream_notification_message" msgid="5626323795587558017">"Devices can\'t connect"</string>
-    <string name="no_upstream_notification_disable_button" msgid="868677179945695858">"Turn off tethering"</string>
-    <string name="upstream_roaming_notification_title" msgid="2870229486619751829">"Hotspot or tethering is on"</string>
-    <string name="upstream_roaming_notification_message" msgid="5229740963392849544">"Additional charges may apply while roaming"</string>
+    <string name="no_upstream_notification_title" msgid="5030042590486713460">"Tethering has no Internet"</string>
+    <string name="no_upstream_notification_message" msgid="3843613362272973447">"Devices can’t connect"</string>
+    <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"Turn off tethering"</string>
+    <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"Hotspot or tethering is on"</string>
+    <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"Additional charges may apply while roaming"</string>
 </resources>
diff --git a/Tethering/res/values-mcc310-mnc004-en-rXC/strings.xml b/Tethering/res/values-mcc310-mnc004-en-rXC/strings.xml
index be00edf..7877074 100644
--- a/Tethering/res/values-mcc310-mnc004-en-rXC/strings.xml
+++ b/Tethering/res/values-mcc310-mnc004-en-rXC/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="3584617491053416666">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‏‎‎‎‏‏‎‏‏‏‏‏‏‎‎‎‏‏‏‏‎‏‎‎‏‏‏‎‏‏‎‏‏‎‎‎‏‏‏‏‎‎‏‎‎‎‎‎‏‎‎‎‎‏‏‎‏‏‎‏‎‎Tethering has no internet‎‏‎‎‏‎"</string>
-    <string name="no_upstream_notification_message" msgid="5626323795587558017">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‎‏‏‏‎‎‎‎‏‎‏‎‎‏‎‏‏‎‏‏‏‏‎‏‎‏‏‏‎‏‎‏‏‏‎‏‏‎‏‎‏‎‎‏‎‏‏‏‎‏‎‏‎‏‎‎‎‎‎‎‏‎Devices can’t connect‎‏‎‎‏‎"</string>
-    <string name="no_upstream_notification_disable_button" msgid="868677179945695858">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‎‏‏‎‎‎‎‎‎‏‏‏‎‎‎‏‎‏‎‎‏‎‏‎‎‏‎‏‎‏‏‏‎‏‏‏‏‎‎‏‎‎‏‏‎‏‎‏‏‏‎‏‎‎‏‏‏‎‎‏‎‎Turn off tethering‎‏‎‎‏‎"</string>
-    <string name="upstream_roaming_notification_title" msgid="2870229486619751829">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‎‎‏‏‏‏‏‎‏‎‏‎‏‎‎‎‏‏‎‏‎‏‎‎‎‎‎‏‎‏‏‎‎‏‏‎‏‏‏‎‏‎‎‏‎‎‏‏‏‎‏‎‏‏‎‎‏‎‏‎‏‎Hotspot or tethering is on‎‏‎‎‏‎"</string>
-    <string name="upstream_roaming_notification_message" msgid="5229740963392849544">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‎‏‎‎‎‏‎‎‏‎‎‏‏‏‏‎‎‎‏‎‏‏‎‏‏‎‎‏‎‏‎‏‎‎‏‏‎‎‏‎‏‎‏‏‏‎‎‎‎‎‏‏‎‏‎‎‎‏‎‎‎‎Additional charges may apply while roaming‎‏‎‎‏‎"</string>
+    <string name="no_upstream_notification_title" msgid="5030042590486713460">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‎‎‏‎‏‏‏‎‎‏‏‏‎‎‏‎‎‏‏‎‏‎‎‎‏‎‏‎‏‏‏‏‏‎‎‏‏‎‎‎‎‏‎‏‏‏‏‎‏‎‏‎‎‎‏‏‏‎‏‎‎‎Tethering has no internet‎‏‎‎‏‎"</string>
+    <string name="no_upstream_notification_message" msgid="3843613362272973447">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‏‎‏‎‏‎‏‎‏‎‏‏‏‎‏‎‎‎‎‎‏‏‏‏‏‏‏‎‏‎‎‎‏‏‎‎‎‎‎‏‏‏‎‎‏‏‎‏‏‎‎‏‎‏‎‎‎‎‏‏‏‎Devices can’t connect‎‏‎‎‏‎"</string>
+    <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‏‏‎‎‎‏‎‎‏‏‏‎‏‏‏‎‏‎‎‏‎‏‎‏‎‎‏‏‏‎‎‎‏‏‎‎‏‏‎‏‎‏‏‏‏‎‎‎‏‎‏‏‎‎‎‏‎‏‎‎‎‎Turn off tethering‎‏‎‎‏‎"</string>
+    <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‎‏‎‎‏‏‏‎‏‏‎‏‎‏‎‏‎‏‏‎‎‎‎‏‎‎‎‏‏‎‎‎‎‎‎‎‎‏‎‎‏‏‏‏‏‏‏‏‏‎‎‎‏‏‎‏‎‎‏‏‏‎Hotspot or tethering is on‎‏‎‎‏‎"</string>
+    <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‏‏‏‎‏‎‏‎‏‎‎‎‏‏‏‏‏‏‏‎‏‏‎‏‏‏‏‎‎‏‏‏‏‏‎‏‏‎‎‎‎‏‏‎‎‎‏‏‏‏‏‎‏‎‏‎‏‏‏‏‎‎Additional charges may apply while roaming‎‏‎‎‏‎"</string>
 </resources>
diff --git a/Tethering/res/values-mcc310-mnc004-es-rUS/strings.xml b/Tethering/res/values-mcc310-mnc004-es-rUS/strings.xml
index e00a7a0..08edd81 100644
--- a/Tethering/res/values-mcc310-mnc004-es-rUS/strings.xml
+++ b/Tethering/res/values-mcc310-mnc004-es-rUS/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="3584617491053416666">"La conexión mediante dispositivo móvil no tiene Internet"</string>
-    <string name="no_upstream_notification_message" msgid="5626323795587558017">"No se pueden conectar los dispositivos"</string>
-    <string name="no_upstream_notification_disable_button" msgid="868677179945695858">"Desactivar conexión mediante dispositivo móvil"</string>
-    <string name="upstream_roaming_notification_title" msgid="2870229486619751829">"Se activó el hotspot o la conexión mediante dispositivo móvil"</string>
-    <string name="upstream_roaming_notification_message" msgid="5229740963392849544">"Es posible que se apliquen cargos adicionales por roaming"</string>
+    <string name="no_upstream_notification_title" msgid="5030042590486713460">"La conexión mediante dispositivo móvil no tiene Internet"</string>
+    <string name="no_upstream_notification_message" msgid="3843613362272973447">"No se pueden conectar los dispositivos"</string>
+    <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"Desactivar conexión mediante dispositivo móvil"</string>
+    <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"Se activó el hotspot o la conexión mediante dispositivo móvil"</string>
+    <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"Es posible que se apliquen cargos adicionales por roaming"</string>
 </resources>
diff --git a/Tethering/res/values-mcc310-mnc004-es/strings.xml b/Tethering/res/values-mcc310-mnc004-es/strings.xml
index 6c7e983..79f51d0 100644
--- a/Tethering/res/values-mcc310-mnc004-es/strings.xml
+++ b/Tethering/res/values-mcc310-mnc004-es/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="3584617491053416666">"La conexión no se puede compartir, porque no hay acceso a Internet"</string>
-    <string name="no_upstream_notification_message" msgid="5626323795587558017">"Los dispositivos no se pueden conectar"</string>
-    <string name="no_upstream_notification_disable_button" msgid="868677179945695858">"Desactivar conexión compartida"</string>
-    <string name="upstream_roaming_notification_title" msgid="2870229486619751829">"Punto de acceso o conexión compartida activados"</string>
-    <string name="upstream_roaming_notification_message" msgid="5229740963392849544">"Pueden aplicarse cargos adicionales en roaming"</string>
+    <string name="no_upstream_notification_title" msgid="5030042590486713460">"La conexión no se puede compartir, porque no hay acceso a Internet"</string>
+    <string name="no_upstream_notification_message" msgid="3843613362272973447">"Los dispositivos no se pueden conectar"</string>
+    <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"Desactivar conexión compartida"</string>
+    <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"Punto de acceso o conexión compartida activados"</string>
+    <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"Puede que se apliquen cargos adicionales en itinerancia"</string>
 </resources>
diff --git a/Tethering/res/values-mcc310-mnc004-et/strings.xml b/Tethering/res/values-mcc310-mnc004-et/strings.xml
index 2f108fc..2da5f8a 100644
--- a/Tethering/res/values-mcc310-mnc004-et/strings.xml
+++ b/Tethering/res/values-mcc310-mnc004-et/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="3584617491053416666">"Jagamisel puudub internetiühendus"</string>
-    <string name="no_upstream_notification_message" msgid="5626323795587558017">"Seadmed ei saa ühendust luua"</string>
-    <string name="no_upstream_notification_disable_button" msgid="868677179945695858">"Lülita jagamine välja"</string>
-    <string name="upstream_roaming_notification_title" msgid="2870229486619751829">"Kuumkoht või jagamine on sisse lülitatud"</string>
-    <string name="upstream_roaming_notification_message" msgid="5229740963392849544">"Rändluse kasutamisega võivad kaasneda lisatasud"</string>
+    <string name="no_upstream_notification_title" msgid="5030042590486713460">"Jagamisel puudub internetiühendus"</string>
+    <string name="no_upstream_notification_message" msgid="3843613362272973447">"Seadmed ei saa ühendust luua"</string>
+    <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"Lülita jagamine välja"</string>
+    <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"Kuumkoht või jagamine on sisse lülitatud"</string>
+    <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"Rändluse kasutamisega võivad kaasneda lisatasud"</string>
 </resources>
diff --git a/Tethering/res/values-mcc310-mnc004-eu/strings.xml b/Tethering/res/values-mcc310-mnc004-eu/strings.xml
index c970dd7..2073f28 100644
--- a/Tethering/res/values-mcc310-mnc004-eu/strings.xml
+++ b/Tethering/res/values-mcc310-mnc004-eu/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     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="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>
+    <string name="no_upstream_notification_title" msgid="5030042590486713460">"Konexioa partekatzeko aukerak ez du Interneteko konexiorik"</string>
+    <string name="no_upstream_notification_message" msgid="3843613362272973447">"Ezin dira konektatu gailuak"</string>
+    <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"Desaktibatu konexioa partekatzeko aukera"</string>
+    <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"Wifi-gunea edo konexioa partekatzeko aukera aktibatuta dago"</string>
+    <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"Baliteke kostu gehigarriak ordaindu behar izatea ibiltaritzan"</string>
 </resources>
diff --git a/Tethering/res/values-mcc310-mnc004-fa/strings.xml b/Tethering/res/values-mcc310-mnc004-fa/strings.xml
index 7333e2f..e21b2a0 100644
--- a/Tethering/res/values-mcc310-mnc004-fa/strings.xml
+++ b/Tethering/res/values-mcc310-mnc004-fa/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="3584617491053416666">"«اشتراک‌گذاری اینترنت» به اینترنت دسترسی ندارد"</string>
-    <string name="no_upstream_notification_message" msgid="5626323795587558017">"دستگاه‌ها متصل نمی‌شوند"</string>
-    <string name="no_upstream_notification_disable_button" msgid="868677179945695858">"خاموش کردن «اشتراک‌گذاری اینترنت»"</string>
-    <string name="upstream_roaming_notification_title" msgid="2870229486619751829">"«نقطه اتصال» یا «اشتراک‌گذاری اینترنت» روشن است"</string>
-    <string name="upstream_roaming_notification_message" msgid="5229740963392849544">"هنگام فراگردی ممکن است هزینه‌های اضافی کسر شود"</string>
+    <string name="no_upstream_notification_title" msgid="5030042590486713460">"«اشتراک‌گذاری اینترنت» به اینترنت دسترسی ندارد"</string>
+    <string name="no_upstream_notification_message" msgid="3843613362272973447">"دستگاه‌ها متصل نمی‌شوند"</string>
+    <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"خاموش کردن «اشتراک‌گذاری اینترنت»"</string>
+    <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"«نقطه اتصال» یا «اشتراک‌گذاری اینترنت» روشن است"</string>
+    <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"ممکن است درحین فراگردی تغییرات دیگر اعمال شود"</string>
 </resources>
diff --git a/Tethering/res/values-mcc310-mnc004-fi/strings.xml b/Tethering/res/values-mcc310-mnc004-fi/strings.xml
index 3faed5b..88b0b13 100644
--- a/Tethering/res/values-mcc310-mnc004-fi/strings.xml
+++ b/Tethering/res/values-mcc310-mnc004-fi/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="3584617491053416666">"Puhelinta ei voi käyttää modeemina, koska sillä ei ole internet-yhteyttä"</string>
-    <string name="no_upstream_notification_message" msgid="5626323795587558017">"Laitteet eivät voi muodostaa yhteyttä"</string>
-    <string name="no_upstream_notification_disable_button" msgid="868677179945695858">"Laita puhelimen käyttäminen modeemina pois päältä"</string>
-    <string name="upstream_roaming_notification_title" msgid="2870229486619751829">"Hotspot tai puhelimen käyttäminen modeemina on päällä"</string>
-    <string name="upstream_roaming_notification_message" msgid="5229740963392849544">"Roaming voi aiheuttaa lisämaksuja"</string>
+    <string name="no_upstream_notification_title" msgid="5030042590486713460">"Ei jaettavaa internetyhteyttä"</string>
+    <string name="no_upstream_notification_message" msgid="3843613362272973447">"Laitteet eivät voi muodostaa yhteyttä"</string>
+    <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"Laita yhteyden jakaminen pois päältä"</string>
+    <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"Hotspot tai yhteyden jakaminen on päällä"</string>
+    <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"Roaming voi aiheuttaa lisämaksuja"</string>
 </resources>
diff --git a/Tethering/res/values-mcc310-mnc004-fr-rCA/strings.xml b/Tethering/res/values-mcc310-mnc004-fr-rCA/strings.xml
index 0659c40..3b781bc 100644
--- a/Tethering/res/values-mcc310-mnc004-fr-rCA/strings.xml
+++ b/Tethering/res/values-mcc310-mnc004-fr-rCA/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="3584617491053416666">"Le partage de connexion n\'est pas connecté à Internet"</string>
-    <string name="no_upstream_notification_message" msgid="5626323795587558017">"Impossible de connecter les appareils"</string>
-    <string name="no_upstream_notification_disable_button" msgid="868677179945695858">"Désactiver le partage de connexion"</string>
-    <string name="upstream_roaming_notification_title" msgid="2870229486619751829">"Le point d\'accès sans fil ou le partage de connexion est activé"</string>
-    <string name="upstream_roaming_notification_message" msgid="5229740963392849544">"En itinérance, des frais supplémentaires peuvent s\'appliquer"</string>
+    <string name="no_upstream_notification_title" msgid="5030042590486713460">"Le partage de connexion n\'est pas connecté à Internet"</string>
+    <string name="no_upstream_notification_message" msgid="3843613362272973447">"Impossible de connecter les appareils"</string>
+    <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"Désactiver le partage de connexion"</string>
+    <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"Le point d\'accès ou le partage de connexion est activé"</string>
+    <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"En itinérance, des frais supplémentaires peuvent s\'appliquer"</string>
 </resources>
diff --git a/Tethering/res/values-mcc310-mnc004-fr/strings.xml b/Tethering/res/values-mcc310-mnc004-fr/strings.xml
index 26065f8..51d7203 100644
--- a/Tethering/res/values-mcc310-mnc004-fr/strings.xml
+++ b/Tethering/res/values-mcc310-mnc004-fr/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="3584617491053416666">"Aucune connexion à Internet disponible pour le partage de connexion"</string>
-    <string name="no_upstream_notification_message" msgid="5626323795587558017">"Impossible de connecter les appareils"</string>
-    <string name="no_upstream_notification_disable_button" msgid="868677179945695858">"Désactiver le partage de connexion"</string>
-    <string name="upstream_roaming_notification_title" msgid="2870229486619751829">"Le point d\'accès ou le partage de connexion est activé"</string>
-    <string name="upstream_roaming_notification_message" msgid="5229740963392849544">"En itinérance, des frais supplémentaires peuvent s\'appliquer"</string>
+    <string name="no_upstream_notification_title" msgid="5030042590486713460">"Aucune connexion à Internet n\'est disponible pour le partage de connexion"</string>
+    <string name="no_upstream_notification_message" msgid="3843613362272973447">"Impossible de connecter les appareils"</string>
+    <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"Désactiver le partage de connexion"</string>
+    <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"Le point d\'accès ou le partage de connexion est activé"</string>
+    <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"En itinérance, des frais supplémentaires peuvent s\'appliquer"</string>
 </resources>
diff --git a/Tethering/res/values-mcc310-mnc004-gl/strings.xml b/Tethering/res/values-mcc310-mnc004-gl/strings.xml
index 924e71b..008ccb4 100644
--- a/Tethering/res/values-mcc310-mnc004-gl/strings.xml
+++ b/Tethering/res/values-mcc310-mnc004-gl/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="3584617491053416666">"A conexión compartida non ten acceso a Internet"</string>
-    <string name="no_upstream_notification_message" msgid="5626323795587558017">"Non se puideron conectar os dispositivos"</string>
-    <string name="no_upstream_notification_disable_button" msgid="868677179945695858">"Desactivar conexión compartida"</string>
-    <string name="upstream_roaming_notification_title" msgid="2870229486619751829">"Está activada a zona wifi ou a conexión compartida"</string>
-    <string name="upstream_roaming_notification_message" msgid="5229740963392849544">"Pódense aplicar cargos adicionais en itinerancia"</string>
+    <string name="no_upstream_notification_title" msgid="5030042590486713460">"A conexión compartida non ten Internet"</string>
+    <string name="no_upstream_notification_message" msgid="3843613362272973447">"Non se puideron conectar os dispositivos"</string>
+    <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"Desactivar conexión compartida"</string>
+    <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"Está activada a zona wifi ou a conexión compartida"</string>
+    <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"Pódense aplicar cargos adicionais en itinerancia"</string>
 </resources>
diff --git a/Tethering/res/values-mcc310-mnc004-gu/strings.xml b/Tethering/res/values-mcc310-mnc004-gu/strings.xml
index ab446df..f2e3b4d 100644
--- a/Tethering/res/values-mcc310-mnc004-gu/strings.xml
+++ b/Tethering/res/values-mcc310-mnc004-gu/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="3584617491053416666">"ઇન્ટરનેટ શેર કરવાની સુવિધામાં ઇન્ટરનેટ નથી"</string>
-    <string name="no_upstream_notification_message" msgid="5626323795587558017">"ડિવાઇસ કનેક્ટ કરી શકાતા નથી"</string>
-    <string name="no_upstream_notification_disable_button" msgid="868677179945695858">"ઇન્ટરનેટ શેર કરવાની સુવિધા બંધ કરો"</string>
-    <string name="upstream_roaming_notification_title" msgid="2870229486619751829">"હૉટસ્પૉટ અથવા ઇન્ટરનેટ શેર કરવાની સુવિધા ચાલુ છે"</string>
-    <string name="upstream_roaming_notification_message" msgid="5229740963392849544">"રોમિંગ દરમિયાન વધારાના શુલ્ક લાગુ થઈ શકે છે"</string>
+    <string name="no_upstream_notification_title" msgid="5030042590486713460">"ઇન્ટરનેટ શેર કરવાની સુવિધામાં ઇન્ટરનેટ નથી"</string>
+    <string name="no_upstream_notification_message" msgid="3843613362272973447">"ડિવાઇસ કનેક્ટ કરી શકાતા નથી"</string>
+    <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"ઇન્ટરનેટ શેર કરવાની સુવિધા બંધ કરો"</string>
+    <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"હૉટસ્પૉટ અથવા ઇન્ટરનેટ શેર કરવાની સુવિધા ચાલુ છે"</string>
+    <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"રોમિંગમાં વધારાના શુલ્ક લાગી શકે છે"</string>
 </resources>
diff --git a/Tethering/res/values-mcc310-mnc004-hi/strings.xml b/Tethering/res/values-mcc310-mnc004-hi/strings.xml
index 073a680..b11839d 100644
--- a/Tethering/res/values-mcc310-mnc004-hi/strings.xml
+++ b/Tethering/res/values-mcc310-mnc004-hi/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="3584617491053416666">"टेदरिंग से इंटरनेट नहीं चल रहा है"</string>
-    <string name="no_upstream_notification_message" msgid="5626323795587558017">"डिवाइस कनेक्ट नहीं हो पा रहे"</string>
-    <string name="no_upstream_notification_disable_button" msgid="868677179945695858">"टेदरिंग बंद करें"</string>
-    <string name="upstream_roaming_notification_title" msgid="2870229486619751829">"हॉटस्पॉट या टेदरिंग चालू है"</string>
-    <string name="upstream_roaming_notification_message" msgid="5229740963392849544">"रोमिंग के दौरान अतिरिक्त शुल्क काटा जा सकता है"</string>
+    <string name="no_upstream_notification_title" msgid="5030042590486713460">"टेदरिंग से इंटरनेट नहीं चल रहा"</string>
+    <string name="no_upstream_notification_message" msgid="3843613362272973447">"डिवाइस कनेक्ट नहीं हो पा रहे"</string>
+    <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"टेदरिंग बंद करें"</string>
+    <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"हॉटस्पॉट या टेदरिंग चालू है"</string>
+    <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"रोमिंग के दौरान अतिरिक्त शुल्क लग सकता है"</string>
 </resources>
diff --git a/Tethering/res/values-mcc310-mnc004-hr/strings.xml b/Tethering/res/values-mcc310-mnc004-hr/strings.xml
index 6cc8415..0a5aca2 100644
--- a/Tethering/res/values-mcc310-mnc004-hr/strings.xml
+++ b/Tethering/res/values-mcc310-mnc004-hr/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="3584617491053416666">"Modemsko povezivanje nema internet"</string>
-    <string name="no_upstream_notification_message" msgid="5626323795587558017">"Uređaji se ne mogu povezati"</string>
-    <string name="no_upstream_notification_disable_button" msgid="868677179945695858">"Isključi modemsko povezivanje"</string>
-    <string name="upstream_roaming_notification_title" msgid="2870229486619751829">"Uključena je žarišna točka ili modemsko povezivanje"</string>
-    <string name="upstream_roaming_notification_message" msgid="5229740963392849544">"U roamingu su mogući dodatni troškovi"</string>
+    <string name="no_upstream_notification_title" msgid="5030042590486713460">"Modemsko povezivanje nema internet"</string>
+    <string name="no_upstream_notification_message" msgid="3843613362272973447">"Uređaji se ne mogu povezati"</string>
+    <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"Isključivanje modemskog povezivanja"</string>
+    <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"Uključena je žarišna točka ili modemsko povezivanje"</string>
+    <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"U roamingu su mogući dodatni troškovi"</string>
 </resources>
diff --git a/Tethering/res/values-mcc310-mnc004-hu/strings.xml b/Tethering/res/values-mcc310-mnc004-hu/strings.xml
index 6ab9565..21c689a 100644
--- a/Tethering/res/values-mcc310-mnc004-hu/strings.xml
+++ b/Tethering/res/values-mcc310-mnc004-hu/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="3584617491053416666">"Nincs internetkapcsolat az internet megosztásához"</string>
-    <string name="no_upstream_notification_message" msgid="5626323795587558017">"Az eszközök nem tudnak csatlakozni"</string>
-    <string name="no_upstream_notification_disable_button" msgid="868677179945695858">"Internetmegosztás kikapcsolása"</string>
-    <string name="upstream_roaming_notification_title" msgid="2870229486619751829">"A hotspot vagy az internetmegosztás be van kapcsolva"</string>
-    <string name="upstream_roaming_notification_message" msgid="5229740963392849544">"Roaming során további díjak léphetnek fel"</string>
+    <string name="no_upstream_notification_title" msgid="5030042590486713460">"Nincs internetkapcsolat az internet megosztásához"</string>
+    <string name="no_upstream_notification_message" msgid="3843613362272973447">"Az eszközök nem tudnak csatlakozni"</string>
+    <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"Internetmegosztás kikapcsolása"</string>
+    <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"A hotspot vagy az internetmegosztás be van kapcsolva"</string>
+    <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"Roaming során további díjak léphetnek fel"</string>
 </resources>
diff --git a/Tethering/res/values-mcc310-mnc004-hy/strings.xml b/Tethering/res/values-mcc310-mnc004-hy/strings.xml
index 75b1c3e..689d928 100644
--- a/Tethering/res/values-mcc310-mnc004-hy/strings.xml
+++ b/Tethering/res/values-mcc310-mnc004-hy/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="3584617491053416666">"Մոդեմի ռեժիմի ինտերնետ կապը բացակայում է"</string>
-    <string name="no_upstream_notification_message" msgid="5626323795587558017">"Չհաջողվեց միացնել սարքերը"</string>
-    <string name="no_upstream_notification_disable_button" msgid="868677179945695858">"Անջատել մոդեմի ռեժիմը"</string>
-    <string name="upstream_roaming_notification_title" msgid="2870229486619751829">"Թեժ կետը կամ մոդեմի ռեժիմը միացված է"</string>
-    <string name="upstream_roaming_notification_message" msgid="5229740963392849544">"Ռոումինգում կարող են լրացուցիչ վճարներ գանձվել"</string>
+    <string name="no_upstream_notification_title" msgid="5030042590486713460">"Մոդեմի ռեժիմի կապը բացակայում է"</string>
+    <string name="no_upstream_notification_message" msgid="3843613362272973447">"Չհաջողվեց միացնել սարքը"</string>
+    <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"Անջատել մոդեմի ռեժիմը"</string>
+    <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"Թեժ կետը կամ մոդեմի ռեժիմը միացված է"</string>
+    <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"Ռոումինգում կարող են լրացուցիչ վճարներ գանձվել"</string>
 </resources>
diff --git a/Tethering/res/values-mcc310-mnc004-in/strings.xml b/Tethering/res/values-mcc310-mnc004-in/strings.xml
index 7289d63..a5f4d19 100644
--- a/Tethering/res/values-mcc310-mnc004-in/strings.xml
+++ b/Tethering/res/values-mcc310-mnc004-in/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="3584617491053416666">"Tidak ada koneksi internet di tethering"</string>
-    <string name="no_upstream_notification_message" msgid="5626323795587558017">"Perangkat tidak dapat terhubung"</string>
-    <string name="no_upstream_notification_disable_button" msgid="868677179945695858">"Nonaktifkan tethering"</string>
-    <string name="upstream_roaming_notification_title" msgid="2870229486619751829">"Hotspot atau tethering aktif"</string>
-    <string name="upstream_roaming_notification_message" msgid="5229740963392849544">"Biaya tambahan mungkin berlaku saat roaming"</string>
+    <string name="no_upstream_notification_title" msgid="5030042590486713460">"Tidak ada koneksi internet di tethering"</string>
+    <string name="no_upstream_notification_message" msgid="3843613362272973447">"Perangkat tidak dapat terhubung"</string>
+    <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"Nonaktifkan tethering"</string>
+    <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"Hotspot atau tethering aktif"</string>
+    <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"Biaya tambahan mungkin berlaku saat roaming"</string>
 </resources>
diff --git a/Tethering/res/values-mcc310-mnc004-is/strings.xml b/Tethering/res/values-mcc310-mnc004-is/strings.xml
index e2f2f9d..fc7e8aa 100644
--- a/Tethering/res/values-mcc310-mnc004-is/strings.xml
+++ b/Tethering/res/values-mcc310-mnc004-is/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="3584617491053416666">"Tjóðrun er ekki með internettengingu"</string>
-    <string name="no_upstream_notification_message" msgid="5626323795587558017">"Tæki geta ekki tengst"</string>
-    <string name="no_upstream_notification_disable_button" msgid="868677179945695858">"Slökkva á tjóðrun"</string>
-    <string name="upstream_roaming_notification_title" msgid="2870229486619751829">"Kveikt er á heitum reit eða tjóðrun"</string>
-    <string name="upstream_roaming_notification_message" msgid="5229740963392849544">"Viðbótargjöld kunna að eiga við í reiki"</string>
+    <string name="no_upstream_notification_title" msgid="5030042590486713460">"Tjóðrun er ekki með internettengingu"</string>
+    <string name="no_upstream_notification_message" msgid="3843613362272973447">"Tæki geta ekki tengst"</string>
+    <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"Slökkva á tjóðrun"</string>
+    <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"Kveikt er á heitum reit eða tjóðrun"</string>
+    <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"Viðbótargjöld kunna að eiga við í reiki"</string>
 </resources>
diff --git a/Tethering/res/values-mcc310-mnc004-it/strings.xml b/Tethering/res/values-mcc310-mnc004-it/strings.xml
index 44805bd..6456dd1 100644
--- a/Tethering/res/values-mcc310-mnc004-it/strings.xml
+++ b/Tethering/res/values-mcc310-mnc004-it/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="3584617491053416666">"Nessuna connessione a internet per il tethering"</string>
-    <string name="no_upstream_notification_message" msgid="5626323795587558017">"Impossibile connettere i dispositivi"</string>
-    <string name="no_upstream_notification_disable_button" msgid="868677179945695858">"Disattiva il tethering"</string>
-    <string name="upstream_roaming_notification_title" msgid="2870229486619751829">"Hotspot o tethering attivo"</string>
-    <string name="upstream_roaming_notification_message" msgid="5229740963392849544">"Potrebbero essere applicati costi aggiuntivi durante il roaming"</string>
+    <string name="no_upstream_notification_title" msgid="5030042590486713460">"Nessuna connessione a Internet per il tethering"</string>
+    <string name="no_upstream_notification_message" msgid="3843613362272973447">"Impossibile connettere i dispositivi"</string>
+    <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"Disattiva il tethering"</string>
+    <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"Hotspot o tethering attivi"</string>
+    <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"Potrebbero essere applicati costi aggiuntivi durante il roaming"</string>
 </resources>
diff --git a/Tethering/res/values-mcc310-mnc004-iw/strings.xml b/Tethering/res/values-mcc310-mnc004-iw/strings.xml
index 0618169..46b24bd 100644
--- a/Tethering/res/values-mcc310-mnc004-iw/strings.xml
+++ b/Tethering/res/values-mcc310-mnc004-iw/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="3584617491053416666">"אי אפשר להפעיל את תכונת שיתוף האינטרנט בין מכשירים כי אין חיבור לאינטרנט"</string>
-    <string name="no_upstream_notification_message" msgid="5626323795587558017">"למכשירים אין אפשרות להתחבר"</string>
-    <string name="no_upstream_notification_disable_button" msgid="868677179945695858">"השבתה של שיתוף האינטרנט בין מכשירים"</string>
-    <string name="upstream_roaming_notification_title" msgid="2870229486619751829">"תכונת הנקודה לשיתוף אינטרנט או תכונת שיתוף האינטרנט בין מכשירים פועלת"</string>
-    <string name="upstream_roaming_notification_message" msgid="5229740963392849544">"ייתכנו חיובים נוספים במהלך נדידה"</string>
+    <string name="no_upstream_notification_title" msgid="5030042590486713460">"אי אפשר להפעיל את תכונת שיתוף האינטרנט בין מכשירים כי אין חיבור לאינטרנט"</string>
+    <string name="no_upstream_notification_message" msgid="3843613362272973447">"למכשירים אין אפשרות להתחבר"</string>
+    <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"השבתה של שיתוף האינטרנט בין מכשירים"</string>
+    <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"תכונת הנקודה לשיתוף אינטרנט או תכונת שיתוף האינטרנט בין מכשירים פועלת"</string>
+    <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"ייתכנו חיובים נוספים בעת נדידה"</string>
 </resources>
diff --git a/Tethering/res/values-mcc310-mnc004-ja/strings.xml b/Tethering/res/values-mcc310-mnc004-ja/strings.xml
index 344167d..e6eb277 100644
--- a/Tethering/res/values-mcc310-mnc004-ja/strings.xml
+++ b/Tethering/res/values-mcc310-mnc004-ja/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="3584617491053416666">"テザリングがインターネットに接続されていません"</string>
-    <string name="no_upstream_notification_message" msgid="5626323795587558017">"デバイスを接続できません"</string>
-    <string name="no_upstream_notification_disable_button" msgid="868677179945695858">"テザリングを OFF にする"</string>
-    <string name="upstream_roaming_notification_title" msgid="2870229486619751829">"アクセス ポイントまたはテザリングが ON です"</string>
-    <string name="upstream_roaming_notification_message" msgid="5229740963392849544">"ローミング時に追加料金が発生することがあります"</string>
+    <string name="no_upstream_notification_title" msgid="5030042590486713460">"テザリングがインターネットに接続されていません"</string>
+    <string name="no_upstream_notification_message" msgid="3843613362272973447">"デバイスを接続できません"</string>
+    <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"テザリングを OFF にする"</string>
+    <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"アクセス ポイントまたはテザリングが ON です"</string>
+    <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"ローミング時に追加料金が発生することがあります"</string>
 </resources>
diff --git a/Tethering/res/values-mcc310-mnc004-ka/strings.xml b/Tethering/res/values-mcc310-mnc004-ka/strings.xml
index 20db7fc..aeddd71 100644
--- a/Tethering/res/values-mcc310-mnc004-ka/strings.xml
+++ b/Tethering/res/values-mcc310-mnc004-ka/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="3584617491053416666">"ტეტერინგს არ აქვს ინტერნეტზე წვდომა"</string>
-    <string name="no_upstream_notification_message" msgid="5626323795587558017">"მოწყობილობები ვერ ახერხებენ დაკავშირებას"</string>
-    <string name="no_upstream_notification_disable_button" msgid="868677179945695858">"ტეტერინგის გამორთვა"</string>
-    <string name="upstream_roaming_notification_title" msgid="2870229486619751829">"ჩართულია უსადენო ქსელი ან ტეტერინგი"</string>
-    <string name="upstream_roaming_notification_message" msgid="5229740963392849544">"როუმინგის გამოყენებისას შეიძლება ჩამოგეჭრათ დამატებითი საფასური"</string>
+    <string name="no_upstream_notification_title" msgid="5030042590486713460">"ტეტერინგს არ აქვს ინტერნეტზე წვდომა"</string>
+    <string name="no_upstream_notification_message" msgid="3843613362272973447">"მოწყობილობები ვერ ახერხებენ დაკავშირებას"</string>
+    <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"ტეტერინგის გამორთვა"</string>
+    <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"ჩართულია უსადენო ქსელი ან ტეტერინგი"</string>
+    <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"როუმინგის გამოყენებისას შეიძლება ჩამოგეჭრათ დამატებითი საფასური"</string>
 </resources>
diff --git a/Tethering/res/values-mcc310-mnc004-kk/strings.xml b/Tethering/res/values-mcc310-mnc004-kk/strings.xml
index 35f1738..255f0a2 100644
--- a/Tethering/res/values-mcc310-mnc004-kk/strings.xml
+++ b/Tethering/res/values-mcc310-mnc004-kk/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="3584617491053416666">"Тетеринг кезінде интернет байланысы жоқ"</string>
-    <string name="no_upstream_notification_message" msgid="5626323795587558017">"Құрылғыларды байланыстыру мүмкін емес"</string>
-    <string name="no_upstream_notification_disable_button" msgid="868677179945695858">"Тетерингіні өшіру"</string>
-    <string name="upstream_roaming_notification_title" msgid="2870229486619751829">"Хотспот немесе тетеринг қосулы"</string>
-    <string name="upstream_roaming_notification_message" msgid="5229740963392849544">"Роуминг кезінде қосымша ақы алынуы мүмкін."</string>
+    <string name="no_upstream_notification_title" msgid="5030042590486713460">"Тетеринг режимі интернет байланысынсыз пайдаланылуда"</string>
+    <string name="no_upstream_notification_message" msgid="3843613362272973447">"Құрылғыларды байланыстыру мүмкін емес"</string>
+    <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"Тетерингіні өшіру"</string>
+    <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"Хотспот немесе тетеринг қосулы"</string>
+    <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"Роуминг кезінде қосымша ақы алынуы мүмкін."</string>
 </resources>
diff --git a/Tethering/res/values-mcc310-mnc004-km/strings.xml b/Tethering/res/values-mcc310-mnc004-km/strings.xml
index 2af80b1..2bceb1c 100644
--- a/Tethering/res/values-mcc310-mnc004-km/strings.xml
+++ b/Tethering/res/values-mcc310-mnc004-km/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="3584617491053416666">"ការភ្ជាប់​មិនមានអ៊ីនធឺណិត​ទេ"</string>
-    <string name="no_upstream_notification_message" msgid="5626323795587558017">"មិនអាច​ភ្ជាប់ឧបករណ៍​បានទេ"</string>
-    <string name="no_upstream_notification_disable_button" msgid="868677179945695858">"បិទការភ្ជាប់"</string>
-    <string name="upstream_roaming_notification_title" msgid="2870229486619751829">"ហតស្ប៉ត ឬការភ្ជាប់​ត្រូវបានបើក"</string>
-    <string name="upstream_roaming_notification_message" msgid="5229740963392849544">"អាចមាន​ការគិតថ្លៃ​បន្ថែម នៅពេល​រ៉ូមីង"</string>
+    <string name="no_upstream_notification_title" msgid="5030042590486713460">"ការភ្ជាប់​មិនមានអ៊ីនធឺណិត​ទេ"</string>
+    <string name="no_upstream_notification_message" msgid="3843613362272973447">"មិនអាច​ភ្ជាប់ឧបករណ៍​បានទេ"</string>
+    <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"បិទការភ្ជាប់"</string>
+    <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"ហតស្ប៉ត ឬការភ្ជាប់​ត្រូវបានបើក"</string>
+    <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"អាចមាន​ការគិតថ្លៃ​បន្ថែម នៅពេល​រ៉ូមីង"</string>
 </resources>
diff --git a/Tethering/res/values-mcc310-mnc004-kn/strings.xml b/Tethering/res/values-mcc310-mnc004-kn/strings.xml
index 16c55d0..ed76930 100644
--- a/Tethering/res/values-mcc310-mnc004-kn/strings.xml
+++ b/Tethering/res/values-mcc310-mnc004-kn/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="3584617491053416666">"ಟೆಥರಿಂಗ್‌ ಯಾವುದೇ ಇಂಟರ್ನೆಟ್ ಕನೆಕ್ಷನ್ ಹೊಂದಿಲ್ಲ"</string>
-    <string name="no_upstream_notification_message" msgid="5626323795587558017">"ಸಾಧನಗಳನ್ನು ಕನೆಕ್ಟ್ ಮಾಡಲು ಸಾಧ್ಯವಿಲ್ಲ"</string>
-    <string name="no_upstream_notification_disable_button" msgid="868677179945695858">"ಟೆಥರಿಂಗ್‌ ಆಫ್ ಮಾಡಿ"</string>
-    <string name="upstream_roaming_notification_title" msgid="2870229486619751829">"ಹಾಟ್‌ಸ್ಪಾಟ್ ಅಥವಾ ಟೆಥರಿಂಗ್‌ ಆನ್ ಆಗಿದೆ"</string>
-    <string name="upstream_roaming_notification_message" msgid="5229740963392849544">"ರೋಮಿಂಗ್‌ನಲ್ಲಿರುವಾಗ ಹೆಚ್ಚುವರಿ ಶುಲ್ಕಗಳು ಅನ್ವಯವಾಗಬಹುದು"</string>
+    <string name="no_upstream_notification_title" msgid="5030042590486713460">"ಟೆಥರಿಂಗ್‌ ಯಾವುದೇ ಇಂಟರ್ನೆಟ್ ಕನೆಕ್ಷನ್ ಹೊಂದಿಲ್ಲ"</string>
+    <string name="no_upstream_notification_message" msgid="3843613362272973447">"ಸಾಧನಗಳನ್ನು ಕನೆಕ್ಟ್ ಮಾಡಲು ಸಾಧ್ಯವಿಲ್ಲ"</string>
+    <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"ಟೆಥರಿಂಗ್‌ ಆಫ್ ಮಾಡಿ"</string>
+    <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"ಹಾಟ್‌ಸ್ಪಾಟ್ ಅಥವಾ ಟೆಥರಿಂಗ್‌ ಆನ್ ಆಗಿದೆ"</string>
+    <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"ರೋಮಿಂಗ್‌ನಲ್ಲಿರುವಾಗ ಹೆಚ್ಚುವರಿ ಶುಲ್ಕಗಳು ಅನ್ವಯವಾಗಬಹುದು"</string>
 </resources>
diff --git a/Tethering/res/values-mcc310-mnc004-ko/strings.xml b/Tethering/res/values-mcc310-mnc004-ko/strings.xml
index 619504f..6e50494 100644
--- a/Tethering/res/values-mcc310-mnc004-ko/strings.xml
+++ b/Tethering/res/values-mcc310-mnc004-ko/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="3584617491053416666">"테더링으로 인터넷을 사용할 수 없음"</string>
-    <string name="no_upstream_notification_message" msgid="5626323795587558017">"기기에서 연결할 수 없음"</string>
-    <string name="no_upstream_notification_disable_button" msgid="868677179945695858">"테더링 사용 중지"</string>
-    <string name="upstream_roaming_notification_title" msgid="2870229486619751829">"핫스팟 또는 테더링이 켜짐"</string>
-    <string name="upstream_roaming_notification_message" msgid="5229740963392849544">"로밍 중에는 추가 요금이 부과될 수 있습니다."</string>
+    <string name="no_upstream_notification_title" msgid="5030042590486713460">"테더링으로 인터넷을 사용할 수 없음"</string>
+    <string name="no_upstream_notification_message" msgid="3843613362272973447">"기기에서 연결할 수 없음"</string>
+    <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"테더링 사용 중지"</string>
+    <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"핫스팟 또는 테더링 켜짐"</string>
+    <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"로밍 중에는 추가 요금이 발생할 수 있습니다."</string>
 </resources>
diff --git a/Tethering/res/values-mcc310-mnc004-ky/strings.xml b/Tethering/res/values-mcc310-mnc004-ky/strings.xml
index f52dd90..d68128b 100644
--- a/Tethering/res/values-mcc310-mnc004-ky/strings.xml
+++ b/Tethering/res/values-mcc310-mnc004-ky/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="3584617491053416666">"Модем режими Интернети жок колдонулууда"</string>
-    <string name="no_upstream_notification_message" msgid="5626323795587558017">"Түзмөктөр туташпай жатат"</string>
-    <string name="no_upstream_notification_disable_button" msgid="868677179945695858">"Модем режимин өчүрүү"</string>
-    <string name="upstream_roaming_notification_title" msgid="2870229486619751829">"Байланыш түйүнү же модем режими күйүк"</string>
-    <string name="upstream_roaming_notification_message" msgid="5229740963392849544">"Роумингде кошумча акы алынышы мүмкүн"</string>
+    <string name="no_upstream_notification_title" msgid="5030042590486713460">"Модем режими Интернети жок колдонулууда"</string>
+    <string name="no_upstream_notification_message" msgid="3843613362272973447">"Түзмөктөр туташпай жатат"</string>
+    <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"Модем режимин өчүрүү"</string>
+    <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"Байланыш түйүнү же модем режими күйүк"</string>
+    <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"Роумингде кошумча акы алынышы мүмкүн"</string>
 </resources>
diff --git a/Tethering/res/values-mcc310-mnc004-lo/strings.xml b/Tethering/res/values-mcc310-mnc004-lo/strings.xml
index d3184f7..03e134a 100644
--- a/Tethering/res/values-mcc310-mnc004-lo/strings.xml
+++ b/Tethering/res/values-mcc310-mnc004-lo/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="3584617491053416666">"ການປ່ອຍສັນຍານບໍ່ມີອິນເຕີເນັດ"</string>
-    <string name="no_upstream_notification_message" msgid="5626323795587558017">"ອຸປະກອນບໍ່ສາມາດເຊື່ອມຕໍ່ໄດ້"</string>
-    <string name="no_upstream_notification_disable_button" msgid="868677179945695858">"ປິດການປ່ອຍສັນຍານ"</string>
-    <string name="upstream_roaming_notification_title" msgid="2870229486619751829">"ຮັອດສະປອດ ຫຼື ການປ່ອຍສັນຍານເປີດຢູ່"</string>
-    <string name="upstream_roaming_notification_message" msgid="5229740963392849544">"ອາດມີຄ່າບໍລິການເພີ່ມເຕີມໃນລະຫວ່າງການໂຣມມິງ"</string>
+    <string name="no_upstream_notification_title" msgid="5030042590486713460">"ການປ່ອຍສັນຍານບໍ່ມີອິນເຕີເນັດ"</string>
+    <string name="no_upstream_notification_message" msgid="3843613362272973447">"ອຸປະກອນບໍ່ສາມາດເຊື່ອມຕໍ່ໄດ້"</string>
+    <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"ປິດການປ່ອຍສັນຍານ"</string>
+    <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"ເປີດໃຊ້ຮັອດສະປອດ ຫຼື ການປ່ອຍສັນຍານຢູ່"</string>
+    <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"ອາດມີຄ່າໃຊ້ຈ່າຍເພີ່ມເຕີມໃນລະຫວ່າງການໂຣມມິງ"</string>
 </resources>
diff --git a/Tethering/res/values-mcc310-mnc004-lt/strings.xml b/Tethering/res/values-mcc310-mnc004-lt/strings.xml
index a07644d..652cedc 100644
--- a/Tethering/res/values-mcc310-mnc004-lt/strings.xml
+++ b/Tethering/res/values-mcc310-mnc004-lt/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="3584617491053416666">"Nėra įrenginio kaip modemo naudojimo interneto ryšio"</string>
-    <string name="no_upstream_notification_message" msgid="5626323795587558017">"Nepavyko susieti įrenginių"</string>
-    <string name="no_upstream_notification_disable_button" msgid="868677179945695858">"Išjungti įrenginio kaip modemo naudojimą"</string>
-    <string name="upstream_roaming_notification_title" msgid="2870229486619751829">"Įjungtas viešosios interneto prieigos taškas arba įrenginio kaip modemo naudojimas"</string>
-    <string name="upstream_roaming_notification_message" msgid="5229740963392849544">"Veikiant tarptinkliniam ryšiui gali būti taikomi papildomi mokesčiai"</string>
+    <string name="no_upstream_notification_title" msgid="5030042590486713460">"Nėra įrenginio kaip modemo naudojimo interneto ryšio"</string>
+    <string name="no_upstream_notification_message" msgid="3843613362272973447">"Nepavyko susieti įrenginių"</string>
+    <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"Išjungti įrenginio kaip modemo naudojimą"</string>
+    <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"Įjungtas viešosios interneto prieigos taškas arba įrenginio kaip modemo naudojimas"</string>
+    <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"Veikiant tarptinkliniam ryšiui gali būti taikomi papildomi mokesčiai"</string>
 </resources>
diff --git a/Tethering/res/values-mcc310-mnc004-lv/strings.xml b/Tethering/res/values-mcc310-mnc004-lv/strings.xml
index 5090ecf..2219722 100644
--- a/Tethering/res/values-mcc310-mnc004-lv/strings.xml
+++ b/Tethering/res/values-mcc310-mnc004-lv/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="3584617491053416666">"Piesaistei nav interneta savienojuma"</string>
-    <string name="no_upstream_notification_message" msgid="5626323795587558017">"Nevar savienot ierīces"</string>
-    <string name="no_upstream_notification_disable_button" msgid="868677179945695858">"Izslēgt piesaisti"</string>
-    <string name="upstream_roaming_notification_title" msgid="2870229486619751829">"Ir ieslēgts tīklājs vai piesaiste"</string>
-    <string name="upstream_roaming_notification_message" msgid="5229740963392849544">"Viesabonēšanas laikā var tikt piemērota papildu maksa."</string>
+    <string name="no_upstream_notification_title" msgid="5030042590486713460">"Piesaistei nav interneta savienojuma"</string>
+    <string name="no_upstream_notification_message" msgid="3843613362272973447">"Nevar savienot ierīces"</string>
+    <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"Izslēgt piesaisti"</string>
+    <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"Ir ieslēgts tīklājs vai piesaiste"</string>
+    <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"Viesabonēšanas laikā var tikt piemērota papildu samaksa"</string>
 </resources>
diff --git a/Tethering/res/values-mcc310-mnc004-mk/strings.xml b/Tethering/res/values-mcc310-mnc004-mk/strings.xml
index c95c80e..227f9e3 100644
--- a/Tethering/res/values-mcc310-mnc004-mk/strings.xml
+++ b/Tethering/res/values-mcc310-mnc004-mk/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="3584617491053416666">"Нема интернет преку мобилен"</string>
-    <string name="no_upstream_notification_message" msgid="5626323795587558017">"Уредите не може да се поврзат"</string>
-    <string name="no_upstream_notification_disable_button" msgid="868677179945695858">"Исклучи интернет преку мобилен"</string>
-    <string name="upstream_roaming_notification_title" msgid="2870229486619751829">"Вклучено: точка на пристап или интернет преку мобилен"</string>
-    <string name="upstream_roaming_notification_message" msgid="5229740963392849544">"При роаминг може да се наплатат дополнителни трошоци"</string>
+    <string name="no_upstream_notification_title" msgid="5030042590486713460">"Нема интернет преку мобилен"</string>
+    <string name="no_upstream_notification_message" msgid="3843613362272973447">"Уредите не може да се поврзат"</string>
+    <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"Исклучи интернет преку мобилен"</string>
+    <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"Точката на пристап или интернетот преку мобилен е вклучен"</string>
+    <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"При роаминг може да се наплатат дополнителни трошоци"</string>
 </resources>
diff --git a/Tethering/res/values-mcc310-mnc004-ml/strings.xml b/Tethering/res/values-mcc310-mnc004-ml/strings.xml
index 7bad5c1..ec43885 100644
--- a/Tethering/res/values-mcc310-mnc004-ml/strings.xml
+++ b/Tethering/res/values-mcc310-mnc004-ml/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="3584617491053416666">"ടെതറിംഗിന് ഇന്റർനെറ്റ് ഇല്ല"</string>
-    <string name="no_upstream_notification_message" msgid="5626323795587558017">"ഉപകരണങ്ങൾ കണക്റ്റ് ചെയ്യാനാവില്ല"</string>
-    <string name="no_upstream_notification_disable_button" msgid="868677179945695858">"ടെതറിംഗ് ഓഫാക്കുക"</string>
-    <string name="upstream_roaming_notification_title" msgid="2870229486619751829">"ഹോട്ട്‌സ്‌പോട്ട് അല്ലെങ്കിൽ ടെതറിംഗ് ഓണാണ്"</string>
-    <string name="upstream_roaming_notification_message" msgid="5229740963392849544">"റോമിംഗ് ചെയ്യുമ്പോൾ അധിക നിരക്കുകൾ ബാധകമായേക്കാം"</string>
+    <string name="no_upstream_notification_title" msgid="5030042590486713460">"ടെതറിംഗിന് ഇന്റർനെറ്റ് ഇല്ല"</string>
+    <string name="no_upstream_notification_message" msgid="3843613362272973447">"ഉപകരണങ്ങൾ കണക്റ്റ് ചെയ്യാനാവില്ല"</string>
+    <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"ടെതറിംഗ് ഓഫാക്കുക"</string>
+    <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"ഹോട്ട്‌സ്‌പോട്ട് അല്ലെങ്കിൽ ടെതറിംഗ് ഓണാണ്"</string>
+    <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"റോമിംഗ് ചെയ്യുമ്പോൾ അധിക നിരക്കുകൾ ബാധകമായേക്കാം"</string>
 </resources>
diff --git a/Tethering/res/values-mcc310-mnc004-mn/strings.xml b/Tethering/res/values-mcc310-mnc004-mn/strings.xml
index ff76236..e263573 100644
--- a/Tethering/res/values-mcc310-mnc004-mn/strings.xml
+++ b/Tethering/res/values-mcc310-mnc004-mn/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="3584617491053416666">"Модем болгоход ямар ч интернэт байхгүй байна"</string>
-    <string name="no_upstream_notification_message" msgid="5626323795587558017">"Төхөөрөмжүүд холбогдох боломжгүй байна"</string>
-    <string name="no_upstream_notification_disable_button" msgid="868677179945695858">"Модем болгохыг унтраах"</string>
-    <string name="upstream_roaming_notification_title" msgid="2870229486619751829">"Сүлжээний цэг эсвэл модем болгох асаалттай байна"</string>
-    <string name="upstream_roaming_notification_message" msgid="5229740963392849544">"Роумингийн үеэр нэмэлт төлбөр тооцогдож магадгүй"</string>
+    <string name="no_upstream_notification_title" msgid="5030042590486713460">"Модемд интернэт алга байна"</string>
+    <string name="no_upstream_notification_message" msgid="3843613362272973447">"Төхөөрөмжүүд холбогдох боломжгүй байна"</string>
+    <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"Модем болгохыг унтраах"</string>
+    <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"Сүлжээний цэг эсвэл модем болгох асаалттай байна"</string>
+    <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"Роумингийн үеэр нэмэлт төлбөр нэхэмжилж болзошгүй"</string>
 </resources>
diff --git a/Tethering/res/values-mcc310-mnc004-mr/strings.xml b/Tethering/res/values-mcc310-mnc004-mr/strings.xml
index 1754dd4..adf845d 100644
--- a/Tethering/res/values-mcc310-mnc004-mr/strings.xml
+++ b/Tethering/res/values-mcc310-mnc004-mr/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="3584617491053416666">"टेदरिंगसाठी इंटरनेट नाही"</string>
-    <string name="no_upstream_notification_message" msgid="5626323795587558017">"डिव्हाइस कनेक्ट होऊ शकत नाहीत"</string>
-    <string name="no_upstream_notification_disable_button" msgid="868677179945695858">"टेदरिंग बंद करा"</string>
-    <string name="upstream_roaming_notification_title" msgid="2870229486619751829">"हॉटस्पॉट किंवा टेदरिंग सुरू आहे"</string>
-    <string name="upstream_roaming_notification_message" msgid="5229740963392849544">"रोमिंगदरम्यान अतिरिक्त शुल्के लागू होऊ शकतात"</string>
+    <string name="no_upstream_notification_title" msgid="5030042590486713460">"टेदरिंगला इंटरनेट नाही"</string>
+    <string name="no_upstream_notification_message" msgid="3843613362272973447">"डिव्हाइस कनेक्ट होऊ शकत नाहीत"</string>
+    <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"टेदरिंग बंद करा"</string>
+    <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"हॉटस्पॉट किंवा टेदरिंग सुरू आहे"</string>
+    <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"रोमिंगदरम्यान अतिरिक्त शुल्क लागू होऊ शकतात"</string>
 </resources>
diff --git a/Tethering/res/values-mcc310-mnc004-ms/strings.xml b/Tethering/res/values-mcc310-mnc004-ms/strings.xml
index 343e6fa..f65c451 100644
--- a/Tethering/res/values-mcc310-mnc004-ms/strings.xml
+++ b/Tethering/res/values-mcc310-mnc004-ms/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="3584617491053416666">"Penambatan tiada Internet"</string>
-    <string name="no_upstream_notification_message" msgid="5626323795587558017">"Peranti tidak dapat disambungkan"</string>
-    <string name="no_upstream_notification_disable_button" msgid="868677179945695858">"Matikan penambatan"</string>
-    <string name="upstream_roaming_notification_title" msgid="2870229486619751829">"Tempat liputan atau penambatan dihidupkan"</string>
-    <string name="upstream_roaming_notification_message" msgid="5229740963392849544">"Caj tambahan boleh dikenakan semasa perayauan"</string>
+    <string name="no_upstream_notification_title" msgid="5030042590486713460">"Penambatan tiada Internet"</string>
+    <string name="no_upstream_notification_message" msgid="3843613362272973447">"Peranti tidak dapat disambungkan"</string>
+    <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"Matikan penambatan"</string>
+    <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"Tempat liputan atau penambatan dihidupkan"</string>
+    <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"Caj tambahan mungkin digunakan semasa perayauan"</string>
 </resources>
diff --git a/Tethering/res/values-mcc310-mnc004-my/strings.xml b/Tethering/res/values-mcc310-mnc004-my/strings.xml
index 152f468..4118e77 100644
--- a/Tethering/res/values-mcc310-mnc004-my/strings.xml
+++ b/Tethering/res/values-mcc310-mnc004-my/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="3584617491053416666">"မိုဘိုင်းသုံး၍ ချိတ်ဆက်ခြင်းတွင် အင်တာနက် မရှိပါ"</string>
-    <string name="no_upstream_notification_message" msgid="5626323795587558017">"စက်ပစ္စည်းများကို ချိတ်ဆက်၍မရပါ"</string>
-    <string name="no_upstream_notification_disable_button" msgid="868677179945695858">"မိုဘိုင်းသုံး၍ ချိတ်ဆက်ခြင်း ပိတ်ရန်"</string>
-    <string name="upstream_roaming_notification_title" msgid="2870229486619751829">"ဟော့စပေါ့ (သို့) မိုဘိုင်းသုံး၍ ချိတ်ဆက်ခြင်း ဖွင့်ထားသည်"</string>
-    <string name="upstream_roaming_notification_message" msgid="5229740963392849544">"ပြင်ပကွန်ရက်သုံးနေစဉ် နောက်ထပ်ကျသင့်ငွေ ပေးရနိုင်သည်"</string>
+    <string name="no_upstream_notification_title" msgid="5030042590486713460">"မိုဘိုင်းဖုန်းသုံး ချိတ်ဆက်မျှဝေခြင်းတွင် အင်တာနက် မရှိပါ"</string>
+    <string name="no_upstream_notification_message" msgid="3843613362272973447">"စက်များ ချိတ်ဆက်၍ မရပါ"</string>
+    <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"မိုဘိုင်းဖုန်းသုံး ချိတ်ဆက်မျှဝေခြင်း ပိတ်ရန်"</string>
+    <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"ဟော့စပေါ့ (သို့) မိုဘိုင်းဖုန်းသုံး ချိတ်ဆက်မျှဝေခြင်း ဖွင့်ထားသည်"</string>
+    <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"ပြင်ပကွန်ရက်နှင့် ချိတ်ဆက်သည့်အခါ နောက်ထပ်ကျသင့်မှုများ ရှိနိုင်သည်"</string>
 </resources>
diff --git a/Tethering/res/values-mcc310-mnc004-nb/strings.xml b/Tethering/res/values-mcc310-mnc004-nb/strings.xml
index 31895d1..3685358 100644
--- a/Tethering/res/values-mcc310-mnc004-nb/strings.xml
+++ b/Tethering/res/values-mcc310-mnc004-nb/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="3584617491053416666">"Internettdeling har ikke internettilgang"</string>
-    <string name="no_upstream_notification_message" msgid="5626323795587558017">"Enheter kan ikke koble til"</string>
-    <string name="no_upstream_notification_disable_button" msgid="868677179945695858">"Slå av internettdeling"</string>
-    <string name="upstream_roaming_notification_title" msgid="2870229486619751829">"Wifi-sone eller internettdeling er på"</string>
-    <string name="upstream_roaming_notification_message" msgid="5229740963392849544">"Det kan påløpe flere kostnader ved roaming"</string>
+    <string name="no_upstream_notification_title" msgid="5030042590486713460">"Internettdeling har ikke internettilgang"</string>
+    <string name="no_upstream_notification_message" msgid="3843613362272973447">"Enhetene kan ikke koble til"</string>
+    <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"Slå av internettdeling"</string>
+    <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"Wi-Fi-sone eller internettdeling er på"</string>
+    <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"Ytterligere kostnader kan påløpe under roaming"</string>
 </resources>
diff --git a/Tethering/res/values-mcc310-mnc004-ne/strings.xml b/Tethering/res/values-mcc310-mnc004-ne/strings.xml
index 4b50773..d074f15 100644
--- a/Tethering/res/values-mcc310-mnc004-ne/strings.xml
+++ b/Tethering/res/values-mcc310-mnc004-ne/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="3584617491053416666">"टेदरिङमार्फत इन्टरनेट कनेक्ट गर्न सकिएन"</string>
-    <string name="no_upstream_notification_message" msgid="5626323795587558017">"डिभाइसहरू कनेक्ट गर्न सकिएन"</string>
-    <string name="no_upstream_notification_disable_button" msgid="868677179945695858">"टेदरिङ अफ गर्नुहोस्"</string>
-    <string name="upstream_roaming_notification_title" msgid="2870229486619751829">"हटस्पट वा टेदरिङ अन छ"</string>
-    <string name="upstream_roaming_notification_message" msgid="5229740963392849544">"रोमिङ सेवा प्रयोग गर्दा अतिरिक्त शुल्क लाग्न सक्छ"</string>
+    <string name="no_upstream_notification_title" msgid="5030042590486713460">"टेदरिङमार्फत इन्टरनेट कनेक्सन प्राप्त हुन सकेन"</string>
+    <string name="no_upstream_notification_message" msgid="3843613362272973447">"यन्त्रहरू कनेक्ट गर्न सकिएन"</string>
+    <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"टेदरिङ निष्क्रिय पार्नुहोस्"</string>
+    <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"हटस्पट वा टेदरिङ सक्रिय छ"</string>
+    <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"रोमिङ सेवा प्रयोग गर्दा अतिरिक्त शुल्क लाग्न सक्छ"</string>
 </resources>
diff --git a/Tethering/res/values-mcc310-mnc004-nl/strings.xml b/Tethering/res/values-mcc310-mnc004-nl/strings.xml
index 8af41fd..1d88894 100644
--- a/Tethering/res/values-mcc310-mnc004-nl/strings.xml
+++ b/Tethering/res/values-mcc310-mnc004-nl/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="3584617491053416666">"Tethering heeft geen internet"</string>
-    <string name="no_upstream_notification_message" msgid="5626323795587558017">"Apparaten kunnen geen verbinding maken"</string>
-    <string name="no_upstream_notification_disable_button" msgid="868677179945695858">"Tethering uitzetten"</string>
-    <string name="upstream_roaming_notification_title" msgid="2870229486619751829">"Hotspot of tethering staat aan"</string>
-    <string name="upstream_roaming_notification_message" msgid="5229740963392849544">"Er kunnen extra kosten voor roaming in rekening worden gebracht"</string>
+    <string name="no_upstream_notification_title" msgid="5030042590486713460">"Tethering heeft geen internet"</string>
+    <string name="no_upstream_notification_message" msgid="3843613362272973447">"Apparaten kunnen niet worden verbonden"</string>
+    <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"Tethering uitschakelen"</string>
+    <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"Hotspot of tethering is ingeschakeld"</string>
+    <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"Er kunnen extra kosten voor roaming in rekening worden gebracht."</string>
 </resources>
diff --git a/Tethering/res/values-mcc310-mnc004-or/strings.xml b/Tethering/res/values-mcc310-mnc004-or/strings.xml
index 6eb0773..8038815 100644
--- a/Tethering/res/values-mcc310-mnc004-or/strings.xml
+++ b/Tethering/res/values-mcc310-mnc004-or/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="3584617491053416666">"ଟିଥରିଂ ପାଇଁ କୌଣସି ଇଣ୍ଟରନେଟ କନେକ୍ସନ ନାହିଁ"</string>
-    <string name="no_upstream_notification_message" msgid="5626323795587558017">"ଡିଭାଇସଗୁଡ଼ିକୁ କନେକ୍ଟ କରାଯାଇପାରିବ ନାହିଁ"</string>
-    <string name="no_upstream_notification_disable_button" msgid="868677179945695858">"ଟିଥରିଂକୁ ବନ୍ଦ କରନ୍ତୁ"</string>
-    <string name="upstream_roaming_notification_title" msgid="2870229486619751829">"ହଟସ୍ପଟ କିମ୍ବା ଟିଥରିଂ ଚାଲୁ ଅଛି"</string>
-    <string name="upstream_roaming_notification_message" msgid="5229740963392849544">"ରୋମିଂ ସମୟରେ ଅତିରିକ୍ତ ଚାର୍ଜ ଲାଗୁ ହୋଇପାରେ"</string>
+    <string name="no_upstream_notification_title" msgid="5030042590486713460">"ଟିଥରିଂ ପାଇଁ କୌଣସି ଇଣ୍ଟର୍ନେଟ୍ ସଂଯୋଗ ନାହିଁ"</string>
+    <string name="no_upstream_notification_message" msgid="3843613362272973447">"ଡିଭାଇସଗୁଡ଼ିକ ସଂଯୋଗ କରାଯାଇପାରିବ ନାହିଁ"</string>
+    <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"ଟିଥରିଂ ବନ୍ଦ କରନ୍ତୁ"</string>
+    <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"ହଟସ୍ପଟ୍ କିମ୍ବା ଟିଥରିଂ ଚାଲୁ ଅଛି"</string>
+    <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"ରୋମିଂରେ ଥିବା ସମୟରେ ଅତିରିକ୍ତ ଶୁଳ୍କ ଲାଗୁ ହୋଇପାରେ"</string>
 </resources>
diff --git a/Tethering/res/values-mcc310-mnc004-pa/strings.xml b/Tethering/res/values-mcc310-mnc004-pa/strings.xml
index 28181e2..819833e 100644
--- a/Tethering/res/values-mcc310-mnc004-pa/strings.xml
+++ b/Tethering/res/values-mcc310-mnc004-pa/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="3584617491053416666">"ਟੈਦਰਿੰਗ ਕੋਲ ਇੰਟਰਨੈੱਟ ਪਹੁੰਚ ਨਹੀਂ ਹੈ"</string>
-    <string name="no_upstream_notification_message" msgid="5626323795587558017">"ਡੀਵਾਈਸ ਕਨੈਕਟ ਨਹੀਂ ਕੀਤੇ ਜਾ ਸਕਦੇ"</string>
-    <string name="no_upstream_notification_disable_button" msgid="868677179945695858">"ਟੈਦਰਿੰਗ ਬੰਦ ਕਰੋ"</string>
-    <string name="upstream_roaming_notification_title" msgid="2870229486619751829">"ਹੌਟਸਪੌਟ ਜਾਂ ਟੈਦਰਿੰਗ ਚਾਲੂ ਹੈ"</string>
-    <string name="upstream_roaming_notification_message" msgid="5229740963392849544">"ਰੋਮਿੰਗ ਦੌਰਾਨ ਵਧੀਕ ਖਰਚੇ ਲਾਗੂ ਹੋ ਸਕਦੇ ਹਨ"</string>
+    <string name="no_upstream_notification_title" msgid="5030042590486713460">"ਟੈਦਰਿੰਗ ਕੋਲ ਇੰਟਰਨੈੱਟ ਪਹੁੰਚ ਨਹੀਂ ਹੈ"</string>
+    <string name="no_upstream_notification_message" msgid="3843613362272973447">"ਡੀਵਾਈਸ ਕਨੈਕਟ ਨਹੀਂ ਕੀਤੇ ਜਾ ਸਕਦੇ"</string>
+    <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"ਟੈਦਰਿੰਗ ਬੰਦ ਕਰੋ"</string>
+    <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"ਹੌਟਸਪੌਟ ਜਾਂ ਟੈਦਰਿੰਗ ਚਾਲੂ ਹੈ"</string>
+    <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"ਰੋਮਿੰਗ ਦੌਰਾਨ ਵਧੀਕ ਖਰਚੇ ਲਾਗੂ ਹੋ ਸਕਦੇ ਹਨ"</string>
 </resources>
diff --git a/Tethering/res/values-mcc310-mnc004-pl/strings.xml b/Tethering/res/values-mcc310-mnc004-pl/strings.xml
index 816302a..65e4380 100644
--- a/Tethering/res/values-mcc310-mnc004-pl/strings.xml
+++ b/Tethering/res/values-mcc310-mnc004-pl/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="3584617491053416666">"Tethering nie ma internetu"</string>
-    <string name="no_upstream_notification_message" msgid="5626323795587558017">"Urządzenia nie mogą się połączyć"</string>
-    <string name="no_upstream_notification_disable_button" msgid="868677179945695858">"Wyłącz tethering"</string>
-    <string name="upstream_roaming_notification_title" msgid="2870229486619751829">"Hotspot lub tethering jest włączony"</string>
-    <string name="upstream_roaming_notification_message" msgid="5229740963392849544">"Podczas korzystania z roamingu mogą zostać naliczone dodatkowe opłaty"</string>
+    <string name="no_upstream_notification_title" msgid="5030042590486713460">"Tethering nie ma internetu"</string>
+    <string name="no_upstream_notification_message" msgid="3843613362272973447">"Urządzenia nie mogą się połączyć"</string>
+    <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"Wyłącz tethering"</string>
+    <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"Hotspot lub tethering jest włączony"</string>
+    <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"Podczas korzystania z roamingu mogą zostać naliczone dodatkowe opłaty"</string>
 </resources>
diff --git a/Tethering/res/values-mcc310-mnc004-pt-rBR/strings.xml b/Tethering/res/values-mcc310-mnc004-pt-rBR/strings.xml
index 55767c2..d886617 100644
--- a/Tethering/res/values-mcc310-mnc004-pt-rBR/strings.xml
+++ b/Tethering/res/values-mcc310-mnc004-pt-rBR/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="3584617491053416666">"O tethering não tem uma conexão de Internet"</string>
-    <string name="no_upstream_notification_message" msgid="5626323795587558017">"Não é possível conectar os dispositivos"</string>
-    <string name="no_upstream_notification_disable_button" msgid="868677179945695858">"Desativar o tethering"</string>
-    <string name="upstream_roaming_notification_title" msgid="2870229486619751829">"O ponto de acesso ou tethering está ativado"</string>
-    <string name="upstream_roaming_notification_message" msgid="5229740963392849544">"Pode haver cobranças extras durante o roaming"</string>
+    <string name="no_upstream_notification_title" msgid="5030042590486713460">"O tethering não tem Internet"</string>
+    <string name="no_upstream_notification_message" msgid="3843613362272973447">"Não é possível conectar os dispositivos"</string>
+    <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"Desativar o tethering"</string>
+    <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"Ponto de acesso ou tethering ativado"</string>
+    <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"Pode haver cobranças extras durante o roaming"</string>
 </resources>
diff --git a/Tethering/res/values-mcc310-mnc004-pt-rPT/strings.xml b/Tethering/res/values-mcc310-mnc004-pt-rPT/strings.xml
index eccabf8..bfd45ca 100644
--- a/Tethering/res/values-mcc310-mnc004-pt-rPT/strings.xml
+++ b/Tethering/res/values-mcc310-mnc004-pt-rPT/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="3584617491053416666">"A ligação (à Internet) via telemóvel não tem Internet"</string>
-    <string name="no_upstream_notification_message" msgid="5626323795587558017">"Não é possível ligar os dispositivos"</string>
-    <string name="no_upstream_notification_disable_button" msgid="868677179945695858">"Desativar ligação (à Internet) via telemóvel"</string>
-    <string name="upstream_roaming_notification_title" msgid="2870229486619751829">"A zona Wi-Fi ou a ligação (à Internet) via telemóvel está ativada"</string>
-    <string name="upstream_roaming_notification_message" msgid="5229740963392849544">"Podem aplicar-se custos adicionais em roaming"</string>
+    <string name="no_upstream_notification_title" msgid="5030042590486713460">"A ligação (à Internet) via telemóvel não tem Internet"</string>
+    <string name="no_upstream_notification_message" msgid="3843613362272973447">"Não é possível ligar os dispositivos"</string>
+    <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"Desativar ligação (à Internet) via telemóvel"</string>
+    <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"A zona Wi-Fi ou a ligação (à Internet) via telemóvel está ativada"</string>
+    <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"Podem aplicar-se custos adicionais em roaming."</string>
 </resources>
diff --git a/Tethering/res/values-mcc310-mnc004-pt/strings.xml b/Tethering/res/values-mcc310-mnc004-pt/strings.xml
index 55767c2..d886617 100644
--- a/Tethering/res/values-mcc310-mnc004-pt/strings.xml
+++ b/Tethering/res/values-mcc310-mnc004-pt/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="3584617491053416666">"O tethering não tem uma conexão de Internet"</string>
-    <string name="no_upstream_notification_message" msgid="5626323795587558017">"Não é possível conectar os dispositivos"</string>
-    <string name="no_upstream_notification_disable_button" msgid="868677179945695858">"Desativar o tethering"</string>
-    <string name="upstream_roaming_notification_title" msgid="2870229486619751829">"O ponto de acesso ou tethering está ativado"</string>
-    <string name="upstream_roaming_notification_message" msgid="5229740963392849544">"Pode haver cobranças extras durante o roaming"</string>
+    <string name="no_upstream_notification_title" msgid="5030042590486713460">"O tethering não tem Internet"</string>
+    <string name="no_upstream_notification_message" msgid="3843613362272973447">"Não é possível conectar os dispositivos"</string>
+    <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"Desativar o tethering"</string>
+    <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"Ponto de acesso ou tethering ativado"</string>
+    <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"Pode haver cobranças extras durante o roaming"</string>
 </resources>
diff --git a/Tethering/res/values-mcc310-mnc004-ro/strings.xml b/Tethering/res/values-mcc310-mnc004-ro/strings.xml
index 3c3d7bc..8d87a9e 100644
--- a/Tethering/res/values-mcc310-mnc004-ro/strings.xml
+++ b/Tethering/res/values-mcc310-mnc004-ro/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="3584617491053416666">"Procesul de tethering nu are internet"</string>
-    <string name="no_upstream_notification_message" msgid="5626323795587558017">"Dispozitivele nu se pot conecta"</string>
-    <string name="no_upstream_notification_disable_button" msgid="868677179945695858">"Dezactivează tetheringul"</string>
-    <string name="upstream_roaming_notification_title" msgid="2870229486619751829">"S-a activat hotspotul sau tetheringul"</string>
-    <string name="upstream_roaming_notification_message" msgid="5229740963392849544">"Se pot aplica taxe suplimentare pentru roaming"</string>
+    <string name="no_upstream_notification_title" msgid="5030042590486713460">"Procesul de tethering nu are internet"</string>
+    <string name="no_upstream_notification_message" msgid="3843613362272973447">"Dispozitivele nu se pot conecta"</string>
+    <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"Dezactivați procesul de tethering"</string>
+    <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"S-a activat hotspotul sau tethering"</string>
+    <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"Se pot aplica taxe suplimentare pentru roaming"</string>
 </resources>
diff --git a/Tethering/res/values-mcc310-mnc004-ru/strings.xml b/Tethering/res/values-mcc310-mnc004-ru/strings.xml
index 7667180..dbdb9eb 100644
--- a/Tethering/res/values-mcc310-mnc004-ru/strings.xml
+++ b/Tethering/res/values-mcc310-mnc004-ru/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="3584617491053416666">"Режим модема используется без доступа к интернету"</string>
-    <string name="no_upstream_notification_message" msgid="5626323795587558017">"Невозможно подключить устройства."</string>
-    <string name="no_upstream_notification_disable_button" msgid="868677179945695858">"Отключить режим модема"</string>
-    <string name="upstream_roaming_notification_title" msgid="2870229486619751829">"Включена точка доступа или режим модема"</string>
-    <string name="upstream_roaming_notification_message" msgid="5229740963392849544">"За использование услуг связи в роуминге может взиматься дополнительная плата."</string>
+    <string name="no_upstream_notification_title" msgid="5030042590486713460">"Режим модема используется без доступа к Интернету"</string>
+    <string name="no_upstream_notification_message" msgid="3843613362272973447">"Невозможно подключить устройства."</string>
+    <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"Отключить режим модема"</string>
+    <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"Включены точка доступа или режим модема"</string>
+    <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"За использование услуг связи в роуминге может взиматься дополнительная плата."</string>
 </resources>
diff --git a/Tethering/res/values-mcc310-mnc004-si/strings.xml b/Tethering/res/values-mcc310-mnc004-si/strings.xml
index 0c88a39..d8301e4 100644
--- a/Tethering/res/values-mcc310-mnc004-si/strings.xml
+++ b/Tethering/res/values-mcc310-mnc004-si/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="3584617491053416666">"ටෙදරින් හට අන්තර්ජාලය නැත"</string>
-    <string name="no_upstream_notification_message" msgid="5626323795587558017">"උපාංගවලට සම්බන්ධ විය නොහැක"</string>
-    <string name="no_upstream_notification_disable_button" msgid="868677179945695858">"ටෙදරින් ක්‍රියාවිරහිත කරන්න"</string>
-    <string name="upstream_roaming_notification_title" msgid="2870229486619751829">"හොට්ස්පොට් හෝ ටෙදරින් ක්‍රියාත්මකයි"</string>
-    <string name="upstream_roaming_notification_message" msgid="5229740963392849544">"රෝමිං අතරේ අතිරේක ගාස්තු අදාළ විය හැක"</string>
+    <string name="no_upstream_notification_title" msgid="5030042590486713460">"ටෙදරින් හට අන්තර්ජාලය නැත"</string>
+    <string name="no_upstream_notification_message" msgid="3843613362272973447">"උපාංගවලට සම්බන්ධ විය නොහැකිය"</string>
+    <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"ටෙදරින් ක්‍රියාවිරහිත කරන්න"</string>
+    <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"හොට්ස්පොට් හෝ ටෙදරින් ක්‍රියාත්මකයි"</string>
+    <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"රෝමිං අතරතුර අමතර ගාස්තු අදාළ විය හැකිය"</string>
 </resources>
diff --git a/Tethering/res/values-mcc310-mnc004-sk/strings.xml b/Tethering/res/values-mcc310-mnc004-sk/strings.xml
index c3b941e..bef7136 100644
--- a/Tethering/res/values-mcc310-mnc004-sk/strings.xml
+++ b/Tethering/res/values-mcc310-mnc004-sk/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="3584617491053416666">"Tethering nemá internetové pripojenie"</string>
-    <string name="no_upstream_notification_message" msgid="5626323795587558017">"Zariadenia sa nemôžu pripojiť"</string>
-    <string name="no_upstream_notification_disable_button" msgid="868677179945695858">"Vypnúť tethering"</string>
-    <string name="upstream_roaming_notification_title" msgid="2870229486619751829">"Je zapnutý hotspot alebo tethering"</string>
-    <string name="upstream_roaming_notification_message" msgid="5229740963392849544">"Počas roamingu vám môžu byť účtované ďalšie poplatky"</string>
+    <string name="no_upstream_notification_title" msgid="5030042590486713460">"Tethering nemá internetové pripojenie"</string>
+    <string name="no_upstream_notification_message" msgid="3843613362272973447">"Zariadenia sa nemôžu pripojiť"</string>
+    <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"Vypnúť tethering"</string>
+    <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"Je zapnutý hotspot alebo tethering"</string>
+    <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"Počas roamingu vám môžu byť účtované ďalšie poplatky"</string>
 </resources>
diff --git a/Tethering/res/values-mcc310-mnc004-sl/strings.xml b/Tethering/res/values-mcc310-mnc004-sl/strings.xml
index 6573475..3202c62 100644
--- a/Tethering/res/values-mcc310-mnc004-sl/strings.xml
+++ b/Tethering/res/values-mcc310-mnc004-sl/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="3584617491053416666">"Povezava računalnika z internetom prek mobilnega telefona nima internetne povezave"</string>
-    <string name="no_upstream_notification_message" msgid="5626323795587558017">"Napravi se ne moreta povezati"</string>
-    <string name="no_upstream_notification_disable_button" msgid="868677179945695858">"Izklopi povezavo računalnika z internetom prek mobilnega telefona"</string>
-    <string name="upstream_roaming_notification_title" msgid="2870229486619751829">"Dostopna točka ali povezava računalnika z internetom prek mobilnega telefona je vklopljena"</string>
-    <string name="upstream_roaming_notification_message" msgid="5229740963392849544">"Med gostovanjem lahko nastanejo dodatni stroški."</string>
+    <string name="no_upstream_notification_title" msgid="5030042590486713460">"Internetna povezava prek mobilnega telefona ni vzpostavljena"</string>
+    <string name="no_upstream_notification_message" msgid="3843613362272973447">"Napravi se ne moreta povezati"</string>
+    <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"Izklopi internetno povezavo prek mobilnega telefona"</string>
+    <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"Dostopna točka ali internetna povezava prek mobilnega telefona je vklopljena"</string>
+    <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"Med gostovanjem lahko nastanejo dodatni stroški"</string>
 </resources>
diff --git a/Tethering/res/values-mcc310-mnc004-sq/strings.xml b/Tethering/res/values-mcc310-mnc004-sq/strings.xml
index 93ea231..37f6ad2 100644
--- a/Tethering/res/values-mcc310-mnc004-sq/strings.xml
+++ b/Tethering/res/values-mcc310-mnc004-sq/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="3584617491053416666">"Ndarja e internetit nuk ka internet"</string>
-    <string name="no_upstream_notification_message" msgid="5626323795587558017">"Pajisjet nuk mund të lidhen"</string>
-    <string name="no_upstream_notification_disable_button" msgid="868677179945695858">"Çaktivizo ndarjen e internetit"</string>
-    <string name="upstream_roaming_notification_title" msgid="2870229486619751829">"Zona e qasjes për internet ose ndarja e internetit është aktive"</string>
-    <string name="upstream_roaming_notification_message" msgid="5229740963392849544">"Mund të zbatohen tarifime shtesë kur je në roaming"</string>
+    <string name="no_upstream_notification_title" msgid="5030042590486713460">"Ndarja e internetit nuk ka internet"</string>
+    <string name="no_upstream_notification_message" msgid="3843613362272973447">"Pajisjet nuk mund të lidhen"</string>
+    <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"Çaktivizo ndarjen e internetit"</string>
+    <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"Zona e qasjes për internet ose ndarja e internetit është aktive"</string>
+    <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"Mund të zbatohen tarifime shtesë kur je në roaming"</string>
 </resources>
diff --git a/Tethering/res/values-mcc310-mnc004-sr/strings.xml b/Tethering/res/values-mcc310-mnc004-sr/strings.xml
index e8fb478..5566d03 100644
--- a/Tethering/res/values-mcc310-mnc004-sr/strings.xml
+++ b/Tethering/res/values-mcc310-mnc004-sr/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="3584617491053416666">"Привезивање нема приступ интернету"</string>
-    <string name="no_upstream_notification_message" msgid="5626323795587558017">"Повезивање уређаја није успело"</string>
-    <string name="no_upstream_notification_disable_button" msgid="868677179945695858">"Искључи привезивање"</string>
-    <string name="upstream_roaming_notification_title" msgid="2870229486619751829">"Укључен је хотспот или привезивање"</string>
-    <string name="upstream_roaming_notification_message" msgid="5229740963392849544">"Можда важе додатни трошкови у ромингу"</string>
+    <string name="no_upstream_notification_title" msgid="5030042590486713460">"Привезивање нема приступ интернету"</string>
+    <string name="no_upstream_notification_message" msgid="3843613362272973447">"Повезивање уређаја није успело"</string>
+    <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"Искључи привезивање"</string>
+    <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"Укључен је хотспот или привезивање"</string>
+    <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"Можда важе додатни трошкови у ромингу"</string>
 </resources>
diff --git a/Tethering/res/values-mcc310-mnc004-sv/strings.xml b/Tethering/res/values-mcc310-mnc004-sv/strings.xml
index 6fc1747..9765acd 100644
--- a/Tethering/res/values-mcc310-mnc004-sv/strings.xml
+++ b/Tethering/res/values-mcc310-mnc004-sv/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="3584617491053416666">"Det finns ingen internetanslutning för internetdelningen"</string>
-    <string name="no_upstream_notification_message" msgid="5626323795587558017">"Enheterna kan inte anslutas"</string>
-    <string name="no_upstream_notification_disable_button" msgid="868677179945695858">"Inaktivera internetdelning"</string>
-    <string name="upstream_roaming_notification_title" msgid="2870229486619751829">"Surfzon eller internetdelning har aktiverats"</string>
-    <string name="upstream_roaming_notification_message" msgid="5229740963392849544">"Ytterligare avgifter kan tillkomma vid roaming"</string>
+    <string name="no_upstream_notification_title" msgid="5030042590486713460">"Det finns ingen internetanslutning för internetdelningen"</string>
+    <string name="no_upstream_notification_message" msgid="3843613362272973447">"Enheterna kan inte anslutas"</string>
+    <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"Inaktivera internetdelning"</string>
+    <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"Surfzon eller internetdelning har aktiverats"</string>
+    <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"Ytterligare avgifter kan tillkomma vid roaming"</string>
 </resources>
diff --git a/Tethering/res/values-mcc310-mnc004-sw/strings.xml b/Tethering/res/values-mcc310-mnc004-sw/strings.xml
index 73a7026..cf850c9 100644
--- a/Tethering/res/values-mcc310-mnc004-sw/strings.xml
+++ b/Tethering/res/values-mcc310-mnc004-sw/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="3584617491053416666">"Huduma ya kusambaza mtandao haina muunganisho wa intaneti"</string>
-    <string name="no_upstream_notification_message" msgid="5626323795587558017">"Imeshindwa kuunganisha vifaa"</string>
-    <string name="no_upstream_notification_disable_button" msgid="868677179945695858">"Zima kipengele cha kusambaza mtandao"</string>
-    <string name="upstream_roaming_notification_title" msgid="2870229486619751829">"Umewasha kipengele cha kusambaza mtandao au mtandao pepe"</string>
-    <string name="upstream_roaming_notification_message" msgid="5229740963392849544">"Huenda ukatozwa gharama za ziada ukitumia mitandao ya ng\'ambo"</string>
+    <string name="no_upstream_notification_title" msgid="5030042590486713460">"Kipengele cha kusambaza mtandao hakina intaneti"</string>
+    <string name="no_upstream_notification_message" msgid="3843613362272973447">"Imeshindwa kuunganisha vifaa"</string>
+    <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"Zima kipengele cha kusambaza mtandao"</string>
+    <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"Umewasha kipengele cha kusambaza mtandao au mtandao pepe"</string>
+    <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"Huenda ukatozwa gharama za ziada ukitumia mitandao ya ng\'ambo"</string>
 </resources>
diff --git a/Tethering/res/values-mcc310-mnc004-ta/strings.xml b/Tethering/res/values-mcc310-mnc004-ta/strings.xml
index 436f00b..f4b15aa 100644
--- a/Tethering/res/values-mcc310-mnc004-ta/strings.xml
+++ b/Tethering/res/values-mcc310-mnc004-ta/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="3584617491053416666">"இணைப்பு முறைக்கு இணைய இணைப்பு இல்லை"</string>
-    <string name="no_upstream_notification_message" msgid="5626323795587558017">"சாதனங்களால் இணைய முடியவில்லை"</string>
-    <string name="no_upstream_notification_disable_button" msgid="868677179945695858">"இணைப்பு முறையை முடக்கு"</string>
-    <string name="upstream_roaming_notification_title" msgid="2870229486619751829">"ஹாட்ஸ்பாட் அல்லது இணைப்பு முறை இயக்கப்பட்டுள்ளது"</string>
-    <string name="upstream_roaming_notification_message" msgid="5229740963392849544">"ரோமிங்கின்போது கூடுதல் கட்டணங்கள் விதிக்கப்படலாம்"</string>
+    <string name="no_upstream_notification_title" msgid="5030042590486713460">"இணைப்பு முறைக்கு இணைய இணைப்பு இல்லை"</string>
+    <string name="no_upstream_notification_message" msgid="3843613362272973447">"சாதனங்களால் இணைய முடியவில்லை"</string>
+    <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"இணைப்பு முறையை ஆஃப் செய்"</string>
+    <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"ஹாட்ஸ்பாட் அல்லது இணைப்பு முறை ஆன் செய்யப்பட்டுள்ளது"</string>
+    <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"ரோமிங்கின்போது கூடுதல் கட்டணங்கள் விதிக்கப்படக்கூடும்"</string>
 </resources>
diff --git a/Tethering/res/values-mcc310-mnc004-te/strings.xml b/Tethering/res/values-mcc310-mnc004-te/strings.xml
index ba83627..937d34d 100644
--- a/Tethering/res/values-mcc310-mnc004-te/strings.xml
+++ b/Tethering/res/values-mcc310-mnc004-te/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="3584617491053416666">"టెథరింగ్ చేయడానికి ఇంటర్నెట్ కనెక్షన్ లేదు"</string>
-    <string name="no_upstream_notification_message" msgid="5626323795587558017">"పరికరాలు కనెక్ట్ అవ్వడం లేదు"</string>
-    <string name="no_upstream_notification_disable_button" msgid="868677179945695858">"టెథరింగ్‌ను ఆఫ్ చేయండి"</string>
-    <string name="upstream_roaming_notification_title" msgid="2870229486619751829">"హాట్‌స్పాట్ లేదా టెథరింగ్ ఆన్‌లో ఉంది"</string>
-    <string name="upstream_roaming_notification_message" msgid="5229740963392849544">"రోమింగ్‌లో ఉన్నప్పుడు అదనపు ఛార్జీలు వర్తించవచ్చు"</string>
+    <string name="no_upstream_notification_title" msgid="5030042590486713460">"టెథరింగ్ చేయడానికి ఇంటర్నెట్ కనెక్షన్ లేదు"</string>
+    <string name="no_upstream_notification_message" msgid="3843613362272973447">"పరికరాలు కనెక్ట్ అవ్వడం లేదు"</string>
+    <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"టెథరింగ్‌ను ఆఫ్ చేయండి"</string>
+    <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"హాట్‌స్పాట్ లేదా టెథరింగ్ ఆన్‌లో ఉంది"</string>
+    <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"రోమింగ్‌లో ఉన్నప్పుడు అదనపు ఛార్జీలు వర్తించవచ్చు"</string>
 </resources>
diff --git a/Tethering/res/values-mcc310-mnc004-th/strings.xml b/Tethering/res/values-mcc310-mnc004-th/strings.xml
index e2ea084..f781fae 100644
--- a/Tethering/res/values-mcc310-mnc004-th/strings.xml
+++ b/Tethering/res/values-mcc310-mnc004-th/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="3584617491053416666">"ไม่มีอินเทอร์เน็ตสำหรับการเชื่อมต่ออินเทอร์เน็ตผ่านมือถือ"</string>
-    <string name="no_upstream_notification_message" msgid="5626323795587558017">"อุปกรณ์เชื่อมต่อไม่ได้"</string>
-    <string name="no_upstream_notification_disable_button" msgid="868677179945695858">"ปิดการเชื่อมต่ออินเทอร์เน็ตผ่านมือถือ"</string>
-    <string name="upstream_roaming_notification_title" msgid="2870229486619751829">"ฮอตสปอตหรือการเชื่อมต่ออินเทอร์เน็ตผ่านมือถือเปิดอยู่"</string>
-    <string name="upstream_roaming_notification_message" msgid="5229740963392849544">"อาจมีค่าใช้จ่ายเพิ่มเติมขณะโรมมิ่ง"</string>
+    <string name="no_upstream_notification_title" msgid="5030042590486713460">"การเชื่อมต่ออินเทอร์เน็ตผ่านมือถือไม่มีอินเทอร์เน็ต"</string>
+    <string name="no_upstream_notification_message" msgid="3843613362272973447">"อุปกรณ์เชื่อมต่อไม่ได้"</string>
+    <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"ปิดการเชื่อมต่ออินเทอร์เน็ตผ่านมือถือ"</string>
+    <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"ฮอตสปอตหรือการเชื่อมต่ออินเทอร์เน็ตผ่านมือถือเปิดอยู่"</string>
+    <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"อาจมีค่าใช้จ่ายเพิ่มเติมขณะโรมมิ่ง"</string>
 </resources>
diff --git a/Tethering/res/values-mcc310-mnc004-tl/strings.xml b/Tethering/res/values-mcc310-mnc004-tl/strings.xml
index 7b4b71c..8d5d465 100644
--- a/Tethering/res/values-mcc310-mnc004-tl/strings.xml
+++ b/Tethering/res/values-mcc310-mnc004-tl/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="3584617491053416666">"Walang internet ang pag-tether"</string>
-    <string name="no_upstream_notification_message" msgid="5626323795587558017">"Hindi makakonekta ang mga device"</string>
-    <string name="no_upstream_notification_disable_button" msgid="868677179945695858">"I-off ang pag-tether"</string>
-    <string name="upstream_roaming_notification_title" msgid="2870229486619751829">"Naka-on ang hotspot o pag-tether"</string>
-    <string name="upstream_roaming_notification_message" msgid="5229740963392849544">"Posibleng magkaroon ng mga karagdagang singil habang nagro-roam"</string>
+    <string name="no_upstream_notification_title" msgid="5030042590486713460">"Walang internet ang pag-tether"</string>
+    <string name="no_upstream_notification_message" msgid="3843613362272973447">"Hindi makakonekta ang mga device"</string>
+    <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"I-off ang pag-tether"</string>
+    <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"Naka-on ang Hotspot o pag-tether"</string>
+    <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"Posibleng magkaroon ng mga karagdagang singil habang nagro-roam"</string>
 </resources>
diff --git a/Tethering/res/values-mcc310-mnc004-tr/strings.xml b/Tethering/res/values-mcc310-mnc004-tr/strings.xml
index 066e1d7..80cab33 100644
--- a/Tethering/res/values-mcc310-mnc004-tr/strings.xml
+++ b/Tethering/res/values-mcc310-mnc004-tr/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="3584617491053416666">"Tethering\'in internet bağlantısı yok"</string>
-    <string name="no_upstream_notification_message" msgid="5626323795587558017">"Cihazlar bağlanamıyor"</string>
-    <string name="no_upstream_notification_disable_button" msgid="868677179945695858">"Tethering\'i kapat"</string>
-    <string name="upstream_roaming_notification_title" msgid="2870229486619751829">"Hotspot veya tethering açık"</string>
-    <string name="upstream_roaming_notification_message" msgid="5229740963392849544">"Dolaşım sırasında ek ücretler uygulanabilir"</string>
+    <string name="no_upstream_notification_title" msgid="5030042590486713460">"Tethering\'in internet bağlantısı yok"</string>
+    <string name="no_upstream_notification_message" msgid="3843613362272973447">"Cihazlar bağlanamıyor"</string>
+    <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"Tethering\'i kapat"</string>
+    <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"Hotspot veya tethering açık"</string>
+    <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"Dolaşım sırasında ek ücretler uygulanabilir"</string>
 </resources>
diff --git a/Tethering/res/values-mcc310-mnc004-uk/strings.xml b/Tethering/res/values-mcc310-mnc004-uk/strings.xml
index 036746a..c05932a 100644
--- a/Tethering/res/values-mcc310-mnc004-uk/strings.xml
+++ b/Tethering/res/values-mcc310-mnc004-uk/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="3584617491053416666">"Телефон, що використовується як модем, не підключений до Інтернету"</string>
-    <string name="no_upstream_notification_message" msgid="5626323795587558017">"Не вдається підключити пристрої"</string>
-    <string name="no_upstream_notification_disable_button" msgid="868677179945695858">"Вимкнути використання телефона як модема"</string>
-    <string name="upstream_roaming_notification_title" msgid="2870229486619751829">"Увімкнено точку доступу або використання телефона як модема"</string>
-    <string name="upstream_roaming_notification_message" msgid="5229740963392849544">"У роумінгу може стягуватися додаткова плата"</string>
+    <string name="no_upstream_notification_title" msgid="5030042590486713460">"Телефон, який використовується як модем, не підключений до Інтернету"</string>
+    <string name="no_upstream_notification_message" msgid="3843613362272973447">"Не вдається підключити пристрої"</string>
+    <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"Вимкнути використання телефона як модема"</string>
+    <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"Увімкнено точку доступу або використання телефона як модема"</string>
+    <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"У роумінгу може стягуватися додаткова плата"</string>
 </resources>
diff --git a/Tethering/res/values-mcc310-mnc004-ur/strings.xml b/Tethering/res/values-mcc310-mnc004-ur/strings.xml
index 90eadef..d820eee 100644
--- a/Tethering/res/values-mcc310-mnc004-ur/strings.xml
+++ b/Tethering/res/values-mcc310-mnc004-ur/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="3584617491053416666">"ٹیدرنگ میں انٹرنیٹ نہیں ہے"</string>
-    <string name="no_upstream_notification_message" msgid="5626323795587558017">"آلات منسلک نہیں ہو سکتے"</string>
-    <string name="no_upstream_notification_disable_button" msgid="868677179945695858">"ٹیدرنگ آف کریں"</string>
-    <string name="upstream_roaming_notification_title" msgid="2870229486619751829">"ہاٹ اسپاٹ یا ٹیدرنگ آن ہے"</string>
-    <string name="upstream_roaming_notification_message" msgid="5229740963392849544">"رومنگ کے دوران اضافی چارجز لاگو ہو سکتے ہیں"</string>
+    <string name="no_upstream_notification_title" msgid="5030042590486713460">"ٹیدرنگ میں انٹرنیٹ نہیں ہے"</string>
+    <string name="no_upstream_notification_message" msgid="3843613362272973447">"آلات منسلک نہیں ہو سکتے"</string>
+    <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"ٹیدرنگ آف کریں"</string>
+    <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"ہاٹ اسپاٹ یا ٹیدرنگ آن ہے"</string>
+    <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"رومنگ کے دوران اضافی چارجز لاگو ہو سکتے ہیں"</string>
 </resources>
diff --git a/Tethering/res/values-mcc310-mnc004-uz/strings.xml b/Tethering/res/values-mcc310-mnc004-uz/strings.xml
index f647572..726148a 100644
--- a/Tethering/res/values-mcc310-mnc004-uz/strings.xml
+++ b/Tethering/res/values-mcc310-mnc004-uz/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="3584617491053416666">"Modem internetga ulanmagan"</string>
-    <string name="no_upstream_notification_message" msgid="5626323795587558017">"Qurilmalar ulanmadi"</string>
-    <string name="no_upstream_notification_disable_button" msgid="868677179945695858">"Modem rejimini faolsizlantirish"</string>
-    <string name="upstream_roaming_notification_title" msgid="2870229486619751829">"Hotspot yoki modem rejimi yoniq"</string>
-    <string name="upstream_roaming_notification_message" msgid="5229740963392849544">"Rouming vaqtida qoʻshimcha haq olinishi mumkin"</string>
+    <string name="no_upstream_notification_title" msgid="5030042590486713460">"Modem internetga ulanmagan"</string>
+    <string name="no_upstream_notification_message" msgid="3843613362272973447">"Qurilmalar ulanmadi"</string>
+    <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"Modem rejimini faolsizlantirish"</string>
+    <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"Hotspot yoki modem rejimi yoniq"</string>
+    <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"Rouming vaqtida qoʻshimcha haq olinishi mumkin"</string>
 </resources>
diff --git a/Tethering/res/values-mcc310-mnc004-vi/strings.xml b/Tethering/res/values-mcc310-mnc004-vi/strings.xml
index 71db045..b7cb045 100644
--- a/Tethering/res/values-mcc310-mnc004-vi/strings.xml
+++ b/Tethering/res/values-mcc310-mnc004-vi/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="3584617491053416666">"Không có Internet để chia sẻ Internet"</string>
-    <string name="no_upstream_notification_message" msgid="5626323795587558017">"Các thiết bị không thể kết nối"</string>
-    <string name="no_upstream_notification_disable_button" msgid="868677179945695858">"Tắt tính năng chia sẻ Internet"</string>
-    <string name="upstream_roaming_notification_title" msgid="2870229486619751829">"Điểm phát sóng hoặc tính năng chia sẻ Internet đang bật"</string>
-    <string name="upstream_roaming_notification_message" msgid="5229740963392849544">"Bạn có thể mất thêm phí dữ liệu khi chuyển vùng"</string>
+    <string name="no_upstream_notification_title" msgid="5030042590486713460">"Không có Internet để chia sẻ kết Internet"</string>
+    <string name="no_upstream_notification_message" msgid="3843613362272973447">"Các thiết bị không thể kết nối"</string>
+    <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"Tắt tính năng chia sẻ Internet"</string>
+    <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"Điểm phát sóng hoặc tính năng chia sẻ Internet đang bật"</string>
+    <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"Bạn có thể mất thêm phí dữ liệu khi chuyển vùng"</string>
 </resources>
diff --git a/Tethering/res/values-mcc310-mnc004-zh-rCN/strings.xml b/Tethering/res/values-mcc310-mnc004-zh-rCN/strings.xml
index d279fdd..af91aff 100644
--- a/Tethering/res/values-mcc310-mnc004-zh-rCN/strings.xml
+++ b/Tethering/res/values-mcc310-mnc004-zh-rCN/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="3584617491053416666">"共享网络未连接到互联网"</string>
-    <string name="no_upstream_notification_message" msgid="5626323795587558017">"设备无法连接"</string>
-    <string name="no_upstream_notification_disable_button" msgid="868677179945695858">"关闭网络共享"</string>
-    <string name="upstream_roaming_notification_title" msgid="2870229486619751829">"热点或网络共享已开启"</string>
-    <string name="upstream_roaming_notification_message" msgid="5229740963392849544">"漫游时可能会产生额外的费用"</string>
+    <string name="no_upstream_notification_title" msgid="5030042590486713460">"共享网络未连接到互联网"</string>
+    <string name="no_upstream_notification_message" msgid="3843613362272973447">"设备无法连接"</string>
+    <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"关闭网络共享"</string>
+    <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"热点或网络共享已开启"</string>
+    <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"漫游时可能会产生额外的费用"</string>
 </resources>
diff --git a/Tethering/res/values-mcc310-mnc004-zh-rHK/strings.xml b/Tethering/res/values-mcc310-mnc004-zh-rHK/strings.xml
index 5bad7e4..28e6b80 100644
--- a/Tethering/res/values-mcc310-mnc004-zh-rHK/strings.xml
+++ b/Tethering/res/values-mcc310-mnc004-zh-rHK/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="3584617491053416666">"無法透過網絡共享連線至互聯網"</string>
-    <string name="no_upstream_notification_message" msgid="5626323795587558017">"裝置無法連接"</string>
-    <string name="no_upstream_notification_disable_button" msgid="868677179945695858">"關閉網絡共享"</string>
-    <string name="upstream_roaming_notification_title" msgid="2870229486619751829">"熱點或網絡共享已開啟"</string>
-    <string name="upstream_roaming_notification_message" msgid="5229740963392849544">"漫遊時可能需要支付額外費用"</string>
+    <string name="no_upstream_notification_title" msgid="5030042590486713460">"無法透過網絡共享連線至互聯網"</string>
+    <string name="no_upstream_notification_message" msgid="3843613362272973447">"裝置無法連接"</string>
+    <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"關閉網絡共享"</string>
+    <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"熱點或網絡共享已開啟"</string>
+    <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"漫遊時可能需要支付額外費用"</string>
 </resources>
diff --git a/Tethering/res/values-mcc310-mnc004-zh-rTW/strings.xml b/Tethering/res/values-mcc310-mnc004-zh-rTW/strings.xml
index 8991ff4..528a1e5 100644
--- a/Tethering/res/values-mcc310-mnc004-zh-rTW/strings.xml
+++ b/Tethering/res/values-mcc310-mnc004-zh-rTW/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="3584617491053416666">"無法透過網路共用連上網際網路"</string>
-    <string name="no_upstream_notification_message" msgid="5626323795587558017">"裝置無法連線"</string>
-    <string name="no_upstream_notification_disable_button" msgid="868677179945695858">"關閉網路共用"</string>
-    <string name="upstream_roaming_notification_title" msgid="2870229486619751829">"無線基地台或網路共用已開啟"</string>
-    <string name="upstream_roaming_notification_message" msgid="5229740963392849544">"使用漫遊服務可能須支付額外費用"</string>
+    <string name="no_upstream_notification_title" msgid="5030042590486713460">"無法透過網路共用連上網際網路"</string>
+    <string name="no_upstream_notification_message" msgid="3843613362272973447">"裝置無法連線"</string>
+    <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"關閉網路共用"</string>
+    <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"無線基地台或網路共用已開啟"</string>
+    <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"使用漫遊服務可能須支付額外費用"</string>
 </resources>
diff --git a/Tethering/res/values-mcc310-mnc004-zu/strings.xml b/Tethering/res/values-mcc310-mnc004-zu/strings.xml
index 31db66a..11eb666 100644
--- a/Tethering/res/values-mcc310-mnc004-zu/strings.xml
+++ b/Tethering/res/values-mcc310-mnc004-zu/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="3584617491053416666">"Ukusebenzisa ifoni njengemodemu akunayo i-inthanethi"</string>
-    <string name="no_upstream_notification_message" msgid="5626323795587558017">"Amadivayisi awakwazi ukuxhuma"</string>
-    <string name="no_upstream_notification_disable_button" msgid="868677179945695858">"Vala ukusebenzisa ifoni njengemodemu"</string>
-    <string name="upstream_roaming_notification_title" msgid="2870229486619751829">"I-hotspot noma ukusebenzisa ifoni njengemodemu kuvuliwe"</string>
-    <string name="upstream_roaming_notification_message" msgid="5229740963392849544">"Kungaba nezinkokhelo ezengeziwe uma uzula"</string>
+    <string name="no_upstream_notification_title" msgid="5030042590486713460">"Ukusebenzisa ifoni njengemodemu akunayo i-inthanethi"</string>
+    <string name="no_upstream_notification_message" msgid="3843613362272973447">"Amadivayisi awakwazi ukuxhumeka"</string>
+    <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"Vala ukusebenzisa ifoni njengemodemu"</string>
+    <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"I-hotspot noma ukusebenzisa ifoni njengemodemu kuvuliwe"</string>
+    <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"Kungaba nezinkokhelo ezengeziwe uma uzula"</string>
 </resources>
diff --git a/Tethering/res/values-mcc311-mnc480-af/strings.xml b/Tethering/res/values-mcc311-mnc480-af/strings.xml
index cc70b66..9bfa531 100644
--- a/Tethering/res/values-mcc311-mnc480-af/strings.xml
+++ b/Tethering/res/values-mcc311-mnc480-af/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="5897815458155858594">"Verbinding het nie internet nie"</string>
-    <string name="no_upstream_notification_message" msgid="9037716118606459874">"Toestelle kan nie koppel nie"</string>
-    <string name="no_upstream_notification_disable_button" msgid="5284024068281565456">"Skakel verbinding af"</string>
-    <string name="upstream_roaming_notification_title" msgid="186331286017243006">"Warmkol of verbinding is aan"</string>
-    <string name="upstream_roaming_notification_message" msgid="7692641323940316538">"Bykomende heffings kan geld terwyl jy swerf"</string>
+    <string name="no_upstream_notification_title" msgid="611650570559011140">"Verbinding het nie internet nie"</string>
+    <string name="no_upstream_notification_message" msgid="6508394877641864863">"Toestelle kan nie koppel nie"</string>
+    <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"Skakel verbinding af"</string>
+    <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"Warmkol of verbinding is aan"</string>
+    <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"Bykomende heffings kan geld terwyl jy swerf"</string>
 </resources>
diff --git a/Tethering/res/values-mcc311-mnc480-am/strings.xml b/Tethering/res/values-mcc311-mnc480-am/strings.xml
index 9808534..5949dfa 100644
--- a/Tethering/res/values-mcc311-mnc480-am/strings.xml
+++ b/Tethering/res/values-mcc311-mnc480-am/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="5897815458155858594">"እንደ ሞደም መሰካት ምንም በይነመረብ የለውም"</string>
-    <string name="no_upstream_notification_message" msgid="9037716118606459874">"መሣሪያዎችን ማገናኘት አልተቻልም"</string>
-    <string name="no_upstream_notification_disable_button" msgid="5284024068281565456">"እንደ ሞደም መሰካትን አጥፋ"</string>
-    <string name="upstream_roaming_notification_title" msgid="186331286017243006">"መገናኛ ነጥብ ወይም እንደ ሞደም መሰካት በርቷል"</string>
-    <string name="upstream_roaming_notification_message" msgid="7692641323940316538">"በሚያንዣብብበት ጊዜ ተጨማሪ ክፍያዎች ተፈጻሚ ሊሆኑ ይችላሉ"</string>
+    <string name="no_upstream_notification_title" msgid="611650570559011140">"ማስተሳሰር ምንም በይነመረብ የለውም"</string>
+    <string name="no_upstream_notification_message" msgid="6508394877641864863">"መሣሪያዎችን ማገናኘት አይቻልም"</string>
+    <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"ማስተሳሰርን አጥፋ"</string>
+    <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"መገናኛ ነጥብ ወይም ማስተሳሰር በርቷል"</string>
+    <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"በሚያንዣብብበት ጊዜ ተጨማሪ ክፍያዎች ተፈጻሚ ሊሆኑ ይችላሉ"</string>
 </resources>
diff --git a/Tethering/res/values-mcc311-mnc480-ar/strings.xml b/Tethering/res/values-mcc311-mnc480-ar/strings.xml
index ab84c4a..8467f9b 100644
--- a/Tethering/res/values-mcc311-mnc480-ar/strings.xml
+++ b/Tethering/res/values-mcc311-mnc480-ar/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="5897815458155858594">"ما مِن اتصال بالإنترنت خلال التوصيل"</string>
-    <string name="no_upstream_notification_message" msgid="9037716118606459874">"يتعذّر اتصال الأجهزة"</string>
-    <string name="no_upstream_notification_disable_button" msgid="5284024068281565456">"إيقاف التوصيل"</string>
-    <string name="upstream_roaming_notification_title" msgid="186331286017243006">"نقطة الاتصال مفعَّلة أو التوصيل مفعَّل"</string>
-    <string name="upstream_roaming_notification_message" msgid="7692641323940316538">"قد يتم تحصيل رسوم إضافية أثناء التجوال."</string>
+    <string name="no_upstream_notification_title" msgid="611650570559011140">"ما مِن اتصال بالإنترنت خلال التوصيل"</string>
+    <string name="no_upstream_notification_message" msgid="6508394877641864863">"تعذّر اتصال الأجهزة"</string>
+    <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"إيقاف التوصيل"</string>
+    <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"نقطة الاتصال أو التوصيل مفعّلان"</string>
+    <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"قد يتم تطبيق رسوم إضافية أثناء التجوال."</string>
 </resources>
diff --git a/Tethering/res/values-mcc311-mnc480-as/strings.xml b/Tethering/res/values-mcc311-mnc480-as/strings.xml
index f7ab7e9..9776bd8 100644
--- a/Tethering/res/values-mcc311-mnc480-as/strings.xml
+++ b/Tethering/res/values-mcc311-mnc480-as/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="5897815458155858594">"টে’ডাৰিঙৰ ইণ্টাৰনেট নাই"</string>
-    <string name="no_upstream_notification_message" msgid="9037716118606459874">"ডিভাইচসমূহ সংযোগ কৰিব নোৱাৰি"</string>
-    <string name="no_upstream_notification_disable_button" msgid="5284024068281565456">"টে’ডাৰিং অফ কৰক"</string>
-    <string name="upstream_roaming_notification_title" msgid="186331286017243006">"হ’টস্প’ট অথবা টে’ডাৰিং অন আছে"</string>
-    <string name="upstream_roaming_notification_message" msgid="7692641323940316538">"ৰ’মিঙত থাকিলে অতিৰিক্ত মাচুল প্ৰযোজ্য হ’ব পাৰে"</string>
+    <string name="no_upstream_notification_title" msgid="611650570559011140">"টে\'ডাৰিঙৰ ইণ্টাৰনেট নাই"</string>
+    <string name="no_upstream_notification_message" msgid="6508394877641864863">"ডিভাইচসমূহ সংযোগ কৰিব নোৱাৰি"</string>
+    <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"টে\'ডাৰিং অফ কৰক"</string>
+    <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"হটস্পট অথবা টে\'ডাৰিং অন আছে"</string>
+    <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"ৰ\'মিঙত থাকিলে অতিৰিক্ত মাচুল প্ৰযোজ্য হ’ব পাৰে"</string>
 </resources>
diff --git a/Tethering/res/values-mcc311-mnc480-az/strings.xml b/Tethering/res/values-mcc311-mnc480-az/strings.xml
index 6e36df1..e6d3eaf 100644
--- a/Tethering/res/values-mcc311-mnc480-az/strings.xml
+++ b/Tethering/res/values-mcc311-mnc480-az/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="5897815458155858594">"Modem rejimi internetə qoşulmayıb"</string>
-    <string name="no_upstream_notification_message" msgid="9037716118606459874">"Cihazları qoşmaq olmur"</string>
-    <string name="no_upstream_notification_disable_button" msgid="5284024068281565456">"Modem rejimini deaktiv edin"</string>
-    <string name="upstream_roaming_notification_title" msgid="186331286017243006">"Hotspot və ya modem rejimi aktivdir"</string>
-    <string name="upstream_roaming_notification_message" msgid="7692641323940316538">"Rouminq zamanı əlavə ödəniş çıxıla bilər"</string>
+    <string name="no_upstream_notification_title" msgid="611650570559011140">"Modemin internetə girişi yoxdur"</string>
+    <string name="no_upstream_notification_message" msgid="6508394877641864863">"Cihazları qoşmaq mümkün deyil"</string>
+    <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"Modemi deaktiv edin"</string>
+    <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"Hotspot və ya modem aktivdir"</string>
+    <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"Rouminq zamanı əlavə ödənişlər tətbiq edilə bilər"</string>
 </resources>
diff --git a/Tethering/res/values-mcc311-mnc480-b+sr+Latn/strings.xml b/Tethering/res/values-mcc311-mnc480-b+sr+Latn/strings.xml
index 1730075..4c8a1df 100644
--- a/Tethering/res/values-mcc311-mnc480-b+sr+Latn/strings.xml
+++ b/Tethering/res/values-mcc311-mnc480-b+sr+Latn/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="5897815458155858594">"Privezivanje nema pristup internetu"</string>
-    <string name="no_upstream_notification_message" msgid="9037716118606459874">"Povezivanje uređaja nije uspelo"</string>
-    <string name="no_upstream_notification_disable_button" msgid="5284024068281565456">"Isključi privezivanje"</string>
-    <string name="upstream_roaming_notification_title" msgid="186331286017243006">"Uključen je hotspot ili privezivanje"</string>
-    <string name="upstream_roaming_notification_message" msgid="7692641323940316538">"Možda važe dodatni troškovi u romingu"</string>
+    <string name="no_upstream_notification_title" msgid="611650570559011140">"Privezivanje nema pristup internetu"</string>
+    <string name="no_upstream_notification_message" msgid="6508394877641864863">"Povezivanje uređaja nije uspelo"</string>
+    <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"Isključi privezivanje"</string>
+    <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"Uključen je hotspot ili privezivanje"</string>
+    <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"Možda važe dodatni troškovi u romingu"</string>
 </resources>
diff --git a/Tethering/res/values-mcc311-mnc480-be/strings.xml b/Tethering/res/values-mcc311-mnc480-be/strings.xml
index 88577d4..edfa41e 100644
--- a/Tethering/res/values-mcc311-mnc480-be/strings.xml
+++ b/Tethering/res/values-mcc311-mnc480-be/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="5897815458155858594">"Рэжым мадэма выкарыстоўваецца без доступу да інтэрнэту"</string>
-    <string name="no_upstream_notification_message" msgid="9037716118606459874">"Не ўдалося падключыць прылады"</string>
-    <string name="no_upstream_notification_disable_button" msgid="5284024068281565456">"Выключыць рэжым мадэма"</string>
-    <string name="upstream_roaming_notification_title" msgid="186331286017243006">"Хот-спот або рэжым мадэма ўключаны"</string>
-    <string name="upstream_roaming_notification_message" msgid="7692641323940316538">"Пры выкарыстанні роўмінгу можа спаганяцца дадатковая плата"</string>
+    <string name="no_upstream_notification_title" msgid="611650570559011140">"Рэжым мадэма выкарыстоўваецца без доступу да інтэрнэту"</string>
+    <string name="no_upstream_notification_message" msgid="6508394877641864863">"Не ўдалося падключыць прылады"</string>
+    <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"Выключыць рэжым мадэма"</string>
+    <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"Хот-спот або рэжым мадэма ўключаны"</string>
+    <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"Пры выкарыстанні роўмінгу можа спаганяцца дадатковая плата"</string>
 </resources>
diff --git a/Tethering/res/values-mcc311-mnc480-bg/strings.xml b/Tethering/res/values-mcc311-mnc480-bg/strings.xml
index d549997..f563981 100644
--- a/Tethering/res/values-mcc311-mnc480-bg/strings.xml
+++ b/Tethering/res/values-mcc311-mnc480-bg/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="5897815458155858594">"Няма връзка с интернет за тетъринг"</string>
-    <string name="no_upstream_notification_message" msgid="9037716118606459874">"Устройствата не могат да установят връзка"</string>
-    <string name="no_upstream_notification_disable_button" msgid="5284024068281565456">"Изключване на функцията за тетъринг"</string>
-    <string name="upstream_roaming_notification_title" msgid="186331286017243006">"Точката за достъп или функцията за тетъринг са включени"</string>
-    <string name="upstream_roaming_notification_message" msgid="7692641323940316538">"Възможно е да ви бъдат начислени допълнителни такси при роуминг"</string>
+    <string name="no_upstream_notification_title" msgid="611650570559011140">"Тетърингът няма връзка с интернет"</string>
+    <string name="no_upstream_notification_message" msgid="6508394877641864863">"Устройствата не могат да установят връзка"</string>
+    <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"Изключване на тетъринга"</string>
+    <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"Точката за достъп или тетърингът са включени"</string>
+    <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"Възможно е да ви бъдат начислени допълнителни такси при роуминг"</string>
 </resources>
diff --git a/Tethering/res/values-mcc311-mnc480-bn/strings.xml b/Tethering/res/values-mcc311-mnc480-bn/strings.xml
index 93e316a..d8ecd2e 100644
--- a/Tethering/res/values-mcc311-mnc480-bn/strings.xml
+++ b/Tethering/res/values-mcc311-mnc480-bn/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="5897815458155858594">"টেথারিং করার জন্য কোনও ইন্টারনেট কানেকশন লাগে না"</string>
-    <string name="no_upstream_notification_message" msgid="9037716118606459874">"ডিভাইস কানেক্ট করা যাচ্ছে না"</string>
-    <string name="no_upstream_notification_disable_button" msgid="5284024068281565456">"টেথারিং বন্ধ করুন"</string>
-    <string name="upstream_roaming_notification_title" msgid="186331286017243006">"হটস্পট বা টেথারিং চালু আছে"</string>
-    <string name="upstream_roaming_notification_message" msgid="7692641323940316538">"রোমিংয়ে থাকার সময় অতিরিক্ত চার্জ লাগতে পারে"</string>
+    <string name="no_upstream_notification_title" msgid="611650570559011140">"টিথারিং করার জন্য কোনও ইন্টারনেট কানেকশন নেই"</string>
+    <string name="no_upstream_notification_message" msgid="6508394877641864863">"ডিভাইস কানেক্ট করতে পারছে না"</string>
+    <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"টিথারিং বন্ধ করুন"</string>
+    <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"হটস্পট বা টিথারিং চালু আছে"</string>
+    <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"রোমিংয়ের সময় অতিরিক্ত চার্জ করা হতে পারে"</string>
 </resources>
diff --git a/Tethering/res/values-mcc311-mnc480-bs/strings.xml b/Tethering/res/values-mcc311-mnc480-bs/strings.xml
index 27777c2..b85fd5e 100644
--- a/Tethering/res/values-mcc311-mnc480-bs/strings.xml
+++ b/Tethering/res/values-mcc311-mnc480-bs/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="5897815458155858594">"Dijeljenje internetske veze nema internet"</string>
-    <string name="no_upstream_notification_message" msgid="9037716118606459874">"Nije moguće povezati uređaje"</string>
-    <string name="no_upstream_notification_disable_button" msgid="5284024068281565456">"Isključi dijeljenje internetske veze"</string>
-    <string name="upstream_roaming_notification_title" msgid="186331286017243006">"Pristupna tačka ili dijeljenje internetske veze su uključeni"</string>
-    <string name="upstream_roaming_notification_message" msgid="7692641323940316538">"Mogu nastati dodatni troškovi u romingu"</string>
+    <string name="no_upstream_notification_title" msgid="611650570559011140">"Povezivanje putem mobitela nema internet"</string>
+    <string name="no_upstream_notification_message" msgid="6508394877641864863">"Uređaji se ne mogu povezati"</string>
+    <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"Isključi povezivanje putem mobitela"</string>
+    <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"Pristupna tačka ili povezivanje putem mobitela je uključeno"</string>
+    <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"Mogu nastati dodatni troškovi u romingu"</string>
 </resources>
diff --git a/Tethering/res/values-mcc311-mnc480-ca/strings.xml b/Tethering/res/values-mcc311-mnc480-ca/strings.xml
index dad35f8..a357215 100644
--- a/Tethering/res/values-mcc311-mnc480-ca/strings.xml
+++ b/Tethering/res/values-mcc311-mnc480-ca/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="5897815458155858594">"La compartició de xarxa no té accés a Internet"</string>
-    <string name="no_upstream_notification_message" msgid="9037716118606459874">"No es poden connectar els dispositius"</string>
-    <string name="no_upstream_notification_disable_button" msgid="5284024068281565456">"Desactiva la compartició de xarxa"</string>
-    <string name="upstream_roaming_notification_title" msgid="186331286017243006">"El punt d\'accés Wi‑Fi o la compartició de xarxa estan activats"</string>
-    <string name="upstream_roaming_notification_message" msgid="7692641323940316538">"És possible que s\'apliquin costos addicionals en itinerància"</string>
+    <string name="no_upstream_notification_title" msgid="611650570559011140">"La compartició de xarxa no té accés a Internet"</string>
+    <string name="no_upstream_notification_message" msgid="6508394877641864863">"No es poden connectar els dispositius"</string>
+    <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"Desactiva la compartició de xarxa"</string>
+    <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"S\'ha activat el punt d\'accés Wi‑Fi o la compartició de xarxa"</string>
+    <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"És possible que s\'apliquin costos addicionals en itinerància"</string>
 </resources>
diff --git a/Tethering/res/values-mcc311-mnc480-cs/strings.xml b/Tethering/res/values-mcc311-mnc480-cs/strings.xml
index cbcabe1..91196be 100644
--- a/Tethering/res/values-mcc311-mnc480-cs/strings.xml
+++ b/Tethering/res/values-mcc311-mnc480-cs/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="5897815458155858594">"Tethering nemá připojení k internetu"</string>
-    <string name="no_upstream_notification_message" msgid="9037716118606459874">"Zařízení se nemůžou připojit"</string>
-    <string name="no_upstream_notification_disable_button" msgid="5284024068281565456">"Vypnout tethering"</string>
-    <string name="upstream_roaming_notification_title" msgid="186331286017243006">"Je zapnutý hotspot nebo tethering"</string>
-    <string name="upstream_roaming_notification_message" msgid="7692641323940316538">"Při roamingu mohou být účtovány dodatečné poplatky"</string>
+    <string name="no_upstream_notification_title" msgid="611650570559011140">"Tethering nemá připojení k internetu"</string>
+    <string name="no_upstream_notification_message" msgid="6508394877641864863">"Zařízení se nemůžou připojit"</string>
+    <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"Vypnout tethering"</string>
+    <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"Je zapnutý hotspot nebo tethering"</string>
+    <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"Při roamingu mohou být účtovány dodatečné poplatky"</string>
 </resources>
diff --git a/Tethering/res/values-mcc311-mnc480-da/strings.xml b/Tethering/res/values-mcc311-mnc480-da/strings.xml
index 9176709..1968900 100644
--- a/Tethering/res/values-mcc311-mnc480-da/strings.xml
+++ b/Tethering/res/values-mcc311-mnc480-da/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="5897815458155858594">"Netdeling har ingen internetforbindelse"</string>
-    <string name="no_upstream_notification_message" msgid="9037716118606459874">"Enheder kan ikke oprette forbindelse"</string>
-    <string name="no_upstream_notification_disable_button" msgid="5284024068281565456">"Deaktiver netdeling"</string>
-    <string name="upstream_roaming_notification_title" msgid="186331286017243006">"Hotspot eller netdeling er aktiveret"</string>
-    <string name="upstream_roaming_notification_message" msgid="7692641323940316538">"Der opkræves muligvis yderligere gebyrer ved roaming"</string>
+    <string name="no_upstream_notification_title" msgid="611650570559011140">"Netdeling har ingen internetforbindelse"</string>
+    <string name="no_upstream_notification_message" msgid="6508394877641864863">"Enheder kan ikke oprette forbindelse"</string>
+    <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"Deaktiver netdeling"</string>
+    <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"Hotspot eller netdeling er aktiveret"</string>
+    <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"Der opkræves muligvis yderligere gebyrer ved roaming"</string>
 </resources>
diff --git a/Tethering/res/values-mcc311-mnc480-de/strings.xml b/Tethering/res/values-mcc311-mnc480-de/strings.xml
index b3bc7d8..eb3f8c5 100644
--- a/Tethering/res/values-mcc311-mnc480-de/strings.xml
+++ b/Tethering/res/values-mcc311-mnc480-de/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="5897815458155858594">"Tethering hat keinen Internetzugriff"</string>
-    <string name="no_upstream_notification_message" msgid="9037716118606459874">"Geräte können sich nicht verbinden"</string>
-    <string name="no_upstream_notification_disable_button" msgid="5284024068281565456">"Tethering deaktivieren"</string>
-    <string name="upstream_roaming_notification_title" msgid="186331286017243006">"Hotspot oder Tethering ist aktiviert"</string>
-    <string name="upstream_roaming_notification_message" msgid="7692641323940316538">"Für das Roaming können zusätzliche Gebühren anfallen"</string>
+    <string name="no_upstream_notification_title" msgid="611650570559011140">"Tethering hat keinen Internetzugriff"</string>
+    <string name="no_upstream_notification_message" msgid="6508394877641864863">"Geräte können sich nicht verbinden"</string>
+    <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"Tethering deaktivieren"</string>
+    <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"Hotspot oder Tethering ist aktiviert"</string>
+    <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"Für das Roaming können zusätzliche Gebühren anfallen"</string>
 </resources>
diff --git a/Tethering/res/values-mcc311-mnc480-el/strings.xml b/Tethering/res/values-mcc311-mnc480-el/strings.xml
index babd62c..56c3d81 100644
--- a/Tethering/res/values-mcc311-mnc480-el/strings.xml
+++ b/Tethering/res/values-mcc311-mnc480-el/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="5897815458155858594">"Η σύνδεση δεν έχει πρόσβαση στο διαδίκτυο"</string>
-    <string name="no_upstream_notification_message" msgid="9037716118606459874">"Δεν είναι δυνατή η σύνδεση των συσκευών"</string>
-    <string name="no_upstream_notification_disable_button" msgid="5284024068281565456">"Απενεργοποίηση σύνδεσης"</string>
-    <string name="upstream_roaming_notification_title" msgid="186331286017243006">"Ενεργό σημείο πρόσβασης Wi-Fi ή ενεργή σύνδεση"</string>
-    <string name="upstream_roaming_notification_message" msgid="7692641323940316538">"Ενδέχεται να ισχύουν επιπλέον χρεώσεις κατά την περιαγωγή."</string>
+    <string name="no_upstream_notification_title" msgid="611650570559011140">"Η σύνδεση δεν έχει πρόσβαση στο διαδίκτυο"</string>
+    <string name="no_upstream_notification_message" msgid="6508394877641864863">"Δεν είναι δυνατή η σύνδεση των συσκευών"</string>
+    <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"Απενεργοποιήστε τη σύνδεση"</string>
+    <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"Ενεργό σημείο πρόσβασης Wi-Fi ή ενεργή σύνδεση"</string>
+    <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"Ενδέχεται να ισχύουν επιπλέον χρεώσεις κατά την περιαγωγή."</string>
 </resources>
diff --git a/Tethering/res/values-mcc311-mnc480-en-rAU/strings.xml b/Tethering/res/values-mcc311-mnc480-en-rAU/strings.xml
index afa4467..dd1a197 100644
--- a/Tethering/res/values-mcc311-mnc480-en-rAU/strings.xml
+++ b/Tethering/res/values-mcc311-mnc480-en-rAU/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="5897815458155858594">"Tethering has no Internet"</string>
-    <string name="no_upstream_notification_message" msgid="9037716118606459874">"Devices can\'t connect"</string>
-    <string name="no_upstream_notification_disable_button" msgid="5284024068281565456">"Turn off tethering"</string>
-    <string name="upstream_roaming_notification_title" msgid="186331286017243006">"Hotspot or tethering is on"</string>
-    <string name="upstream_roaming_notification_message" msgid="7692641323940316538">"Additional charges may apply while roaming"</string>
+    <string name="no_upstream_notification_title" msgid="611650570559011140">"Tethering has no Internet"</string>
+    <string name="no_upstream_notification_message" msgid="6508394877641864863">"Devices can’t connect"</string>
+    <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"Turn off tethering"</string>
+    <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"Hotspot or tethering is on"</string>
+    <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"Additional charges may apply while roaming"</string>
 </resources>
diff --git a/Tethering/res/values-mcc311-mnc480-en-rCA/strings.xml b/Tethering/res/values-mcc311-mnc480-en-rCA/strings.xml
index 251cad6..dd1a197 100644
--- a/Tethering/res/values-mcc311-mnc480-en-rCA/strings.xml
+++ b/Tethering/res/values-mcc311-mnc480-en-rCA/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="5897815458155858594">"Tethering has no internet"</string>
-    <string name="no_upstream_notification_message" msgid="9037716118606459874">"Devices can’t connect"</string>
-    <string name="no_upstream_notification_disable_button" msgid="5284024068281565456">"Turn off tethering"</string>
-    <string name="upstream_roaming_notification_title" msgid="186331286017243006">"Hotspot or tethering is on"</string>
-    <string name="upstream_roaming_notification_message" msgid="7692641323940316538">"Additional charges may apply while roaming"</string>
+    <string name="no_upstream_notification_title" msgid="611650570559011140">"Tethering has no Internet"</string>
+    <string name="no_upstream_notification_message" msgid="6508394877641864863">"Devices can’t connect"</string>
+    <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"Turn off tethering"</string>
+    <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"Hotspot or tethering is on"</string>
+    <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"Additional charges may apply while roaming"</string>
 </resources>
diff --git a/Tethering/res/values-mcc311-mnc480-en-rGB/strings.xml b/Tethering/res/values-mcc311-mnc480-en-rGB/strings.xml
index afa4467..dd1a197 100644
--- a/Tethering/res/values-mcc311-mnc480-en-rGB/strings.xml
+++ b/Tethering/res/values-mcc311-mnc480-en-rGB/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="5897815458155858594">"Tethering has no Internet"</string>
-    <string name="no_upstream_notification_message" msgid="9037716118606459874">"Devices can\'t connect"</string>
-    <string name="no_upstream_notification_disable_button" msgid="5284024068281565456">"Turn off tethering"</string>
-    <string name="upstream_roaming_notification_title" msgid="186331286017243006">"Hotspot or tethering is on"</string>
-    <string name="upstream_roaming_notification_message" msgid="7692641323940316538">"Additional charges may apply while roaming"</string>
+    <string name="no_upstream_notification_title" msgid="611650570559011140">"Tethering has no Internet"</string>
+    <string name="no_upstream_notification_message" msgid="6508394877641864863">"Devices can’t connect"</string>
+    <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"Turn off tethering"</string>
+    <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"Hotspot or tethering is on"</string>
+    <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"Additional charges may apply while roaming"</string>
 </resources>
diff --git a/Tethering/res/values-mcc311-mnc480-en-rIN/strings.xml b/Tethering/res/values-mcc311-mnc480-en-rIN/strings.xml
index afa4467..dd1a197 100644
--- a/Tethering/res/values-mcc311-mnc480-en-rIN/strings.xml
+++ b/Tethering/res/values-mcc311-mnc480-en-rIN/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="5897815458155858594">"Tethering has no Internet"</string>
-    <string name="no_upstream_notification_message" msgid="9037716118606459874">"Devices can\'t connect"</string>
-    <string name="no_upstream_notification_disable_button" msgid="5284024068281565456">"Turn off tethering"</string>
-    <string name="upstream_roaming_notification_title" msgid="186331286017243006">"Hotspot or tethering is on"</string>
-    <string name="upstream_roaming_notification_message" msgid="7692641323940316538">"Additional charges may apply while roaming"</string>
+    <string name="no_upstream_notification_title" msgid="611650570559011140">"Tethering has no Internet"</string>
+    <string name="no_upstream_notification_message" msgid="6508394877641864863">"Devices can’t connect"</string>
+    <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"Turn off tethering"</string>
+    <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"Hotspot or tethering is on"</string>
+    <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"Additional charges may apply while roaming"</string>
 </resources>
diff --git a/Tethering/res/values-mcc311-mnc480-en-rXC/strings.xml b/Tethering/res/values-mcc311-mnc480-en-rXC/strings.xml
index 766a0e8..d3347aa 100644
--- a/Tethering/res/values-mcc311-mnc480-en-rXC/strings.xml
+++ b/Tethering/res/values-mcc311-mnc480-en-rXC/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="5897815458155858594">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‏‎‎‎‏‏‏‎‏‏‎‎‏‎‎‏‏‏‏‏‏‏‏‏‎‏‎‎‏‎‏‎‎‎‏‏‎‎‎‏‏‎‎‎‎‎‎‎‎‏‎‏‎‏‎‏‎‎‎‏‎‎Tethering has no internet‎‏‎‎‏‎"</string>
-    <string name="no_upstream_notification_message" msgid="9037716118606459874">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‏‏‏‎‏‎‏‏‎‏‏‎‎‎‏‏‎‏‎‏‎‏‏‏‎‏‏‎‏‎‎‎‎‏‏‏‎‎‏‎‏‏‏‎‎‎‎‏‏‎‎‏‏‏‏‏‎‎‎‏‎‎Devices can’t connect‎‏‎‎‏‎"</string>
-    <string name="no_upstream_notification_disable_button" msgid="5284024068281565456">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‎‏‎‎‏‎‏‎‏‎‏‎‎‏‎‎‏‏‏‏‏‏‏‏‎‎‏‎‎‏‏‏‎‎‏‏‎‏‎‎‎‏‎‎‏‏‎‎‎‎‎‎‏‎‎‎‏‎‎‎‎‎Turn off tethering‎‏‎‎‏‎"</string>
-    <string name="upstream_roaming_notification_title" msgid="186331286017243006">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‎‏‎‏‎‏‎‎‏‎‏‎‏‏‏‏‏‏‎‏‏‎‏‎‏‎‎‎‏‎‏‎‎‎‎‎‏‏‎‎‏‏‎‎‏‎‏‏‏‎‏‏‏‎‏‏‏‏‏‏‎‎Hotspot or tethering is on‎‏‎‎‏‎"</string>
-    <string name="upstream_roaming_notification_message" msgid="7692641323940316538">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‎‏‎‏‎‏‏‎‎‎‎‎‏‏‏‎‎‎‎‎‎‏‎‎‎‎‏‏‎‏‏‎‏‏‎‎‏‏‏‏‏‎‏‎‎‎‎‎‎‎‏‎‏‎‏‏‏‏‎‏‎‎Additional charges may apply while roaming‎‏‎‎‏‎"</string>
+    <string name="no_upstream_notification_title" msgid="611650570559011140">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‎‏‎‎‎‎‏‏‏‏‏‎‏‎‎‎‎‎‏‎‎‏‏‏‏‎‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‎‏‎‏‏‎‏‏‏‎‎‏‎‏‎‎‎‏‎‎‎Tethering has no internet‎‏‎‎‏‎"</string>
+    <string name="no_upstream_notification_message" msgid="6508394877641864863">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‏‏‎‏‎‎‏‎‏‎‎‏‎‎‏‏‏‎‏‏‎‏‎‏‎‏‎‎‎‏‎‎‎‎‎‏‏‏‏‎‏‎‎‎‎‏‎‏‏‎‏‏‎‏‎‎‏‏‏‏‏‎Devices can’t connect‎‏‎‎‏‎"</string>
+    <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‎‏‎‎‏‏‎‎‏‏‎‎‏‏‏‎‏‎‏‎‎‎‏‏‏‎‎‏‏‏‏‎‎‏‏‏‏‏‏‎‎‎‎‎‎‎‏‏‏‏‏‏‏‏‎‏‎‏‏‎‎‎Turn off tethering‎‏‎‎‏‎"</string>
+    <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‏‎‎‏‏‏‎‏‏‏‎‎‏‎‎‏‎‏‎‏‏‏‎‏‎‎‏‏‏‏‏‏‎‏‏‏‏‏‏‏‏‎‏‎‎‏‏‏‏‎‏‎‎‎‏‎‎‎‎‏‏‎Hotspot or tethering is on‎‏‎‎‏‎"</string>
+    <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‎‏‎‎‏‎‏‏‏‎‏‎‏‎‏‎‎‎‏‎‏‎‏‏‎‏‎‎‎‏‏‏‏‎‎‏‏‏‏‎‎‎‏‎‎‎‎‏‏‎‏‎‏‎‎‏‏‎‎‏‏‎Additional charges may apply while roaming‎‏‎‎‏‎"</string>
 </resources>
diff --git a/Tethering/res/values-mcc311-mnc480-es-rUS/strings.xml b/Tethering/res/values-mcc311-mnc480-es-rUS/strings.xml
index 16c6059..2f0504f 100644
--- a/Tethering/res/values-mcc311-mnc480-es-rUS/strings.xml
+++ b/Tethering/res/values-mcc311-mnc480-es-rUS/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="5897815458155858594">"La conexión mediante dispositivo móvil no tiene Internet"</string>
-    <string name="no_upstream_notification_message" msgid="9037716118606459874">"No se pueden conectar los dispositivos"</string>
-    <string name="no_upstream_notification_disable_button" msgid="5284024068281565456">"Desactivar conexión mediante dispositivo móvil"</string>
-    <string name="upstream_roaming_notification_title" msgid="186331286017243006">"Se activó el hotspot o la conexión mediante dispositivo móvil"</string>
-    <string name="upstream_roaming_notification_message" msgid="7692641323940316538">"Es posible que se apliquen cargos adicionales por roaming"</string>
+    <string name="no_upstream_notification_title" msgid="611650570559011140">"La conexión mediante dispositivo móvil no tiene Internet"</string>
+    <string name="no_upstream_notification_message" msgid="6508394877641864863">"No se pueden conectar los dispositivos"</string>
+    <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"Desactivar conexión mediante dispositivo móvil"</string>
+    <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"Se activó el hotspot o la conexión mediante dispositivo móvil"</string>
+    <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"Es posible que se apliquen cargos adicionales por roaming"</string>
 </resources>
diff --git a/Tethering/res/values-mcc311-mnc480-es/strings.xml b/Tethering/res/values-mcc311-mnc480-es/strings.xml
index 952e056..2d8f882 100644
--- a/Tethering/res/values-mcc311-mnc480-es/strings.xml
+++ b/Tethering/res/values-mcc311-mnc480-es/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="5897815458155858594">"La conexión no se puede compartir, porque no hay acceso a Internet"</string>
-    <string name="no_upstream_notification_message" msgid="9037716118606459874">"Los dispositivos no se pueden conectar"</string>
-    <string name="no_upstream_notification_disable_button" msgid="5284024068281565456">"Desactivar conexión compartida"</string>
-    <string name="upstream_roaming_notification_title" msgid="186331286017243006">"Punto de acceso o conexión compartida activados"</string>
-    <string name="upstream_roaming_notification_message" msgid="7692641323940316538">"Pueden aplicarse cargos adicionales en roaming"</string>
+    <string name="no_upstream_notification_title" msgid="611650570559011140">"La conexión no se puede compartir, porque no hay acceso a Internet"</string>
+    <string name="no_upstream_notification_message" msgid="6508394877641864863">"Los dispositivos no se pueden conectar"</string>
+    <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"Desactivar conexión compartida"</string>
+    <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"Punto de acceso o conexión compartida activados"</string>
+    <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"Puede que se apliquen cargos adicionales en itinerancia"</string>
 </resources>
diff --git a/Tethering/res/values-mcc311-mnc480-et/strings.xml b/Tethering/res/values-mcc311-mnc480-et/strings.xml
index c9cae1f..8493c47 100644
--- a/Tethering/res/values-mcc311-mnc480-et/strings.xml
+++ b/Tethering/res/values-mcc311-mnc480-et/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="5897815458155858594">"Jagamisel puudub internetiühendus"</string>
-    <string name="no_upstream_notification_message" msgid="9037716118606459874">"Seadmed ei saa ühendust luua"</string>
-    <string name="no_upstream_notification_disable_button" msgid="5284024068281565456">"Lülita jagamine välja"</string>
-    <string name="upstream_roaming_notification_title" msgid="186331286017243006">"Kuumkoht või jagamine on sisse lülitatud"</string>
-    <string name="upstream_roaming_notification_message" msgid="7692641323940316538">"Rändluse kasutamisega võivad kaasneda lisatasud"</string>
+    <string name="no_upstream_notification_title" msgid="611650570559011140">"Jagamisel puudub internetiühendus"</string>
+    <string name="no_upstream_notification_message" msgid="6508394877641864863">"Seadmed ei saa ühendust luua"</string>
+    <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"Lülita jagamine välja"</string>
+    <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"Kuumkoht või jagamine on sisse lülitatud"</string>
+    <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"Rändluse kasutamisega võivad kaasneda lisatasud"</string>
 </resources>
diff --git a/Tethering/res/values-mcc311-mnc480-eu/strings.xml b/Tethering/res/values-mcc311-mnc480-eu/strings.xml
index 7abb4b0..33bccab 100644
--- a/Tethering/res/values-mcc311-mnc480-eu/strings.xml
+++ b/Tethering/res/values-mcc311-mnc480-eu/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="5897815458155858594">"Konexioa partekatzeko aukerak ez du Interneteko konexiorik"</string>
-    <string name="no_upstream_notification_message" msgid="9037716118606459874">"Ezin dira konektatu gailuak"</string>
-    <string name="no_upstream_notification_disable_button" msgid="5284024068281565456">"Desaktibatu konexioa partekatzeko aukera"</string>
-    <string name="upstream_roaming_notification_title" msgid="186331286017243006">"Wifi-gunea edo konexioa partekatzeko aukera aktibatuta dago"</string>
-    <string name="upstream_roaming_notification_message" msgid="7692641323940316538">"Baliteke tarifa gehigarriak ordaindu behar izatea ibiltaritza erabili bitartean"</string>
+    <string name="no_upstream_notification_title" msgid="611650570559011140">"Konexioa partekatzeko aukerak ez du Interneteko konexiorik"</string>
+    <string name="no_upstream_notification_message" msgid="6508394877641864863">"Ezin dira konektatu gailuak"</string>
+    <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"Desaktibatu konexioa partekatzeko aukera"</string>
+    <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"Wifi-gunea edo konexioa partekatzeko aukera aktibatuta dago"</string>
+    <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"Baliteke kostu gehigarriak ordaindu behar izatea ibiltaritzan"</string>
 </resources>
diff --git a/Tethering/res/values-mcc311-mnc480-fa/strings.xml b/Tethering/res/values-mcc311-mnc480-fa/strings.xml
index 6bdf387..cf8a0cc 100644
--- a/Tethering/res/values-mcc311-mnc480-fa/strings.xml
+++ b/Tethering/res/values-mcc311-mnc480-fa/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="5897815458155858594">"«اشتراک‌گذاری اینترنت» به اینترنت دسترسی ندارد"</string>
-    <string name="no_upstream_notification_message" msgid="9037716118606459874">"دستگاه‌ها متصل نمی‌شوند"</string>
-    <string name="no_upstream_notification_disable_button" msgid="5284024068281565456">"خاموش کردن «اشتراک‌گذاری اینترنت»"</string>
-    <string name="upstream_roaming_notification_title" msgid="186331286017243006">"«نقطه اتصال» یا «اشتراک‌گذاری اینترنت» روشن است"</string>
-    <string name="upstream_roaming_notification_message" msgid="7692641323940316538">"هنگام فراگردی ممکن است هزینه‌های اضافی کسر شود"</string>
+    <string name="no_upstream_notification_title" msgid="611650570559011140">"«اشتراک‌گذاری اینترنت» به اینترنت دسترسی ندارد"</string>
+    <string name="no_upstream_notification_message" msgid="6508394877641864863">"دستگاه‌ها متصل نمی‌شوند"</string>
+    <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"خاموش کردن «اشتراک‌گذاری اینترنت»"</string>
+    <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"«نقطه اتصال» یا «اشتراک‌گذاری اینترنت» روشن است"</string>
+    <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"ممکن است درحین فراگردی تغییرات دیگر اعمال شود"</string>
 </resources>
diff --git a/Tethering/res/values-mcc311-mnc480-fi/strings.xml b/Tethering/res/values-mcc311-mnc480-fi/strings.xml
index 57f16bb..6a3ab80 100644
--- a/Tethering/res/values-mcc311-mnc480-fi/strings.xml
+++ b/Tethering/res/values-mcc311-mnc480-fi/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="5897815458155858594">"Puhelinta ei voi käyttää modeemina, koska sillä ei ole internet-yhteyttä"</string>
-    <string name="no_upstream_notification_message" msgid="9037716118606459874">"Laitteet eivät voi muodostaa yhteyttä"</string>
-    <string name="no_upstream_notification_disable_button" msgid="5284024068281565456">"Laita puhelimen käyttäminen modeemina pois päältä"</string>
-    <string name="upstream_roaming_notification_title" msgid="186331286017243006">"Hotspot tai puhelimen käyttäminen modeemina on päällä"</string>
-    <string name="upstream_roaming_notification_message" msgid="7692641323940316538">"Roaming voi aiheuttaa lisämaksuja"</string>
+    <string name="no_upstream_notification_title" msgid="611650570559011140">"Ei jaettavaa internetyhteyttä"</string>
+    <string name="no_upstream_notification_message" msgid="6508394877641864863">"Laitteet eivät voi muodostaa yhteyttä"</string>
+    <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"Laita yhteyden jakaminen pois päältä"</string>
+    <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"Hotspot tai yhteyden jakaminen on päällä"</string>
+    <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"Roaming voi aiheuttaa lisämaksuja"</string>
 </resources>
diff --git a/Tethering/res/values-mcc311-mnc480-fr-rCA/strings.xml b/Tethering/res/values-mcc311-mnc480-fr-rCA/strings.xml
index bf3d634..ffb9bf6 100644
--- a/Tethering/res/values-mcc311-mnc480-fr-rCA/strings.xml
+++ b/Tethering/res/values-mcc311-mnc480-fr-rCA/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="5897815458155858594">"Le partage de connexion n\'est pas connecté à Internet"</string>
-    <string name="no_upstream_notification_message" msgid="9037716118606459874">"Impossible de connecter les appareils"</string>
-    <string name="no_upstream_notification_disable_button" msgid="5284024068281565456">"Désactiver le partage de connexion"</string>
-    <string name="upstream_roaming_notification_title" msgid="186331286017243006">"Le point d\'accès sans fil ou le partage de connexion est activé"</string>
-    <string name="upstream_roaming_notification_message" msgid="7692641323940316538">"En itinérance, des frais supplémentaires peuvent s\'appliquer"</string>
+    <string name="no_upstream_notification_title" msgid="611650570559011140">"Le partage de connexion n\'est pas connecté à Internet"</string>
+    <string name="no_upstream_notification_message" msgid="6508394877641864863">"Impossible de connecter les appareils"</string>
+    <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"Désactiver le partage de connexion"</string>
+    <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"Le point d\'accès ou le partage de connexion est activé"</string>
+    <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"En itinérance, des frais supplémentaires peuvent s\'appliquer"</string>
 </resources>
diff --git a/Tethering/res/values-mcc311-mnc480-fr/strings.xml b/Tethering/res/values-mcc311-mnc480-fr/strings.xml
index 6faa61e..768bce3 100644
--- a/Tethering/res/values-mcc311-mnc480-fr/strings.xml
+++ b/Tethering/res/values-mcc311-mnc480-fr/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="5897815458155858594">"Aucune connexion à Internet disponible pour le partage de connexion"</string>
-    <string name="no_upstream_notification_message" msgid="9037716118606459874">"Impossible de connecter les appareils"</string>
-    <string name="no_upstream_notification_disable_button" msgid="5284024068281565456">"Désactiver le partage de connexion"</string>
-    <string name="upstream_roaming_notification_title" msgid="186331286017243006">"Le point d\'accès ou le partage de connexion est activé"</string>
-    <string name="upstream_roaming_notification_message" msgid="7692641323940316538">"En itinérance, des frais supplémentaires peuvent s\'appliquer"</string>
+    <string name="no_upstream_notification_title" msgid="611650570559011140">"Aucune connexion à Internet n\'est disponible pour le partage de connexion"</string>
+    <string name="no_upstream_notification_message" msgid="6508394877641864863">"Impossible de connecter les appareils"</string>
+    <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"Désactiver le partage de connexion"</string>
+    <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"Le point d\'accès ou le partage de connexion est activé"</string>
+    <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"En itinérance, des frais supplémentaires peuvent s\'appliquer"</string>
 </resources>
diff --git a/Tethering/res/values-mcc311-mnc480-gl/strings.xml b/Tethering/res/values-mcc311-mnc480-gl/strings.xml
index 446d706..0c4195a 100644
--- a/Tethering/res/values-mcc311-mnc480-gl/strings.xml
+++ b/Tethering/res/values-mcc311-mnc480-gl/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="5897815458155858594">"A conexión compartida non ten acceso a Internet"</string>
-    <string name="no_upstream_notification_message" msgid="9037716118606459874">"Non se puideron conectar os dispositivos"</string>
-    <string name="no_upstream_notification_disable_button" msgid="5284024068281565456">"Desactivar conexión compartida"</string>
-    <string name="upstream_roaming_notification_title" msgid="186331286017243006">"Está activada a zona wifi ou a conexión compartida"</string>
-    <string name="upstream_roaming_notification_message" msgid="7692641323940316538">"Pódense aplicar cargos adicionais en itinerancia"</string>
+    <string name="no_upstream_notification_title" msgid="611650570559011140">"A conexión compartida non ten Internet"</string>
+    <string name="no_upstream_notification_message" msgid="6508394877641864863">"Non se puideron conectar os dispositivos"</string>
+    <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"Desactivar conexión compartida"</string>
+    <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"Está activada a zona wifi ou a conexión compartida"</string>
+    <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"Pódense aplicar cargos adicionais en itinerancia"</string>
 </resources>
diff --git a/Tethering/res/values-mcc311-mnc480-gu/strings.xml b/Tethering/res/values-mcc311-mnc480-gu/strings.xml
index 577874d..e9d33a7 100644
--- a/Tethering/res/values-mcc311-mnc480-gu/strings.xml
+++ b/Tethering/res/values-mcc311-mnc480-gu/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="5897815458155858594">"ઇન્ટરનેટ શેર કરવાની સુવિધામાં ઇન્ટરનેટ નથી"</string>
-    <string name="no_upstream_notification_message" msgid="9037716118606459874">"ડિવાઇસ કનેક્ટ કરી શકાતા નથી"</string>
-    <string name="no_upstream_notification_disable_button" msgid="5284024068281565456">"ઇન્ટરનેટ શેર કરવાની સુવિધા બંધ કરો"</string>
-    <string name="upstream_roaming_notification_title" msgid="186331286017243006">"હૉટસ્પૉટ અથવા ઇન્ટરનેટ શેર કરવાની સુવિધા ચાલુ છે"</string>
-    <string name="upstream_roaming_notification_message" msgid="7692641323940316538">"રોમિંગ દરમિયાન વધારાના શુલ્ક લાગુ થઈ શકે છે"</string>
+    <string name="no_upstream_notification_title" msgid="611650570559011140">"ઇન્ટરનેટ શેર કરવાની સુવિધામાં ઇન્ટરનેટ નથી"</string>
+    <string name="no_upstream_notification_message" msgid="6508394877641864863">"ડિવાઇસ કનેક્ટ કરી શકાતા નથી"</string>
+    <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"ઇન્ટરનેટ શેર કરવાની સુવિધા બંધ કરો"</string>
+    <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"હૉટસ્પૉટ અથવા ઇન્ટરનેટ શેર કરવાની સુવિધા ચાલુ છે"</string>
+    <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"રોમિંગમાં વધારાના શુલ્ક લાગી શકે છે"</string>
 </resources>
diff --git a/Tethering/res/values-mcc311-mnc480-hi/strings.xml b/Tethering/res/values-mcc311-mnc480-hi/strings.xml
index f2a4773..aa418ac 100644
--- a/Tethering/res/values-mcc311-mnc480-hi/strings.xml
+++ b/Tethering/res/values-mcc311-mnc480-hi/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="5897815458155858594">"टेदरिंग से इंटरनेट नहीं चल रहा है"</string>
-    <string name="no_upstream_notification_message" msgid="9037716118606459874">"डिवाइस कनेक्ट नहीं हो पा रहे"</string>
-    <string name="no_upstream_notification_disable_button" msgid="5284024068281565456">"टेदरिंग बंद करें"</string>
-    <string name="upstream_roaming_notification_title" msgid="186331286017243006">"हॉटस्पॉट या टेदरिंग चालू है"</string>
-    <string name="upstream_roaming_notification_message" msgid="7692641323940316538">"रोमिंग के दौरान अतिरिक्त शुल्क काटा जा सकता है"</string>
+    <string name="no_upstream_notification_title" msgid="611650570559011140">"टेदरिंग से इंटरनेट नहीं चल रहा"</string>
+    <string name="no_upstream_notification_message" msgid="6508394877641864863">"डिवाइस कनेक्ट नहीं हो पा रहे"</string>
+    <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"टेदरिंग बंद करें"</string>
+    <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"हॉटस्पॉट या टेदरिंग चालू है"</string>
+    <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"रोमिंग के दौरान अतिरिक्त शुल्क लग सकता है"</string>
 </resources>
diff --git a/Tethering/res/values-mcc311-mnc480-hr/strings.xml b/Tethering/res/values-mcc311-mnc480-hr/strings.xml
index a08f822..51c524a 100644
--- a/Tethering/res/values-mcc311-mnc480-hr/strings.xml
+++ b/Tethering/res/values-mcc311-mnc480-hr/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="5897815458155858594">"Modemsko povezivanje nema internet"</string>
-    <string name="no_upstream_notification_message" msgid="9037716118606459874">"Uređaji se ne mogu povezati"</string>
-    <string name="no_upstream_notification_disable_button" msgid="5284024068281565456">"Isključi modemsko povezivanje"</string>
-    <string name="upstream_roaming_notification_title" msgid="186331286017243006">"Uključena je žarišna točka ili modemsko povezivanje"</string>
-    <string name="upstream_roaming_notification_message" msgid="7692641323940316538">"U roamingu su mogući dodatni troškovi"</string>
+    <string name="no_upstream_notification_title" msgid="611650570559011140">"Modemsko povezivanje nema internet"</string>
+    <string name="no_upstream_notification_message" msgid="6508394877641864863">"Uređaji se ne mogu povezati"</string>
+    <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"Isključivanje modemskog povezivanja"</string>
+    <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"Uključena je žarišna točka ili modemsko povezivanje"</string>
+    <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"U roamingu su mogući dodatni troškovi"</string>
 </resources>
diff --git a/Tethering/res/values-mcc311-mnc480-hu/strings.xml b/Tethering/res/values-mcc311-mnc480-hu/strings.xml
index 61a399a..164e45e 100644
--- a/Tethering/res/values-mcc311-mnc480-hu/strings.xml
+++ b/Tethering/res/values-mcc311-mnc480-hu/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="5897815458155858594">"Nincs internetkapcsolat az internet megosztásához"</string>
-    <string name="no_upstream_notification_message" msgid="9037716118606459874">"Az eszközök nem tudnak csatlakozni"</string>
-    <string name="no_upstream_notification_disable_button" msgid="5284024068281565456">"Internetmegosztás kikapcsolása"</string>
-    <string name="upstream_roaming_notification_title" msgid="186331286017243006">"A hotspot vagy az internetmegosztás be van kapcsolva"</string>
-    <string name="upstream_roaming_notification_message" msgid="7692641323940316538">"Roaming során további díjak léphetnek fel"</string>
+    <string name="no_upstream_notification_title" msgid="611650570559011140">"Nincs internetkapcsolat az internet megosztásához"</string>
+    <string name="no_upstream_notification_message" msgid="6508394877641864863">"Az eszközök nem tudnak csatlakozni"</string>
+    <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"Internetmegosztás kikapcsolása"</string>
+    <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"A hotspot vagy az internetmegosztás be van kapcsolva"</string>
+    <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"Roaming során további díjak léphetnek fel"</string>
 </resources>
diff --git a/Tethering/res/values-mcc311-mnc480-hy/strings.xml b/Tethering/res/values-mcc311-mnc480-hy/strings.xml
index f4d63c9..e76c0a4 100644
--- a/Tethering/res/values-mcc311-mnc480-hy/strings.xml
+++ b/Tethering/res/values-mcc311-mnc480-hy/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="5897815458155858594">"Մոդեմի ռեժիմի ինտերնետ կապը բացակայում է"</string>
-    <string name="no_upstream_notification_message" msgid="9037716118606459874">"Չհաջողվեց միացնել սարքերը"</string>
-    <string name="no_upstream_notification_disable_button" msgid="5284024068281565456">"Անջատել մոդեմի ռեժիմը"</string>
-    <string name="upstream_roaming_notification_title" msgid="186331286017243006">"Թեժ կետը կամ մոդեմի ռեժիմը միացված է"</string>
-    <string name="upstream_roaming_notification_message" msgid="7692641323940316538">"Ռոումինգում կարող են լրացուցիչ վճարներ գանձվել"</string>
+    <string name="no_upstream_notification_title" msgid="611650570559011140">"Մոդեմի ռեժիմի կապը բացակայում է"</string>
+    <string name="no_upstream_notification_message" msgid="6508394877641864863">"Չհաջողվեց միացնել սարքը"</string>
+    <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"Անջատել մոդեմի ռեժիմը"</string>
+    <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"Թեժ կետը կամ մոդեմի ռեժիմը միացված է"</string>
+    <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"Ռոումինգում կարող են լրացուցիչ վճարներ գանձվել"</string>
 </resources>
diff --git a/Tethering/res/values-mcc311-mnc480-in/strings.xml b/Tethering/res/values-mcc311-mnc480-in/strings.xml
index 98c6d71..2b817f8 100644
--- a/Tethering/res/values-mcc311-mnc480-in/strings.xml
+++ b/Tethering/res/values-mcc311-mnc480-in/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="5897815458155858594">"Tidak ada koneksi internet di tethering"</string>
-    <string name="no_upstream_notification_message" msgid="9037716118606459874">"Perangkat tidak dapat terhubung"</string>
-    <string name="no_upstream_notification_disable_button" msgid="5284024068281565456">"Nonaktifkan tethering"</string>
-    <string name="upstream_roaming_notification_title" msgid="186331286017243006">"Hotspot atau tethering aktif"</string>
-    <string name="upstream_roaming_notification_message" msgid="7692641323940316538">"Biaya tambahan mungkin berlaku saat roaming"</string>
+    <string name="no_upstream_notification_title" msgid="611650570559011140">"Tidak ada koneksi internet di tethering"</string>
+    <string name="no_upstream_notification_message" msgid="6508394877641864863">"Perangkat tidak dapat terhubung"</string>
+    <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"Nonaktifkan tethering"</string>
+    <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"Hotspot atau tethering aktif"</string>
+    <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"Biaya tambahan mungkin berlaku saat roaming"</string>
 </resources>
diff --git a/Tethering/res/values-mcc311-mnc480-is/strings.xml b/Tethering/res/values-mcc311-mnc480-is/strings.xml
index ade1b01..a338d9c 100644
--- a/Tethering/res/values-mcc311-mnc480-is/strings.xml
+++ b/Tethering/res/values-mcc311-mnc480-is/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="5897815458155858594">"Tjóðrun er ekki með internettengingu"</string>
-    <string name="no_upstream_notification_message" msgid="9037716118606459874">"Tæki geta ekki tengst"</string>
-    <string name="no_upstream_notification_disable_button" msgid="5284024068281565456">"Slökkva á tjóðrun"</string>
-    <string name="upstream_roaming_notification_title" msgid="186331286017243006">"Kveikt er á heitum reit eða tjóðrun"</string>
-    <string name="upstream_roaming_notification_message" msgid="7692641323940316538">"Viðbótargjöld kunna að eiga við í reiki"</string>
+    <string name="no_upstream_notification_title" msgid="611650570559011140">"Tjóðrun er ekki með internettengingu"</string>
+    <string name="no_upstream_notification_message" msgid="6508394877641864863">"Tæki geta ekki tengst"</string>
+    <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"Slökkva á tjóðrun"</string>
+    <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"Kveikt er á heitum reit eða tjóðrun"</string>
+    <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"Viðbótargjöld kunna að eiga við í reiki"</string>
 </resources>
diff --git a/Tethering/res/values-mcc311-mnc480-it/strings.xml b/Tethering/res/values-mcc311-mnc480-it/strings.xml
index 07e1526..77769c2 100644
--- a/Tethering/res/values-mcc311-mnc480-it/strings.xml
+++ b/Tethering/res/values-mcc311-mnc480-it/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="5897815458155858594">"Nessuna connessione a internet per il tethering"</string>
-    <string name="no_upstream_notification_message" msgid="9037716118606459874">"Impossibile connettere i dispositivi"</string>
-    <string name="no_upstream_notification_disable_button" msgid="5284024068281565456">"Disattiva il tethering"</string>
-    <string name="upstream_roaming_notification_title" msgid="186331286017243006">"Hotspot o tethering attivo"</string>
-    <string name="upstream_roaming_notification_message" msgid="7692641323940316538">"Potrebbero essere applicati costi aggiuntivi durante il roaming"</string>
+    <string name="no_upstream_notification_title" msgid="611650570559011140">"Nessuna connessione a Internet per il tethering"</string>
+    <string name="no_upstream_notification_message" msgid="6508394877641864863">"Impossibile connettere i dispositivi"</string>
+    <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"Disattiva il tethering"</string>
+    <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"Hotspot o tethering attivi"</string>
+    <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"Potrebbero essere applicati costi aggiuntivi durante il roaming"</string>
 </resources>
diff --git a/Tethering/res/values-mcc311-mnc480-iw/strings.xml b/Tethering/res/values-mcc311-mnc480-iw/strings.xml
index ebebae8..5267b51 100644
--- a/Tethering/res/values-mcc311-mnc480-iw/strings.xml
+++ b/Tethering/res/values-mcc311-mnc480-iw/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="5897815458155858594">"אי אפשר להפעיל את תכונת שיתוף האינטרנט בין מכשירים כי אין חיבור לאינטרנט"</string>
-    <string name="no_upstream_notification_message" msgid="9037716118606459874">"למכשירים אין אפשרות להתחבר"</string>
-    <string name="no_upstream_notification_disable_button" msgid="5284024068281565456">"השבתה של שיתוף האינטרנט בין מכשירים"</string>
-    <string name="upstream_roaming_notification_title" msgid="186331286017243006">"תכונת הנקודה לשיתוף אינטרנט או תכונת שיתוף האינטרנט בין מכשירים פועלת"</string>
-    <string name="upstream_roaming_notification_message" msgid="7692641323940316538">"ייתכנו חיובים נוספים במהלך נדידה"</string>
+    <string name="no_upstream_notification_title" msgid="611650570559011140">"אי אפשר להפעיל את תכונת שיתוף האינטרנט בין מכשירים כי אין חיבור לאינטרנט"</string>
+    <string name="no_upstream_notification_message" msgid="6508394877641864863">"למכשירים אין אפשרות להתחבר"</string>
+    <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"השבתה של שיתוף האינטרנט בין מכשירים"</string>
+    <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"תכונת הנקודה לשיתוף אינטרנט או תכונת שיתוף האינטרנט בין מכשירים פועלת"</string>
+    <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"ייתכנו חיובים נוספים בעת נדידה"</string>
 </resources>
diff --git a/Tethering/res/values-mcc311-mnc480-ja/strings.xml b/Tethering/res/values-mcc311-mnc480-ja/strings.xml
index 334d362..66a9a6d 100644
--- a/Tethering/res/values-mcc311-mnc480-ja/strings.xml
+++ b/Tethering/res/values-mcc311-mnc480-ja/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="5897815458155858594">"テザリングがインターネットに接続されていません"</string>
-    <string name="no_upstream_notification_message" msgid="9037716118606459874">"デバイスを接続できません"</string>
-    <string name="no_upstream_notification_disable_button" msgid="5284024068281565456">"テザリングを OFF にする"</string>
-    <string name="upstream_roaming_notification_title" msgid="186331286017243006">"アクセス ポイントまたはテザリングが ON です"</string>
-    <string name="upstream_roaming_notification_message" msgid="7692641323940316538">"ローミング時に追加料金が発生することがあります"</string>
+    <string name="no_upstream_notification_title" msgid="611650570559011140">"テザリングがインターネットに接続されていません"</string>
+    <string name="no_upstream_notification_message" msgid="6508394877641864863">"デバイスを接続できません"</string>
+    <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"テザリングを OFF にする"</string>
+    <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"アクセス ポイントまたはテザリングが ON です"</string>
+    <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"ローミング時に追加料金が発生することがあります"</string>
 </resources>
diff --git a/Tethering/res/values-mcc311-mnc480-ka/strings.xml b/Tethering/res/values-mcc311-mnc480-ka/strings.xml
index d369d20..d8ad880 100644
--- a/Tethering/res/values-mcc311-mnc480-ka/strings.xml
+++ b/Tethering/res/values-mcc311-mnc480-ka/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="5897815458155858594">"ტეტერინგს არ აქვს ინტერნეტზე წვდომა"</string>
-    <string name="no_upstream_notification_message" msgid="9037716118606459874">"მოწყობილობები ვერ ახერხებენ დაკავშირებას"</string>
-    <string name="no_upstream_notification_disable_button" msgid="5284024068281565456">"ტეტერინგის გამორთვა"</string>
-    <string name="upstream_roaming_notification_title" msgid="186331286017243006">"ჩართულია უსადენო ქსელი ან ტეტერინგი"</string>
-    <string name="upstream_roaming_notification_message" msgid="7692641323940316538">"როუმინგის გამოყენებისას შეიძლება ჩამოგეჭრათ დამატებითი საფასური"</string>
+    <string name="no_upstream_notification_title" msgid="611650570559011140">"ტეტერინგს არ აქვს ინტერნეტზე წვდომა"</string>
+    <string name="no_upstream_notification_message" msgid="6508394877641864863">"მოწყობილობები ვერ ახერხებენ დაკავშირებას"</string>
+    <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"ტეტერინგის გამორთვა"</string>
+    <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"ჩართულია უსადენო ქსელი ან ტეტერინგი"</string>
+    <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"როუმინგის გამოყენებისას შეიძლება ჩამოგეჭრათ დამატებითი საფასური"</string>
 </resources>
diff --git a/Tethering/res/values-mcc311-mnc480-kk/strings.xml b/Tethering/res/values-mcc311-mnc480-kk/strings.xml
index c16c106..1ddd6b4 100644
--- a/Tethering/res/values-mcc311-mnc480-kk/strings.xml
+++ b/Tethering/res/values-mcc311-mnc480-kk/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="5897815458155858594">"Тетеринг кезінде интернет байланысы жоқ"</string>
-    <string name="no_upstream_notification_message" msgid="9037716118606459874">"Құрылғыларды байланыстыру мүмкін емес"</string>
-    <string name="no_upstream_notification_disable_button" msgid="5284024068281565456">"Тетерингіні өшіру"</string>
-    <string name="upstream_roaming_notification_title" msgid="186331286017243006">"Хотспот немесе тетеринг қосулы"</string>
-    <string name="upstream_roaming_notification_message" msgid="7692641323940316538">"Роуминг кезінде қосымша ақы алынуы мүмкін."</string>
+    <string name="no_upstream_notification_title" msgid="611650570559011140">"Тетеринг режимі интернет байланысынсыз пайдаланылуда"</string>
+    <string name="no_upstream_notification_message" msgid="6508394877641864863">"Құрылғыларды байланыстыру мүмкін емес"</string>
+    <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"Тетерингіні өшіру"</string>
+    <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"Хотспот немесе тетеринг қосулы"</string>
+    <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"Роуминг кезінде қосымша ақы алынуы мүмкін."</string>
 </resources>
diff --git a/Tethering/res/values-mcc311-mnc480-km/strings.xml b/Tethering/res/values-mcc311-mnc480-km/strings.xml
index 8084b87..cf5a137 100644
--- a/Tethering/res/values-mcc311-mnc480-km/strings.xml
+++ b/Tethering/res/values-mcc311-mnc480-km/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="5897815458155858594">"ការភ្ជាប់​មិនមានអ៊ីនធឺណិត​ទេ"</string>
-    <string name="no_upstream_notification_message" msgid="9037716118606459874">"មិនអាច​ភ្ជាប់ឧបករណ៍​បានទេ"</string>
-    <string name="no_upstream_notification_disable_button" msgid="5284024068281565456">"បិទការភ្ជាប់"</string>
-    <string name="upstream_roaming_notification_title" msgid="186331286017243006">"ហតស្ប៉ត ឬការភ្ជាប់​ត្រូវបានបើក"</string>
-    <string name="upstream_roaming_notification_message" msgid="7692641323940316538">"អាចមាន​ការគិតថ្លៃ​បន្ថែម នៅពេល​រ៉ូមីង"</string>
+    <string name="no_upstream_notification_title" msgid="611650570559011140">"ការភ្ជាប់​មិនមានអ៊ីនធឺណិត​ទេ"</string>
+    <string name="no_upstream_notification_message" msgid="6508394877641864863">"មិនអាច​ភ្ជាប់ឧបករណ៍​បានទេ"</string>
+    <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"បិទការភ្ជាប់"</string>
+    <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"ហតស្ប៉ត ឬការភ្ជាប់​ត្រូវបានបើក"</string>
+    <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"អាចមាន​ការគិតថ្លៃ​បន្ថែម នៅពេល​រ៉ូមីង"</string>
 </resources>
diff --git a/Tethering/res/values-mcc311-mnc480-kn/strings.xml b/Tethering/res/values-mcc311-mnc480-kn/strings.xml
index 528cdbf..68ae68b 100644
--- a/Tethering/res/values-mcc311-mnc480-kn/strings.xml
+++ b/Tethering/res/values-mcc311-mnc480-kn/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="5897815458155858594">"ಟೆಥರಿಂಗ್‌ ಯಾವುದೇ ಇಂಟರ್ನೆಟ್ ಕನೆಕ್ಷನ್ ಹೊಂದಿಲ್ಲ"</string>
-    <string name="no_upstream_notification_message" msgid="9037716118606459874">"ಸಾಧನಗಳನ್ನು ಕನೆಕ್ಟ್ ಮಾಡಲು ಸಾಧ್ಯವಿಲ್ಲ"</string>
-    <string name="no_upstream_notification_disable_button" msgid="5284024068281565456">"ಟೆಥರಿಂಗ್‌ ಆಫ್ ಮಾಡಿ"</string>
-    <string name="upstream_roaming_notification_title" msgid="186331286017243006">"ಹಾಟ್‌ಸ್ಪಾಟ್ ಅಥವಾ ಟೆಥರಿಂಗ್‌ ಆನ್ ಆಗಿದೆ"</string>
-    <string name="upstream_roaming_notification_message" msgid="7692641323940316538">"ರೋಮಿಂಗ್‌ನಲ್ಲಿರುವಾಗ ಹೆಚ್ಚುವರಿ ಶುಲ್ಕಗಳು ಅನ್ವಯವಾಗಬಹುದು"</string>
+    <string name="no_upstream_notification_title" msgid="611650570559011140">"ಟೆಥರಿಂಗ್‌ ಯಾವುದೇ ಇಂಟರ್ನೆಟ್ ಕನೆಕ್ಷನ್ ಹೊಂದಿಲ್ಲ"</string>
+    <string name="no_upstream_notification_message" msgid="6508394877641864863">"ಸಾಧನಗಳನ್ನು ಕನೆಕ್ಟ್ ಮಾಡಲು ಸಾಧ್ಯವಿಲ್ಲ"</string>
+    <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"ಟೆಥರಿಂಗ್‌ ಆಫ್ ಮಾಡಿ"</string>
+    <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"ಹಾಟ್‌ಸ್ಪಾಟ್ ಅಥವಾ ಟೆಥರಿಂಗ್‌ ಆನ್ ಆಗಿದೆ"</string>
+    <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"ರೋಮಿಂಗ್‌ನಲ್ಲಿರುವಾಗ ಹೆಚ್ಚುವರಿ ಶುಲ್ಕಗಳು ಅನ್ವಯವಾಗಬಹುದು"</string>
 </resources>
diff --git a/Tethering/res/values-mcc311-mnc480-ko/strings.xml b/Tethering/res/values-mcc311-mnc480-ko/strings.xml
index f195c82..17185ba 100644
--- a/Tethering/res/values-mcc311-mnc480-ko/strings.xml
+++ b/Tethering/res/values-mcc311-mnc480-ko/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="5897815458155858594">"테더링으로 인터넷을 사용할 수 없음"</string>
-    <string name="no_upstream_notification_message" msgid="9037716118606459874">"기기에서 연결할 수 없음"</string>
-    <string name="no_upstream_notification_disable_button" msgid="5284024068281565456">"테더링 사용 중지"</string>
-    <string name="upstream_roaming_notification_title" msgid="186331286017243006">"핫스팟 또는 테더링이 켜짐"</string>
-    <string name="upstream_roaming_notification_message" msgid="7692641323940316538">"로밍 중에는 추가 요금이 부과될 수 있습니다."</string>
+    <string name="no_upstream_notification_title" msgid="611650570559011140">"테더링으로 인터넷을 사용할 수 없음"</string>
+    <string name="no_upstream_notification_message" msgid="6508394877641864863">"기기에서 연결할 수 없음"</string>
+    <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"테더링 사용 중지"</string>
+    <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"핫스팟 또는 테더링 켜짐"</string>
+    <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"로밍 중에는 추가 요금이 발생할 수 있습니다."</string>
 </resources>
diff --git a/Tethering/res/values-mcc311-mnc480-ky/strings.xml b/Tethering/res/values-mcc311-mnc480-ky/strings.xml
index f8ca531..6a9fb98 100644
--- a/Tethering/res/values-mcc311-mnc480-ky/strings.xml
+++ b/Tethering/res/values-mcc311-mnc480-ky/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="5897815458155858594">"Модем режими Интернети жок колдонулууда"</string>
-    <string name="no_upstream_notification_message" msgid="9037716118606459874">"Түзмөктөр туташпай жатат"</string>
-    <string name="no_upstream_notification_disable_button" msgid="5284024068281565456">"Модем режимин өчүрүү"</string>
-    <string name="upstream_roaming_notification_title" msgid="186331286017243006">"Байланыш түйүнү же модем режими күйүк"</string>
-    <string name="upstream_roaming_notification_message" msgid="7692641323940316538">"Роумингде кошумча акы алынышы мүмкүн"</string>
+    <string name="no_upstream_notification_title" msgid="611650570559011140">"Модем режими Интернети жок колдонулууда"</string>
+    <string name="no_upstream_notification_message" msgid="6508394877641864863">"Түзмөктөр туташпай жатат"</string>
+    <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"Модем режимин өчүрүү"</string>
+    <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"Байланыш түйүнү же модем режими күйүк"</string>
+    <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"Роумингде кошумча акы алынышы мүмкүн"</string>
 </resources>
diff --git a/Tethering/res/values-mcc311-mnc480-lo/strings.xml b/Tethering/res/values-mcc311-mnc480-lo/strings.xml
index 6258bc0..bcc4b57 100644
--- a/Tethering/res/values-mcc311-mnc480-lo/strings.xml
+++ b/Tethering/res/values-mcc311-mnc480-lo/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="5897815458155858594">"ການປ່ອຍສັນຍານບໍ່ມີອິນເຕີເນັດ"</string>
-    <string name="no_upstream_notification_message" msgid="9037716118606459874">"ອຸປະກອນບໍ່ສາມາດເຊື່ອມຕໍ່ໄດ້"</string>
-    <string name="no_upstream_notification_disable_button" msgid="5284024068281565456">"ປິດການປ່ອຍສັນຍານ"</string>
-    <string name="upstream_roaming_notification_title" msgid="186331286017243006">"ຮັອດສະປອດ ຫຼື ການປ່ອຍສັນຍານເປີດຢູ່"</string>
-    <string name="upstream_roaming_notification_message" msgid="7692641323940316538">"ອາດມີຄ່າບໍລິການເພີ່ມເຕີມໃນລະຫວ່າງການໂຣມມິງ"</string>
+    <string name="no_upstream_notification_title" msgid="611650570559011140">"ການປ່ອຍສັນຍານບໍ່ມີອິນເຕີເນັດ"</string>
+    <string name="no_upstream_notification_message" msgid="6508394877641864863">"ອຸປະກອນບໍ່ສາມາດເຊື່ອມຕໍ່ໄດ້"</string>
+    <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"ປິດການປ່ອຍສັນຍານ"</string>
+    <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"ເປີດໃຊ້ຮັອດສະປອດ ຫຼື ການປ່ອຍສັນຍານຢູ່"</string>
+    <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"ອາດມີຄ່າໃຊ້ຈ່າຍເພີ່ມເຕີມໃນລະຫວ່າງການໂຣມມິງ"</string>
 </resources>
diff --git a/Tethering/res/values-mcc311-mnc480-lt/strings.xml b/Tethering/res/values-mcc311-mnc480-lt/strings.xml
index 267c7f6..011c2c1 100644
--- a/Tethering/res/values-mcc311-mnc480-lt/strings.xml
+++ b/Tethering/res/values-mcc311-mnc480-lt/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="5897815458155858594">"Nėra įrenginio kaip modemo naudojimo interneto ryšio"</string>
-    <string name="no_upstream_notification_message" msgid="9037716118606459874">"Nepavyko susieti įrenginių"</string>
-    <string name="no_upstream_notification_disable_button" msgid="5284024068281565456">"Išjungti įrenginio kaip modemo naudojimą"</string>
-    <string name="upstream_roaming_notification_title" msgid="186331286017243006">"Įjungtas viešosios interneto prieigos taškas arba įrenginio kaip modemo naudojimas"</string>
-    <string name="upstream_roaming_notification_message" msgid="7692641323940316538">"Veikiant tarptinkliniam ryšiui gali būti taikomi papildomi mokesčiai"</string>
+    <string name="no_upstream_notification_title" msgid="611650570559011140">"Nėra įrenginio kaip modemo naudojimo interneto ryšio"</string>
+    <string name="no_upstream_notification_message" msgid="6508394877641864863">"Nepavyko susieti įrenginių"</string>
+    <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"Išjungti įrenginio kaip modemo naudojimą"</string>
+    <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"Įjungtas viešosios interneto prieigos taškas arba įrenginio kaip modemo naudojimas"</string>
+    <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"Veikiant tarptinkliniam ryšiui gali būti taikomi papildomi mokesčiai"</string>
 </resources>
diff --git a/Tethering/res/values-mcc311-mnc480-lv/strings.xml b/Tethering/res/values-mcc311-mnc480-lv/strings.xml
index ca54195..5cb2f3b 100644
--- a/Tethering/res/values-mcc311-mnc480-lv/strings.xml
+++ b/Tethering/res/values-mcc311-mnc480-lv/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="5897815458155858594">"Piesaistei nav interneta savienojuma"</string>
-    <string name="no_upstream_notification_message" msgid="9037716118606459874">"Nevar savienot ierīces"</string>
-    <string name="no_upstream_notification_disable_button" msgid="5284024068281565456">"Izslēgt piesaisti"</string>
-    <string name="upstream_roaming_notification_title" msgid="186331286017243006">"Ir ieslēgts tīklājs vai piesaiste"</string>
-    <string name="upstream_roaming_notification_message" msgid="7692641323940316538">"Viesabonēšanas laikā var tikt piemērota papildu maksa."</string>
+    <string name="no_upstream_notification_title" msgid="611650570559011140">"Piesaistei nav interneta savienojuma"</string>
+    <string name="no_upstream_notification_message" msgid="6508394877641864863">"Nevar savienot ierīces"</string>
+    <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"Izslēgt piesaisti"</string>
+    <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"Ir ieslēgts tīklājs vai piesaiste"</string>
+    <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"Viesabonēšanas laikā var tikt piemērota papildu samaksa"</string>
 </resources>
diff --git a/Tethering/res/values-mcc311-mnc480-mk/strings.xml b/Tethering/res/values-mcc311-mnc480-mk/strings.xml
index 8c2b8aa..4cbfd88 100644
--- a/Tethering/res/values-mcc311-mnc480-mk/strings.xml
+++ b/Tethering/res/values-mcc311-mnc480-mk/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="5897815458155858594">"Нема интернет преку мобилен"</string>
-    <string name="no_upstream_notification_message" msgid="9037716118606459874">"Уредите не може да се поврзат"</string>
-    <string name="no_upstream_notification_disable_button" msgid="5284024068281565456">"Исклучи интернет преку мобилен"</string>
-    <string name="upstream_roaming_notification_title" msgid="186331286017243006">"Вклучено: точка на пристап или интернет преку мобилен"</string>
-    <string name="upstream_roaming_notification_message" msgid="7692641323940316538">"При роаминг може да се наплатат дополнителни трошоци"</string>
+    <string name="no_upstream_notification_title" msgid="611650570559011140">"Нема интернет преку мобилен"</string>
+    <string name="no_upstream_notification_message" msgid="6508394877641864863">"Уредите не може да се поврзат"</string>
+    <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"Исклучи интернет преку мобилен"</string>
+    <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"Точката на пристап или интернетот преку мобилен е вклучен"</string>
+    <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"При роаминг може да се наплатат дополнителни трошоци"</string>
 </resources>
diff --git a/Tethering/res/values-mcc311-mnc480-ml/strings.xml b/Tethering/res/values-mcc311-mnc480-ml/strings.xml
index 9a722c5..9cf4eaf 100644
--- a/Tethering/res/values-mcc311-mnc480-ml/strings.xml
+++ b/Tethering/res/values-mcc311-mnc480-ml/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="5897815458155858594">"ടെതറിംഗിന് ഇന്റർനെറ്റ് ഇല്ല"</string>
-    <string name="no_upstream_notification_message" msgid="9037716118606459874">"ഉപകരണങ്ങൾ കണക്റ്റ് ചെയ്യാനാവില്ല"</string>
-    <string name="no_upstream_notification_disable_button" msgid="5284024068281565456">"ടെതറിംഗ് ഓഫാക്കുക"</string>
-    <string name="upstream_roaming_notification_title" msgid="186331286017243006">"ഹോട്ട്‌സ്‌പോട്ട് അല്ലെങ്കിൽ ടെതറിംഗ് ഓണാണ്"</string>
-    <string name="upstream_roaming_notification_message" msgid="7692641323940316538">"റോമിംഗ് ചെയ്യുമ്പോൾ അധിക നിരക്കുകൾ ബാധകമായേക്കാം"</string>
+    <string name="no_upstream_notification_title" msgid="611650570559011140">"ടെതറിംഗിന് ഇന്റർനെറ്റ് ഇല്ല"</string>
+    <string name="no_upstream_notification_message" msgid="6508394877641864863">"ഉപകരണങ്ങൾ കണക്റ്റ് ചെയ്യാനാവില്ല"</string>
+    <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"ടെതറിംഗ് ഓഫാക്കുക"</string>
+    <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"ഹോട്ട്‌സ്‌പോട്ട് അല്ലെങ്കിൽ ടെതറിംഗ് ഓണാണ്"</string>
+    <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"റോമിംഗ് ചെയ്യുമ്പോൾ അധിക നിരക്കുകൾ ബാധകമായേക്കാം"</string>
 </resources>
diff --git a/Tethering/res/values-mcc311-mnc480-mn/strings.xml b/Tethering/res/values-mcc311-mnc480-mn/strings.xml
index f6fcf01..47c82c1 100644
--- a/Tethering/res/values-mcc311-mnc480-mn/strings.xml
+++ b/Tethering/res/values-mcc311-mnc480-mn/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="5897815458155858594">"Модем болгоход ямар ч интернэт байхгүй байна"</string>
-    <string name="no_upstream_notification_message" msgid="9037716118606459874">"Төхөөрөмжүүд холбогдох боломжгүй байна"</string>
-    <string name="no_upstream_notification_disable_button" msgid="5284024068281565456">"Модем болгохыг унтраах"</string>
-    <string name="upstream_roaming_notification_title" msgid="186331286017243006">"Сүлжээний цэг эсвэл модем болгох асаалттай байна"</string>
-    <string name="upstream_roaming_notification_message" msgid="7692641323940316538">"Роумингийн үеэр нэмэлт төлбөр тооцогдож магадгүй"</string>
+    <string name="no_upstream_notification_title" msgid="611650570559011140">"Модемд интернэт алга байна"</string>
+    <string name="no_upstream_notification_message" msgid="6508394877641864863">"Төхөөрөмжүүд холбогдох боломжгүй байна"</string>
+    <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"Модем болгохыг унтраах"</string>
+    <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"Сүлжээний цэг эсвэл модем болгох асаалттай байна"</string>
+    <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"Роумингийн үеэр нэмэлт төлбөр нэхэмжилж болзошгүй"</string>
 </resources>
diff --git a/Tethering/res/values-mcc311-mnc480-mr/strings.xml b/Tethering/res/values-mcc311-mnc480-mr/strings.xml
index 2563e15..ad9e809 100644
--- a/Tethering/res/values-mcc311-mnc480-mr/strings.xml
+++ b/Tethering/res/values-mcc311-mnc480-mr/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="5897815458155858594">"टेदरिंगसाठी इंटरनेट नाही"</string>
-    <string name="no_upstream_notification_message" msgid="9037716118606459874">"डिव्हाइस कनेक्ट होऊ शकत नाहीत"</string>
-    <string name="no_upstream_notification_disable_button" msgid="5284024068281565456">"टेदरिंग बंद करा"</string>
-    <string name="upstream_roaming_notification_title" msgid="186331286017243006">"हॉटस्पॉट किंवा टेदरिंग सुरू आहे"</string>
-    <string name="upstream_roaming_notification_message" msgid="7692641323940316538">"रोमिंगदरम्यान अतिरिक्त शुल्के लागू होऊ शकतात"</string>
+    <string name="no_upstream_notification_title" msgid="611650570559011140">"टेदरिंगला इंटरनेट नाही"</string>
+    <string name="no_upstream_notification_message" msgid="6508394877641864863">"डिव्हाइस कनेक्ट होऊ शकत नाहीत"</string>
+    <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"टेदरिंग बंद करा"</string>
+    <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"हॉटस्पॉट किंवा टेदरिंग सुरू आहे"</string>
+    <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"रोमिंगदरम्यान अतिरिक्त शुल्क लागू होऊ शकतात"</string>
 </resources>
diff --git a/Tethering/res/values-mcc311-mnc480-ms/strings.xml b/Tethering/res/values-mcc311-mnc480-ms/strings.xml
index b2ccbbb..e708cb8 100644
--- a/Tethering/res/values-mcc311-mnc480-ms/strings.xml
+++ b/Tethering/res/values-mcc311-mnc480-ms/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="5897815458155858594">"Penambatan tiada Internet"</string>
-    <string name="no_upstream_notification_message" msgid="9037716118606459874">"Peranti tidak dapat disambungkan"</string>
-    <string name="no_upstream_notification_disable_button" msgid="5284024068281565456">"Matikan penambatan"</string>
-    <string name="upstream_roaming_notification_title" msgid="186331286017243006">"Tempat liputan atau penambatan dihidupkan"</string>
-    <string name="upstream_roaming_notification_message" msgid="7692641323940316538">"Caj tambahan boleh dikenakan semasa perayauan"</string>
+    <string name="no_upstream_notification_title" msgid="611650570559011140">"Penambatan tiada Internet"</string>
+    <string name="no_upstream_notification_message" msgid="6508394877641864863">"Peranti tidak dapat disambungkan"</string>
+    <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"Matikan penambatan"</string>
+    <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"Tempat liputan atau penambatan dihidupkan"</string>
+    <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"Caj tambahan mungkin digunakan semasa perayauan"</string>
 </resources>
diff --git a/Tethering/res/values-mcc311-mnc480-my/strings.xml b/Tethering/res/values-mcc311-mnc480-my/strings.xml
index 2281b7b..ba54622 100644
--- a/Tethering/res/values-mcc311-mnc480-my/strings.xml
+++ b/Tethering/res/values-mcc311-mnc480-my/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="5897815458155858594">"မိုဘိုင်းသုံး၍ ချိတ်ဆက်ခြင်းတွင် အင်တာနက် မရှိပါ"</string>
-    <string name="no_upstream_notification_message" msgid="9037716118606459874">"စက်ပစ္စည်းများကို ချိတ်ဆက်၍မရပါ"</string>
-    <string name="no_upstream_notification_disable_button" msgid="5284024068281565456">"မိုဘိုင်းသုံး၍ ချိတ်ဆက်ခြင်း ပိတ်ရန်"</string>
-    <string name="upstream_roaming_notification_title" msgid="186331286017243006">"ဟော့စပေါ့ (သို့) မိုဘိုင်းသုံး၍ ချိတ်ဆက်ခြင်း ဖွင့်ထားသည်"</string>
-    <string name="upstream_roaming_notification_message" msgid="7692641323940316538">"ပြင်ပကွန်ရက်သုံးနေစဉ် နောက်ထပ်ကျသင့်ငွေ ပေးရနိုင်သည်"</string>
+    <string name="no_upstream_notification_title" msgid="611650570559011140">"မိုဘိုင်းဖုန်းသုံး ချိတ်ဆက်မျှဝေခြင်းတွင် အင်တာနက် မရှိပါ"</string>
+    <string name="no_upstream_notification_message" msgid="6508394877641864863">"စက်များ ချိတ်ဆက်၍ မရပါ"</string>
+    <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"မိုဘိုင်းဖုန်းသုံး ချိတ်ဆက်မျှဝေခြင်း ပိတ်ရန်"</string>
+    <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"ဟော့စပေါ့ (သို့) မိုဘိုင်းဖုန်းသုံး ချိတ်ဆက်မျှဝေခြင်း ဖွင့်ထားသည်"</string>
+    <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"ပြင်ပကွန်ရက်နှင့် ချိတ်ဆက်သည့်အခါ နောက်ထပ်ကျသင့်မှုများ ရှိနိုင်သည်"</string>
 </resources>
diff --git a/Tethering/res/values-mcc311-mnc480-nb/strings.xml b/Tethering/res/values-mcc311-mnc480-nb/strings.xml
index 92e6300..57db484 100644
--- a/Tethering/res/values-mcc311-mnc480-nb/strings.xml
+++ b/Tethering/res/values-mcc311-mnc480-nb/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="5897815458155858594">"Internettdeling har ikke internettilgang"</string>
-    <string name="no_upstream_notification_message" msgid="9037716118606459874">"Enheter kan ikke koble til"</string>
-    <string name="no_upstream_notification_disable_button" msgid="5284024068281565456">"Slå av internettdeling"</string>
-    <string name="upstream_roaming_notification_title" msgid="186331286017243006">"Wifi-sone eller internettdeling er på"</string>
-    <string name="upstream_roaming_notification_message" msgid="7692641323940316538">"Det kan påløpe flere kostnader ved roaming"</string>
+    <string name="no_upstream_notification_title" msgid="611650570559011140">"Internettdeling har ikke internettilgang"</string>
+    <string name="no_upstream_notification_message" msgid="6508394877641864863">"Enhetene kan ikke koble til"</string>
+    <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"Slå av internettdeling"</string>
+    <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"Wi-Fi-sone eller internettdeling er på"</string>
+    <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"Ytterligere kostnader kan påløpe under roaming"</string>
 </resources>
diff --git a/Tethering/res/values-mcc311-mnc480-ne/strings.xml b/Tethering/res/values-mcc311-mnc480-ne/strings.xml
index bfd6108..1503244 100644
--- a/Tethering/res/values-mcc311-mnc480-ne/strings.xml
+++ b/Tethering/res/values-mcc311-mnc480-ne/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="5897815458155858594">"टेदरिङमार्फत इन्टरनेट कनेक्ट गर्न सकिएन"</string>
-    <string name="no_upstream_notification_message" msgid="9037716118606459874">"डिभाइसहरू कनेक्ट गर्न सकिएन"</string>
-    <string name="no_upstream_notification_disable_button" msgid="5284024068281565456">"टेदरिङ अफ गर्नुहोस्"</string>
-    <string name="upstream_roaming_notification_title" msgid="186331286017243006">"हटस्पट वा टेदरिङ अन छ"</string>
-    <string name="upstream_roaming_notification_message" msgid="7692641323940316538">"रोमिङ सेवा प्रयोग गर्दा अतिरिक्त शुल्क लाग्न सक्छ"</string>
+    <string name="no_upstream_notification_title" msgid="611650570559011140">"टेदरिङमार्फत इन्टरनेट कनेक्सन प्राप्त हुन सकेन"</string>
+    <string name="no_upstream_notification_message" msgid="6508394877641864863">"यन्त्रहरू कनेक्ट गर्न सकिएन"</string>
+    <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"टेदरिङ निष्क्रिय पार्नुहोस्"</string>
+    <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"हटस्पट वा टेदरिङ सक्रिय छ"</string>
+    <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"रोमिङ सेवा प्रयोग गर्दा अतिरिक्त शुल्क लाग्न सक्छ"</string>
 </resources>
diff --git a/Tethering/res/values-mcc311-mnc480-nl/strings.xml b/Tethering/res/values-mcc311-mnc480-nl/strings.xml
index 7533b6f..b08133f 100644
--- a/Tethering/res/values-mcc311-mnc480-nl/strings.xml
+++ b/Tethering/res/values-mcc311-mnc480-nl/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="5897815458155858594">"Tethering heeft geen internet"</string>
-    <string name="no_upstream_notification_message" msgid="9037716118606459874">"Apparaten kunnen geen verbinding maken"</string>
-    <string name="no_upstream_notification_disable_button" msgid="5284024068281565456">"Tethering uitzetten"</string>
-    <string name="upstream_roaming_notification_title" msgid="186331286017243006">"Hotspot of tethering staat aan"</string>
-    <string name="upstream_roaming_notification_message" msgid="7692641323940316538">"Er kunnen extra kosten voor roaming in rekening worden gebracht"</string>
+    <string name="no_upstream_notification_title" msgid="611650570559011140">"Tethering heeft geen internet"</string>
+    <string name="no_upstream_notification_message" msgid="6508394877641864863">"Apparaten kunnen niet worden verbonden"</string>
+    <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"Tethering uitschakelen"</string>
+    <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"Hotspot of tethering is ingeschakeld"</string>
+    <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"Er kunnen extra kosten voor roaming in rekening worden gebracht."</string>
 </resources>
diff --git a/Tethering/res/values-mcc311-mnc480-or/strings.xml b/Tethering/res/values-mcc311-mnc480-or/strings.xml
index 7324de1..1ad4ca3 100644
--- a/Tethering/res/values-mcc311-mnc480-or/strings.xml
+++ b/Tethering/res/values-mcc311-mnc480-or/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="5897815458155858594">"ଟିଥରିଂ ପାଇଁ କୌଣସି ଇଣ୍ଟରନେଟ କନେକ୍ସନ ନାହିଁ"</string>
-    <string name="no_upstream_notification_message" msgid="9037716118606459874">"ଡିଭାଇସଗୁଡ଼ିକୁ କନେକ୍ଟ କରାଯାଇପାରିବ ନାହିଁ"</string>
-    <string name="no_upstream_notification_disable_button" msgid="5284024068281565456">"ଟିଥରିଂକୁ ବନ୍ଦ କରନ୍ତୁ"</string>
-    <string name="upstream_roaming_notification_title" msgid="186331286017243006">"ହଟସ୍ପଟ କିମ୍ବା ଟିଥରିଂ ଚାଲୁ ଅଛି"</string>
-    <string name="upstream_roaming_notification_message" msgid="7692641323940316538">"ରୋମିଂ ସମୟରେ ଅତିରିକ୍ତ ଚାର୍ଜ ଲାଗୁ ହୋଇପାରେ"</string>
+    <string name="no_upstream_notification_title" msgid="611650570559011140">"ଟିଥରିଂ ପାଇଁ କୌଣସି ଇଣ୍ଟର୍ନେଟ୍ ସଂଯୋଗ ନାହିଁ"</string>
+    <string name="no_upstream_notification_message" msgid="6508394877641864863">"ଡିଭାଇସଗୁଡ଼ିକ ସଂଯୋଗ କରାଯାଇପାରିବ ନାହିଁ"</string>
+    <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"ଟିଥରିଂ ବନ୍ଦ କରନ୍ତୁ"</string>
+    <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"ହଟସ୍ପଟ୍ କିମ୍ବା ଟିଥରିଂ ଚାଲୁ ଅଛି"</string>
+    <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"ରୋମିଂରେ ଥିବା ସମୟରେ ଅତିରିକ୍ତ ଶୁଳ୍କ ଲାଗୁ ହୋଇପାରେ"</string>
 </resources>
diff --git a/Tethering/res/values-mcc311-mnc480-pa/strings.xml b/Tethering/res/values-mcc311-mnc480-pa/strings.xml
index 0ca0af5..88def56 100644
--- a/Tethering/res/values-mcc311-mnc480-pa/strings.xml
+++ b/Tethering/res/values-mcc311-mnc480-pa/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="5897815458155858594">"ਟੈਦਰਿੰਗ ਕੋਲ ਇੰਟਰਨੈੱਟ ਪਹੁੰਚ ਨਹੀਂ ਹੈ"</string>
-    <string name="no_upstream_notification_message" msgid="9037716118606459874">"ਡੀਵਾਈਸ ਕਨੈਕਟ ਨਹੀਂ ਕੀਤੇ ਜਾ ਸਕਦੇ"</string>
-    <string name="no_upstream_notification_disable_button" msgid="5284024068281565456">"ਟੈਦਰਿੰਗ ਬੰਦ ਕਰੋ"</string>
-    <string name="upstream_roaming_notification_title" msgid="186331286017243006">"ਹੌਟਸਪੌਟ ਜਾਂ ਟੈਦਰਿੰਗ ਚਾਲੂ ਹੈ"</string>
-    <string name="upstream_roaming_notification_message" msgid="7692641323940316538">"ਰੋਮਿੰਗ ਦੌਰਾਨ ਵਧੀਕ ਖਰਚੇ ਲਾਗੂ ਹੋ ਸਕਦੇ ਹਨ"</string>
+    <string name="no_upstream_notification_title" msgid="611650570559011140">"ਟੈਦਰਿੰਗ ਕੋਲ ਇੰਟਰਨੈੱਟ ਪਹੁੰਚ ਨਹੀਂ ਹੈ"</string>
+    <string name="no_upstream_notification_message" msgid="6508394877641864863">"ਡੀਵਾਈਸ ਕਨੈਕਟ ਨਹੀਂ ਕੀਤੇ ਜਾ ਸਕਦੇ"</string>
+    <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"ਟੈਦਰਿੰਗ ਬੰਦ ਕਰੋ"</string>
+    <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"ਹੌਟਸਪੌਟ ਜਾਂ ਟੈਦਰਿੰਗ ਚਾਲੂ ਹੈ"</string>
+    <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"ਰੋਮਿੰਗ ਦੌਰਾਨ ਵਧੀਕ ਖਰਚੇ ਲਾਗੂ ਹੋ ਸਕਦੇ ਹਨ"</string>
 </resources>
diff --git a/Tethering/res/values-mcc311-mnc480-pl/strings.xml b/Tethering/res/values-mcc311-mnc480-pl/strings.xml
index 62bb68c..f9890ab 100644
--- a/Tethering/res/values-mcc311-mnc480-pl/strings.xml
+++ b/Tethering/res/values-mcc311-mnc480-pl/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="5897815458155858594">"Tethering nie ma internetu"</string>
-    <string name="no_upstream_notification_message" msgid="9037716118606459874">"Urządzenia nie mogą się połączyć"</string>
-    <string name="no_upstream_notification_disable_button" msgid="5284024068281565456">"Wyłącz tethering"</string>
-    <string name="upstream_roaming_notification_title" msgid="186331286017243006">"Hotspot lub tethering jest włączony"</string>
-    <string name="upstream_roaming_notification_message" msgid="7692641323940316538">"Podczas korzystania z roamingu mogą zostać naliczone dodatkowe opłaty"</string>
+    <string name="no_upstream_notification_title" msgid="611650570559011140">"Tethering nie ma internetu"</string>
+    <string name="no_upstream_notification_message" msgid="6508394877641864863">"Urządzenia nie mogą się połączyć"</string>
+    <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"Wyłącz tethering"</string>
+    <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"Hotspot lub tethering jest włączony"</string>
+    <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"Podczas korzystania z roamingu mogą zostać naliczone dodatkowe opłaty"</string>
 </resources>
diff --git a/Tethering/res/values-mcc311-mnc480-pt-rBR/strings.xml b/Tethering/res/values-mcc311-mnc480-pt-rBR/strings.xml
index ae033fa..ce3b884 100644
--- a/Tethering/res/values-mcc311-mnc480-pt-rBR/strings.xml
+++ b/Tethering/res/values-mcc311-mnc480-pt-rBR/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="5897815458155858594">"O tethering não tem uma conexão de Internet"</string>
-    <string name="no_upstream_notification_message" msgid="9037716118606459874">"Não é possível conectar os dispositivos"</string>
-    <string name="no_upstream_notification_disable_button" msgid="5284024068281565456">"Desativar o tethering"</string>
-    <string name="upstream_roaming_notification_title" msgid="186331286017243006">"O ponto de acesso ou tethering está ativado"</string>
-    <string name="upstream_roaming_notification_message" msgid="7692641323940316538">"Pode haver cobranças extras durante o roaming"</string>
+    <string name="no_upstream_notification_title" msgid="611650570559011140">"O tethering não tem Internet"</string>
+    <string name="no_upstream_notification_message" msgid="6508394877641864863">"Não é possível conectar os dispositivos"</string>
+    <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"Desativar o tethering"</string>
+    <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"Ponto de acesso ou tethering ativado"</string>
+    <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"Pode haver cobranças extras durante o roaming"</string>
 </resources>
diff --git a/Tethering/res/values-mcc311-mnc480-pt-rPT/strings.xml b/Tethering/res/values-mcc311-mnc480-pt-rPT/strings.xml
index c544864..7e883ea 100644
--- a/Tethering/res/values-mcc311-mnc480-pt-rPT/strings.xml
+++ b/Tethering/res/values-mcc311-mnc480-pt-rPT/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="5897815458155858594">"A ligação (à Internet) via telemóvel não tem Internet"</string>
-    <string name="no_upstream_notification_message" msgid="9037716118606459874">"Não é possível ligar os dispositivos"</string>
-    <string name="no_upstream_notification_disable_button" msgid="5284024068281565456">"Desativar ligação (à Internet) via telemóvel"</string>
-    <string name="upstream_roaming_notification_title" msgid="186331286017243006">"A zona Wi-Fi ou a ligação (à Internet) via telemóvel está ativada"</string>
-    <string name="upstream_roaming_notification_message" msgid="7692641323940316538">"Podem aplicar-se custos adicionais em roaming"</string>
+    <string name="no_upstream_notification_title" msgid="611650570559011140">"A ligação (à Internet) via telemóvel não tem Internet"</string>
+    <string name="no_upstream_notification_message" msgid="6508394877641864863">"Não é possível ligar os dispositivos"</string>
+    <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"Desativar ligação (à Internet) via telemóvel"</string>
+    <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"A zona Wi-Fi ou a ligação (à Internet) via telemóvel está ativada"</string>
+    <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"Podem aplicar-se custos adicionais em roaming."</string>
 </resources>
diff --git a/Tethering/res/values-mcc311-mnc480-pt/strings.xml b/Tethering/res/values-mcc311-mnc480-pt/strings.xml
index ae033fa..ce3b884 100644
--- a/Tethering/res/values-mcc311-mnc480-pt/strings.xml
+++ b/Tethering/res/values-mcc311-mnc480-pt/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="5897815458155858594">"O tethering não tem uma conexão de Internet"</string>
-    <string name="no_upstream_notification_message" msgid="9037716118606459874">"Não é possível conectar os dispositivos"</string>
-    <string name="no_upstream_notification_disable_button" msgid="5284024068281565456">"Desativar o tethering"</string>
-    <string name="upstream_roaming_notification_title" msgid="186331286017243006">"O ponto de acesso ou tethering está ativado"</string>
-    <string name="upstream_roaming_notification_message" msgid="7692641323940316538">"Pode haver cobranças extras durante o roaming"</string>
+    <string name="no_upstream_notification_title" msgid="611650570559011140">"O tethering não tem Internet"</string>
+    <string name="no_upstream_notification_message" msgid="6508394877641864863">"Não é possível conectar os dispositivos"</string>
+    <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"Desativar o tethering"</string>
+    <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"Ponto de acesso ou tethering ativado"</string>
+    <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"Pode haver cobranças extras durante o roaming"</string>
 </resources>
diff --git a/Tethering/res/values-mcc311-mnc480-ro/strings.xml b/Tethering/res/values-mcc311-mnc480-ro/strings.xml
index 484b8b3..1009417 100644
--- a/Tethering/res/values-mcc311-mnc480-ro/strings.xml
+++ b/Tethering/res/values-mcc311-mnc480-ro/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="5897815458155858594">"Procesul de tethering nu are internet"</string>
-    <string name="no_upstream_notification_message" msgid="9037716118606459874">"Dispozitivele nu se pot conecta"</string>
-    <string name="no_upstream_notification_disable_button" msgid="5284024068281565456">"Dezactivează tetheringul"</string>
-    <string name="upstream_roaming_notification_title" msgid="186331286017243006">"S-a activat hotspotul sau tetheringul"</string>
-    <string name="upstream_roaming_notification_message" msgid="7692641323940316538">"Se pot aplica taxe suplimentare pentru roaming"</string>
+    <string name="no_upstream_notification_title" msgid="611650570559011140">"Procesul de tethering nu are internet"</string>
+    <string name="no_upstream_notification_message" msgid="6508394877641864863">"Dispozitivele nu se pot conecta"</string>
+    <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"Dezactivați procesul de tethering"</string>
+    <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"S-a activat hotspotul sau tethering"</string>
+    <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"Se pot aplica taxe suplimentare pentru roaming"</string>
 </resources>
diff --git a/Tethering/res/values-mcc311-mnc480-ru/strings.xml b/Tethering/res/values-mcc311-mnc480-ru/strings.xml
index ef38703..88683be 100644
--- a/Tethering/res/values-mcc311-mnc480-ru/strings.xml
+++ b/Tethering/res/values-mcc311-mnc480-ru/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="5897815458155858594">"Режим модема используется без доступа к интернету"</string>
-    <string name="no_upstream_notification_message" msgid="9037716118606459874">"Невозможно подключить устройства."</string>
-    <string name="no_upstream_notification_disable_button" msgid="5284024068281565456">"Отключить режим модема"</string>
-    <string name="upstream_roaming_notification_title" msgid="186331286017243006">"Включена точка доступа или режим модема"</string>
-    <string name="upstream_roaming_notification_message" msgid="7692641323940316538">"За использование услуг связи в роуминге может взиматься дополнительная плата."</string>
+    <string name="no_upstream_notification_title" msgid="611650570559011140">"Режим модема используется без доступа к Интернету"</string>
+    <string name="no_upstream_notification_message" msgid="6508394877641864863">"Невозможно подключить устройства."</string>
+    <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"Отключить режим модема"</string>
+    <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"Включены точка доступа или режим модема"</string>
+    <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"За использование услуг связи в роуминге может взиматься дополнительная плата."</string>
 </resources>
diff --git a/Tethering/res/values-mcc311-mnc480-si/strings.xml b/Tethering/res/values-mcc311-mnc480-si/strings.xml
index 3069085..176bcdb 100644
--- a/Tethering/res/values-mcc311-mnc480-si/strings.xml
+++ b/Tethering/res/values-mcc311-mnc480-si/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="5897815458155858594">"ටෙදරින් හට අන්තර්ජාලය නැත"</string>
-    <string name="no_upstream_notification_message" msgid="9037716118606459874">"උපාංගවලට සම්බන්ධ විය නොහැක"</string>
-    <string name="no_upstream_notification_disable_button" msgid="5284024068281565456">"ටෙදරින් ක්‍රියාවිරහිත කරන්න"</string>
-    <string name="upstream_roaming_notification_title" msgid="186331286017243006">"හොට්ස්පොට් හෝ ටෙදරින් ක්‍රියාත්මකයි"</string>
-    <string name="upstream_roaming_notification_message" msgid="7692641323940316538">"රෝමිං අතරේ අතිරේක ගාස්තු අදාළ විය හැක"</string>
+    <string name="no_upstream_notification_title" msgid="611650570559011140">"ටෙදරින් හට අන්තර්ජාලය නැත"</string>
+    <string name="no_upstream_notification_message" msgid="6508394877641864863">"උපාංගවලට සම්බන්ධ විය නොහැකිය"</string>
+    <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"ටෙදරින් ක්‍රියාවිරහිත කරන්න"</string>
+    <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"හොට්ස්පොට් හෝ ටෙදරින් ක්‍රියාත්මකයි"</string>
+    <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"රෝමිං අතරතුර අමතර ගාස්තු අදාළ විය හැකිය"</string>
 </resources>
diff --git a/Tethering/res/values-mcc311-mnc480-sk/strings.xml b/Tethering/res/values-mcc311-mnc480-sk/strings.xml
index 9f70311..b9e2127 100644
--- a/Tethering/res/values-mcc311-mnc480-sk/strings.xml
+++ b/Tethering/res/values-mcc311-mnc480-sk/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="5897815458155858594">"Tethering nemá internetové pripojenie"</string>
-    <string name="no_upstream_notification_message" msgid="9037716118606459874">"Zariadenia sa nemôžu pripojiť"</string>
-    <string name="no_upstream_notification_disable_button" msgid="5284024068281565456">"Vypnúť tethering"</string>
-    <string name="upstream_roaming_notification_title" msgid="186331286017243006">"Je zapnutý hotspot alebo tethering"</string>
-    <string name="upstream_roaming_notification_message" msgid="7692641323940316538">"Počas roamingu vám môžu byť účtované ďalšie poplatky"</string>
+    <string name="no_upstream_notification_title" msgid="611650570559011140">"Tethering nemá internetové pripojenie"</string>
+    <string name="no_upstream_notification_message" msgid="6508394877641864863">"Zariadenia sa nemôžu pripojiť"</string>
+    <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"Vypnúť tethering"</string>
+    <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"Je zapnutý hotspot alebo tethering"</string>
+    <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"Počas roamingu vám môžu byť účtované ďalšie poplatky"</string>
 </resources>
diff --git a/Tethering/res/values-mcc311-mnc480-sl/strings.xml b/Tethering/res/values-mcc311-mnc480-sl/strings.xml
index 613d7a8..e8140e6 100644
--- a/Tethering/res/values-mcc311-mnc480-sl/strings.xml
+++ b/Tethering/res/values-mcc311-mnc480-sl/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="5897815458155858594">"Povezava računalnika z internetom prek mobilnega telefona nima internetne povezave"</string>
-    <string name="no_upstream_notification_message" msgid="9037716118606459874">"Napravi se ne moreta povezati"</string>
-    <string name="no_upstream_notification_disable_button" msgid="5284024068281565456">"Izklopi povezavo računalnika z internetom prek mobilnega telefona"</string>
-    <string name="upstream_roaming_notification_title" msgid="186331286017243006">"Dostopna točka ali povezava računalnika z internetom prek mobilnega telefona je vklopljena"</string>
-    <string name="upstream_roaming_notification_message" msgid="7692641323940316538">"Med gostovanjem lahko nastanejo dodatni stroški."</string>
+    <string name="no_upstream_notification_title" msgid="611650570559011140">"Internetna povezava prek mobilnega telefona ni vzpostavljena"</string>
+    <string name="no_upstream_notification_message" msgid="6508394877641864863">"Napravi se ne moreta povezati"</string>
+    <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"Izklopi internetno povezavo prek mobilnega telefona"</string>
+    <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"Dostopna točka ali internetna povezava prek mobilnega telefona je vklopljena"</string>
+    <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"Med gostovanjem lahko nastanejo dodatni stroški"</string>
 </resources>
diff --git a/Tethering/res/values-mcc311-mnc480-sq/strings.xml b/Tethering/res/values-mcc311-mnc480-sq/strings.xml
index 0472d4d..61e698d 100644
--- a/Tethering/res/values-mcc311-mnc480-sq/strings.xml
+++ b/Tethering/res/values-mcc311-mnc480-sq/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="5897815458155858594">"Ndarja e internetit nuk ka internet"</string>
-    <string name="no_upstream_notification_message" msgid="9037716118606459874">"Pajisjet nuk mund të lidhen"</string>
-    <string name="no_upstream_notification_disable_button" msgid="5284024068281565456">"Çaktivizo ndarjen e internetit"</string>
-    <string name="upstream_roaming_notification_title" msgid="186331286017243006">"Zona e qasjes për internet ose ndarja e internetit është aktive"</string>
-    <string name="upstream_roaming_notification_message" msgid="7692641323940316538">"Mund të zbatohen tarifime shtesë kur je në roaming"</string>
+    <string name="no_upstream_notification_title" msgid="611650570559011140">"Ndarja e internetit nuk ka internet"</string>
+    <string name="no_upstream_notification_message" msgid="6508394877641864863">"Pajisjet nuk mund të lidhen"</string>
+    <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"Çaktivizo ndarjen e internetit"</string>
+    <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"Zona e qasjes për internet ose ndarja e internetit është aktive"</string>
+    <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"Mund të zbatohen tarifime shtesë kur je në roaming"</string>
 </resources>
diff --git a/Tethering/res/values-mcc311-mnc480-sr/strings.xml b/Tethering/res/values-mcc311-mnc480-sr/strings.xml
index bc70cf6..b4c411c 100644
--- a/Tethering/res/values-mcc311-mnc480-sr/strings.xml
+++ b/Tethering/res/values-mcc311-mnc480-sr/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="5897815458155858594">"Привезивање нема приступ интернету"</string>
-    <string name="no_upstream_notification_message" msgid="9037716118606459874">"Повезивање уређаја није успело"</string>
-    <string name="no_upstream_notification_disable_button" msgid="5284024068281565456">"Искључи привезивање"</string>
-    <string name="upstream_roaming_notification_title" msgid="186331286017243006">"Укључен је хотспот или привезивање"</string>
-    <string name="upstream_roaming_notification_message" msgid="7692641323940316538">"Можда важе додатни трошкови у ромингу"</string>
+    <string name="no_upstream_notification_title" msgid="611650570559011140">"Привезивање нема приступ интернету"</string>
+    <string name="no_upstream_notification_message" msgid="6508394877641864863">"Повезивање уређаја није успело"</string>
+    <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"Искључи привезивање"</string>
+    <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"Укључен је хотспот или привезивање"</string>
+    <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"Можда важе додатни трошкови у ромингу"</string>
 </resources>
diff --git a/Tethering/res/values-mcc311-mnc480-sv/strings.xml b/Tethering/res/values-mcc311-mnc480-sv/strings.xml
index 507acc8..4f543e4 100644
--- a/Tethering/res/values-mcc311-mnc480-sv/strings.xml
+++ b/Tethering/res/values-mcc311-mnc480-sv/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="5897815458155858594">"Det finns ingen internetanslutning för internetdelningen"</string>
-    <string name="no_upstream_notification_message" msgid="9037716118606459874">"Enheterna kan inte anslutas"</string>
-    <string name="no_upstream_notification_disable_button" msgid="5284024068281565456">"Inaktivera internetdelning"</string>
-    <string name="upstream_roaming_notification_title" msgid="186331286017243006">"Surfzon eller internetdelning har aktiverats"</string>
-    <string name="upstream_roaming_notification_message" msgid="7692641323940316538">"Ytterligare avgifter kan tillkomma vid roaming"</string>
+    <string name="no_upstream_notification_title" msgid="611650570559011140">"Det finns ingen internetanslutning för internetdelningen"</string>
+    <string name="no_upstream_notification_message" msgid="6508394877641864863">"Enheterna kan inte anslutas"</string>
+    <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"Inaktivera internetdelning"</string>
+    <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"Surfzon eller internetdelning har aktiverats"</string>
+    <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"Ytterligare avgifter kan tillkomma vid roaming"</string>
 </resources>
diff --git a/Tethering/res/values-mcc311-mnc480-sw/strings.xml b/Tethering/res/values-mcc311-mnc480-sw/strings.xml
index 865b0e3..ac347ab 100644
--- a/Tethering/res/values-mcc311-mnc480-sw/strings.xml
+++ b/Tethering/res/values-mcc311-mnc480-sw/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="5897815458155858594">"Huduma ya kusambaza mtandao haina muunganisho wa intaneti"</string>
-    <string name="no_upstream_notification_message" msgid="9037716118606459874">"Imeshindwa kuunganisha vifaa"</string>
-    <string name="no_upstream_notification_disable_button" msgid="5284024068281565456">"Zima kipengele cha kusambaza mtandao"</string>
-    <string name="upstream_roaming_notification_title" msgid="186331286017243006">"Umewasha kipengele cha kusambaza mtandao au mtandao pepe"</string>
-    <string name="upstream_roaming_notification_message" msgid="7692641323940316538">"Huenda ukatozwa gharama za ziada ukitumia mitandao ya ng\'ambo"</string>
+    <string name="no_upstream_notification_title" msgid="611650570559011140">"Kipengele cha kusambaza mtandao hakina intaneti"</string>
+    <string name="no_upstream_notification_message" msgid="6508394877641864863">"Imeshindwa kuunganisha vifaa"</string>
+    <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"Zima kipengele cha kusambaza mtandao"</string>
+    <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"Umewasha kipengele cha kusambaza mtandao au mtandao pepe"</string>
+    <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"Huenda ukatozwa gharama za ziada ukitumia mitandao ya ng\'ambo"</string>
 </resources>
diff --git a/Tethering/res/values-mcc311-mnc480-ta/strings.xml b/Tethering/res/values-mcc311-mnc480-ta/strings.xml
index e5f33ee..2ea2467 100644
--- a/Tethering/res/values-mcc311-mnc480-ta/strings.xml
+++ b/Tethering/res/values-mcc311-mnc480-ta/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="5897815458155858594">"இணைப்பு முறைக்கு இணைய இணைப்பு இல்லை"</string>
-    <string name="no_upstream_notification_message" msgid="9037716118606459874">"சாதனங்களால் இணைய முடியவில்லை"</string>
-    <string name="no_upstream_notification_disable_button" msgid="5284024068281565456">"இணைப்பு முறையை முடக்கு"</string>
-    <string name="upstream_roaming_notification_title" msgid="186331286017243006">"ஹாட்ஸ்பாட் அல்லது இணைப்பு முறை இயக்கப்பட்டுள்ளது"</string>
-    <string name="upstream_roaming_notification_message" msgid="7692641323940316538">"ரோமிங்கின்போது கூடுதல் கட்டணங்கள் விதிக்கப்படலாம்"</string>
+    <string name="no_upstream_notification_title" msgid="611650570559011140">"இணைப்பு முறைக்கு இணைய இணைப்பு இல்லை"</string>
+    <string name="no_upstream_notification_message" msgid="6508394877641864863">"சாதனங்களால் இணைய முடியவில்லை"</string>
+    <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"இணைப்பு முறையை ஆஃப் செய்"</string>
+    <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"ஹாட்ஸ்பாட் அல்லது இணைப்பு முறை ஆன் செய்யப்பட்டுள்ளது"</string>
+    <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"ரோமிங்கின்போது கூடுதல் கட்டணங்கள் விதிக்கப்படக்கூடும்"</string>
 </resources>
diff --git a/Tethering/res/values-mcc311-mnc480-te/strings.xml b/Tethering/res/values-mcc311-mnc480-te/strings.xml
index 1bb4786..9360297 100644
--- a/Tethering/res/values-mcc311-mnc480-te/strings.xml
+++ b/Tethering/res/values-mcc311-mnc480-te/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="5897815458155858594">"టెథరింగ్ చేయడానికి ఇంటర్నెట్ కనెక్షన్ లేదు"</string>
-    <string name="no_upstream_notification_message" msgid="9037716118606459874">"పరికరాలు కనెక్ట్ అవ్వడం లేదు"</string>
-    <string name="no_upstream_notification_disable_button" msgid="5284024068281565456">"టెథరింగ్‌ను ఆఫ్ చేయండి"</string>
-    <string name="upstream_roaming_notification_title" msgid="186331286017243006">"హాట్‌స్పాట్ లేదా టెథరింగ్ ఆన్‌లో ఉంది"</string>
-    <string name="upstream_roaming_notification_message" msgid="7692641323940316538">"రోమింగ్‌లో ఉన్నప్పుడు అదనపు ఛార్జీలు వర్తించవచ్చు"</string>
+    <string name="no_upstream_notification_title" msgid="611650570559011140">"టెథరింగ్ చేయడానికి ఇంటర్నెట్ కనెక్షన్ లేదు"</string>
+    <string name="no_upstream_notification_message" msgid="6508394877641864863">"పరికరాలు కనెక్ట్ అవ్వడం లేదు"</string>
+    <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"టెథరింగ్‌ను ఆఫ్ చేయండి"</string>
+    <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"హాట్‌స్పాట్ లేదా టెథరింగ్ ఆన్‌లో ఉంది"</string>
+    <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"రోమింగ్‌లో ఉన్నప్పుడు అదనపు ఛార్జీలు వర్తించవచ్చు"</string>
 </resources>
diff --git a/Tethering/res/values-mcc311-mnc480-th/strings.xml b/Tethering/res/values-mcc311-mnc480-th/strings.xml
index e76f735..9c4d7e0 100644
--- a/Tethering/res/values-mcc311-mnc480-th/strings.xml
+++ b/Tethering/res/values-mcc311-mnc480-th/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="5897815458155858594">"ไม่มีอินเทอร์เน็ตสำหรับการเชื่อมต่ออินเทอร์เน็ตผ่านมือถือ"</string>
-    <string name="no_upstream_notification_message" msgid="9037716118606459874">"อุปกรณ์เชื่อมต่อไม่ได้"</string>
-    <string name="no_upstream_notification_disable_button" msgid="5284024068281565456">"ปิดการเชื่อมต่ออินเทอร์เน็ตผ่านมือถือ"</string>
-    <string name="upstream_roaming_notification_title" msgid="186331286017243006">"ฮอตสปอตหรือการเชื่อมต่ออินเทอร์เน็ตผ่านมือถือเปิดอยู่"</string>
-    <string name="upstream_roaming_notification_message" msgid="7692641323940316538">"อาจมีค่าใช้จ่ายเพิ่มเติมขณะโรมมิ่ง"</string>
+    <string name="no_upstream_notification_title" msgid="611650570559011140">"การเชื่อมต่ออินเทอร์เน็ตผ่านมือถือไม่มีอินเทอร์เน็ต"</string>
+    <string name="no_upstream_notification_message" msgid="6508394877641864863">"อุปกรณ์เชื่อมต่อไม่ได้"</string>
+    <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"ปิดการเชื่อมต่ออินเทอร์เน็ตผ่านมือถือ"</string>
+    <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"ฮอตสปอตหรือการเชื่อมต่ออินเทอร์เน็ตผ่านมือถือเปิดอยู่"</string>
+    <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"อาจมีค่าใช้จ่ายเพิ่มเติมขณะโรมมิ่ง"</string>
 </resources>
diff --git a/Tethering/res/values-mcc311-mnc480-tl/strings.xml b/Tethering/res/values-mcc311-mnc480-tl/strings.xml
index cccc8c4..a7c78a5 100644
--- a/Tethering/res/values-mcc311-mnc480-tl/strings.xml
+++ b/Tethering/res/values-mcc311-mnc480-tl/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="5897815458155858594">"Walang internet ang pag-tether"</string>
-    <string name="no_upstream_notification_message" msgid="9037716118606459874">"Hindi makakonekta ang mga device"</string>
-    <string name="no_upstream_notification_disable_button" msgid="5284024068281565456">"I-off ang pag-tether"</string>
-    <string name="upstream_roaming_notification_title" msgid="186331286017243006">"Naka-on ang hotspot o pag-tether"</string>
-    <string name="upstream_roaming_notification_message" msgid="7692641323940316538">"Posibleng magkaroon ng mga karagdagang singil habang nagro-roam"</string>
+    <string name="no_upstream_notification_title" msgid="611650570559011140">"Walang internet ang pag-tether"</string>
+    <string name="no_upstream_notification_message" msgid="6508394877641864863">"Hindi makakonekta ang mga device"</string>
+    <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"I-off ang pag-tether"</string>
+    <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"Naka-on ang Hotspot o pag-tether"</string>
+    <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"Posibleng magkaroon ng mga karagdagang singil habang nagro-roam"</string>
 </resources>
diff --git a/Tethering/res/values-mcc311-mnc480-tr/strings.xml b/Tethering/res/values-mcc311-mnc480-tr/strings.xml
index 93bef12..93da2c3 100644
--- a/Tethering/res/values-mcc311-mnc480-tr/strings.xml
+++ b/Tethering/res/values-mcc311-mnc480-tr/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="5897815458155858594">"Tethering\'in internet bağlantısı yok"</string>
-    <string name="no_upstream_notification_message" msgid="9037716118606459874">"Cihazlar bağlanamıyor"</string>
-    <string name="no_upstream_notification_disable_button" msgid="5284024068281565456">"Tethering\'i kapat"</string>
-    <string name="upstream_roaming_notification_title" msgid="186331286017243006">"Hotspot veya tethering açık"</string>
-    <string name="upstream_roaming_notification_message" msgid="7692641323940316538">"Dolaşım sırasında ek ücretler uygulanabilir"</string>
+    <string name="no_upstream_notification_title" msgid="611650570559011140">"Tethering\'in internet bağlantısı yok"</string>
+    <string name="no_upstream_notification_message" msgid="6508394877641864863">"Cihazlar bağlanamıyor"</string>
+    <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"Tethering\'i kapat"</string>
+    <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"Hotspot veya tethering açık"</string>
+    <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"Dolaşım sırasında ek ücretler uygulanabilir"</string>
 </resources>
diff --git a/Tethering/res/values-mcc311-mnc480-uk/strings.xml b/Tethering/res/values-mcc311-mnc480-uk/strings.xml
index 1bc2c06..ee0dcd2 100644
--- a/Tethering/res/values-mcc311-mnc480-uk/strings.xml
+++ b/Tethering/res/values-mcc311-mnc480-uk/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="5897815458155858594">"Телефон, що використовується як модем, не підключений до Інтернету"</string>
-    <string name="no_upstream_notification_message" msgid="9037716118606459874">"Не вдається підключити пристрої"</string>
-    <string name="no_upstream_notification_disable_button" msgid="5284024068281565456">"Вимкнути використання телефона як модема"</string>
-    <string name="upstream_roaming_notification_title" msgid="186331286017243006">"Увімкнено точку доступу або використання телефона як модема"</string>
-    <string name="upstream_roaming_notification_message" msgid="7692641323940316538">"У роумінгу може стягуватися додаткова плата"</string>
+    <string name="no_upstream_notification_title" msgid="611650570559011140">"Телефон, який використовується як модем, не підключений до Інтернету"</string>
+    <string name="no_upstream_notification_message" msgid="6508394877641864863">"Не вдається підключити пристрої"</string>
+    <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"Вимкнути використання телефона як модема"</string>
+    <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"Увімкнено точку доступу або використання телефона як модема"</string>
+    <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"У роумінгу може стягуватися додаткова плата"</string>
 </resources>
diff --git a/Tethering/res/values-mcc311-mnc480-ur/strings.xml b/Tethering/res/values-mcc311-mnc480-ur/strings.xml
index 63d8e1b..41cd28e 100644
--- a/Tethering/res/values-mcc311-mnc480-ur/strings.xml
+++ b/Tethering/res/values-mcc311-mnc480-ur/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="5897815458155858594">"ٹیدرنگ میں انٹرنیٹ نہیں ہے"</string>
-    <string name="no_upstream_notification_message" msgid="9037716118606459874">"آلات منسلک نہیں ہو سکتے"</string>
-    <string name="no_upstream_notification_disable_button" msgid="5284024068281565456">"ٹیدرنگ آف کریں"</string>
-    <string name="upstream_roaming_notification_title" msgid="186331286017243006">"ہاٹ اسپاٹ یا ٹیدرنگ آن ہے"</string>
-    <string name="upstream_roaming_notification_message" msgid="7692641323940316538">"رومنگ کے دوران اضافی چارجز لاگو ہو سکتے ہیں"</string>
+    <string name="no_upstream_notification_title" msgid="611650570559011140">"ٹیدرنگ میں انٹرنیٹ نہیں ہے"</string>
+    <string name="no_upstream_notification_message" msgid="6508394877641864863">"آلات منسلک نہیں ہو سکتے"</string>
+    <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"ٹیدرنگ آف کریں"</string>
+    <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"ہاٹ اسپاٹ یا ٹیدرنگ آن ہے"</string>
+    <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"رومنگ کے دوران اضافی چارجز لاگو ہو سکتے ہیں"</string>
 </resources>
diff --git a/Tethering/res/values-mcc311-mnc480-uz/strings.xml b/Tethering/res/values-mcc311-mnc480-uz/strings.xml
index 4d655d9..c847bc9 100644
--- a/Tethering/res/values-mcc311-mnc480-uz/strings.xml
+++ b/Tethering/res/values-mcc311-mnc480-uz/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="5897815458155858594">"Modem internetga ulanmagan"</string>
-    <string name="no_upstream_notification_message" msgid="9037716118606459874">"Qurilmalar ulanmadi"</string>
-    <string name="no_upstream_notification_disable_button" msgid="5284024068281565456">"Modem rejimini faolsizlantirish"</string>
-    <string name="upstream_roaming_notification_title" msgid="186331286017243006">"Hotspot yoki modem rejimi yoniq"</string>
-    <string name="upstream_roaming_notification_message" msgid="7692641323940316538">"Rouming vaqtida qoʻshimcha haq olinishi mumkin"</string>
+    <string name="no_upstream_notification_title" msgid="611650570559011140">"Modem internetga ulanmagan"</string>
+    <string name="no_upstream_notification_message" msgid="6508394877641864863">"Qurilmalar ulanmadi"</string>
+    <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"Modem rejimini faolsizlantirish"</string>
+    <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"Hotspot yoki modem rejimi yoniq"</string>
+    <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"Rouming vaqtida qoʻshimcha haq olinishi mumkin"</string>
 </resources>
diff --git a/Tethering/res/values-mcc311-mnc480-vi/strings.xml b/Tethering/res/values-mcc311-mnc480-vi/strings.xml
index 15e7a01..a74326f 100644
--- a/Tethering/res/values-mcc311-mnc480-vi/strings.xml
+++ b/Tethering/res/values-mcc311-mnc480-vi/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="5897815458155858594">"Không có Internet để chia sẻ Internet"</string>
-    <string name="no_upstream_notification_message" msgid="9037716118606459874">"Các thiết bị không thể kết nối"</string>
-    <string name="no_upstream_notification_disable_button" msgid="5284024068281565456">"Tắt tính năng chia sẻ Internet"</string>
-    <string name="upstream_roaming_notification_title" msgid="186331286017243006">"Điểm phát sóng hoặc tính năng chia sẻ Internet đang bật"</string>
-    <string name="upstream_roaming_notification_message" msgid="7692641323940316538">"Bạn có thể mất thêm phí dữ liệu khi chuyển vùng"</string>
+    <string name="no_upstream_notification_title" msgid="611650570559011140">"Không có Internet để chia sẻ kết Internet"</string>
+    <string name="no_upstream_notification_message" msgid="6508394877641864863">"Các thiết bị không thể kết nối"</string>
+    <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"Tắt tính năng chia sẻ Internet"</string>
+    <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"Điểm phát sóng hoặc tính năng chia sẻ Internet đang bật"</string>
+    <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"Bạn có thể mất thêm phí dữ liệu khi chuyển vùng"</string>
 </resources>
diff --git a/Tethering/res/values-mcc311-mnc480-zh-rCN/strings.xml b/Tethering/res/values-mcc311-mnc480-zh-rCN/strings.xml
index 8a200aa..d737003 100644
--- a/Tethering/res/values-mcc311-mnc480-zh-rCN/strings.xml
+++ b/Tethering/res/values-mcc311-mnc480-zh-rCN/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="5897815458155858594">"共享网络未连接到互联网"</string>
-    <string name="no_upstream_notification_message" msgid="9037716118606459874">"设备无法连接"</string>
-    <string name="no_upstream_notification_disable_button" msgid="5284024068281565456">"关闭网络共享"</string>
-    <string name="upstream_roaming_notification_title" msgid="186331286017243006">"热点或网络共享已开启"</string>
-    <string name="upstream_roaming_notification_message" msgid="7692641323940316538">"漫游时可能会产生额外的费用"</string>
+    <string name="no_upstream_notification_title" msgid="611650570559011140">"共享网络未连接到互联网"</string>
+    <string name="no_upstream_notification_message" msgid="6508394877641864863">"设备无法连接"</string>
+    <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"关闭网络共享"</string>
+    <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"热点或网络共享已开启"</string>
+    <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"漫游时可能会产生额外的费用"</string>
 </resources>
diff --git a/Tethering/res/values-mcc311-mnc480-zh-rHK/strings.xml b/Tethering/res/values-mcc311-mnc480-zh-rHK/strings.xml
index b2e64d1..f378a9d 100644
--- a/Tethering/res/values-mcc311-mnc480-zh-rHK/strings.xml
+++ b/Tethering/res/values-mcc311-mnc480-zh-rHK/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="5897815458155858594">"無法透過網絡共享連線至互聯網"</string>
-    <string name="no_upstream_notification_message" msgid="9037716118606459874">"裝置無法連接"</string>
-    <string name="no_upstream_notification_disable_button" msgid="5284024068281565456">"關閉網絡共享"</string>
-    <string name="upstream_roaming_notification_title" msgid="186331286017243006">"熱點或網絡共享已開啟"</string>
-    <string name="upstream_roaming_notification_message" msgid="7692641323940316538">"漫遊時可能需要支付額外費用"</string>
+    <string name="no_upstream_notification_title" msgid="611650570559011140">"無法透過網絡共享連線至互聯網"</string>
+    <string name="no_upstream_notification_message" msgid="6508394877641864863">"裝置無法連接"</string>
+    <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"關閉網絡共享"</string>
+    <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"熱點或網絡共享已開啟"</string>
+    <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"漫遊時可能需要支付額外費用"</string>
 </resources>
diff --git a/Tethering/res/values-mcc311-mnc480-zh-rTW/strings.xml b/Tethering/res/values-mcc311-mnc480-zh-rTW/strings.xml
index 0d7ddf2..cd653df 100644
--- a/Tethering/res/values-mcc311-mnc480-zh-rTW/strings.xml
+++ b/Tethering/res/values-mcc311-mnc480-zh-rTW/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="5897815458155858594">"無法透過網路共用連上網際網路"</string>
-    <string name="no_upstream_notification_message" msgid="9037716118606459874">"裝置無法連線"</string>
-    <string name="no_upstream_notification_disable_button" msgid="5284024068281565456">"關閉網路共用"</string>
-    <string name="upstream_roaming_notification_title" msgid="186331286017243006">"無線基地台或網路共用已開啟"</string>
-    <string name="upstream_roaming_notification_message" msgid="7692641323940316538">"使用漫遊服務可能須支付額外費用"</string>
+    <string name="no_upstream_notification_title" msgid="611650570559011140">"無法透過網路共用連上網際網路"</string>
+    <string name="no_upstream_notification_message" msgid="6508394877641864863">"裝置無法連線"</string>
+    <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"關閉網路共用"</string>
+    <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"無線基地台或網路共用已開啟"</string>
+    <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"使用漫遊服務可能須支付額外費用"</string>
 </resources>
diff --git a/Tethering/res/values-mcc311-mnc480-zu/strings.xml b/Tethering/res/values-mcc311-mnc480-zu/strings.xml
index d18f079..32f6df5 100644
--- a/Tethering/res/values-mcc311-mnc480-zu/strings.xml
+++ b/Tethering/res/values-mcc311-mnc480-zu/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="5897815458155858594">"Ukusebenzisa ifoni njengemodemu akunayo i-inthanethi"</string>
-    <string name="no_upstream_notification_message" msgid="9037716118606459874">"Amadivayisi awakwazi ukuxhuma"</string>
-    <string name="no_upstream_notification_disable_button" msgid="5284024068281565456">"Vala ukusebenzisa ifoni njengemodemu"</string>
-    <string name="upstream_roaming_notification_title" msgid="186331286017243006">"I-hotspot noma ukusebenzisa ifoni njengemodemu kuvuliwe"</string>
-    <string name="upstream_roaming_notification_message" msgid="7692641323940316538">"Kungaba nezinkokhelo ezengeziwe uma uzula"</string>
+    <string name="no_upstream_notification_title" msgid="611650570559011140">"Ukusebenzisa ifoni njengemodemu akunayo i-inthanethi"</string>
+    <string name="no_upstream_notification_message" msgid="6508394877641864863">"Amadivayisi awakwazi ukuxhumeka"</string>
+    <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"Vala ukusebenzisa ifoni njengemodemu"</string>
+    <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"I-hotspot noma ukusebenzisa ifoni njengemodemu kuvuliwe"</string>
+    <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"Kungaba nezinkokhelo ezengeziwe uma uzula"</string>
 </resources>
diff --git a/Tethering/res/values-mk/strings.xml b/Tethering/res/values-mk/strings.xml
index f1b15e6..9ad9b9a 100644
--- a/Tethering/res/values-mk/strings.xml
+++ b/Tethering/res/values-mk/strings.xml
@@ -16,14 +16,14 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="tethered_notification_title" msgid="5350162111436634622">"Активно: интернет преку мобилен или точка на пристап"</string>
-    <string name="tethered_notification_message" msgid="2338023450330652098">"Допрете за поставување."</string>
-    <string name="disable_tether_notification_title" msgid="3183576627492925522">"Интернетот преку мобилен е оневозможен"</string>
-    <string name="disable_tether_notification_message" msgid="6655882039707534929">"За детали, контактирајте со администраторот"</string>
-    <string name="notification_channel_tethering_status" msgid="7030733422705019001">"Статус на точка на пристап и интернет преку мобилен"</string>
-    <string name="no_upstream_notification_title" msgid="2052743091868702475"></string>
-    <string name="no_upstream_notification_message" msgid="6932020551635470134"></string>
-    <string name="no_upstream_notification_disable_button" msgid="8836277213343697023"></string>
-    <string name="upstream_roaming_notification_title" msgid="8614262557406849762"></string>
-    <string name="upstream_roaming_notification_message" msgid="5999740876323106599"></string>
+    <string name="tethered_notification_title" msgid="6426563586025792944">"Активно е врзување или точка на пристап"</string>
+    <string name="tethered_notification_message" msgid="64800879503420696">"Допрете за поставување."</string>
+    <string name="disable_tether_notification_title" msgid="3004509127903564191">"Врзувањето е оневозможено"</string>
+    <string name="disable_tether_notification_message" msgid="6717523799293901476">"Контактирајте со администраторот за детали"</string>
+    <string name="notification_channel_tethering_status" msgid="2663463891530932727">"Статус на точката на пристап и врзувањето"</string>
+    <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+    <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+    <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+    <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+    <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
 </resources>
diff --git a/Tethering/res/values-ml/strings.xml b/Tethering/res/values-ml/strings.xml
index 8182b11..9db79ce 100644
--- a/Tethering/res/values-ml/strings.xml
+++ b/Tethering/res/values-ml/strings.xml
@@ -16,14 +16,14 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="tethered_notification_title" msgid="5350162111436634622">"ടെതറിംഗ് അല്ലെങ്കിൽ ഹോട്ട്സ്‌പോട്ട് സജീവമാണ്"</string>
-    <string name="tethered_notification_message" msgid="2338023450330652098">"സജ്ജീകരിക്കാൻ ടാപ്പ് ചെയ്യുക."</string>
-    <string name="disable_tether_notification_title" msgid="3183576627492925522">"ടെതറിംഗ് പ്രവർത്തനരഹിതമാക്കിയിരിക്കുന്നു"</string>
-    <string name="disable_tether_notification_message" msgid="6655882039707534929">"വിശദാംശങ്ങൾക്ക് നിങ്ങളുടെ അഡ്മിനെ ബന്ധപ്പെടുക"</string>
-    <string name="notification_channel_tethering_status" msgid="7030733422705019001">"ഹോട്ട്‌സ്പോട്ടിന്റെയും ടെതറിംഗിന്റെയും നില"</string>
-    <string name="no_upstream_notification_title" msgid="2052743091868702475"></string>
-    <string name="no_upstream_notification_message" msgid="6932020551635470134"></string>
-    <string name="no_upstream_notification_disable_button" msgid="8836277213343697023"></string>
-    <string name="upstream_roaming_notification_title" msgid="8614262557406849762"></string>
-    <string name="upstream_roaming_notification_message" msgid="5999740876323106599"></string>
+    <string name="tethered_notification_title" msgid="6426563586025792944">"ടെതറിംഗ് അല്ലെങ്കിൽ ഹോട്ട്സ്‌പോട്ട് സജീവമാണ്"</string>
+    <string name="tethered_notification_message" msgid="64800879503420696">"സജ്ജീകരിക്കാൻ ടാപ്പ് ചെയ്യുക."</string>
+    <string name="disable_tether_notification_title" msgid="3004509127903564191">"ടെതറിംഗ് പ്രവർത്തനരഹിതമാക്കിയിരിക്കുന്നു"</string>
+    <string name="disable_tether_notification_message" msgid="6717523799293901476">"വിശദാംശങ്ങൾക്ക് നിങ്ങളുടെ അഡ്മിനെ ബന്ധപ്പെടുക"</string>
+    <string name="notification_channel_tethering_status" msgid="2663463891530932727">"ഹോട്ട്‌സ്പോട്ടിന്റെയും ടെതറിംഗിന്റെയും നില"</string>
+    <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+    <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+    <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+    <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+    <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
 </resources>
diff --git a/Tethering/res/values-mn/strings.xml b/Tethering/res/values-mn/strings.xml
index a9aef5c..42d1edb 100644
--- a/Tethering/res/values-mn/strings.xml
+++ b/Tethering/res/values-mn/strings.xml
@@ -16,14 +16,14 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="tethered_notification_title" msgid="5350162111436634622">"Модем болгох эсвэл сүлжээний цэг идэвхтэй байна"</string>
-    <string name="tethered_notification_message" msgid="2338023450330652098">"Тохируулахын тулд товшино уу."</string>
-    <string name="disable_tether_notification_title" msgid="3183576627492925522">"Модем болгохыг идэвхгүй болгосон"</string>
-    <string name="disable_tether_notification_message" msgid="6655882039707534929">"Дэлгэрэнгүй мэдээлэл авах бол админтайгаа холбогдоно уу"</string>
-    <string name="notification_channel_tethering_status" msgid="7030733422705019001">"Сүлжээний цэг болон модем болгохын төлөв"</string>
-    <string name="no_upstream_notification_title" msgid="2052743091868702475"></string>
-    <string name="no_upstream_notification_message" msgid="6932020551635470134"></string>
-    <string name="no_upstream_notification_disable_button" msgid="8836277213343697023"></string>
-    <string name="upstream_roaming_notification_title" msgid="8614262557406849762"></string>
-    <string name="upstream_roaming_notification_message" msgid="5999740876323106599"></string>
+    <string name="tethered_notification_title" msgid="6426563586025792944">"Модем болгох эсвэл сүлжээний цэг идэвхтэй байна"</string>
+    <string name="tethered_notification_message" msgid="64800879503420696">"Тохируулахын тулд товшино уу."</string>
+    <string name="disable_tether_notification_title" msgid="3004509127903564191">"Модем болгохыг идэвхгүй болгосон"</string>
+    <string name="disable_tether_notification_message" msgid="6717523799293901476">"Дэлгэрэнгүй мэдээлэл авахын тулд админтайгаа холбогдоно уу"</string>
+    <string name="notification_channel_tethering_status" msgid="2663463891530932727">"Сүлжээний цэг болон модем болгох төлөв"</string>
+    <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+    <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+    <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+    <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+    <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
 </resources>
diff --git a/Tethering/res/values-mr/strings.xml b/Tethering/res/values-mr/strings.xml
index d49cc61..13995b6 100644
--- a/Tethering/res/values-mr/strings.xml
+++ b/Tethering/res/values-mr/strings.xml
@@ -16,14 +16,14 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="tethered_notification_title" msgid="5350162111436634622">"टेदरिंग किंवा हॉटस्पॉट अ‍ॅक्टिव्ह आहे"</string>
-    <string name="tethered_notification_message" msgid="2338023450330652098">"सेट करण्यासाठी टॅप करा."</string>
-    <string name="disable_tether_notification_title" msgid="3183576627492925522">"टेदरिंग बंद केले आहे"</string>
-    <string name="disable_tether_notification_message" msgid="6655882039707534929">"तपशिलांसाठी तुमच्या ॲडमिनशी संपर्क साधा"</string>
-    <string name="notification_channel_tethering_status" msgid="7030733422705019001">"हॉटस्पॉट &amp; टेदरिंग स्टेटस"</string>
-    <string name="no_upstream_notification_title" msgid="2052743091868702475"></string>
-    <string name="no_upstream_notification_message" msgid="6932020551635470134"></string>
-    <string name="no_upstream_notification_disable_button" msgid="8836277213343697023"></string>
-    <string name="upstream_roaming_notification_title" msgid="8614262557406849762"></string>
-    <string name="upstream_roaming_notification_message" msgid="5999740876323106599"></string>
+    <string name="tethered_notification_title" msgid="6426563586025792944">"टेदरिंग किंवा हॉटस्पॉट अ‍ॅक्टिव्ह आहे"</string>
+    <string name="tethered_notification_message" msgid="64800879503420696">"सेट करण्यासाठी टॅप करा."</string>
+    <string name="disable_tether_notification_title" msgid="3004509127903564191">"टेदरिंग बंद केले आहे"</string>
+    <string name="disable_tether_notification_message" msgid="6717523799293901476">"तपशीलांसाठी तुमच्या ॲडमिनशी संपर्क साधा"</string>
+    <string name="notification_channel_tethering_status" msgid="2663463891530932727">"हॉटस्पॉट आणि टेदरिंगची स्थिती"</string>
+    <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+    <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+    <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+    <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+    <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
 </resources>
diff --git a/Tethering/res/values-ms/strings.xml b/Tethering/res/values-ms/strings.xml
index bc7aab3..d6a67f3 100644
--- a/Tethering/res/values-ms/strings.xml
+++ b/Tethering/res/values-ms/strings.xml
@@ -16,14 +16,14 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="tethered_notification_title" msgid="5350162111436634622">"Penambatan atau tempat liputan aktif"</string>
-    <string name="tethered_notification_message" msgid="2338023450330652098">"Ketik untuk membuat persediaan."</string>
-    <string name="disable_tether_notification_title" msgid="3183576627492925522">"Penambatan dilumpuhkan"</string>
-    <string name="disable_tether_notification_message" msgid="6655882039707534929">"Hubungi pentadbir anda untuk mendapatkan maklumat lanjut"</string>
-    <string name="notification_channel_tethering_status" msgid="7030733422705019001">"Status tempat liputan &amp; penambatan"</string>
-    <string name="no_upstream_notification_title" msgid="2052743091868702475"></string>
-    <string name="no_upstream_notification_message" msgid="6932020551635470134"></string>
-    <string name="no_upstream_notification_disable_button" msgid="8836277213343697023"></string>
-    <string name="upstream_roaming_notification_title" msgid="8614262557406849762"></string>
-    <string name="upstream_roaming_notification_message" msgid="5999740876323106599"></string>
+    <string name="tethered_notification_title" msgid="6426563586025792944">"Penambatan atau tempat liputan aktif"</string>
+    <string name="tethered_notification_message" msgid="64800879503420696">"Ketik untuk membuat persediaan."</string>
+    <string name="disable_tether_notification_title" msgid="3004509127903564191">"Penambatan dilumpuhkan"</string>
+    <string name="disable_tether_notification_message" msgid="6717523799293901476">"Hubungi pentadbir anda untuk mendapatkan maklumat lanjut"</string>
+    <string name="notification_channel_tethering_status" msgid="2663463891530932727">"Status tempat liputan &amp; penambatan"</string>
+    <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+    <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+    <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+    <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+    <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
 </resources>
diff --git a/Tethering/res/values-my/strings.xml b/Tethering/res/values-my/strings.xml
index 4f40423..49f6b88 100644
--- a/Tethering/res/values-my/strings.xml
+++ b/Tethering/res/values-my/strings.xml
@@ -16,14 +16,14 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="tethered_notification_title" msgid="5350162111436634622">"မိုဘိုင်းသုံး၍ ချိတ်ဆက်ခြင်း (သို့) ဟော့စပေါ့ ဖွင့်ထားသည်"</string>
-    <string name="tethered_notification_message" msgid="2338023450330652098">"စနစ်ထည့်သွင်းရန် တို့ပါ။"</string>
-    <string name="disable_tether_notification_title" msgid="3183576627492925522">"မိုဘိုင်းသုံး၍ ချိတ်ဆက်ခြင်း ပိတ်ထားသည်"</string>
-    <string name="disable_tether_notification_message" msgid="6655882039707534929">"အသေးစိတ်သိရန် သင့်စီမံခန့်ခွဲသူထံ ဆက်သွယ်ပါ"</string>
-    <string name="notification_channel_tethering_status" msgid="7030733422705019001">"ဟော့စပေါ့နှင့် မိုဘိုင်းသုံး၍ ချိတ်ဆက်ခြင်း အခြေအနေ"</string>
-    <string name="no_upstream_notification_title" msgid="2052743091868702475"></string>
-    <string name="no_upstream_notification_message" msgid="6932020551635470134"></string>
-    <string name="no_upstream_notification_disable_button" msgid="8836277213343697023"></string>
-    <string name="upstream_roaming_notification_title" msgid="8614262557406849762"></string>
-    <string name="upstream_roaming_notification_message" msgid="5999740876323106599"></string>
+    <string name="tethered_notification_title" msgid="6426563586025792944">"မိုဘိုင်းဖုန်းသုံး ချိတ်ဆက်မျှဝေခြင်း သို့မဟုတ် ဟော့စပေါ့ ဖွင့်ထားသည်"</string>
+    <string name="tethered_notification_message" msgid="64800879503420696">"စနစ်ထည့်သွင်းရန် တို့ပါ။"</string>
+    <string name="disable_tether_notification_title" msgid="3004509127903564191">"မိုဘိုင်းဖုန်းသုံး ချိတ်ဆက်မျှဝေခြင်းကို ပိတ်ထားသည်"</string>
+    <string name="disable_tether_notification_message" msgid="6717523799293901476">"အသေးစိတ်အတွက် သင့်စီမံခန့်ခွဲသူကို ဆက်သွယ်ပါ"</string>
+    <string name="notification_channel_tethering_status" msgid="2663463891530932727">"ဟော့စပေါ့နှင့် မိုဘိုင်းဖုန်းသုံး ချိတ်ဆက်မျှဝေခြင်း အခြေအနေ"</string>
+    <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+    <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+    <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+    <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+    <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
 </resources>
diff --git a/Tethering/res/values-nb/strings.xml b/Tethering/res/values-nb/strings.xml
index e9024c0..9594e0a 100644
--- a/Tethering/res/values-nb/strings.xml
+++ b/Tethering/res/values-nb/strings.xml
@@ -16,14 +16,14 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="tethered_notification_title" msgid="5350162111436634622">"Internettdeling eller wifi-sone er aktiv"</string>
-    <string name="tethered_notification_message" msgid="2338023450330652098">"Trykk for å konfigurere."</string>
-    <string name="disable_tether_notification_title" msgid="3183576627492925522">"Internettdeling er slått av"</string>
-    <string name="disable_tether_notification_message" msgid="6655882039707534929">"Kontakt administratoren din for å få mer informasjon"</string>
-    <string name="notification_channel_tethering_status" msgid="7030733422705019001">"Status for wifi-sone og internettdeling"</string>
-    <string name="no_upstream_notification_title" msgid="2052743091868702475"></string>
-    <string name="no_upstream_notification_message" msgid="6932020551635470134"></string>
-    <string name="no_upstream_notification_disable_button" msgid="8836277213343697023"></string>
-    <string name="upstream_roaming_notification_title" msgid="8614262557406849762"></string>
-    <string name="upstream_roaming_notification_message" msgid="5999740876323106599"></string>
+    <string name="tethered_notification_title" msgid="6426563586025792944">"Internettdeling eller Wi-Fi-sone er aktiv"</string>
+    <string name="tethered_notification_message" msgid="64800879503420696">"Trykk for å konfigurere."</string>
+    <string name="disable_tether_notification_title" msgid="3004509127903564191">"Internettdeling er slått av"</string>
+    <string name="disable_tether_notification_message" msgid="6717523799293901476">"Ta kontakt med administratoren din for å få mer informasjon"</string>
+    <string name="notification_channel_tethering_status" msgid="2663463891530932727">"Status for Wi-Fi-sone og internettdeling"</string>
+    <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+    <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+    <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+    <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+    <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
 </resources>
diff --git a/Tethering/res/values-ne/strings.xml b/Tethering/res/values-ne/strings.xml
index 988d5c2..72ae3a8 100644
--- a/Tethering/res/values-ne/strings.xml
+++ b/Tethering/res/values-ne/strings.xml
@@ -16,14 +16,14 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="tethered_notification_title" msgid="5350162111436634622">"टेदरिङ वा हटस्पट अन छ"</string>
-    <string name="tethered_notification_message" msgid="2338023450330652098">"सेटअप गर्न ट्याप गर्नुहोस्।"</string>
-    <string name="disable_tether_notification_title" msgid="3183576627492925522">"टेदरिङ सुविधा अफ गरिएको छ"</string>
-    <string name="disable_tether_notification_message" msgid="6655882039707534929">"विस्तृत जानकारीका लागि एड्मिनलाई सम्पर्क गर्नुहोस्"</string>
-    <string name="notification_channel_tethering_status" msgid="7030733422705019001">"हटस्पट तथा टेदरिङको स्थिति"</string>
-    <string name="no_upstream_notification_title" msgid="2052743091868702475"></string>
-    <string name="no_upstream_notification_message" msgid="6932020551635470134"></string>
-    <string name="no_upstream_notification_disable_button" msgid="8836277213343697023"></string>
-    <string name="upstream_roaming_notification_title" msgid="8614262557406849762"></string>
-    <string name="upstream_roaming_notification_message" msgid="5999740876323106599"></string>
+    <string name="tethered_notification_title" msgid="6426563586025792944">"टेदरिङ वा हटस्पट सक्रिय छ"</string>
+    <string name="tethered_notification_message" msgid="64800879503420696">"सेटअप गर्न ट्याप गर्नुहोस्।"</string>
+    <string name="disable_tether_notification_title" msgid="3004509127903564191">"टेदरिङ सुविधा असक्षम पारिएको छ"</string>
+    <string name="disable_tether_notification_message" msgid="6717523799293901476">"विवरणहरूका लागि आफ्ना प्रशासकलाई सम्पर्क गर्नुहोस्"</string>
+    <string name="notification_channel_tethering_status" msgid="2663463891530932727">"हटस्पट तथा टेदरिङको स्थिति"</string>
+    <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+    <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+    <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+    <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+    <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
 </resources>
diff --git a/Tethering/res/values-nl/strings.xml b/Tethering/res/values-nl/strings.xml
index d6a0a1a..18b2bbf 100644
--- a/Tethering/res/values-nl/strings.xml
+++ b/Tethering/res/values-nl/strings.xml
@@ -16,14 +16,14 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="tethered_notification_title" msgid="5350162111436634622">"Tethering of hotspot actief"</string>
-    <string name="tethered_notification_message" msgid="2338023450330652098">"Tik om in te stellen."</string>
-    <string name="disable_tether_notification_title" msgid="3183576627492925522">"Tethering staat uit"</string>
-    <string name="disable_tether_notification_message" msgid="6655882039707534929">"Neem contact op met je beheerder voor meer informatie"</string>
-    <string name="notification_channel_tethering_status" msgid="7030733422705019001">"Status van hotspot en tethering"</string>
-    <string name="no_upstream_notification_title" msgid="2052743091868702475"></string>
-    <string name="no_upstream_notification_message" msgid="6932020551635470134"></string>
-    <string name="no_upstream_notification_disable_button" msgid="8836277213343697023"></string>
-    <string name="upstream_roaming_notification_title" msgid="8614262557406849762"></string>
-    <string name="upstream_roaming_notification_message" msgid="5999740876323106599"></string>
+    <string name="tethered_notification_title" msgid="6426563586025792944">"Tethering of hotspot actief"</string>
+    <string name="tethered_notification_message" msgid="64800879503420696">"Tik om in te stellen."</string>
+    <string name="disable_tether_notification_title" msgid="3004509127903564191">"Tethering is uitgeschakeld"</string>
+    <string name="disable_tether_notification_message" msgid="6717523799293901476">"Neem contact op met je beheerder voor meer informatie"</string>
+    <string name="notification_channel_tethering_status" msgid="2663463891530932727">"Status van hotspot en tethering"</string>
+    <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+    <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+    <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+    <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+    <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
 </resources>
diff --git a/Tethering/res/values-or/strings.xml b/Tethering/res/values-or/strings.xml
index 9abca6c..a15a6db 100644
--- a/Tethering/res/values-or/strings.xml
+++ b/Tethering/res/values-or/strings.xml
@@ -16,14 +16,14 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="tethered_notification_title" msgid="5350162111436634622">"ଟିଥରିଂ କିମ୍ବା ହଟସ୍ପଟ ସକ୍ରିୟ ଅଛି"</string>
-    <string name="tethered_notification_message" msgid="2338023450330652098">"ସେଟ ଅପ କରିବାକୁ ଟାପ କରନ୍ତୁ।"</string>
-    <string name="disable_tether_notification_title" msgid="3183576627492925522">"ଟିଥରିଂକୁ ଅକ୍ଷମ କରାଯାଇଛି"</string>
-    <string name="disable_tether_notification_message" msgid="6655882039707534929">"ବିବରଣୀ ପାଇଁ ଆପଣଙ୍କ ଆଡମିନଙ୍କ ସହ କଣ୍ଟାକ୍ଟ କରନ୍ତୁ"</string>
-    <string name="notification_channel_tethering_status" msgid="7030733422705019001">"ହଟସ୍ପଟ ଏବଂ ଟିଥରିଂ ସ୍ଥିତି"</string>
-    <string name="no_upstream_notification_title" msgid="2052743091868702475"></string>
-    <string name="no_upstream_notification_message" msgid="6932020551635470134"></string>
-    <string name="no_upstream_notification_disable_button" msgid="8836277213343697023"></string>
-    <string name="upstream_roaming_notification_title" msgid="8614262557406849762"></string>
-    <string name="upstream_roaming_notification_message" msgid="5999740876323106599"></string>
+    <string name="tethered_notification_title" msgid="6426563586025792944">"ଟିଥେରିଂ କିମ୍ୱା ହଟସ୍ପଟ୍ ସକ୍ରିୟ ଅଛି"</string>
+    <string name="tethered_notification_message" msgid="64800879503420696">"ସେଟ୍ ଅପ୍ କରିବାକୁ ଟାପ୍ କରନ୍ତୁ।"</string>
+    <string name="disable_tether_notification_title" msgid="3004509127903564191">"ଟିଥେରିଂ ଅକ୍ଷମ କରାଯାଇଛି"</string>
+    <string name="disable_tether_notification_message" msgid="6717523799293901476">"ବିବରଣୀଗୁଡ଼ିକ ପାଇଁ ଆପଣଙ୍କ ଆଡମିନଙ୍କ ସହ ଯୋଗାଯୋଗ କରନ୍ତୁ"</string>
+    <string name="notification_channel_tethering_status" msgid="2663463891530932727">"ହଟସ୍ପଟ୍ ଓ ଟିଥେରିଂ ସ୍ଥିତି"</string>
+    <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+    <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+    <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+    <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+    <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
 </resources>
diff --git a/Tethering/res/values-pa/strings.xml b/Tethering/res/values-pa/strings.xml
index bcd1c14..a8235e4 100644
--- a/Tethering/res/values-pa/strings.xml
+++ b/Tethering/res/values-pa/strings.xml
@@ -16,14 +16,14 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="tethered_notification_title" msgid="5350162111436634622">"ਟੈਦਰਿੰਗ ਜਾਂ ਹੌਟਸਪੌਟ ਕਿਰਿਆਸ਼ੀਲ"</string>
-    <string name="tethered_notification_message" msgid="2338023450330652098">"ਸੈੱਟਅੱਪ ਕਰਨ ਲਈ ਟੈਪ ਕਰੋ।"</string>
-    <string name="disable_tether_notification_title" msgid="3183576627492925522">"ਟੈਦਰਿੰਗ ਨੂੰ ਬੰਦ ਕੀਤਾ ਗਿਆ ਹੈ"</string>
-    <string name="disable_tether_notification_message" msgid="6655882039707534929">"ਵੇਰਵਿਆਂ ਲਈ ਆਪਣੇ ਪ੍ਰਸ਼ਾਸਕ ਨਾਲ ਸੰਪਰਕ ਕਰੋ"</string>
-    <string name="notification_channel_tethering_status" msgid="7030733422705019001">"ਹੌਟਸਪੌਟ ਅਤੇ ਟੈਦਰਿੰਗ ਦੀ ਸਥਿਤੀ"</string>
-    <string name="no_upstream_notification_title" msgid="2052743091868702475"></string>
-    <string name="no_upstream_notification_message" msgid="6932020551635470134"></string>
-    <string name="no_upstream_notification_disable_button" msgid="8836277213343697023"></string>
-    <string name="upstream_roaming_notification_title" msgid="8614262557406849762"></string>
-    <string name="upstream_roaming_notification_message" msgid="5999740876323106599"></string>
+    <string name="tethered_notification_title" msgid="6426563586025792944">"ਟੈਦਰਿੰਗ ਜਾਂ ਹੌਟਸਪੌਟ ਕਿਰਿਆਸ਼ੀਲ"</string>
+    <string name="tethered_notification_message" msgid="64800879503420696">"ਸੈੱਟਅੱਪ ਕਰਨ ਲਈ ਟੈਪ ਕਰੋ।"</string>
+    <string name="disable_tether_notification_title" msgid="3004509127903564191">"ਟੈਦਰਿੰਗ ਨੂੰ ਬੰਦ ਕੀਤਾ ਗਿਆ ਹੈ"</string>
+    <string name="disable_tether_notification_message" msgid="6717523799293901476">"ਵੇਰਵਿਆਂ ਲਈ ਆਪਣੇ ਪ੍ਰਸ਼ਾਸਕ ਨਾਲ ਸੰਪਰਕ ਕਰੋ"</string>
+    <string name="notification_channel_tethering_status" msgid="2663463891530932727">"ਹੌਟਸਪੌਟ ਅਤੇ ਟੈਦਰਿੰਗ ਦੀ ਸਥਿਤੀ"</string>
+    <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+    <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+    <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+    <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+    <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
 </resources>
diff --git a/Tethering/res/values-pl/strings.xml b/Tethering/res/values-pl/strings.xml
index 855afb4..ccb017d 100644
--- a/Tethering/res/values-pl/strings.xml
+++ b/Tethering/res/values-pl/strings.xml
@@ -16,14 +16,14 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="tethered_notification_title" msgid="5350162111436634622">"Aktywny tethering lub hotspot"</string>
-    <string name="tethered_notification_message" msgid="2338023450330652098">"Kliknij, aby skonfigurować."</string>
-    <string name="disable_tether_notification_title" msgid="3183576627492925522">"Tethering jest wyłączony"</string>
-    <string name="disable_tether_notification_message" msgid="6655882039707534929">"Aby uzyskać szczegółowe informacje, skontaktuj się z administratorem"</string>
-    <string name="notification_channel_tethering_status" msgid="7030733422705019001">"Stan hotspotu i tetheringu"</string>
-    <string name="no_upstream_notification_title" msgid="2052743091868702475"></string>
-    <string name="no_upstream_notification_message" msgid="6932020551635470134"></string>
-    <string name="no_upstream_notification_disable_button" msgid="8836277213343697023"></string>
-    <string name="upstream_roaming_notification_title" msgid="8614262557406849762"></string>
-    <string name="upstream_roaming_notification_message" msgid="5999740876323106599"></string>
+    <string name="tethered_notification_title" msgid="6426563586025792944">"Aktywny tethering lub punkt dostępu"</string>
+    <string name="tethered_notification_message" msgid="64800879503420696">"Kliknij, by skonfigurować"</string>
+    <string name="disable_tether_notification_title" msgid="3004509127903564191">"Tethering został wyłączony"</string>
+    <string name="disable_tether_notification_message" msgid="6717523799293901476">"Aby uzyskać szczegółowe informacje, skontaktuj się z administratorem"</string>
+    <string name="notification_channel_tethering_status" msgid="2663463891530932727">"Hotspot i tethering – stan"</string>
+    <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+    <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+    <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+    <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+    <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
 </resources>
diff --git a/Tethering/res/values-pt-rBR/strings.xml b/Tethering/res/values-pt-rBR/strings.xml
index 7e19f0e..a0a4745 100644
--- a/Tethering/res/values-pt-rBR/strings.xml
+++ b/Tethering/res/values-pt-rBR/strings.xml
@@ -16,14 +16,14 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="tethered_notification_title" msgid="5350162111436634622">"Ponto de acesso ou tethering ativo"</string>
-    <string name="tethered_notification_message" msgid="2338023450330652098">"Toque para configurar."</string>
-    <string name="disable_tether_notification_title" msgid="3183576627492925522">"O tethering está desativado"</string>
-    <string name="disable_tether_notification_message" msgid="6655882039707534929">"Entre em contato com seu administrador para saber detalhes"</string>
-    <string name="notification_channel_tethering_status" msgid="7030733422705019001">"Status do ponto de acesso e do tethering"</string>
-    <string name="no_upstream_notification_title" msgid="2052743091868702475"></string>
-    <string name="no_upstream_notification_message" msgid="6932020551635470134"></string>
-    <string name="no_upstream_notification_disable_button" msgid="8836277213343697023"></string>
-    <string name="upstream_roaming_notification_title" msgid="8614262557406849762"></string>
-    <string name="upstream_roaming_notification_message" msgid="5999740876323106599"></string>
+    <string name="tethered_notification_title" msgid="6426563586025792944">"Ponto de acesso ou tethering ativo"</string>
+    <string name="tethered_notification_message" msgid="64800879503420696">"Toque para configurar."</string>
+    <string name="disable_tether_notification_title" msgid="3004509127903564191">"Tethering desativado"</string>
+    <string name="disable_tether_notification_message" msgid="6717523799293901476">"Fale com seu administrador para saber detalhes"</string>
+    <string name="notification_channel_tethering_status" msgid="2663463891530932727">"Status de ponto de acesso e tethering"</string>
+    <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+    <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+    <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+    <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+    <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
 </resources>
diff --git a/Tethering/res/values-pt-rPT/strings.xml b/Tethering/res/values-pt-rPT/strings.xml
index ac8ea5c..e3f03fc 100644
--- a/Tethering/res/values-pt-rPT/strings.xml
+++ b/Tethering/res/values-pt-rPT/strings.xml
@@ -16,14 +16,14 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="tethered_notification_title" msgid="5350162111436634622">"Ligação (à Internet) via telemóvel ou zona Wi-Fi ativa"</string>
-    <string name="tethered_notification_message" msgid="2338023450330652098">"Toque para configurar."</string>
-    <string name="disable_tether_notification_title" msgid="3183576627492925522">"A ligação (à Internet) via telemóvel está desativada"</string>
-    <string name="disable_tether_notification_message" msgid="6655882039707534929">"Contacte o administrador para obter detalhes"</string>
-    <string name="notification_channel_tethering_status" msgid="7030733422705019001">"Estado da zona Wi-Fi e da ligação (à Internet) via telemóvel"</string>
-    <string name="no_upstream_notification_title" msgid="2052743091868702475"></string>
-    <string name="no_upstream_notification_message" msgid="6932020551635470134"></string>
-    <string name="no_upstream_notification_disable_button" msgid="8836277213343697023"></string>
-    <string name="upstream_roaming_notification_title" msgid="8614262557406849762"></string>
-    <string name="upstream_roaming_notification_message" msgid="5999740876323106599"></string>
+    <string name="tethered_notification_title" msgid="6426563586025792944">"Ligação (à Internet) via telemóvel ou zona Wi-Fi ativas"</string>
+    <string name="tethered_notification_message" msgid="64800879503420696">"Toque para configurar."</string>
+    <string name="disable_tether_notification_title" msgid="3004509127903564191">"A ligação (à Internet) via telemóvel está desativada."</string>
+    <string name="disable_tether_notification_message" msgid="6717523799293901476">"Contacte o administrador para obter detalhes."</string>
+    <string name="notification_channel_tethering_status" msgid="2663463891530932727">"Estado da zona Wi-Fi e da ligação (à Internet) via telemóvel"</string>
+    <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+    <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+    <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+    <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+    <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
 </resources>
diff --git a/Tethering/res/values-pt/strings.xml b/Tethering/res/values-pt/strings.xml
index 7e19f0e..a0a4745 100644
--- a/Tethering/res/values-pt/strings.xml
+++ b/Tethering/res/values-pt/strings.xml
@@ -16,14 +16,14 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="tethered_notification_title" msgid="5350162111436634622">"Ponto de acesso ou tethering ativo"</string>
-    <string name="tethered_notification_message" msgid="2338023450330652098">"Toque para configurar."</string>
-    <string name="disable_tether_notification_title" msgid="3183576627492925522">"O tethering está desativado"</string>
-    <string name="disable_tether_notification_message" msgid="6655882039707534929">"Entre em contato com seu administrador para saber detalhes"</string>
-    <string name="notification_channel_tethering_status" msgid="7030733422705019001">"Status do ponto de acesso e do tethering"</string>
-    <string name="no_upstream_notification_title" msgid="2052743091868702475"></string>
-    <string name="no_upstream_notification_message" msgid="6932020551635470134"></string>
-    <string name="no_upstream_notification_disable_button" msgid="8836277213343697023"></string>
-    <string name="upstream_roaming_notification_title" msgid="8614262557406849762"></string>
-    <string name="upstream_roaming_notification_message" msgid="5999740876323106599"></string>
+    <string name="tethered_notification_title" msgid="6426563586025792944">"Ponto de acesso ou tethering ativo"</string>
+    <string name="tethered_notification_message" msgid="64800879503420696">"Toque para configurar."</string>
+    <string name="disable_tether_notification_title" msgid="3004509127903564191">"Tethering desativado"</string>
+    <string name="disable_tether_notification_message" msgid="6717523799293901476">"Fale com seu administrador para saber detalhes"</string>
+    <string name="notification_channel_tethering_status" msgid="2663463891530932727">"Status de ponto de acesso e tethering"</string>
+    <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+    <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+    <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+    <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+    <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
 </resources>
diff --git a/Tethering/res/values-ro/strings.xml b/Tethering/res/values-ro/strings.xml
index e022504..5706a4a 100644
--- a/Tethering/res/values-ro/strings.xml
+++ b/Tethering/res/values-ro/strings.xml
@@ -16,14 +16,14 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="tethered_notification_title" msgid="5350162111436634622">"Tethering sau hotspot activ"</string>
-    <string name="tethered_notification_message" msgid="2338023450330652098">"Atinge pentru a configura."</string>
-    <string name="disable_tether_notification_title" msgid="3183576627492925522">"Tetheringul este dezactivat"</string>
-    <string name="disable_tether_notification_message" msgid="6655882039707534929">"Contactează administratorul pentru detalii"</string>
-    <string name="notification_channel_tethering_status" msgid="7030733422705019001">"Starea hotspotului și a tetheringului"</string>
-    <string name="no_upstream_notification_title" msgid="2052743091868702475"></string>
-    <string name="no_upstream_notification_message" msgid="6932020551635470134"></string>
-    <string name="no_upstream_notification_disable_button" msgid="8836277213343697023"></string>
-    <string name="upstream_roaming_notification_title" msgid="8614262557406849762"></string>
-    <string name="upstream_roaming_notification_message" msgid="5999740876323106599"></string>
+    <string name="tethered_notification_title" msgid="6426563586025792944">"Tethering sau hotspot activ"</string>
+    <string name="tethered_notification_message" msgid="64800879503420696">"Atingeți ca să configurați."</string>
+    <string name="disable_tether_notification_title" msgid="3004509127903564191">"Tetheringul este dezactivat"</string>
+    <string name="disable_tether_notification_message" msgid="6717523799293901476">"Contactați administratorul pentru detalii"</string>
+    <string name="notification_channel_tethering_status" msgid="2663463891530932727">"Starea hotspotului și a tetheringului"</string>
+    <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+    <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+    <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+    <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+    <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
 </resources>
diff --git a/Tethering/res/values-ru/strings.xml b/Tethering/res/values-ru/strings.xml
index 4361d70..7cb6f7d 100644
--- a/Tethering/res/values-ru/strings.xml
+++ b/Tethering/res/values-ru/strings.xml
@@ -16,14 +16,14 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="tethered_notification_title" msgid="5350162111436634622">"Включен режим модема или точка доступа"</string>
-    <string name="tethered_notification_message" msgid="2338023450330652098">"Нажмите, чтобы настроить."</string>
-    <string name="disable_tether_notification_title" msgid="3183576627492925522">"Включить режим модема нельзя"</string>
-    <string name="disable_tether_notification_message" msgid="6655882039707534929">"Чтобы узнать больше, обратитесь к администратору."</string>
-    <string name="notification_channel_tethering_status" msgid="7030733422705019001">"Статус точки доступа и режима модема"</string>
-    <string name="no_upstream_notification_title" msgid="2052743091868702475"></string>
-    <string name="no_upstream_notification_message" msgid="6932020551635470134"></string>
-    <string name="no_upstream_notification_disable_button" msgid="8836277213343697023"></string>
-    <string name="upstream_roaming_notification_title" msgid="8614262557406849762"></string>
-    <string name="upstream_roaming_notification_message" msgid="5999740876323106599"></string>
+    <string name="tethered_notification_title" msgid="6426563586025792944">"Включен режим модема или точка доступа"</string>
+    <string name="tethered_notification_message" msgid="64800879503420696">"Нажмите, чтобы настроить."</string>
+    <string name="disable_tether_notification_title" msgid="3004509127903564191">"Использование телефона в качестве модема запрещено"</string>
+    <string name="disable_tether_notification_message" msgid="6717523799293901476">"Чтобы узнать подробности, обратитесь к администратору."</string>
+    <string name="notification_channel_tethering_status" msgid="2663463891530932727">"Статус хот-спота и режима модема"</string>
+    <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+    <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+    <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+    <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+    <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
 </resources>
diff --git a/Tethering/res/values-si/strings.xml b/Tethering/res/values-si/strings.xml
index 14f30e9..ec34c22 100644
--- a/Tethering/res/values-si/strings.xml
+++ b/Tethering/res/values-si/strings.xml
@@ -16,14 +16,14 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="tethered_notification_title" msgid="5350162111436634622">"ටෙදරින් හෝ හොට්ස්පොට් සක්‍රියයි"</string>
-    <string name="tethered_notification_message" msgid="2338023450330652098">"පිහිටුවීමට තට්ටු කරන්න."</string>
-    <string name="disable_tether_notification_title" msgid="3183576627492925522">"ටෙදරින් අබල කර ඇත"</string>
-    <string name="disable_tether_notification_message" msgid="6655882039707534929">"විස්තර සඳහා ඔබේ පරිපාලක අමතන්න"</string>
-    <string name="notification_channel_tethering_status" msgid="7030733422705019001">"හොට්ස්පොට් සහ ටෙදරින් තත්ත්වය"</string>
-    <string name="no_upstream_notification_title" msgid="2052743091868702475"></string>
-    <string name="no_upstream_notification_message" msgid="6932020551635470134"></string>
-    <string name="no_upstream_notification_disable_button" msgid="8836277213343697023"></string>
-    <string name="upstream_roaming_notification_title" msgid="8614262557406849762"></string>
-    <string name="upstream_roaming_notification_message" msgid="5999740876323106599"></string>
+    <string name="tethered_notification_title" msgid="6426563586025792944">"ටෙදරින් හෝ හොට්ස්පොට් සක්‍රීයයි"</string>
+    <string name="tethered_notification_message" msgid="64800879503420696">"පිහිටුවීමට තට්ටු කරන්න."</string>
+    <string name="disable_tether_notification_title" msgid="3004509127903564191">"ටෙදරින් අබල කර ඇත"</string>
+    <string name="disable_tether_notification_message" msgid="6717523799293901476">"විස්තර සඳහා ඔබගේ පරිපාලක අමතන්න"</string>
+    <string name="notification_channel_tethering_status" msgid="2663463891530932727">"හොට්ස්පොට් &amp; ටෙදරින් තත්ත්වය"</string>
+    <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+    <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+    <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+    <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+    <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
 </resources>
diff --git a/Tethering/res/values-sk/strings.xml b/Tethering/res/values-sk/strings.xml
index 15845e7..43e787c 100644
--- a/Tethering/res/values-sk/strings.xml
+++ b/Tethering/res/values-sk/strings.xml
@@ -16,14 +16,14 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="tethered_notification_title" msgid="5350162111436634622">"Tethering alebo hotspot je aktívny"</string>
-    <string name="tethered_notification_message" msgid="2338023450330652098">"Nastavíte ho klepnutím."</string>
-    <string name="disable_tether_notification_title" msgid="3183576627492925522">"Tethering je deaktivovaný"</string>
-    <string name="disable_tether_notification_message" msgid="6655882039707534929">"O podrobnosti požiadajte svojho správcu"</string>
-    <string name="notification_channel_tethering_status" msgid="7030733422705019001">"Stav hotspotu a tetheringu"</string>
-    <string name="no_upstream_notification_title" msgid="2052743091868702475"></string>
-    <string name="no_upstream_notification_message" msgid="6932020551635470134"></string>
-    <string name="no_upstream_notification_disable_button" msgid="8836277213343697023"></string>
-    <string name="upstream_roaming_notification_title" msgid="8614262557406849762"></string>
-    <string name="upstream_roaming_notification_message" msgid="5999740876323106599"></string>
+    <string name="tethered_notification_title" msgid="6426563586025792944">"Tethering alebo prístupový bod je aktívny"</string>
+    <string name="tethered_notification_message" msgid="64800879503420696">"Klepnutím prejdete na nastavenie."</string>
+    <string name="disable_tether_notification_title" msgid="3004509127903564191">"Tethering je deaktivovaný"</string>
+    <string name="disable_tether_notification_message" msgid="6717523799293901476">"O podrobnosti požiadajte svojho správcu"</string>
+    <string name="notification_channel_tethering_status" msgid="2663463891530932727">"Stav hotspotu a tetheringu"</string>
+    <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+    <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+    <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+    <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+    <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
 </resources>
diff --git a/Tethering/res/values-sl/strings.xml b/Tethering/res/values-sl/strings.xml
index 4c9bd3c..5943362 100644
--- a/Tethering/res/values-sl/strings.xml
+++ b/Tethering/res/values-sl/strings.xml
@@ -16,14 +16,14 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="tethered_notification_title" msgid="5350162111436634622">"Povezava računalnika z internetom prek mobilnega telefona ali dostopna točka je aktivna."</string>
-    <string name="tethered_notification_message" msgid="2338023450330652098">"Dotaknite se za nastavitev."</string>
-    <string name="disable_tether_notification_title" msgid="3183576627492925522">"Povezava računalnika z internetom prek mobilnega telefona je onemogočena."</string>
-    <string name="disable_tether_notification_message" msgid="6655882039707534929">"Za podrobnosti se obrnite na skrbnika."</string>
-    <string name="notification_channel_tethering_status" msgid="7030733422705019001">"Stanje dostopne točke in povezave računalnika z internetom prek mobilnega telefona"</string>
-    <string name="no_upstream_notification_title" msgid="2052743091868702475"></string>
-    <string name="no_upstream_notification_message" msgid="6932020551635470134"></string>
-    <string name="no_upstream_notification_disable_button" msgid="8836277213343697023"></string>
-    <string name="upstream_roaming_notification_title" msgid="8614262557406849762"></string>
-    <string name="upstream_roaming_notification_message" msgid="5999740876323106599"></string>
+    <string name="tethered_notification_title" msgid="6426563586025792944">"Povezava z internetom prek mobilnega telefona ali dostopna točka je aktivna"</string>
+    <string name="tethered_notification_message" msgid="64800879503420696">"Dotaknite se, če želite nastaviti."</string>
+    <string name="disable_tether_notification_title" msgid="3004509127903564191">"Povezava z internetom prek mobilnega telefona je onemogočena"</string>
+    <string name="disable_tether_notification_message" msgid="6717523799293901476">"Za podrobnosti se obrnite na skrbnika"</string>
+    <string name="notification_channel_tethering_status" msgid="2663463891530932727">"Stanje dostopne točke in povezave z internetom prek mobilnega telefona"</string>
+    <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+    <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+    <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+    <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+    <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
 </resources>
diff --git a/Tethering/res/values-sq/strings.xml b/Tethering/res/values-sq/strings.xml
index e39e98d..21e1155 100644
--- a/Tethering/res/values-sq/strings.xml
+++ b/Tethering/res/values-sq/strings.xml
@@ -16,14 +16,14 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="tethered_notification_title" msgid="5350162111436634622">"Lidhja e çiftimit ose ajo e qasjes në zona publike interneti është aktive"</string>
-    <string name="tethered_notification_message" msgid="2338023450330652098">"Trokit për ta konfiguruar."</string>
-    <string name="disable_tether_notification_title" msgid="3183576627492925522">"Ndarja e internetit është çaktivizuar"</string>
-    <string name="disable_tether_notification_message" msgid="6655882039707534929">"Kontakto me administratorin për detaje"</string>
-    <string name="notification_channel_tethering_status" msgid="7030733422705019001">"Statusi i zonës së qasjes dhe ndarjes së internetit"</string>
-    <string name="no_upstream_notification_title" msgid="2052743091868702475"></string>
-    <string name="no_upstream_notification_message" msgid="6932020551635470134"></string>
-    <string name="no_upstream_notification_disable_button" msgid="8836277213343697023"></string>
-    <string name="upstream_roaming_notification_title" msgid="8614262557406849762"></string>
-    <string name="upstream_roaming_notification_message" msgid="5999740876323106599"></string>
+    <string name="tethered_notification_title" msgid="6426563586025792944">"Ndarja e internetit ose zona e qasjes së internetit është aktive"</string>
+    <string name="tethered_notification_message" msgid="64800879503420696">"Trokit për ta konfiguruar."</string>
+    <string name="disable_tether_notification_title" msgid="3004509127903564191">"Ndarja e internetit është çaktivizuar"</string>
+    <string name="disable_tether_notification_message" msgid="6717523799293901476">"Kontakto me administratorin për detaje"</string>
+    <string name="notification_channel_tethering_status" msgid="2663463891530932727">"Statusi i zonës së qasjes dhe ndarjes së internetit"</string>
+    <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+    <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+    <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+    <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+    <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
 </resources>
diff --git a/Tethering/res/values-sr/strings.xml b/Tethering/res/values-sr/strings.xml
index ca3ba59..e2e4dc6 100644
--- a/Tethering/res/values-sr/strings.xml
+++ b/Tethering/res/values-sr/strings.xml
@@ -16,14 +16,14 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="tethered_notification_title" msgid="5350162111436634622">"Активно је привезивање или хотспот"</string>
-    <string name="tethered_notification_message" msgid="2338023450330652098">"Додирните да бисте подесили."</string>
-    <string name="disable_tether_notification_title" msgid="3183576627492925522">"Привезивање је онемогућено"</string>
-    <string name="disable_tether_notification_message" msgid="6655882039707534929">"Потражите детаље од администратора"</string>
-    <string name="notification_channel_tethering_status" msgid="7030733422705019001">"Статус хотспота и привезивања"</string>
-    <string name="no_upstream_notification_title" msgid="2052743091868702475"></string>
-    <string name="no_upstream_notification_message" msgid="6932020551635470134"></string>
-    <string name="no_upstream_notification_disable_button" msgid="8836277213343697023"></string>
-    <string name="upstream_roaming_notification_title" msgid="8614262557406849762"></string>
-    <string name="upstream_roaming_notification_message" msgid="5999740876323106599"></string>
+    <string name="tethered_notification_title" msgid="6426563586025792944">"Привезивање или хотспот је активан"</string>
+    <string name="tethered_notification_message" msgid="64800879503420696">"Додирните да бисте подесили."</string>
+    <string name="disable_tether_notification_title" msgid="3004509127903564191">"Привезивање је онемогућено"</string>
+    <string name="disable_tether_notification_message" msgid="6717523799293901476">"Потражите детаље од администратора"</string>
+    <string name="notification_channel_tethering_status" msgid="2663463891530932727">"Статус хотспота и привезивања"</string>
+    <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+    <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+    <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+    <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+    <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
 </resources>
diff --git a/Tethering/res/values-sv/strings.xml b/Tethering/res/values-sv/strings.xml
index da5e104..72702c2 100644
--- a/Tethering/res/values-sv/strings.xml
+++ b/Tethering/res/values-sv/strings.xml
@@ -16,14 +16,14 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="tethered_notification_title" msgid="5350162111436634622">"Internetdelning eller surfzon är aktiv"</string>
-    <string name="tethered_notification_message" msgid="2338023450330652098">"Tryck om du vill konfigurera."</string>
-    <string name="disable_tether_notification_title" msgid="3183576627492925522">"Internetdelning har inaktiverats"</string>
-    <string name="disable_tether_notification_message" msgid="6655882039707534929">"Kontakta administratören om du vill veta mer"</string>
-    <string name="notification_channel_tethering_status" msgid="7030733422705019001">"Status för surfzon och internetdelning"</string>
-    <string name="no_upstream_notification_title" msgid="2052743091868702475"></string>
-    <string name="no_upstream_notification_message" msgid="6932020551635470134"></string>
-    <string name="no_upstream_notification_disable_button" msgid="8836277213343697023"></string>
-    <string name="upstream_roaming_notification_title" msgid="8614262557406849762"></string>
-    <string name="upstream_roaming_notification_message" msgid="5999740876323106599"></string>
+    <string name="tethered_notification_title" msgid="6426563586025792944">"Internetdelning eller surfzon har aktiverats"</string>
+    <string name="tethered_notification_message" msgid="64800879503420696">"Tryck om du vill konfigurera."</string>
+    <string name="disable_tether_notification_title" msgid="3004509127903564191">"Internetdelning har inaktiverats"</string>
+    <string name="disable_tether_notification_message" msgid="6717523799293901476">"Kontakta administratören om du vill veta mer"</string>
+    <string name="notification_channel_tethering_status" msgid="2663463891530932727">"Trådlös surfzon och internetdelning har inaktiverats"</string>
+    <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+    <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+    <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+    <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+    <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
 </resources>
diff --git a/Tethering/res/values-sw/strings.xml b/Tethering/res/values-sw/strings.xml
index 3e58667..65e4aa8 100644
--- a/Tethering/res/values-sw/strings.xml
+++ b/Tethering/res/values-sw/strings.xml
@@ -16,14 +16,14 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="tethered_notification_title" msgid="5350162111436634622">"Umewasha kipengele cha kusambaza mtandao au mtandao pepe"</string>
-    <string name="tethered_notification_message" msgid="2338023450330652098">"Gusa ili uweke mipangilio."</string>
-    <string name="disable_tether_notification_title" msgid="3183576627492925522">"Umezima kipengele cha kusambaza mtandao"</string>
-    <string name="disable_tether_notification_message" msgid="6655882039707534929">"Wasiliana na msimamizi wako ili upate maelezo zaidi"</string>
-    <string name="notification_channel_tethering_status" msgid="7030733422705019001">"Mtandaopepe na hali ya kusambaza mtandao"</string>
-    <string name="no_upstream_notification_title" msgid="2052743091868702475"></string>
-    <string name="no_upstream_notification_message" msgid="6932020551635470134"></string>
-    <string name="no_upstream_notification_disable_button" msgid="8836277213343697023"></string>
-    <string name="upstream_roaming_notification_title" msgid="8614262557406849762"></string>
-    <string name="upstream_roaming_notification_message" msgid="5999740876323106599"></string>
+    <string name="tethered_notification_title" msgid="6426563586025792944">"Kusambaza mtandao au mtandaopepe umewashwa"</string>
+    <string name="tethered_notification_message" msgid="64800879503420696">"Gusa ili uweke mipangilio."</string>
+    <string name="disable_tether_notification_title" msgid="3004509127903564191">"Umezima kipengele cha kusambaza mtandao"</string>
+    <string name="disable_tether_notification_message" msgid="6717523799293901476">"Wasiliana na msimamizi wako ili upate maelezo zaidi"</string>
+    <string name="notification_channel_tethering_status" msgid="2663463891530932727">"Mtandaopepe na hali ya kusambaza mtandao"</string>
+    <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+    <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+    <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+    <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+    <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
 </resources>
diff --git a/Tethering/res/values-ta/strings.xml b/Tethering/res/values-ta/strings.xml
index a811e67..4aba62d 100644
--- a/Tethering/res/values-ta/strings.xml
+++ b/Tethering/res/values-ta/strings.xml
@@ -16,14 +16,14 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="tethered_notification_title" msgid="5350162111436634622">"இணைப்பு முறை அல்லது ஹாட்ஸ்பாட் செயல்பாட்டில் உள்ளது"</string>
-    <string name="tethered_notification_message" msgid="2338023450330652098">"அமைக்க தட்டவும்."</string>
-    <string name="disable_tether_notification_title" msgid="3183576627492925522">"இணைப்பு முறை முடக்கப்பட்டுள்ளது"</string>
-    <string name="disable_tether_notification_message" msgid="6655882039707534929">"விவரங்களுக்கு உங்கள் நிர்வாகியைத் தொடர்புகொள்ளவும்"</string>
-    <string name="notification_channel_tethering_status" msgid="7030733422705019001">"ஹாட்ஸ்பாட் &amp; இணைப்பு முறை நிலை"</string>
-    <string name="no_upstream_notification_title" msgid="2052743091868702475"></string>
-    <string name="no_upstream_notification_message" msgid="6932020551635470134"></string>
-    <string name="no_upstream_notification_disable_button" msgid="8836277213343697023"></string>
-    <string name="upstream_roaming_notification_title" msgid="8614262557406849762"></string>
-    <string name="upstream_roaming_notification_message" msgid="5999740876323106599"></string>
+    <string name="tethered_notification_title" msgid="6426563586025792944">"டெதெரிங் அல்லது ஹாட்ஸ்பாட் இயங்குகிறது"</string>
+    <string name="tethered_notification_message" msgid="64800879503420696">"அமைக்க, தட்டவும்."</string>
+    <string name="disable_tether_notification_title" msgid="3004509127903564191">"டெதெரிங் முடக்கப்பட்டுள்ளது"</string>
+    <string name="disable_tether_notification_message" msgid="6717523799293901476">"விவரங்களுக்கு உங்கள் நிர்வாகியைத் தொடர்புகொள்ளவும்"</string>
+    <string name="notification_channel_tethering_status" msgid="2663463891530932727">"ஹாட்ஸ்பாட் &amp; டெதெரிங் நிலை"</string>
+    <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+    <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+    <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+    <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+    <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
 </resources>
diff --git a/Tethering/res/values-te/strings.xml b/Tethering/res/values-te/strings.xml
index a92208d..1f91791 100644
--- a/Tethering/res/values-te/strings.xml
+++ b/Tethering/res/values-te/strings.xml
@@ -16,14 +16,14 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="tethered_notification_title" msgid="5350162111436634622">"టెథరింగ్ లేదా హాట్‌స్పాట్ యాక్టివ్‌గా ఉంది"</string>
-    <string name="tethered_notification_message" msgid="2338023450330652098">"సెటప్ చేయడానికి ట్యాప్ చేయండి."</string>
-    <string name="disable_tether_notification_title" msgid="3183576627492925522">"టెథరింగ్ డిజేబుల్ చేయబడింది"</string>
-    <string name="disable_tether_notification_message" msgid="6655882039707534929">"వివరాల కోసం మీ అడ్మిన్‌ను కాంటాక్ట్ చేయండి"</string>
-    <string name="notification_channel_tethering_status" msgid="7030733422705019001">"హాట్‌స్పాట్ &amp; టెథరింగ్ స్టేటస్"</string>
-    <string name="no_upstream_notification_title" msgid="2052743091868702475"></string>
-    <string name="no_upstream_notification_message" msgid="6932020551635470134"></string>
-    <string name="no_upstream_notification_disable_button" msgid="8836277213343697023"></string>
-    <string name="upstream_roaming_notification_title" msgid="8614262557406849762"></string>
-    <string name="upstream_roaming_notification_message" msgid="5999740876323106599"></string>
+    <string name="tethered_notification_title" msgid="6426563586025792944">"టెథరింగ్ లేదా హాట్‌స్పాట్ యాక్టివ్‌గా ఉంది"</string>
+    <string name="tethered_notification_message" msgid="64800879503420696">"సెటప్ చేయడానికి ట్యాప్ చేయండి."</string>
+    <string name="disable_tether_notification_title" msgid="3004509127903564191">"టెథరింగ్ డిజేబుల్ చేయబడింది"</string>
+    <string name="disable_tether_notification_message" msgid="6717523799293901476">"వివరాల కోసం మీ అడ్మిన్‌ని సంప్రదించండి"</string>
+    <string name="notification_channel_tethering_status" msgid="2663463891530932727">"హాట్‌స్పాట్ &amp; టెథరింగ్ స్థితి"</string>
+    <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+    <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+    <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+    <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+    <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
 </resources>
diff --git a/Tethering/res/values-th/strings.xml b/Tethering/res/values-th/strings.xml
index 5ebbc80..44171c0 100644
--- a/Tethering/res/values-th/strings.xml
+++ b/Tethering/res/values-th/strings.xml
@@ -16,14 +16,14 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="tethered_notification_title" msgid="5350162111436634622">"การเชื่อมต่ออินเทอร์เน็ตผ่านมือถือหรือฮอตสปอตทำงานอยู่"</string>
-    <string name="tethered_notification_message" msgid="2338023450330652098">"แตะเพื่อตั้งค่า"</string>
-    <string name="disable_tether_notification_title" msgid="3183576627492925522">"การเชื่อมต่ออินเทอร์เน็ตผ่านมือถือปิดอยู่"</string>
-    <string name="disable_tether_notification_message" msgid="6655882039707534929">"ติดต่อผู้ดูแลระบบเพื่อขอรายละเอียด"</string>
-    <string name="notification_channel_tethering_status" msgid="7030733422705019001">"สถานะฮอตสปอตและการเชื่อมต่ออินเทอร์เน็ตผ่านมือถือ"</string>
-    <string name="no_upstream_notification_title" msgid="2052743091868702475"></string>
-    <string name="no_upstream_notification_message" msgid="6932020551635470134"></string>
-    <string name="no_upstream_notification_disable_button" msgid="8836277213343697023"></string>
-    <string name="upstream_roaming_notification_title" msgid="8614262557406849762"></string>
-    <string name="upstream_roaming_notification_message" msgid="5999740876323106599"></string>
+    <string name="tethered_notification_title" msgid="6426563586025792944">"การเชื่อมต่ออินเทอร์เน็ตผ่านมือถือหรือฮอตสปอตทำงานอยู่"</string>
+    <string name="tethered_notification_message" msgid="64800879503420696">"แตะเพื่อตั้งค่า"</string>
+    <string name="disable_tether_notification_title" msgid="3004509127903564191">"ปิดใช้การเชื่อมต่ออินเทอร์เน็ตผ่านมือถือแล้ว"</string>
+    <string name="disable_tether_notification_message" msgid="6717523799293901476">"ติดต่อผู้ดูแลระบบเพื่อขอรายละเอียด"</string>
+    <string name="notification_channel_tethering_status" msgid="2663463891530932727">"สถานะฮอตสปอตและการเชื่อมต่ออินเทอร์เน็ตผ่านมือถือ"</string>
+    <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+    <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+    <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+    <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+    <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
 </resources>
diff --git a/Tethering/res/values-tl/strings.xml b/Tethering/res/values-tl/strings.xml
index 3364e52..7347dd3 100644
--- a/Tethering/res/values-tl/strings.xml
+++ b/Tethering/res/values-tl/strings.xml
@@ -16,14 +16,14 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="tethered_notification_title" msgid="5350162111436634622">"Aktibo ang pag-tether o hotspot"</string>
-    <string name="tethered_notification_message" msgid="2338023450330652098">"I-tap para i-set up."</string>
-    <string name="disable_tether_notification_title" msgid="3183576627492925522">"Naka-disable ang pag-tether"</string>
-    <string name="disable_tether_notification_message" msgid="6655882039707534929">"Makipag-ugnayan sa iyong admin para sa mga detalye"</string>
-    <string name="notification_channel_tethering_status" msgid="7030733422705019001">"Status ng hotspot at pag-tether"</string>
-    <string name="no_upstream_notification_title" msgid="2052743091868702475"></string>
-    <string name="no_upstream_notification_message" msgid="6932020551635470134"></string>
-    <string name="no_upstream_notification_disable_button" msgid="8836277213343697023"></string>
-    <string name="upstream_roaming_notification_title" msgid="8614262557406849762"></string>
-    <string name="upstream_roaming_notification_message" msgid="5999740876323106599"></string>
+    <string name="tethered_notification_title" msgid="6426563586025792944">"Aktibo ang pag-tether o hotspot"</string>
+    <string name="tethered_notification_message" msgid="64800879503420696">"I-tap para i-set up."</string>
+    <string name="disable_tether_notification_title" msgid="3004509127903564191">"Naka-disable ang pag-tether"</string>
+    <string name="disable_tether_notification_message" msgid="6717523799293901476">"Makipag-ugnayan sa iyong admin para sa mga detalye"</string>
+    <string name="notification_channel_tethering_status" msgid="2663463891530932727">"Status ng hotspot at pag-tether"</string>
+    <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+    <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+    <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+    <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+    <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
 </resources>
diff --git a/Tethering/res/values-tr/strings.xml b/Tethering/res/values-tr/strings.xml
index 0bb273c..32030f1 100644
--- a/Tethering/res/values-tr/strings.xml
+++ b/Tethering/res/values-tr/strings.xml
@@ -16,14 +16,14 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="tethered_notification_title" msgid="5350162111436634622">"Tethering veya hotspot etkin"</string>
-    <string name="tethered_notification_message" msgid="2338023450330652098">"Ayarlamak için dokunun."</string>
-    <string name="disable_tether_notification_title" msgid="3183576627492925522">"Tethering devre dışı bırakıldı"</string>
-    <string name="disable_tether_notification_message" msgid="6655882039707534929">"Ayrıntılı bilgi için yöneticinize başvurun"</string>
-    <string name="notification_channel_tethering_status" msgid="7030733422705019001">"Hotspot ve tethering durumu"</string>
-    <string name="no_upstream_notification_title" msgid="2052743091868702475"></string>
-    <string name="no_upstream_notification_message" msgid="6932020551635470134"></string>
-    <string name="no_upstream_notification_disable_button" msgid="8836277213343697023"></string>
-    <string name="upstream_roaming_notification_title" msgid="8614262557406849762"></string>
-    <string name="upstream_roaming_notification_message" msgid="5999740876323106599"></string>
+    <string name="tethered_notification_title" msgid="6426563586025792944">"Tethering veya hotspot etkin"</string>
+    <string name="tethered_notification_message" msgid="64800879503420696">"Ayarlamak için dokunun."</string>
+    <string name="disable_tether_notification_title" msgid="3004509127903564191">"Tethering devre dışı bırakıldı"</string>
+    <string name="disable_tether_notification_message" msgid="6717523799293901476">"Ayrıntılı bilgi için yöneticinize başvurun"</string>
+    <string name="notification_channel_tethering_status" msgid="2663463891530932727">"Hotspot ve tethering durumu"</string>
+    <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+    <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+    <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+    <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+    <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
 </resources>
diff --git a/Tethering/res/values-uk/strings.xml b/Tethering/res/values-uk/strings.xml
index 11962e5..1ca89b3 100644
--- a/Tethering/res/values-uk/strings.xml
+++ b/Tethering/res/values-uk/strings.xml
@@ -16,14 +16,14 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="tethered_notification_title" msgid="5350162111436634622">"Модем чи точка доступу активні"</string>
-    <string name="tethered_notification_message" msgid="2338023450330652098">"Натисніть, щоб налаштувати."</string>
-    <string name="disable_tether_notification_title" msgid="3183576627492925522">"Використання телефона як модема вимкнено"</string>
-    <string name="disable_tether_notification_message" msgid="6655882039707534929">"Щоб дізнатися більше, зверніться до адміністратора"</string>
-    <string name="notification_channel_tethering_status" msgid="7030733422705019001">"Статус точки доступу й модема"</string>
-    <string name="no_upstream_notification_title" msgid="2052743091868702475"></string>
-    <string name="no_upstream_notification_message" msgid="6932020551635470134"></string>
-    <string name="no_upstream_notification_disable_button" msgid="8836277213343697023"></string>
-    <string name="upstream_roaming_notification_title" msgid="8614262557406849762"></string>
-    <string name="upstream_roaming_notification_message" msgid="5999740876323106599"></string>
+    <string name="tethered_notification_title" msgid="6426563586025792944">"Модем чи точка доступу активні"</string>
+    <string name="tethered_notification_message" msgid="64800879503420696">"Натисніть, щоб налаштувати."</string>
+    <string name="disable_tether_notification_title" msgid="3004509127903564191">"Використання телефона як модема вимкнено"</string>
+    <string name="disable_tether_notification_message" msgid="6717523799293901476">"Щоб дізнатися більше, зв\'яжіться з адміністратором"</string>
+    <string name="notification_channel_tethering_status" msgid="2663463891530932727">"Статус точки доступу та модема"</string>
+    <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+    <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+    <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+    <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+    <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
 </resources>
diff --git a/Tethering/res/values-ur/strings.xml b/Tethering/res/values-ur/strings.xml
index c70e44f..d72c7d4 100644
--- a/Tethering/res/values-ur/strings.xml
+++ b/Tethering/res/values-ur/strings.xml
@@ -16,14 +16,14 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="tethered_notification_title" msgid="5350162111436634622">"ٹیدرنگ یا ہاٹ اسپاٹ فعال ہے"</string>
-    <string name="tethered_notification_message" msgid="2338023450330652098">"سیٹ اپ کرنے کیلئے تھپتھپائیں۔"</string>
-    <string name="disable_tether_notification_title" msgid="3183576627492925522">"ٹیدرنگ غیر فعال ہے"</string>
-    <string name="disable_tether_notification_message" msgid="6655882039707534929">"تفصیلات کیلئے اپنے منتظم سے رابطہ کریں"</string>
-    <string name="notification_channel_tethering_status" msgid="7030733422705019001">"ہاٹ اسپاٹ اور ٹیتھرنگ کا اسٹیٹس"</string>
-    <string name="no_upstream_notification_title" msgid="2052743091868702475"></string>
-    <string name="no_upstream_notification_message" msgid="6932020551635470134"></string>
-    <string name="no_upstream_notification_disable_button" msgid="8836277213343697023"></string>
-    <string name="upstream_roaming_notification_title" msgid="8614262557406849762"></string>
-    <string name="upstream_roaming_notification_message" msgid="5999740876323106599"></string>
+    <string name="tethered_notification_title" msgid="6426563586025792944">"ٹیدرنگ یا ہاٹ اسپاٹ فعال"</string>
+    <string name="tethered_notification_message" msgid="64800879503420696">"سیٹ اپ کرنے کیلئے تھپتھپائیں۔"</string>
+    <string name="disable_tether_notification_title" msgid="3004509127903564191">"ٹیدرنگ غیر فعال ہے"</string>
+    <string name="disable_tether_notification_message" msgid="6717523799293901476">"تفصیلات کے لئے اپنے منتظم سے رابطہ کریں"</string>
+    <string name="notification_channel_tethering_status" msgid="2663463891530932727">"ہاٹ اسپاٹ اور ٹیتھرنگ کا اسٹیٹس"</string>
+    <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+    <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+    <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+    <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+    <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
 </resources>
diff --git a/Tethering/res/values-uz/strings.xml b/Tethering/res/values-uz/strings.xml
index b315901..af3b2eb 100644
--- a/Tethering/res/values-uz/strings.xml
+++ b/Tethering/res/values-uz/strings.xml
@@ -16,14 +16,14 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="tethered_notification_title" msgid="5350162111436634622">"Modem rejimi yoki hotspot yoniq"</string>
-    <string name="tethered_notification_message" msgid="2338023450330652098">"Sozlash uchun bosing."</string>
-    <string name="disable_tether_notification_title" msgid="3183576627492925522">"Modem rejimi faolsizlantirildi"</string>
-    <string name="disable_tether_notification_message" msgid="6655882039707534929">"Tafsilotlari uchun administratoringizga murojaat qiling"</string>
-    <string name="notification_channel_tethering_status" msgid="7030733422705019001">"Hotspot va modem rejimi holati"</string>
-    <string name="no_upstream_notification_title" msgid="2052743091868702475"></string>
-    <string name="no_upstream_notification_message" msgid="6932020551635470134"></string>
-    <string name="no_upstream_notification_disable_button" msgid="8836277213343697023"></string>
-    <string name="upstream_roaming_notification_title" msgid="8614262557406849762"></string>
-    <string name="upstream_roaming_notification_message" msgid="5999740876323106599"></string>
+    <string name="tethered_notification_title" msgid="6426563586025792944">"Modem rejimi yoki hotspot yoniq"</string>
+    <string name="tethered_notification_message" msgid="64800879503420696">"Sozlash uchun bosing."</string>
+    <string name="disable_tether_notification_title" msgid="3004509127903564191">"Modem rejimi faolsizlantirildi"</string>
+    <string name="disable_tether_notification_message" msgid="6717523799293901476">"Tafsilotlari uchun administratoringizga murojaat qiling"</string>
+    <string name="notification_channel_tethering_status" msgid="2663463891530932727">"Hotspot va modem rejimi holati"</string>
+    <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+    <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+    <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+    <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+    <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
 </resources>
diff --git a/Tethering/res/values-vi/strings.xml b/Tethering/res/values-vi/strings.xml
index 8e1b91e..21a0735 100644
--- a/Tethering/res/values-vi/strings.xml
+++ b/Tethering/res/values-vi/strings.xml
@@ -16,14 +16,14 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="tethered_notification_title" msgid="5350162111436634622">"Tính năng chia sẻ Internet hoặc điểm phát sóng đang hoạt động"</string>
-    <string name="tethered_notification_message" msgid="2338023450330652098">"Hãy nhấn để thiết lập."</string>
-    <string name="disable_tether_notification_title" msgid="3183576627492925522">"Tính năng chia sẻ Internet đã bị tắt"</string>
-    <string name="disable_tether_notification_message" msgid="6655882039707534929">"Hãy liên hệ với quản trị viên của bạn để biết thông tin chi tiết"</string>
-    <string name="notification_channel_tethering_status" msgid="7030733422705019001">"Trạng thái của chế độ cài đặt \"Điểm phát sóng và chia sẻ Internet\""</string>
-    <string name="no_upstream_notification_title" msgid="2052743091868702475"></string>
-    <string name="no_upstream_notification_message" msgid="6932020551635470134"></string>
-    <string name="no_upstream_notification_disable_button" msgid="8836277213343697023"></string>
-    <string name="upstream_roaming_notification_title" msgid="8614262557406849762"></string>
-    <string name="upstream_roaming_notification_message" msgid="5999740876323106599"></string>
+    <string name="tethered_notification_title" msgid="6426563586025792944">"Tính năng chia sẻ Internet hoặc điểm phát sóng đang hoạt động"</string>
+    <string name="tethered_notification_message" msgid="64800879503420696">"Hãy nhấn để thiết lập."</string>
+    <string name="disable_tether_notification_title" msgid="3004509127903564191">"Đã tắt tính năng chia sẻ Internet"</string>
+    <string name="disable_tether_notification_message" msgid="6717523799293901476">"Hãy liên hệ với quản trị viên của bạn để biết chi tiết"</string>
+    <string name="notification_channel_tethering_status" msgid="2663463891530932727">"Trạng thái điểm phát sóng và chia sẻ Internet"</string>
+    <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+    <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+    <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+    <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+    <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
 </resources>
diff --git a/Tethering/res/values-zh-rCN/strings.xml b/Tethering/res/values-zh-rCN/strings.xml
index 054344e..98e3b4b 100644
--- a/Tethering/res/values-zh-rCN/strings.xml
+++ b/Tethering/res/values-zh-rCN/strings.xml
@@ -16,14 +16,14 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="tethered_notification_title" msgid="5350162111436634622">"网络共享或热点已启用"</string>
-    <string name="tethered_notification_message" msgid="2338023450330652098">"点按即可设置。"</string>
-    <string name="disable_tether_notification_title" msgid="3183576627492925522">"网络共享已停用"</string>
-    <string name="disable_tether_notification_message" msgid="6655882039707534929">"如需了解详情,请与您的管理员联系"</string>
-    <string name="notification_channel_tethering_status" msgid="7030733422705019001">"热点和网络共享状态"</string>
-    <string name="no_upstream_notification_title" msgid="2052743091868702475"></string>
-    <string name="no_upstream_notification_message" msgid="6932020551635470134"></string>
-    <string name="no_upstream_notification_disable_button" msgid="8836277213343697023"></string>
-    <string name="upstream_roaming_notification_title" msgid="8614262557406849762"></string>
-    <string name="upstream_roaming_notification_message" msgid="5999740876323106599"></string>
+    <string name="tethered_notification_title" msgid="6426563586025792944">"网络共享或热点已启用"</string>
+    <string name="tethered_notification_message" msgid="64800879503420696">"点按即可设置。"</string>
+    <string name="disable_tether_notification_title" msgid="3004509127903564191">"网络共享已停用"</string>
+    <string name="disable_tether_notification_message" msgid="6717523799293901476">"如需了解详情,请与您的管理员联系"</string>
+    <string name="notification_channel_tethering_status" msgid="2663463891530932727">"热点和网络共享状态"</string>
+    <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+    <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+    <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+    <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+    <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
 </resources>
diff --git a/Tethering/res/values-zh-rHK/strings.xml b/Tethering/res/values-zh-rHK/strings.xml
index 790d40a..9cafd42 100644
--- a/Tethering/res/values-zh-rHK/strings.xml
+++ b/Tethering/res/values-zh-rHK/strings.xml
@@ -16,14 +16,14 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="tethered_notification_title" msgid="5350162111436634622">"網絡共享或熱點已啟用"</string>
-    <string name="tethered_notification_message" msgid="2338023450330652098">"輕按即可設定。"</string>
-    <string name="disable_tether_notification_title" msgid="3183576627492925522">"網絡共享已停用"</string>
-    <string name="disable_tether_notification_message" msgid="6655882039707534929">"請聯絡你的管理員以瞭解詳情"</string>
-    <string name="notification_channel_tethering_status" msgid="7030733422705019001">"熱點和網絡共享狀態"</string>
-    <string name="no_upstream_notification_title" msgid="2052743091868702475"></string>
-    <string name="no_upstream_notification_message" msgid="6932020551635470134"></string>
-    <string name="no_upstream_notification_disable_button" msgid="8836277213343697023"></string>
-    <string name="upstream_roaming_notification_title" msgid="8614262557406849762"></string>
-    <string name="upstream_roaming_notification_message" msgid="5999740876323106599"></string>
+    <string name="tethered_notification_title" msgid="6426563586025792944">"網絡共享或熱點已啟用"</string>
+    <string name="tethered_notification_message" msgid="64800879503420696">"輕按即可設定。"</string>
+    <string name="disable_tether_notification_title" msgid="3004509127903564191">"網絡共享已停用"</string>
+    <string name="disable_tether_notification_message" msgid="6717523799293901476">"請聯絡您的管理員以瞭解詳情"</string>
+    <string name="notification_channel_tethering_status" msgid="2663463891530932727">"熱點和網絡共享狀態"</string>
+    <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+    <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+    <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+    <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+    <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
 </resources>
diff --git a/Tethering/res/values-zh-rTW/strings.xml b/Tethering/res/values-zh-rTW/strings.xml
index 65a689e..50a50bf 100644
--- a/Tethering/res/values-zh-rTW/strings.xml
+++ b/Tethering/res/values-zh-rTW/strings.xml
@@ -16,14 +16,14 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="tethered_notification_title" msgid="5350162111436634622">"網路共用或無線基地台已啟用"</string>
-    <string name="tethered_notification_message" msgid="2338023450330652098">"輕觸即可設定。"</string>
-    <string name="disable_tether_notification_title" msgid="3183576627492925522">"網路共用已停用"</string>
-    <string name="disable_tether_notification_message" msgid="6655882039707534929">"詳情請洽你的管理員"</string>
-    <string name="notification_channel_tethering_status" msgid="7030733422705019001">"無線基地台與網路共用狀態"</string>
-    <string name="no_upstream_notification_title" msgid="2052743091868702475"></string>
-    <string name="no_upstream_notification_message" msgid="6932020551635470134"></string>
-    <string name="no_upstream_notification_disable_button" msgid="8836277213343697023"></string>
-    <string name="upstream_roaming_notification_title" msgid="8614262557406849762"></string>
-    <string name="upstream_roaming_notification_message" msgid="5999740876323106599"></string>
+    <string name="tethered_notification_title" msgid="6426563586025792944">"網路共用或無線基地台已啟用"</string>
+    <string name="tethered_notification_message" msgid="64800879503420696">"輕觸即可進行設定。"</string>
+    <string name="disable_tether_notification_title" msgid="3004509127903564191">"網路共用已停用"</string>
+    <string name="disable_tether_notification_message" msgid="6717523799293901476">"詳情請洽你的管理員"</string>
+    <string name="notification_channel_tethering_status" msgid="2663463891530932727">"無線基地台與網路共用狀態"</string>
+    <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+    <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+    <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+    <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+    <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
 </resources>
diff --git a/Tethering/res/values-zu/strings.xml b/Tethering/res/values-zu/strings.xml
index e9651dd..f210f87 100644
--- a/Tethering/res/values-zu/strings.xml
+++ b/Tethering/res/values-zu/strings.xml
@@ -16,14 +16,14 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="tethered_notification_title" msgid="5350162111436634622">"Ukusebenzisa njengemodemu noma i-hotspot ephathekayo kuvuliwe"</string>
-    <string name="tethered_notification_message" msgid="2338023450330652098">"Thepha ukuze usethe."</string>
-    <string name="disable_tether_notification_title" msgid="3183576627492925522">"Ukusebenzisa ifoni njengemodemu kukhutshaziwe"</string>
-    <string name="disable_tether_notification_message" msgid="6655882039707534929">"Xhumana nomphathi wakho ukuze uthole imininingwane"</string>
-    <string name="notification_channel_tethering_status" msgid="7030733422705019001">"I-Hotspot nesimo sokusebenzisa ifoni njengemodemu"</string>
-    <string name="no_upstream_notification_title" msgid="2052743091868702475"></string>
-    <string name="no_upstream_notification_message" msgid="6932020551635470134"></string>
-    <string name="no_upstream_notification_disable_button" msgid="8836277213343697023"></string>
-    <string name="upstream_roaming_notification_title" msgid="8614262557406849762"></string>
-    <string name="upstream_roaming_notification_message" msgid="5999740876323106599"></string>
+    <string name="tethered_notification_title" msgid="6426563586025792944">"Ukusebenzisa njengemodemu noma i-hotspot ephathekayo kuvuliwe"</string>
+    <string name="tethered_notification_message" msgid="64800879503420696">"Thepha ukuze usethe."</string>
+    <string name="disable_tether_notification_title" msgid="3004509127903564191">"Ukusebenzisa ifoni njengemodemu kukhutshaziwe"</string>
+    <string name="disable_tether_notification_message" msgid="6717523799293901476">"Xhumana nomphathi wakho ukuze uthole imininingwane"</string>
+    <string name="notification_channel_tethering_status" msgid="2663463891530932727">"I-Hotspot nesimo sokusebenzisa ifoni njengemodemu"</string>
+    <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+    <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+    <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+    <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+    <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
 </resources>
diff --git a/Tethering/src/android/net/ip/IpServer.java b/Tethering/src/android/net/ip/IpServer.java
index 79d9a23..544ba01 100644
--- a/Tethering/src/android/net/ip/IpServer.java
+++ b/Tethering/src/android/net/ip/IpServer.java
@@ -60,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;
 
@@ -838,7 +839,7 @@
 
     private void addInterfaceToNetwork(final int netId, @NonNull final String ifaceName) {
         try {
-            if (null != mRoutingCoordinator.value) {
+            if (SdkLevel.isAtLeastS() && null != mRoutingCoordinator.value) {
                 // TODO : remove this call in favor of using the LocalNetworkConfiguration
                 // correctly, which will let ConnectivityService do it automatically.
                 mRoutingCoordinator.value.addInterfaceToNetwork(netId, ifaceName);
@@ -852,7 +853,7 @@
 
     private void addInterfaceForward(@NonNull final String fromIface,
             @NonNull final String toIface) throws ServiceSpecificException, RemoteException {
-        if (null != mRoutingCoordinator.value) {
+        if (SdkLevel.isAtLeastS() && null != mRoutingCoordinator.value) {
             mRoutingCoordinator.value.addInterfaceForward(fromIface, toIface);
         } else {
             mNetd.tetherAddForward(fromIface, toIface);
@@ -862,7 +863,7 @@
 
     private void removeInterfaceForward(@NonNull final String fromIface,
             @NonNull final String toIface) {
-        if (null != mRoutingCoordinator.value) {
+        if (SdkLevel.isAtLeastS() && null != mRoutingCoordinator.value) {
             try {
                 mRoutingCoordinator.value.removeInterfaceForward(fromIface, toIface);
             } catch (ServiceSpecificException e) {
@@ -899,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));
@@ -907,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);
             }
@@ -919,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();
@@ -940,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);
             }
@@ -1547,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));
@@ -1578,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));
@@ -1588,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 50d6c4b..d848ea8 100644
--- a/Tethering/src/android/net/ip/RouterAdvertisementDaemon.java
+++ b/Tethering/src/android/net/ip/RouterAdvertisementDaemon.java
@@ -41,6 +41,7 @@
 import android.system.ErrnoException;
 import android.system.Os;
 import android.system.StructTimeval;
+import android.util.ArraySet;
 import android.util.Log;
 
 import com.android.internal.annotations.GuardedBy;
@@ -134,23 +135,23 @@
         public boolean hasDefaultRoute;
         public byte hopLimit;
         public int mtu;
-        public HashSet<IpPrefix> prefixes;
-        public HashSet<Inet6Address> dnses;
+        public ArraySet<IpPrefix> prefixes;
+        public ArraySet<Inet6Address> dnses;
 
         public RaParams() {
             hasDefaultRoute = false;
             hopLimit = DEFAULT_HOPLIMIT;
             mtu = IPV6_MIN_MTU;
-            prefixes = new HashSet<IpPrefix>();
-            dnses = new HashSet<Inet6Address>();
+            prefixes = new ArraySet<IpPrefix>();
+            dnses = new ArraySet<Inet6Address>();
         }
 
         public RaParams(RaParams other) {
             hasDefaultRoute = other.hasDefaultRoute;
             hopLimit = other.hopLimit;
             mtu = other.mtu;
-            prefixes = (HashSet) other.prefixes.clone();
-            dnses = (HashSet) other.dnses.clone();
+            prefixes = new ArraySet<IpPrefix>(other.prefixes);
+            dnses = new ArraySet<Inet6Address>(other.dnses);
         }
 
         /**
diff --git a/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java b/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
index 4bd7e6a..81e18ab 100644
--- a/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
+++ b/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
@@ -90,7 +90,6 @@
 import java.net.NetworkInterface;
 import java.net.SocketException;
 import java.net.UnknownHostException;
-import java.nio.ByteBuffer;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
@@ -380,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;
@@ -392,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;
@@ -404,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;
@@ -415,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);
@@ -428,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;
@@ -440,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;
@@ -452,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;
@@ -1258,12 +1257,18 @@
         pw.decreaseIndent();
     }
 
-    private IpPrefix longToPrefix(long ip64) {
-        final ByteBuffer prefixBuffer = ByteBuffer.allocate(IPV6_ADDR_LEN);
-        prefixBuffer.putLong(ip64);
+    /**
+     * Returns a /64 IpPrefix corresponding to the passed in byte array
+     *
+     * @param ip64 byte array to convert format
+     * @return the converted IpPrefix
+     */
+    @VisibleForTesting
+    public static IpPrefix bytesToPrefix(final byte[] ip64) {
         IpPrefix sourcePrefix;
+        byte[] prefixBytes = Arrays.copyOf(ip64, IPV6_ADDR_LEN);
         try {
-            sourcePrefix = new IpPrefix(InetAddress.getByAddress(prefixBuffer.array()), 64);
+            sourcePrefix = new IpPrefix(InetAddress.getByAddress(prefixBytes), 64);
         } catch (UnknownHostException e) {
             // Cannot happen. InetAddress.getByAddress can only throw an exception if the byte array
             // is the wrong length, but we allocate it with fixed length IPV6_ADDR_LEN.
@@ -1274,7 +1279,7 @@
 
     private String ipv6UpstreamRuleToString(TetherUpstream6Key key, Tether6Value value) {
         return String.format("%d(%s) [%s] [%s] -> %d(%s) %04x [%s] [%s]",
-                key.iif, getIfName(key.iif), key.dstMac, longToPrefix(key.src64), value.oif,
+                key.iif, getIfName(key.iif), key.dstMac, bytesToPrefix(key.src64), value.oif,
                 getIfName(value.oif), value.ethProto, value.ethSrcMac, value.ethDstMac);
     }
 
@@ -1570,7 +1575,7 @@
          */
         @NonNull
         public TetherUpstream6Key makeTetherUpstream6Key() {
-            long prefix64 = ByteBuffer.wrap(sourcePrefix.getRawAddress()).getLong();
+            final byte[] prefix64 = Arrays.copyOf(sourcePrefix.getRawAddress(), 8);
             return new TetherUpstream6Key(downstreamIfindex, inDstMac, prefix64);
         }
 
diff --git a/Tethering/src/com/android/networkstack/tethering/OffloadHalAidlImpl.java b/Tethering/src/com/android/networkstack/tethering/OffloadHalAidlImpl.java
index e7dc757..9ef0f45 100644
--- a/Tethering/src/com/android/networkstack/tethering/OffloadHalAidlImpl.java
+++ b/Tethering/src/com/android/networkstack/tethering/OffloadHalAidlImpl.java
@@ -19,18 +19,21 @@
 import static com.android.networkstack.tethering.OffloadHardwareInterface.OFFLOAD_HAL_VERSION_AIDL;
 
 import android.annotation.NonNull;
+import android.annotation.TargetApi;
 import android.hardware.tetheroffload.ForwardedStats;
 import android.hardware.tetheroffload.IOffload;
 import android.hardware.tetheroffload.ITetheringOffloadCallback;
 import android.hardware.tetheroffload.NatTimeoutUpdate;
 import android.hardware.tetheroffload.NetworkProtocol;
 import android.hardware.tetheroffload.OffloadCallbackEvent;
+import android.os.Build;
 import android.os.Handler;
 import android.os.NativeHandle;
 import android.os.ParcelFileDescriptor;
 import android.os.ServiceManager;
 import android.system.OsConstants;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.modules.utils.build.SdkLevel;
 import com.android.net.module.util.SharedLog;
 import com.android.networkstack.tethering.OffloadHardwareInterface.OffloadHalCallback;
@@ -40,6 +43,7 @@
 /**
  * The implementation of IOffloadHal which based on Stable AIDL interface
  */
+@TargetApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
 public class OffloadHalAidlImpl implements IOffloadHal {
     private static final String TAG = OffloadHalAidlImpl.class.getSimpleName();
     private static final String HAL_INSTANCE_NAME = IOffload.DESCRIPTOR + "/default";
@@ -52,6 +56,7 @@
 
     private TetheringOffloadCallback mTetheringOffloadCallback;
 
+    @VisibleForTesting
     public OffloadHalAidlImpl(int version, @NonNull IOffload offload, @NonNull Handler handler,
             @NonNull SharedLog log) {
         mOffloadVersion = version;
diff --git a/Tethering/src/com/android/networkstack/tethering/OffloadHalHidlImpl.java b/Tethering/src/com/android/networkstack/tethering/OffloadHalHidlImpl.java
index e0a9878..71922f9 100644
--- a/Tethering/src/com/android/networkstack/tethering/OffloadHalHidlImpl.java
+++ b/Tethering/src/com/android/networkstack/tethering/OffloadHalHidlImpl.java
@@ -74,10 +74,7 @@
      */
     public boolean initOffload(@NonNull NativeHandle handle1, @NonNull NativeHandle handle2,
             @NonNull OffloadHalCallback callback) {
-        final String logmsg = String.format("initOffload(%d, %d, %s)",
-                handle1.getFileDescriptor().getInt$(), handle2.getFileDescriptor().getInt$(),
-                (callback == null) ? "null"
-                : "0x" + Integer.toHexString(System.identityHashCode(callback)));
+        final String logmsg = "initOffload()";
 
         mOffloadHalCallback = callback;
         mTetheringOffloadCallback = new TetheringOffloadCallback(
diff --git a/Tethering/src/com/android/networkstack/tethering/OffloadHardwareInterface.java b/Tethering/src/com/android/networkstack/tethering/OffloadHardwareInterface.java
index 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/PrivateAddressCoordinator.java b/Tethering/src/com/android/networkstack/tethering/PrivateAddressCoordinator.java
index 6c0ca82..528991f 100644
--- a/Tethering/src/com/android/networkstack/tethering/PrivateAddressCoordinator.java
+++ b/Tethering/src/com/android/networkstack/tethering/PrivateAddressCoordinator.java
@@ -79,6 +79,7 @@
     private final TetheringConfiguration mConfig;
     // keyed by downstream type(TetheringManager.TETHERING_*).
     private final ArrayMap<AddressKey, LinkAddress> mCachedAddresses;
+    private final Random mRandom;
 
     public PrivateAddressCoordinator(Context context, TetheringConfiguration config) {
         mDownstreams = new ArraySet<>();
@@ -95,6 +96,7 @@
 
         mTetheringPrefixes = new ArrayList<>(Arrays.asList(new IpPrefix("192.168.0.0/16"),
             new IpPrefix("172.16.0.0/12"), new IpPrefix("10.0.0.0/8")));
+        mRandom = new Random();
     }
 
     /**
@@ -187,7 +189,10 @@
             return cachedAddress;
         }
 
-        for (IpPrefix prefixRange : mTetheringPrefixes) {
+        final int prefixIndex = getStartedPrefixIndex();
+        for (int i = 0; i < mTetheringPrefixes.size(); i++) {
+            final IpPrefix prefixRange = mTetheringPrefixes.get(
+                    (prefixIndex + i) % mTetheringPrefixes.size());
             final LinkAddress newAddress = chooseDownstreamAddress(prefixRange);
             if (newAddress != null) {
                 mDownstreams.add(ipServer);
@@ -200,6 +205,28 @@
         return null;
     }
 
+    private int getStartedPrefixIndex() {
+        if (!mConfig.isRandomPrefixBaseEnabled()) return 0;
+
+        final int random = getRandomInt() & 0xffffff;
+        // This is to select the starting prefix range (/8, /12, or /16) instead of the actual
+        // LinkAddress. To avoid complex operations in the selection logic and make the selected
+        // rate approximate consistency with that /8 is around 2^4 times of /12 and /12 is around
+        // 2^4 times of /16, we simply define a map between the value and the prefix value like
+        // this:
+        //
+        // Value 0 ~ 0xffff (65536/16777216 = 0.39%) -> 192.168.0.0/16
+        // Value 0x10000 ~ 0xfffff (983040/16777216 = 5.86%) -> 172.16.0.0/12
+        // Value 0x100000 ~ 0xffffff (15728640/16777216 = 93.7%) -> 10.0.0.0/8
+        if (random > 0xfffff) {
+            return 2;
+        } else if (random > 0xffff) {
+            return 1;
+        } else {
+            return 0;
+        }
+    }
+
     private int getPrefixBaseAddress(final IpPrefix prefix) {
         return inet4AddressToIntHTH((Inet4Address) prefix.getAddress());
     }
@@ -263,12 +290,13 @@
         // is less than 127.0.0.0 = 0x7f000000 = 2130706432.
         //
         // Additionally, it makes debug output easier to read by making the numbers smaller.
-        final int randomPrefixStart = getRandomInt() & ~prefixRangeMask & prefixMask;
+        final int randomInt = getRandomInt();
+        final int randomPrefixStart = randomInt & ~prefixRangeMask & prefixMask;
 
         // A random offset within the prefix. Used to determine the local address once the prefix
         // is selected. It does not result in an IPv4 address ending in .0, .1, or .255
-        // For a PREFIX_LENGTH of 255, this is a number between 2 and 254.
-        final int subAddress = getSanitizedSubAddr(~prefixMask);
+        // For a PREFIX_LENGTH of 24, this is a number between 2 and 254.
+        final int subAddress = getSanitizedSubAddr(randomInt, ~prefixMask);
 
         // Find a prefix length PREFIX_LENGTH between randomPrefixStart and the end of the block,
         // such that the prefix does not conflict with any upstream.
@@ -310,12 +338,12 @@
     /** Get random int which could be used to generate random address. */
     @VisibleForTesting
     public int getRandomInt() {
-        return (new Random()).nextInt();
+        return mRandom.nextInt();
     }
 
     /** Get random subAddress and avoid selecting x.x.x.0, x.x.x.1 and x.x.x.255 address. */
-    private int getSanitizedSubAddr(final int subAddrMask) {
-        final int randomSubAddr = getRandomInt() & subAddrMask;
+    private int getSanitizedSubAddr(final int randomInt, final int subAddrMask) {
+        final int randomSubAddr = randomInt & subAddrMask;
         // If prefix length > 30, the selecting speace would be less than 4 which may be hard to
         // avoid 3 consecutive address.
         if (PREFIX_LENGTH > 30) return randomSubAddr;
diff --git a/Tethering/src/com/android/networkstack/tethering/TetherUpstream6Key.java b/Tethering/src/com/android/networkstack/tethering/TetherUpstream6Key.java
index 36a1c3c..0cc3dd9 100644
--- a/Tethering/src/com/android/networkstack/tethering/TetherUpstream6Key.java
+++ b/Tethering/src/com/android/networkstack/tethering/TetherUpstream6Key.java
@@ -32,10 +32,10 @@
     @Field(order = 1, type = Type.EUI48, padding = 6)
     public final MacAddress dstMac; // Destination ethernet mac address (zeroed iff rawip ingress).
 
-    @Field(order = 2, type = Type.S64)
-    public final long src64; // The top 64-bits of the source ip.
+    @Field(order = 2, type = Type.ByteArray, arraysize = 8)
+    public final byte[] src64; // The top 64-bits of the source ip.
 
-    public TetherUpstream6Key(int iif, @NonNull final MacAddress dstMac, long src64) {
+    public TetherUpstream6Key(int iif, @NonNull final MacAddress dstMac, final byte[] src64) {
         Objects.requireNonNull(dstMac);
 
         this.iif = iif;
diff --git a/Tethering/src/com/android/networkstack/tethering/Tethering.java b/Tethering/src/com/android/networkstack/tethering/Tethering.java
index 996ee11..873961a 100644
--- a/Tethering/src/com/android/networkstack/tethering/Tethering.java
+++ b/Tethering/src/com/android/networkstack/tethering/Tethering.java
@@ -136,6 +136,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;
@@ -161,11 +162,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;
 
 /**
  *
@@ -303,9 +301,9 @@
         mContext = mDeps.getContext();
         mNetd = mDeps.getINetd(mContext);
         mRoutingCoordinator = mDeps.getRoutingCoordinator(mContext);
-        mLooper = mDeps.getTetheringLooper();
-        mNotificationUpdater = mDeps.getNotificationUpdater(mContext, mLooper);
-        mTetheringMetrics = mDeps.getTetheringMetrics();
+        mLooper = mDeps.makeTetheringLooper();
+        mNotificationUpdater = mDeps.makeNotificationUpdater(mContext, mLooper);
+        mTetheringMetrics = mDeps.makeTetheringMetrics();
 
         // This is intended to ensrure that if something calls startTethering(bluetooth) just after
         // bluetooth is enabled. Before onServiceConnected is called, store the calls into this
@@ -319,7 +317,7 @@
         mTetherMainSM.start();
 
         mHandler = mTetherMainSM.getHandler();
-        mOffloadController = mDeps.getOffloadController(mHandler, mLog,
+        mOffloadController = mDeps.makeOffloadController(mHandler, mLog,
                 new OffloadController.Dependencies() {
 
                     @Override
@@ -327,15 +325,17 @@
                         return mConfig;
                     }
                 });
-        mUpstreamNetworkMonitor = mDeps.getUpstreamNetworkMonitor(mContext, mTetherMainSM, mLog,
-                TetherMainSM.EVENT_UPSTREAM_CALLBACK);
+        mUpstreamNetworkMonitor = mDeps.makeUpstreamNetworkMonitor(mContext, mHandler, mLog,
+                (what, obj) -> {
+                    mTetherMainSM.sendMessage(TetherMainSM.EVENT_UPSTREAM_CALLBACK, what, 0, obj);
+                });
         mForwardedDownstreams = new HashSet<>();
 
         IntentFilter filter = new IntentFilter();
         filter.addAction(ACTION_CARRIER_CONFIG_CHANGED);
         // EntitlementManager will send EVENT_UPSTREAM_PERMISSION_CHANGED when cellular upstream
         // permission is changed according to entitlement check result.
-        mEntitlementMgr = mDeps.getEntitlementManager(mContext, mHandler, mLog,
+        mEntitlementMgr = mDeps.makeEntitlementManager(mContext, mHandler, mLog,
                 () -> mTetherMainSM.sendMessage(
                 TetherMainSM.EVENT_UPSTREAM_PERMISSION_CHANGED));
         mEntitlementMgr.setOnTetherProvisioningFailedListener((downstream, reason) -> {
@@ -368,14 +368,15 @@
 
         // Load tethering configuration.
         updateConfiguration();
+        mConfig.readEnableSyncSM(mContext);
         // It is OK for the configuration to be passed to the PrivateAddressCoordinator at
         // construction time because the only part of the configuration it uses is
         // shouldEnableWifiP2pDedicatedIp(), and currently do not support changing that.
-        mPrivateAddressCoordinator = mDeps.getPrivateAddressCoordinator(mContext, mConfig);
+        mPrivateAddressCoordinator = mDeps.makePrivateAddressCoordinator(mContext, mConfig);
 
         // Must be initialized after tethering configuration is loaded because BpfCoordinator
         // constructor needs to use the configuration.
-        mBpfCoordinator = mDeps.getBpfCoordinator(
+        mBpfCoordinator = mDeps.makeBpfCoordinator(
                 new BpfCoordinator.Dependencies() {
                     @NonNull
                     public Handler getHandler() {
@@ -404,7 +405,7 @@
                 });
 
         if (SdkLevel.isAtLeastT() && mConfig.isWearTetheringEnabled()) {
-            mWearableConnectionManager = mDeps.getWearableConnectionManager(mContext);
+            mWearableConnectionManager = mDeps.makeWearableConnectionManager(mContext);
         } else {
             mWearableConnectionManager = null;
         }
@@ -873,7 +874,7 @@
 
     private void changeBluetoothTetheringSettings(@NonNull final BluetoothPan bluetoothPan,
             final boolean enable) {
-        final BluetoothPanShim panShim = mDeps.getBluetoothPanShim(bluetoothPan);
+        final BluetoothPanShim panShim = mDeps.makeBluetoothPanShim(bluetoothPan);
         if (enable) {
             if (mBluetoothIfaceRequest != null) {
                 Log.d(TAG, "Bluetooth tethering settings already enabled");
@@ -1737,7 +1738,7 @@
             addState(mSetDnsForwardersErrorState);
 
             mNotifyList = new ArrayList<>();
-            mIPv6TetheringCoordinator = deps.getIPv6TetheringCoordinator(mNotifyList, mLog);
+            mIPv6TetheringCoordinator = deps.makeIPv6TetheringCoordinator(mNotifyList, mLog);
             mOffload = new OffloadWrapper();
 
             setInitialState(mInitialState);
@@ -2692,31 +2693,10 @@
             return;
         }
 
-        final CountDownLatch latch = new CountDownLatch(1);
-
-        // Don't crash the system if something in doDump throws an exception, but try to propagate
-        // the exception to the caller.
-        AtomicReference<RuntimeException> exceptionRef = new AtomicReference<>();
-        mHandler.post(() -> {
-            try {
-                doDump(fd, writer, args);
-            } catch (RuntimeException e) {
-                exceptionRef.set(e);
-            }
-            latch.countDown();
-        });
-
-        try {
-            if (!latch.await(DUMP_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
-                writer.println("Dump timeout after " + DUMP_TIMEOUT_MS + "ms");
-                return;
-            }
-        } catch (InterruptedException e) {
-            exceptionRef.compareAndSet(null, new IllegalStateException("Dump interrupted", e));
+        if (!HandlerUtils.runWithScissorsForDump(mHandler, () -> doDump(fd, writer, args),
+                DUMP_TIMEOUT_MS)) {
+            writer.println("Dump timeout after " + DUMP_TIMEOUT_MS + "ms");
         }
-
-        final RuntimeException e = exceptionRef.get();
-        if (e != null) throw e;
     }
 
     private void maybeDhcpLeasesChanged() {
@@ -2732,84 +2712,73 @@
         }
     }
 
-    private IpServer.Callback makeControlCallback() {
-        return new IpServer.Callback() {
-            @Override
-            public void updateInterfaceState(IpServer who, int state, int lastError) {
-                notifyInterfaceStateChange(who, state, lastError);
+    private class ControlCallback extends IpServer.Callback {
+        @Override
+        public void updateInterfaceState(IpServer who, int state, int lastError) {
+            final String iface = who.interfaceName();
+            final TetherState tetherState = mTetherStates.get(iface);
+            if (tetherState != null && tetherState.ipServer.equals(who)) {
+                tetherState.lastState = state;
+                tetherState.lastError = lastError;
+            } else {
+                if (DBG) Log.d(TAG, "got notification from stale iface " + iface);
             }
 
-            @Override
-            public void updateLinkProperties(IpServer who, LinkProperties newLp) {
-                notifyLinkPropertiesChanged(who, newLp);
-            }
+            mLog.log(String.format("OBSERVED iface=%s state=%s error=%s", iface, state, lastError));
 
-            @Override
-            public void dhcpLeasesChanged() {
-                maybeDhcpLeasesChanged();
+            // If TetherMainSM is in ErrorState, TetherMainSM stays there.
+            // Thus we give a chance for TetherMainSM to recover to InitialState
+            // by sending CMD_CLEAR_ERROR
+            if (lastError == TETHER_ERROR_INTERNAL_ERROR) {
+                mTetherMainSM.sendMessage(TetherMainSM.CMD_CLEAR_ERROR, who);
             }
-
-            @Override
-            public void requestEnableTethering(int tetheringType, boolean enabled) {
-                mTetherMainSM.sendMessage(TetherMainSM.EVENT_REQUEST_CHANGE_DOWNSTREAM,
-                        tetheringType, 0, enabled ? Boolean.TRUE : Boolean.FALSE);
+            int which;
+            switch (state) {
+                case IpServer.STATE_UNAVAILABLE:
+                case IpServer.STATE_AVAILABLE:
+                    which = TetherMainSM.EVENT_IFACE_SERVING_STATE_INACTIVE;
+                    break;
+                case IpServer.STATE_TETHERED:
+                case IpServer.STATE_LOCAL_ONLY:
+                    which = TetherMainSM.EVENT_IFACE_SERVING_STATE_ACTIVE;
+                    break;
+                default:
+                    Log.wtf(TAG, "Unknown interface state: " + state);
+                    return;
             }
-        };
-    }
-
-    // TODO: Move into TetherMainSM.
-    private void notifyInterfaceStateChange(IpServer who, int state, int error) {
-        final String iface = who.interfaceName();
-        final TetherState tetherState = mTetherStates.get(iface);
-        if (tetherState != null && tetherState.ipServer.equals(who)) {
-            tetherState.lastState = state;
-            tetherState.lastError = error;
-        } else {
-            if (DBG) Log.d(TAG, "got notification from stale iface " + iface);
+            mTetherMainSM.sendMessage(which, state, 0, who);
+            sendTetherStateChangedBroadcast();
         }
 
-        mLog.log(String.format("OBSERVED iface=%s state=%s error=%s", iface, state, error));
-
-        // If TetherMainSM is in ErrorState, TetherMainSM stays there.
-        // Thus we give a chance for TetherMainSM to recover to InitialState
-        // by sending CMD_CLEAR_ERROR
-        if (error == TETHER_ERROR_INTERNAL_ERROR) {
-            mTetherMainSM.sendMessage(TetherMainSM.CMD_CLEAR_ERROR, who);
-        }
-        int which;
-        switch (state) {
-            case IpServer.STATE_UNAVAILABLE:
-            case IpServer.STATE_AVAILABLE:
-                which = TetherMainSM.EVENT_IFACE_SERVING_STATE_INACTIVE;
-                break;
-            case IpServer.STATE_TETHERED:
-            case IpServer.STATE_LOCAL_ONLY:
-                which = TetherMainSM.EVENT_IFACE_SERVING_STATE_ACTIVE;
-                break;
-            default:
-                Log.wtf(TAG, "Unknown interface state: " + state);
+        @Override
+        public void updateLinkProperties(IpServer who, LinkProperties newLp) {
+            final String iface = who.interfaceName();
+            final int state;
+            final TetherState tetherState = mTetherStates.get(iface);
+            if (tetherState != null && tetherState.ipServer.equals(who)) {
+                state = tetherState.lastState;
+            } else {
+                mLog.log("got notification from stale iface " + iface);
                 return;
-        }
-        mTetherMainSM.sendMessage(which, state, 0, who);
-        sendTetherStateChangedBroadcast();
-    }
+            }
 
-    private void notifyLinkPropertiesChanged(IpServer who, LinkProperties newLp) {
-        final String iface = who.interfaceName();
-        final int state;
-        final TetherState tetherState = mTetherStates.get(iface);
-        if (tetherState != null && tetherState.ipServer.equals(who)) {
-            state = tetherState.lastState;
-        } else {
-            mLog.log("got notification from stale iface " + iface);
-            return;
+            mLog.log(String.format(
+                    "OBSERVED LinkProperties update iface=%s state=%s lp=%s",
+                    iface, IpServer.getStateString(state), newLp));
+            final int which = TetherMainSM.EVENT_IFACE_UPDATE_LINKPROPERTIES;
+            mTetherMainSM.sendMessage(which, state, 0, newLp);
         }
 
-        mLog.log(String.format(
-                "OBSERVED LinkProperties update iface=%s state=%s lp=%s",
-                iface, IpServer.getStateString(state), newLp));
-        final int which = TetherMainSM.EVENT_IFACE_UPDATE_LINKPROPERTIES;
-        mTetherMainSM.sendMessage(which, state, 0, newLp);
+        @Override
+        public void dhcpLeasesChanged() {
+            maybeDhcpLeasesChanged();
+        }
+
+        @Override
+        public void requestEnableTethering(int tetheringType, boolean enabled) {
+            mTetherMainSM.sendMessage(TetherMainSM.EVENT_REQUEST_CHANGE_DOWNSTREAM,
+                    tetheringType, 0, enabled ? Boolean.TRUE : Boolean.FALSE);
+        }
     }
 
     private boolean hasSystemFeature(final String feature) {
@@ -2851,9 +2820,9 @@
         mLog.i("adding IpServer for: " + iface);
         final TetherState tetherState = new TetherState(
                 new IpServer(iface, mHandler, interfaceType, mLog, mNetd, mBpfCoordinator,
-                        mRoutingCoordinator, makeControlCallback(), mConfig,
+                        mRoutingCoordinator, new ControlCallback(), mConfig,
                         mPrivateAddressCoordinator, mTetheringMetrics,
-                        mDeps.getIpServerDependencies()), isNcm);
+                        mDeps.makeIpServerDependencies()), isNcm);
         mTetherStates.put(iface, tetherState);
         tetherState.ipServer.start();
     }
@@ -2879,4 +2848,9 @@
             } catch (RemoteException e) { }
         });
     }
+
+    @VisibleForTesting
+    public TetherMainSM getTetherMainSMForTesting() {
+        return mTetherMainSM;
+    }
 }
diff --git a/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java b/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java
index 502fee8..298940e 100644
--- a/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java
+++ b/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java
@@ -130,6 +130,11 @@
     public static final String TETHER_ENABLE_WEAR_TETHERING =
             "tether_enable_wear_tethering";
 
+    public static final String TETHER_FORCE_RANDOM_PREFIX_BASE_SELECTION =
+            "tether_force_random_prefix_base_selection";
+
+    public static final String TETHER_ENABLE_SYNC_SM = "tether_enable_sync_sm";
+
     /**
      * Default value that used to periodic polls tether offload stats from tethering offload HAL
      * to make the data warnings work.
@@ -137,7 +142,7 @@
     public static final int DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS = 5000;
 
     /** A flag for using synchronous or asynchronous state machine. */
-    public static final boolean USE_SYNC_SM = false;
+    public static boolean USE_SYNC_SM = false;
 
     public final String[] tetherableUsbRegexs;
     public final String[] tetherableWifiRegexs;
@@ -171,6 +176,7 @@
     private final int mP2pLeasesSubnetPrefixLength;
 
     private final boolean mEnableWearTethering;
+    private final boolean mRandomPrefixBase;
 
     private final int mUsbTetheringFunction;
     protected final ContentResolver mContentResolver;
@@ -288,6 +294,8 @@
 
         mEnableWearTethering = shouldEnableWearTethering(ctx);
 
+        mRandomPrefixBase = mDeps.isFeatureEnabled(ctx, TETHER_FORCE_RANDOM_PREFIX_BASE_SELECTION);
+
         configLog.log(toString());
     }
 
@@ -376,6 +384,20 @@
         return mEnableWearTethering;
     }
 
+    public boolean isRandomPrefixBaseEnabled() {
+        return mRandomPrefixBase;
+    }
+
+    /**
+     * Check whether sync SM is enabled then set it to USE_SYNC_SM. This should be called once
+     * when tethering is created. Otherwise if the flag is pushed while tethering is enabled,
+     * then it's possible for some IpServer(s) running the new sync state machine while others
+     * use the async state machine.
+     */
+    public void readEnableSyncSM(final Context ctx) {
+        USE_SYNC_SM = mDeps.isFeatureEnabled(ctx, TETHER_ENABLE_SYNC_SM);
+    }
+
     /** Does the dumping.*/
     public void dump(PrintWriter pw) {
         pw.print("activeDataSubId: ");
@@ -426,6 +448,12 @@
 
         pw.print("mUsbTetheringFunction: ");
         pw.println(isUsingNcm() ? "NCM" : "RNDIS");
+
+        pw.print("mRandomPrefixBase: ");
+        pw.println(mRandomPrefixBase);
+
+        pw.print("USE_SYNC_SM: ");
+        pw.println(USE_SYNC_SM);
     }
 
     /** Returns the string representation of this object.*/
diff --git a/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java b/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java
index c274165..9dfd225 100644
--- a/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java
+++ b/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java
@@ -35,7 +35,6 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.RequiresApi;
 
-import com.android.internal.util.StateMachine;
 import com.android.modules.utils.build.SdkLevel;
 import com.android.net.module.util.SdkUtil.LateSdk;
 import com.android.net.module.util.SharedLog;
@@ -54,58 +53,58 @@
  */
 public abstract class TetheringDependencies {
     /**
-     * Get a reference to the BpfCoordinator to be used by tethering.
+     * Make the BpfCoordinator to be used by tethering.
      */
-    public @NonNull BpfCoordinator getBpfCoordinator(
+    public @NonNull BpfCoordinator makeBpfCoordinator(
             @NonNull BpfCoordinator.Dependencies deps) {
         return new BpfCoordinator(deps);
     }
 
     /**
-     * Get a reference to the offload hardware interface to be used by tethering.
+     * Make the offload hardware interface to be used by tethering.
      */
-    public OffloadHardwareInterface getOffloadHardwareInterface(Handler h, SharedLog log) {
+    public OffloadHardwareInterface makeOffloadHardwareInterface(Handler h, SharedLog log) {
         return new OffloadHardwareInterface(h, log);
     }
 
     /**
-     * Get a reference to the offload controller to be used by tethering.
+     * Make the offload controller to be used by tethering.
      */
     @NonNull
-    public OffloadController getOffloadController(@NonNull Handler h,
+    public OffloadController makeOffloadController(@NonNull Handler h,
             @NonNull SharedLog log, @NonNull OffloadController.Dependencies deps) {
         final NetworkStatsManager statsManager =
                 (NetworkStatsManager) getContext().getSystemService(Context.NETWORK_STATS_SERVICE);
-        return new OffloadController(h, getOffloadHardwareInterface(h, log),
+        return new OffloadController(h, makeOffloadHardwareInterface(h, log),
                 getContext().getContentResolver(), statsManager, log, deps);
     }
 
 
     /**
-     * Get a reference to the UpstreamNetworkMonitor to be used by tethering.
+     * Make the UpstreamNetworkMonitor to be used by tethering.
      */
-    public UpstreamNetworkMonitor getUpstreamNetworkMonitor(Context ctx, StateMachine target,
-            SharedLog log, int what) {
-        return new UpstreamNetworkMonitor(ctx, target, log, what);
+    public UpstreamNetworkMonitor makeUpstreamNetworkMonitor(Context ctx, Handler h,
+            SharedLog log, UpstreamNetworkMonitor.EventListener listener) {
+        return new UpstreamNetworkMonitor(ctx, h, log, listener);
     }
 
     /**
-     * Get a reference to the IPv6TetheringCoordinator to be used by tethering.
+     * Make the IPv6TetheringCoordinator to be used by tethering.
      */
-    public IPv6TetheringCoordinator getIPv6TetheringCoordinator(
+    public IPv6TetheringCoordinator makeIPv6TetheringCoordinator(
             ArrayList<IpServer> notifyList, SharedLog log) {
         return new IPv6TetheringCoordinator(notifyList, log);
     }
 
     /**
-     * Get dependencies to be used by IpServer.
+     * Make dependencies to be used by IpServer.
      */
-    public abstract IpServer.Dependencies getIpServerDependencies();
+    public abstract IpServer.Dependencies makeIpServerDependencies();
 
     /**
-     * Get a reference to the EntitlementManager to be used by tethering.
+     * Make the EntitlementManager to be used by tethering.
      */
-    public EntitlementManager getEntitlementManager(Context ctx, Handler h, SharedLog log,
+    public EntitlementManager makeEntitlementManager(Context ctx, Handler h, SharedLog log,
             Runnable callback) {
         return new EntitlementManager(ctx, h, log, callback);
     }
@@ -137,17 +136,17 @@
     }
 
     /**
-     * Get a reference to the TetheringNotificationUpdater to be used by tethering.
+     * Make the TetheringNotificationUpdater to be used by tethering.
      */
-    public TetheringNotificationUpdater getNotificationUpdater(@NonNull final Context ctx,
+    public TetheringNotificationUpdater makeNotificationUpdater(@NonNull final Context ctx,
             @NonNull final Looper looper) {
         return new TetheringNotificationUpdater(ctx, looper);
     }
 
     /**
-     * Get tethering thread looper.
+     * Make tethering thread looper.
      */
-    public abstract Looper getTetheringLooper();
+    public abstract Looper makeTetheringLooper();
 
     /**
      *  Get Context of TetheringService.
@@ -167,26 +166,26 @@
     }
 
     /**
-     * Get a reference to PrivateAddressCoordinator to be used by Tethering.
+     * Make PrivateAddressCoordinator to be used by Tethering.
      */
-    public PrivateAddressCoordinator getPrivateAddressCoordinator(Context ctx,
+    public PrivateAddressCoordinator makePrivateAddressCoordinator(Context ctx,
             TetheringConfiguration cfg) {
         return new PrivateAddressCoordinator(ctx, cfg);
     }
 
     /**
-     * Get BluetoothPanShim object to enable/disable bluetooth tethering.
+     * Make BluetoothPanShim object to enable/disable bluetooth tethering.
      *
      * TODO: use BluetoothPan directly when mainline module is built with API 32.
      */
-    public BluetoothPanShim getBluetoothPanShim(BluetoothPan pan) {
+    public BluetoothPanShim makeBluetoothPanShim(BluetoothPan pan) {
         return BluetoothPanShimImpl.newInstance(pan);
     }
 
     /**
-     * Get a reference to the TetheringMetrics to be used by tethering.
+     * Make the TetheringMetrics to be used by tethering.
      */
-    public TetheringMetrics getTetheringMetrics() {
+    public TetheringMetrics makeTetheringMetrics() {
         return new TetheringMetrics();
     }
 
@@ -194,7 +193,7 @@
      * Returns the implementation of WearableConnectionManager.
      */
     @RequiresApi(Build.VERSION_CODES.TIRAMISU)
-    public WearableConnectionManager getWearableConnectionManager(Context ctx) {
+    public WearableConnectionManager makeWearableConnectionManager(Context ctx) {
         return new WearableConnectionManager(ctx);
     }
 }
diff --git a/Tethering/src/com/android/networkstack/tethering/TetheringService.java b/Tethering/src/com/android/networkstack/tethering/TetheringService.java
index 96ddfa0..aa73819 100644
--- a/Tethering/src/com/android/networkstack/tethering/TetheringService.java
+++ b/Tethering/src/com/android/networkstack/tethering/TetheringService.java
@@ -322,7 +322,7 @@
     public TetheringDependencies makeTetheringDependencies() {
         return new TetheringDependencies() {
             @Override
-            public Looper getTetheringLooper() {
+            public Looper makeTetheringLooper() {
                 final HandlerThread tetherThread = new HandlerThread("android.tethering");
                 tetherThread.start();
                 return tetherThread.getLooper();
@@ -334,7 +334,7 @@
             }
 
             @Override
-            public IpServer.Dependencies getIpServerDependencies() {
+            public IpServer.Dependencies makeIpServerDependencies() {
                 return new IpServer.Dependencies() {
                     @Override
                     public void makeDhcpServer(String ifName, DhcpServingParamsParcel params,
diff --git a/Tethering/src/com/android/networkstack/tethering/UpstreamNetworkMonitor.java b/Tethering/src/com/android/networkstack/tethering/UpstreamNetworkMonitor.java
index ac2aa7b..7a05d74 100644
--- a/Tethering/src/com/android/networkstack/tethering/UpstreamNetworkMonitor.java
+++ b/Tethering/src/com/android/networkstack/tethering/UpstreamNetworkMonitor.java
@@ -44,7 +44,6 @@
 import androidx.annotation.Nullable;
 
 import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.util.StateMachine;
 import com.android.net.module.util.SharedLog;
 import com.android.networkstack.apishim.ConnectivityManagerShimImpl;
 import com.android.networkstack.apishim.common.ConnectivityManagerShim;
@@ -111,9 +110,8 @@
 
     private final Context mContext;
     private final SharedLog mLog;
-    private final StateMachine mTarget;
     private final Handler mHandler;
-    private final int mWhat;
+    private final EventListener mEventListener;
     private final HashMap<Network, UpstreamNetworkState> mNetworkMap = new HashMap<>();
     private HashSet<IpPrefix> mLocalPrefixes;
     private ConnectivityManager mCM;
@@ -135,12 +133,11 @@
     private Network mDefaultInternetNetwork;
     private boolean mPreferTestNetworks;
 
-    public UpstreamNetworkMonitor(Context ctx, StateMachine tgt, SharedLog log, int what) {
+    public UpstreamNetworkMonitor(Context ctx, Handler h, SharedLog log, EventListener listener) {
         mContext = ctx;
-        mTarget = tgt;
-        mHandler = mTarget.getHandler();
+        mHandler = h;
         mLog = log.forSubComponent(TAG);
-        mWhat = what;
+        mEventListener = listener;
         mLocalPrefixes = new HashSet<>();
         mIsDefaultCellularUpstream = false;
         mCM = (ConnectivityManager) ctx.getSystemService(Context.CONNECTIVITY_SERVICE);
@@ -374,11 +371,12 @@
                     network, newNc));
         }
 
-        mNetworkMap.put(network, new UpstreamNetworkState(
-                prev.linkProperties, newNc, network));
+        final UpstreamNetworkState uns =
+                new UpstreamNetworkState(prev.linkProperties, newNc, network);
+        mNetworkMap.put(network, uns);
         // TODO: If sufficient information is available to select a more
         // preferable upstream, do so now and notify the target.
-        notifyTarget(EVENT_ON_CAPABILITIES, network);
+        mEventListener.onUpstreamEvent(EVENT_ON_CAPABILITIES, uns);
     }
 
     private @Nullable UpstreamNetworkState updateLinkProperties(@NonNull Network network,
@@ -411,7 +409,7 @@
     private void handleLinkProp(Network network, LinkProperties newLp) {
         final UpstreamNetworkState ns = updateLinkProperties(network, newLp);
         if (ns != null) {
-            notifyTarget(EVENT_ON_LINKPROPERTIES, ns);
+            mEventListener.onUpstreamEvent(EVENT_ON_LINKPROPERTIES, ns);
         }
     }
 
@@ -438,7 +436,7 @@
         // preferable upstream, do so now and notify the target.  Likewise,
         // if the current upstream network is gone, notify the target of the
         // fact that we now have no upstream at all.
-        notifyTarget(EVENT_ON_LOST, mNetworkMap.remove(network));
+        mEventListener.onUpstreamEvent(EVENT_ON_LOST, mNetworkMap.remove(network));
     }
 
     private void maybeHandleNetworkSwitch(@NonNull Network network) {
@@ -456,14 +454,14 @@
         // Default network changed. Update local data and notify tethering.
         Log.d(TAG, "New default Internet network: " + network);
         mDefaultInternetNetwork = network;
-        notifyTarget(EVENT_DEFAULT_SWITCHED, ns);
+        mEventListener.onUpstreamEvent(EVENT_DEFAULT_SWITCHED, ns);
     }
 
     private void recomputeLocalPrefixes() {
         final HashSet<IpPrefix> localPrefixes = allLocalPrefixes(mNetworkMap.values());
         if (!mLocalPrefixes.equals(localPrefixes)) {
             mLocalPrefixes = localPrefixes;
-            notifyTarget(NOTIFY_LOCAL_PREFIXES, localPrefixes.clone());
+            mEventListener.onUpstreamEvent(NOTIFY_LOCAL_PREFIXES, localPrefixes.clone());
         }
     }
 
@@ -502,12 +500,13 @@
                 // onLinkPropertiesChanged right after this method and mDefaultInternetNetwork will
                 // be updated then.
                 //
-                // Technically, not updating here isn't necessary, because the notifications to
-                // Tethering sent by notifyTarget are messages sent to a state machine running on
-                // the same thread as this method, and so cannot arrive until after this method has
-                // returned. However, it is not a good idea to rely on that because fact that
-                // Tethering uses multiple state machines running on the same thread is a major
-                // source of race conditions and something that should be fixed.
+                // Technically, mDefaultInternetNetwork could be updated here, because the
+                // Callback#onChange implementation sends messages to the state machine running
+                // on the same thread as this method. If there is new default network change,
+                // the message cannot arrive until onLinkPropertiesChanged returns.
+                // However, it is not a good idea to rely on that because fact that Tethering uses
+                // multiple state machines running on the same thread is a major source of race
+                // conditions and something that should be fixed.
                 //
                 // TODO: is it correct that this code always updates EntitlementManager?
                 // This code runs when the default network connects or changes capabilities, but the
@@ -551,7 +550,7 @@
                 mIsDefaultCellularUpstream = false;
                 mEntitlementMgr.notifyUpstream(false);
                 Log.d(TAG, "Lost default Internet network: " + network);
-                notifyTarget(EVENT_DEFAULT_SWITCHED, null);
+                mEventListener.onUpstreamEvent(EVENT_DEFAULT_SWITCHED, null);
                 return;
             }
 
@@ -569,14 +568,6 @@
         if (cb != null) cm().unregisterNetworkCallback(cb);
     }
 
-    private void notifyTarget(int which, Network network) {
-        notifyTarget(which, mNetworkMap.get(network));
-    }
-
-    private void notifyTarget(int which, Object obj) {
-        mTarget.sendMessage(mWhat, which, 0, obj);
-    }
-
     private static class TypeStatePair {
         public int type = TYPE_NONE;
         public UpstreamNetworkState ns = null;
@@ -698,4 +689,10 @@
     public void setPreferTestNetworks(boolean prefer) {
         mPreferTestNetworks = prefer;
     }
+
+    /** An interface to notify upstream network changes. */
+    public interface EventListener {
+        /** Notify the client of some event */
+        void onUpstreamEvent(int what, Object obj);
+    }
 }
diff --git a/Tethering/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..f17396d 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"],
 }
 
@@ -50,7 +51,7 @@
     visibility: [
         "//packages/modules/Connectivity/Tethering/tests/mts",
         "//packages/modules/Connectivity/tests/cts/net",
-    ]
+    ],
 }
 
 // Library including tethering integration tests targeting the latest stable SDK.
@@ -67,7 +68,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.
@@ -83,7 +84,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/BpfCoordinatorTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java
index 6c1721e..47ecf58 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java
@@ -64,6 +64,7 @@
 import static com.android.networkstack.tethering.TetheringConfiguration.DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS;
 import static com.android.testutils.MiscAsserts.assertSameElements;
 
+import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
@@ -166,7 +167,6 @@
 import java.net.Inet4Address;
 import java.net.Inet6Address;
 import java.net.InetAddress;
-import java.nio.ByteBuffer;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashMap;
@@ -794,7 +794,7 @@
         if (!mDeps.isAtLeastS()) return;
         ArrayMap<TetherUpstream6Key, Tether6Value> expected = new ArrayMap<>();
         for (IpPrefix upstreamPrefix : upstreamPrefixes) {
-            long prefix64 = prefixToLong(upstreamPrefix);
+            final byte[] prefix64 = prefixToIp64(upstreamPrefix);
             final TetherUpstream6Key key = new TetherUpstream6Key(DOWNSTREAM_IFACE_PARAMS.index,
                     DOWNSTREAM_IFACE_PARAMS.macAddr, prefix64);
             final Tether6Value value = new Tether6Value(upstreamIfindex,
@@ -822,7 +822,7 @@
         if (!mDeps.isAtLeastS()) return;
         Set<TetherUpstream6Key> expected = new ArraySet<>();
         for (IpPrefix upstreamPrefix : upstreamPrefixes) {
-            long prefix64 = prefixToLong(upstreamPrefix);
+            final byte[] prefix64 = prefixToIp64(upstreamPrefix);
             final TetherUpstream6Key key = new TetherUpstream6Key(DOWNSTREAM_IFACE_PARAMS.index,
                     DOWNSTREAM_IFACE_PARAMS.macAddr, prefix64);
             expected.add(key);
@@ -1275,34 +1275,74 @@
     }
 
     @Test
-    public void testRuleMakeTetherDownstream6Key() throws Exception {
+    public void testIpv6DownstreamRuleMakeTetherDownstream6Key() throws Exception {
         final int mobileIfIndex = 100;
         final Ipv6DownstreamRule rule = buildTestDownstreamRule(mobileIfIndex, NEIGH_A, MAC_A);
 
         final TetherDownstream6Key key = rule.makeTetherDownstream6Key();
-        assertEquals(key.iif, mobileIfIndex);
-        assertEquals(key.dstMac, MacAddress.ALL_ZEROS_ADDRESS);  // rawip upstream
-        assertTrue(Arrays.equals(key.neigh6, NEIGH_A.getAddress()));
+        assertEquals(mobileIfIndex, key.iif);
+        assertEquals(MacAddress.ALL_ZEROS_ADDRESS, key.dstMac);  // rawip upstream
+        assertArrayEquals(NEIGH_A.getAddress(), key.neigh6);
         // iif (4) + dstMac(6) + padding(2) + neigh6 (16) = 28.
         assertEquals(28, key.writeToBytes().length);
     }
 
     @Test
-    public void testRuleMakeTether6Value() throws Exception {
+    public void testIpv6DownstreamRuleMakeTether6Value() throws Exception {
         final int mobileIfIndex = 100;
         final Ipv6DownstreamRule rule = buildTestDownstreamRule(mobileIfIndex, NEIGH_A, MAC_A);
 
         final Tether6Value value = rule.makeTether6Value();
-        assertEquals(value.oif, DOWNSTREAM_IFINDEX);
-        assertEquals(value.ethDstMac, MAC_A);
-        assertEquals(value.ethSrcMac, DOWNSTREAM_MAC);
-        assertEquals(value.ethProto, ETH_P_IPV6);
-        assertEquals(value.pmtu, NetworkStackConstants.ETHER_MTU);
-        // oif (4) + ethDstMac (6) + ethSrcMac (6) + ethProto (2) + pmtu (2) = 20.
+        assertEquals(DOWNSTREAM_IFINDEX, value.oif);
+        assertEquals(MAC_A, value.ethDstMac);
+        assertEquals(DOWNSTREAM_MAC, value.ethSrcMac);
+        assertEquals(ETH_P_IPV6, value.ethProto);
+        assertEquals(NetworkStackConstants.ETHER_MTU, value.pmtu);
+        // oif (4) + ethDstMac (6) + ethSrcMac (6) + ethProto (2) + pmtu (2) = 20
         assertEquals(20, value.writeToBytes().length);
     }
 
     @Test
+    public void testIpv6UpstreamRuleMakeTetherUpstream6Key() {
+        final byte[] bytes = new byte[]{(byte) 0x20, (byte) 0x01, (byte) 0x0d, (byte) 0xb8,
+                (byte) 0xab, (byte) 0xcd, (byte) 0xfe, (byte) 0x00};
+        final IpPrefix prefix = new IpPrefix("2001:db8:abcd:fe00::/64");
+        final Ipv6UpstreamRule rule = buildTestUpstreamRule(UPSTREAM_IFINDEX,
+                DOWNSTREAM_IFINDEX, prefix, DOWNSTREAM_MAC);
+
+        final TetherUpstream6Key key = rule.makeTetherUpstream6Key();
+        assertEquals(DOWNSTREAM_IFINDEX, key.iif);
+        assertEquals(DOWNSTREAM_MAC, key.dstMac);
+        assertArrayEquals(bytes, key.src64);
+        // iif (4) + dstMac (6) + padding (6) + src64 (8) = 24
+        assertEquals(24, key.writeToBytes().length);
+    }
+
+    @Test
+    public void testIpv6UpstreamRuleMakeTether6Value() {
+        final IpPrefix prefix = new IpPrefix("2001:db8:abcd:fe00::/64");
+        final Ipv6UpstreamRule rule = buildTestUpstreamRule(UPSTREAM_IFINDEX,
+                DOWNSTREAM_IFINDEX, prefix, DOWNSTREAM_MAC);
+
+        final Tether6Value value = rule.makeTether6Value();
+        assertEquals(UPSTREAM_IFINDEX, value.oif);
+        assertEquals(MAC_NULL, value.ethDstMac);
+        assertEquals(MAC_NULL, value.ethSrcMac);
+        assertEquals(ETH_P_IPV6, value.ethProto);
+        assertEquals(NetworkStackConstants.ETHER_MTU, value.pmtu);
+        // oif (4) + ethDstMac (6) + ethSrcMac (6) + ethProto (2) + pmtu (2) = 20
+        assertEquals(20, value.writeToBytes().length);
+    }
+
+    @Test
+    public void testBytesToPrefix() {
+        final byte[] bytes = new byte[]{(byte) 0x20, (byte) 0x01, (byte) 0x0d, (byte) 0xb8,
+                (byte) 0x00, (byte) 0x00, (byte) 0x12, (byte) 0x34};
+        final IpPrefix prefix = new IpPrefix("2001:db8:0:1234::/64");
+        assertEquals(prefix, BpfCoordinator.bytesToPrefix(bytes));
+    }
+
+    @Test
     public void testSetDataLimit() throws Exception {
         setupFunctioningNetdInterface();
 
@@ -2269,8 +2309,8 @@
                 100 /* nonzero, CT_NEW */);
     }
 
-    private static long prefixToLong(IpPrefix prefix) {
-        return ByteBuffer.wrap(prefix.getRawAddress()).getLong();
+    private static byte[] prefixToIp64(IpPrefix prefix) {
+        return Arrays.copyOf(prefix.getRawAddress(), 8);
     }
 
     void checkRule4ExistInUpstreamDownstreamMap() throws Exception {
@@ -2515,7 +2555,7 @@
         final Ipv6DownstreamRule rule = buildTestDownstreamRule(UPSTREAM_IFINDEX, NEIGH_A, MAC_A);
         mBpfDownstream6Map.insertEntry(rule.makeTetherDownstream6Key(), rule.makeTether6Value());
 
-        final long prefix64 = prefixToLong(UPSTREAM_PREFIX);
+        final byte[] prefix64 = prefixToIp64(UPSTREAM_PREFIX);
         final TetherUpstream6Key upstream6Key = new TetherUpstream6Key(DOWNSTREAM_IFINDEX,
                 DOWNSTREAM_MAC, prefix64);
         final Tether6Value upstream6Value = new Tether6Value(UPSTREAM_IFINDEX,
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/PrivateAddressCoordinatorTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/PrivateAddressCoordinatorTest.java
index 6ebd6ae..2298a1a 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/PrivateAddressCoordinatorTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/PrivateAddressCoordinatorTest.java
@@ -30,6 +30,7 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertTrue;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.spy;
@@ -104,6 +105,7 @@
         when(mContext.getSystemService(Context.CONNECTIVITY_SERVICE)).thenReturn(mConnectivityMgr);
         when(mConnectivityMgr.getAllNetworks()).thenReturn(mAllNetworks);
         when(mConfig.shouldEnableWifiP2pDedicatedIp()).thenReturn(false);
+        when(mConfig.isRandomPrefixBaseEnabled()).thenReturn(false);
         setUpIpServers();
         mPrivateAddressCoordinator = spy(new PrivateAddressCoordinator(mContext, mConfig));
     }
@@ -572,4 +574,41 @@
         assertEquals("Wrong local hotspot prefix: ", new LinkAddress("192.168.134.5/24"),
                 localHotspotAddress);
     }
+
+    @Test
+    public void testStartedPrefixRange() throws Exception {
+        when(mConfig.isRandomPrefixBaseEnabled()).thenReturn(true);
+
+        startedPrefixBaseTest("192.168.0.0/16", 0);
+
+        startedPrefixBaseTest("192.168.0.0/16", 1);
+
+        startedPrefixBaseTest("192.168.0.0/16", 0xffff);
+
+        startedPrefixBaseTest("172.16.0.0/12", 0x10000);
+
+        startedPrefixBaseTest("172.16.0.0/12", 0x11111);
+
+        startedPrefixBaseTest("172.16.0.0/12", 0xfffff);
+
+        startedPrefixBaseTest("10.0.0.0/8", 0x100000);
+
+        startedPrefixBaseTest("10.0.0.0/8", 0x1fffff);
+
+        startedPrefixBaseTest("10.0.0.0/8", 0xffffff);
+
+        startedPrefixBaseTest("192.168.0.0/16", 0x1000000);
+    }
+
+    private void startedPrefixBaseTest(final String expected, final int randomIntForPrefixBase)
+            throws Exception {
+        mPrivateAddressCoordinator = spy(new PrivateAddressCoordinator(mContext, mConfig));
+        when(mPrivateAddressCoordinator.getRandomInt()).thenReturn(randomIntForPrefixBase);
+        final LinkAddress address = requestDownstreamAddress(mHotspotIpServer,
+                CONNECTIVITY_SCOPE_GLOBAL, false /* useLastAddress */);
+        final IpPrefix prefixBase = new IpPrefix(expected);
+        assertTrue(address + " is not part of " + prefixBase,
+                prefixBase.containsPrefix(asIpPrefix(address)));
+
+    }
 }
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringConfigurationTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringConfigurationTest.java
index aa322dc..dd51c7a 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringConfigurationTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringConfigurationTest.java
@@ -754,4 +754,27 @@
                 new TetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID, mDeps);
         assertEquals(p2pLeasesSubnetPrefixLength, p2pCfg.getP2pLeasesSubnetPrefixLength());
     }
+
+    private void setTetherEnableSyncSMFlagEnabled(Boolean enabled) {
+        mDeps.setFeatureEnabled(TetheringConfiguration.TETHER_ENABLE_SYNC_SM, enabled);
+        new TetheringConfiguration(
+                mMockContext, mLog, INVALID_SUBSCRIPTION_ID, mDeps).readEnableSyncSM(mMockContext);
+    }
+
+    private void assertEnableSyncSM(boolean value) {
+        assertEquals(value, TetheringConfiguration.USE_SYNC_SM);
+    }
+
+    @Test
+    public void testEnableSyncSMFlag() throws Exception {
+        // Test default disabled
+        setTetherEnableSyncSMFlagEnabled(null);
+        assertEnableSyncSM(false);
+
+        setTetherEnableSyncSMFlagEnabled(true);
+        assertEnableSyncSM(true);
+
+        setTetherEnableSyncSMFlagEnabled(false);
+        assertEnableSyncSM(false);
+    }
 }
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
index 6eba590..750bfce 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
@@ -187,7 +187,6 @@
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
-import com.android.internal.util.StateMachine;
 import com.android.internal.util.test.BroadcastInterceptingContext;
 import com.android.internal.util.test.FakeSettingsProvider;
 import com.android.net.module.util.CollectionUtils;
@@ -310,6 +309,7 @@
     private BroadcastReceiver mBroadcastReceiver;
     private Tethering mTethering;
     private TestTetheringEventCallback mTetheringEventCallback;
+    private Tethering.TetherMainSM mTetherMainSM;
     private PhoneStateListener mPhoneStateListener;
     private InterfaceConfigurationParcel mInterfaceConfiguration;
     private TetheringConfiguration mConfig;
@@ -319,6 +319,7 @@
     private SoftApCallback mSoftApCallback;
     private SoftApCallback mLocalOnlyHotspotCallback;
     private UpstreamNetworkMonitor mUpstreamNetworkMonitor;
+    private UpstreamNetworkMonitor.EventListener mEventListener;
     private TetheredInterfaceCallbackShim mTetheredInterfaceCallbackShim;
 
     private TestConnectivityManager mCm;
@@ -432,55 +433,54 @@
     }
 
     public class MockTetheringDependencies extends TetheringDependencies {
-        StateMachine mUpstreamNetworkMonitorSM;
         ArrayList<IpServer> mAllDownstreams;
 
         @Override
-        public BpfCoordinator getBpfCoordinator(
+        public BpfCoordinator makeBpfCoordinator(
                 BpfCoordinator.Dependencies deps) {
             return mBpfCoordinator;
         }
 
         @Override
-        public OffloadHardwareInterface getOffloadHardwareInterface(Handler h, SharedLog log) {
+        public OffloadHardwareInterface makeOffloadHardwareInterface(Handler h, SharedLog log) {
             return mOffloadHardwareInterface;
         }
 
         @Override
-        public OffloadController getOffloadController(Handler h, SharedLog log,
+        public OffloadController makeOffloadController(Handler h, SharedLog log,
                 OffloadController.Dependencies deps) {
-            mOffloadCtrl = spy(super.getOffloadController(h, log, deps));
+            mOffloadCtrl = spy(super.makeOffloadController(h, log, deps));
             // Return real object here instead of mock because
             // testReportFailCallbackIfOffloadNotSupported depend on real OffloadController object.
             return mOffloadCtrl;
         }
 
         @Override
-        public UpstreamNetworkMonitor getUpstreamNetworkMonitor(Context ctx,
-                StateMachine target, SharedLog log, int what) {
+        public UpstreamNetworkMonitor makeUpstreamNetworkMonitor(Context ctx, Handler h,
+                SharedLog log, UpstreamNetworkMonitor.EventListener listener) {
             // Use a real object instead of a mock so that some tests can use a real UNM and some
             // can use a mock.
-            mUpstreamNetworkMonitorSM = target;
-            mUpstreamNetworkMonitor = spy(super.getUpstreamNetworkMonitor(ctx, target, log, what));
+            mEventListener = listener;
+            mUpstreamNetworkMonitor = spy(super.makeUpstreamNetworkMonitor(ctx, h, log, listener));
             return mUpstreamNetworkMonitor;
         }
 
         @Override
-        public IPv6TetheringCoordinator getIPv6TetheringCoordinator(
+        public IPv6TetheringCoordinator makeIPv6TetheringCoordinator(
                 ArrayList<IpServer> notifyList, SharedLog log) {
             mAllDownstreams = notifyList;
             return mIPv6TetheringCoordinator;
         }
 
         @Override
-        public IpServer.Dependencies getIpServerDependencies() {
+        public IpServer.Dependencies makeIpServerDependencies() {
             return mIpServerDependencies;
         }
 
         @Override
-        public EntitlementManager getEntitlementManager(Context ctx, Handler h, SharedLog log,
+        public EntitlementManager makeEntitlementManager(Context ctx, Handler h, SharedLog log,
                 Runnable callback) {
-            mEntitleMgr = spy(super.getEntitlementManager(ctx, h, log, callback));
+            mEntitleMgr = spy(super.makeEntitlementManager(ctx, h, log, callback));
             return mEntitleMgr;
         }
 
@@ -503,7 +503,7 @@
         }
 
         @Override
-        public Looper getTetheringLooper() {
+        public Looper makeTetheringLooper() {
             return mLooper.getLooper();
         }
 
@@ -518,7 +518,7 @@
         }
 
         @Override
-        public TetheringNotificationUpdater getNotificationUpdater(Context ctx, Looper looper) {
+        public TetheringNotificationUpdater makeNotificationUpdater(Context ctx, Looper looper) {
             return mNotificationUpdater;
         }
 
@@ -528,19 +528,19 @@
         }
 
         @Override
-        public TetheringMetrics getTetheringMetrics() {
+        public TetheringMetrics makeTetheringMetrics() {
             return mTetheringMetrics;
         }
 
         @Override
-        public PrivateAddressCoordinator getPrivateAddressCoordinator(Context ctx,
+        public PrivateAddressCoordinator makePrivateAddressCoordinator(Context ctx,
                 TetheringConfiguration cfg) {
-            mPrivateAddressCoordinator = super.getPrivateAddressCoordinator(ctx, cfg);
+            mPrivateAddressCoordinator = super.makePrivateAddressCoordinator(ctx, cfg);
             return mPrivateAddressCoordinator;
         }
 
         @Override
-        public BluetoothPanShim getBluetoothPanShim(BluetoothPan pan) {
+        public BluetoothPanShim makeBluetoothPanShim(BluetoothPan pan) {
             try {
                 when(mBluetoothPanShim.requestTetheredInterface(
                         any(), any())).thenReturn(mTetheredInterfaceRequestShim);
@@ -688,6 +688,7 @@
     private void initTetheringOnTestThread() throws Exception {
         mLooper = new TestLooper();
         mTethering = new Tethering(mTetheringDependencies);
+        mTetherMainSM = mTethering.getTetherMainSMForTesting();
         verify(mStatsManager, times(1)).registerNetworkStatsProvider(anyString(), any());
         verify(mNetd).registerUnsolicitedEventListener(any());
         verifyDefaultNetworkRequestFiled();
@@ -1182,10 +1183,7 @@
         initTetheringUpstream(upstreamState);
 
         // Upstream LinkProperties changed: UpstreamNetworkMonitor sends EVENT_ON_LINKPROPERTIES.
-        mTetheringDependencies.mUpstreamNetworkMonitorSM.sendMessage(
-                Tethering.TetherMainSM.EVENT_UPSTREAM_CALLBACK,
-                UpstreamNetworkMonitor.EVENT_ON_LINKPROPERTIES,
-                0,
+        mEventListener.onUpstreamEvent(UpstreamNetworkMonitor.EVENT_ON_LINKPROPERTIES,
                 upstreamState);
         mLooper.dispatchAll();
 
@@ -2713,14 +2711,12 @@
     @Test
     public void testUpstreamNetworkChanged() throws Exception {
         initTetheringOnTestThread();
-        final Tethering.TetherMainSM stateMachine = (Tethering.TetherMainSM)
-                mTetheringDependencies.mUpstreamNetworkMonitorSM;
         final InOrder inOrder = inOrder(mNotificationUpdater);
 
         // Gain upstream.
         final UpstreamNetworkState upstreamState = buildMobileIPv4UpstreamState();
         initTetheringUpstream(upstreamState);
-        stateMachine.chooseUpstreamType(true);
+        mTetherMainSM.chooseUpstreamType(true);
         mTetheringEventCallback.expectUpstreamChanged(upstreamState.network);
         inOrder.verify(mNotificationUpdater)
                 .onUpstreamCapabilitiesChanged(upstreamState.networkCapabilities);
@@ -2728,7 +2724,7 @@
         // Set the upstream with the same network ID but different object and the same capability.
         final UpstreamNetworkState upstreamState2 = buildMobileIPv4UpstreamState();
         initTetheringUpstream(upstreamState2);
-        stateMachine.chooseUpstreamType(true);
+        mTetherMainSM.chooseUpstreamType(true);
         // Expect that no upstream change event and capabilities changed event.
         mTetheringEventCallback.assertNoUpstreamChangeCallback();
         inOrder.verify(mNotificationUpdater, never()).onUpstreamCapabilitiesChanged(any());
@@ -2738,17 +2734,17 @@
         assertFalse(upstreamState3.networkCapabilities.hasCapability(NET_CAPABILITY_VALIDATED));
         upstreamState3.networkCapabilities.addCapability(NET_CAPABILITY_VALIDATED);
         initTetheringUpstream(upstreamState3);
-        stateMachine.chooseUpstreamType(true);
+        mTetherMainSM.chooseUpstreamType(true);
         // Expect that no upstream change event and capabilities changed event.
         mTetheringEventCallback.assertNoUpstreamChangeCallback();
-        stateMachine.handleUpstreamNetworkMonitorCallback(EVENT_ON_CAPABILITIES, upstreamState3);
+        mTetherMainSM.handleUpstreamNetworkMonitorCallback(EVENT_ON_CAPABILITIES, upstreamState3);
         inOrder.verify(mNotificationUpdater)
                 .onUpstreamCapabilitiesChanged(upstreamState3.networkCapabilities);
 
 
         // Lose upstream.
         initTetheringUpstream(null);
-        stateMachine.chooseUpstreamType(true);
+        mTetherMainSM.chooseUpstreamType(true);
         mTetheringEventCallback.expectUpstreamChanged(NULL_NETWORK);
         inOrder.verify(mNotificationUpdater).onUpstreamCapabilitiesChanged(null);
     }
@@ -2756,17 +2752,15 @@
     @Test
     public void testUpstreamCapabilitiesChanged() throws Exception {
         initTetheringOnTestThread();
-        final Tethering.TetherMainSM stateMachine = (Tethering.TetherMainSM)
-                mTetheringDependencies.mUpstreamNetworkMonitorSM;
         final InOrder inOrder = inOrder(mNotificationUpdater);
         final UpstreamNetworkState upstreamState = buildMobileIPv4UpstreamState();
         initTetheringUpstream(upstreamState);
 
-        stateMachine.chooseUpstreamType(true);
+        mTetherMainSM.chooseUpstreamType(true);
         inOrder.verify(mNotificationUpdater)
                 .onUpstreamCapabilitiesChanged(upstreamState.networkCapabilities);
 
-        stateMachine.handleUpstreamNetworkMonitorCallback(EVENT_ON_CAPABILITIES, upstreamState);
+        mTetherMainSM.handleUpstreamNetworkMonitorCallback(EVENT_ON_CAPABILITIES, upstreamState);
         inOrder.verify(mNotificationUpdater)
                 .onUpstreamCapabilitiesChanged(upstreamState.networkCapabilities);
 
@@ -2775,7 +2769,7 @@
         // Expect that capability is changed with new capability VALIDATED.
         assertFalse(upstreamState.networkCapabilities.hasCapability(NET_CAPABILITY_VALIDATED));
         upstreamState.networkCapabilities.addCapability(NET_CAPABILITY_VALIDATED);
-        stateMachine.handleUpstreamNetworkMonitorCallback(EVENT_ON_CAPABILITIES, upstreamState);
+        mTetherMainSM.handleUpstreamNetworkMonitorCallback(EVENT_ON_CAPABILITIES, upstreamState);
         inOrder.verify(mNotificationUpdater)
                 .onUpstreamCapabilitiesChanged(upstreamState.networkCapabilities);
 
@@ -2784,7 +2778,7 @@
         final UpstreamNetworkState upstreamState2 = new UpstreamNetworkState(
                 upstreamState.linkProperties, upstreamState.networkCapabilities,
                 new Network(WIFI_NETID));
-        stateMachine.handleUpstreamNetworkMonitorCallback(EVENT_ON_CAPABILITIES, upstreamState2);
+        mTetherMainSM.handleUpstreamNetworkMonitorCallback(EVENT_ON_CAPABILITIES, upstreamState2);
         inOrder.verify(mNotificationUpdater, never()).onUpstreamCapabilitiesChanged(any());
     }
 
@@ -2816,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
@@ -2907,11 +2899,7 @@
             final String iface, final int transportType) {
         final UpstreamNetworkState upstream = buildV4UpstreamState(ipv4Address, network, iface,
                 transportType);
-        mTetheringDependencies.mUpstreamNetworkMonitorSM.sendMessage(
-                Tethering.TetherMainSM.EVENT_UPSTREAM_CALLBACK,
-                UpstreamNetworkMonitor.EVENT_ON_LINKPROPERTIES,
-                0,
-                upstream);
+        mEventListener.onUpstreamEvent(UpstreamNetworkMonitor.EVENT_ON_LINKPROPERTIES, upstream);
         mLooper.dispatchAll();
     }
 
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/UpstreamNetworkMonitorTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/UpstreamNetworkMonitorTest.java
index e756bd3..90fd709 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/UpstreamNetworkMonitorTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/UpstreamNetworkMonitorTest.java
@@ -30,10 +30,12 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.argThat;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.anyInt;
 import static org.mockito.Mockito.anyString;
+import static org.mockito.Mockito.inOrder;
 import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.times;
@@ -51,27 +53,23 @@
 import android.net.NetworkRequest;
 import android.os.Build;
 import android.os.Handler;
-import android.os.Looper;
-import android.os.Message;
 import android.os.test.TestLooper;
 
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
-import com.android.internal.util.State;
-import com.android.internal.util.StateMachine;
 import com.android.net.module.util.SharedLog;
 import com.android.networkstack.tethering.TestConnectivityManager.NetworkRequestInfo;
 import com.android.networkstack.tethering.TestConnectivityManager.TestNetworkAgent;
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter;
 
-import org.junit.After;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
+import org.mockito.InOrder;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
@@ -84,8 +82,6 @@
 @RunWith(AndroidJUnit4.class)
 @SmallTest
 public class UpstreamNetworkMonitorTest {
-    private static final int EVENT_UNM_UPDATE = 1;
-
     private static final boolean INCLUDES = true;
     private static final boolean EXCLUDES = false;
 
@@ -102,32 +98,24 @@
     @Mock private EntitlementManager mEntitleMgr;
     @Mock private IConnectivityManager mCS;
     @Mock private SharedLog mLog;
+    @Mock private UpstreamNetworkMonitor.EventListener mListener;
 
-    private TestStateMachine mSM;
     private TestConnectivityManager mCM;
     private UpstreamNetworkMonitor mUNM;
 
     private final TestLooper mLooper = new TestLooper();
+    private InOrder mCallbackOrder;
 
     @Before public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
-        reset(mContext);
-        reset(mCS);
-        reset(mLog);
         when(mLog.forSubComponent(anyString())).thenReturn(mLog);
         when(mEntitleMgr.isCellularUpstreamPermitted()).thenReturn(true);
 
+        mCallbackOrder = inOrder(mListener);
         mCM = spy(new TestConnectivityManager(mContext, mCS));
         when(mContext.getSystemService(eq(Context.CONNECTIVITY_SERVICE))).thenReturn(mCM);
-        mSM = new TestStateMachine(mLooper.getLooper());
-        mUNM = new UpstreamNetworkMonitor(mContext, mSM, mLog, EVENT_UNM_UPDATE);
-    }
-
-    @After public void tearDown() throws Exception {
-        if (mSM != null) {
-            mSM.quit();
-            mSM = null;
-        }
+        mUNM = new UpstreamNetworkMonitor(mContext, new Handler(mLooper.getLooper()), mLog,
+                mListener);
     }
 
     @Test
@@ -603,14 +591,17 @@
         mCM.makeDefaultNetwork(cellAgent);
         mLooper.dispatchAll();
         verifyCurrentLinkProperties(cellAgent);
-        int messageIndex = mSM.messages.size() - 1;
+        verifyNotifyNetworkCapabilitiesChange(cellAgent.networkCapabilities);
+        verifyNotifyLinkPropertiesChange(cellLp);
+        verifyNotifyDefaultSwitch(cellAgent);
+        verifyNoMoreInteractions(mListener);
 
         addLinkAddresses(cellLp, ipv6Addr1);
         mCM.sendLinkProperties(cellAgent, false /* updateDefaultFirst */);
         mLooper.dispatchAll();
         verifyCurrentLinkProperties(cellAgent);
-        verifyNotifyLinkPropertiesChange(messageIndex);
-        messageIndex = mSM.messages.size() - 1;
+        verifyNotifyLinkPropertiesChange(cellLp);
+        verifyNoMoreInteractions(mListener);
 
         removeLinkAddresses(cellLp, ipv6Addr1);
         addLinkAddresses(cellLp, ipv6Addr2);
@@ -618,7 +609,8 @@
         mLooper.dispatchAll();
         assertEquals(cellAgent.linkProperties, mUNM.getCurrentPreferredUpstream().linkProperties);
         verifyCurrentLinkProperties(cellAgent);
-        verifyNotifyLinkPropertiesChange(messageIndex);
+        verifyNotifyLinkPropertiesChange(cellLp);
+        verifyNoMoreInteractions(mListener);
     }
 
     private void verifyCurrentLinkProperties(TestNetworkAgent agent) {
@@ -626,12 +618,33 @@
         assertEquals(agent.linkProperties, mUNM.getCurrentPreferredUpstream().linkProperties);
     }
 
-    private void verifyNotifyLinkPropertiesChange(int lastMessageIndex) {
-        assertEquals(UpstreamNetworkMonitor.EVENT_ON_LINKPROPERTIES,
-                mSM.messages.get(++lastMessageIndex).arg1);
-        assertEquals(UpstreamNetworkMonitor.NOTIFY_LOCAL_PREFIXES,
-                mSM.messages.get(++lastMessageIndex).arg1);
-        assertEquals(lastMessageIndex + 1, mSM.messages.size());
+    private void verifyNotifyNetworkCapabilitiesChange(final NetworkCapabilities cap) {
+        mCallbackOrder.verify(mListener).onUpstreamEvent(
+                eq(UpstreamNetworkMonitor.EVENT_ON_CAPABILITIES),
+                argThat(uns -> uns instanceof UpstreamNetworkState
+                    && cap.equals(((UpstreamNetworkState) uns).networkCapabilities)));
+
+    }
+
+    private void verifyNotifyLinkPropertiesChange(final LinkProperties lp) {
+        mCallbackOrder.verify(mListener).onUpstreamEvent(
+                eq(UpstreamNetworkMonitor.EVENT_ON_LINKPROPERTIES),
+                argThat(uns -> uns instanceof UpstreamNetworkState
+                    && lp.equals(((UpstreamNetworkState) uns).linkProperties)));
+
+        mCallbackOrder.verify(mListener).onUpstreamEvent(
+                eq(UpstreamNetworkMonitor.NOTIFY_LOCAL_PREFIXES), any());
+    }
+
+    private void verifyNotifyDefaultSwitch(TestNetworkAgent agent) {
+        mCallbackOrder.verify(mListener).onUpstreamEvent(
+                eq(UpstreamNetworkMonitor.EVENT_DEFAULT_SWITCHED),
+                argThat(uns ->
+                    uns instanceof UpstreamNetworkState
+                    && agent.networkId.equals(((UpstreamNetworkState) uns).network)
+                    && agent.linkProperties.equals(((UpstreamNetworkState) uns).linkProperties)
+                    && agent.networkCapabilities.equals(
+                        ((UpstreamNetworkState) uns).networkCapabilities)));
     }
 
     private void addLinkAddresses(LinkProperties lp, String... addrs) {
@@ -673,33 +686,6 @@
         return false;
     }
 
-    public static class TestStateMachine extends StateMachine {
-        public final ArrayList<Message> messages = new ArrayList<>();
-        private final State mLoggingState = new LoggingState();
-
-        class LoggingState extends State {
-            @Override public void enter() {
-                messages.clear();
-            }
-
-            @Override public void exit() {
-                messages.clear();
-            }
-
-            @Override public boolean processMessage(Message msg) {
-                messages.add(msg);
-                return true;
-            }
-        }
-
-        public TestStateMachine(Looper looper) {
-            super("UpstreamNetworkMonitor.TestStateMachine", looper);
-            addState(mLoggingState);
-            setInitialState(mLoggingState);
-            super.start();
-        }
-    }
-
     static void assertPrefixSet(Set<IpPrefix> prefixes, boolean expectation, String... expected) {
         final Set<String> expectedSet = new HashSet<>();
         Collections.addAll(expectedSet, expected);
diff --git a/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/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..f4b4cae 100644
--- a/common/Android.bp
+++ b/common/Android.bp
@@ -15,12 +15,11 @@
 //
 
 package {
+    default_team: "trendy_team_fwk_core_networking",
     // See: http://go/android-license-faq
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
-build = ["TrunkStable.bp"]
-
 // This is a placeholder comment to avoid merge conflicts
 // as the above target may not exist
 // depending on the branch
@@ -49,5 +48,7 @@
     apex_available: [
         "com.android.tethering",
     ],
-    lint: { strict_updatability_linting: true },
+    lint: {
+        strict_updatability_linting: true,
+    },
 }
diff --git a/common/TrunkStable.bp b/common/FlaggedApi.bp
similarity index 79%
rename from common/TrunkStable.bp
rename to common/FlaggedApi.bp
index 56938fc..2cb9b2f 100644
--- a/common/TrunkStable.bp
+++ b/common/FlaggedApi.bp
@@ -1,5 +1,5 @@
 //
-// 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.
@@ -18,9 +18,5 @@
     name: "com.android.net.flags-aconfig",
     package: "com.android.net.flags",
     srcs: ["flags.aconfig"],
-}
-
-java_aconfig_library {
-    name: "connectivity_flags_aconfig_lib",
-    aconfig_declarations: "com.android.net.flags-aconfig",
+    visibility: ["//packages/modules/Connectivity:__subpackages__"],
 }
diff --git a/common/flags.aconfig b/common/flags.aconfig
index b85c2fe..8eb3cbf 100644
--- a/common/flags.aconfig
+++ b/common/flags.aconfig
@@ -1,11 +1,7 @@
 package: "com.android.net.flags"
 
-flag {
-  name: "track_multiple_network_activities"
-  namespace: "android_core_networking"
-  description: "NetworkActivityTracker tracks multiple networks including non default networks"
-  bug: "267870186"
-}
+# This file contains aconfig flags for FlaggedAPI annotations
+# Flags used from platform code must be in under frameworks
 
 flag {
   name: "forbidden_capability"
@@ -15,13 +11,6 @@
 }
 
 flag {
-  name: "nsd_expired_services_removal"
-  namespace: "android_core_networking"
-  description: "Remove expired services from MdnsServiceCache"
-  bug: "304649384"
-}
-
-flag {
   name: "set_data_saver_via_cm"
   namespace: "android_core_networking"
   description: "Set data saver through ConnectivityManager API"
@@ -41,3 +30,11 @@
   description: "Block network access for apps in a low importance background state"
   bug: "304347838"
 }
+
+flag {
+  name: "register_nsd_offload_engine"
+  namespace: "android_core_networking"
+  description: "The flag controls the access for registerOffloadEngine API in NsdManager"
+  bug: "294777050"
+}
+
diff --git a/framework-t/Android.bp b/framework-t/Android.bp
index d177ea9..f485a44 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.
@@ -124,7 +130,6 @@
     defaults: [
         "framework-connectivity-t-defaults",
         "enable-framework-connectivity-t-targets",
-        "FlaggedApiDefaults",
     ],
     // Do not add static_libs to this library: put them in framework-connectivity instead.
     // The jarjar rules are only so that references to jarjared utils in
@@ -183,10 +188,14 @@
         "//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",
+    ],
 }
 
 // This rule is not used anymore(b/268440216).
diff --git a/framework-t/api/current.txt b/framework-t/api/current.txt
index 86745d4..7cd3d4f 100644
--- a/framework-t/api/current.txt
+++ b/framework-t/api/current.txt
@@ -127,6 +127,7 @@
 
   public final class IpSecTransform implements java.lang.AutoCloseable {
     method public void close();
+    method @FlaggedApi("com.android.net.flags.ipsec_transform_state") public void getIpSecTransformState(@NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<android.net.IpSecTransformState,java.lang.RuntimeException>);
   }
 
   public static class IpSecTransform.Builder {
@@ -138,6 +139,29 @@
     method @NonNull public android.net.IpSecTransform.Builder setIpv4Encapsulation(@NonNull android.net.IpSecManager.UdpEncapsulationSocket, int);
   }
 
+  @FlaggedApi("com.android.net.flags.ipsec_transform_state") public final class IpSecTransformState implements android.os.Parcelable {
+    method public int describeContents();
+    method public long getByteCount();
+    method public long getPacketCount();
+    method @NonNull public byte[] getReplayBitmap();
+    method public long getRxHighestSequenceNumber();
+    method public long getTimestamp();
+    method public long getTxHighestSequenceNumber();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.net.IpSecTransformState> CREATOR;
+  }
+
+  @FlaggedApi("com.android.net.flags.ipsec_transform_state") public static final class IpSecTransformState.Builder {
+    ctor public IpSecTransformState.Builder();
+    method @NonNull public android.net.IpSecTransformState build();
+    method @NonNull public android.net.IpSecTransformState.Builder setByteCount(long);
+    method @NonNull public android.net.IpSecTransformState.Builder setPacketCount(long);
+    method @NonNull public android.net.IpSecTransformState.Builder setReplayBitmap(@NonNull byte[]);
+    method @NonNull public android.net.IpSecTransformState.Builder setRxHighestSequenceNumber(long);
+    method @NonNull public android.net.IpSecTransformState.Builder setTimestamp(long);
+    method @NonNull public android.net.IpSecTransformState.Builder setTxHighestSequenceNumber(long);
+  }
+
   public class TrafficStats {
     ctor public TrafficStats();
     method public static void clearThreadStatsTag();
@@ -186,9 +210,26 @@
 
 package android.net.nsd {
 
+  @FlaggedApi("com.android.net.flags.nsd_subtypes_support_enabled") public final class DiscoveryRequest implements android.os.Parcelable {
+    method public int describeContents();
+    method @Nullable public android.net.Network getNetwork();
+    method @NonNull public String getServiceType();
+    method @Nullable public String getSubtype();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.net.nsd.DiscoveryRequest> CREATOR;
+  }
+
+  public static final class DiscoveryRequest.Builder {
+    ctor public DiscoveryRequest.Builder(@NonNull String);
+    method @NonNull public android.net.nsd.DiscoveryRequest build();
+    method @NonNull public android.net.nsd.DiscoveryRequest.Builder setNetwork(@Nullable android.net.Network);
+    method @NonNull public android.net.nsd.DiscoveryRequest.Builder setSubtype(@Nullable String);
+  }
+
   public final class NsdManager {
     method public void discoverServices(String, int, android.net.nsd.NsdManager.DiscoveryListener);
     method public void discoverServices(@NonNull String, int, @Nullable android.net.Network, @NonNull java.util.concurrent.Executor, @NonNull android.net.nsd.NsdManager.DiscoveryListener);
+    method @FlaggedApi("com.android.net.flags.nsd_subtypes_support_enabled") public void discoverServices(@NonNull android.net.nsd.DiscoveryRequest, @NonNull java.util.concurrent.Executor, @NonNull android.net.nsd.NsdManager.DiscoveryListener);
     method @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public void discoverServices(@NonNull String, int, @NonNull android.net.NetworkRequest, @NonNull java.util.concurrent.Executor, @NonNull android.net.nsd.NsdManager.DiscoveryListener);
     method public void registerService(android.net.nsd.NsdServiceInfo, int, android.net.nsd.NsdManager.RegistrationListener);
     method public void registerService(@NonNull android.net.nsd.NsdServiceInfo, int, @NonNull java.util.concurrent.Executor, @NonNull android.net.nsd.NsdManager.RegistrationListener);
@@ -251,6 +292,7 @@
     method public int getPort();
     method public String getServiceName();
     method public String getServiceType();
+    method @FlaggedApi("com.android.net.flags.nsd_subtypes_support_enabled") @NonNull public java.util.Set<java.lang.String> getSubtypes();
     method public void removeAttribute(String);
     method public void setAttribute(String, String);
     method @Deprecated public void setHost(java.net.InetAddress);
@@ -259,6 +301,7 @@
     method public void setPort(int);
     method public void setServiceName(String);
     method public void setServiceType(String);
+    method @FlaggedApi("com.android.net.flags.nsd_subtypes_support_enabled") public void setSubtypes(@NonNull java.util.Set<java.lang.String>);
     method public void writeToParcel(android.os.Parcel, int);
     field @NonNull public static final android.os.Parcelable.Creator<android.net.nsd.NsdServiceInfo> CREATOR;
   }
diff --git a/framework-t/api/system-current.txt b/framework-t/api/system-current.txt
index b285d85..8251f85 100644
--- a/framework-t/api/system-current.txt
+++ b/framework-t/api/system-current.txt
@@ -378,11 +378,11 @@
 package android.net.nsd {
 
   public final class NsdManager {
-    method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_STACK}) public void registerOffloadEngine(@NonNull String, long, long, @NonNull java.util.concurrent.Executor, @NonNull android.net.nsd.OffloadEngine);
-    method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_STACK}) public void unregisterOffloadEngine(@NonNull android.net.nsd.OffloadEngine);
+    method @FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api") @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_STACK}) public void registerOffloadEngine(@NonNull String, long, long, @NonNull java.util.concurrent.Executor, @NonNull android.net.nsd.OffloadEngine);
+    method @FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api") @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_STACK}) public void unregisterOffloadEngine(@NonNull android.net.nsd.OffloadEngine);
   }
 
-  public interface OffloadEngine {
+  @FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api") public interface OffloadEngine {
     method public void onOffloadServiceRemoved(@NonNull android.net.nsd.OffloadServiceInfo);
     method public void onOffloadServiceUpdated(@NonNull android.net.nsd.OffloadServiceInfo);
     field public static final int OFFLOAD_CAPABILITY_BYPASS_MULTICAST_LOCK = 1; // 0x1
@@ -391,7 +391,7 @@
     field public static final int OFFLOAD_TYPE_REPLY = 1; // 0x1
   }
 
-  public final class OffloadServiceInfo implements android.os.Parcelable {
+  @FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api") public final class OffloadServiceInfo implements android.os.Parcelable {
     ctor public OffloadServiceInfo(@NonNull android.net.nsd.OffloadServiceInfo.Key, @NonNull java.util.List<java.lang.String>, @NonNull String, @Nullable byte[], @IntRange(from=0, to=java.lang.Integer.MAX_VALUE) int, long);
     method public int describeContents();
     method @NonNull public String getHostname();
@@ -500,13 +500,17 @@
     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 public void unregisterOperationalDatasetCallback(@NonNull android.net.thread.ThreadNetworkController.OperationalDatasetCallback);
-    method public void unregisterStateCallback(@NonNull android.net.thread.ThreadNetworkController.StateCallback);
+    method @RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED") public void setEnabled(boolean, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.net.thread.ThreadNetworkException>);
+    method @RequiresPermission(allOf={android.Manifest.permission.ACCESS_NETWORK_STATE, "android.permission.THREAD_NETWORK_PRIVILEGED"}) public void unregisterOperationalDatasetCallback(@NonNull android.net.thread.ThreadNetworkController.OperationalDatasetCallback);
+    method @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public void unregisterStateCallback(@NonNull android.net.thread.ThreadNetworkController.StateCallback);
     field public static final int DEVICE_ROLE_CHILD = 2; // 0x2
     field public static final int DEVICE_ROLE_DETACHED = 1; // 0x1
     field public static final int DEVICE_ROLE_LEADER = 4; // 0x4
     field public static final int DEVICE_ROLE_ROUTER = 3; // 0x3
     field public static final int DEVICE_ROLE_STOPPED = 0; // 0x0
+    field public static final int STATE_DISABLED = 0; // 0x0
+    field public static final int STATE_DISABLING = 2; // 0x2
+    field public static final int STATE_ENABLED = 1; // 0x1
     field public static final int THREAD_VERSION_1_3 = 4; // 0x4
   }
 
@@ -518,6 +522,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 +535,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/IIpSecService.aidl b/framework-t/src/android/net/IIpSecService.aidl
index 88ffd0e..f972ab9 100644
--- a/framework-t/src/android/net/IIpSecService.aidl
+++ b/framework-t/src/android/net/IIpSecService.aidl
@@ -22,6 +22,7 @@
 import android.net.IpSecUdpEncapResponse;
 import android.net.IpSecSpiResponse;
 import android.net.IpSecTransformResponse;
+import android.net.IpSecTransformState;
 import android.net.IpSecTunnelInterfaceResponse;
 import android.os.Bundle;
 import android.os.IBinder;
@@ -74,6 +75,8 @@
 
     void deleteTransform(int transformId);
 
+    IpSecTransformState getTransformState(int transformId);
+
     void applyTransportModeTransform(
             in ParcelFileDescriptor socket, int direction, int transformId);
 
diff --git a/framework-t/src/android/net/IpSecManager.java b/framework-t/src/android/net/IpSecManager.java
index 3afa6ef..3f74e1c 100644
--- a/framework-t/src/android/net/IpSecManager.java
+++ b/framework-t/src/android/net/IpSecManager.java
@@ -65,6 +65,13 @@
 public class IpSecManager {
     private static final String TAG = "IpSecManager";
 
+    // TODO : remove this class when udc-mainline-prod is abandoned and android.net.flags.Flags is
+    // available here
+    /** @hide */
+    public static class Flags {
+        static final String IPSEC_TRANSFORM_STATE = "com.android.net.flags.ipsec_transform_state";
+    }
+
     /**
      * Feature flag to declare the kernel support of updating IPsec SAs.
      *
@@ -1084,6 +1091,12 @@
         }
     }
 
+    /** @hide */
+    public IpSecTransformState getTransformState(int transformId)
+            throws IllegalStateException, RemoteException {
+        return mService.getTransformState(transformId);
+    }
+
     /**
      * Construct an instance of IpSecManager within an application context.
      *
diff --git a/framework-t/src/android/net/IpSecTransform.java b/framework-t/src/android/net/IpSecTransform.java
index c236b6c..246a2dd 100644
--- a/framework-t/src/android/net/IpSecTransform.java
+++ b/framework-t/src/android/net/IpSecTransform.java
@@ -15,8 +15,11 @@
  */
 package android.net;
 
+import static android.net.IpSecManager.Flags.IPSEC_TRANSFORM_STATE;
 import static android.net.IpSecManager.INVALID_RESOURCE_ID;
 
+import android.annotation.CallbackExecutor;
+import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -26,6 +29,8 @@
 import android.content.Context;
 import android.content.pm.PackageManager;
 import android.os.Binder;
+import android.os.OutcomeReceiver;
+import android.os.RemoteException;
 import android.os.ServiceSpecificException;
 import android.util.Log;
 
@@ -38,6 +43,7 @@
 import java.lang.annotation.RetentionPolicy;
 import java.net.InetAddress;
 import java.util.Objects;
+import java.util.concurrent.Executor;
 
 /**
  * This class represents a transform, which roughly corresponds to an IPsec Security Association.
@@ -201,6 +207,43 @@
     }
 
     /**
+     * Retrieve the current state of this IpSecTransform.
+     *
+     * @param executor The {@link Executor} on which to call the supplied callback.
+     * @param callback Callback that's called after the transform state is ready or when an error
+     *     occurs.
+     * @see IpSecTransformState
+     */
+    @FlaggedApi(IPSEC_TRANSFORM_STATE)
+    public void getIpSecTransformState(
+            @CallbackExecutor @NonNull Executor executor,
+            @NonNull OutcomeReceiver<IpSecTransformState, RuntimeException> callback) {
+        Objects.requireNonNull(executor);
+        Objects.requireNonNull(callback);
+
+        // TODO: Consider adding check to prevent DDoS attack.
+
+        try {
+            final IpSecTransformState ipSecTransformState =
+                    getIpSecManager(mContext).getTransformState(mResourceId);
+            executor.execute(
+                    () -> {
+                        callback.onResult(ipSecTransformState);
+                    });
+        } catch (IllegalStateException e) {
+            executor.execute(
+                    () -> {
+                        callback.onError(e);
+                    });
+        } catch (RemoteException e) {
+            executor.execute(
+                    () -> {
+                        callback.onError(e.rethrowFromSystemServer());
+                    });
+        }
+    }
+
+    /**
      * A callback class to provide status information regarding a NAT-T keepalive session
      *
      * <p>Use this callback to receive status information regarding a NAT-T keepalive session
diff --git a/framework-t/src/android/net/IpSecTransformState.aidl b/framework-t/src/android/net/IpSecTransformState.aidl
new file mode 100644
index 0000000..69cce28
--- /dev/null
+++ b/framework-t/src/android/net/IpSecTransformState.aidl
@@ -0,0 +1,20 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+/** @hide */
+parcelable IpSecTransformState;
\ No newline at end of file
diff --git a/framework-t/src/android/net/IpSecTransformState.java b/framework-t/src/android/net/IpSecTransformState.java
new file mode 100644
index 0000000..b575dd5
--- /dev/null
+++ b/framework-t/src/android/net/IpSecTransformState.java
@@ -0,0 +1,282 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.net;
+
+import static android.net.IpSecManager.Flags.IPSEC_TRANSFORM_STATE;
+
+import static com.android.internal.annotations.VisibleForTesting.Visibility;
+
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.net.module.util.HexDump;
+
+import java.util.Objects;
+
+/**
+ * This class represents a snapshot of the state of an IpSecTransform
+ *
+ * <p>This class provides the current state of an IpSecTransform, enabling link metric analysis by
+ * the caller. Use cases include understanding transform usage, such as packet and byte counts, as
+ * well as observing out-of-order delivery by checking the bitmap. Additionally, callers can query
+ * IpSecTransformStates at two timestamps. By comparing the changes in packet counts and sequence
+ * numbers, callers can estimate IPsec data loss in the inbound direction.
+ */
+@FlaggedApi(IPSEC_TRANSFORM_STATE)
+public final class IpSecTransformState implements Parcelable {
+    private final long mTimeStamp;
+    private final long mTxHighestSequenceNumber;
+    private final long mRxHighestSequenceNumber;
+    private final long mPacketCount;
+    private final long mByteCount;
+    private final byte[] mReplayBitmap;
+
+    private IpSecTransformState(
+            long timestamp,
+            long txHighestSequenceNumber,
+            long rxHighestSequenceNumber,
+            long packetCount,
+            long byteCount,
+            byte[] replayBitmap) {
+        mTimeStamp = timestamp;
+        mTxHighestSequenceNumber = txHighestSequenceNumber;
+        mRxHighestSequenceNumber = rxHighestSequenceNumber;
+        mPacketCount = packetCount;
+        mByteCount = byteCount;
+
+        Objects.requireNonNull(replayBitmap, "replayBitmap is null");
+        mReplayBitmap = replayBitmap.clone();
+
+        validate();
+    }
+
+    private void validate() {
+        Objects.requireNonNull(mReplayBitmap, "mReplayBitmap is null");
+    }
+
+    /**
+     * Deserializes a IpSecTransformState from a PersistableBundle.
+     *
+     * @hide
+     */
+    @VisibleForTesting(visibility = Visibility.PRIVATE)
+    public IpSecTransformState(@NonNull Parcel in) {
+        Objects.requireNonNull(in, "The input PersistableBundle is null");
+        mTimeStamp = in.readLong();
+        mTxHighestSequenceNumber = in.readLong();
+        mRxHighestSequenceNumber = in.readLong();
+        mPacketCount = in.readLong();
+        mByteCount = in.readLong();
+        mReplayBitmap = HexDump.hexStringToByteArray(in.readString());
+
+        validate();
+    }
+
+    // Parcelable methods
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel out, int flags) {
+        out.writeLong(mTimeStamp);
+        out.writeLong(mTxHighestSequenceNumber);
+        out.writeLong(mRxHighestSequenceNumber);
+        out.writeLong(mPacketCount);
+        out.writeLong(mByteCount);
+        out.writeString(HexDump.toHexString(mReplayBitmap));
+    }
+
+    @NonNull
+    public static final Parcelable.Creator<IpSecTransformState> CREATOR =
+            new Parcelable.Creator<IpSecTransformState>() {
+                @NonNull
+                public IpSecTransformState createFromParcel(Parcel in) {
+                    return new IpSecTransformState(in);
+                }
+
+                @NonNull
+                public IpSecTransformState[] newArray(int size) {
+                    return new IpSecTransformState[size];
+                }
+            };
+
+    /**
+     * Retrieve the epoch timestamp (milliseconds) for when this state was created
+     *
+     * @see Builder#setTimestamp(long)
+     */
+    public long getTimestamp() {
+        return mTimeStamp;
+    }
+
+    /**
+     * Retrieve the highest sequence number sent so far
+     *
+     * @see Builder#setTxHighestSequenceNumber(long)
+     */
+    public long getTxHighestSequenceNumber() {
+        return mTxHighestSequenceNumber;
+    }
+
+    /**
+     * Retrieve the highest sequence number received so far
+     *
+     * @see Builder#setRxHighestSequenceNumber(long)
+     */
+    public long getRxHighestSequenceNumber() {
+        return mRxHighestSequenceNumber;
+    }
+
+    /**
+     * Retrieve the number of packets received AND sent so far
+     *
+     * @see Builder#setPacketCount(long)
+     */
+    public long getPacketCount() {
+        return mPacketCount;
+    }
+
+    /**
+     * Retrieve the number of bytes received AND sent so far
+     *
+     * @see Builder#setByteCount(long)
+     */
+    public long getByteCount() {
+        return mByteCount;
+    }
+
+    /**
+     * Retrieve the replay bitmap
+     *
+     * <p>This bitmap represents a replay window, allowing the caller to observe out-of-order
+     * delivery. The last bit represents the highest sequence number received so far and bits for
+     * the received packets will be marked as true.
+     *
+     * <p>The size of a replay bitmap will never change over the lifetime of an IpSecTransform
+     *
+     * <p>The replay bitmap is solely useful for inbound IpSecTransforms. For outbound
+     * IpSecTransforms, all bits will be unchecked.
+     *
+     * @see Builder#setReplayBitmap(byte[])
+     */
+    @NonNull
+    public byte[] getReplayBitmap() {
+        return mReplayBitmap.clone();
+    }
+
+    /** Builder class for testing purposes */
+    @FlaggedApi(IPSEC_TRANSFORM_STATE)
+    public static final class Builder {
+        private long mTimeStamp;
+        private long mTxHighestSequenceNumber;
+        private long mRxHighestSequenceNumber;
+        private long mPacketCount;
+        private long mByteCount;
+        private byte[] mReplayBitmap;
+
+        public Builder() {
+            mTimeStamp = System.currentTimeMillis();
+        }
+
+        /**
+         * Set the epoch timestamp (milliseconds) for when this state was created
+         *
+         * @see IpSecTransformState#getTimestamp()
+         */
+        @NonNull
+        public Builder setTimestamp(long timeStamp) {
+            mTimeStamp = timeStamp;
+            return this;
+        }
+
+        /**
+         * Set the highest sequence number sent so far
+         *
+         * @see IpSecTransformState#getTxHighestSequenceNumber()
+         */
+        @NonNull
+        public Builder setTxHighestSequenceNumber(long seqNum) {
+            mTxHighestSequenceNumber = seqNum;
+            return this;
+        }
+
+        /**
+         * Set the highest sequence number received so far
+         *
+         * @see IpSecTransformState#getRxHighestSequenceNumber()
+         */
+        @NonNull
+        public Builder setRxHighestSequenceNumber(long seqNum) {
+            mRxHighestSequenceNumber = seqNum;
+            return this;
+        }
+
+        /**
+         * Set the number of packets received AND sent so far
+         *
+         * @see IpSecTransformState#getPacketCount()
+         */
+        @NonNull
+        public Builder setPacketCount(long packetCount) {
+            mPacketCount = packetCount;
+            return this;
+        }
+
+        /**
+         * Set the number of bytes received AND sent so far
+         *
+         * @see IpSecTransformState#getByteCount()
+         */
+        @NonNull
+        public Builder setByteCount(long byteCount) {
+            mByteCount = byteCount;
+            return this;
+        }
+
+        /**
+         * Set the replay bitmap
+         *
+         * @see IpSecTransformState#getReplayBitmap()
+         */
+        @NonNull
+        public Builder setReplayBitmap(@NonNull byte[] bitMap) {
+            mReplayBitmap = bitMap.clone();
+            return this;
+        }
+
+        /**
+         * Build and validate the IpSecTransformState
+         *
+         * @return an immutable IpSecTransformState instance
+         */
+        @NonNull
+        public IpSecTransformState build() {
+            return new IpSecTransformState(
+                    mTimeStamp,
+                    mTxHighestSequenceNumber,
+                    mRxHighestSequenceNumber,
+                    mPacketCount,
+                    mByteCount,
+                    mReplayBitmap);
+        }
+    }
+}
diff --git a/framework-t/src/android/net/NetworkStatsAccess.java b/framework-t/src/android/net/NetworkStatsAccess.java
index 23902dc..7fe499b 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);
diff --git a/framework-t/src/android/net/NetworkStatsCollection.java b/framework-t/src/android/net/NetworkStatsCollection.java
index e23faa4..934b4c6 100644
--- a/framework-t/src/android/net/NetworkStatsCollection.java
+++ b/framework-t/src/android/net/NetworkStatsCollection.java
@@ -26,11 +26,13 @@
 import static android.net.NetworkStats.ROAMING_YES;
 import static android.net.NetworkStats.SET_ALL;
 import static android.net.NetworkStats.SET_DEFAULT;
+import static android.net.NetworkStats.SET_FOREGROUND;
 import static android.net.NetworkStats.TAG_NONE;
 import static android.net.NetworkStats.UID_ALL;
 import static android.net.NetworkTemplate.MATCH_BLUETOOTH;
 import static android.net.NetworkTemplate.MATCH_ETHERNET;
 import static android.net.NetworkTemplate.MATCH_MOBILE;
+import static android.net.NetworkTemplate.MATCH_PROXY;
 import static android.net.NetworkTemplate.MATCH_WIFI;
 import static android.net.TrafficStats.UID_REMOVED;
 import static android.text.format.DateUtils.WEEK_IN_MILLIS;
@@ -49,6 +51,7 @@
 import android.telephony.SubscriptionPlan;
 import android.text.format.DateUtils;
 import android.util.ArrayMap;
+import android.util.ArraySet;
 import android.util.AtomicFile;
 import android.util.IndentingPrintWriter;
 import android.util.Log;
@@ -57,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;
 
@@ -113,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();
     }
 
@@ -480,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 {
@@ -784,6 +805,7 @@
         dumpCheckin(pw, start, end, new NetworkTemplate.Builder(MATCH_WIFI).build(), "wifi");
         dumpCheckin(pw, start, end, new NetworkTemplate.Builder(MATCH_ETHERNET).build(), "eth");
         dumpCheckin(pw, start, end, new NetworkTemplate.Builder(MATCH_BLUETOOTH).build(), "bt");
+        dumpCheckin(pw, start, end, new NetworkTemplate.Builder(MATCH_PROXY).build(), "proxy");
     }
 
     /**
@@ -922,6 +944,100 @@
         }
     }
 
+
+    private static String str(NetworkStatsCollection.Key key) {
+        StringBuilder sb = new StringBuilder()
+                .append(key.ident.toString())
+                .append(" uid=").append(key.uid);
+        if (key.set != SET_FOREGROUND) {
+            sb.append(" set=").append(key.set);
+        }
+        if (key.tag != 0) {
+            sb.append(" tag=").append(key.tag);
+        }
+        return sb.toString();
+    }
+
+    // The importer will modify some keys when importing them.
+    // In order to keep the comparison code simple, add such special cases here and simply
+    // ignore them. This should not impact fidelity much because the start/end checks and the total
+    // bytes check still need to pass.
+    private static boolean couldKeyChangeOnImport(NetworkStatsCollection.Key key) {
+        if (key.ident.isEmpty()) return false;
+        final NetworkIdentity firstIdent = key.ident.iterator().next();
+
+        // Non-mobile network with non-empty RAT type.
+        // This combination is invalid and the NetworkIdentity.Builder will throw if it is passed
+        // in, but it looks like it was previously possible to persist it to disk. The importer sets
+        // the RAT type to NETWORK_TYPE_ALL.
+        if (firstIdent.getType() != ConnectivityManager.TYPE_MOBILE
+                && firstIdent.getRatType() != NetworkTemplate.NETWORK_TYPE_ALL) {
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * Compare two {@link NetworkStatsCollection} instances and returning a human-readable
+     * string description of difference for debugging purpose.
+     *
+     * @hide
+     */
+    @Nullable
+    public static String compareStats(NetworkStatsCollection migrated,
+                                      NetworkStatsCollection legacy, boolean allowKeyChange) {
+        final Map<NetworkStatsCollection.Key, NetworkStatsHistory> migEntries =
+                migrated.getEntries();
+        final Map<NetworkStatsCollection.Key, NetworkStatsHistory> legEntries = legacy.getEntries();
+
+        final ArraySet<NetworkStatsCollection.Key> unmatchedLegKeys =
+                new ArraySet<>(legEntries.keySet());
+
+        for (NetworkStatsCollection.Key legKey : legEntries.keySet()) {
+            final NetworkStatsHistory legHistory = legEntries.get(legKey);
+            final NetworkStatsHistory migHistory = migEntries.get(legKey);
+
+            if (migHistory == null && allowKeyChange && couldKeyChangeOnImport(legKey)) {
+                unmatchedLegKeys.remove(legKey);
+                continue;
+            }
+
+            if (migHistory == null) {
+                return "Missing migrated history for legacy key " + str(legKey)
+                        + ", legacy history was " + legHistory;
+            }
+            if (!migHistory.isSameAs(legHistory)) {
+                return "Difference in history for key " + legKey + "; legacy history " + legHistory
+                        + ", migrated history " + migHistory;
+            }
+            unmatchedLegKeys.remove(legKey);
+        }
+
+        if (!unmatchedLegKeys.isEmpty()) {
+            final NetworkStatsHistory first = legEntries.get(unmatchedLegKeys.valueAt(0));
+            return "Found unmatched legacy keys: count=" + unmatchedLegKeys.size()
+                    + ", first unmatched collection " + first;
+        }
+
+        if (migrated.getStartMillis() != legacy.getStartMillis()
+                || migrated.getEndMillis() != legacy.getEndMillis()) {
+            return "Start / end of the collections "
+                    + migrated.getStartMillis() + "/" + legacy.getStartMillis() + " and "
+                    + migrated.getEndMillis() + "/" + legacy.getEndMillis()
+                    + " don't match";
+        }
+
+        if (migrated.getTotalBytes() != legacy.getTotalBytes()) {
+            return "Total bytes " + migrated.getTotalBytes() + " and " + legacy.getTotalBytes()
+                    + " don't match for collections with start/end "
+                    + migrated.getStartMillis()
+                    + "/" + legacy.getStartMillis();
+        }
+
+        return null;
+    }
+
     /**
      * the identifier that associate with the {@link NetworkStatsHistory} object to identify
      * a certain record in the {@link NetworkStatsCollection} object.
diff --git a/framework-t/src/android/net/NetworkTemplate.java b/framework-t/src/android/net/NetworkTemplate.java
index 33bd884..77b166c 100644
--- a/framework-t/src/android/net/NetworkTemplate.java
+++ b/framework-t/src/android/net/NetworkTemplate.java
@@ -1170,7 +1170,7 @@
          * @param matchRule the target match rule to be checked.
          */
         private static void assertRequestableMatchRule(final int matchRule) {
-            if (!isKnownMatchRule(matchRule) || matchRule == MATCH_PROXY) {
+            if (!isKnownMatchRule(matchRule)) {
                 throw new IllegalArgumentException("Invalid match rule: "
                         + getMatchRuleName(matchRule));
             }
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..b1ef98f
--- /dev/null
+++ b/framework-t/src/android/net/nsd/AdvertisingRequest.java
@@ -0,0 +1,180 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.net.nsd;
+
+import android.annotation.LongDef;
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+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 int 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();
+                    return new AdvertisingRequest(serviceInfo, protocolType, advertiseConfig);
+                }
+
+                @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;
+
+    /**
+     * @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) {
+        mServiceInfo = serviceInfo;
+        mProtocolType = protocolType;
+        mAdvertisingConfig = advertisingConfig;
+    }
+
+    /**
+     * 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;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+        sb.append("serviceInfo: ").append(mServiceInfo)
+                .append(", protocolType: ").append(mProtocolType)
+                .append(", advertisingConfig: ").append(mAdvertisingConfig);
+        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;
+        }
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mServiceInfo, mProtocolType, mAdvertisingConfig);
+    }
+
+    @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);
+    }
+
+//    @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;
+        /**
+         * 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;
+        }
+
+
+        /** Creates a new {@link AdvertisingRequest} object. */
+        @NonNull
+        public AdvertisingRequest build() {
+            return new AdvertisingRequest(mServiceInfo, mProtocolType, mAdvertisingConfig);
+        }
+    }
+}
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/MDnsManager.java b/framework-t/src/android/net/nsd/MDnsManager.java
index c11e60c..c7ded25 100644
--- a/framework-t/src/android/net/nsd/MDnsManager.java
+++ b/framework-t/src/android/net/nsd/MDnsManager.java
@@ -51,7 +51,7 @@
     public void startDaemon() {
         try {
             mMdns.startDaemon();
-        } catch (RemoteException | ServiceSpecificException e) {
+        } catch (RemoteException | ServiceSpecificException | UnsupportedOperationException e) {
             Log.e(TAG, "Start mdns failed.", e);
         }
     }
@@ -62,7 +62,7 @@
     public void stopDaemon() {
         try {
             mMdns.stopDaemon();
-        } catch (RemoteException | ServiceSpecificException e) {
+        } catch (RemoteException | ServiceSpecificException | UnsupportedOperationException e) {
             Log.e(TAG, "Stop mdns failed.", e);
         }
     }
@@ -85,7 +85,7 @@
                 registrationType, port, txtRecord, interfaceIdx);
         try {
             mMdns.registerService(info);
-        } catch (RemoteException | ServiceSpecificException e) {
+        } catch (RemoteException | ServiceSpecificException | UnsupportedOperationException e) {
             Log.e(TAG, "Register service failed.", e);
             return false;
         }
@@ -105,7 +105,7 @@
                 registrationType, "" /* domainName */, interfaceIdx, NETID_UNSET);
         try {
             mMdns.discover(info);
-        } catch (RemoteException | ServiceSpecificException e) {
+        } catch (RemoteException | ServiceSpecificException | UnsupportedOperationException e) {
             Log.e(TAG, "Discover service failed.", e);
             return false;
         }
@@ -129,7 +129,7 @@
                 new byte[0] /* txtRecord */, interfaceIdx);
         try {
             mMdns.resolve(info);
-        } catch (RemoteException | ServiceSpecificException e) {
+        } catch (RemoteException | ServiceSpecificException | UnsupportedOperationException e) {
             Log.e(TAG, "Resolve service failed.", e);
             return false;
         }
@@ -149,7 +149,7 @@
                 "" /* address */, interfaceIdx, NETID_UNSET);
         try {
             mMdns.getServiceAddress(info);
-        } catch (RemoteException | ServiceSpecificException e) {
+        } catch (RemoteException | ServiceSpecificException | UnsupportedOperationException e) {
             Log.e(TAG, "Get service address failed.", e);
             return false;
         }
@@ -165,7 +165,7 @@
     public boolean stopOperation(int id) {
         try {
             mMdns.stopOperation(id);
-        } catch (RemoteException | ServiceSpecificException e) {
+        } catch (RemoteException | ServiceSpecificException | UnsupportedOperationException e) {
             Log.e(TAG, "Stop operation failed.", e);
             return false;
         }
@@ -180,7 +180,7 @@
     public void registerEventListener(@NonNull IMDnsEventListener listener) {
         try {
             mMdns.registerEventListener(listener);
-        } catch (RemoteException | ServiceSpecificException e) {
+        } catch (RemoteException | ServiceSpecificException | UnsupportedOperationException e) {
             Log.e(TAG, "Register listener failed.", e);
         }
     }
@@ -193,7 +193,7 @@
     public void unregisterEventListener(@NonNull IMDnsEventListener listener) {
         try {
             mMdns.unregisterEventListener(listener);
-        } catch (RemoteException | ServiceSpecificException e) {
+        } catch (RemoteException | ServiceSpecificException | UnsupportedOperationException e) {
             Log.e(TAG, "Unregister listener failed.", e);
         }
     }
diff --git a/framework-t/src/android/net/nsd/NsdManager.java b/framework-t/src/android/net/nsd/NsdManager.java
index ef0e34b..27b4955 100644
--- a/framework-t/src/android/net/nsd/NsdManager.java
+++ b/framework-t/src/android/net/nsd/NsdManager.java
@@ -22,6 +22,7 @@
 import static android.net.connectivity.ConnectivityCompatChanges.ENABLE_PLATFORM_MDNS_BACKEND;
 import static android.net.connectivity.ConnectivityCompatChanges.RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER;
 
+import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -34,10 +35,10 @@
 import android.content.Context;
 import android.net.ConnectivityManager;
 import android.net.ConnectivityManager.NetworkCallback;
+import android.net.ConnectivityThread;
 import android.net.Network;
 import android.net.NetworkRequest;
 import android.os.Handler;
-import android.os.HandlerThread;
 import android.os.Looper;
 import android.os.Message;
 import android.os.RemoteException;
@@ -45,17 +46,22 @@
 import android.util.ArrayMap;
 import android.util.ArraySet;
 import android.util.Log;
+import android.util.Pair;
 import android.util.SparseArray;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.modules.utils.build.SdkLevel;
 import com.android.net.module.util.CollectionUtils;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.util.ArrayList;
+import java.util.List;
 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
@@ -143,6 +149,47 @@
     private static final String TAG = NsdManager.class.getSimpleName();
     private static final boolean DBG = false;
 
+    // TODO : remove this class when udc-mainline-prod is abandoned and android.net.flags.Flags is
+    // available here
+    /** @hide */
+    public static class Flags {
+        static final String REGISTER_NSD_OFFLOAD_ENGINE_API =
+                "com.android.net.flags.register_nsd_offload_engine_api";
+        static final String NSD_SUBTYPES_SUPPORT_ENABLED =
+                "com.android.net.flags.nsd_subtypes_support_enabled";
+        static final String ADVERTISE_REQUEST_API =
+                "com.android.net.flags.advertise_request_api";
+        static final String NSD_CUSTOM_HOSTNAME_ENABLED =
+                "com.android.net.flags.nsd_custom_hostname_enabled";
+    }
+
+    /**
+     * A regex for the acceptable format of a type or subtype label.
+     * @hide
+     */
+    public static final String TYPE_SUBTYPE_LABEL_REGEX = "_[a-zA-Z0-9-_]{1,61}[a-zA-Z0-9]";
+
+    /**
+     * 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
+            "^(?:(" + TYPE_SUBTYPE_LABEL_REGEX + ")\\.)?"
+                    // Actual type (_type._tcp.local)
+                    + "(" + TYPE_SUBTYPE_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
+                    + "((?:," + TYPE_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
@@ -316,6 +363,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
@@ -365,6 +414,7 @@
      *
      * @hide
      */
+    @FlaggedApi(NsdManager.Flags.REGISTER_NSD_OFFLOAD_ENGINE_API)
     @SystemApi
     @RequiresPermission(anyOf = {NETWORK_SETTINGS, PERMISSION_MAINLINE_NETWORK_STACK,
             NETWORK_STACK})
@@ -402,6 +452,7 @@
      *
      * @hide
      */
+    @FlaggedApi(NsdManager.Flags.REGISTER_NSD_OFFLOAD_ENGINE_API)
     @SystemApi
     @RequiresPermission(anyOf = {NETWORK_SETTINGS, PERMISSION_MAINLINE_NETWORK_STACK,
             NETWORK_STACK})
@@ -632,10 +683,9 @@
      */
     public NsdManager(Context context, INsdManager service) {
         mContext = context;
-
-        HandlerThread t = new HandlerThread("NsdManager");
-        t.start();
-        mHandler = new ServiceHandler(t.getLooper());
+        // Use a common singleton thread ConnectivityThread to be shared among all nsd tasks.
+        // Instead of launching separate threads to handle tasks from the various instances.
+        mHandler = new ServiceHandler(ConnectivityThread.getInstanceLooper());
 
         try {
             mService = service.connect(new NsdCallbackImpl(mHandler), CompatChanges.isChangeEnabled(
@@ -644,9 +694,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) {
@@ -667,6 +720,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));
         }
@@ -676,8 +735,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
@@ -955,10 +1014,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) {
@@ -966,17 +1027,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(
@@ -991,12 +1057,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);
@@ -1069,17 +1135,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);
         }
@@ -1090,6 +1178,7 @@
         synchronized (mMapLock) {
             mListenerMap.remove(key);
             mServiceMap.remove(key);
+            mDiscoveryMap.remove(key);
             mExecutorMap.remove(key);
         }
     }
@@ -1105,9 +1194,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();
     }
 
     /**
@@ -1150,14 +1239,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();
         }
@@ -1251,15 +1437,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();
         }
@@ -1310,12 +1525,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);
@@ -1396,7 +1609,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);
@@ -1447,9 +1660,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);
@@ -1488,13 +1702,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");
@@ -1503,4 +1717,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..146d4ca 100644
--- a/framework-t/src/android/net/nsd/NsdServiceInfo.java
+++ b/framework-t/src/android/net/nsd/NsdServiceInfo.java
@@ -16,6 +16,9 @@
 
 package android.net.nsd;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import android.annotation.FlaggedApi;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.compat.annotation.UnsupportedAppUsage;
@@ -24,6 +27,7 @@
 import android.os.Parcelable;
 import android.text.TextUtils;
 import android.util.ArrayMap;
+import android.util.ArraySet;
 import android.util.Log;
 
 import com.android.net.module.util.InetAddressUtils;
@@ -35,6 +39,7 @@
 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 +49,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;
 
@@ -60,14 +72,35 @@
     private int mInterfaceIndex;
 
     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();
+    }
+
     /** Get the service name */
     public String getServiceName() {
         return mServiceName;
@@ -142,6 +175,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,12 +461,43 @@
         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);
+    }
+
     @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);
 
@@ -414,6 +515,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 +538,7 @@
         for (InetAddress address : mHostAddresses) {
             InetAddressUtils.parcelInetAddress(dest, address, flags);
         }
+        dest.writeString(mHostname);
     }
 
     /** Implement the Parcelable interface */
@@ -445,6 +548,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 +568,7 @@
                 for (int i = 0; i < size; i++) {
                     info.mHostAddresses.add(InetAddressUtils.unparcelInetAddress(in));
                 }
+                info.mHostname = in.readString();
                 return info;
             }
 
diff --git a/framework-t/src/android/net/nsd/OffloadEngine.java b/framework-t/src/android/net/nsd/OffloadEngine.java
index b566b13..9015985 100644
--- a/framework-t/src/android/net/nsd/OffloadEngine.java
+++ b/framework-t/src/android/net/nsd/OffloadEngine.java
@@ -16,6 +16,7 @@
 
 package android.net.nsd;
 
+import android.annotation.FlaggedApi;
 import android.annotation.LongDef;
 import android.annotation.NonNull;
 import android.annotation.SystemApi;
@@ -33,6 +34,7 @@
  *
  * @hide
  */
+@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")
 @SystemApi
 public interface OffloadEngine {
     /**
diff --git a/framework-t/src/android/net/nsd/OffloadServiceInfo.java b/framework-t/src/android/net/nsd/OffloadServiceInfo.java
index d5dbf19..98dc83a 100644
--- a/framework-t/src/android/net/nsd/OffloadServiceInfo.java
+++ b/framework-t/src/android/net/nsd/OffloadServiceInfo.java
@@ -16,6 +16,7 @@
 
 package android.net.nsd;
 
+import android.annotation.FlaggedApi;
 import android.annotation.IntRange;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -39,6 +40,7 @@
  *
  * @hide
  */
+@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")
 @SystemApi
 @RequiresApi(Build.VERSION_CODES.TIRAMISU)
 public final class OffloadServiceInfo implements Parcelable {
diff --git a/framework-t/udc-extended-api/OWNERS b/framework-t/udc-extended-api/OWNERS
deleted file mode 100644
index 607f85a..0000000
--- a/framework-t/udc-extended-api/OWNERS
+++ /dev/null
@@ -1,2 +0,0 @@
-file:platform/packages/modules/Connectivity:main:/nearby/OWNERS
-file:platform/packages/modules/Connectivity:main:/remoteauth/OWNERS
diff --git a/framework-t/udc-extended-api/current.txt b/framework-t/udc-extended-api/current.txt
deleted file mode 100644
index 86745d4..0000000
--- a/framework-t/udc-extended-api/current.txt
+++ /dev/null
@@ -1,267 +0,0 @@
-// Signature format: 2.0
-package android.app.usage {
-
-  public final class NetworkStats implements java.lang.AutoCloseable {
-    method public void close();
-    method public boolean getNextBucket(@Nullable android.app.usage.NetworkStats.Bucket);
-    method public boolean hasNextBucket();
-  }
-
-  public static class NetworkStats.Bucket {
-    ctor public NetworkStats.Bucket();
-    method public int getDefaultNetworkStatus();
-    method public long getEndTimeStamp();
-    method public int getMetered();
-    method public int getRoaming();
-    method public long getRxBytes();
-    method public long getRxPackets();
-    method public long getStartTimeStamp();
-    method public int getState();
-    method public int getTag();
-    method public long getTxBytes();
-    method public long getTxPackets();
-    method public int getUid();
-    field public static final int DEFAULT_NETWORK_ALL = -1; // 0xffffffff
-    field public static final int DEFAULT_NETWORK_NO = 1; // 0x1
-    field public static final int DEFAULT_NETWORK_YES = 2; // 0x2
-    field public static final int METERED_ALL = -1; // 0xffffffff
-    field public static final int METERED_NO = 1; // 0x1
-    field public static final int METERED_YES = 2; // 0x2
-    field public static final int ROAMING_ALL = -1; // 0xffffffff
-    field public static final int ROAMING_NO = 1; // 0x1
-    field public static final int ROAMING_YES = 2; // 0x2
-    field public static final int STATE_ALL = -1; // 0xffffffff
-    field public static final int STATE_DEFAULT = 1; // 0x1
-    field public static final int STATE_FOREGROUND = 2; // 0x2
-    field public static final int TAG_NONE = 0; // 0x0
-    field public static final int UID_ALL = -1; // 0xffffffff
-    field public static final int UID_REMOVED = -4; // 0xfffffffc
-    field public static final int UID_TETHERING = -5; // 0xfffffffb
-  }
-
-  public class NetworkStatsManager {
-    method @WorkerThread public android.app.usage.NetworkStats queryDetails(int, @Nullable String, long, long) throws android.os.RemoteException, java.lang.SecurityException;
-    method @NonNull @WorkerThread public android.app.usage.NetworkStats queryDetailsForUid(int, @Nullable String, long, long, int) throws java.lang.SecurityException;
-    method @NonNull @WorkerThread public android.app.usage.NetworkStats queryDetailsForUidTag(int, @Nullable String, long, long, int, int) throws java.lang.SecurityException;
-    method @NonNull @WorkerThread public android.app.usage.NetworkStats queryDetailsForUidTagState(int, @Nullable String, long, long, int, int, int) throws java.lang.SecurityException;
-    method @WorkerThread public android.app.usage.NetworkStats querySummary(int, @Nullable String, long, long) throws android.os.RemoteException, java.lang.SecurityException;
-    method @WorkerThread public android.app.usage.NetworkStats.Bucket querySummaryForDevice(int, @Nullable String, long, long) throws android.os.RemoteException, java.lang.SecurityException;
-    method @WorkerThread public android.app.usage.NetworkStats.Bucket querySummaryForUser(int, @Nullable String, long, long) throws android.os.RemoteException, java.lang.SecurityException;
-    method public void registerUsageCallback(int, @Nullable String, long, @NonNull android.app.usage.NetworkStatsManager.UsageCallback);
-    method public void registerUsageCallback(int, @Nullable String, long, @NonNull android.app.usage.NetworkStatsManager.UsageCallback, @Nullable android.os.Handler);
-    method public void unregisterUsageCallback(@NonNull android.app.usage.NetworkStatsManager.UsageCallback);
-  }
-
-  public abstract static class NetworkStatsManager.UsageCallback {
-    ctor public NetworkStatsManager.UsageCallback();
-    method public abstract void onThresholdReached(int, @Nullable String);
-  }
-
-}
-
-package android.net {
-
-  public final class EthernetNetworkSpecifier extends android.net.NetworkSpecifier implements android.os.Parcelable {
-    ctor public EthernetNetworkSpecifier(@NonNull String);
-    method public int describeContents();
-    method @Nullable public String getInterfaceName();
-    method public void writeToParcel(@NonNull android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.net.EthernetNetworkSpecifier> CREATOR;
-  }
-
-  public final class IpSecAlgorithm implements android.os.Parcelable {
-    ctor public IpSecAlgorithm(@NonNull String, @NonNull byte[]);
-    ctor public IpSecAlgorithm(@NonNull String, @NonNull byte[], int);
-    method public int describeContents();
-    method @NonNull public byte[] getKey();
-    method @NonNull public String getName();
-    method @NonNull public static java.util.Set<java.lang.String> getSupportedAlgorithms();
-    method public int getTruncationLengthBits();
-    method public void writeToParcel(android.os.Parcel, int);
-    field public static final String AUTH_AES_CMAC = "cmac(aes)";
-    field public static final String AUTH_AES_XCBC = "xcbc(aes)";
-    field public static final String AUTH_CRYPT_AES_GCM = "rfc4106(gcm(aes))";
-    field public static final String AUTH_CRYPT_CHACHA20_POLY1305 = "rfc7539esp(chacha20,poly1305)";
-    field public static final String AUTH_HMAC_MD5 = "hmac(md5)";
-    field public static final String AUTH_HMAC_SHA1 = "hmac(sha1)";
-    field public static final String AUTH_HMAC_SHA256 = "hmac(sha256)";
-    field public static final String AUTH_HMAC_SHA384 = "hmac(sha384)";
-    field public static final String AUTH_HMAC_SHA512 = "hmac(sha512)";
-    field @NonNull public static final android.os.Parcelable.Creator<android.net.IpSecAlgorithm> CREATOR;
-    field public static final String CRYPT_AES_CBC = "cbc(aes)";
-    field public static final String CRYPT_AES_CTR = "rfc3686(ctr(aes))";
-  }
-
-  public class IpSecManager {
-    method @NonNull public android.net.IpSecManager.SecurityParameterIndex allocateSecurityParameterIndex(@NonNull java.net.InetAddress) throws android.net.IpSecManager.ResourceUnavailableException;
-    method @NonNull public android.net.IpSecManager.SecurityParameterIndex allocateSecurityParameterIndex(@NonNull java.net.InetAddress, int) throws android.net.IpSecManager.ResourceUnavailableException, android.net.IpSecManager.SpiUnavailableException;
-    method public void applyTransportModeTransform(@NonNull java.net.Socket, int, @NonNull android.net.IpSecTransform) throws java.io.IOException;
-    method public void applyTransportModeTransform(@NonNull java.net.DatagramSocket, int, @NonNull android.net.IpSecTransform) throws java.io.IOException;
-    method public void applyTransportModeTransform(@NonNull java.io.FileDescriptor, int, @NonNull android.net.IpSecTransform) throws java.io.IOException;
-    method @NonNull public android.net.IpSecManager.UdpEncapsulationSocket openUdpEncapsulationSocket(int) throws java.io.IOException, android.net.IpSecManager.ResourceUnavailableException;
-    method @NonNull public android.net.IpSecManager.UdpEncapsulationSocket openUdpEncapsulationSocket() throws java.io.IOException, android.net.IpSecManager.ResourceUnavailableException;
-    method public void removeTransportModeTransforms(@NonNull java.net.Socket) throws java.io.IOException;
-    method public void removeTransportModeTransforms(@NonNull java.net.DatagramSocket) throws java.io.IOException;
-    method public void removeTransportModeTransforms(@NonNull java.io.FileDescriptor) throws java.io.IOException;
-    field public static final int DIRECTION_IN = 0; // 0x0
-    field public static final int DIRECTION_OUT = 1; // 0x1
-  }
-
-  public static final class IpSecManager.ResourceUnavailableException extends android.util.AndroidException {
-  }
-
-  public static final class IpSecManager.SecurityParameterIndex implements java.lang.AutoCloseable {
-    method public void close();
-    method public int getSpi();
-  }
-
-  public static final class IpSecManager.SpiUnavailableException extends android.util.AndroidException {
-    method public int getSpi();
-  }
-
-  public static final class IpSecManager.UdpEncapsulationSocket implements java.lang.AutoCloseable {
-    method public void close() throws java.io.IOException;
-    method public java.io.FileDescriptor getFileDescriptor();
-    method public int getPort();
-  }
-
-  public final class IpSecTransform implements java.lang.AutoCloseable {
-    method public void close();
-  }
-
-  public static class IpSecTransform.Builder {
-    ctor public IpSecTransform.Builder(@NonNull android.content.Context);
-    method @NonNull public android.net.IpSecTransform buildTransportModeTransform(@NonNull java.net.InetAddress, @NonNull android.net.IpSecManager.SecurityParameterIndex) throws java.io.IOException, android.net.IpSecManager.ResourceUnavailableException, android.net.IpSecManager.SpiUnavailableException;
-    method @NonNull public android.net.IpSecTransform.Builder setAuthenticatedEncryption(@NonNull android.net.IpSecAlgorithm);
-    method @NonNull public android.net.IpSecTransform.Builder setAuthentication(@NonNull android.net.IpSecAlgorithm);
-    method @NonNull public android.net.IpSecTransform.Builder setEncryption(@NonNull android.net.IpSecAlgorithm);
-    method @NonNull public android.net.IpSecTransform.Builder setIpv4Encapsulation(@NonNull android.net.IpSecManager.UdpEncapsulationSocket, int);
-  }
-
-  public class TrafficStats {
-    ctor public TrafficStats();
-    method public static void clearThreadStatsTag();
-    method public static void clearThreadStatsUid();
-    method public static int getAndSetThreadStatsTag(int);
-    method public static long getMobileRxBytes();
-    method public static long getMobileRxPackets();
-    method public static long getMobileTxBytes();
-    method public static long getMobileTxPackets();
-    method public static long getRxBytes(@NonNull String);
-    method public static long getRxPackets(@NonNull String);
-    method public static int getThreadStatsTag();
-    method public static int getThreadStatsUid();
-    method public static long getTotalRxBytes();
-    method public static long getTotalRxPackets();
-    method public static long getTotalTxBytes();
-    method public static long getTotalTxPackets();
-    method public static long getTxBytes(@NonNull String);
-    method public static long getTxPackets(@NonNull String);
-    method public static long getUidRxBytes(int);
-    method public static long getUidRxPackets(int);
-    method @Deprecated public static long getUidTcpRxBytes(int);
-    method @Deprecated public static long getUidTcpRxSegments(int);
-    method @Deprecated public static long getUidTcpTxBytes(int);
-    method @Deprecated public static long getUidTcpTxSegments(int);
-    method public static long getUidTxBytes(int);
-    method public static long getUidTxPackets(int);
-    method @Deprecated public static long getUidUdpRxBytes(int);
-    method @Deprecated public static long getUidUdpRxPackets(int);
-    method @Deprecated public static long getUidUdpTxBytes(int);
-    method @Deprecated public static long getUidUdpTxPackets(int);
-    method public static void incrementOperationCount(int);
-    method public static void incrementOperationCount(int, int);
-    method public static void setThreadStatsTag(int);
-    method public static void setThreadStatsUid(int);
-    method public static void tagDatagramSocket(@NonNull java.net.DatagramSocket) throws java.net.SocketException;
-    method public static void tagFileDescriptor(@NonNull java.io.FileDescriptor) throws java.io.IOException;
-    method public static void tagSocket(@NonNull java.net.Socket) throws java.net.SocketException;
-    method public static void untagDatagramSocket(@NonNull java.net.DatagramSocket) throws java.net.SocketException;
-    method public static void untagFileDescriptor(@NonNull java.io.FileDescriptor) throws java.io.IOException;
-    method public static void untagSocket(@NonNull java.net.Socket) throws java.net.SocketException;
-    field public static final int UNSUPPORTED = -1; // 0xffffffff
-  }
-
-}
-
-package android.net.nsd {
-
-  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 @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);
-    method public void registerServiceInfoCallback(@NonNull android.net.nsd.NsdServiceInfo, @NonNull java.util.concurrent.Executor, @NonNull android.net.nsd.NsdManager.ServiceInfoCallback);
-    method @Deprecated public void resolveService(android.net.nsd.NsdServiceInfo, android.net.nsd.NsdManager.ResolveListener);
-    method @Deprecated public void resolveService(@NonNull android.net.nsd.NsdServiceInfo, @NonNull java.util.concurrent.Executor, @NonNull android.net.nsd.NsdManager.ResolveListener);
-    method public void stopServiceDiscovery(android.net.nsd.NsdManager.DiscoveryListener);
-    method public void stopServiceResolution(@NonNull android.net.nsd.NsdManager.ResolveListener);
-    method public void unregisterService(android.net.nsd.NsdManager.RegistrationListener);
-    method public void unregisterServiceInfoCallback(@NonNull android.net.nsd.NsdManager.ServiceInfoCallback);
-    field public static final String ACTION_NSD_STATE_CHANGED = "android.net.nsd.STATE_CHANGED";
-    field public static final String EXTRA_NSD_STATE = "nsd_state";
-    field public static final int FAILURE_ALREADY_ACTIVE = 3; // 0x3
-    field public static final int FAILURE_BAD_PARAMETERS = 6; // 0x6
-    field public static final int FAILURE_INTERNAL_ERROR = 0; // 0x0
-    field public static final int FAILURE_MAX_LIMIT = 4; // 0x4
-    field public static final int FAILURE_OPERATION_NOT_RUNNING = 5; // 0x5
-    field public static final int NSD_STATE_DISABLED = 1; // 0x1
-    field public static final int NSD_STATE_ENABLED = 2; // 0x2
-    field public static final int PROTOCOL_DNS_SD = 1; // 0x1
-  }
-
-  public static interface NsdManager.DiscoveryListener {
-    method public void onDiscoveryStarted(String);
-    method public void onDiscoveryStopped(String);
-    method public void onServiceFound(android.net.nsd.NsdServiceInfo);
-    method public void onServiceLost(android.net.nsd.NsdServiceInfo);
-    method public void onStartDiscoveryFailed(String, int);
-    method public void onStopDiscoveryFailed(String, int);
-  }
-
-  public static interface NsdManager.RegistrationListener {
-    method public void onRegistrationFailed(android.net.nsd.NsdServiceInfo, int);
-    method public void onServiceRegistered(android.net.nsd.NsdServiceInfo);
-    method public void onServiceUnregistered(android.net.nsd.NsdServiceInfo);
-    method public void onUnregistrationFailed(android.net.nsd.NsdServiceInfo, int);
-  }
-
-  public static interface NsdManager.ResolveListener {
-    method public default void onResolutionStopped(@NonNull android.net.nsd.NsdServiceInfo);
-    method public void onResolveFailed(android.net.nsd.NsdServiceInfo, int);
-    method public void onServiceResolved(android.net.nsd.NsdServiceInfo);
-    method public default void onStopResolutionFailed(@NonNull android.net.nsd.NsdServiceInfo, int);
-  }
-
-  public static interface NsdManager.ServiceInfoCallback {
-    method public void onServiceInfoCallbackRegistrationFailed(int);
-    method public void onServiceInfoCallbackUnregistered();
-    method public void onServiceLost();
-    method public void onServiceUpdated(@NonNull android.net.nsd.NsdServiceInfo);
-  }
-
-  public final class NsdServiceInfo implements android.os.Parcelable {
-    ctor public NsdServiceInfo();
-    method public int describeContents();
-    method public java.util.Map<java.lang.String,byte[]> getAttributes();
-    method @Deprecated public java.net.InetAddress getHost();
-    method @NonNull public java.util.List<java.net.InetAddress> getHostAddresses();
-    method @Nullable public android.net.Network getNetwork();
-    method public int getPort();
-    method public String getServiceName();
-    method public String getServiceType();
-    method public void removeAttribute(String);
-    method public void setAttribute(String, String);
-    method @Deprecated public void setHost(java.net.InetAddress);
-    method public void setHostAddresses(@NonNull java.util.List<java.net.InetAddress>);
-    method public void setNetwork(@Nullable android.net.Network);
-    method public void setPort(int);
-    method public void setServiceName(String);
-    method public void setServiceType(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/udc-extended-api/lint-baseline.txt b/framework-t/udc-extended-api/lint-baseline.txt
deleted file mode 100644
index 2996a3e..0000000
--- a/framework-t/udc-extended-api/lint-baseline.txt
+++ /dev/null
@@ -1,89 +0,0 @@
-// Baseline format: 1.0
-BannedThrow: android.app.usage.NetworkStatsManager#queryDetails(int, String, long, long):
-    Methods must not mention RuntimeException subclasses in throws clauses (was `java.lang.SecurityException`)
-BannedThrow: android.app.usage.NetworkStatsManager#queryDetailsForUid(int, String, long, long, int):
-    Methods must not mention RuntimeException subclasses in throws clauses (was `java.lang.SecurityException`)
-BannedThrow: android.app.usage.NetworkStatsManager#queryDetailsForUidTag(int, String, long, long, int, int):
-    Methods must not mention RuntimeException subclasses in throws clauses (was `java.lang.SecurityException`)
-BannedThrow: android.app.usage.NetworkStatsManager#queryDetailsForUidTagState(int, String, long, long, int, int, int):
-    Methods must not mention RuntimeException subclasses in throws clauses (was `java.lang.SecurityException`)
-BannedThrow: android.app.usage.NetworkStatsManager#querySummary(int, String, long, long):
-    Methods must not mention RuntimeException subclasses in throws clauses (was `java.lang.SecurityException`)
-BannedThrow: android.app.usage.NetworkStatsManager#querySummaryForDevice(int, String, long, long):
-    Methods must not mention RuntimeException subclasses in throws clauses (was `java.lang.SecurityException`)
-BannedThrow: android.app.usage.NetworkStatsManager#querySummaryForUser(int, String, long, long):
-    Methods must not mention RuntimeException subclasses in throws clauses (was `java.lang.SecurityException`)
-
-
-BuilderSetStyle: android.net.IpSecTransform.Builder#buildTransportModeTransform(java.net.InetAddress, android.net.IpSecManager.SecurityParameterIndex):
-    Builder methods names should use setFoo() / addFoo() / clearFoo() style: method android.net.IpSecTransform.Builder.buildTransportModeTransform(java.net.InetAddress,android.net.IpSecManager.SecurityParameterIndex)
-
-
-EqualsAndHashCode: android.net.IpSecTransform#equals(Object):
-    Must override both equals and hashCode; missing one in android.net.IpSecTransform
-
-
-ExecutorRegistration: android.app.usage.NetworkStatsManager#registerUsageCallback(int, String, long, android.app.usage.NetworkStatsManager.UsageCallback, android.os.Handler):
-    Registration methods should have overload that accepts delivery Executor: `registerUsageCallback`
-
-
-GenericException: android.app.usage.NetworkStats#finalize():
-    Methods must not throw generic exceptions (`java.lang.Throwable`)
-GenericException: android.net.IpSecManager.SecurityParameterIndex#finalize():
-    Methods must not throw generic exceptions (`java.lang.Throwable`)
-GenericException: android.net.IpSecManager.UdpEncapsulationSocket#finalize():
-    Methods must not throw generic exceptions (`java.lang.Throwable`)
-GenericException: android.net.IpSecTransform#finalize():
-    Methods must not throw generic exceptions (`java.lang.Throwable`)
-
-
-MissingBuildMethod: android.net.IpSecTransform.Builder:
-    android.net.IpSecTransform.Builder does not declare a `build()` method, but builder classes are expected to
-
-
-MissingNullability: android.app.usage.NetworkStatsManager#queryDetails(int, String, long, long):
-    Missing nullability on method `queryDetails` return
-MissingNullability: android.app.usage.NetworkStatsManager#querySummary(int, String, long, long):
-    Missing nullability on method `querySummary` return
-MissingNullability: android.app.usage.NetworkStatsManager#querySummaryForDevice(int, String, long, long):
-    Missing nullability on method `querySummaryForDevice` return
-MissingNullability: android.app.usage.NetworkStatsManager#querySummaryForUser(int, String, long, long):
-    Missing nullability on method `querySummaryForUser` return
-MissingNullability: android.net.IpSecAlgorithm#writeToParcel(android.os.Parcel, int) parameter #0:
-    Missing nullability on parameter `out` in method `writeToParcel`
-MissingNullability: android.net.IpSecManager.UdpEncapsulationSocket#getFileDescriptor():
-    Missing nullability on method `getFileDescriptor` return
-
-
-RethrowRemoteException: android.app.usage.NetworkStatsManager#queryDetails(int, String, long, long):
-    Methods calling system APIs should rethrow `RemoteException` as `RuntimeException` (but do not list it in the throws clause)
-RethrowRemoteException: android.app.usage.NetworkStatsManager#querySummary(int, String, long, long):
-    Methods calling system APIs should rethrow `RemoteException` as `RuntimeException` (but do not list it in the throws clause)
-RethrowRemoteException: android.app.usage.NetworkStatsManager#querySummaryForDevice(int, String, long, long):
-    Methods calling system APIs should rethrow `RemoteException` as `RuntimeException` (but do not list it in the throws clause)
-RethrowRemoteException: android.app.usage.NetworkStatsManager#querySummaryForUser(int, String, long, long):
-    Methods calling system APIs should rethrow `RemoteException` as `RuntimeException` (but do not list it in the throws clause)
-
-
-StaticFinalBuilder: android.net.IpSecTransform.Builder:
-    Builder must be final: android.net.IpSecTransform.Builder
-
-
-StaticUtils: android.net.TrafficStats:
-    Fully-static utility classes must not have constructor
-
-
-UseParcelFileDescriptor: android.net.IpSecManager#applyTransportModeTransform(java.io.FileDescriptor, int, android.net.IpSecTransform) parameter #0:
-    Must use ParcelFileDescriptor instead of FileDescriptor in parameter socket in android.net.IpSecManager.applyTransportModeTransform(java.io.FileDescriptor socket, int direction, android.net.IpSecTransform transform)
-UseParcelFileDescriptor: android.net.IpSecManager#removeTransportModeTransforms(java.io.FileDescriptor) parameter #0:
-    Must use ParcelFileDescriptor instead of FileDescriptor in parameter socket in android.net.IpSecManager.removeTransportModeTransforms(java.io.FileDescriptor socket)
-UseParcelFileDescriptor: android.net.IpSecManager.UdpEncapsulationSocket#getFileDescriptor():
-    Must use ParcelFileDescriptor instead of FileDescriptor in method android.net.IpSecManager.UdpEncapsulationSocket.getFileDescriptor()
-UseParcelFileDescriptor: android.net.TrafficStats#tagFileDescriptor(java.io.FileDescriptor) parameter #0:
-    Must use ParcelFileDescriptor instead of FileDescriptor in parameter fd in android.net.TrafficStats.tagFileDescriptor(java.io.FileDescriptor fd)
-UseParcelFileDescriptor: android.net.TrafficStats#untagFileDescriptor(java.io.FileDescriptor) parameter #0:
-    Must use ParcelFileDescriptor instead of FileDescriptor in parameter fd in android.net.TrafficStats.untagFileDescriptor(java.io.FileDescriptor fd)
-UseParcelFileDescriptor: com.android.server.NetworkManagementSocketTagger#tag(java.io.FileDescriptor) parameter #0:
-    Must use ParcelFileDescriptor instead of FileDescriptor in parameter fd in com.android.server.NetworkManagementSocketTagger.tag(java.io.FileDescriptor fd)
-UseParcelFileDescriptor: com.android.server.NetworkManagementSocketTagger#untag(java.io.FileDescriptor) parameter #0:
-    Must use ParcelFileDescriptor instead of FileDescriptor in parameter fd in com.android.server.NetworkManagementSocketTagger.untag(java.io.FileDescriptor fd)
diff --git a/framework-t/udc-extended-api/module-lib-current.txt b/framework-t/udc-extended-api/module-lib-current.txt
deleted file mode 100644
index 5a8d47b..0000000
--- a/framework-t/udc-extended-api/module-lib-current.txt
+++ /dev/null
@@ -1,209 +0,0 @@
-// Signature format: 2.0
-package android.app.usage {
-
-  public class NetworkStatsManager {
-    method @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_STACK}) public void forceUpdate();
-    method public static int getCollapsedRatType(int);
-    method @NonNull @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_STACK}) public android.net.NetworkStats getMobileUidStats();
-    method @NonNull @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_STACK}) public android.net.NetworkStats getWifiUidStats();
-    method @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_STACK}) public void noteUidForeground(int, boolean);
-    method @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_STACK}) public void notifyNetworkStatus(@NonNull java.util.List<android.net.Network>, @NonNull java.util.List<android.net.NetworkStateSnapshot>, @Nullable String, @NonNull java.util.List<android.net.UnderlyingNetworkInfo>);
-    method @NonNull @WorkerThread public android.app.usage.NetworkStats queryDetailsForDevice(@NonNull android.net.NetworkTemplate, long, long);
-    method @NonNull @WorkerThread public android.app.usage.NetworkStats queryDetailsForUidTagState(@NonNull android.net.NetworkTemplate, long, long, int, int, int) throws java.lang.SecurityException;
-    method @NonNull @WorkerThread public android.app.usage.NetworkStats querySummary(@NonNull android.net.NetworkTemplate, long, long) throws java.lang.SecurityException;
-    method @NonNull @WorkerThread public android.app.usage.NetworkStats.Bucket querySummaryForDevice(@NonNull android.net.NetworkTemplate, long, long);
-    method @NonNull @WorkerThread public android.app.usage.NetworkStats queryTaggedSummary(@NonNull android.net.NetworkTemplate, long, long) throws java.lang.SecurityException;
-    method @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_STACK}, conditional=true) public void registerUsageCallback(@NonNull android.net.NetworkTemplate, long, @NonNull java.util.concurrent.Executor, @NonNull android.app.usage.NetworkStatsManager.UsageCallback);
-    method @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_STACK}) public void setDefaultGlobalAlert(long);
-    method public void setPollForce(boolean);
-    method @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_STACK}) public void setPollOnOpen(boolean);
-    method @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_STACK}) public void setStatsProviderWarningAndLimitAsync(@NonNull String, long, long);
-    field public static final int NETWORK_TYPE_5G_NSA = -2; // 0xfffffffe
-  }
-
-  public abstract static class NetworkStatsManager.UsageCallback {
-    method public void onThresholdReached(@NonNull android.net.NetworkTemplate);
-  }
-
-}
-
-package android.nearby {
-
-  public final class NearbyFrameworkInitializer {
-    method public static void registerServiceWrappers();
-  }
-
-}
-
-package android.net {
-
-  public final class ConnectivityFrameworkInitializerTiramisu {
-    method public static void registerServiceWrappers();
-  }
-
-  public class EthernetManager {
-    method @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public void addEthernetStateListener(@NonNull java.util.concurrent.Executor, @NonNull java.util.function.IntConsumer);
-    method @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public void addInterfaceStateListener(@NonNull java.util.concurrent.Executor, @NonNull android.net.EthernetManager.InterfaceStateListener);
-    method @NonNull @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public java.util.List<java.lang.String> getInterfaceList();
-    method @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public void removeEthernetStateListener(@NonNull java.util.function.IntConsumer);
-    method public void removeInterfaceStateListener(@NonNull android.net.EthernetManager.InterfaceStateListener);
-    method @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_STACK, android.Manifest.permission.NETWORK_SETTINGS}) public void setEthernetEnabled(boolean);
-    method public void setIncludeTestInterfaces(boolean);
-    field public static final int ETHERNET_STATE_DISABLED = 0; // 0x0
-    field public static final int ETHERNET_STATE_ENABLED = 1; // 0x1
-    field public static final int ROLE_CLIENT = 1; // 0x1
-    field public static final int ROLE_NONE = 0; // 0x0
-    field public static final int ROLE_SERVER = 2; // 0x2
-    field public static final int STATE_ABSENT = 0; // 0x0
-    field public static final int STATE_LINK_DOWN = 1; // 0x1
-    field public static final int STATE_LINK_UP = 2; // 0x2
-  }
-
-  public static interface EthernetManager.InterfaceStateListener {
-    method public void onInterfaceStateChanged(@NonNull String, int, int, @Nullable android.net.IpConfiguration);
-  }
-
-  public class IpSecManager {
-    field public static final int DIRECTION_FWD = 2; // 0x2
-  }
-
-  public static final class IpSecManager.UdpEncapsulationSocket implements java.lang.AutoCloseable {
-    method public int getResourceId();
-  }
-
-  public class NetworkIdentity {
-    method public int getOemManaged();
-    method public int getRatType();
-    method public int getSubId();
-    method @Nullable public String getSubscriberId();
-    method public int getType();
-    method @Nullable public String getWifiNetworkKey();
-    method public boolean isDefaultNetwork();
-    method public boolean isMetered();
-    method public boolean isRoaming();
-  }
-
-  public static final class NetworkIdentity.Builder {
-    ctor public NetworkIdentity.Builder();
-    method @NonNull public android.net.NetworkIdentity build();
-    method @NonNull public android.net.NetworkIdentity.Builder clearRatType();
-    method @NonNull public android.net.NetworkIdentity.Builder setDefaultNetwork(boolean);
-    method @NonNull public android.net.NetworkIdentity.Builder setMetered(boolean);
-    method @NonNull public android.net.NetworkIdentity.Builder setNetworkStateSnapshot(@NonNull android.net.NetworkStateSnapshot);
-    method @NonNull public android.net.NetworkIdentity.Builder setOemManaged(int);
-    method @NonNull public android.net.NetworkIdentity.Builder setRatType(int);
-    method @NonNull public android.net.NetworkIdentity.Builder setRoaming(boolean);
-    method @NonNull public android.net.NetworkIdentity.Builder setSubId(int);
-    method @NonNull public android.net.NetworkIdentity.Builder setSubscriberId(@Nullable String);
-    method @NonNull public android.net.NetworkIdentity.Builder setType(int);
-    method @NonNull public android.net.NetworkIdentity.Builder setWifiNetworkKey(@Nullable String);
-  }
-
-  public final class NetworkStateSnapshot implements android.os.Parcelable {
-    ctor public NetworkStateSnapshot(@NonNull android.net.Network, @NonNull android.net.NetworkCapabilities, @NonNull android.net.LinkProperties, @Nullable String, int);
-    method public int describeContents();
-    method public int getLegacyType();
-    method @NonNull public android.net.LinkProperties getLinkProperties();
-    method @NonNull public android.net.Network getNetwork();
-    method @NonNull public android.net.NetworkCapabilities getNetworkCapabilities();
-    method public int getSubId();
-    method @Deprecated @Nullable public String getSubscriberId();
-    method public void writeToParcel(@NonNull android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.net.NetworkStateSnapshot> CREATOR;
-  }
-
-  public class NetworkStatsCollection {
-    method @NonNull public java.util.Map<android.net.NetworkStatsCollection.Key,android.net.NetworkStatsHistory> getEntries();
-  }
-
-  public static final class NetworkStatsCollection.Builder {
-    ctor public NetworkStatsCollection.Builder(long);
-    method @NonNull public android.net.NetworkStatsCollection.Builder addEntry(@NonNull android.net.NetworkStatsCollection.Key, @NonNull android.net.NetworkStatsHistory);
-    method @NonNull public android.net.NetworkStatsCollection build();
-  }
-
-  public static final class NetworkStatsCollection.Key {
-    ctor public NetworkStatsCollection.Key(@NonNull java.util.Set<android.net.NetworkIdentity>, int, int, int);
-  }
-
-  public final class NetworkStatsHistory implements android.os.Parcelable {
-    method public int describeContents();
-    method @NonNull public java.util.List<android.net.NetworkStatsHistory.Entry> getEntries();
-    method public void writeToParcel(@NonNull android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.net.NetworkStatsHistory> CREATOR;
-  }
-
-  public static final class NetworkStatsHistory.Builder {
-    ctor public NetworkStatsHistory.Builder(long, int);
-    method @NonNull public android.net.NetworkStatsHistory.Builder addEntry(@NonNull android.net.NetworkStatsHistory.Entry);
-    method @NonNull public android.net.NetworkStatsHistory build();
-  }
-
-  public static final class NetworkStatsHistory.Entry {
-    ctor public NetworkStatsHistory.Entry(long, long, long, long, long, long, long);
-    method public long getActiveTime();
-    method public long getBucketStart();
-    method public long getOperations();
-    method public long getRxBytes();
-    method public long getRxPackets();
-    method public long getTxBytes();
-    method public long getTxPackets();
-  }
-
-  public final class NetworkTemplate implements android.os.Parcelable {
-    method public int describeContents();
-    method public int getDefaultNetworkStatus();
-    method public int getMatchRule();
-    method public int getMeteredness();
-    method public int getOemManaged();
-    method public int getRatType();
-    method public int getRoaming();
-    method @NonNull public java.util.Set<java.lang.String> getSubscriberIds();
-    method @NonNull public java.util.Set<java.lang.String> getWifiNetworkKeys();
-    method public boolean matches(@NonNull android.net.NetworkIdentity);
-    method public void writeToParcel(@NonNull android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.net.NetworkTemplate> CREATOR;
-    field public static final int MATCH_BLUETOOTH = 8; // 0x8
-    field public static final int MATCH_CARRIER = 10; // 0xa
-    field public static final int MATCH_ETHERNET = 5; // 0x5
-    field public static final int MATCH_MOBILE = 1; // 0x1
-    field public static final int MATCH_PROXY = 9; // 0x9
-    field public static final int MATCH_WIFI = 4; // 0x4
-    field public static final int NETWORK_TYPE_ALL = -1; // 0xffffffff
-    field public static final int OEM_MANAGED_ALL = -1; // 0xffffffff
-    field public static final int OEM_MANAGED_NO = 0; // 0x0
-    field public static final int OEM_MANAGED_PAID = 1; // 0x1
-    field public static final int OEM_MANAGED_PRIVATE = 2; // 0x2
-    field public static final int OEM_MANAGED_YES = -2; // 0xfffffffe
-  }
-
-  public static final class NetworkTemplate.Builder {
-    ctor public NetworkTemplate.Builder(int);
-    method @NonNull public android.net.NetworkTemplate build();
-    method @NonNull public android.net.NetworkTemplate.Builder setDefaultNetworkStatus(int);
-    method @NonNull public android.net.NetworkTemplate.Builder setMeteredness(int);
-    method @NonNull public android.net.NetworkTemplate.Builder setOemManaged(int);
-    method @NonNull public android.net.NetworkTemplate.Builder setRatType(int);
-    method @NonNull public android.net.NetworkTemplate.Builder setRoaming(int);
-    method @NonNull public android.net.NetworkTemplate.Builder setSubscriberIds(@NonNull java.util.Set<java.lang.String>);
-    method @NonNull public android.net.NetworkTemplate.Builder setWifiNetworkKeys(@NonNull java.util.Set<java.lang.String>);
-  }
-
-  public class TrafficStats {
-    method public static void attachSocketTagger();
-    method public static void init(@NonNull android.content.Context);
-    method public static void setThreadStatsTagDownload();
-  }
-
-  public final class UnderlyingNetworkInfo implements android.os.Parcelable {
-    ctor public UnderlyingNetworkInfo(int, @NonNull String, @NonNull java.util.List<java.lang.String>);
-    method public int describeContents();
-    method @NonNull public String getInterface();
-    method public int getOwnerUid();
-    method @NonNull public java.util.List<java.lang.String> getUnderlyingInterfaces();
-    method public void writeToParcel(@NonNull android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.net.UnderlyingNetworkInfo> CREATOR;
-  }
-
-}
-
diff --git a/framework-t/udc-extended-api/module-lib-lint-baseline.txt b/framework-t/udc-extended-api/module-lib-lint-baseline.txt
deleted file mode 100644
index 3158bd4..0000000
--- a/framework-t/udc-extended-api/module-lib-lint-baseline.txt
+++ /dev/null
@@ -1,7 +0,0 @@
-// Baseline format: 1.0
-BannedThrow: android.app.usage.NetworkStatsManager#queryDetailsForUidTagState(android.net.NetworkTemplate, long, long, int, int, int):
-    Methods must not mention RuntimeException subclasses in throws clauses (was `java.lang.SecurityException`)
-BannedThrow: android.app.usage.NetworkStatsManager#querySummary(android.net.NetworkTemplate, long, long):
-    Methods must not mention RuntimeException subclasses in throws clauses (was `java.lang.SecurityException`)
-BannedThrow: android.app.usage.NetworkStatsManager#queryTaggedSummary(android.net.NetworkTemplate, long, long):
-    Methods must not mention RuntimeException subclasses in throws clauses (was `java.lang.SecurityException`)
diff --git a/framework-t/udc-extended-api/module-lib-removed.txt b/framework-t/udc-extended-api/module-lib-removed.txt
deleted file mode 100644
index d802177..0000000
--- a/framework-t/udc-extended-api/module-lib-removed.txt
+++ /dev/null
@@ -1 +0,0 @@
-// Signature format: 2.0
diff --git a/framework-t/udc-extended-api/removed.txt b/framework-t/udc-extended-api/removed.txt
deleted file mode 100644
index 1ba87d8..0000000
--- a/framework-t/udc-extended-api/removed.txt
+++ /dev/null
@@ -1,9 +0,0 @@
-// Signature format: 2.0
-package android.net {
-
-  public class TrafficStats {
-    method @Deprecated public static void setThreadStatsUidSelf();
-  }
-
-}
-
diff --git a/framework-t/udc-extended-api/system-current.txt b/framework-t/udc-extended-api/system-current.txt
deleted file mode 100644
index 6f0119e..0000000
--- a/framework-t/udc-extended-api/system-current.txt
+++ /dev/null
@@ -1,417 +0,0 @@
-// Signature format: 2.0
-package android.app.usage {
-
-  public class NetworkStatsManager {
-    method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_STATS_PROVIDER, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void registerNetworkStatsProvider(@NonNull String, @NonNull android.net.netstats.provider.NetworkStatsProvider);
-    method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_STATS_PROVIDER, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void unregisterNetworkStatsProvider(@NonNull android.net.netstats.provider.NetworkStatsProvider);
-  }
-
-}
-
-package android.nearby {
-
-  public interface BroadcastCallback {
-    method public void onStatusChanged(int);
-    field public static final int STATUS_FAILURE = 1; // 0x1
-    field public static final int STATUS_FAILURE_ALREADY_REGISTERED = 2; // 0x2
-    field public static final int STATUS_FAILURE_MISSING_PERMISSIONS = 4; // 0x4
-    field public static final int STATUS_FAILURE_SIZE_EXCEED_LIMIT = 3; // 0x3
-    field public static final int STATUS_OK = 0; // 0x0
-  }
-
-  public abstract class BroadcastRequest {
-    method @NonNull public java.util.List<java.lang.Integer> getMediums();
-    method @IntRange(from=0xffffff81, to=126) public int getTxPower();
-    method public int getType();
-    method public int getVersion();
-    field public static final int BROADCAST_TYPE_NEARBY_PRESENCE = 3; // 0x3
-    field public static final int BROADCAST_TYPE_UNKNOWN = -1; // 0xffffffff
-    field public static final int MEDIUM_BLE = 1; // 0x1
-    field public static final int PRESENCE_VERSION_UNKNOWN = -1; // 0xffffffff
-    field public static final int PRESENCE_VERSION_V0 = 0; // 0x0
-    field public static final int PRESENCE_VERSION_V1 = 1; // 0x1
-    field public static final int UNKNOWN_TX_POWER = -127; // 0xffffff81
-  }
-
-  public final class CredentialElement implements android.os.Parcelable {
-    ctor public CredentialElement(@NonNull String, @NonNull byte[]);
-    method public int describeContents();
-    method @NonNull public String getKey();
-    method @NonNull public byte[] getValue();
-    method public void writeToParcel(@NonNull android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.nearby.CredentialElement> CREATOR;
-  }
-
-  public final class DataElement implements android.os.Parcelable {
-    ctor public DataElement(int, @NonNull byte[]);
-    method public int describeContents();
-    method public int getKey();
-    method @NonNull public byte[] getValue();
-    method public void writeToParcel(@NonNull android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.nearby.DataElement> CREATOR;
-  }
-
-  public abstract class NearbyDevice {
-    method @NonNull public java.util.List<java.lang.Integer> getMediums();
-    method @Nullable public String getName();
-    method @IntRange(from=0xffffff81, to=126) public int getRssi();
-    method public static boolean isValidMedium(int);
-  }
-
-  public class NearbyManager {
-    method public void queryOffloadCapability(@NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<android.nearby.OffloadCapability>);
-    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);
-  }
-
-  public final class OffloadCapability implements android.os.Parcelable {
-    method public int describeContents();
-    method public long getVersion();
-    method public boolean isFastPairSupported();
-    method public boolean isNearbyShareSupported();
-    method public void writeToParcel(@NonNull android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.nearby.OffloadCapability> CREATOR;
-  }
-
-  public static final class OffloadCapability.Builder {
-    ctor public OffloadCapability.Builder();
-    method @NonNull public android.nearby.OffloadCapability build();
-    method @NonNull public android.nearby.OffloadCapability.Builder setFastPairSupported(boolean);
-    method @NonNull public android.nearby.OffloadCapability.Builder setNearbyShareSupported(boolean);
-    method @NonNull public android.nearby.OffloadCapability.Builder setVersion(long);
-  }
-
-  public final class PresenceBroadcastRequest extends android.nearby.BroadcastRequest implements android.os.Parcelable {
-    method public int describeContents();
-    method @NonNull public java.util.List<java.lang.Integer> getActions();
-    method @NonNull public android.nearby.PrivateCredential getCredential();
-    method @NonNull public java.util.List<android.nearby.DataElement> getExtendedProperties();
-    method @NonNull public byte[] getSalt();
-    method public void writeToParcel(@NonNull android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.nearby.PresenceBroadcastRequest> CREATOR;
-  }
-
-  public static final class PresenceBroadcastRequest.Builder {
-    ctor public PresenceBroadcastRequest.Builder(@NonNull java.util.List<java.lang.Integer>, @NonNull byte[], @NonNull android.nearby.PrivateCredential);
-    method @NonNull public android.nearby.PresenceBroadcastRequest.Builder addAction(@IntRange(from=1, to=255) int);
-    method @NonNull public android.nearby.PresenceBroadcastRequest.Builder addExtendedProperty(@NonNull android.nearby.DataElement);
-    method @NonNull public android.nearby.PresenceBroadcastRequest build();
-    method @NonNull public android.nearby.PresenceBroadcastRequest.Builder setTxPower(@IntRange(from=0xffffff81, to=126) int);
-    method @NonNull public android.nearby.PresenceBroadcastRequest.Builder setVersion(int);
-  }
-
-  public abstract class PresenceCredential {
-    method @NonNull public byte[] getAuthenticityKey();
-    method @NonNull public java.util.List<android.nearby.CredentialElement> getCredentialElements();
-    method public int getIdentityType();
-    method @NonNull public byte[] getSecretId();
-    method public int getType();
-    field public static final int CREDENTIAL_TYPE_PRIVATE = 0; // 0x0
-    field public static final int CREDENTIAL_TYPE_PUBLIC = 1; // 0x1
-    field public static final int IDENTITY_TYPE_PRIVATE = 1; // 0x1
-    field public static final int IDENTITY_TYPE_PROVISIONED = 2; // 0x2
-    field public static final int IDENTITY_TYPE_TRUSTED = 3; // 0x3
-    field public static final int IDENTITY_TYPE_UNKNOWN = 0; // 0x0
-  }
-
-  public final class PresenceDevice extends android.nearby.NearbyDevice implements android.os.Parcelable {
-    method public int describeContents();
-    method @NonNull public String getDeviceId();
-    method @Nullable public String getDeviceImageUrl();
-    method public int getDeviceType();
-    method public long getDiscoveryTimestampMillis();
-    method @NonNull public byte[] getEncryptedIdentity();
-    method @NonNull public java.util.List<android.nearby.DataElement> getExtendedProperties();
-    method @NonNull public byte[] getSalt();
-    method @NonNull public byte[] getSecretId();
-    method public void writeToParcel(@NonNull android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.nearby.PresenceDevice> CREATOR;
-  }
-
-  public static final class PresenceDevice.Builder {
-    ctor public PresenceDevice.Builder(@NonNull String, @NonNull byte[], @NonNull byte[], @NonNull byte[]);
-    method @NonNull public android.nearby.PresenceDevice.Builder addExtendedProperty(@NonNull android.nearby.DataElement);
-    method @NonNull public android.nearby.PresenceDevice.Builder addMedium(int);
-    method @NonNull public android.nearby.PresenceDevice build();
-    method @NonNull public android.nearby.PresenceDevice.Builder setDeviceImageUrl(@Nullable String);
-    method @NonNull public android.nearby.PresenceDevice.Builder setDeviceType(int);
-    method @NonNull public android.nearby.PresenceDevice.Builder setDiscoveryTimestampMillis(long);
-    method @NonNull public android.nearby.PresenceDevice.Builder setName(@Nullable String);
-    method @NonNull public android.nearby.PresenceDevice.Builder setRssi(int);
-  }
-
-  public final class PresenceScanFilter extends android.nearby.ScanFilter implements android.os.Parcelable {
-    method public int describeContents();
-    method @NonNull public java.util.List<android.nearby.PublicCredential> getCredentials();
-    method @NonNull public java.util.List<android.nearby.DataElement> getExtendedProperties();
-    method @NonNull public java.util.List<java.lang.Integer> getPresenceActions();
-    method public void writeToParcel(@NonNull android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.nearby.PresenceScanFilter> CREATOR;
-  }
-
-  public static final class PresenceScanFilter.Builder {
-    ctor public PresenceScanFilter.Builder();
-    method @NonNull public android.nearby.PresenceScanFilter.Builder addCredential(@NonNull android.nearby.PublicCredential);
-    method @NonNull public android.nearby.PresenceScanFilter.Builder addExtendedProperty(@NonNull android.nearby.DataElement);
-    method @NonNull public android.nearby.PresenceScanFilter.Builder addPresenceAction(@IntRange(from=1, to=255) int);
-    method @NonNull public android.nearby.PresenceScanFilter build();
-    method @NonNull public android.nearby.PresenceScanFilter.Builder setMaxPathLoss(@IntRange(from=0, to=127) int);
-  }
-
-  public final class PrivateCredential extends android.nearby.PresenceCredential implements android.os.Parcelable {
-    method public int describeContents();
-    method @NonNull public String getDeviceName();
-    method @NonNull public byte[] getMetadataEncryptionKey();
-    method public void writeToParcel(@NonNull android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.nearby.PrivateCredential> CREATOR;
-  }
-
-  public static final class PrivateCredential.Builder {
-    ctor public PrivateCredential.Builder(@NonNull byte[], @NonNull byte[], @NonNull byte[], @NonNull String);
-    method @NonNull public android.nearby.PrivateCredential.Builder addCredentialElement(@NonNull android.nearby.CredentialElement);
-    method @NonNull public android.nearby.PrivateCredential build();
-    method @NonNull public android.nearby.PrivateCredential.Builder setIdentityType(int);
-  }
-
-  public final class PublicCredential extends android.nearby.PresenceCredential implements android.os.Parcelable {
-    method public int describeContents();
-    method @NonNull public byte[] getEncryptedMetadata();
-    method @NonNull public byte[] getEncryptedMetadataKeyTag();
-    method @NonNull public byte[] getPublicKey();
-    method public void writeToParcel(@NonNull android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.nearby.PublicCredential> CREATOR;
-  }
-
-  public static final class PublicCredential.Builder {
-    ctor public PublicCredential.Builder(@NonNull byte[], @NonNull byte[], @NonNull byte[], @NonNull byte[], @NonNull byte[]);
-    method @NonNull public android.nearby.PublicCredential.Builder addCredentialElement(@NonNull android.nearby.CredentialElement);
-    method @NonNull public android.nearby.PublicCredential build();
-    method @NonNull public android.nearby.PublicCredential.Builder setIdentityType(int);
-  }
-
-  public interface ScanCallback {
-    method public void onDiscovered(@NonNull android.nearby.NearbyDevice);
-    method public default void onError(int);
-    method public void onLost(@NonNull android.nearby.NearbyDevice);
-    method public void onUpdated(@NonNull android.nearby.NearbyDevice);
-    field public static final int ERROR_INVALID_ARGUMENT = 2; // 0x2
-    field public static final int ERROR_PERMISSION_DENIED = 3; // 0x3
-    field public static final int ERROR_RESOURCE_EXHAUSTED = 4; // 0x4
-    field public static final int ERROR_UNKNOWN = 0; // 0x0
-    field public static final int ERROR_UNSUPPORTED = 1; // 0x1
-  }
-
-  public abstract class ScanFilter {
-    method @IntRange(from=0, to=127) public int getMaxPathLoss();
-    method public int getType();
-  }
-
-  public final class ScanRequest implements android.os.Parcelable {
-    method public int describeContents();
-    method @NonNull public java.util.List<android.nearby.ScanFilter> getScanFilters();
-    method public int getScanMode();
-    method public int getScanType();
-    method @NonNull public android.os.WorkSource getWorkSource();
-    method public boolean isBleEnabled();
-    method public boolean isOffloadOnly();
-    method public static boolean isValidScanMode(int);
-    method public static boolean isValidScanType(int);
-    method @NonNull public static String scanModeToString(int);
-    method public void writeToParcel(@NonNull android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.nearby.ScanRequest> CREATOR;
-    field public static final int SCAN_MODE_BALANCED = 1; // 0x1
-    field public static final int SCAN_MODE_LOW_LATENCY = 2; // 0x2
-    field public static final int SCAN_MODE_LOW_POWER = 0; // 0x0
-    field public static final int SCAN_MODE_NO_POWER = -1; // 0xffffffff
-    field public static final int SCAN_TYPE_FAST_PAIR = 1; // 0x1
-    field public static final int SCAN_TYPE_NEARBY_PRESENCE = 2; // 0x2
-  }
-
-  public static final class ScanRequest.Builder {
-    ctor public ScanRequest.Builder();
-    method @NonNull public android.nearby.ScanRequest.Builder addScanFilter(@NonNull android.nearby.ScanFilter);
-    method @NonNull public android.nearby.ScanRequest build();
-    method @NonNull public android.nearby.ScanRequest.Builder setBleEnabled(boolean);
-    method @NonNull public android.nearby.ScanRequest.Builder setOffloadOnly(boolean);
-    method @NonNull public android.nearby.ScanRequest.Builder setScanMode(int);
-    method @NonNull public android.nearby.ScanRequest.Builder setScanType(int);
-    method @NonNull @RequiresPermission(android.Manifest.permission.UPDATE_DEVICE_STATS) public android.nearby.ScanRequest.Builder setWorkSource(@Nullable android.os.WorkSource);
-  }
-
-}
-
-package android.net {
-
-  public class EthernetManager {
-    method @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_STACK, android.Manifest.permission.MANAGE_ETHERNET_NETWORKS}) public void disableInterface(@NonNull String, @Nullable java.util.concurrent.Executor, @Nullable android.os.OutcomeReceiver<java.lang.String,android.net.EthernetNetworkManagementException>);
-    method @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_STACK, android.Manifest.permission.MANAGE_ETHERNET_NETWORKS}) public void enableInterface(@NonNull String, @Nullable java.util.concurrent.Executor, @Nullable android.os.OutcomeReceiver<java.lang.String,android.net.EthernetNetworkManagementException>);
-    method @NonNull @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_STACK, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public android.net.EthernetManager.TetheredInterfaceRequest requestTetheredInterface(@NonNull java.util.concurrent.Executor, @NonNull android.net.EthernetManager.TetheredInterfaceCallback);
-    method @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_STACK, android.Manifest.permission.MANAGE_ETHERNET_NETWORKS}) public void updateConfiguration(@NonNull String, @NonNull android.net.EthernetNetworkUpdateRequest, @Nullable java.util.concurrent.Executor, @Nullable android.os.OutcomeReceiver<java.lang.String,android.net.EthernetNetworkManagementException>);
-  }
-
-  public static interface EthernetManager.TetheredInterfaceCallback {
-    method public void onAvailable(@NonNull String);
-    method public void onUnavailable();
-  }
-
-  public static class EthernetManager.TetheredInterfaceRequest {
-    method public void release();
-  }
-
-  public final class EthernetNetworkManagementException extends java.lang.RuntimeException implements android.os.Parcelable {
-    ctor public EthernetNetworkManagementException(@NonNull String);
-    method public int describeContents();
-    method public void writeToParcel(@NonNull android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.net.EthernetNetworkManagementException> CREATOR;
-  }
-
-  public final class EthernetNetworkUpdateRequest implements android.os.Parcelable {
-    method public int describeContents();
-    method @Nullable public android.net.IpConfiguration getIpConfiguration();
-    method @Nullable public android.net.NetworkCapabilities getNetworkCapabilities();
-    method public void writeToParcel(@NonNull android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.net.EthernetNetworkUpdateRequest> CREATOR;
-  }
-
-  public static final class EthernetNetworkUpdateRequest.Builder {
-    ctor public EthernetNetworkUpdateRequest.Builder();
-    ctor public EthernetNetworkUpdateRequest.Builder(@NonNull android.net.EthernetNetworkUpdateRequest);
-    method @NonNull public android.net.EthernetNetworkUpdateRequest build();
-    method @NonNull public android.net.EthernetNetworkUpdateRequest.Builder setIpConfiguration(@Nullable android.net.IpConfiguration);
-    method @NonNull public android.net.EthernetNetworkUpdateRequest.Builder setNetworkCapabilities(@Nullable android.net.NetworkCapabilities);
-  }
-
-  public class IpSecManager {
-    method @RequiresPermission(android.Manifest.permission.MANAGE_IPSEC_TUNNELS) public void applyTunnelModeTransform(@NonNull android.net.IpSecManager.IpSecTunnelInterface, int, @NonNull android.net.IpSecTransform) throws java.io.IOException;
-    method @NonNull @RequiresPermission(android.Manifest.permission.MANAGE_IPSEC_TUNNELS) public android.net.IpSecManager.IpSecTunnelInterface createIpSecTunnelInterface(@NonNull java.net.InetAddress, @NonNull java.net.InetAddress, @NonNull android.net.Network) throws java.io.IOException, android.net.IpSecManager.ResourceUnavailableException;
-    method @RequiresPermission(android.Manifest.permission.MANAGE_IPSEC_TUNNELS) public void startTunnelModeTransformMigration(@NonNull android.net.IpSecTransform, @NonNull java.net.InetAddress, @NonNull java.net.InetAddress);
-  }
-
-  public static final class IpSecManager.IpSecTunnelInterface implements java.lang.AutoCloseable {
-    method @RequiresPermission(android.Manifest.permission.MANAGE_IPSEC_TUNNELS) public void addAddress(@NonNull java.net.InetAddress, int) throws java.io.IOException;
-    method public void close();
-    method @NonNull public String getInterfaceName();
-    method @RequiresPermission(android.Manifest.permission.MANAGE_IPSEC_TUNNELS) public void removeAddress(@NonNull java.net.InetAddress, int) throws java.io.IOException;
-    method @RequiresPermission(android.Manifest.permission.MANAGE_IPSEC_TUNNELS) public void setUnderlyingNetwork(@NonNull android.net.Network) throws java.io.IOException;
-  }
-
-  public static class IpSecTransform.Builder {
-    method @NonNull @RequiresPermission(android.Manifest.permission.MANAGE_IPSEC_TUNNELS) public android.net.IpSecTransform buildTunnelModeTransform(@NonNull java.net.InetAddress, @NonNull android.net.IpSecManager.SecurityParameterIndex) throws java.io.IOException, android.net.IpSecManager.ResourceUnavailableException, android.net.IpSecManager.SpiUnavailableException;
-  }
-
-  public final class NetworkStats implements java.lang.Iterable<android.net.NetworkStats.Entry> android.os.Parcelable {
-    ctor public NetworkStats(long, int);
-    method @NonNull public android.net.NetworkStats add(@NonNull android.net.NetworkStats);
-    method @NonNull public android.net.NetworkStats addEntry(@NonNull android.net.NetworkStats.Entry);
-    method public android.net.NetworkStats clone();
-    method public int describeContents();
-    method @NonNull public java.util.Iterator<android.net.NetworkStats.Entry> iterator();
-    method @NonNull public android.net.NetworkStats subtract(@NonNull android.net.NetworkStats);
-    method public void writeToParcel(@NonNull android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.net.NetworkStats> CREATOR;
-    field public static final int DEFAULT_NETWORK_ALL = -1; // 0xffffffff
-    field public static final int DEFAULT_NETWORK_NO = 0; // 0x0
-    field public static final int DEFAULT_NETWORK_YES = 1; // 0x1
-    field public static final String IFACE_VT = "vt_data0";
-    field public static final int METERED_ALL = -1; // 0xffffffff
-    field public static final int METERED_NO = 0; // 0x0
-    field public static final int METERED_YES = 1; // 0x1
-    field public static final int ROAMING_ALL = -1; // 0xffffffff
-    field public static final int ROAMING_NO = 0; // 0x0
-    field public static final int ROAMING_YES = 1; // 0x1
-    field public static final int SET_ALL = -1; // 0xffffffff
-    field public static final int SET_DEFAULT = 0; // 0x0
-    field public static final int SET_FOREGROUND = 1; // 0x1
-    field public static final int TAG_NONE = 0; // 0x0
-    field public static final int UID_ALL = -1; // 0xffffffff
-    field public static final int UID_TETHERING = -5; // 0xfffffffb
-  }
-
-  public static class NetworkStats.Entry {
-    ctor public NetworkStats.Entry(@Nullable String, int, int, int, int, int, int, long, long, long, long, long);
-    method public int getDefaultNetwork();
-    method public int getMetered();
-    method public long getOperations();
-    method public int getRoaming();
-    method public long getRxBytes();
-    method public long getRxPackets();
-    method public int getSet();
-    method public int getTag();
-    method public long getTxBytes();
-    method public long getTxPackets();
-    method public int getUid();
-  }
-
-  public class TrafficStats {
-    method public static void setThreadStatsTagApp();
-    method public static void setThreadStatsTagBackup();
-    method public static void setThreadStatsTagRestore();
-    field public static final int TAG_NETWORK_STACK_IMPERSONATION_RANGE_END = -113; // 0xffffff8f
-    field public static final int TAG_NETWORK_STACK_IMPERSONATION_RANGE_START = -128; // 0xffffff80
-    field public static final int TAG_NETWORK_STACK_RANGE_END = -257; // 0xfffffeff
-    field public static final int TAG_NETWORK_STACK_RANGE_START = -768; // 0xfffffd00
-    field public static final int TAG_SYSTEM_IMPERSONATION_RANGE_END = -241; // 0xffffff0f
-    field public static final int TAG_SYSTEM_IMPERSONATION_RANGE_START = -256; // 0xffffff00
-  }
-
-}
-
-package android.net.netstats.provider {
-
-  public abstract class NetworkStatsProvider {
-    ctor public NetworkStatsProvider();
-    method public void notifyAlertReached();
-    method public void notifyLimitReached();
-    method public void notifyStatsUpdated(int, @NonNull android.net.NetworkStats, @NonNull android.net.NetworkStats);
-    method public void notifyWarningReached();
-    method public abstract void onRequestStatsUpdate(int);
-    method public abstract void onSetAlert(long);
-    method public abstract void onSetLimit(@NonNull String, long);
-    method public void onSetWarningAndLimit(@NonNull String, long, long);
-    field public static final int QUOTA_UNLIMITED = -1; // 0xffffffff
-  }
-
-}
-
-package android.net.nsd {
-
-  public final class NsdManager {
-    method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_STACK}) public void registerOffloadEngine(@NonNull String, long, long, @NonNull java.util.concurrent.Executor, @NonNull android.net.nsd.OffloadEngine);
-    method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_STACK}) public void unregisterOffloadEngine(@NonNull android.net.nsd.OffloadEngine);
-  }
-
-  public interface OffloadEngine {
-    method public void onOffloadServiceRemoved(@NonNull android.net.nsd.OffloadServiceInfo);
-    method public void onOffloadServiceUpdated(@NonNull android.net.nsd.OffloadServiceInfo);
-    field public static final int OFFLOAD_CAPABILITY_BYPASS_MULTICAST_LOCK = 1; // 0x1
-    field public static final int OFFLOAD_TYPE_FILTER_QUERIES = 2; // 0x2
-    field public static final int OFFLOAD_TYPE_FILTER_REPLIES = 4; // 0x4
-    field public static final int OFFLOAD_TYPE_REPLY = 1; // 0x1
-  }
-
-  public final class OffloadServiceInfo implements android.os.Parcelable {
-    ctor public OffloadServiceInfo(@NonNull android.net.nsd.OffloadServiceInfo.Key, @NonNull java.util.List<java.lang.String>, @NonNull String, @Nullable byte[], @IntRange(from=0, to=java.lang.Integer.MAX_VALUE) int, long);
-    method public int describeContents();
-    method @NonNull public String getHostname();
-    method @NonNull public android.net.nsd.OffloadServiceInfo.Key getKey();
-    method @Nullable public byte[] getOffloadPayload();
-    method public long getOffloadType();
-    method public int getPriority();
-    method @NonNull public java.util.List<java.lang.String> getSubtypes();
-    method public void writeToParcel(@NonNull android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.net.nsd.OffloadServiceInfo> CREATOR;
-  }
-
-  public static final class OffloadServiceInfo.Key implements android.os.Parcelable {
-    ctor public OffloadServiceInfo.Key(@NonNull String, @NonNull String);
-    method public int describeContents();
-    method @NonNull public String getServiceName();
-    method @NonNull public String getServiceType();
-    method public void writeToParcel(@NonNull android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.net.nsd.OffloadServiceInfo.Key> CREATOR;
-  }
-
-}
-
diff --git a/framework-t/udc-extended-api/system-lint-baseline.txt b/framework-t/udc-extended-api/system-lint-baseline.txt
deleted file mode 100644
index 9baf991..0000000
--- a/framework-t/udc-extended-api/system-lint-baseline.txt
+++ /dev/null
@@ -1,7 +0,0 @@
-// Baseline format: 1.0
-BuilderSetStyle: android.net.IpSecTransform.Builder#buildTunnelModeTransform(java.net.InetAddress, android.net.IpSecManager.SecurityParameterIndex):
-    Builder methods names should use setFoo() / addFoo() / clearFoo() style: method android.net.IpSecTransform.Builder.buildTunnelModeTransform(java.net.InetAddress,android.net.IpSecManager.SecurityParameterIndex)
-
-
-GenericException: android.net.IpSecManager.IpSecTunnelInterface#finalize():
-    Methods must not throw generic exceptions (`java.lang.Throwable`)
diff --git a/framework-t/udc-extended-api/system-removed.txt b/framework-t/udc-extended-api/system-removed.txt
deleted file mode 100644
index d802177..0000000
--- a/framework-t/udc-extended-api/system-removed.txt
+++ /dev/null
@@ -1 +0,0 @@
-// Signature format: 2.0
diff --git a/framework/Android.bp b/framework/Android.bp
index c88bacc..31ddb53 100644
--- a/framework/Android.bp
+++ b/framework/Android.bp
@@ -15,19 +15,11 @@
 //
 
 package {
+    default_team: "trendy_team_fwk_core_networking",
     // See: http://go/android-license-faq
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
-// In the branch which does not support FlaggedAPI, use this default to ignore the annotated APIs.
-java_defaults {
-    name: "FlaggedApiDefaults",
-}
-
-// The above variables may have different values
-// depending on the branch, and this comment helps
-// separate them from the rest of the file to avoid merge conflicts
-
 filegroup {
     name: "framework-connectivity-internal-sources",
     srcs: [
@@ -94,7 +86,9 @@
         "framework-wifi.stubs.module_lib",
     ],
     static_libs: [
-        "mdns_aidl_interface-lateststable-java",
+        // Not using the latest stable version because all functions in the latest version of
+        // mdns_aidl_interface are deprecated.
+        "mdns_aidl_interface-V1-java",
         "modules-utils-backgroundthread",
         "modules-utils-build",
         "modules-utils-preconditions",
@@ -102,6 +96,7 @@
     ],
     impl_only_static_libs: [
         "net-utils-device-common-bpf",
+        "net-utils-device-common-struct",
     ],
     libs: [
         "androidx.annotation_annotation",
@@ -112,7 +107,9 @@
     apex_available: [
         "com.android.tethering",
     ],
-    lint: { strict_updatability_linting: true },
+    lint: {
+        strict_updatability_linting: true,
+    },
 }
 
 java_library {
@@ -131,6 +128,7 @@
         // Even if the library is included in "impl_only_static_libs" of defaults. This is still
         // needed because java_library which doesn't understand "impl_only_static_libs".
         "net-utils-device-common-bpf",
+        "net-utils-device-common-struct",
     ],
     libs: [
         // This cannot be in the defaults clause above because if it were, it would be used
@@ -141,7 +139,7 @@
         "framework-tethering.impl",
         "framework-wifi.stubs.module_lib",
     ],
-    visibility: ["//packages/modules/Connectivity:__subpackages__"]
+    visibility: ["//packages/modules/Connectivity:__subpackages__"],
 }
 
 java_defaults {
@@ -161,7 +159,6 @@
     defaults: [
         "framework-connectivity-defaults",
         "CronetJavaDefaults",
-        "FlaggedApiDefaults",
     ],
     installable: true,
     jarjar_rules: ":framework-connectivity-jarjar-rules",
@@ -193,10 +190,14 @@
         "//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",
+    },
 }
 
 platform_compat_config {
@@ -266,7 +267,7 @@
         ":framework-connectivity-t-pre-jarjar{.jar}",
         ":framework-connectivity.stubs.module_lib{.jar}",
         ":framework-connectivity-t.stubs.module_lib{.jar}",
-        ":framework-connectivity-flagged-apis{.jar}",
+        ":framework-connectivity-module-api-stubs-including-flagged{.jar}",
         "jarjar-excludes.txt",
     ],
     tools: [
@@ -279,7 +280,7 @@
         "--prefix android.net.connectivity " +
         "--apistubs $(location :framework-connectivity.stubs.module_lib{.jar}) " +
         "--apistubs $(location :framework-connectivity-t.stubs.module_lib{.jar}) " +
-        "--apistubs $(location :framework-connectivity-flagged-apis{.jar}) " +
+        "--apistubs $(location :framework-connectivity-module-api-stubs-including-flagged{.jar}) " +
         // Make a ":"-separated list. There will be an extra ":" but empty items are ignored.
         "--unsupportedapi $$(printf ':%s' $(locations :connectivity-hiddenapi-files)) " +
         "--excludes $(location jarjar-excludes.txt) " +
@@ -292,7 +293,7 @@
 }
 
 droidstubs {
-    name: "framework-connectivity-flagged-apis-droidstubs",
+    name: "framework-connectivity-module-api-stubs-including-flagged-droidstubs",
     srcs: [
         ":framework-connectivity-sources",
         ":framework-connectivity-tiramisu-updatable-sources",
@@ -300,7 +301,10 @@
         ":framework-thread-sources",
     ],
     flags: [
-        "--show-annotation android.annotation.FlaggedApi",
+        "--show-for-stub-purposes-annotation android.annotation.SystemApi" +
+            "\\(client=android.annotation.SystemApi.Client.PRIVILEGED_APPS\\)",
+        "--show-for-stub-purposes-annotation android.annotation.SystemApi" +
+            "\\(client=android.annotation.SystemApi.Client.MODULE_LIBRARIES\\)",
     ],
     aidl: {
         include_dirs: [
@@ -311,8 +315,8 @@
 }
 
 java_library {
-    name: "framework-connectivity-flagged-apis",
-    srcs: [":framework-connectivity-flagged-apis-droidstubs"],
+    name: "framework-connectivity-module-api-stubs-including-flagged",
+    srcs: [":framework-connectivity-module-api-stubs-including-flagged-droidstubs"],
 }
 
 // Library providing limited APIs within the connectivity module, so that R+ components like
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/jni/android_net_NetworkUtils.cpp b/framework/jni/android_net_NetworkUtils.cpp
index ca297e5..51eaf1c 100644
--- a/framework/jni/android_net_NetworkUtils.cpp
+++ b/framework/jni/android_net_NetworkUtils.cpp
@@ -24,8 +24,10 @@
 #include <string.h>
 
 #include <bpf/BpfClassic.h>
+#include <bpf/KernelUtils.h>
 #include <DnsProxydProtocol.h> // NETID_USE_LOCAL_NAMESERVERS
 #include <nativehelper/JNIPlatformHelp.h>
+#include <nativehelper/ScopedPrimitiveArray.h>
 #include <utils/Log.h>
 
 #include "jni.h"
@@ -240,6 +242,19 @@
             trw.rcv_wnd, trw.rcv_wup, tcpinfo.tcpi_rcv_wscale);
 }
 
+static void android_net_utils_setsockoptBytes(JNIEnv *env, jclass clazz, jobject javaFd,
+        jint level, jint option, jbyteArray javaBytes) {
+    int sock = AFileDescriptor_getFd(env, javaFd);
+    ScopedByteArrayRO value(env, javaBytes);
+    if (setsockopt(sock, level, option, value.get(), value.size()) != 0) {
+        jniThrowErrnoException(env, "setsockoptBytes", errno);
+    }
+}
+
+static jboolean android_net_utils_isKernel64Bit(JNIEnv *env, jclass clazz) {
+    return bpf::isKernel64Bit();
+}
+
 // ----------------------------------------------------------------------------
 
 /*
@@ -260,6 +275,9 @@
     { "resNetworkResult", "(Ljava/io/FileDescriptor;)Landroid/net/DnsResolver$DnsResponse;", (void*) android_net_utils_resNetworkResult },
     { "resNetworkCancel", "(Ljava/io/FileDescriptor;)V", (void*) android_net_utils_resNetworkCancel },
     { "getDnsNetwork", "()Landroid/net/Network;", (void*) android_net_utils_getDnsNetwork },
+    { "setsockoptBytes", "(Ljava/io/FileDescriptor;II[B)V",
+    (void*) android_net_utils_setsockoptBytes},
+    { "isKernel64Bit", "()Z", (void*) android_net_utils_isKernel64Bit },
 };
 // 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/BpfNetMapsReader.java b/framework/src/android/net/BpfNetMapsReader.java
index 37c58f0..ee422ab 100644
--- a/framework/src/android/net/BpfNetMapsReader.java
+++ b/framework/src/android/net/BpfNetMapsReader.java
@@ -17,6 +17,9 @@
 package android.net;
 
 import static android.net.BpfNetMapsConstants.CONFIGURATION_MAP_PATH;
+import static android.net.BpfNetMapsConstants.DATA_SAVER_ENABLED;
+import static android.net.BpfNetMapsConstants.DATA_SAVER_ENABLED_KEY;
+import static android.net.BpfNetMapsConstants.DATA_SAVER_ENABLED_MAP_PATH;
 import static android.net.BpfNetMapsConstants.HAPPY_BOX_MATCH;
 import static android.net.BpfNetMapsConstants.PENALTY_BOX_MATCH;
 import static android.net.BpfNetMapsConstants.UID_OWNER_MAP_PATH;
@@ -38,9 +41,9 @@
 import com.android.modules.utils.build.SdkLevel;
 import com.android.net.module.util.BpfMap;
 import com.android.net.module.util.IBpfMap;
-import com.android.net.module.util.Struct;
 import com.android.net.module.util.Struct.S32;
 import com.android.net.module.util.Struct.U32;
+import com.android.net.module.util.Struct.U8;
 
 /**
  * A helper class to *read* java BpfMaps.
@@ -48,6 +51,8 @@
  */
 @RequiresApi(Build.VERSION_CODES.TIRAMISU)  // BPF maps were only mainlined in T
 public class BpfNetMapsReader {
+    private static final String TAG = BpfNetMapsReader.class.getSimpleName();
+
     // Locally store the handle of bpf maps. The FileDescriptors are statically cached inside the
     // BpfMap implementation.
 
@@ -57,6 +62,7 @@
     // Bpf map to store per uid traffic control configurations.
     // See {@link UidOwnerValue} for more detail.
     private final IBpfMap<S32, UidOwnerValue> mUidOwnerMap;
+    private final IBpfMap<S32, U8> mDataSaverEnabledMap;
     private final Dependencies mDeps;
 
     // Bitmaps for calculating whether a given uid is blocked by firewall chains.
@@ -104,6 +110,7 @@
         mDeps = deps;
         mConfigurationMap = mDeps.getConfigurationMap();
         mUidOwnerMap = mDeps.getUidOwnerMap();
+        mDataSaverEnabledMap = mDeps.getDataSaverEnabledMap();
     }
 
     /**
@@ -130,6 +137,16 @@
                 throw new IllegalStateException("Cannot open uid owner map", e);
             }
         }
+
+        /** Get the data saver enabled map. */
+        public  IBpfMap<S32, U8> getDataSaverEnabledMap() {
+            try {
+                return new BpfMap<>(DATA_SAVER_ENABLED_MAP_PATH, BpfMap.BPF_F_RDONLY, S32.class,
+                        U8.class);
+            } catch (ErrnoException e) {
+                throw new IllegalStateException("Cannot open data saver enabled map", e);
+            }
+        }
     }
 
     /**
@@ -171,12 +188,12 @@
      *                                  cause of the failure.
      */
     public static boolean isChainEnabled(
-            final IBpfMap<Struct.S32, Struct.U32> configurationMap, final int chain) {
+            final IBpfMap<S32, U32> configurationMap, final int chain) {
         throwIfPreT("isChainEnabled is not available on pre-T devices");
 
         final long match = getMatchByFirewallChain(chain);
         try {
-            final Struct.U32 config = configurationMap.getValue(UID_RULES_CONFIGURATION_KEY);
+            final U32 config = configurationMap.getValue(UID_RULES_CONFIGURATION_KEY);
             return (config.val & match) != 0;
         } catch (ErrnoException e) {
             throw new ServiceSpecificException(e.errno,
@@ -195,14 +212,14 @@
      * @throws ServiceSpecificException      in case of failure, with an error code indicating the
      *                                       cause of the failure.
      */
-    public static int getUidRule(final IBpfMap<Struct.S32, UidOwnerValue> uidOwnerMap,
+    public static int getUidRule(final IBpfMap<S32, UidOwnerValue> uidOwnerMap,
             final int chain, final int uid) {
         throwIfPreT("getUidRule is not available on pre-T devices");
 
         final long match = getMatchByFirewallChain(chain);
         final boolean isAllowList = isFirewallAllowList(chain);
         try {
-            final UidOwnerValue uidMatch = uidOwnerMap.getValue(new Struct.S32(uid));
+            final UidOwnerValue uidMatch = uidOwnerMap.getValue(new S32(uid));
             final boolean isMatchEnabled = uidMatch != null && (uidMatch.rule & match) != 0;
             return isMatchEnabled == isAllowList ? FIREWALL_RULE_ALLOW : FIREWALL_RULE_DENY;
         } catch (ErrnoException e) {
@@ -249,4 +266,22 @@
         if ((uidMatch & HAPPY_BOX_MATCH) != 0) return false;
         return isDataSaverEnabled;
     }
+
+    /**
+     * Get Data Saver enabled or disabled
+     *
+     * @return whether Data Saver is enabled or disabled.
+     * @throws ServiceSpecificException in case of failure, with an error code indicating the
+     *                                  cause of the failure.
+     */
+    public boolean getDataSaverEnabled() {
+        throwIfPreT("getDataSaverEnabled is not available on pre-T devices");
+
+        try {
+            return mDataSaverEnabledMap.getValue(DATA_SAVER_ENABLED_KEY).val == DATA_SAVER_ENABLED;
+        } catch (ErrnoException e) {
+            throw new ServiceSpecificException(e.errno, "Unable to get data saver: "
+                    + Os.strerror(e.errno));
+        }
+    }
 }
diff --git a/framework/src/android/net/BpfNetMapsUtils.java b/framework/src/android/net/BpfNetMapsUtils.java
index 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 57ecf49..1ea1815 100644
--- a/framework/src/android/net/ConnectivityManager.java
+++ b/framework/src/android/net/ConnectivityManager.java
@@ -16,8 +16,6 @@
 package android.net;
 
 import static android.annotation.SystemApi.Client.MODULE_LIBRARIES;
-import static android.content.pm.ApplicationInfo.FLAG_PERSISTENT;
-import static android.content.pm.ApplicationInfo.FLAG_SYSTEM;
 import static android.net.NetworkCapabilities.NET_ENTERPRISE_ID_1;
 import static android.net.NetworkRequest.Type.BACKGROUND_REQUEST;
 import static android.net.NetworkRequest.Type.LISTEN;
@@ -27,8 +25,6 @@
 import static android.net.NetworkRequest.Type.TRACK_SYSTEM_DEFAULT;
 import static android.net.QosCallback.QosCallbackRegistrationException;
 
-import static com.android.internal.annotations.VisibleForTesting.Visibility.PRIVATE;
-
 import android.annotation.CallbackExecutor;
 import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
@@ -41,16 +37,12 @@
 import android.annotation.SuppressLint;
 import android.annotation.SystemApi;
 import android.annotation.SystemService;
-import android.annotation.TargetApi;
-import android.app.Application;
 import android.app.PendingIntent;
 import android.app.admin.DevicePolicyManager;
 import android.compat.annotation.UnsupportedAppUsage;
-import android.content.BroadcastReceiver;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
-import android.content.IntentFilter;
 import android.net.ConnectivityDiagnosticsManager.DataStallReport.DetectionMethod;
 import android.net.IpSecManager.UdpEncapsulationSocket;
 import android.net.SocketKeepalive.Callback;
@@ -82,7 +74,6 @@
 import android.util.SparseIntArray;
 
 import com.android.internal.annotations.GuardedBy;
-import com.android.internal.annotations.VisibleForTesting;
 
 import libcore.net.event.NetworkEventDispatcher;
 
@@ -104,7 +95,6 @@
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 import java.util.concurrent.RejectedExecutionException;
-import java.util.concurrent.atomic.AtomicBoolean;
 
 /**
  * Class that answers queries about the state of network connectivity. It also
@@ -6032,6 +6022,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
@@ -6261,90 +6258,6 @@
     }
 
     /**
-     * Helper class to track data saver status.
-     *
-     * The class will fetch current data saver status from {@link NetworkPolicyManager} when
-     * initialized, and listening for status changed intent to cache the latest status.
-     *
-     * @hide
-     */
-    @TargetApi(Build.VERSION_CODES.TIRAMISU) // RECEIVER_NOT_EXPORTED requires T.
-    @VisibleForTesting(visibility = PRIVATE)
-    public static class DataSaverStatusTracker extends BroadcastReceiver {
-        private static final Object sDataSaverStatusTrackerLock = new Object();
-
-        private static volatile DataSaverStatusTracker sInstance;
-
-        /**
-         * Gets a static instance of the class.
-         *
-         * @param context A {@link Context} for initialization. Note that since the data saver
-         *                status is global on a device, passing any context is equivalent.
-         * @return The static instance of a {@link DataSaverStatusTracker}.
-         */
-        public static DataSaverStatusTracker getInstance(@NonNull Context context) {
-            if (sInstance == null) {
-                synchronized (sDataSaverStatusTrackerLock) {
-                    if (sInstance == null) {
-                        sInstance = new DataSaverStatusTracker(context);
-                    }
-                }
-            }
-            return sInstance;
-        }
-
-        private final NetworkPolicyManager mNpm;
-        // The value updates on the caller's binder thread or UI thread.
-        private final AtomicBoolean mIsDataSaverEnabled;
-
-        @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
-        public DataSaverStatusTracker(final Context context) {
-            // To avoid leaks, take the application context.
-            final Context appContext;
-            if (context instanceof Application) {
-                appContext = context;
-            } else {
-                appContext = context.getApplicationContext();
-            }
-
-            if ((appContext.getApplicationInfo().flags & FLAG_PERSISTENT) == 0
-                    && (appContext.getApplicationInfo().flags & FLAG_SYSTEM) == 0) {
-                throw new IllegalStateException("Unexpected caller: "
-                        + appContext.getApplicationInfo().packageName);
-            }
-
-            mNpm = appContext.getSystemService(NetworkPolicyManager.class);
-            final IntentFilter filter = new IntentFilter(
-                    ConnectivityManager.ACTION_RESTRICT_BACKGROUND_CHANGED);
-            // The receiver should not receive broadcasts from other Apps.
-            appContext.registerReceiver(this, filter, Context.RECEIVER_NOT_EXPORTED);
-            mIsDataSaverEnabled = new AtomicBoolean();
-            updateDataSaverEnabled();
-        }
-
-        // Runs on caller's UI thread.
-        @Override
-        public void onReceive(Context context, Intent intent) {
-            if (intent.getAction().equals(ConnectivityManager.ACTION_RESTRICT_BACKGROUND_CHANGED)) {
-                updateDataSaverEnabled();
-            } else {
-                throw new IllegalStateException("Unexpected intent " + intent);
-            }
-        }
-
-        public boolean getDataSaverEnabled() {
-            return mIsDataSaverEnabled.get();
-        }
-
-        private void updateDataSaverEnabled() {
-            // Uid doesn't really matter, but use a fixed UID to make things clearer.
-            final int dataSaverForCallerUid = mNpm.getRestrictBackgroundStatus(Process.SYSTEM_UID);
-            mIsDataSaverEnabled.set(dataSaverForCallerUid
-                    != ConnectivityManager.RESTRICT_BACKGROUND_STATUS_DISABLED);
-        }
-    }
-
-    /**
      * Return whether the network is blocked for the given uid and metered condition.
      *
      * Similar to {@link NetworkPolicyManager#isUidNetworkingBlocked}, but directly reads the BPF
@@ -6369,14 +6282,12 @@
     @RequiresPermission(NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK)
     public boolean isUidNetworkingBlocked(int uid, boolean isNetworkMetered) {
         final BpfNetMapsReader reader = BpfNetMapsReader.getInstance();
-
-        final boolean isDataSaverEnabled;
-        // TODO: For U-QPR3+ devices, get data saver status from bpf configuration map directly.
-        final DataSaverStatusTracker dataSaverStatusTracker =
-                DataSaverStatusTracker.getInstance(mContext);
-        isDataSaverEnabled = dataSaverStatusTracker.getDataSaverEnabled();
-
-        return reader.isUidNetworkingBlocked(uid, isNetworkMetered, isDataSaverEnabled);
+        // Note that before V, the data saver status in bpf is written by ConnectivityService
+        // when receiving {@link #ACTION_RESTRICT_BACKGROUND_CHANGED}. Thus,
+        // the status is not synchronized.
+        // On V+, the data saver status is set by platform code when enabling/disabling
+        // data saver, which is synchronized.
+        return reader.isUidNetworkingBlocked(uid, isNetworkMetered, reader.getDataSaverEnabled());
     }
 
     /** @hide */
diff --git a/framework/src/android/net/NetworkAgent.java b/framework/src/android/net/NetworkAgent.java
index 4e9087c..574ab2f 100644
--- a/framework/src/android/net/NetworkAgent.java
+++ b/framework/src/android/net/NetworkAgent.java
@@ -756,11 +756,20 @@
             }
             final ConnectivityManager cm = (ConnectivityManager) mInitialConfiguration.context
                     .getSystemService(Context.CONNECTIVITY_SERVICE);
-            mNetwork = cm.registerNetworkAgent(new NetworkAgentBinder(mHandler),
-                    new NetworkInfo(mInitialConfiguration.info),
-                    mInitialConfiguration.properties, mInitialConfiguration.capabilities,
-                    mInitialConfiguration.localNetworkConfig, mInitialConfiguration.score,
-                    mInitialConfiguration.config, providerId);
+            if (mInitialConfiguration.localNetworkConfig == null) {
+                // Call registerNetworkAgent without localNetworkConfig argument to pass
+                // android.net.cts.NetworkAgentTest#testAgentStartsInConnecting in old cts
+                mNetwork = cm.registerNetworkAgent(new NetworkAgentBinder(mHandler),
+                        new NetworkInfo(mInitialConfiguration.info),
+                        mInitialConfiguration.properties, mInitialConfiguration.capabilities,
+                        mInitialConfiguration.score, mInitialConfiguration.config, providerId);
+            } else {
+                mNetwork = cm.registerNetworkAgent(new NetworkAgentBinder(mHandler),
+                        new NetworkInfo(mInitialConfiguration.info),
+                        mInitialConfiguration.properties, mInitialConfiguration.capabilities,
+                        mInitialConfiguration.localNetworkConfig, mInitialConfiguration.score,
+                        mInitialConfiguration.config, providerId);
+            }
             mInitialConfiguration = null; // All this memory can now be GC'd
         }
         return mNetwork;
diff --git a/framework/src/android/net/NetworkCapabilities.java b/framework/src/android/net/NetworkCapabilities.java
index f959114..efae754 100644
--- a/framework/src/android/net/NetworkCapabilities.java
+++ b/framework/src/android/net/NetworkCapabilities.java
@@ -1457,6 +1457,18 @@
         return mTransportTypes == (1 << transportType);
     }
 
+    /**
+     * Returns true iff this NC has the specified transport and no other, ignoring TRANSPORT_TEST.
+     *
+     * If this NC has the passed transport and no other, this method returns true.
+     * If this NC has the passed transport, TRANSPORT_TEST and no other, this method returns true.
+     * Otherwise, this method returns false.
+     * @hide
+     */
+    public boolean hasSingleTransportBesidesTest(@Transport int transportType) {
+        return (mTransportTypes & ~(1 << TRANSPORT_TEST)) == (1 << transportType);
+    }
+
     private boolean satisfiedByTransportTypes(NetworkCapabilities nc) {
         return ((this.mTransportTypes == 0)
                 || ((this.mTransportTypes & nc.mTransportTypes) != 0));
diff --git a/framework/src/android/net/NetworkRequest.java b/framework/src/android/net/NetworkRequest.java
index 9824faa..653e41d 100644
--- a/framework/src/android/net/NetworkRequest.java
+++ b/framework/src/android/net/NetworkRequest.java
@@ -290,7 +290,10 @@
          * Therefore these capabilities are only in NetworkRequest.
          */
         private static final int[] DEFAULT_FORBIDDEN_CAPABILITIES = new int[] {
-            NET_CAPABILITY_LOCAL_NETWORK
+            // TODO(b/313030307): this should contain NET_CAPABILITY_LOCAL_NETWORK.
+            // We cannot currently add it because doing so would crash if the module rolls back,
+            // because JobScheduler persists NetworkRequests to disk, and existing production code
+            // does not consider LOCAL_NETWORK to be a valid capability.
         };
 
         private final NetworkCapabilities mNetworkCapabilities;
diff --git a/framework/src/android/net/NetworkUtils.java b/framework/src/android/net/NetworkUtils.java
index 2679b62..785c029 100644
--- a/framework/src/android/net/NetworkUtils.java
+++ b/framework/src/android/net/NetworkUtils.java
@@ -426,4 +426,18 @@
         return routedIPCount;
     }
 
+    /**
+     * Sets a socket option with byte array
+     *
+     * @param fd The socket file descriptor
+     * @param level The level at which the option is defined
+     * @param option The socket option for which the value is to be set
+     * @param value The option value to be set in byte array
+     * @throws ErrnoException if setsockopt fails
+     */
+    public static native void setsockoptBytes(FileDescriptor fd, int level, int option,
+            byte[] value) throws ErrnoException;
+
+    /** Returns whether the Linux Kernel is 64 bit */
+    public static native boolean isKernel64Bit();
 }
diff --git a/framework/udc-extended-api/current.txt b/framework/udc-extended-api/current.txt
deleted file mode 100644
index 6860c3c..0000000
--- a/framework/udc-extended-api/current.txt
+++ /dev/null
@@ -1,816 +0,0 @@
-// Signature format: 2.0
-package android.net {
-
-  public class CaptivePortal implements android.os.Parcelable {
-    method public int describeContents();
-    method public void ignoreNetwork();
-    method public void reportCaptivePortalDismissed();
-    method public void writeToParcel(android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.net.CaptivePortal> CREATOR;
-  }
-
-  public class ConnectivityDiagnosticsManager {
-    method public void registerConnectivityDiagnosticsCallback(@NonNull android.net.NetworkRequest, @NonNull java.util.concurrent.Executor, @NonNull android.net.ConnectivityDiagnosticsManager.ConnectivityDiagnosticsCallback);
-    method public void unregisterConnectivityDiagnosticsCallback(@NonNull android.net.ConnectivityDiagnosticsManager.ConnectivityDiagnosticsCallback);
-  }
-
-  public abstract static class ConnectivityDiagnosticsManager.ConnectivityDiagnosticsCallback {
-    ctor public ConnectivityDiagnosticsManager.ConnectivityDiagnosticsCallback();
-    method public void onConnectivityReportAvailable(@NonNull android.net.ConnectivityDiagnosticsManager.ConnectivityReport);
-    method public void onDataStallSuspected(@NonNull android.net.ConnectivityDiagnosticsManager.DataStallReport);
-    method public void onNetworkConnectivityReported(@NonNull android.net.Network, boolean);
-  }
-
-  public static final class ConnectivityDiagnosticsManager.ConnectivityReport implements android.os.Parcelable {
-    ctor public ConnectivityDiagnosticsManager.ConnectivityReport(@NonNull android.net.Network, long, @NonNull android.net.LinkProperties, @NonNull android.net.NetworkCapabilities, @NonNull android.os.PersistableBundle);
-    method public int describeContents();
-    method @NonNull public android.os.PersistableBundle getAdditionalInfo();
-    method @NonNull public android.net.LinkProperties getLinkProperties();
-    method @NonNull public android.net.Network getNetwork();
-    method @NonNull public android.net.NetworkCapabilities getNetworkCapabilities();
-    method public long getReportTimestamp();
-    method public void writeToParcel(@NonNull android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.net.ConnectivityDiagnosticsManager.ConnectivityReport> CREATOR;
-    field public static final String KEY_NETWORK_PROBES_ATTEMPTED_BITMASK = "networkProbesAttempted";
-    field public static final String KEY_NETWORK_PROBES_SUCCEEDED_BITMASK = "networkProbesSucceeded";
-    field public static final String KEY_NETWORK_VALIDATION_RESULT = "networkValidationResult";
-    field public static final int NETWORK_PROBE_DNS = 4; // 0x4
-    field public static final int NETWORK_PROBE_FALLBACK = 32; // 0x20
-    field public static final int NETWORK_PROBE_HTTP = 8; // 0x8
-    field public static final int NETWORK_PROBE_HTTPS = 16; // 0x10
-    field public static final int NETWORK_PROBE_PRIVATE_DNS = 64; // 0x40
-    field public static final int NETWORK_VALIDATION_RESULT_INVALID = 0; // 0x0
-    field public static final int NETWORK_VALIDATION_RESULT_PARTIALLY_VALID = 2; // 0x2
-    field public static final int NETWORK_VALIDATION_RESULT_SKIPPED = 3; // 0x3
-    field public static final int NETWORK_VALIDATION_RESULT_VALID = 1; // 0x1
-  }
-
-  public static final class ConnectivityDiagnosticsManager.DataStallReport implements android.os.Parcelable {
-    ctor public ConnectivityDiagnosticsManager.DataStallReport(@NonNull android.net.Network, long, int, @NonNull android.net.LinkProperties, @NonNull android.net.NetworkCapabilities, @NonNull android.os.PersistableBundle);
-    method public int describeContents();
-    method public int getDetectionMethod();
-    method @NonNull public android.net.LinkProperties getLinkProperties();
-    method @NonNull public android.net.Network getNetwork();
-    method @NonNull public android.net.NetworkCapabilities getNetworkCapabilities();
-    method public long getReportTimestamp();
-    method @NonNull public android.os.PersistableBundle getStallDetails();
-    method public void writeToParcel(@NonNull android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.net.ConnectivityDiagnosticsManager.DataStallReport> CREATOR;
-    field public static final int DETECTION_METHOD_DNS_EVENTS = 1; // 0x1
-    field public static final int DETECTION_METHOD_TCP_METRICS = 2; // 0x2
-    field public static final String KEY_DNS_CONSECUTIVE_TIMEOUTS = "dnsConsecutiveTimeouts";
-    field public static final String KEY_TCP_METRICS_COLLECTION_PERIOD_MILLIS = "tcpMetricsCollectionPeriodMillis";
-    field public static final String KEY_TCP_PACKET_FAIL_RATE = "tcpPacketFailRate";
-  }
-
-  public class ConnectivityManager {
-    method public void addDefaultNetworkActiveListener(android.net.ConnectivityManager.OnNetworkActiveListener);
-    method public boolean bindProcessToNetwork(@Nullable android.net.Network);
-    method @NonNull public android.net.SocketKeepalive createSocketKeepalive(@NonNull android.net.Network, @NonNull android.net.IpSecManager.UdpEncapsulationSocket, @NonNull java.net.InetAddress, @NonNull java.net.InetAddress, @NonNull java.util.concurrent.Executor, @NonNull android.net.SocketKeepalive.Callback);
-    method @Nullable @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public android.net.Network getActiveNetwork();
-    method @Deprecated @Nullable @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public android.net.NetworkInfo getActiveNetworkInfo();
-    method @Deprecated @NonNull @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public android.net.NetworkInfo[] getAllNetworkInfo();
-    method @Deprecated @NonNull @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public android.net.Network[] getAllNetworks();
-    method @Deprecated public boolean getBackgroundDataSetting();
-    method @Nullable public android.net.Network getBoundNetworkForProcess();
-    method public int getConnectionOwnerUid(int, @NonNull java.net.InetSocketAddress, @NonNull java.net.InetSocketAddress);
-    method @Nullable public android.net.ProxyInfo getDefaultProxy();
-    method @Nullable @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public android.net.LinkProperties getLinkProperties(@Nullable android.net.Network);
-    method @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public int getMultipathPreference(@Nullable android.net.Network);
-    method @Nullable @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public android.net.NetworkCapabilities getNetworkCapabilities(@Nullable android.net.Network);
-    method @Deprecated @Nullable @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public android.net.NetworkInfo getNetworkInfo(int);
-    method @Deprecated @Nullable @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public android.net.NetworkInfo getNetworkInfo(@Nullable android.net.Network);
-    method @Deprecated @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public int getNetworkPreference();
-    method @Nullable public byte[] getNetworkWatchlistConfigHash();
-    method @Deprecated @Nullable public static android.net.Network getProcessDefaultNetwork();
-    method public int getRestrictBackgroundStatus();
-    method @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public boolean isActiveNetworkMetered();
-    method public boolean isDefaultNetworkActive();
-    method @Deprecated public static boolean isNetworkTypeValid(int);
-    method public void registerBestMatchingNetworkCallback(@NonNull android.net.NetworkRequest, @NonNull android.net.ConnectivityManager.NetworkCallback, @NonNull android.os.Handler);
-    method @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public void registerDefaultNetworkCallback(@NonNull android.net.ConnectivityManager.NetworkCallback);
-    method @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public void registerDefaultNetworkCallback(@NonNull android.net.ConnectivityManager.NetworkCallback, @NonNull android.os.Handler);
-    method @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public void registerNetworkCallback(@NonNull android.net.NetworkRequest, @NonNull android.net.ConnectivityManager.NetworkCallback);
-    method @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public void registerNetworkCallback(@NonNull android.net.NetworkRequest, @NonNull android.net.ConnectivityManager.NetworkCallback, @NonNull android.os.Handler);
-    method @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public void registerNetworkCallback(@NonNull android.net.NetworkRequest, @NonNull android.app.PendingIntent);
-    method public void releaseNetworkRequest(@NonNull android.app.PendingIntent);
-    method public void removeDefaultNetworkActiveListener(@NonNull android.net.ConnectivityManager.OnNetworkActiveListener);
-    method @Deprecated public void reportBadNetwork(@Nullable android.net.Network);
-    method public void reportNetworkConnectivity(@Nullable android.net.Network, boolean);
-    method public boolean requestBandwidthUpdate(@NonNull android.net.Network);
-    method public void requestNetwork(@NonNull android.net.NetworkRequest, @NonNull android.net.ConnectivityManager.NetworkCallback);
-    method public void requestNetwork(@NonNull android.net.NetworkRequest, @NonNull android.net.ConnectivityManager.NetworkCallback, @NonNull android.os.Handler);
-    method public void requestNetwork(@NonNull android.net.NetworkRequest, @NonNull android.net.ConnectivityManager.NetworkCallback, int);
-    method public void requestNetwork(@NonNull android.net.NetworkRequest, @NonNull android.net.ConnectivityManager.NetworkCallback, @NonNull android.os.Handler, int);
-    method public void requestNetwork(@NonNull android.net.NetworkRequest, @NonNull android.app.PendingIntent);
-    method @Deprecated public void setNetworkPreference(int);
-    method @Deprecated public static boolean setProcessDefaultNetwork(@Nullable android.net.Network);
-    method public void unregisterNetworkCallback(@NonNull android.net.ConnectivityManager.NetworkCallback);
-    method public void unregisterNetworkCallback(@NonNull android.app.PendingIntent);
-    field @Deprecated public static final String ACTION_BACKGROUND_DATA_SETTING_CHANGED = "android.net.conn.BACKGROUND_DATA_SETTING_CHANGED";
-    field public static final String ACTION_CAPTIVE_PORTAL_SIGN_IN = "android.net.conn.CAPTIVE_PORTAL";
-    field public static final String ACTION_RESTRICT_BACKGROUND_CHANGED = "android.net.conn.RESTRICT_BACKGROUND_CHANGED";
-    field @Deprecated public static final String CONNECTIVITY_ACTION = "android.net.conn.CONNECTIVITY_CHANGE";
-    field @Deprecated public static final int DEFAULT_NETWORK_PREFERENCE = 1; // 0x1
-    field public static final String EXTRA_CAPTIVE_PORTAL = "android.net.extra.CAPTIVE_PORTAL";
-    field public static final String EXTRA_CAPTIVE_PORTAL_URL = "android.net.extra.CAPTIVE_PORTAL_URL";
-    field @Deprecated public static final String EXTRA_EXTRA_INFO = "extraInfo";
-    field @Deprecated public static final String EXTRA_IS_FAILOVER = "isFailover";
-    field public static final String EXTRA_NETWORK = "android.net.extra.NETWORK";
-    field @Deprecated public static final String EXTRA_NETWORK_INFO = "networkInfo";
-    field public static final String EXTRA_NETWORK_REQUEST = "android.net.extra.NETWORK_REQUEST";
-    field @Deprecated public static final String EXTRA_NETWORK_TYPE = "networkType";
-    field public static final String EXTRA_NO_CONNECTIVITY = "noConnectivity";
-    field @Deprecated public static final String EXTRA_OTHER_NETWORK_INFO = "otherNetwork";
-    field public static final String EXTRA_REASON = "reason";
-    field public static final int MULTIPATH_PREFERENCE_HANDOVER = 1; // 0x1
-    field public static final int MULTIPATH_PREFERENCE_PERFORMANCE = 4; // 0x4
-    field public static final int MULTIPATH_PREFERENCE_RELIABILITY = 2; // 0x2
-    field public static final int RESTRICT_BACKGROUND_STATUS_DISABLED = 1; // 0x1
-    field public static final int RESTRICT_BACKGROUND_STATUS_ENABLED = 3; // 0x3
-    field public static final int RESTRICT_BACKGROUND_STATUS_WHITELISTED = 2; // 0x2
-    field @Deprecated public static final int TYPE_BLUETOOTH = 7; // 0x7
-    field @Deprecated public static final int TYPE_DUMMY = 8; // 0x8
-    field @Deprecated public static final int TYPE_ETHERNET = 9; // 0x9
-    field @Deprecated public static final int TYPE_MOBILE = 0; // 0x0
-    field @Deprecated public static final int TYPE_MOBILE_DUN = 4; // 0x4
-    field @Deprecated public static final int TYPE_MOBILE_HIPRI = 5; // 0x5
-    field @Deprecated public static final int TYPE_MOBILE_MMS = 2; // 0x2
-    field @Deprecated public static final int TYPE_MOBILE_SUPL = 3; // 0x3
-    field @Deprecated public static final int TYPE_VPN = 17; // 0x11
-    field @Deprecated public static final int TYPE_WIFI = 1; // 0x1
-    field @Deprecated public static final int TYPE_WIMAX = 6; // 0x6
-  }
-
-  public static class ConnectivityManager.NetworkCallback {
-    ctor public ConnectivityManager.NetworkCallback();
-    ctor public ConnectivityManager.NetworkCallback(int);
-    method public void onAvailable(@NonNull android.net.Network);
-    method public void onBlockedStatusChanged(@NonNull android.net.Network, boolean);
-    method public void onCapabilitiesChanged(@NonNull android.net.Network, @NonNull android.net.NetworkCapabilities);
-    method public void onLinkPropertiesChanged(@NonNull android.net.Network, @NonNull android.net.LinkProperties);
-    method public void onLosing(@NonNull android.net.Network, int);
-    method public void onLost(@NonNull android.net.Network);
-    method public void onUnavailable();
-    field public static final int FLAG_INCLUDE_LOCATION_INFO = 1; // 0x1
-  }
-
-  public static interface ConnectivityManager.OnNetworkActiveListener {
-    method public void onNetworkActive();
-  }
-
-  public class DhcpInfo implements android.os.Parcelable {
-    ctor public DhcpInfo();
-    method public int describeContents();
-    method public void writeToParcel(android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.net.DhcpInfo> CREATOR;
-    field public int dns1;
-    field public int dns2;
-    field public int gateway;
-    field public int ipAddress;
-    field public int leaseDuration;
-    field public int netmask;
-    field public int serverAddress;
-  }
-
-  public final class DnsResolver {
-    method @NonNull public static android.net.DnsResolver getInstance();
-    method public void query(@Nullable android.net.Network, @NonNull String, int, @NonNull java.util.concurrent.Executor, @Nullable android.os.CancellationSignal, @NonNull android.net.DnsResolver.Callback<? super java.util.List<java.net.InetAddress>>);
-    method public void query(@Nullable android.net.Network, @NonNull String, int, int, @NonNull java.util.concurrent.Executor, @Nullable android.os.CancellationSignal, @NonNull android.net.DnsResolver.Callback<? super java.util.List<java.net.InetAddress>>);
-    method public void rawQuery(@Nullable android.net.Network, @NonNull byte[], int, @NonNull java.util.concurrent.Executor, @Nullable android.os.CancellationSignal, @NonNull android.net.DnsResolver.Callback<? super byte[]>);
-    method public void rawQuery(@Nullable android.net.Network, @NonNull String, int, int, int, @NonNull java.util.concurrent.Executor, @Nullable android.os.CancellationSignal, @NonNull android.net.DnsResolver.Callback<? super byte[]>);
-    field public static final int CLASS_IN = 1; // 0x1
-    field public static final int ERROR_PARSE = 0; // 0x0
-    field public static final int ERROR_SYSTEM = 1; // 0x1
-    field public static final int FLAG_EMPTY = 0; // 0x0
-    field public static final int FLAG_NO_CACHE_LOOKUP = 4; // 0x4
-    field public static final int FLAG_NO_CACHE_STORE = 2; // 0x2
-    field public static final int FLAG_NO_RETRY = 1; // 0x1
-    field public static final int TYPE_A = 1; // 0x1
-    field public static final int TYPE_AAAA = 28; // 0x1c
-  }
-
-  public static interface DnsResolver.Callback<T> {
-    method public void onAnswer(@NonNull T, int);
-    method public void onError(@NonNull android.net.DnsResolver.DnsException);
-  }
-
-  public static class DnsResolver.DnsException extends java.lang.Exception {
-    ctor public DnsResolver.DnsException(int, @Nullable Throwable);
-    field public final int code;
-  }
-
-  public class InetAddresses {
-    method public static boolean isNumericAddress(@NonNull String);
-    method @NonNull public static java.net.InetAddress parseNumericAddress(@NonNull String);
-  }
-
-  public final class IpConfiguration implements android.os.Parcelable {
-    method public int describeContents();
-    method @Nullable public android.net.ProxyInfo getHttpProxy();
-    method @Nullable public android.net.StaticIpConfiguration getStaticIpConfiguration();
-    method public void writeToParcel(@NonNull android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.net.IpConfiguration> CREATOR;
-  }
-
-  public static final class IpConfiguration.Builder {
-    ctor public IpConfiguration.Builder();
-    method @NonNull public android.net.IpConfiguration build();
-    method @NonNull public android.net.IpConfiguration.Builder setHttpProxy(@Nullable android.net.ProxyInfo);
-    method @NonNull public android.net.IpConfiguration.Builder setStaticIpConfiguration(@Nullable android.net.StaticIpConfiguration);
-  }
-
-  public final class IpPrefix implements android.os.Parcelable {
-    ctor public IpPrefix(@NonNull java.net.InetAddress, @IntRange(from=0, to=128) int);
-    method public boolean contains(@NonNull java.net.InetAddress);
-    method public int describeContents();
-    method @NonNull public java.net.InetAddress getAddress();
-    method @IntRange(from=0, to=128) public int getPrefixLength();
-    method @NonNull public byte[] getRawAddress();
-    method public void writeToParcel(android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.net.IpPrefix> CREATOR;
-  }
-
-  public class LinkAddress implements android.os.Parcelable {
-    method public int describeContents();
-    method public java.net.InetAddress getAddress();
-    method public int getFlags();
-    method @IntRange(from=0, to=128) public int getPrefixLength();
-    method public int getScope();
-    method public void writeToParcel(android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.net.LinkAddress> CREATOR;
-  }
-
-  public final class LinkProperties implements android.os.Parcelable {
-    ctor public LinkProperties();
-    method public boolean addRoute(@NonNull android.net.RouteInfo);
-    method public void clear();
-    method public int describeContents();
-    method @Nullable public java.net.Inet4Address getDhcpServerAddress();
-    method @NonNull public java.util.List<java.net.InetAddress> getDnsServers();
-    method @Nullable public String getDomains();
-    method @Nullable public android.net.ProxyInfo getHttpProxy();
-    method @Nullable public String getInterfaceName();
-    method @NonNull public java.util.List<android.net.LinkAddress> getLinkAddresses();
-    method public int getMtu();
-    method @Nullable public android.net.IpPrefix getNat64Prefix();
-    method @Nullable public String getPrivateDnsServerName();
-    method @NonNull public java.util.List<android.net.RouteInfo> getRoutes();
-    method public boolean isPrivateDnsActive();
-    method public boolean isWakeOnLanSupported();
-    method public void setDhcpServerAddress(@Nullable java.net.Inet4Address);
-    method public void setDnsServers(@NonNull java.util.Collection<java.net.InetAddress>);
-    method public void setDomains(@Nullable String);
-    method public void setHttpProxy(@Nullable android.net.ProxyInfo);
-    method public void setInterfaceName(@Nullable String);
-    method public void setLinkAddresses(@NonNull java.util.Collection<android.net.LinkAddress>);
-    method public void setMtu(int);
-    method public void setNat64Prefix(@Nullable android.net.IpPrefix);
-    method public void writeToParcel(android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.net.LinkProperties> CREATOR;
-  }
-
-  public final class MacAddress implements android.os.Parcelable {
-    method public int describeContents();
-    method @NonNull public static android.net.MacAddress fromBytes(@NonNull byte[]);
-    method @NonNull public static android.net.MacAddress fromString(@NonNull String);
-    method public int getAddressType();
-    method @Nullable public java.net.Inet6Address getLinkLocalIpv6FromEui48Mac();
-    method public boolean isLocallyAssigned();
-    method public boolean matches(@NonNull android.net.MacAddress, @NonNull android.net.MacAddress);
-    method @NonNull public byte[] toByteArray();
-    method @NonNull public String toOuiString();
-    method public void writeToParcel(android.os.Parcel, int);
-    field public static final android.net.MacAddress BROADCAST_ADDRESS;
-    field @NonNull public static final android.os.Parcelable.Creator<android.net.MacAddress> CREATOR;
-    field public static final int TYPE_BROADCAST = 3; // 0x3
-    field public static final int TYPE_MULTICAST = 2; // 0x2
-    field public static final int TYPE_UNICAST = 1; // 0x1
-  }
-
-  public class Network implements android.os.Parcelable {
-    method public void bindSocket(java.net.DatagramSocket) throws java.io.IOException;
-    method public void bindSocket(java.net.Socket) throws java.io.IOException;
-    method public void bindSocket(java.io.FileDescriptor) throws java.io.IOException;
-    method public int describeContents();
-    method public static android.net.Network fromNetworkHandle(long);
-    method public java.net.InetAddress[] getAllByName(String) throws java.net.UnknownHostException;
-    method public java.net.InetAddress getByName(String) throws java.net.UnknownHostException;
-    method public long getNetworkHandle();
-    method public javax.net.SocketFactory getSocketFactory();
-    method public java.net.URLConnection openConnection(java.net.URL) throws java.io.IOException;
-    method public java.net.URLConnection openConnection(java.net.URL, java.net.Proxy) throws java.io.IOException;
-    method public void writeToParcel(android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.net.Network> CREATOR;
-  }
-
-  public final class NetworkCapabilities implements android.os.Parcelable {
-    ctor public NetworkCapabilities();
-    ctor public NetworkCapabilities(android.net.NetworkCapabilities);
-    method public int describeContents();
-    method @NonNull public int[] getCapabilities();
-    method @NonNull public int[] getEnterpriseIds();
-    method public int getLinkDownstreamBandwidthKbps();
-    method public int getLinkUpstreamBandwidthKbps();
-    method @Nullable public android.net.NetworkSpecifier getNetworkSpecifier();
-    method public int getOwnerUid();
-    method public int getSignalStrength();
-    method @Nullable public android.net.TransportInfo getTransportInfo();
-    method public boolean hasCapability(int);
-    method public boolean hasEnterpriseId(int);
-    method public boolean hasTransport(int);
-    method public void writeToParcel(android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.net.NetworkCapabilities> CREATOR;
-    field public static final int NET_CAPABILITY_CAPTIVE_PORTAL = 17; // 0x11
-    field public static final int NET_CAPABILITY_CBS = 5; // 0x5
-    field public static final int NET_CAPABILITY_DUN = 2; // 0x2
-    field public static final int NET_CAPABILITY_EIMS = 10; // 0xa
-    field public static final int NET_CAPABILITY_ENTERPRISE = 29; // 0x1d
-    field public static final int NET_CAPABILITY_FOREGROUND = 19; // 0x13
-    field public static final int NET_CAPABILITY_FOTA = 3; // 0x3
-    field public static final int NET_CAPABILITY_HEAD_UNIT = 32; // 0x20
-    field public static final int NET_CAPABILITY_IA = 7; // 0x7
-    field public static final int NET_CAPABILITY_IMS = 4; // 0x4
-    field public static final int NET_CAPABILITY_INTERNET = 12; // 0xc
-    field public static final int NET_CAPABILITY_MCX = 23; // 0x17
-    field public static final int NET_CAPABILITY_MMS = 0; // 0x0
-    field public static final int NET_CAPABILITY_MMTEL = 33; // 0x21
-    field public static final int NET_CAPABILITY_NOT_CONGESTED = 20; // 0x14
-    field public static final int NET_CAPABILITY_NOT_METERED = 11; // 0xb
-    field public static final int NET_CAPABILITY_NOT_RESTRICTED = 13; // 0xd
-    field public static final int NET_CAPABILITY_NOT_ROAMING = 18; // 0x12
-    field public static final int NET_CAPABILITY_NOT_SUSPENDED = 21; // 0x15
-    field public static final int NET_CAPABILITY_NOT_VPN = 15; // 0xf
-    field public static final int NET_CAPABILITY_PRIORITIZE_BANDWIDTH = 35; // 0x23
-    field public static final int NET_CAPABILITY_PRIORITIZE_LATENCY = 34; // 0x22
-    field public static final int NET_CAPABILITY_RCS = 8; // 0x8
-    field public static final int NET_CAPABILITY_SUPL = 1; // 0x1
-    field public static final int NET_CAPABILITY_TEMPORARILY_NOT_METERED = 25; // 0x19
-    field public static final int NET_CAPABILITY_TRUSTED = 14; // 0xe
-    field public static final int NET_CAPABILITY_VALIDATED = 16; // 0x10
-    field public static final int NET_CAPABILITY_WIFI_P2P = 6; // 0x6
-    field public static final int NET_CAPABILITY_XCAP = 9; // 0x9
-    field public static final int NET_ENTERPRISE_ID_1 = 1; // 0x1
-    field public static final int NET_ENTERPRISE_ID_2 = 2; // 0x2
-    field public static final int NET_ENTERPRISE_ID_3 = 3; // 0x3
-    field public static final int NET_ENTERPRISE_ID_4 = 4; // 0x4
-    field public static final int NET_ENTERPRISE_ID_5 = 5; // 0x5
-    field public static final int SIGNAL_STRENGTH_UNSPECIFIED = -2147483648; // 0x80000000
-    field public static final int TRANSPORT_BLUETOOTH = 2; // 0x2
-    field public static final int TRANSPORT_CELLULAR = 0; // 0x0
-    field public static final int TRANSPORT_ETHERNET = 3; // 0x3
-    field public static final int TRANSPORT_LOWPAN = 6; // 0x6
-    field public static final int TRANSPORT_THREAD = 9; // 0x9
-    field public static final int TRANSPORT_USB = 8; // 0x8
-    field public static final int TRANSPORT_VPN = 4; // 0x4
-    field public static final int TRANSPORT_WIFI = 1; // 0x1
-    field public static final int TRANSPORT_WIFI_AWARE = 5; // 0x5
-  }
-
-  @Deprecated public class NetworkInfo implements android.os.Parcelable {
-    ctor @Deprecated public NetworkInfo(int, int, @Nullable String, @Nullable String);
-    method @Deprecated public int describeContents();
-    method @Deprecated @NonNull public android.net.NetworkInfo.DetailedState getDetailedState();
-    method @Deprecated public String getExtraInfo();
-    method @Deprecated public String getReason();
-    method @Deprecated public android.net.NetworkInfo.State getState();
-    method @Deprecated public int getSubtype();
-    method @Deprecated public String getSubtypeName();
-    method @Deprecated public int getType();
-    method @Deprecated public String getTypeName();
-    method @Deprecated public boolean isAvailable();
-    method @Deprecated public boolean isConnected();
-    method @Deprecated public boolean isConnectedOrConnecting();
-    method @Deprecated public boolean isFailover();
-    method @Deprecated public boolean isRoaming();
-    method @Deprecated public void setDetailedState(@NonNull android.net.NetworkInfo.DetailedState, @Nullable String, @Nullable String);
-    method @Deprecated public void writeToParcel(android.os.Parcel, int);
-    field @Deprecated @NonNull public static final android.os.Parcelable.Creator<android.net.NetworkInfo> CREATOR;
-  }
-
-  @Deprecated public enum NetworkInfo.DetailedState {
-    enum_constant @Deprecated public static final android.net.NetworkInfo.DetailedState AUTHENTICATING;
-    enum_constant @Deprecated public static final android.net.NetworkInfo.DetailedState BLOCKED;
-    enum_constant @Deprecated public static final android.net.NetworkInfo.DetailedState CAPTIVE_PORTAL_CHECK;
-    enum_constant @Deprecated public static final android.net.NetworkInfo.DetailedState CONNECTED;
-    enum_constant @Deprecated public static final android.net.NetworkInfo.DetailedState CONNECTING;
-    enum_constant @Deprecated public static final android.net.NetworkInfo.DetailedState DISCONNECTED;
-    enum_constant @Deprecated public static final android.net.NetworkInfo.DetailedState DISCONNECTING;
-    enum_constant @Deprecated public static final android.net.NetworkInfo.DetailedState FAILED;
-    enum_constant @Deprecated public static final android.net.NetworkInfo.DetailedState IDLE;
-    enum_constant @Deprecated public static final android.net.NetworkInfo.DetailedState OBTAINING_IPADDR;
-    enum_constant @Deprecated public static final android.net.NetworkInfo.DetailedState SCANNING;
-    enum_constant @Deprecated public static final android.net.NetworkInfo.DetailedState SUSPENDED;
-    enum_constant @Deprecated public static final android.net.NetworkInfo.DetailedState VERIFYING_POOR_LINK;
-  }
-
-  @Deprecated public enum NetworkInfo.State {
-    enum_constant @Deprecated public static final android.net.NetworkInfo.State CONNECTED;
-    enum_constant @Deprecated public static final android.net.NetworkInfo.State CONNECTING;
-    enum_constant @Deprecated public static final android.net.NetworkInfo.State DISCONNECTED;
-    enum_constant @Deprecated public static final android.net.NetworkInfo.State DISCONNECTING;
-    enum_constant @Deprecated public static final android.net.NetworkInfo.State SUSPENDED;
-    enum_constant @Deprecated public static final android.net.NetworkInfo.State UNKNOWN;
-  }
-
-  public class NetworkRequest implements android.os.Parcelable {
-    method public boolean canBeSatisfiedBy(@Nullable android.net.NetworkCapabilities);
-    method public int describeContents();
-    method @NonNull public int[] getCapabilities();
-    method @Nullable public android.net.NetworkSpecifier getNetworkSpecifier();
-    method @NonNull public int[] getTransportTypes();
-    method public boolean hasCapability(int);
-    method public boolean hasTransport(int);
-    method public void writeToParcel(android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.net.NetworkRequest> CREATOR;
-  }
-
-  public static class NetworkRequest.Builder {
-    ctor public NetworkRequest.Builder();
-    ctor public NetworkRequest.Builder(@NonNull android.net.NetworkRequest);
-    method public android.net.NetworkRequest.Builder addCapability(int);
-    method public android.net.NetworkRequest.Builder addTransportType(int);
-    method public android.net.NetworkRequest build();
-    method @NonNull public android.net.NetworkRequest.Builder clearCapabilities();
-    method public android.net.NetworkRequest.Builder removeCapability(int);
-    method public android.net.NetworkRequest.Builder removeTransportType(int);
-    method @NonNull public android.net.NetworkRequest.Builder setIncludeOtherUidNetworks(boolean);
-    method @Deprecated public android.net.NetworkRequest.Builder setNetworkSpecifier(String);
-    method public android.net.NetworkRequest.Builder setNetworkSpecifier(android.net.NetworkSpecifier);
-  }
-
-  public class ParseException extends java.lang.RuntimeException {
-    ctor public ParseException(@NonNull String);
-    ctor public ParseException(@NonNull String, @NonNull Throwable);
-    field public String response;
-  }
-
-  public class ProxyInfo implements android.os.Parcelable {
-    ctor public ProxyInfo(@Nullable android.net.ProxyInfo);
-    method public static android.net.ProxyInfo buildDirectProxy(String, int);
-    method public static android.net.ProxyInfo buildDirectProxy(String, int, java.util.List<java.lang.String>);
-    method public static android.net.ProxyInfo buildPacProxy(android.net.Uri);
-    method @NonNull public static android.net.ProxyInfo buildPacProxy(@NonNull android.net.Uri, int);
-    method public int describeContents();
-    method public String[] getExclusionList();
-    method public String getHost();
-    method public android.net.Uri getPacFileUrl();
-    method public int getPort();
-    method public boolean isValid();
-    method public void writeToParcel(android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.net.ProxyInfo> CREATOR;
-  }
-
-  public final class RouteInfo implements android.os.Parcelable {
-    method public int describeContents();
-    method @NonNull public android.net.IpPrefix getDestination();
-    method @Nullable public java.net.InetAddress getGateway();
-    method @Nullable public String getInterface();
-    method public int getType();
-    method public boolean hasGateway();
-    method public boolean isDefaultRoute();
-    method public boolean matches(java.net.InetAddress);
-    method public void writeToParcel(android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.net.RouteInfo> CREATOR;
-    field public static final int RTN_THROW = 9; // 0x9
-    field public static final int RTN_UNICAST = 1; // 0x1
-    field public static final int RTN_UNREACHABLE = 7; // 0x7
-  }
-
-  public abstract class SocketKeepalive implements java.lang.AutoCloseable {
-    method public final void close();
-    method public final void start(@IntRange(from=0xa, to=0xe10) int);
-    method public final void stop();
-    field public static final int ERROR_HARDWARE_ERROR = -31; // 0xffffffe1
-    field public static final int ERROR_INSUFFICIENT_RESOURCES = -32; // 0xffffffe0
-    field public static final int ERROR_INVALID_INTERVAL = -24; // 0xffffffe8
-    field public static final int ERROR_INVALID_IP_ADDRESS = -21; // 0xffffffeb
-    field public static final int ERROR_INVALID_LENGTH = -23; // 0xffffffe9
-    field public static final int ERROR_INVALID_NETWORK = -20; // 0xffffffec
-    field public static final int ERROR_INVALID_PORT = -22; // 0xffffffea
-    field public static final int ERROR_INVALID_SOCKET = -25; // 0xffffffe7
-    field public static final int ERROR_SOCKET_NOT_IDLE = -26; // 0xffffffe6
-    field public static final int ERROR_UNSUPPORTED = -30; // 0xffffffe2
-  }
-
-  public static class SocketKeepalive.Callback {
-    ctor public SocketKeepalive.Callback();
-    method public void onDataReceived();
-    method public void onError(int);
-    method public void onStarted();
-    method public void onStopped();
-  }
-
-  public final class StaticIpConfiguration implements android.os.Parcelable {
-    method public int describeContents();
-    method @NonNull public java.util.List<java.net.InetAddress> getDnsServers();
-    method @Nullable public String getDomains();
-    method @Nullable public java.net.InetAddress getGateway();
-    method @NonNull public android.net.LinkAddress getIpAddress();
-    method public void writeToParcel(@NonNull android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.net.StaticIpConfiguration> CREATOR;
-  }
-
-  public static final class StaticIpConfiguration.Builder {
-    ctor public StaticIpConfiguration.Builder();
-    method @NonNull public android.net.StaticIpConfiguration build();
-    method @NonNull public android.net.StaticIpConfiguration.Builder setDnsServers(@NonNull Iterable<java.net.InetAddress>);
-    method @NonNull public android.net.StaticIpConfiguration.Builder setDomains(@Nullable String);
-    method @NonNull public android.net.StaticIpConfiguration.Builder setGateway(@Nullable java.net.InetAddress);
-    method @NonNull public android.net.StaticIpConfiguration.Builder setIpAddress(@NonNull android.net.LinkAddress);
-  }
-
-  public interface TransportInfo {
-  }
-
-}
-
-package android.net.http {
-
-  public abstract class BidirectionalStream {
-    ctor public BidirectionalStream();
-    method public abstract void cancel();
-    method public abstract void flush();
-    method @NonNull public abstract android.net.http.HeaderBlock getHeaders();
-    method @NonNull public abstract String getHttpMethod();
-    method public abstract int getPriority();
-    method public abstract int getTrafficStatsTag();
-    method public abstract int getTrafficStatsUid();
-    method public abstract boolean hasTrafficStatsTag();
-    method public abstract boolean hasTrafficStatsUid();
-    method public abstract boolean isDelayRequestHeadersUntilFirstFlushEnabled();
-    method public abstract boolean isDone();
-    method public abstract void read(@NonNull java.nio.ByteBuffer);
-    method public abstract void start();
-    method public abstract void write(@NonNull java.nio.ByteBuffer, boolean);
-    field public static final int STREAM_PRIORITY_HIGHEST = 4; // 0x4
-    field public static final int STREAM_PRIORITY_IDLE = 0; // 0x0
-    field public static final int STREAM_PRIORITY_LOW = 2; // 0x2
-    field public static final int STREAM_PRIORITY_LOWEST = 1; // 0x1
-    field public static final int STREAM_PRIORITY_MEDIUM = 3; // 0x3
-  }
-
-  public abstract static class BidirectionalStream.Builder {
-    ctor public BidirectionalStream.Builder();
-    method @NonNull public abstract android.net.http.BidirectionalStream.Builder addHeader(@NonNull String, @NonNull String);
-    method @NonNull public abstract android.net.http.BidirectionalStream build();
-    method @NonNull public abstract android.net.http.BidirectionalStream.Builder setDelayRequestHeadersUntilFirstFlushEnabled(boolean);
-    method @NonNull public abstract android.net.http.BidirectionalStream.Builder setHttpMethod(@NonNull String);
-    method @NonNull public abstract android.net.http.BidirectionalStream.Builder setPriority(int);
-    method @NonNull public abstract android.net.http.BidirectionalStream.Builder setTrafficStatsTag(int);
-    method @NonNull public abstract android.net.http.BidirectionalStream.Builder setTrafficStatsUid(int);
-  }
-
-  public static interface BidirectionalStream.Callback {
-    method public void onCanceled(@NonNull android.net.http.BidirectionalStream, @Nullable android.net.http.UrlResponseInfo);
-    method public void onFailed(@NonNull android.net.http.BidirectionalStream, @Nullable android.net.http.UrlResponseInfo, @NonNull android.net.http.HttpException);
-    method public void onReadCompleted(@NonNull android.net.http.BidirectionalStream, @NonNull android.net.http.UrlResponseInfo, @NonNull java.nio.ByteBuffer, boolean);
-    method public void onResponseHeadersReceived(@NonNull android.net.http.BidirectionalStream, @NonNull android.net.http.UrlResponseInfo);
-    method public void onResponseTrailersReceived(@NonNull android.net.http.BidirectionalStream, @NonNull android.net.http.UrlResponseInfo, @NonNull android.net.http.HeaderBlock);
-    method public void onStreamReady(@NonNull android.net.http.BidirectionalStream);
-    method public void onSucceeded(@NonNull android.net.http.BidirectionalStream, @NonNull android.net.http.UrlResponseInfo);
-    method public void onWriteCompleted(@NonNull android.net.http.BidirectionalStream, @NonNull android.net.http.UrlResponseInfo, @NonNull java.nio.ByteBuffer, boolean);
-  }
-
-  public abstract class CallbackException extends android.net.http.HttpException {
-    ctor protected CallbackException(@Nullable String, @Nullable Throwable);
-  }
-
-  public class ConnectionMigrationOptions {
-    method public int getAllowNonDefaultNetworkUsage();
-    method public int getDefaultNetworkMigration();
-    method public int getPathDegradationMigration();
-    field public static final int MIGRATION_OPTION_DISABLED = 2; // 0x2
-    field public static final int MIGRATION_OPTION_ENABLED = 1; // 0x1
-    field public static final int MIGRATION_OPTION_UNSPECIFIED = 0; // 0x0
-  }
-
-  public static final class ConnectionMigrationOptions.Builder {
-    ctor public ConnectionMigrationOptions.Builder();
-    method @NonNull public android.net.http.ConnectionMigrationOptions build();
-    method @NonNull public android.net.http.ConnectionMigrationOptions.Builder setAllowNonDefaultNetworkUsage(int);
-    method @NonNull public android.net.http.ConnectionMigrationOptions.Builder setDefaultNetworkMigration(int);
-    method @NonNull public android.net.http.ConnectionMigrationOptions.Builder setPathDegradationMigration(int);
-  }
-
-  public final class DnsOptions {
-    method public int getPersistHostCache();
-    method @Nullable public java.time.Duration getPersistHostCachePeriod();
-    method public int getPreestablishConnectionsToStaleDnsResults();
-    method public int getStaleDns();
-    method @Nullable public android.net.http.DnsOptions.StaleDnsOptions getStaleDnsOptions();
-    method public int getUseHttpStackDnsResolver();
-    field public static final int DNS_OPTION_DISABLED = 2; // 0x2
-    field public static final int DNS_OPTION_ENABLED = 1; // 0x1
-    field public static final int DNS_OPTION_UNSPECIFIED = 0; // 0x0
-  }
-
-  public static final class DnsOptions.Builder {
-    ctor public DnsOptions.Builder();
-    method @NonNull public android.net.http.DnsOptions build();
-    method @NonNull public android.net.http.DnsOptions.Builder setPersistHostCache(int);
-    method @NonNull public android.net.http.DnsOptions.Builder setPersistHostCachePeriod(@NonNull java.time.Duration);
-    method @NonNull public android.net.http.DnsOptions.Builder setPreestablishConnectionsToStaleDnsResults(int);
-    method @NonNull public android.net.http.DnsOptions.Builder setStaleDns(int);
-    method @NonNull public android.net.http.DnsOptions.Builder setStaleDnsOptions(@NonNull android.net.http.DnsOptions.StaleDnsOptions);
-    method @NonNull public android.net.http.DnsOptions.Builder setUseHttpStackDnsResolver(int);
-  }
-
-  public static class DnsOptions.StaleDnsOptions {
-    method public int getAllowCrossNetworkUsage();
-    method @Nullable public java.time.Duration getFreshLookupTimeout();
-    method @Nullable public java.time.Duration getMaxExpiredDelay();
-    method public int getUseStaleOnNameNotResolved();
-  }
-
-  public static final class DnsOptions.StaleDnsOptions.Builder {
-    ctor public DnsOptions.StaleDnsOptions.Builder();
-    method @NonNull public android.net.http.DnsOptions.StaleDnsOptions build();
-    method @NonNull public android.net.http.DnsOptions.StaleDnsOptions.Builder setAllowCrossNetworkUsage(int);
-    method @NonNull public android.net.http.DnsOptions.StaleDnsOptions.Builder setFreshLookupTimeout(@NonNull java.time.Duration);
-    method @NonNull public android.net.http.DnsOptions.StaleDnsOptions.Builder setMaxExpiredDelay(@NonNull java.time.Duration);
-    method @NonNull public android.net.http.DnsOptions.StaleDnsOptions.Builder setUseStaleOnNameNotResolved(int);
-  }
-
-  public abstract class HeaderBlock {
-    ctor public HeaderBlock();
-    method @NonNull public abstract java.util.List<java.util.Map.Entry<java.lang.String,java.lang.String>> getAsList();
-    method @NonNull public abstract java.util.Map<java.lang.String,java.util.List<java.lang.String>> getAsMap();
-  }
-
-  public abstract class HttpEngine {
-    method public void bindToNetwork(@Nullable android.net.Network);
-    method @NonNull public abstract java.net.URLStreamHandlerFactory createUrlStreamHandlerFactory();
-    method @NonNull public static String getVersionString();
-    method @NonNull public abstract android.net.http.BidirectionalStream.Builder newBidirectionalStreamBuilder(@NonNull String, @NonNull java.util.concurrent.Executor, @NonNull android.net.http.BidirectionalStream.Callback);
-    method @NonNull public abstract android.net.http.UrlRequest.Builder newUrlRequestBuilder(@NonNull String, @NonNull java.util.concurrent.Executor, @NonNull android.net.http.UrlRequest.Callback);
-    method @NonNull public abstract java.net.URLConnection openConnection(@NonNull java.net.URL) throws java.io.IOException;
-    method public abstract void shutdown();
-  }
-
-  public static class HttpEngine.Builder {
-    ctor public HttpEngine.Builder(@NonNull android.content.Context);
-    method @NonNull public android.net.http.HttpEngine.Builder addPublicKeyPins(@NonNull String, @NonNull java.util.Set<byte[]>, boolean, @NonNull java.time.Instant);
-    method @NonNull public android.net.http.HttpEngine.Builder addQuicHint(@NonNull String, int, int);
-    method @NonNull public android.net.http.HttpEngine build();
-    method @NonNull public String getDefaultUserAgent();
-    method @NonNull public android.net.http.HttpEngine.Builder setConnectionMigrationOptions(@NonNull android.net.http.ConnectionMigrationOptions);
-    method @NonNull public android.net.http.HttpEngine.Builder setDnsOptions(@NonNull android.net.http.DnsOptions);
-    method @NonNull public android.net.http.HttpEngine.Builder setEnableBrotli(boolean);
-    method @NonNull public android.net.http.HttpEngine.Builder setEnableHttp2(boolean);
-    method @NonNull public android.net.http.HttpEngine.Builder setEnableHttpCache(int, long);
-    method @NonNull public android.net.http.HttpEngine.Builder setEnablePublicKeyPinningBypassForLocalTrustAnchors(boolean);
-    method @NonNull public android.net.http.HttpEngine.Builder setEnableQuic(boolean);
-    method @NonNull public android.net.http.HttpEngine.Builder setQuicOptions(@NonNull android.net.http.QuicOptions);
-    method @NonNull public android.net.http.HttpEngine.Builder setStoragePath(@NonNull String);
-    method @NonNull public android.net.http.HttpEngine.Builder setUserAgent(@NonNull String);
-    field public static final int HTTP_CACHE_DISABLED = 0; // 0x0
-    field public static final int HTTP_CACHE_DISK = 3; // 0x3
-    field public static final int HTTP_CACHE_DISK_NO_HTTP = 2; // 0x2
-    field public static final int HTTP_CACHE_IN_MEMORY = 1; // 0x1
-  }
-
-  public class HttpException extends java.io.IOException {
-    ctor public HttpException(@Nullable String, @Nullable Throwable);
-  }
-
-  public final class InlineExecutionProhibitedException extends java.util.concurrent.RejectedExecutionException {
-    ctor public InlineExecutionProhibitedException();
-  }
-
-  public abstract class NetworkException extends android.net.http.HttpException {
-    ctor public NetworkException(@Nullable String, @Nullable Throwable);
-    method public abstract int getErrorCode();
-    method public abstract boolean isImmediatelyRetryable();
-    field public static final int ERROR_ADDRESS_UNREACHABLE = 9; // 0x9
-    field public static final int ERROR_CONNECTION_CLOSED = 5; // 0x5
-    field public static final int ERROR_CONNECTION_REFUSED = 7; // 0x7
-    field public static final int ERROR_CONNECTION_RESET = 8; // 0x8
-    field public static final int ERROR_CONNECTION_TIMED_OUT = 6; // 0x6
-    field public static final int ERROR_HOSTNAME_NOT_RESOLVED = 1; // 0x1
-    field public static final int ERROR_INTERNET_DISCONNECTED = 2; // 0x2
-    field public static final int ERROR_NETWORK_CHANGED = 3; // 0x3
-    field public static final int ERROR_OTHER = 11; // 0xb
-    field public static final int ERROR_QUIC_PROTOCOL_FAILED = 10; // 0xa
-    field public static final int ERROR_TIMED_OUT = 4; // 0x4
-  }
-
-  public abstract class QuicException extends android.net.http.NetworkException {
-    ctor protected QuicException(@Nullable String, @Nullable Throwable);
-  }
-
-  public class QuicOptions {
-    method @NonNull public java.util.Set<java.lang.String> getAllowedQuicHosts();
-    method @Nullable public String getHandshakeUserAgent();
-    method @Nullable public java.time.Duration getIdleConnectionTimeout();
-    method public int getInMemoryServerConfigsCacheSize();
-    method public boolean hasInMemoryServerConfigsCacheSize();
-  }
-
-  public static final class QuicOptions.Builder {
-    ctor public QuicOptions.Builder();
-    method @NonNull public android.net.http.QuicOptions.Builder addAllowedQuicHost(@NonNull String);
-    method @NonNull public android.net.http.QuicOptions build();
-    method @NonNull public android.net.http.QuicOptions.Builder setHandshakeUserAgent(@NonNull String);
-    method @NonNull public android.net.http.QuicOptions.Builder setIdleConnectionTimeout(@NonNull java.time.Duration);
-    method @NonNull public android.net.http.QuicOptions.Builder setInMemoryServerConfigsCacheSize(int);
-  }
-
-  public abstract class UploadDataProvider implements java.io.Closeable {
-    ctor public UploadDataProvider();
-    method public void close() throws java.io.IOException;
-    method public abstract long getLength() throws java.io.IOException;
-    method public abstract void read(@NonNull android.net.http.UploadDataSink, @NonNull java.nio.ByteBuffer) throws java.io.IOException;
-    method public abstract void rewind(@NonNull android.net.http.UploadDataSink) throws java.io.IOException;
-  }
-
-  public abstract class UploadDataSink {
-    ctor public UploadDataSink();
-    method public abstract void onReadError(@NonNull Exception);
-    method public abstract void onReadSucceeded(boolean);
-    method public abstract void onRewindError(@NonNull Exception);
-    method public abstract void onRewindSucceeded();
-  }
-
-  public abstract class UrlRequest {
-    method public abstract void cancel();
-    method public abstract void followRedirect();
-    method @NonNull public abstract android.net.http.HeaderBlock getHeaders();
-    method @Nullable public abstract String getHttpMethod();
-    method public abstract int getPriority();
-    method public abstract void getStatus(@NonNull android.net.http.UrlRequest.StatusListener);
-    method public abstract int getTrafficStatsTag();
-    method public abstract int getTrafficStatsUid();
-    method public abstract boolean hasTrafficStatsTag();
-    method public abstract boolean hasTrafficStatsUid();
-    method public abstract boolean isCacheDisabled();
-    method public abstract boolean isDirectExecutorAllowed();
-    method public abstract boolean isDone();
-    method public abstract void read(@NonNull java.nio.ByteBuffer);
-    method public abstract void start();
-    field public static final int REQUEST_PRIORITY_HIGHEST = 4; // 0x4
-    field public static final int REQUEST_PRIORITY_IDLE = 0; // 0x0
-    field public static final int REQUEST_PRIORITY_LOW = 2; // 0x2
-    field public static final int REQUEST_PRIORITY_LOWEST = 1; // 0x1
-    field public static final int REQUEST_PRIORITY_MEDIUM = 3; // 0x3
-  }
-
-  public abstract static class UrlRequest.Builder {
-    method @NonNull public abstract android.net.http.UrlRequest.Builder addHeader(@NonNull String, @NonNull String);
-    method @NonNull public abstract android.net.http.UrlRequest.Builder bindToNetwork(@Nullable android.net.Network);
-    method @NonNull public abstract android.net.http.UrlRequest build();
-    method @NonNull public abstract android.net.http.UrlRequest.Builder setCacheDisabled(boolean);
-    method @NonNull public abstract android.net.http.UrlRequest.Builder setDirectExecutorAllowed(boolean);
-    method @NonNull public abstract android.net.http.UrlRequest.Builder setHttpMethod(@NonNull String);
-    method @NonNull public abstract android.net.http.UrlRequest.Builder setPriority(int);
-    method @NonNull public abstract android.net.http.UrlRequest.Builder setTrafficStatsTag(int);
-    method @NonNull public abstract android.net.http.UrlRequest.Builder setTrafficStatsUid(int);
-    method @NonNull public abstract android.net.http.UrlRequest.Builder setUploadDataProvider(@NonNull android.net.http.UploadDataProvider, @NonNull java.util.concurrent.Executor);
-  }
-
-  public static interface UrlRequest.Callback {
-    method public void onCanceled(@NonNull android.net.http.UrlRequest, @Nullable android.net.http.UrlResponseInfo);
-    method public void onFailed(@NonNull android.net.http.UrlRequest, @Nullable android.net.http.UrlResponseInfo, @NonNull android.net.http.HttpException);
-    method public void onReadCompleted(@NonNull android.net.http.UrlRequest, @NonNull android.net.http.UrlResponseInfo, @NonNull java.nio.ByteBuffer) throws java.lang.Exception;
-    method public void onRedirectReceived(@NonNull android.net.http.UrlRequest, @NonNull android.net.http.UrlResponseInfo, @NonNull String) throws java.lang.Exception;
-    method public void onResponseStarted(@NonNull android.net.http.UrlRequest, @NonNull android.net.http.UrlResponseInfo) throws java.lang.Exception;
-    method public void onSucceeded(@NonNull android.net.http.UrlRequest, @NonNull android.net.http.UrlResponseInfo);
-  }
-
-  public static class UrlRequest.Status {
-    field public static final int CONNECTING = 10; // 0xa
-    field public static final int DOWNLOADING_PAC_FILE = 5; // 0x5
-    field public static final int ESTABLISHING_PROXY_TUNNEL = 8; // 0x8
-    field public static final int IDLE = 0; // 0x0
-    field public static final int INVALID = -1; // 0xffffffff
-    field public static final int READING_RESPONSE = 14; // 0xe
-    field public static final int RESOLVING_HOST = 9; // 0x9
-    field public static final int RESOLVING_HOST_IN_PAC_FILE = 7; // 0x7
-    field public static final int RESOLVING_PROXY_FOR_URL = 6; // 0x6
-    field public static final int SENDING_REQUEST = 12; // 0xc
-    field public static final int SSL_HANDSHAKE = 11; // 0xb
-    field public static final int WAITING_FOR_AVAILABLE_SOCKET = 2; // 0x2
-    field public static final int WAITING_FOR_CACHE = 4; // 0x4
-    field public static final int WAITING_FOR_DELEGATE = 3; // 0x3
-    field public static final int WAITING_FOR_RESPONSE = 13; // 0xd
-    field public static final int WAITING_FOR_STALLED_SOCKET_POOL = 1; // 0x1
-  }
-
-  public static interface UrlRequest.StatusListener {
-    method public void onStatus(int);
-  }
-
-  public abstract class UrlResponseInfo {
-    ctor public UrlResponseInfo();
-    method @NonNull public abstract android.net.http.HeaderBlock getHeaders();
-    method public abstract int getHttpStatusCode();
-    method @NonNull public abstract String getHttpStatusText();
-    method @NonNull public abstract String getNegotiatedProtocol();
-    method public abstract long getReceivedByteCount();
-    method @NonNull public abstract String getUrl();
-    method @NonNull public abstract java.util.List<java.lang.String> getUrlChain();
-    method public abstract boolean wasCached();
-  }
-
-}
-
diff --git a/framework/udc-extended-api/lint-baseline.txt b/framework/udc-extended-api/lint-baseline.txt
deleted file mode 100644
index 2f4004a..0000000
--- a/framework/udc-extended-api/lint-baseline.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-// Baseline format: 1.0
-VisiblySynchronized: android.net.NetworkInfo#toString():
-    Internal locks must not be exposed (synchronizing on this or class is still
-    externally observable): method android.net.NetworkInfo.toString()
diff --git a/framework/udc-extended-api/module-lib-current.txt b/framework/udc-extended-api/module-lib-current.txt
deleted file mode 100644
index 193bd92..0000000
--- a/framework/udc-extended-api/module-lib-current.txt
+++ /dev/null
@@ -1,239 +0,0 @@
-// Signature format: 2.0
-package android.net {
-
-  public final class ConnectivityFrameworkInitializer {
-    method public static void registerServiceWrappers();
-  }
-
-  public class ConnectivityManager {
-    method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_STACK, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void addUidToMeteredNetworkAllowList(int);
-    method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_STACK, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void addUidToMeteredNetworkDenyList(int);
-    method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void factoryReset();
-    method @NonNull @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_STACK, android.Manifest.permission.NETWORK_SETTINGS}) public java.util.List<android.net.NetworkStateSnapshot> getAllNetworkStateSnapshots();
-    method @Nullable public android.net.ProxyInfo getGlobalProxy();
-    method @NonNull public static android.util.Range<java.lang.Integer> getIpSecNetIdRange();
-    method @Nullable @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_STACK, android.Manifest.permission.NETWORK_SETTINGS}) public android.net.LinkProperties getRedactedLinkPropertiesForPackage(@NonNull android.net.LinkProperties, int, @NonNull String);
-    method @Nullable @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_STACK, android.Manifest.permission.NETWORK_SETTINGS}) public android.net.NetworkCapabilities getRedactedNetworkCapabilitiesForPackage(@NonNull android.net.NetworkCapabilities, int, @NonNull String);
-    method @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_SETTINGS}) public void registerDefaultNetworkCallbackForUid(int, @NonNull android.net.ConnectivityManager.NetworkCallback, @NonNull android.os.Handler);
-    method @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_SETUP_WIZARD, android.Manifest.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS}) public void registerSystemDefaultNetworkCallback(@NonNull android.net.ConnectivityManager.NetworkCallback, @NonNull android.os.Handler);
-    method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_STACK, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void removeUidFromMeteredNetworkAllowList(int);
-    method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_STACK, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void removeUidFromMeteredNetworkDenyList(int);
-    method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_STACK, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void replaceFirewallChain(int, @NonNull int[]);
-    method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_STACK, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void requestBackgroundNetwork(@NonNull android.net.NetworkRequest, @NonNull android.net.ConnectivityManager.NetworkCallback, @NonNull android.os.Handler);
-    method @Deprecated public boolean requestRouteToHostAddress(int, java.net.InetAddress);
-    method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_SETUP_WIZARD, android.Manifest.permission.NETWORK_STACK, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void setAcceptPartialConnectivity(@NonNull android.net.Network, boolean, boolean);
-    method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_SETUP_WIZARD, android.Manifest.permission.NETWORK_STACK, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void setAcceptUnvalidated(@NonNull android.net.Network, boolean, boolean);
-    method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_SETUP_WIZARD, android.Manifest.permission.NETWORK_STACK, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void setAvoidUnvalidated(@NonNull android.net.Network);
-    method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_STACK, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void setFirewallChainEnabled(int, boolean);
-    method @RequiresPermission(android.Manifest.permission.NETWORK_STACK) public void setGlobalProxy(@Nullable android.net.ProxyInfo);
-    method @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_STACK, android.Manifest.permission.NETWORK_SETTINGS}) public void setLegacyLockdownVpnEnabled(boolean);
-    method @Deprecated @RequiresPermission(android.Manifest.permission.NETWORK_STACK) public void setProfileNetworkPreference(@NonNull android.os.UserHandle, int, @Nullable java.util.concurrent.Executor, @Nullable Runnable);
-    method @RequiresPermission(android.Manifest.permission.NETWORK_STACK) public void setProfileNetworkPreferences(@NonNull android.os.UserHandle, @NonNull java.util.List<android.net.ProfileNetworkPreference>, @Nullable java.util.concurrent.Executor, @Nullable Runnable);
-    method @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_STACK, android.Manifest.permission.NETWORK_SETTINGS}) public void setRequireVpnForUids(boolean, @NonNull java.util.Collection<android.util.Range<java.lang.Integer>>);
-    method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_STACK, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void setUidFirewallRule(int, int, int);
-    method @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_STACK, android.Manifest.permission.NETWORK_SETTINGS}) public void setVpnDefaultForUids(@NonNull String, @NonNull java.util.Collection<android.util.Range<java.lang.Integer>>);
-    method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_TEST_NETWORKS, android.Manifest.permission.NETWORK_STACK}) public void simulateDataStall(int, long, @NonNull android.net.Network, @NonNull android.os.PersistableBundle);
-    method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_STACK, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void startCaptivePortalApp(@NonNull android.net.Network);
-    method public void systemReady();
-    field public static final String ACTION_CLEAR_DNS_CACHE = "android.net.action.CLEAR_DNS_CACHE";
-    field public static final String ACTION_PROMPT_LOST_VALIDATION = "android.net.action.PROMPT_LOST_VALIDATION";
-    field public static final String ACTION_PROMPT_PARTIAL_CONNECTIVITY = "android.net.action.PROMPT_PARTIAL_CONNECTIVITY";
-    field public static final String ACTION_PROMPT_UNVALIDATED = "android.net.action.PROMPT_UNVALIDATED";
-    field public static final int BLOCKED_METERED_REASON_ADMIN_DISABLED = 262144; // 0x40000
-    field public static final int BLOCKED_METERED_REASON_DATA_SAVER = 65536; // 0x10000
-    field public static final int BLOCKED_METERED_REASON_MASK = -65536; // 0xffff0000
-    field public static final int BLOCKED_METERED_REASON_USER_RESTRICTED = 131072; // 0x20000
-    field public static final int BLOCKED_REASON_APP_STANDBY = 4; // 0x4
-    field public static final int BLOCKED_REASON_BATTERY_SAVER = 1; // 0x1
-    field public static final int BLOCKED_REASON_DOZE = 2; // 0x2
-    field public static final int BLOCKED_REASON_LOCKDOWN_VPN = 16; // 0x10
-    field public static final int BLOCKED_REASON_LOW_POWER_STANDBY = 32; // 0x20
-    field public static final int BLOCKED_REASON_NONE = 0; // 0x0
-    field public static final int BLOCKED_REASON_RESTRICTED_MODE = 8; // 0x8
-    field public static final int FIREWALL_CHAIN_DOZABLE = 1; // 0x1
-    field public static final int FIREWALL_CHAIN_LOW_POWER_STANDBY = 5; // 0x5
-    field public static final int FIREWALL_CHAIN_OEM_DENY_1 = 7; // 0x7
-    field public static final int FIREWALL_CHAIN_OEM_DENY_2 = 8; // 0x8
-    field public static final int FIREWALL_CHAIN_OEM_DENY_3 = 9; // 0x9
-    field public static final int FIREWALL_CHAIN_POWERSAVE = 3; // 0x3
-    field public static final int FIREWALL_CHAIN_RESTRICTED = 4; // 0x4
-    field public static final int FIREWALL_CHAIN_STANDBY = 2; // 0x2
-    field public static final int FIREWALL_RULE_ALLOW = 1; // 0x1
-    field public static final int FIREWALL_RULE_DEFAULT = 0; // 0x0
-    field public static final int FIREWALL_RULE_DENY = 2; // 0x2
-    field public static final int PROFILE_NETWORK_PREFERENCE_DEFAULT = 0; // 0x0
-    field public static final int PROFILE_NETWORK_PREFERENCE_ENTERPRISE = 1; // 0x1
-    field public static final int PROFILE_NETWORK_PREFERENCE_ENTERPRISE_BLOCKING = 3; // 0x3
-    field public static final int PROFILE_NETWORK_PREFERENCE_ENTERPRISE_NO_FALLBACK = 2; // 0x2
-  }
-
-  public static class ConnectivityManager.NetworkCallback {
-    method public void onBlockedStatusChanged(@NonNull android.net.Network, int);
-  }
-
-  public class ConnectivitySettingsManager {
-    method public static void clearGlobalProxy(@NonNull android.content.Context);
-    method @Nullable public static String getCaptivePortalHttpUrl(@NonNull android.content.Context);
-    method public static int getCaptivePortalMode(@NonNull android.content.Context, int);
-    method @NonNull public static java.time.Duration getConnectivityKeepPendingIntentDuration(@NonNull android.content.Context, @NonNull java.time.Duration);
-    method @NonNull public static android.util.Range<java.lang.Integer> getDnsResolverSampleRanges(@NonNull android.content.Context);
-    method @NonNull public static java.time.Duration getDnsResolverSampleValidityDuration(@NonNull android.content.Context, @NonNull java.time.Duration);
-    method public static int getDnsResolverSuccessThresholdPercent(@NonNull android.content.Context, int);
-    method @Nullable public static android.net.ProxyInfo getGlobalProxy(@NonNull android.content.Context);
-    method public static long getIngressRateLimitInBytesPerSecond(@NonNull android.content.Context);
-    method @NonNull public static java.time.Duration getMobileDataActivityTimeout(@NonNull android.content.Context, @NonNull java.time.Duration);
-    method public static boolean getMobileDataAlwaysOn(@NonNull android.content.Context, boolean);
-    method @NonNull public static java.util.Set<java.lang.Integer> getMobileDataPreferredUids(@NonNull android.content.Context);
-    method public static int getNetworkAvoidBadWifi(@NonNull android.content.Context);
-    method @Nullable public static String getNetworkMeteredMultipathPreference(@NonNull android.content.Context);
-    method public static int getNetworkSwitchNotificationMaximumDailyCount(@NonNull android.content.Context, int);
-    method @NonNull public static java.time.Duration getNetworkSwitchNotificationRateDuration(@NonNull android.content.Context, @NonNull java.time.Duration);
-    method @NonNull public static String getPrivateDnsDefaultMode(@NonNull android.content.Context);
-    method @Nullable public static String getPrivateDnsHostname(@NonNull android.content.Context);
-    method public static int getPrivateDnsMode(@NonNull android.content.Context);
-    method @NonNull public static java.util.Set<java.lang.Integer> getUidsAllowedOnRestrictedNetworks(@NonNull android.content.Context);
-    method public static boolean getWifiAlwaysRequested(@NonNull android.content.Context, boolean);
-    method @NonNull public static java.time.Duration getWifiDataActivityTimeout(@NonNull android.content.Context, @NonNull java.time.Duration);
-    method public static void setCaptivePortalHttpUrl(@NonNull android.content.Context, @Nullable String);
-    method public static void setCaptivePortalMode(@NonNull android.content.Context, int);
-    method public static void setConnectivityKeepPendingIntentDuration(@NonNull android.content.Context, @NonNull java.time.Duration);
-    method public static void setDnsResolverSampleRanges(@NonNull android.content.Context, @NonNull android.util.Range<java.lang.Integer>);
-    method public static void setDnsResolverSampleValidityDuration(@NonNull android.content.Context, @NonNull java.time.Duration);
-    method public static void setDnsResolverSuccessThresholdPercent(@NonNull android.content.Context, @IntRange(from=0, to=100) int);
-    method public static void setGlobalProxy(@NonNull android.content.Context, @NonNull android.net.ProxyInfo);
-    method public static void setIngressRateLimitInBytesPerSecond(@NonNull android.content.Context, @IntRange(from=-1L, to=4294967295L) long);
-    method public static void setMobileDataActivityTimeout(@NonNull android.content.Context, @NonNull java.time.Duration);
-    method public static void setMobileDataAlwaysOn(@NonNull android.content.Context, boolean);
-    method public static void setMobileDataPreferredUids(@NonNull android.content.Context, @NonNull java.util.Set<java.lang.Integer>);
-    method public static void setNetworkAvoidBadWifi(@NonNull android.content.Context, int);
-    method public static void setNetworkMeteredMultipathPreference(@NonNull android.content.Context, @NonNull String);
-    method public static void setNetworkSwitchNotificationMaximumDailyCount(@NonNull android.content.Context, @IntRange(from=0) int);
-    method public static void setNetworkSwitchNotificationRateDuration(@NonNull android.content.Context, @NonNull java.time.Duration);
-    method public static void setPrivateDnsDefaultMode(@NonNull android.content.Context, @NonNull int);
-    method public static void setPrivateDnsHostname(@NonNull android.content.Context, @Nullable String);
-    method public static void setPrivateDnsMode(@NonNull android.content.Context, int);
-    method public static void setUidsAllowedOnRestrictedNetworks(@NonNull android.content.Context, @NonNull java.util.Set<java.lang.Integer>);
-    method public static void setWifiAlwaysRequested(@NonNull android.content.Context, boolean);
-    method public static void setWifiDataActivityTimeout(@NonNull android.content.Context, @NonNull java.time.Duration);
-    field public static final int CAPTIVE_PORTAL_MODE_AVOID = 2; // 0x2
-    field public static final int CAPTIVE_PORTAL_MODE_IGNORE = 0; // 0x0
-    field public static final int CAPTIVE_PORTAL_MODE_PROMPT = 1; // 0x1
-    field public static final int NETWORK_AVOID_BAD_WIFI_AVOID = 2; // 0x2
-    field public static final int NETWORK_AVOID_BAD_WIFI_IGNORE = 0; // 0x0
-    field public static final int NETWORK_AVOID_BAD_WIFI_PROMPT = 1; // 0x1
-    field public static final int PRIVATE_DNS_MODE_OFF = 1; // 0x1
-    field public static final int PRIVATE_DNS_MODE_OPPORTUNISTIC = 2; // 0x2
-    field public static final int PRIVATE_DNS_MODE_PROVIDER_HOSTNAME = 3; // 0x3
-  }
-
-  public final class DhcpOption implements android.os.Parcelable {
-    ctor public DhcpOption(byte, @Nullable byte[]);
-    method public int describeContents();
-    method public byte getType();
-    method @Nullable public byte[] getValue();
-    method public void writeToParcel(@NonNull android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.net.DhcpOption> CREATOR;
-  }
-
-  public final class NetworkAgentConfig implements android.os.Parcelable {
-    method @Nullable public String getSubscriberId();
-    method public boolean isBypassableVpn();
-    method public boolean isVpnValidationRequired();
-  }
-
-  public static final class NetworkAgentConfig.Builder {
-    method @NonNull public android.net.NetworkAgentConfig.Builder setBypassableVpn(boolean);
-    method @NonNull public android.net.NetworkAgentConfig.Builder setLocalRoutesExcludedForVpn(boolean);
-    method @NonNull public android.net.NetworkAgentConfig.Builder setSubscriberId(@Nullable String);
-    method @NonNull public android.net.NetworkAgentConfig.Builder setVpnRequiresValidation(boolean);
-  }
-
-  public final class NetworkCapabilities implements android.os.Parcelable {
-    method @NonNull @RequiresPermission(android.Manifest.permission.NETWORK_FACTORY) public java.util.Set<java.lang.Integer> getAllowedUids();
-    method @Nullable public java.util.Set<android.util.Range<java.lang.Integer>> getUids();
-    method public boolean hasForbiddenCapability(int);
-    field public static final long REDACT_ALL = -1L; // 0xffffffffffffffffL
-    field public static final long REDACT_FOR_ACCESS_FINE_LOCATION = 1L; // 0x1L
-    field public static final long REDACT_FOR_LOCAL_MAC_ADDRESS = 2L; // 0x2L
-    field public static final long REDACT_FOR_NETWORK_SETTINGS = 4L; // 0x4L
-    field public static final long REDACT_NONE = 0L; // 0x0L
-    field public static final int TRANSPORT_TEST = 7; // 0x7
-  }
-
-  public static final class NetworkCapabilities.Builder {
-    method @NonNull @RequiresPermission(android.Manifest.permission.NETWORK_FACTORY) public android.net.NetworkCapabilities.Builder setAllowedUids(@NonNull java.util.Set<java.lang.Integer>);
-    method @NonNull public android.net.NetworkCapabilities.Builder setUids(@Nullable java.util.Set<android.util.Range<java.lang.Integer>>);
-  }
-
-  public class NetworkRequest implements android.os.Parcelable {
-    method @NonNull public int[] getEnterpriseIds();
-    method @NonNull public int[] getForbiddenCapabilities();
-    method public boolean hasEnterpriseId(int);
-    method public boolean hasForbiddenCapability(int);
-  }
-
-  public static class NetworkRequest.Builder {
-    method @NonNull public android.net.NetworkRequest.Builder addForbiddenCapability(int);
-    method @NonNull public android.net.NetworkRequest.Builder removeForbiddenCapability(int);
-    method @NonNull public android.net.NetworkRequest.Builder setUids(@Nullable java.util.Set<android.util.Range<java.lang.Integer>>);
-  }
-
-  public final class ProfileNetworkPreference implements android.os.Parcelable {
-    method public int describeContents();
-    method @NonNull public int[] getExcludedUids();
-    method @NonNull public int[] getIncludedUids();
-    method public int getPreference();
-    method public int getPreferenceEnterpriseId();
-    method public void writeToParcel(@NonNull android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.net.ProfileNetworkPreference> CREATOR;
-  }
-
-  public static final class ProfileNetworkPreference.Builder {
-    ctor public ProfileNetworkPreference.Builder();
-    method @NonNull public android.net.ProfileNetworkPreference build();
-    method @NonNull public android.net.ProfileNetworkPreference.Builder setExcludedUids(@NonNull int[]);
-    method @NonNull public android.net.ProfileNetworkPreference.Builder setIncludedUids(@NonNull int[]);
-    method @NonNull public android.net.ProfileNetworkPreference.Builder setPreference(int);
-    method @NonNull public android.net.ProfileNetworkPreference.Builder setPreferenceEnterpriseId(int);
-  }
-
-  public final class TestNetworkInterface implements android.os.Parcelable {
-    ctor public TestNetworkInterface(@NonNull android.os.ParcelFileDescriptor, @NonNull String);
-    method public int describeContents();
-    method @NonNull public android.os.ParcelFileDescriptor getFileDescriptor();
-    method @NonNull public String getInterfaceName();
-    method @Nullable public android.net.MacAddress getMacAddress();
-    method public int getMtu();
-    method public void writeToParcel(@NonNull android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.net.TestNetworkInterface> CREATOR;
-  }
-
-  public class TestNetworkManager {
-    method @NonNull @RequiresPermission(android.Manifest.permission.MANAGE_TEST_NETWORKS) public android.net.TestNetworkInterface createTapInterface();
-    method @NonNull @RequiresPermission(android.Manifest.permission.MANAGE_TEST_NETWORKS) public android.net.TestNetworkInterface createTunInterface(@NonNull java.util.Collection<android.net.LinkAddress>);
-    method @RequiresPermission(android.Manifest.permission.MANAGE_TEST_NETWORKS) public void setupTestNetwork(@NonNull String, @NonNull android.os.IBinder);
-    method @RequiresPermission(android.Manifest.permission.MANAGE_TEST_NETWORKS) public void teardownTestNetwork(@NonNull android.net.Network);
-    field public static final String TEST_TAP_PREFIX = "testtap";
-  }
-
-  public final class TestNetworkSpecifier extends android.net.NetworkSpecifier implements android.os.Parcelable {
-    ctor public TestNetworkSpecifier(@NonNull String);
-    method public int describeContents();
-    method @Nullable public String getInterfaceName();
-    method public void writeToParcel(@NonNull android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.net.TestNetworkSpecifier> CREATOR;
-  }
-
-  public interface TransportInfo {
-    method public default long getApplicableRedactions();
-    method @NonNull public default android.net.TransportInfo makeCopy(long);
-  }
-
-  public final class VpnTransportInfo implements android.os.Parcelable android.net.TransportInfo {
-    ctor @Deprecated public VpnTransportInfo(int, @Nullable String);
-    method @Nullable public String getSessionId();
-    method @NonNull public android.net.VpnTransportInfo makeCopy(long);
-  }
-
-}
-
diff --git a/framework/udc-extended-api/module-lib-removed.txt b/framework/udc-extended-api/module-lib-removed.txt
deleted file mode 100644
index d802177..0000000
--- a/framework/udc-extended-api/module-lib-removed.txt
+++ /dev/null
@@ -1 +0,0 @@
-// Signature format: 2.0
diff --git a/framework/udc-extended-api/removed.txt b/framework/udc-extended-api/removed.txt
deleted file mode 100644
index 303a1e6..0000000
--- a/framework/udc-extended-api/removed.txt
+++ /dev/null
@@ -1,11 +0,0 @@
-// Signature format: 2.0
-package android.net {
-
-  public class ConnectivityManager {
-    method @Deprecated public boolean requestRouteToHost(int, int);
-    method @Deprecated public int startUsingNetworkFeature(int, String);
-    method @Deprecated public int stopUsingNetworkFeature(int, String);
-  }
-
-}
-
diff --git a/framework/udc-extended-api/system-current.txt b/framework/udc-extended-api/system-current.txt
deleted file mode 100644
index e812024..0000000
--- a/framework/udc-extended-api/system-current.txt
+++ /dev/null
@@ -1,546 +0,0 @@
-// Signature format: 2.0
-package android.net {
-
-  public class CaptivePortal implements android.os.Parcelable {
-    method @Deprecated public void logEvent(int, @NonNull String);
-    method @RequiresPermission(android.Manifest.permission.NETWORK_STACK) public void reevaluateNetwork();
-    method public void useNetwork();
-    field public static final int APP_REQUEST_REEVALUATION_REQUIRED = 100; // 0x64
-    field public static final int APP_RETURN_DISMISSED = 0; // 0x0
-    field public static final int APP_RETURN_UNWANTED = 1; // 0x1
-    field public static final int APP_RETURN_WANTED_AS_IS = 2; // 0x2
-  }
-
-  public final class CaptivePortalData implements android.os.Parcelable {
-    method public int describeContents();
-    method public long getByteLimit();
-    method public long getExpiryTimeMillis();
-    method public long getRefreshTimeMillis();
-    method @Nullable public android.net.Uri getUserPortalUrl();
-    method public int getUserPortalUrlSource();
-    method @Nullable public CharSequence getVenueFriendlyName();
-    method @Nullable public android.net.Uri getVenueInfoUrl();
-    method public int getVenueInfoUrlSource();
-    method public boolean isCaptive();
-    method public boolean isSessionExtendable();
-    method public void writeToParcel(@NonNull android.os.Parcel, int);
-    field public static final int CAPTIVE_PORTAL_DATA_SOURCE_OTHER = 0; // 0x0
-    field public static final int CAPTIVE_PORTAL_DATA_SOURCE_PASSPOINT = 1; // 0x1
-    field @NonNull public static final android.os.Parcelable.Creator<android.net.CaptivePortalData> CREATOR;
-  }
-
-  public static class CaptivePortalData.Builder {
-    ctor public CaptivePortalData.Builder();
-    ctor public CaptivePortalData.Builder(@Nullable android.net.CaptivePortalData);
-    method @NonNull public android.net.CaptivePortalData build();
-    method @NonNull public android.net.CaptivePortalData.Builder setBytesRemaining(long);
-    method @NonNull public android.net.CaptivePortalData.Builder setCaptive(boolean);
-    method @NonNull public android.net.CaptivePortalData.Builder setExpiryTime(long);
-    method @NonNull public android.net.CaptivePortalData.Builder setRefreshTime(long);
-    method @NonNull public android.net.CaptivePortalData.Builder setSessionExtendable(boolean);
-    method @NonNull public android.net.CaptivePortalData.Builder setUserPortalUrl(@Nullable android.net.Uri);
-    method @NonNull public android.net.CaptivePortalData.Builder setUserPortalUrl(@Nullable android.net.Uri, int);
-    method @NonNull public android.net.CaptivePortalData.Builder setVenueFriendlyName(@Nullable CharSequence);
-    method @NonNull public android.net.CaptivePortalData.Builder setVenueInfoUrl(@Nullable android.net.Uri);
-    method @NonNull public android.net.CaptivePortalData.Builder setVenueInfoUrl(@Nullable android.net.Uri, int);
-  }
-
-  public class ConnectivityManager {
-    method @NonNull @RequiresPermission(android.Manifest.permission.PACKET_KEEPALIVE_OFFLOAD) public android.net.SocketKeepalive createNattKeepalive(@NonNull android.net.Network, @NonNull android.os.ParcelFileDescriptor, @NonNull java.net.InetAddress, @NonNull java.net.InetAddress, @NonNull java.util.concurrent.Executor, @NonNull android.net.SocketKeepalive.Callback);
-    method @NonNull @RequiresPermission(android.Manifest.permission.PACKET_KEEPALIVE_OFFLOAD) public android.net.SocketKeepalive createSocketKeepalive(@NonNull android.net.Network, @NonNull java.net.Socket, @NonNull java.util.concurrent.Executor, @NonNull android.net.SocketKeepalive.Callback);
-    method @Deprecated @RequiresPermission(android.Manifest.permission.NETWORK_SETTINGS) public String getCaptivePortalServerUrl();
-    method @Deprecated @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED) public void getLatestTetheringEntitlementResult(int, boolean, @NonNull java.util.concurrent.Executor, @NonNull android.net.ConnectivityManager.OnTetheringEntitlementResultListener);
-    method @Deprecated @RequiresPermission(anyOf={android.Manifest.permission.TETHER_PRIVILEGED, android.Manifest.permission.WRITE_SETTINGS}) public boolean isTetheringSupported();
-    method @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_FACTORY}) public int registerNetworkProvider(@NonNull android.net.NetworkProvider);
-    method public void registerQosCallback(@NonNull android.net.QosSocketInfo, @NonNull java.util.concurrent.Executor, @NonNull android.net.QosCallback);
-    method @Deprecated @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED) public void registerTetheringEventCallback(@NonNull java.util.concurrent.Executor, @NonNull android.net.ConnectivityManager.OnTetheringEventCallback);
-    method @RequiresPermission(android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK) public void requestNetwork(@NonNull android.net.NetworkRequest, int, int, @NonNull android.os.Handler, @NonNull android.net.ConnectivityManager.NetworkCallback);
-    method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_AIRPLANE_MODE, android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_SETUP_WIZARD, android.Manifest.permission.NETWORK_STACK}) public void setAirplaneMode(boolean);
-    method @RequiresPermission(android.Manifest.permission.CONTROL_OEM_PAID_NETWORK_PREFERENCE) public void setOemNetworkPreference(@NonNull android.net.OemNetworkPreferences, @Nullable java.util.concurrent.Executor, @Nullable Runnable);
-    method @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_STACK}) public boolean shouldAvoidBadWifi();
-    method @RequiresPermission(android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK) public void startCaptivePortalApp(@NonNull android.net.Network, @NonNull android.os.Bundle);
-    method @Deprecated @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED) public void startTethering(int, boolean, android.net.ConnectivityManager.OnStartTetheringCallback);
-    method @Deprecated @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED) public void startTethering(int, boolean, android.net.ConnectivityManager.OnStartTetheringCallback, android.os.Handler);
-    method @Deprecated @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED) public void stopTethering(int);
-    method @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_FACTORY}) public void unregisterNetworkProvider(@NonNull android.net.NetworkProvider);
-    method public void unregisterQosCallback(@NonNull android.net.QosCallback);
-    method @Deprecated @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED) public void unregisterTetheringEventCallback(@NonNull android.net.ConnectivityManager.OnTetheringEventCallback);
-    field public static final String EXTRA_CAPTIVE_PORTAL_PROBE_SPEC = "android.net.extra.CAPTIVE_PORTAL_PROBE_SPEC";
-    field public static final String EXTRA_CAPTIVE_PORTAL_USER_AGENT = "android.net.extra.CAPTIVE_PORTAL_USER_AGENT";
-    field public static final int TETHERING_BLUETOOTH = 2; // 0x2
-    field public static final int TETHERING_USB = 1; // 0x1
-    field public static final int TETHERING_WIFI = 0; // 0x0
-    field @Deprecated public static final int TETHER_ERROR_ENTITLEMENT_UNKONWN = 13; // 0xd
-    field @Deprecated public static final int TETHER_ERROR_NO_ERROR = 0; // 0x0
-    field @Deprecated public static final int TETHER_ERROR_PROVISION_FAILED = 11; // 0xb
-    field public static final int TYPE_NONE = -1; // 0xffffffff
-    field @Deprecated public static final int TYPE_PROXY = 16; // 0x10
-    field @Deprecated public static final int TYPE_WIFI_P2P = 13; // 0xd
-  }
-
-  @Deprecated public abstract static class ConnectivityManager.OnStartTetheringCallback {
-    ctor @Deprecated public ConnectivityManager.OnStartTetheringCallback();
-    method @Deprecated public void onTetheringFailed();
-    method @Deprecated public void onTetheringStarted();
-  }
-
-  @Deprecated public static interface ConnectivityManager.OnTetheringEntitlementResultListener {
-    method @Deprecated public void onTetheringEntitlementResult(int);
-  }
-
-  @Deprecated public abstract static class ConnectivityManager.OnTetheringEventCallback {
-    ctor @Deprecated public ConnectivityManager.OnTetheringEventCallback();
-    method @Deprecated public void onUpstreamChanged(@Nullable android.net.Network);
-  }
-
-  public final class DscpPolicy implements android.os.Parcelable {
-    method public int describeContents();
-    method @Nullable public java.net.InetAddress getDestinationAddress();
-    method @Nullable public android.util.Range<java.lang.Integer> getDestinationPortRange();
-    method public int getDscpValue();
-    method public int getPolicyId();
-    method public int getProtocol();
-    method @Nullable public java.net.InetAddress getSourceAddress();
-    method public int getSourcePort();
-    method public void writeToParcel(@NonNull android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.net.DscpPolicy> CREATOR;
-    field public static final int PROTOCOL_ANY = -1; // 0xffffffff
-    field public static final int SOURCE_PORT_ANY = -1; // 0xffffffff
-  }
-
-  public static final class DscpPolicy.Builder {
-    ctor public DscpPolicy.Builder(int, int);
-    method @NonNull public android.net.DscpPolicy build();
-    method @NonNull public android.net.DscpPolicy.Builder setDestinationAddress(@NonNull java.net.InetAddress);
-    method @NonNull public android.net.DscpPolicy.Builder setDestinationPortRange(@NonNull android.util.Range<java.lang.Integer>);
-    method @NonNull public android.net.DscpPolicy.Builder setProtocol(int);
-    method @NonNull public android.net.DscpPolicy.Builder setSourceAddress(@NonNull java.net.InetAddress);
-    method @NonNull public android.net.DscpPolicy.Builder setSourcePort(int);
-  }
-
-  public final class InvalidPacketException extends java.lang.Exception {
-    ctor public InvalidPacketException(int);
-    method public int getError();
-    field public static final int ERROR_INVALID_IP_ADDRESS = -21; // 0xffffffeb
-    field public static final int ERROR_INVALID_LENGTH = -23; // 0xffffffe9
-    field public static final int ERROR_INVALID_PORT = -22; // 0xffffffea
-  }
-
-  public final class IpConfiguration implements android.os.Parcelable {
-    ctor public IpConfiguration();
-    ctor public IpConfiguration(@NonNull android.net.IpConfiguration);
-    method @NonNull public android.net.IpConfiguration.IpAssignment getIpAssignment();
-    method @NonNull public android.net.IpConfiguration.ProxySettings getProxySettings();
-    method public void setHttpProxy(@Nullable android.net.ProxyInfo);
-    method public void setIpAssignment(@NonNull android.net.IpConfiguration.IpAssignment);
-    method public void setProxySettings(@NonNull android.net.IpConfiguration.ProxySettings);
-    method public void setStaticIpConfiguration(@Nullable android.net.StaticIpConfiguration);
-  }
-
-  public enum IpConfiguration.IpAssignment {
-    enum_constant public static final android.net.IpConfiguration.IpAssignment DHCP;
-    enum_constant public static final android.net.IpConfiguration.IpAssignment STATIC;
-    enum_constant public static final android.net.IpConfiguration.IpAssignment UNASSIGNED;
-  }
-
-  public enum IpConfiguration.ProxySettings {
-    enum_constant public static final android.net.IpConfiguration.ProxySettings NONE;
-    enum_constant public static final android.net.IpConfiguration.ProxySettings PAC;
-    enum_constant public static final android.net.IpConfiguration.ProxySettings STATIC;
-    enum_constant public static final android.net.IpConfiguration.ProxySettings UNASSIGNED;
-  }
-
-  public final class IpPrefix implements android.os.Parcelable {
-    ctor public IpPrefix(@NonNull String);
-  }
-
-  public class KeepalivePacketData {
-    ctor protected KeepalivePacketData(@NonNull java.net.InetAddress, @IntRange(from=0, to=65535) int, @NonNull java.net.InetAddress, @IntRange(from=0, to=65535) int, @NonNull byte[]) throws android.net.InvalidPacketException;
-    method @NonNull public java.net.InetAddress getDstAddress();
-    method public int getDstPort();
-    method @NonNull public byte[] getPacket();
-    method @NonNull public java.net.InetAddress getSrcAddress();
-    method public int getSrcPort();
-  }
-
-  public class LinkAddress implements android.os.Parcelable {
-    ctor public LinkAddress(@NonNull java.net.InetAddress, @IntRange(from=0, to=128) int, int, int);
-    ctor public LinkAddress(@NonNull java.net.InetAddress, @IntRange(from=0, to=128) int, int, int, long, long);
-    ctor public LinkAddress(@NonNull java.net.InetAddress, @IntRange(from=0, to=128) int);
-    ctor public LinkAddress(@NonNull String);
-    ctor public LinkAddress(@NonNull String, int, int);
-    method public long getDeprecationTime();
-    method public long getExpirationTime();
-    method public boolean isGlobalPreferred();
-    method public boolean isIpv4();
-    method public boolean isIpv6();
-    method public boolean isSameAddressAs(@Nullable android.net.LinkAddress);
-    field public static final long LIFETIME_PERMANENT = 9223372036854775807L; // 0x7fffffffffffffffL
-    field public static final long LIFETIME_UNKNOWN = -1L; // 0xffffffffffffffffL
-  }
-
-  public final class LinkProperties implements android.os.Parcelable {
-    ctor public LinkProperties(@Nullable android.net.LinkProperties);
-    ctor public LinkProperties(@Nullable android.net.LinkProperties, boolean);
-    method public boolean addDnsServer(@NonNull java.net.InetAddress);
-    method public boolean addLinkAddress(@NonNull android.net.LinkAddress);
-    method public boolean addPcscfServer(@NonNull java.net.InetAddress);
-    method @NonNull public java.util.List<java.net.InetAddress> getAddresses();
-    method @NonNull public java.util.List<java.lang.String> getAllInterfaceNames();
-    method @NonNull public java.util.List<android.net.LinkAddress> getAllLinkAddresses();
-    method @NonNull public java.util.List<android.net.RouteInfo> getAllRoutes();
-    method @Nullable public android.net.Uri getCaptivePortalApiUrl();
-    method @Nullable public android.net.CaptivePortalData getCaptivePortalData();
-    method @NonNull public java.util.List<java.net.InetAddress> getPcscfServers();
-    method @Nullable public String getTcpBufferSizes();
-    method @NonNull public java.util.List<java.net.InetAddress> getValidatedPrivateDnsServers();
-    method public boolean hasGlobalIpv6Address();
-    method public boolean hasIpv4Address();
-    method public boolean hasIpv4DefaultRoute();
-    method public boolean hasIpv4DnsServer();
-    method public boolean hasIpv6DefaultRoute();
-    method public boolean hasIpv6DnsServer();
-    method public boolean isIpv4Provisioned();
-    method public boolean isIpv6Provisioned();
-    method public boolean isProvisioned();
-    method public boolean isReachable(@NonNull java.net.InetAddress);
-    method public boolean removeDnsServer(@NonNull java.net.InetAddress);
-    method public boolean removeLinkAddress(@NonNull android.net.LinkAddress);
-    method public boolean removeRoute(@NonNull android.net.RouteInfo);
-    method public void setCaptivePortalApiUrl(@Nullable android.net.Uri);
-    method public void setCaptivePortalData(@Nullable android.net.CaptivePortalData);
-    method public void setPcscfServers(@NonNull java.util.Collection<java.net.InetAddress>);
-    method public void setPrivateDnsServerName(@Nullable String);
-    method public void setTcpBufferSizes(@Nullable String);
-    method public void setUsePrivateDns(boolean);
-    method public void setValidatedPrivateDnsServers(@NonNull java.util.Collection<java.net.InetAddress>);
-  }
-
-  public final class NattKeepalivePacketData extends android.net.KeepalivePacketData implements android.os.Parcelable {
-    ctor public NattKeepalivePacketData(@NonNull java.net.InetAddress, int, @NonNull java.net.InetAddress, int, @NonNull byte[]) throws android.net.InvalidPacketException;
-    method public int describeContents();
-    method public void writeToParcel(@NonNull android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.net.NattKeepalivePacketData> CREATOR;
-  }
-
-  public class Network implements android.os.Parcelable {
-    ctor public Network(@NonNull android.net.Network);
-    method public int getNetId();
-    method @NonNull public android.net.Network getPrivateDnsBypassingCopy();
-  }
-
-  public abstract class NetworkAgent {
-    ctor public NetworkAgent(@NonNull android.content.Context, @NonNull android.os.Looper, @NonNull String, @NonNull android.net.NetworkCapabilities, @NonNull android.net.LinkProperties, int, @NonNull android.net.NetworkAgentConfig, @Nullable android.net.NetworkProvider);
-    ctor public NetworkAgent(@NonNull android.content.Context, @NonNull android.os.Looper, @NonNull String, @NonNull android.net.NetworkCapabilities, @NonNull android.net.LinkProperties, @NonNull android.net.NetworkScore, @NonNull android.net.NetworkAgentConfig, @Nullable android.net.NetworkProvider);
-    method @Nullable public android.net.Network getNetwork();
-    method public void markConnected();
-    method public void onAddKeepalivePacketFilter(int, @NonNull android.net.KeepalivePacketData);
-    method public void onAutomaticReconnectDisabled();
-    method public void onBandwidthUpdateRequested();
-    method public void onDscpPolicyStatusUpdated(int, int);
-    method public void onNetworkCreated();
-    method public void onNetworkDestroyed();
-    method public void onNetworkUnwanted();
-    method public void onQosCallbackRegistered(int, @NonNull android.net.QosFilter);
-    method public void onQosCallbackUnregistered(int);
-    method public void onRemoveKeepalivePacketFilter(int);
-    method public void onSaveAcceptUnvalidated(boolean);
-    method public void onSignalStrengthThresholdsUpdated(@NonNull int[]);
-    method public void onStartSocketKeepalive(int, @NonNull java.time.Duration, @NonNull android.net.KeepalivePacketData);
-    method public void onStopSocketKeepalive(int);
-    method public void onValidationStatus(int, @Nullable android.net.Uri);
-    method @NonNull public android.net.Network register();
-    method public void sendAddDscpPolicy(@NonNull android.net.DscpPolicy);
-    method public void sendLinkProperties(@NonNull android.net.LinkProperties);
-    method public void sendNetworkCapabilities(@NonNull android.net.NetworkCapabilities);
-    method public void sendNetworkScore(@NonNull android.net.NetworkScore);
-    method public void sendNetworkScore(@IntRange(from=0, to=99) int);
-    method public final void sendQosCallbackError(int, int);
-    method public final void sendQosSessionAvailable(int, int, @NonNull android.net.QosSessionAttributes);
-    method public final void sendQosSessionLost(int, int, int);
-    method public void sendRemoveAllDscpPolicies();
-    method public void sendRemoveDscpPolicy(int);
-    method public final void sendSocketKeepaliveEvent(int, int);
-    method @Deprecated public void setLegacySubtype(int, @NonNull String);
-    method public void setLingerDuration(@NonNull java.time.Duration);
-    method public void setTeardownDelayMillis(@IntRange(from=0, to=0x1388) int);
-    method public void setUnderlyingNetworks(@Nullable java.util.List<android.net.Network>);
-    method public void unregister();
-    method public void unregisterAfterReplacement(@IntRange(from=0, to=0x1388) int);
-    field public static final int DSCP_POLICY_STATUS_DELETED = 4; // 0x4
-    field public static final int DSCP_POLICY_STATUS_INSUFFICIENT_PROCESSING_RESOURCES = 3; // 0x3
-    field public static final int DSCP_POLICY_STATUS_POLICY_NOT_FOUND = 5; // 0x5
-    field public static final int DSCP_POLICY_STATUS_REQUESTED_CLASSIFIER_NOT_SUPPORTED = 2; // 0x2
-    field public static final int DSCP_POLICY_STATUS_REQUEST_DECLINED = 1; // 0x1
-    field public static final int DSCP_POLICY_STATUS_SUCCESS = 0; // 0x0
-    field public static final int VALIDATION_STATUS_NOT_VALID = 2; // 0x2
-    field public static final int VALIDATION_STATUS_VALID = 1; // 0x1
-  }
-
-  public final class NetworkAgentConfig implements android.os.Parcelable {
-    method public int describeContents();
-    method public int getLegacyType();
-    method @NonNull public String getLegacyTypeName();
-    method public boolean isExplicitlySelected();
-    method public boolean isPartialConnectivityAcceptable();
-    method public boolean isUnvalidatedConnectivityAcceptable();
-    method public void writeToParcel(@NonNull android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.net.NetworkAgentConfig> CREATOR;
-  }
-
-  public static final class NetworkAgentConfig.Builder {
-    ctor public NetworkAgentConfig.Builder();
-    method @NonNull public android.net.NetworkAgentConfig build();
-    method @NonNull public android.net.NetworkAgentConfig.Builder setExplicitlySelected(boolean);
-    method @NonNull public android.net.NetworkAgentConfig.Builder setLegacyExtraInfo(@NonNull String);
-    method @NonNull public android.net.NetworkAgentConfig.Builder setLegacySubType(int);
-    method @NonNull public android.net.NetworkAgentConfig.Builder setLegacySubTypeName(@NonNull String);
-    method @NonNull public android.net.NetworkAgentConfig.Builder setLegacyType(int);
-    method @NonNull public android.net.NetworkAgentConfig.Builder setLegacyTypeName(@NonNull String);
-    method @NonNull public android.net.NetworkAgentConfig.Builder setNat64DetectionEnabled(boolean);
-    method @NonNull public android.net.NetworkAgentConfig.Builder setPartialConnectivityAcceptable(boolean);
-    method @NonNull public android.net.NetworkAgentConfig.Builder setProvisioningNotificationEnabled(boolean);
-    method @NonNull public android.net.NetworkAgentConfig.Builder setUnvalidatedConnectivityAcceptable(boolean);
-  }
-
-  public final class NetworkCapabilities implements android.os.Parcelable {
-    method @NonNull public int[] getAdministratorUids();
-    method @Nullable public static String getCapabilityCarrierName(int);
-    method @Nullable public String getSsid();
-    method @NonNull public java.util.Set<java.lang.Integer> getSubscriptionIds();
-    method @NonNull public int[] getTransportTypes();
-    method @Nullable public java.util.List<android.net.Network> getUnderlyingNetworks();
-    method public boolean isPrivateDnsBroken();
-    method public boolean satisfiedByNetworkCapabilities(@Nullable android.net.NetworkCapabilities);
-    field public static final int NET_CAPABILITY_BIP = 31; // 0x1f
-    field public static final int NET_CAPABILITY_NOT_VCN_MANAGED = 28; // 0x1c
-    field public static final int NET_CAPABILITY_OEM_PAID = 22; // 0x16
-    field public static final int NET_CAPABILITY_OEM_PRIVATE = 26; // 0x1a
-    field public static final int NET_CAPABILITY_PARTIAL_CONNECTIVITY = 24; // 0x18
-    field public static final int NET_CAPABILITY_VEHICLE_INTERNAL = 27; // 0x1b
-    field public static final int NET_CAPABILITY_VSIM = 30; // 0x1e
-  }
-
-  public static final class NetworkCapabilities.Builder {
-    ctor public NetworkCapabilities.Builder();
-    ctor public NetworkCapabilities.Builder(@NonNull android.net.NetworkCapabilities);
-    method @NonNull public android.net.NetworkCapabilities.Builder addCapability(int);
-    method @NonNull public android.net.NetworkCapabilities.Builder addEnterpriseId(int);
-    method @NonNull public android.net.NetworkCapabilities.Builder addTransportType(int);
-    method @NonNull public android.net.NetworkCapabilities build();
-    method @NonNull public android.net.NetworkCapabilities.Builder removeCapability(int);
-    method @NonNull public android.net.NetworkCapabilities.Builder removeEnterpriseId(int);
-    method @NonNull public android.net.NetworkCapabilities.Builder removeTransportType(int);
-    method @NonNull @RequiresPermission(android.Manifest.permission.NETWORK_FACTORY) public android.net.NetworkCapabilities.Builder setAdministratorUids(@NonNull int[]);
-    method @NonNull public android.net.NetworkCapabilities.Builder setLinkDownstreamBandwidthKbps(int);
-    method @NonNull public android.net.NetworkCapabilities.Builder setLinkUpstreamBandwidthKbps(int);
-    method @NonNull public android.net.NetworkCapabilities.Builder setNetworkSpecifier(@Nullable android.net.NetworkSpecifier);
-    method @NonNull @RequiresPermission(android.Manifest.permission.NETWORK_FACTORY) public android.net.NetworkCapabilities.Builder setOwnerUid(int);
-    method @NonNull @RequiresPermission(android.Manifest.permission.NETWORK_FACTORY) public android.net.NetworkCapabilities.Builder setRequestorPackageName(@Nullable String);
-    method @NonNull @RequiresPermission(android.Manifest.permission.NETWORK_FACTORY) public android.net.NetworkCapabilities.Builder setRequestorUid(int);
-    method @NonNull @RequiresPermission(android.Manifest.permission.NETWORK_SIGNAL_STRENGTH_WAKEUP) public android.net.NetworkCapabilities.Builder setSignalStrength(int);
-    method @NonNull @RequiresPermission(android.Manifest.permission.NETWORK_FACTORY) public android.net.NetworkCapabilities.Builder setSsid(@Nullable String);
-    method @NonNull public android.net.NetworkCapabilities.Builder setSubscriptionIds(@NonNull java.util.Set<java.lang.Integer>);
-    method @NonNull public android.net.NetworkCapabilities.Builder setTransportInfo(@Nullable android.net.TransportInfo);
-    method @NonNull @RequiresPermission(android.Manifest.permission.NETWORK_FACTORY) public android.net.NetworkCapabilities.Builder setUnderlyingNetworks(@Nullable java.util.List<android.net.Network>);
-    method @NonNull public static android.net.NetworkCapabilities.Builder withoutDefaultCapabilities();
-  }
-
-  public class NetworkProvider {
-    ctor public NetworkProvider(@NonNull android.content.Context, @NonNull android.os.Looper, @NonNull String);
-    method @RequiresPermission(android.Manifest.permission.NETWORK_FACTORY) public void declareNetworkRequestUnfulfillable(@NonNull android.net.NetworkRequest);
-    method public int getProviderId();
-    method public void onNetworkRequestWithdrawn(@NonNull android.net.NetworkRequest);
-    method public void onNetworkRequested(@NonNull android.net.NetworkRequest, @IntRange(from=0, to=99) int, int);
-    method @RequiresPermission(android.Manifest.permission.NETWORK_FACTORY) public void registerNetworkOffer(@NonNull android.net.NetworkScore, @NonNull android.net.NetworkCapabilities, @NonNull java.util.concurrent.Executor, @NonNull android.net.NetworkProvider.NetworkOfferCallback);
-    method @RequiresPermission(android.Manifest.permission.NETWORK_FACTORY) public void unregisterNetworkOffer(@NonNull android.net.NetworkProvider.NetworkOfferCallback);
-    field public static final int ID_NONE = -1; // 0xffffffff
-  }
-
-  public static interface NetworkProvider.NetworkOfferCallback {
-    method public void onNetworkNeeded(@NonNull android.net.NetworkRequest);
-    method public void onNetworkUnneeded(@NonNull android.net.NetworkRequest);
-  }
-
-  public class NetworkReleasedException extends java.lang.Exception {
-    ctor public NetworkReleasedException();
-  }
-
-  public class NetworkRequest implements android.os.Parcelable {
-    method @Nullable public String getRequestorPackageName();
-    method public int getRequestorUid();
-  }
-
-  public static class NetworkRequest.Builder {
-    method @NonNull @RequiresPermission(android.Manifest.permission.NETWORK_SIGNAL_STRENGTH_WAKEUP) public android.net.NetworkRequest.Builder setSignalStrength(int);
-    method @NonNull public android.net.NetworkRequest.Builder setSubscriptionIds(@NonNull java.util.Set<java.lang.Integer>);
-  }
-
-  public final class NetworkScore implements android.os.Parcelable {
-    method public int describeContents();
-    method public int getKeepConnectedReason();
-    method public int getLegacyInt();
-    method public boolean isExiting();
-    method public boolean isTransportPrimary();
-    method public void writeToParcel(@NonNull android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.net.NetworkScore> CREATOR;
-    field public static final int KEEP_CONNECTED_FOR_HANDOVER = 1; // 0x1
-    field public static final int KEEP_CONNECTED_NONE = 0; // 0x0
-  }
-
-  public static final class NetworkScore.Builder {
-    ctor public NetworkScore.Builder();
-    method @NonNull public android.net.NetworkScore build();
-    method @NonNull public android.net.NetworkScore.Builder setExiting(boolean);
-    method @NonNull public android.net.NetworkScore.Builder setKeepConnectedReason(int);
-    method @NonNull public android.net.NetworkScore.Builder setLegacyInt(int);
-    method @NonNull public android.net.NetworkScore.Builder setTransportPrimary(boolean);
-  }
-
-  public final class OemNetworkPreferences implements android.os.Parcelable {
-    method public int describeContents();
-    method @NonNull public java.util.Map<java.lang.String,java.lang.Integer> getNetworkPreferences();
-    method public void writeToParcel(@NonNull android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.net.OemNetworkPreferences> CREATOR;
-    field public static final int OEM_NETWORK_PREFERENCE_OEM_PAID = 1; // 0x1
-    field public static final int OEM_NETWORK_PREFERENCE_OEM_PAID_NO_FALLBACK = 2; // 0x2
-    field public static final int OEM_NETWORK_PREFERENCE_OEM_PAID_ONLY = 3; // 0x3
-    field public static final int OEM_NETWORK_PREFERENCE_OEM_PRIVATE_ONLY = 4; // 0x4
-    field public static final int OEM_NETWORK_PREFERENCE_UNINITIALIZED = 0; // 0x0
-  }
-
-  public static final class OemNetworkPreferences.Builder {
-    ctor public OemNetworkPreferences.Builder();
-    ctor public OemNetworkPreferences.Builder(@NonNull android.net.OemNetworkPreferences);
-    method @NonNull public android.net.OemNetworkPreferences.Builder addNetworkPreference(@NonNull String, int);
-    method @NonNull public android.net.OemNetworkPreferences build();
-    method @NonNull public android.net.OemNetworkPreferences.Builder clearNetworkPreference(@NonNull String);
-  }
-
-  public abstract class QosCallback {
-    ctor public QosCallback();
-    method public void onError(@NonNull android.net.QosCallbackException);
-    method public void onQosSessionAvailable(@NonNull android.net.QosSession, @NonNull android.net.QosSessionAttributes);
-    method public void onQosSessionLost(@NonNull android.net.QosSession);
-  }
-
-  public static class QosCallback.QosCallbackRegistrationException extends java.lang.RuntimeException {
-  }
-
-  public final class QosCallbackException extends java.lang.Exception {
-    ctor public QosCallbackException(@NonNull String);
-    ctor public QosCallbackException(@NonNull Throwable);
-  }
-
-  public abstract class QosFilter {
-    method @NonNull public abstract android.net.Network getNetwork();
-    method public abstract boolean matchesLocalAddress(@NonNull java.net.InetAddress, int, int);
-    method public boolean matchesProtocol(int);
-    method public abstract boolean matchesRemoteAddress(@NonNull java.net.InetAddress, int, int);
-  }
-
-  public final class QosSession implements android.os.Parcelable {
-    ctor public QosSession(int, int);
-    method public int describeContents();
-    method public int getSessionId();
-    method public int getSessionType();
-    method public long getUniqueId();
-    method public void writeToParcel(@NonNull android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.net.QosSession> CREATOR;
-    field public static final int TYPE_EPS_BEARER = 1; // 0x1
-    field public static final int TYPE_NR_BEARER = 2; // 0x2
-  }
-
-  public interface QosSessionAttributes {
-  }
-
-  public final class QosSocketInfo implements android.os.Parcelable {
-    ctor public QosSocketInfo(@NonNull android.net.Network, @NonNull java.net.Socket) throws java.io.IOException;
-    ctor public QosSocketInfo(@NonNull android.net.Network, @NonNull java.net.DatagramSocket) throws java.io.IOException;
-    method public int describeContents();
-    method @NonNull public java.net.InetSocketAddress getLocalSocketAddress();
-    method @NonNull public android.net.Network getNetwork();
-    method @Nullable public java.net.InetSocketAddress getRemoteSocketAddress();
-    method public void writeToParcel(@NonNull android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.net.QosSocketInfo> CREATOR;
-  }
-
-  public final class RouteInfo implements android.os.Parcelable {
-    ctor public RouteInfo(@Nullable android.net.IpPrefix, @Nullable java.net.InetAddress, @Nullable String, int);
-    ctor public RouteInfo(@Nullable android.net.IpPrefix, @Nullable java.net.InetAddress, @Nullable String, int, int);
-    method public int getMtu();
-  }
-
-  public abstract class SocketKeepalive implements java.lang.AutoCloseable {
-    method public final void start(@IntRange(from=0xa, to=0xe10) int, int, @Nullable android.net.Network);
-    field public static final int ERROR_NO_SUCH_SLOT = -33; // 0xffffffdf
-    field public static final int FLAG_AUTOMATIC_ON_OFF = 1; // 0x1
-    field public static final int SUCCESS = 0; // 0x0
-  }
-
-  public class SocketLocalAddressChangedException extends java.lang.Exception {
-    ctor public SocketLocalAddressChangedException();
-  }
-
-  public class SocketNotBoundException extends java.lang.Exception {
-    ctor public SocketNotBoundException();
-  }
-
-  public class SocketNotConnectedException extends java.lang.Exception {
-    ctor public SocketNotConnectedException();
-  }
-
-  public class SocketRemoteAddressChangedException extends java.lang.Exception {
-    ctor public SocketRemoteAddressChangedException();
-  }
-
-  public final class StaticIpConfiguration implements android.os.Parcelable {
-    ctor public StaticIpConfiguration();
-    ctor public StaticIpConfiguration(@Nullable android.net.StaticIpConfiguration);
-    method public void addDnsServer(@NonNull java.net.InetAddress);
-    method public void clear();
-    method @NonNull public java.util.List<android.net.RouteInfo> getRoutes(@Nullable String);
-  }
-
-  public final class TcpKeepalivePacketData extends android.net.KeepalivePacketData implements android.os.Parcelable {
-    ctor public TcpKeepalivePacketData(@NonNull java.net.InetAddress, int, @NonNull java.net.InetAddress, int, @NonNull byte[], int, int, int, int, int, int) throws android.net.InvalidPacketException;
-    method public int describeContents();
-    method public int getIpTos();
-    method public int getIpTtl();
-    method public int getTcpAck();
-    method public int getTcpSeq();
-    method public int getTcpWindow();
-    method public int getTcpWindowScale();
-    method public void writeToParcel(@NonNull android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.net.TcpKeepalivePacketData> CREATOR;
-  }
-
-  public final class VpnTransportInfo implements android.os.Parcelable android.net.TransportInfo {
-    ctor public VpnTransportInfo(int, @Nullable String, boolean, boolean);
-    method public boolean areLongLivedTcpConnectionsExpensive();
-    method public int describeContents();
-    method public int getType();
-    method public boolean isBypassable();
-    method public void writeToParcel(@NonNull android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.net.VpnTransportInfo> CREATOR;
-  }
-
-}
-
-package android.net.apf {
-
-  public final class ApfCapabilities implements android.os.Parcelable {
-    ctor public ApfCapabilities(int, int, int);
-    method public int describeContents();
-    method public static boolean getApfDrop8023Frames();
-    method @NonNull public static int[] getApfEtherTypeBlackList();
-    method public boolean hasDataAccess();
-    method public void writeToParcel(android.os.Parcel, int);
-    field public static final android.os.Parcelable.Creator<android.net.apf.ApfCapabilities> CREATOR;
-    field public final int apfPacketFormat;
-    field public final int apfVersionSupported;
-    field public final int maximumApfProgramSize;
-  }
-
-}
-
diff --git a/framework/udc-extended-api/system-lint-baseline.txt b/framework/udc-extended-api/system-lint-baseline.txt
deleted file mode 100644
index 9a97707..0000000
--- a/framework/udc-extended-api/system-lint-baseline.txt
+++ /dev/null
@@ -1 +0,0 @@
-// Baseline format: 1.0
diff --git a/framework/udc-extended-api/system-removed.txt b/framework/udc-extended-api/system-removed.txt
deleted file mode 100644
index d802177..0000000
--- a/framework/udc-extended-api/system-removed.txt
+++ /dev/null
@@ -1 +0,0 @@
-// Signature format: 2.0
diff --git a/nearby/README.md b/nearby/README.md
index 8451882..8dac61c 100644
--- a/nearby/README.md
+++ b/nearby/README.md
@@ -47,12 +47,29 @@
 ## Build and Install
 
 ```sh
-$ source build/envsetup.sh && lunch <TARGET>
-$ m com.google.android.tethering.next deapexer
-$ $ANDROID_BUILD_TOP/out/host/linux-x86/bin/deapexer decompress --input \
-    ${ANDROID_PRODUCT_OUT}/system/apex/com.google.android.tethering.next.capex \
-    --output /tmp/tethering.apex
-$ adb install -r /tmp/tethering.apex
+For master on AOSP (Android) host
+$ source build/envsetup.sh
+$ lunch aosp_oriole-trunk_staging-userdebug
+$ m com.android.tethering
+$ $ANDROID_BUILD_TOP/out/host/linux-x86/bin/deapexer decompress --input $ANDROID_PRODUCT_OUT/system/apex/com.android.tethering.capex --output /tmp/tethering.apex
+$ adb install /tmp/tethering.apex
+$ adb reboot
+
+NOTE: Developers should use AOSP by default, udc-mainline-prod should not be used unless for Google internal features.
+For udc-mainline-prod on Google internal host
+Build unbundled module using banchan
+$ source build/envsetup.sh
+$ banchan com.google.android.tethering mainline_modules_arm64
+$ m apps_only dist
+$ adb install out/dist/com.google.android.tethering.apex
+$ adb reboot
+Ensure that the module you are installing is compatible with the module currently preloaded on the phone (in /system/apex/com.google.android.tethering.apex). Compatible means:
+
+1. Same package name
+2. Same keys used to sign the apex and the payload
+3. Higher version
+
+See go/mainline-local-build#build-install-local-module for more information
 ```
 
 ## Build and Install from tm-mainline-prod branch
@@ -63,7 +80,7 @@
 This is because the device is flashed with AOSP built from master or other branches, which has
 prebuilt APEX with higher version. We can use root access to replace the prebuilt APEX with the APEX
 built from tm-mainline-prod as below.
-1. adb root && adb remount; adb shell mount -orw,remount /system/apex
+1. adb root && adb remount -R
 2. cp tethering.next.apex com.google.android.tethering.apex
 3. adb push  com.google.android.tethering.apex  /system/apex/
 4. adb reboot
diff --git a/nearby/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..0fd9a89 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"],
 }
 
diff --git a/nearby/framework/java/android/nearby/DataElement.java b/nearby/framework/java/android/nearby/DataElement.java
index 10c1132..8f032bf 100644
--- a/nearby/framework/java/android/nearby/DataElement.java
+++ b/nearby/framework/java/android/nearby/DataElement.java
@@ -39,10 +39,15 @@
     private final int mKey;
     private final byte[] mValue;
 
-    /** @hide */
+    /**
+     * Note this interface is used for internal implementation only.
+     * We only keep those data element types used for encoding and decoding from the specs.
+     * Please read the nearby specs for learning more about each data type and use it as the only
+     * source.
+     *
+     * @hide
+     */
     @IntDef({
-            DataType.BLE_SERVICE_DATA,
-            DataType.BLE_ADDRESS,
             DataType.SALT,
             DataType.PRIVATE_IDENTITY,
             DataType.TRUSTED_IDENTITY,
@@ -50,20 +55,17 @@
             DataType.PROVISIONED_IDENTITY,
             DataType.TX_POWER,
             DataType.ACTION,
-            DataType.MODEL_ID,
-            DataType.EDDYSTONE_EPHEMERAL_IDENTIFIER,
             DataType.ACCOUNT_KEY_DATA,
             DataType.CONNECTION_STATUS,
             DataType.BATTERY,
+            DataType.ENCRYPTION_INFO,
+            DataType.BLE_SERVICE_DATA,
+            DataType.BLE_ADDRESS,
             DataType.SCAN_MODE,
             DataType.TEST_DE_BEGIN,
             DataType.TEST_DE_END
     })
     public @interface DataType {
-        int BLE_SERVICE_DATA = 100;
-        int BLE_ADDRESS = 101;
-        // This is to indicate if the scan is offload only
-        int SCAN_MODE = 102;
         int SALT = 0;
         int PRIVATE_IDENTITY = 1;
         int TRUSTED_IDENTITY = 2;
@@ -71,11 +73,19 @@
         int PROVISIONED_IDENTITY = 4;
         int TX_POWER = 5;
         int ACTION = 6;
-        int MODEL_ID = 7;
-        int EDDYSTONE_EPHEMERAL_IDENTIFIER = 8;
         int ACCOUNT_KEY_DATA = 9;
         int CONNECTION_STATUS = 10;
         int BATTERY = 11;
+
+        int ENCRYPTION_INFO = 16;
+
+        // Not defined in the spec. Reserved for internal use only.
+        int BLE_SERVICE_DATA = 100;
+        int BLE_ADDRESS = 101;
+        // This is to indicate if the scan is offload only
+        int SCAN_MODE = 102;
+
+        int DEVICE_TYPE = 22;
         // Reserves test DE ranges from {@link DataElement.DataType#TEST_DE_BEGIN}
         // to {@link DataElement.DataType#TEST_DE_END}, inclusive.
         // Reserves 128 Test DEs.
@@ -84,27 +94,6 @@
     }
 
     /**
-     * @hide
-     */
-    public static boolean isValidType(int type) {
-        return type == DataType.BLE_SERVICE_DATA
-                || type == DataType.ACCOUNT_KEY_DATA
-                || type == DataType.BLE_ADDRESS
-                || type == DataType.SCAN_MODE
-                || type == DataType.SALT
-                || type == DataType.PRIVATE_IDENTITY
-                || type == DataType.TRUSTED_IDENTITY
-                || type == DataType.PUBLIC_IDENTITY
-                || type == DataType.PROVISIONED_IDENTITY
-                || type == DataType.TX_POWER
-                || type == DataType.ACTION
-                || type == DataType.MODEL_ID
-                || type == DataType.EDDYSTONE_EPHEMERAL_IDENTIFIER
-                || type == DataType.CONNECTION_STATUS
-                || type == DataType.BATTERY;
-    }
-
-    /**
      * @return {@code true} if this is identity type.
      * @hide
      */
diff --git a/nearby/framework/java/android/nearby/NearbyManager.java b/nearby/framework/java/android/nearby/NearbyManager.java
index 070a2b6..00f1c38 100644
--- a/nearby/framework/java/android/nearby/NearbyManager.java
+++ b/nearby/framework/java/android/nearby/NearbyManager.java
@@ -284,6 +284,8 @@
      */
     public void queryOffloadCapability(@NonNull @CallbackExecutor Executor executor,
             @NonNull Consumer<OffloadCapability> callback) {
+        Objects.requireNonNull(executor);
+        Objects.requireNonNull(callback);
         try {
             mService.queryOffloadCapability(new OffloadTransport(executor, callback));
         } catch (RemoteException e) {
diff --git a/nearby/service/Android.bp b/nearby/service/Android.bp
index 4630902..d34fd83 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",
@@ -66,13 +67,16 @@
     apex_available: [
         "com.android.tethering",
     ],
+    lint: {
+        baseline_filename: "lint-baseline.xml",
+    },
 }
 
 genrule {
     name: "statslog-nearby-java-gen",
     tools: ["stats-log-api-gen"],
     cmd: "$(location stats-log-api-gen) --java $(out) --module nearby " +
-         " --javaPackage com.android.server.nearby.proto --javaClass NearbyStatsLog" +
-         " --minApiLevel 33",
+        " --javaPackage com.android.server.nearby.proto --javaClass NearbyStatsLog" +
+        " --minApiLevel 33",
     out: ["com/android/server/nearby/proto/NearbyStatsLog.java"],
 }
diff --git a/nearby/service/java/com/android/server/nearby/NearbyConfiguration.java b/nearby/service/java/com/android/server/nearby/NearbyConfiguration.java
index 9b32d69..9ef905d 100644
--- a/nearby/service/java/com/android/server/nearby/NearbyConfiguration.java
+++ b/nearby/service/java/com/android/server/nearby/NearbyConfiguration.java
@@ -54,6 +54,11 @@
     public static final String NEARBY_REFACTOR_DISCOVERY_MANAGER =
             "nearby_refactor_discovery_manager";
 
+    /**
+     * Flag to guard enable BLE during Nearby Service init time.
+     */
+    public static final String NEARBY_ENABLE_BLE_IN_INIT = "nearby_enable_ble_in_init";
+
     private static final boolean IS_USER_BUILD = "user".equals(Build.TYPE);
 
     private final DeviceConfigListener mDeviceConfigListener = new DeviceConfigListener();
@@ -67,6 +72,8 @@
     private boolean mSupportTestApp;
     @GuardedBy("mDeviceConfigLock")
     private boolean mRefactorDiscoveryManager;
+    @GuardedBy("mDeviceConfigLock")
+    private boolean mEnableBleInInit;
 
     public NearbyConfiguration() {
         mDeviceConfigListener.start();
@@ -131,6 +138,15 @@
         }
     }
 
+    /**
+     * @return {@code true} if enableBLE() is called during NearbyService init time.
+     */
+    public boolean enableBleInInit() {
+        synchronized (mDeviceConfigLock) {
+            return mEnableBleInInit;
+        }
+    }
+
     private class DeviceConfigListener implements DeviceConfig.OnPropertiesChangedListener {
         public void start() {
             DeviceConfig.addOnPropertiesChangedListener(getNamespace(),
@@ -149,6 +165,8 @@
                         NEARBY_SUPPORT_TEST_APP, false /* defaultValue */);
                 mRefactorDiscoveryManager = getDeviceConfigBoolean(
                         NEARBY_REFACTOR_DISCOVERY_MANAGER, false /* defaultValue */);
+                mEnableBleInInit = getDeviceConfigBoolean(
+                        NEARBY_ENABLE_BLE_IN_INIT, true /* defaultValue */);
             }
         }
     }
diff --git a/nearby/service/java/com/android/server/nearby/managers/BroadcastProviderManager.java b/nearby/service/java/com/android/server/nearby/managers/BroadcastProviderManager.java
index 024bff8..28a33fa 100644
--- a/nearby/service/java/com/android/server/nearby/managers/BroadcastProviderManager.java
+++ b/nearby/service/java/com/android/server/nearby/managers/BroadcastProviderManager.java
@@ -22,6 +22,7 @@
 import android.nearby.BroadcastRequest;
 import android.nearby.IBroadcastListener;
 import android.nearby.PresenceBroadcastRequest;
+import android.os.IBinder;
 import android.os.RemoteException;
 import android.util.Log;
 
@@ -49,6 +50,10 @@
     private final NearbyConfiguration mNearbyConfiguration;
 
     private IBroadcastListener mBroadcastListener;
+    // Used with mBroadcastListener. Now we only support single client, for multi clients, a map
+    // between live binder to the information over the binder is needed.
+    // TODO: Finish multi-client logic for broadcast.
+    private BroadcastListenerDeathRecipient mDeathRecipient;
 
     public BroadcastProviderManager(Context context, Injector injector) {
         this(ForegroundThread.getExecutor(),
@@ -62,6 +67,7 @@
         mLock = new Object();
         mNearbyConfiguration = new NearbyConfiguration();
         mBroadcastListener = null;
+        mDeathRecipient = null;
     }
 
     /**
@@ -70,6 +76,15 @@
     public void startBroadcast(BroadcastRequest broadcastRequest, IBroadcastListener listener) {
         synchronized (mLock) {
             mExecutor.execute(() -> {
+                if (listener == null) {
+                    return;
+                }
+                if (mBroadcastListener != null) {
+                    Log.i(TAG, "We do not support multi clients yet,"
+                            + " please stop previous broadcast first.");
+                    reportBroadcastStatus(listener, BroadcastCallback.STATUS_FAILURE);
+                    return;
+                }
                 if (!mNearbyConfiguration.isTestAppSupported()) {
                     NearbyConfiguration configuration = new NearbyConfiguration();
                     if (!configuration.isPresenceBroadcastLegacyEnabled()) {
@@ -89,7 +104,17 @@
                     reportBroadcastStatus(listener, BroadcastCallback.STATUS_FAILURE);
                     return;
                 }
+                BroadcastListenerDeathRecipient deathRecipient =
+                        new BroadcastListenerDeathRecipient(listener);
+                try {
+                    listener.asBinder().linkToDeath(deathRecipient, 0);
+                } catch (RemoteException e) {
+                    // This binder has already died, so call the DeathRecipient as if we had
+                    // called linkToDeath in time.
+                    deathRecipient.binderDied();
+                }
                 mBroadcastListener = listener;
+                mDeathRecipient = deathRecipient;
                 mBleBroadcastProvider.start(presenceBroadcastRequest.getVersion(),
                         advertisement.toBytes(), this);
             });
@@ -113,13 +138,19 @@
      */
     public void stopBroadcast(IBroadcastListener listener) {
         synchronized (mLock) {
-            if (!mNearbyConfiguration.isTestAppSupported()
-                    && !mNearbyConfiguration.isPresenceBroadcastLegacyEnabled()) {
-                reportBroadcastStatus(listener, BroadcastCallback.STATUS_FAILURE);
-                return;
+            if (listener != null) {
+                if (!mNearbyConfiguration.isTestAppSupported()
+                        && !mNearbyConfiguration.isPresenceBroadcastLegacyEnabled()) {
+                    reportBroadcastStatus(listener, BroadcastCallback.STATUS_FAILURE);
+                    return;
+                }
+                if (mDeathRecipient != null) {
+                    listener.asBinder().unlinkToDeath(mDeathRecipient, 0);
+                }
             }
             mBroadcastListener = null;
-            mExecutor.execute(() -> mBleBroadcastProvider.stop());
+            mDeathRecipient = null;
+            mExecutor.execute(mBleBroadcastProvider::stop);
         }
     }
 
@@ -142,4 +173,21 @@
             Log.e(TAG, "remote exception when reporting status");
         }
     }
+
+    /**
+     * Class to make listener unregister after the binder is dead.
+     */
+    public class BroadcastListenerDeathRecipient implements IBinder.DeathRecipient {
+        public IBroadcastListener mListener;
+
+        BroadcastListenerDeathRecipient(IBroadcastListener listener) {
+            mListener = listener;
+        }
+
+        @Override
+        public void binderDied() {
+            Log.d(TAG, "Binder is dead - stop broadcast listener");
+            stopBroadcast(mListener);
+        }
+    }
 }
diff --git a/nearby/service/java/com/android/server/nearby/managers/DiscoveryProviderManager.java b/nearby/service/java/com/android/server/nearby/managers/DiscoveryProviderManager.java
index 0c41426..8995232 100644
--- a/nearby/service/java/com/android/server/nearby/managers/DiscoveryProviderManager.java
+++ b/nearby/service/java/com/android/server/nearby/managers/DiscoveryProviderManager.java
@@ -21,6 +21,7 @@
 import static com.android.server.nearby.NearbyService.TAG;
 
 import android.annotation.Nullable;
+import android.bluetooth.BluetoothAdapter;
 import android.content.Context;
 import android.nearby.DataElement;
 import android.nearby.IScanListener;
@@ -35,6 +36,7 @@
 import androidx.annotation.NonNull;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.nearby.NearbyConfiguration;
 import com.android.server.nearby.injector.Injector;
 import com.android.server.nearby.managers.registration.DiscoveryRegistration;
 import com.android.server.nearby.provider.AbstractDiscoveryProvider;
@@ -66,6 +68,7 @@
     private final BleDiscoveryProvider mBleDiscoveryProvider;
     private final Injector mInjector;
     private final Executor mExecutor;
+    private final NearbyConfiguration mNearbyConfiguration;
 
     public DiscoveryProviderManager(Context context, Injector injector) {
         Log.v(TAG, "DiscoveryProviderManager: ");
@@ -75,6 +78,7 @@
         mChreDiscoveryProvider = new ChreDiscoveryProvider(mContext,
                 new ChreCommunication(injector, mContext, mExecutor), mExecutor);
         mInjector = injector;
+        mNearbyConfiguration = new NearbyConfiguration();
     }
 
     @VisibleForTesting
@@ -86,6 +90,7 @@
         mInjector = injector;
         mBleDiscoveryProvider = bleDiscoveryProvider;
         mChreDiscoveryProvider = chreDiscoveryProvider;
+        mNearbyConfiguration = new NearbyConfiguration();
     }
 
     private static boolean isChreOnly(Set<ScanFilter> scanFilters) {
@@ -141,6 +146,10 @@
 
     /** Called after boot completed. */
     public void init() {
+        // Register BLE only scan when Bluetooth is turned off
+        if (mNearbyConfiguration.enableBleInInit()) {
+            setBleScanEnabled();
+        }
         if (mInjector.getContextHubManager() != null) {
             mChreDiscoveryProvider.init();
         }
@@ -242,7 +251,7 @@
     @GuardedBy("mMultiplexerLock")
     private void startBleProvider(Set<ScanFilter> scanFilters) {
         if (!mBleDiscoveryProvider.getController().isStarted()) {
-            Log.d(TAG, "DiscoveryProviderManager starts Ble scanning.");
+            Log.d(TAG, "DiscoveryProviderManager starts BLE scanning.");
             mBleDiscoveryProvider.getController().setListener(this);
             mBleDiscoveryProvider.getController().setProviderScanMode(mMerged.getScanMode());
             mBleDiscoveryProvider.getController().setProviderScanFilters(
@@ -313,4 +322,29 @@
     public void onMergedRegistrationsUpdated() {
         invalidateProviderScanMode();
     }
+
+    /**
+     * Registers Nearby service to Ble scan if Bluetooth is off. (Even when Bluetooth is off)
+     * @return {@code true} when Nearby currently can scan through Bluetooth or Ble or successfully
+     * registers Nearby service to Ble scan when Blutooth is off.
+     */
+    public boolean setBleScanEnabled() {
+        BluetoothAdapter adapter = mInjector.getBluetoothAdapter();
+        if (adapter == null) {
+            Log.e(TAG, "BluetoothAdapter is null.");
+            return false;
+        }
+        if (adapter.isEnabled() || adapter.isLeEnabled()) {
+            return true;
+        }
+        if (!adapter.isBleScanAlwaysAvailable()) {
+            Log.v(TAG, "Ble always on scan is disabled.");
+            return false;
+        }
+        if (!adapter.enableBLE()) {
+            Log.e(TAG, "Failed to register Ble scan.");
+            return false;
+        }
+        return true;
+    }
 }
diff --git a/nearby/service/java/com/android/server/nearby/managers/DiscoveryProviderManagerLegacy.java b/nearby/service/java/com/android/server/nearby/managers/DiscoveryProviderManagerLegacy.java
index 4b76eba..3ef8fb7 100644
--- a/nearby/service/java/com/android/server/nearby/managers/DiscoveryProviderManagerLegacy.java
+++ b/nearby/service/java/com/android/server/nearby/managers/DiscoveryProviderManagerLegacy.java
@@ -22,6 +22,7 @@
 
 import android.annotation.Nullable;
 import android.app.AppOpsManager;
+import android.bluetooth.BluetoothAdapter;
 import android.content.Context;
 import android.nearby.DataElement;
 import android.nearby.IScanListener;
@@ -38,6 +39,7 @@
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.nearby.NearbyConfiguration;
 import com.android.server.nearby.injector.Injector;
 import com.android.server.nearby.metrics.NearbyMetrics;
 import com.android.server.nearby.presence.PresenceDiscoveryResult;
@@ -69,6 +71,7 @@
     private final Context mContext;
     private final BleDiscoveryProvider mBleDiscoveryProvider;
     private final Injector mInjector;
+    private final NearbyConfiguration mNearbyConfiguration;
     @ScanRequest.ScanMode
     private int mScanMode;
     @GuardedBy("mLock")
@@ -83,6 +86,7 @@
                         mContext, new ChreCommunication(injector, mContext, executor), executor);
         mScanTypeScanListenerRecordMap = new HashMap<>();
         mInjector = injector;
+        mNearbyConfiguration = new NearbyConfiguration();
         Log.v(TAG, "DiscoveryProviderManagerLegacy: ");
     }
 
@@ -96,6 +100,7 @@
         mBleDiscoveryProvider = bleDiscoveryProvider;
         mChreDiscoveryProvider = chreDiscoveryProvider;
         mScanTypeScanListenerRecordMap = scanTypeScanListenerRecordMap;
+        mNearbyConfiguration = new NearbyConfiguration();
     }
 
     private static boolean isChreOnly(List<ScanFilter> scanFilters) {
@@ -142,18 +147,18 @@
             for (IBinder listenerBinder : mScanTypeScanListenerRecordMap.keySet()) {
                 ScanListenerRecord record = mScanTypeScanListenerRecordMap.get(listenerBinder);
                 if (record == null) {
-                    Log.w(TAG, "DiscoveryProviderManager cannot find the scan record.");
+                    Log.w(TAG, "DiscoveryProviderManagerLegacy cannot find the scan record.");
                     continue;
                 }
                 CallerIdentity callerIdentity = record.getCallerIdentity();
                 if (!DiscoveryPermissions.noteDiscoveryResultDelivery(
                         appOpsManager, callerIdentity)) {
-                    Log.w(TAG, "[DiscoveryProviderManager] scan permission revoked "
+                    Log.w(TAG, "[DiscoveryProviderManagerLegacy] scan permission revoked "
                             + "- not forwarding results");
                     try {
                         record.getScanListener().onError(ScanCallback.ERROR_PERMISSION_DENIED);
                     } catch (RemoteException e) {
-                        Log.w(TAG, "DiscoveryProviderManager failed to report error.", e);
+                        Log.w(TAG, "DiscoveryProviderManagerLegacy failed to report error.", e);
                     }
                     return;
                 }
@@ -180,7 +185,7 @@
                     NearbyMetrics.logScanDeviceDiscovered(
                             record.hashCode(), record.getScanRequest(), nearbyDevice);
                 } catch (RemoteException e) {
-                    Log.w(TAG, "DiscoveryProviderManager failed to report onDiscovered.", e);
+                    Log.w(TAG, "DiscoveryProviderManagerLegacy failed to report onDiscovered.", e);
                 }
             }
         }
@@ -193,18 +198,18 @@
             for (IBinder listenerBinder : mScanTypeScanListenerRecordMap.keySet()) {
                 ScanListenerRecord record = mScanTypeScanListenerRecordMap.get(listenerBinder);
                 if (record == null) {
-                    Log.w(TAG, "DiscoveryProviderManager cannot find the scan record.");
+                    Log.w(TAG, "DiscoveryProviderManagerLegacy cannot find the scan record.");
                     continue;
                 }
                 CallerIdentity callerIdentity = record.getCallerIdentity();
                 if (!DiscoveryPermissions.noteDiscoveryResultDelivery(
                         appOpsManager, callerIdentity)) {
-                    Log.w(TAG, "[DiscoveryProviderManager] scan permission revoked "
+                    Log.w(TAG, "[DiscoveryProviderManagerLegacy] scan permission revoked "
                             + "- not forwarding results");
                     try {
                         record.getScanListener().onError(ScanCallback.ERROR_PERMISSION_DENIED);
                     } catch (RemoteException e) {
-                        Log.w(TAG, "DiscoveryProviderManager failed to report error.", e);
+                        Log.w(TAG, "DiscoveryProviderManagerLegacy failed to report error.", e);
                     }
                     return;
                 }
@@ -212,7 +217,7 @@
                 try {
                     record.getScanListener().onError(errorCode);
                 } catch (RemoteException e) {
-                    Log.w(TAG, "DiscoveryProviderManager failed to report onError.", e);
+                    Log.w(TAG, "DiscoveryProviderManagerLegacy failed to report onError.", e);
                 }
             }
         }
@@ -220,6 +225,10 @@
 
     /** Called after boot completed. */
     public void init() {
+        // Register BLE only scan when Bluetooth is turned off
+        if (mNearbyConfiguration.enableBleInInit()) {
+            setBleScanEnabled();
+        }
         if (mInjector.getContextHubManager() != null) {
             mChreDiscoveryProvider.init();
         }
@@ -293,10 +302,10 @@
             if (listenerBinder != null && deathRecipient != null) {
                 listenerBinder.unlinkToDeath(removedRecord.getDeathRecipient(), 0);
             }
-            Log.v(TAG, "DiscoveryProviderManager unregistered scan listener.");
+            Log.v(TAG, "DiscoveryProviderManagerLegacy unregistered scan listener.");
             NearbyMetrics.logScanStopped(removedRecord.hashCode(), removedRecord.getScanRequest());
             if (mScanTypeScanListenerRecordMap.isEmpty()) {
-                Log.v(TAG, "DiscoveryProviderManager stops provider because there is no "
+                Log.v(TAG, "DiscoveryProviderManagerLegacy stops provider because there is no "
                         + "scan listener registered.");
                 stopProviders();
                 return;
@@ -306,8 +315,8 @@
 
             // Removes current highest scan mode requested and sets the next highest scan mode.
             if (removedRecord.getScanRequest().getScanMode() == mScanMode) {
-                Log.v(TAG, "DiscoveryProviderManager starts to find the new highest scan mode "
-                        + "because the highest scan mode listener was unregistered.");
+                Log.v(TAG, "DiscoveryProviderManagerLegacy starts to find the new highest "
+                        + "scan mode because the highest scan mode listener was unregistered.");
                 @ScanRequest.ScanMode int highestScanModeRequested = ScanRequest.SCAN_MODE_NO_POWER;
                 // find the next highest scan mode;
                 for (ScanListenerRecord record : mScanTypeScanListenerRecordMap.values()) {
@@ -377,7 +386,7 @@
 
     private void startBleProvider(List<ScanFilter> scanFilters) {
         if (!mBleDiscoveryProvider.getController().isStarted()) {
-            Log.d(TAG, "DiscoveryProviderManager starts Ble scanning.");
+            Log.d(TAG, "DiscoveryProviderManagerLegacy starts BLE scanning.");
             mBleDiscoveryProvider.getController().setListener(this);
             mBleDiscoveryProvider.getController().setProviderScanMode(mScanMode);
             mBleDiscoveryProvider.getController().setProviderScanFilters(scanFilters);
@@ -387,7 +396,7 @@
 
     @VisibleForTesting
     void startChreProvider(List<ScanFilter> scanFilters) {
-        Log.d(TAG, "DiscoveryProviderManager starts CHRE scanning.");
+        Log.d(TAG, "DiscoveryProviderManagerLegacy starts CHRE scanning.");
         mChreDiscoveryProvider.getController().setProviderScanFilters(scanFilters);
         mChreDiscoveryProvider.getController().setProviderScanMode(mScanMode);
         mChreDiscoveryProvider.getController().start();
@@ -503,4 +512,29 @@
             unregisterScanListener(listener);
         }
     }
+
+    /**
+     * Registers Nearby service to Ble scan if Bluetooth is off. (Even when Bluetooth is off)
+     * @return {@code true} when Nearby currently can scan through Bluetooth or Ble or successfully
+     * registers Nearby service to Ble scan when Blutooth is off.
+     */
+    public boolean setBleScanEnabled() {
+        BluetoothAdapter adapter = mInjector.getBluetoothAdapter();
+        if (adapter == null) {
+            Log.e(TAG, "BluetoothAdapter is null.");
+            return false;
+        }
+        if (adapter.isEnabled() || adapter.isLeEnabled()) {
+            return true;
+        }
+        if (!adapter.isBleScanAlwaysAvailable()) {
+            Log.v(TAG, "Ble always on scan is disabled.");
+            return false;
+        }
+        if (!adapter.enableBLE()) {
+            Log.e(TAG, "Failed to register Ble scan.");
+            return false;
+        }
+        return true;
+    }
 }
diff --git a/nearby/service/java/com/android/server/nearby/presence/EncryptionInfo.java b/nearby/service/java/com/android/server/nearby/presence/EncryptionInfo.java
new file mode 100644
index 0000000..ac1e18f
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/presence/EncryptionInfo.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.presence;
+
+import android.annotation.IntDef;
+import android.annotation.Nullable;
+import android.nearby.DataElement;
+
+import androidx.annotation.NonNull;
+
+import com.android.internal.util.Preconditions;
+import com.android.server.nearby.util.ArrayUtils;
+
+import java.util.Arrays;
+
+/**
+ * Data Element that indicates the presence of extended 16 bytes salt instead of 2 byte salt and to
+ * indicate which encoding scheme (MIC tag or Signature) is used for a section. If present, it must
+ * be the first DE in a section.
+ */
+public class EncryptionInfo {
+
+    @IntDef({EncodingScheme.MIC, EncodingScheme.SIGNATURE})
+    public @interface EncodingScheme {
+        int MIC = 0;
+        int SIGNATURE = 1;
+    }
+
+    // 1st byte : encryption scheme
+    // 2nd to 17th bytes: salt
+    public static final int ENCRYPTION_INFO_LENGTH = 17;
+    private static final int ENCODING_SCHEME_MASK = 0b01111000;
+    private static final int ENCODING_SCHEME_OFFSET = 3;
+    @EncodingScheme
+    private final int mEncodingScheme;
+    private final byte[] mSalt;
+
+    /**
+     * Constructs a {@link DataElement}.
+     */
+    public EncryptionInfo(@NonNull byte[] value) {
+        Preconditions.checkArgument(value.length == ENCRYPTION_INFO_LENGTH);
+        mEncodingScheme = (value[0] & ENCODING_SCHEME_MASK) >> ENCODING_SCHEME_OFFSET;
+        Preconditions.checkArgument(isValidEncodingScheme(mEncodingScheme));
+        mSalt = Arrays.copyOfRange(value, 1, ENCRYPTION_INFO_LENGTH);
+    }
+
+    private boolean isValidEncodingScheme(int scheme) {
+        return scheme == EncodingScheme.MIC || scheme == EncodingScheme.SIGNATURE;
+    }
+
+    @EncodingScheme
+    public int getEncodingScheme() {
+        return mEncodingScheme;
+    }
+
+    public byte[] getSalt() {
+        return mSalt;
+    }
+
+    /** Combines the encoding scheme and salt to a byte array
+     * that represents an {@link EncryptionInfo}.
+     */
+    @Nullable
+    public static byte[] toByte(@EncodingScheme int encodingScheme, byte[] salt) {
+        if (ArrayUtils.isEmpty(salt)) {
+            return null;
+        }
+        if (salt.length != ENCRYPTION_INFO_LENGTH - 1) {
+            return null;
+        }
+        byte schemeByte =
+                (byte) ((encodingScheme << ENCODING_SCHEME_OFFSET) & ENCODING_SCHEME_MASK);
+        return ArrayUtils.append(schemeByte, salt);
+    }
+}
diff --git a/nearby/service/java/com/android/server/nearby/presence/ExtendedAdvertisement.java b/nearby/service/java/com/android/server/nearby/presence/ExtendedAdvertisement.java
index 34a7514..c2304cc 100644
--- a/nearby/service/java/com/android/server/nearby/presence/ExtendedAdvertisement.java
+++ b/nearby/service/java/com/android/server/nearby/presence/ExtendedAdvertisement.java
@@ -16,22 +16,27 @@
 
 package com.android.server.nearby.presence;
 
+import static android.nearby.BroadcastRequest.PRESENCE_VERSION_V1;
+
 import static com.android.server.nearby.NearbyService.TAG;
+import static com.android.server.nearby.presence.EncryptionInfo.ENCRYPTION_INFO_LENGTH;
+import static com.android.server.nearby.presence.PresenceConstants.PRESENCE_UUID_BYTES;
 
 import android.annotation.Nullable;
-import android.nearby.BroadcastRequest;
+import android.nearby.BroadcastRequest.BroadcastVersion;
 import android.nearby.DataElement;
+import android.nearby.DataElement.DataType;
 import android.nearby.PresenceBroadcastRequest;
 import android.nearby.PresenceCredential;
 import android.nearby.PublicCredential;
 import android.util.Log;
 
+import com.android.server.nearby.util.ArrayUtils;
 import com.android.server.nearby.util.encryption.Cryptor;
-import com.android.server.nearby.util.encryption.CryptorImpFake;
-import com.android.server.nearby.util.encryption.CryptorImpIdentityV1;
-import com.android.server.nearby.util.encryption.CryptorImpV1;
+import com.android.server.nearby.util.encryption.CryptorMicImp;
 
 import java.nio.ByteBuffer;
+import java.security.GeneralSecurityException;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
@@ -51,37 +56,51 @@
  * The header contains:
  * version (3 bits) | 5 bit reserved for future use (RFU)
  */
-public class ExtendedAdvertisement extends Advertisement{
+public class ExtendedAdvertisement extends Advertisement {
 
     public static final int SALT_DATA_LENGTH = 2;
-
     static final int HEADER_LENGTH = 1;
 
     static final int IDENTITY_DATA_LENGTH = 16;
-
+    // Identity Index is always 2 .
+    // 0 is reserved, 1 is Salt or Credential Element.
+    private static final int CIPHER_START_INDEX = 2;
     private final List<DataElement> mDataElements;
+    private final byte[] mKeySeed;
 
-    private final byte[] mAuthenticityKey;
+    private final byte[] mData;
 
-    // All Data Elements including salt and identity.
-    // Each list item (byte array) is a Data Element (with its header).
-    private final List<byte[]> mCompleteDataElementsBytes;
-    // Signature generated from data elements.
-    private final byte[] mHmacTag;
+    private ExtendedAdvertisement(
+            @PresenceCredential.IdentityType int identityType,
+            byte[] identity,
+            byte[] salt,
+            byte[] keySeed,
+            List<Integer> actions,
+            List<DataElement> dataElements) {
+        this.mVersion = PRESENCE_VERSION_V1;
+        this.mIdentityType = identityType;
+        this.mIdentity = identity;
+        this.mSalt = salt;
+        this.mKeySeed = keySeed;
+        this.mDataElements = dataElements;
+        this.mActions = actions;
+        mData = toBytesInternal();
+    }
 
     /**
      * Creates an {@link ExtendedAdvertisement} from a Presence Broadcast Request.
+     *
      * @return {@link ExtendedAdvertisement} object. {@code null} when the request is illegal.
      */
     @Nullable
     public static ExtendedAdvertisement createFromRequest(PresenceBroadcastRequest request) {
-        if (request.getVersion() != BroadcastRequest.PRESENCE_VERSION_V1) {
+        if (request.getVersion() != PRESENCE_VERSION_V1) {
             Log.v(TAG, "ExtendedAdvertisement only supports V1 now.");
             return null;
         }
 
         byte[] salt = request.getSalt();
-        if (salt.length != SALT_DATA_LENGTH) {
+        if (salt.length != SALT_DATA_LENGTH && salt.length != ENCRYPTION_INFO_LENGTH - 1) {
             Log.v(TAG, "Salt does not match correct length");
             return null;
         }
@@ -94,12 +113,12 @@
         }
 
         List<Integer> actions = request.getActions();
-        if (actions.isEmpty()) {
-            Log.v(TAG, "ExtendedAdvertisement must contain at least one action");
-            return null;
-        }
-
         List<DataElement> dataElements = request.getExtendedProperties();
+        // DataElements should include actions.
+        for (int action : actions) {
+            dataElements.add(
+                    new DataElement(DataType.ACTION, new byte[]{(byte) action}));
+        }
         return new ExtendedAdvertisement(
                 request.getCredential().getIdentityType(),
                 identity,
@@ -109,149 +128,252 @@
                 dataElements);
     }
 
-    /** Serialize an {@link ExtendedAdvertisement} object into bytes with {@link DataElement}s */
-    @Nullable
-    public byte[] toBytes() {
-        ByteBuffer buffer = ByteBuffer.allocate(getLength());
-
-        // Header
-        buffer.put(ExtendedAdvertisementUtils.constructHeader(getVersion()));
-
-        // Salt
-        buffer.put(mCompleteDataElementsBytes.get(0));
-
-        // Identity
-        buffer.put(mCompleteDataElementsBytes.get(1));
-
-        List<Byte> rawDataBytes = new ArrayList<>();
-        // Data Elements (Already includes salt and identity)
-        for (int i = 2; i < mCompleteDataElementsBytes.size(); i++) {
-            byte[] dataElementBytes = mCompleteDataElementsBytes.get(i);
-            for (Byte b : dataElementBytes) {
-                rawDataBytes.add(b);
-            }
-        }
-
-        byte[] dataElements = new byte[rawDataBytes.size()];
-        for (int i = 0; i < rawDataBytes.size(); i++) {
-            dataElements[i] = rawDataBytes.get(i);
-        }
-
-        buffer.put(
-                getCryptor(/* encrypt= */ true).encrypt(dataElements, getSalt(), mAuthenticityKey));
-
-        buffer.put(mHmacTag);
-
-        return buffer.array();
-    }
-
-    /** Deserialize from bytes into an {@link ExtendedAdvertisement} object.
-     * {@code null} when there is something when parsing.
+    /**
+     * Deserialize from bytes into an {@link ExtendedAdvertisement} object.
+     * Return {@code null} when there is an error in parsing.
      */
     @Nullable
-    public static ExtendedAdvertisement fromBytes(byte[] bytes, PublicCredential publicCredential) {
-        @BroadcastRequest.BroadcastVersion
+    public static ExtendedAdvertisement fromBytes(byte[] bytes, PublicCredential sharedCredential) {
+        @BroadcastVersion
         int version = ExtendedAdvertisementUtils.getVersion(bytes);
-        if (version != PresenceBroadcastRequest.PRESENCE_VERSION_V1) {
+        if (version != PRESENCE_VERSION_V1) {
             Log.v(TAG, "ExtendedAdvertisement is used in V1 only and version is " + version);
             return null;
         }
 
-        byte[] authenticityKey = publicCredential.getAuthenticityKey();
-
-        int index = HEADER_LENGTH;
-        // Salt
-        byte[] saltHeaderArray = ExtendedAdvertisementUtils.getDataElementHeader(bytes, index);
-        DataElementHeader saltHeader = DataElementHeader.fromBytes(version, saltHeaderArray);
-        if (saltHeader == null || saltHeader.getDataType() != DataElement.DataType.SALT) {
-            Log.v(TAG, "First data element has to be salt.");
+        byte[] keySeed = sharedCredential.getAuthenticityKey();
+        byte[] metadataEncryptionKeyUnsignedAdvTag = sharedCredential.getEncryptedMetadataKeyTag();
+        if (keySeed == null || metadataEncryptionKeyUnsignedAdvTag == null) {
             return null;
         }
-        index += saltHeaderArray.length;
-        byte[] salt = new byte[saltHeader.getDataLength()];
-        for (int i = 0; i < saltHeader.getDataLength(); i++) {
-            salt[i] = bytes[index++];
-        }
 
-        // Identity
+        int index = 0;
+        // Header
+        byte[] header = new byte[]{bytes[index]};
+        index += HEADER_LENGTH;
+        // Section header
+        byte[] sectionHeader = new byte[]{bytes[index]};
+        index += HEADER_LENGTH;
+        // Salt or Encryption Info
+        byte[] firstHeaderArray = ExtendedAdvertisementUtils.getDataElementHeader(bytes, index);
+        DataElementHeader firstHeader = DataElementHeader.fromBytes(version, firstHeaderArray);
+        if (firstHeader == null) {
+            Log.v(TAG, "Cannot find salt.");
+            return null;
+        }
+        @DataType int firstType = firstHeader.getDataType();
+        if (firstType != DataType.SALT && firstType != DataType.ENCRYPTION_INFO) {
+            Log.v(TAG, "First data element has to be Salt or Encryption Info.");
+            return null;
+        }
+        index += firstHeaderArray.length;
+        byte[] firstDeBytes = new byte[firstHeader.getDataLength()];
+        for (int i = 0; i < firstHeader.getDataLength(); i++) {
+            firstDeBytes[i] = bytes[index++];
+        }
+        byte[] nonce = getNonce(firstType, firstDeBytes);
+        if (nonce == null) {
+            return null;
+        }
+        byte[] saltBytes = firstType == DataType.SALT
+                ? firstDeBytes : (new EncryptionInfo(firstDeBytes)).getSalt();
+
+        // Identity header
         byte[] identityHeaderArray = ExtendedAdvertisementUtils.getDataElementHeader(bytes, index);
         DataElementHeader identityHeader =
                 DataElementHeader.fromBytes(version, identityHeaderArray);
-        if (identityHeader == null) {
-            Log.v(TAG, "The second element has to be identity.");
+        if (identityHeader == null || identityHeader.getDataLength() != IDENTITY_DATA_LENGTH) {
+            Log.v(TAG, "The second element has to be a 16-bytes identity.");
             return null;
         }
         index += identityHeaderArray.length;
         @PresenceCredential.IdentityType int identityType =
                 toPresenceCredentialIdentityType(identityHeader.getDataType());
-        if (identityType == PresenceCredential.IDENTITY_TYPE_UNKNOWN) {
-            Log.v(TAG, "The identity type is unknown.");
+        if (identityType != PresenceCredential.IDENTITY_TYPE_PRIVATE
+                && identityType != PresenceCredential.IDENTITY_TYPE_TRUSTED) {
+            Log.v(TAG, "Only supports encrypted advertisement.");
             return null;
         }
-        byte[] encryptedIdentity = new byte[identityHeader.getDataLength()];
-        for (int i = 0; i < identityHeader.getDataLength(); i++) {
-            encryptedIdentity[i] = bytes[index++];
-        }
-        byte[] identity =
-                CryptorImpIdentityV1
-                        .getInstance().decrypt(encryptedIdentity, salt, authenticityKey);
-
-        Cryptor cryptor = getCryptor(/* encrypt= */ true);
-        byte[] encryptedDataElements =
-                new byte[bytes.length - index - cryptor.getSignatureLength()];
-        // Decrypt other data elements
-        System.arraycopy(bytes, index, encryptedDataElements, 0, encryptedDataElements.length);
-        byte[] decryptedDataElements =
-                cryptor.decrypt(encryptedDataElements, salt, authenticityKey);
-        if (decryptedDataElements == null) {
+        // Ciphertext
+        Cryptor cryptor = CryptorMicImp.getInstance();
+        byte[] ciphertext = new byte[bytes.length - index - cryptor.getSignatureLength()];
+        System.arraycopy(bytes, index, ciphertext, 0, ciphertext.length);
+        byte[] plaintext = cryptor.decrypt(ciphertext, nonce, keySeed);
+        if (plaintext == null) {
             return null;
         }
 
+        // Verification
+        // Verify the computed metadata encryption key tag
+        // First 16 bytes is metadata encryption key data
+        byte[] metadataEncryptionKey = new byte[IDENTITY_DATA_LENGTH];
+        System.arraycopy(plaintext, 0, metadataEncryptionKey, 0, IDENTITY_DATA_LENGTH);
+        // Verify metadata encryption key tag
+        byte[] computedMetadataEncryptionKeyTag =
+                CryptorMicImp.generateMetadataEncryptionKeyTag(metadataEncryptionKey,
+                        keySeed);
+        if (!Arrays.equals(computedMetadataEncryptionKeyTag, metadataEncryptionKeyUnsignedAdvTag)) {
+            Log.w(TAG,
+                    "The calculated metadata encryption key tag is different from the metadata "
+                            + "encryption key unsigned adv tag in the SharedCredential.");
+            return null;
+        }
         // Verify the computed HMAC tag is equal to HMAC tag in advertisement
-        if (cryptor.getSignatureLength() > 0) {
-            byte[] expectedHmacTag = new byte[cryptor.getSignatureLength()];
-            System.arraycopy(
-                    bytes, bytes.length - cryptor.getSignatureLength(),
-                    expectedHmacTag, 0, cryptor.getSignatureLength());
-            if (!cryptor.verify(decryptedDataElements, authenticityKey, expectedHmacTag)) {
-                Log.e(TAG, "HMAC tags not match.");
-                return null;
-            }
+        byte[] expectedHmacTag = new byte[cryptor.getSignatureLength()];
+        System.arraycopy(
+                bytes, bytes.length - cryptor.getSignatureLength(),
+                expectedHmacTag, 0, cryptor.getSignatureLength());
+        byte[] micInput =  ArrayUtils.concatByteArrays(
+                PRESENCE_UUID_BYTES, header, sectionHeader,
+                firstHeaderArray, firstDeBytes,
+                nonce, identityHeaderArray, ciphertext);
+        if (!cryptor.verify(micInput, keySeed, expectedHmacTag)) {
+            Log.e(TAG, "HMAC tag not match.");
+            return null;
         }
 
-        int dataElementArrayIndex = 0;
-        // Other Data Elements
-        List<Integer> actions = new ArrayList<>();
-        List<DataElement> dataElements = new ArrayList<>();
-        while (dataElementArrayIndex < decryptedDataElements.length) {
-            byte[] deHeaderArray = ExtendedAdvertisementUtils
-                    .getDataElementHeader(decryptedDataElements, dataElementArrayIndex);
-            DataElementHeader deHeader = DataElementHeader.fromBytes(version, deHeaderArray);
-            dataElementArrayIndex += deHeaderArray.length;
+        byte[] otherDataElements = new byte[plaintext.length - IDENTITY_DATA_LENGTH];
+        System.arraycopy(plaintext, IDENTITY_DATA_LENGTH,
+                otherDataElements, 0, otherDataElements.length);
+        List<DataElement> dataElements = getDataElementsFromBytes(version, otherDataElements);
+        if (dataElements.isEmpty()) {
+            return null;
+        }
+        List<Integer> actions = getActionsFromDataElements(dataElements);
+        if (actions == null) {
+            return null;
+        }
+        return new ExtendedAdvertisement(identityType, metadataEncryptionKey, saltBytes, keySeed,
+                actions, dataElements);
+    }
 
-            @DataElement.DataType int type = Objects.requireNonNull(deHeader).getDataType();
-            if (type == DataElement.DataType.ACTION) {
-                if (deHeader.getDataLength() != 1) {
-                    Log.v(TAG, "Action id should only 1 byte.");
+    @PresenceCredential.IdentityType
+    private static int toPresenceCredentialIdentityType(@DataType int type) {
+        switch (type) {
+            case DataType.PRIVATE_IDENTITY:
+                return PresenceCredential.IDENTITY_TYPE_PRIVATE;
+            case DataType.PROVISIONED_IDENTITY:
+                return PresenceCredential.IDENTITY_TYPE_PROVISIONED;
+            case DataType.TRUSTED_IDENTITY:
+                return PresenceCredential.IDENTITY_TYPE_TRUSTED;
+            case DataType.PUBLIC_IDENTITY:
+            default:
+                return PresenceCredential.IDENTITY_TYPE_UNKNOWN;
+        }
+    }
+
+    @DataType
+    private static int toDataType(@PresenceCredential.IdentityType int identityType) {
+        switch (identityType) {
+            case PresenceCredential.IDENTITY_TYPE_PRIVATE:
+                return DataType.PRIVATE_IDENTITY;
+            case PresenceCredential.IDENTITY_TYPE_PROVISIONED:
+                return DataType.PROVISIONED_IDENTITY;
+            case PresenceCredential.IDENTITY_TYPE_TRUSTED:
+                return DataType.TRUSTED_IDENTITY;
+            case PresenceCredential.IDENTITY_TYPE_UNKNOWN:
+            default:
+                return DataType.PUBLIC_IDENTITY;
+        }
+    }
+
+    /**
+     * Returns {@code true} if the given {@link DataType} is salt, or one of the
+     * identities. Identities should be able to convert to {@link PresenceCredential.IdentityType}s.
+     */
+    private static boolean isSaltOrIdentity(@DataType int type) {
+        return type == DataType.SALT || type == DataType.ENCRYPTION_INFO
+                || type == DataType.PRIVATE_IDENTITY
+                || type == DataType.TRUSTED_IDENTITY
+                || type == DataType.PROVISIONED_IDENTITY
+                || type == DataType.PUBLIC_IDENTITY;
+    }
+
+    /** Serialize an {@link ExtendedAdvertisement} object into bytes with {@link DataElement}s */
+    @Nullable
+    public byte[] toBytes() {
+        return mData.clone();
+    }
+
+    /** Serialize an {@link ExtendedAdvertisement} object into bytes with {@link DataElement}s */
+    @Nullable
+    public byte[] toBytesInternal() {
+        int sectionLength = 0;
+        // Salt
+        DataElement saltDe;
+        byte[] nonce;
+        try {
+            switch (mSalt.length) {
+                case SALT_DATA_LENGTH:
+                    saltDe = new DataElement(DataType.SALT, mSalt);
+                    nonce = CryptorMicImp.generateAdvNonce(mSalt);
+                    break;
+                case ENCRYPTION_INFO_LENGTH - 1:
+                    saltDe = new DataElement(DataType.ENCRYPTION_INFO,
+                            EncryptionInfo.toByte(EncryptionInfo.EncodingScheme.MIC, mSalt));
+                    nonce = CryptorMicImp.generateAdvNonce(mSalt, CIPHER_START_INDEX);
+                    break;
+                default:
+                    Log.w(TAG, "Invalid salt size.");
                     return null;
-                }
-                actions.add((int) decryptedDataElements[dataElementArrayIndex++]);
-            } else {
-                if (isSaltOrIdentity(type)) {
-                    Log.v(TAG, "Type " + type + " is duplicated. There should be only one salt"
-                            + " and one identity in the advertisement.");
-                    return null;
-                }
-                byte[] deData = new byte[deHeader.getDataLength()];
-                for (int i = 0; i < deHeader.getDataLength(); i++) {
-                    deData[i] = decryptedDataElements[dataElementArrayIndex++];
-                }
-                dataElements.add(new DataElement(type, deData));
             }
+        } catch (GeneralSecurityException e) {
+            Log.w(TAG, "Failed to generate the IV for encryption.", e);
+            return null;
         }
 
-        return new ExtendedAdvertisement(identityType, identity, salt, authenticityKey, actions,
-                dataElements);
+        byte[] saltOrEncryptionInfoBytes =
+                ExtendedAdvertisementUtils.convertDataElementToBytes(saltDe);
+        sectionLength += saltOrEncryptionInfoBytes.length;
+        // 16 bytes encrypted identity
+        @DataType int identityDataType = toDataType(getIdentityType());
+        byte[] identityHeaderBytes = new DataElementHeader(PRESENCE_VERSION_V1,
+                identityDataType, mIdentity.length).toBytes();
+        sectionLength += identityHeaderBytes.length;
+        final List<DataElement> dataElementList = getDataElements();
+        byte[] ciphertext = getCiphertext(nonce, dataElementList);
+        if (ciphertext == null) {
+            return null;
+        }
+        sectionLength += ciphertext.length;
+        // mic
+        sectionLength += CryptorMicImp.MIC_LENGTH;
+        mLength = sectionLength;
+        // header
+        byte header = ExtendedAdvertisementUtils.constructHeader(getVersion());
+        mLength += HEADER_LENGTH;
+        // section header
+        if (sectionLength > 255) {
+            Log.e(TAG, "A section should be shorter than 255 bytes.");
+            return null;
+        }
+        byte sectionHeader = (byte) sectionLength;
+        mLength += HEADER_LENGTH;
+
+        // generates mic
+        ByteBuffer micInputBuffer = ByteBuffer.allocate(
+                mLength + PRESENCE_UUID_BYTES.length + nonce.length - CryptorMicImp.MIC_LENGTH);
+        micInputBuffer.put(PRESENCE_UUID_BYTES);
+        micInputBuffer.put(header);
+        micInputBuffer.put(sectionHeader);
+        micInputBuffer.put(saltOrEncryptionInfoBytes);
+        micInputBuffer.put(nonce);
+        micInputBuffer.put(identityHeaderBytes);
+        micInputBuffer.put(ciphertext);
+        byte[] micInput = micInputBuffer.array();
+        byte[] mic = CryptorMicImp.getInstance().sign(micInput, mKeySeed);
+        if (mic == null) {
+            return null;
+        }
+
+        ByteBuffer buffer = ByteBuffer.allocate(mLength);
+        buffer.put(header);
+        buffer.put(sectionHeader);
+        buffer.put(saltOrEncryptionInfoBytes);
+        buffer.put(identityHeaderBytes);
+        buffer.put(ciphertext);
+        buffer.put(mic);
+        return buffer.array();
     }
 
     /** Returns the {@link DataElement}s in the advertisement. */
@@ -260,7 +382,7 @@
     }
 
     /** Returns the {@link DataElement}s in the advertisement according to the key. */
-    public List<DataElement> getDataElements(@DataElement.DataType int key) {
+    public List<DataElement> getDataElements(@DataType int key) {
         List<DataElement> res = new ArrayList<>();
         for (DataElement dataElement : mDataElements) {
             if (key == dataElement.getKey()) {
@@ -285,125 +407,86 @@
                 getActions());
     }
 
-    ExtendedAdvertisement(
-            @PresenceCredential.IdentityType int identityType,
-            byte[] identity,
-            byte[] salt,
-            byte[] authenticityKey,
-            List<Integer> actions,
-            List<DataElement> dataElements) {
-        this.mVersion = BroadcastRequest.PRESENCE_VERSION_V1;
-        this.mIdentityType = identityType;
-        this.mIdentity = identity;
-        this.mSalt = salt;
-        this.mAuthenticityKey = authenticityKey;
-        this.mActions = actions;
-        this.mDataElements = dataElements;
-        this.mCompleteDataElementsBytes = new ArrayList<>();
+    @Nullable
+    private byte[] getCiphertext(byte[] nonce, List<DataElement> dataElements) {
+        Cryptor cryptor = CryptorMicImp.getInstance();
+        byte[] rawDeBytes = mIdentity;
+        for (DataElement dataElement : dataElements) {
+            rawDeBytes = ArrayUtils.concatByteArrays(rawDeBytes,
+                    ExtendedAdvertisementUtils.convertDataElementToBytes(dataElement));
+        }
+        return cryptor.encrypt(rawDeBytes, nonce, mKeySeed);
+    }
 
-        int length = HEADER_LENGTH; // header
+    private static List<DataElement> getDataElementsFromBytes(
+            @BroadcastVersion int version, byte[] bytes) {
+        List<DataElement> res = new ArrayList<>();
+        if (ArrayUtils.isEmpty(bytes)) {
+            return res;
+        }
+        int index = 0;
+        while (index < bytes.length) {
+            byte[] deHeaderArray = ExtendedAdvertisementUtils
+                    .getDataElementHeader(bytes, index);
+            DataElementHeader deHeader = DataElementHeader.fromBytes(version, deHeaderArray);
+            index += deHeaderArray.length;
+            @DataType int type = Objects.requireNonNull(deHeader).getDataType();
+            if (isSaltOrIdentity(type)) {
+                Log.v(TAG, "Type " + type + " is duplicated. There should be only one salt"
+                        + " and one identity in the advertisement.");
+                return new ArrayList<>();
+            }
+            byte[] deData = new byte[deHeader.getDataLength()];
+            for (int i = 0; i < deHeader.getDataLength(); i++) {
+                deData[i] = bytes[index++];
+            }
+            res.add(new DataElement(type, deData));
+        }
+        return res;
+    }
 
-        // Salt
-        DataElement saltElement = new DataElement(DataElement.DataType.SALT, salt);
-        byte[] saltByteArray = ExtendedAdvertisementUtils.convertDataElementToBytes(saltElement);
-        mCompleteDataElementsBytes.add(saltByteArray);
-        length += saltByteArray.length;
+    @Nullable
+    private static byte[] getNonce(@DataType int type, byte[] data) {
+        try {
+            if (type == DataType.SALT) {
+                if (data.length != SALT_DATA_LENGTH) {
+                    Log.v(TAG, "Salt DataElement needs to be 2 bytes.");
+                    return null;
+                }
+                return CryptorMicImp.generateAdvNonce(data);
+            } else if (type == DataType.ENCRYPTION_INFO) {
+                try {
+                    EncryptionInfo info = new EncryptionInfo(data);
+                    if (info.getEncodingScheme() != EncryptionInfo.EncodingScheme.MIC) {
+                        Log.v(TAG, "Not support Signature yet.");
+                        return null;
+                    }
+                    return CryptorMicImp.generateAdvNonce(info.getSalt(), CIPHER_START_INDEX);
+                } catch (IllegalArgumentException e) {
+                    Log.w(TAG, "Salt DataElement needs to be 17 bytes.", e);
+                    return null;
+                }
+            }
+        }  catch (GeneralSecurityException e) {
+            Log.w(TAG, "Failed to decrypt metadata encryption key.", e);
+            return null;
+        }
+        return null;
+    }
 
-        // Identity
-        byte[] encryptedIdentity =
-                CryptorImpIdentityV1.getInstance().encrypt(identity, salt, authenticityKey);
-        DataElement identityElement = new DataElement(toDataType(identityType), encryptedIdentity);
-        byte[] identityByteArray =
-                ExtendedAdvertisementUtils.convertDataElementToBytes(identityElement);
-        mCompleteDataElementsBytes.add(identityByteArray);
-        length += identityByteArray.length;
-
-        List<Byte> dataElementBytes = new ArrayList<>();
-        // Intents
-        for (int action : mActions) {
-            DataElement actionElement = new DataElement(DataElement.DataType.ACTION,
-                    new byte[] {(byte) action});
-            byte[] intentByteArray =
-                    ExtendedAdvertisementUtils.convertDataElementToBytes(actionElement);
-            mCompleteDataElementsBytes.add(intentByteArray);
-            for (Byte b : intentByteArray) {
-                dataElementBytes.add(b);
+    @Nullable
+    private static List<Integer> getActionsFromDataElements(List<DataElement> dataElements) {
+        List<Integer> actions = new ArrayList<>();
+        for (DataElement dataElement : dataElements) {
+            if (dataElement.getKey() == DataElement.DataType.ACTION) {
+                byte[] value = dataElement.getValue();
+                if (value.length != 1) {
+                    Log.w(TAG, "Action should be only 1 byte.");
+                    return null;
+                }
+                actions.add(Byte.toUnsignedInt(value[0]));
             }
         }
-
-        // Data Elements (Extended properties)
-        for (DataElement dataElement : mDataElements) {
-            byte[] deByteArray = ExtendedAdvertisementUtils.convertDataElementToBytes(dataElement);
-            mCompleteDataElementsBytes.add(deByteArray);
-            for (Byte b : deByteArray) {
-                dataElementBytes.add(b);
-            }
-        }
-
-        byte[] data = new byte[dataElementBytes.size()];
-        for (int i = 0; i < dataElementBytes.size(); i++) {
-            data[i] = dataElementBytes.get(i);
-        }
-        Cryptor cryptor = getCryptor(/* encrypt= */ true);
-        byte[] encryptedDeBytes = cryptor.encrypt(data, salt, authenticityKey);
-
-        length += encryptedDeBytes.length;
-
-        // Signature
-        byte[] hmacTag = Objects.requireNonNull(cryptor.sign(data, authenticityKey));
-        mHmacTag = hmacTag;
-        length += hmacTag.length;
-
-        this.mLength = length;
-    }
-
-    @PresenceCredential.IdentityType
-    private static int toPresenceCredentialIdentityType(@DataElement.DataType int type) {
-        switch (type) {
-            case DataElement.DataType.PRIVATE_IDENTITY:
-                return PresenceCredential.IDENTITY_TYPE_PRIVATE;
-            case DataElement.DataType.PROVISIONED_IDENTITY:
-                return PresenceCredential.IDENTITY_TYPE_PROVISIONED;
-            case DataElement.DataType.TRUSTED_IDENTITY:
-                return PresenceCredential.IDENTITY_TYPE_TRUSTED;
-            case DataElement.DataType.PUBLIC_IDENTITY:
-            default:
-                return PresenceCredential.IDENTITY_TYPE_UNKNOWN;
-        }
-    }
-
-    @DataElement.DataType
-    private static int toDataType(@PresenceCredential.IdentityType int identityType) {
-        switch (identityType) {
-            case PresenceCredential.IDENTITY_TYPE_PRIVATE:
-                return DataElement.DataType.PRIVATE_IDENTITY;
-            case PresenceCredential.IDENTITY_TYPE_PROVISIONED:
-                return DataElement.DataType.PROVISIONED_IDENTITY;
-            case PresenceCredential.IDENTITY_TYPE_TRUSTED:
-                return DataElement.DataType.TRUSTED_IDENTITY;
-            case PresenceCredential.IDENTITY_TYPE_UNKNOWN:
-            default:
-                return DataElement.DataType.PUBLIC_IDENTITY;
-        }
-    }
-
-    /**
-     * Returns {@code true} if the given {@link DataElement.DataType} is salt, or one of the
-     * identities. Identities should be able to convert to {@link PresenceCredential.IdentityType}s.
-     */
-    private static boolean isSaltOrIdentity(@DataElement.DataType int type) {
-        return type == DataElement.DataType.SALT || type == DataElement.DataType.PRIVATE_IDENTITY
-                || type == DataElement.DataType.TRUSTED_IDENTITY
-                || type == DataElement.DataType.PROVISIONED_IDENTITY
-                || type == DataElement.DataType.PUBLIC_IDENTITY;
-    }
-
-    private static Cryptor getCryptor(boolean encrypt) {
-        if (encrypt) {
-            Log.d(TAG, "get V1 Cryptor");
-            return CryptorImpV1.getInstance();
-        }
-        Log.d(TAG, "get fake Cryptor");
-        return CryptorImpFake.getInstance();
+        return actions;
     }
 }
diff --git a/nearby/service/java/com/android/server/nearby/presence/PresenceConstants.java b/nearby/service/java/com/android/server/nearby/presence/PresenceConstants.java
index 39bc60b..50dada2 100644
--- a/nearby/service/java/com/android/server/nearby/presence/PresenceConstants.java
+++ b/nearby/service/java/com/android/server/nearby/presence/PresenceConstants.java
@@ -18,10 +18,14 @@
 
 import android.os.ParcelUuid;
 
+import com.android.server.nearby.util.ArrayUtils;
+
 /**
  * Constants for Nearby Presence operations.
  */
 public class PresenceConstants {
+    /** The Presence UUID value in byte array format. */
+    public static final byte[] PRESENCE_UUID_BYTES = ArrayUtils.intToByteArray(0xFCF1);
 
     /** Presence advertisement service data uuid. */
     public static final ParcelUuid PRESENCE_UUID =
diff --git a/nearby/service/java/com/android/server/nearby/provider/BleBroadcastProvider.java b/nearby/service/java/com/android/server/nearby/provider/BleBroadcastProvider.java
index 6829fba..66ae79c 100644
--- a/nearby/service/java/com/android/server/nearby/provider/BleBroadcastProvider.java
+++ b/nearby/service/java/com/android/server/nearby/provider/BleBroadcastProvider.java
@@ -85,6 +85,7 @@
                         case BroadcastRequest.PRESENCE_VERSION_V0:
                             bluetoothLeAdvertiser.startAdvertising(getAdvertiseSettings(),
                                     advertiseData, this);
+                            Log.v(TAG, "Start to broadcast V0 advertisement.");
                             break;
                         case BroadcastRequest.PRESENCE_VERSION_V1:
                             if (adapter.isLeExtendedAdvertisingSupported()) {
@@ -92,6 +93,7 @@
                                         getAdvertisingSetParameters(),
                                         advertiseData,
                                         null, null, null, mAdvertisingSetCallback);
+                                Log.v(TAG, "Start to broadcast V1 advertisement.");
                             } else {
                                 Log.w(TAG, "Failed to start advertising set because the chipset"
                                         + " does not supports LE Extended Advertising feature.");
@@ -103,7 +105,8 @@
                                     + " is wrong.");
                             advertiseStarted = false;
                     }
-                } catch (NullPointerException | IllegalStateException | SecurityException e) {
+                } catch (NullPointerException | IllegalStateException | SecurityException
+                    | IllegalArgumentException e) {
                     Log.w(TAG, "Failed to start advertising.", e);
                     advertiseStarted = false;
                 }
diff --git a/nearby/service/java/com/android/server/nearby/provider/BleDiscoveryProvider.java b/nearby/service/java/com/android/server/nearby/provider/BleDiscoveryProvider.java
index 355f7cf..e4651b7 100644
--- a/nearby/service/java/com/android/server/nearby/provider/BleDiscoveryProvider.java
+++ b/nearby/service/java/com/android/server/nearby/provider/BleDiscoveryProvider.java
@@ -45,7 +45,6 @@
 import com.android.server.nearby.presence.ExtendedAdvertisement;
 import com.android.server.nearby.util.ArrayUtils;
 import com.android.server.nearby.util.ForegroundThread;
-import com.android.server.nearby.util.encryption.CryptorImpIdentityV1;
 
 import com.google.common.annotations.VisibleForTesting;
 
@@ -69,15 +68,6 @@
     @GuardedBy("mLock")
     @Nullable
     private List<android.nearby.ScanFilter> mScanFilters;
-    private android.bluetooth.le.ScanCallback mScanCallbackLegacy =
-            new android.bluetooth.le.ScanCallback() {
-                @Override
-                public void onScanResult(int callbackType, ScanResult scanResult) {
-                }
-                @Override
-                public void onScanFailed(int errorCode) {
-                }
-            };
     private android.bluetooth.le.ScanCallback mScanCallback =
             new android.bluetooth.le.ScanCallback() {
                 @Override
@@ -133,10 +123,6 @@
                         .addMedium(NearbyDevice.Medium.BLE)
                         .setName(deviceName)
                         .setRssi(rssi);
-        for (int i : advertisement.getActions()) {
-            builder.addExtendedProperty(new DataElement(DataElement.DataType.ACTION,
-                    new byte[]{(byte) i}));
-        }
         for (DataElement dataElement : advertisement.getDataElements()) {
             builder.addExtendedProperty(dataElement);
         }
@@ -175,7 +161,6 @@
         if (isBleAvailable()) {
             Log.d(TAG, "BleDiscoveryProvider started");
             startScan(getScanFilters(), getScanSettings(/* legacy= */ false), mScanCallback);
-            startScan(getScanFilters(), getScanSettings(/* legacy= */ true), mScanCallbackLegacy);
             return;
         }
         Log.w(TAG, "Cannot start BleDiscoveryProvider because Ble is not available.");
@@ -192,7 +177,6 @@
         }
         Log.v(TAG, "Ble scan stopped.");
         bluetoothLeScanner.stopScan(mScanCallback);
-        bluetoothLeScanner.stopScan(mScanCallbackLegacy);
         synchronized (mLock) {
             if (mScanFilters != null) {
                 mScanFilters = null;
@@ -284,17 +268,13 @@
                         if (advertisement == null) {
                             continue;
                         }
-                        if (CryptorImpIdentityV1.getInstance().verify(
-                                advertisement.getIdentity(),
-                                credential.getEncryptedMetadataKeyTag())) {
-                            builder.setPresenceDevice(getPresenceDevice(advertisement, deviceName,
-                                    rssi));
-                            builder.setEncryptionKeyTag(credential.getEncryptedMetadataKeyTag());
-                            if (!ArrayUtils.isEmpty(credential.getSecretId())) {
-                                builder.setDeviceId(Arrays.hashCode(credential.getSecretId()));
-                            }
-                            return;
+                        builder.setPresenceDevice(getPresenceDevice(advertisement, deviceName,
+                                rssi));
+                        builder.setEncryptionKeyTag(credential.getEncryptedMetadataKeyTag());
+                        if (!ArrayUtils.isEmpty(credential.getSecretId())) {
+                            builder.setDeviceId(Arrays.hashCode(credential.getSecretId()));
                         }
+                        return;
                     }
                 }
             }
diff --git a/nearby/service/java/com/android/server/nearby/provider/ChreDiscoveryProvider.java b/nearby/service/java/com/android/server/nearby/provider/ChreDiscoveryProvider.java
index 7ab0523..21ec252 100644
--- a/nearby/service/java/com/android/server/nearby/provider/ChreDiscoveryProvider.java
+++ b/nearby/service/java/com/android/server/nearby/provider/ChreDiscoveryProvider.java
@@ -20,6 +20,9 @@
 
 import static com.android.server.nearby.NearbyService.TAG;
 
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static java.util.concurrent.TimeUnit.NANOSECONDS;
+
 import android.annotation.Nullable;
 import android.content.BroadcastReceiver;
 import android.content.Context;
@@ -132,7 +135,6 @@
     protected void onSetScanFilters(List<ScanFilter> filters) {
         synchronized (mLock) {
             mScanFilters = filters == null ? null : List.copyOf(filters);
-            updateFiltersLocked();
         }
     }
 
@@ -156,7 +158,7 @@
             builder.setFastPairSupported(version != ChreCommunication.INVALID_NANO_APP_VERSION);
             try {
                 callback.onQueryComplete(builder.build());
-            } catch (RemoteException e) {
+            } catch (RemoteException | NullPointerException e) {
                 e.printStackTrace();
             }
         });
@@ -351,7 +353,11 @@
                                             DataElement.DataType.ACTION,
                                             new byte[]{(byte) filterResult.getIntent()}));
                         }
-
+                        if (filterResult.hasTimestampNs()) {
+                            presenceDeviceBuilder
+                                    .setDiscoveryTimestampMillis(MILLISECONDS.convert(
+                                            filterResult.getTimestampNs(), NANOSECONDS));
+                        }
                         PublicCredential publicCredential =
                                 new PublicCredential.Builder(
                                         secretId,
diff --git a/nearby/service/java/com/android/server/nearby/util/ArrayUtils.java b/nearby/service/java/com/android/server/nearby/util/ArrayUtils.java
index 35251d8..e69d004 100644
--- a/nearby/service/java/com/android/server/nearby/util/ArrayUtils.java
+++ b/nearby/service/java/com/android/server/nearby/util/ArrayUtils.java
@@ -22,6 +22,10 @@
  * ArrayUtils class that help manipulate array.
  */
 public class ArrayUtils {
+    private static final char[] HEX_UPPERCASE = {
+            '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'
+    };
+
     /** Concatenate N arrays of bytes into a single array. */
     public static byte[] concatByteArrays(byte[]... arrays) {
         // Degenerate case - no input provided.
@@ -52,4 +56,67 @@
     public static boolean isEmpty(byte[] bytes) {
         return bytes == null || bytes.length == 0;
     }
+
+    /** Appends a byte array to a byte. */
+    public static byte[] append(byte a, byte[] b) {
+        if (b == null) {
+            return new byte[]{a};
+        }
+
+        int length = b.length;
+        byte[] result = new byte[length + 1];
+        result[0] = a;
+        System.arraycopy(b, 0, result, 1, length);
+        return result;
+    }
+
+    /**
+     * Converts an Integer to a 2-byte array.
+     */
+    public static byte[] intToByteArray(int value) {
+        return new byte[] {(byte) (value >> 8), (byte) value};
+    }
+
+    /** Appends a byte to a byte array. */
+    public static byte[] append(byte[] a, byte b) {
+        if (a == null) {
+            return new byte[]{b};
+        }
+
+        int length = a.length;
+        byte[] result = new byte[length + 1];
+        System.arraycopy(a, 0, result, 0, length);
+        result[length] = b;
+        return result;
+    }
+
+    /** Convert an hex string to a byte array. */
+
+    public static byte[] stringToBytes(String hex) throws IllegalArgumentException {
+        int length = hex.length();
+        if (length % 2 != 0) {
+            throw new IllegalArgumentException("Hex string has odd number of characters");
+        }
+        byte[] out = new byte[length / 2];
+        for (int i = 0; i < length; i += 2) {
+            // Byte.parseByte() doesn't work here because it expects a hex value in -128, 127, and
+            // our hex values are in 0, 255.
+            out[i / 2] = (byte) Integer.parseInt(hex.substring(i, i + 2), 16);
+        }
+        return out;
+    }
+
+    /** Encodes a byte array as a hexadecimal representation of bytes. */
+    public static String bytesToStringUppercase(byte[] bytes) {
+        int length = bytes.length;
+        StringBuilder out = new StringBuilder(length * 2);
+        for (int i = 0; i < length; i++) {
+            if (i == length - 1 && (bytes[i] & 0xff) == 0) {
+                break;
+            }
+            out.append(HEX_UPPERCASE[(bytes[i] & 0xf0) >>> 4]);
+            out.append(HEX_UPPERCASE[bytes[i] & 0x0f]);
+        }
+        return out.toString();
+    }
 }
diff --git a/nearby/service/java/com/android/server/nearby/util/encryption/Cryptor.java b/nearby/service/java/com/android/server/nearby/util/encryption/Cryptor.java
index 3c5132d..ba9ca41 100644
--- a/nearby/service/java/com/android/server/nearby/util/encryption/Cryptor.java
+++ b/nearby/service/java/com/android/server/nearby/util/encryption/Cryptor.java
@@ -22,6 +22,10 @@
 
 import androidx.annotation.Nullable;
 
+import com.google.common.hash.Hashing;
+
+import java.nio.charset.StandardCharsets;
+import java.security.GeneralSecurityException;
 import java.security.InvalidKeyException;
 import java.security.NoSuchAlgorithmException;
 
@@ -30,22 +34,28 @@
 
 /** Class for encryption/decryption functionality. */
 public abstract class Cryptor {
+    /**
+     * In the form of "algorithm/mode/padding". Must be the same across broadcast and scan devices.
+     */
+    public static final String CIPHER_ALGORITHM = "AES/CTR/NoPadding";
+
+    public static final byte[] NP_HKDF_SALT = "Google Nearby".getBytes(StandardCharsets.US_ASCII);
 
     /** AES only supports key sizes of 16, 24 or 32 bytes. */
     static final int AUTHENTICITY_KEY_BYTE_SIZE = 16;
 
-    private static final String HMAC_SHA256_ALGORITHM = "HmacSHA256";
+    public static final String HMAC_SHA256_ALGORITHM = "HmacSHA256";
 
     /**
      * Encrypt the provided data blob.
      *
      * @param data data blob to be encrypted.
-     * @param salt used for IV
+     * @param iv advertisement nonce
      * @param secretKeyBytes secrete key accessed from credentials
      * @return encrypted data, {@code null} if failed to encrypt.
      */
     @Nullable
-    public byte[] encrypt(byte[] data, byte[] salt, byte[] secretKeyBytes) {
+    public byte[] encrypt(byte[] data, byte[] iv, byte[] secretKeyBytes) {
         return data;
     }
 
@@ -53,12 +63,12 @@
      * Decrypt the original data blob from the provided byte array.
      *
      * @param encryptedData data blob to be decrypted.
-     * @param salt used for IV
+     * @param iv advertisement nonce
      * @param secretKeyBytes secrete key accessed from credentials
      * @return decrypted data, {@code null} if failed to decrypt.
      */
     @Nullable
-    public byte[] decrypt(byte[] encryptedData, byte[] salt, byte[] secretKeyBytes) {
+    public byte[] decrypt(byte[] encryptedData, byte[] iv, byte[] secretKeyBytes) {
         return encryptedData;
     }
 
@@ -90,7 +100,7 @@
      * A HAMC sha256 based HKDF algorithm to pseudo randomly hash data and salt into a byte array of
      * given size.
      */
-    // Based on google3/third_party/tink/java/src/main/java/com/google/crypto/tink/subtle/Hkdf.java
+    // Based on crypto/tink/subtle/Hkdf.java
     @Nullable
     static byte[] computeHkdf(byte[] ikm, byte[] salt, int size) {
         Mac mac;
@@ -146,4 +156,66 @@
 
         return result;
     }
+
+    /**
+     * Computes an HKDF.
+     *
+     * @param macAlgorithm the MAC algorithm used for computing the Hkdf. I.e., "HMACSHA1" or
+     *                     "HMACSHA256".
+     * @param ikm          the input keying material.
+     * @param salt         optional salt. A possibly non-secret random value.
+     *                     (If no salt is provided i.e. if salt has length 0)
+     *                     then an array of 0s of the same size as the hash
+     *                     digest is used as salt.
+     * @param info         optional context and application specific information.
+     * @param size         The length of the generated pseudorandom string in bytes.
+     * @throws GeneralSecurityException if the {@code macAlgorithm} is not supported or if {@code
+     *                                  size} is too large or if {@code salt} is not a valid key for
+     *                                  macAlgorithm (which should not
+     *                                  happen since HMAC allows key sizes up to 2^64).
+     */
+    public static byte[] computeHkdf(
+            String macAlgorithm, final byte[] ikm, final byte[] salt, final byte[] info, int size)
+            throws GeneralSecurityException {
+        Mac mac = Mac.getInstance(macAlgorithm);
+        if (size > 255 * mac.getMacLength()) {
+            throw new GeneralSecurityException("size too large");
+        }
+        if (salt == null || salt.length == 0) {
+            // According to RFC 5869, Section 2.2 the salt is optional. If no salt is provided
+            // then HKDF uses a salt that is an array of zeros of the same length as the hash
+            // digest.
+            mac.init(new SecretKeySpec(new byte[mac.getMacLength()], macAlgorithm));
+        } else {
+            mac.init(new SecretKeySpec(salt, macAlgorithm));
+        }
+        byte[] prk = mac.doFinal(ikm);
+        byte[] result = new byte[size];
+        int ctr = 1;
+        int pos = 0;
+        mac.init(new SecretKeySpec(prk, macAlgorithm));
+        byte[] digest = new byte[0];
+        while (true) {
+            mac.update(digest);
+            mac.update(info);
+            mac.update((byte) ctr);
+            digest = mac.doFinal();
+            if (pos + digest.length < size) {
+                System.arraycopy(digest, 0, result, pos, digest.length);
+                pos += digest.length;
+                ctr++;
+            } else {
+                System.arraycopy(digest, 0, result, pos, size - pos);
+                break;
+            }
+        }
+        return result;
+    }
+
+    /** Generates the HMAC bytes. */
+    public static byte[] generateHmac(String algorithm, byte[] input, byte[] key) {
+        return Hashing.hmacSha256(new SecretKeySpec(key, algorithm))
+                .hashBytes(input)
+                .asBytes();
+    }
 }
diff --git a/nearby/service/java/com/android/server/nearby/util/encryption/CryptorImpFake.java b/nearby/service/java/com/android/server/nearby/util/encryption/CryptorImpFake.java
deleted file mode 100644
index 1c0ec9e..0000000
--- a/nearby/service/java/com/android/server/nearby/util/encryption/CryptorImpFake.java
+++ /dev/null
@@ -1,39 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.nearby.util.encryption;
-
-import androidx.annotation.Nullable;
-
-/**
- * A Cryptor that returns the original data without actual encryption
- */
-public class CryptorImpFake extends Cryptor {
-    // Lazily instantiated when {@link #getInstance()} is called.
-    @Nullable
-    private static CryptorImpFake sCryptor;
-
-    /** Returns an instance of CryptorImpFake. */
-    public static CryptorImpFake getInstance() {
-        if (sCryptor == null) {
-            sCryptor = new CryptorImpFake();
-        }
-        return sCryptor;
-    }
-
-    private CryptorImpFake() {
-    }
-}
diff --git a/nearby/service/java/com/android/server/nearby/util/encryption/CryptorImpIdentityV1.java b/nearby/service/java/com/android/server/nearby/util/encryption/CryptorImpIdentityV1.java
deleted file mode 100644
index b0e19b4..0000000
--- a/nearby/service/java/com/android/server/nearby/util/encryption/CryptorImpIdentityV1.java
+++ /dev/null
@@ -1,208 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.nearby.util.encryption;
-
-import static com.android.server.nearby.NearbyService.TAG;
-
-import android.security.keystore.KeyProperties;
-import android.util.Log;
-
-import androidx.annotation.Nullable;
-
-import com.android.internal.annotations.VisibleForTesting;
-
-import java.security.InvalidAlgorithmParameterException;
-import java.security.InvalidKeyException;
-import java.security.NoSuchAlgorithmException;
-import java.util.Arrays;
-
-import javax.crypto.BadPaddingException;
-import javax.crypto.Cipher;
-import javax.crypto.IllegalBlockSizeException;
-import javax.crypto.NoSuchPaddingException;
-import javax.crypto.SecretKey;
-import javax.crypto.spec.IvParameterSpec;
-import javax.crypto.spec.SecretKeySpec;
-
-/**
- * {@link android.nearby.BroadcastRequest#PRESENCE_VERSION_V1} for identity
- * encryption and decryption.
- */
-public class CryptorImpIdentityV1 extends Cryptor {
-
-    // 3 16 byte arrays known by both the encryptor and decryptor.
-    private static final byte[] EK_IV =
-            new byte[] {14, -123, -39, 42, 109, 127, 83, 27, 27, 11, 91, -38, 92, 17, -84, 66};
-    private static final byte[] ESALT_IV =
-            new byte[] {46, 83, -19, 10, -127, -31, -31, 12, 31, 76, 63, -9, 33, -66, 15, -10};
-    private static final byte[] KTAG_IV =
-            {-22, -83, -6, 67, 16, -99, -13, -9, 8, -3, -16, 37, -75, 47, 1, -56};
-
-    /** Length of encryption key required by AES/GCM encryption. */
-    private static final int ENCRYPTION_KEY_SIZE = 32;
-
-    /** Length of salt required by AES/GCM encryption. */
-    private static final int AES_CTR_IV_SIZE = 16;
-
-    /** Length HMAC tag */
-    public static final int HMAC_TAG_SIZE = 8;
-
-    /**
-     * In the form of "algorithm/mode/padding". Must be the same across broadcast and scan devices.
-     */
-    private static final String CIPHER_ALGORITHM = "AES/CTR/NoPadding";
-
-    @VisibleForTesting
-    static final String ENCRYPT_ALGORITHM = KeyProperties.KEY_ALGORITHM_AES;
-
-    // Lazily instantiated when {@link #getInstance()} is called.
-    @Nullable private static CryptorImpIdentityV1 sCryptor;
-
-    /** Returns an instance of CryptorImpIdentityV1. */
-    public static CryptorImpIdentityV1 getInstance() {
-        if (sCryptor == null) {
-            sCryptor = new CryptorImpIdentityV1();
-        }
-        return sCryptor;
-    }
-
-    @Nullable
-    @Override
-    public byte[] encrypt(byte[] data, byte[] salt, byte[] authenticityKey) {
-        if (authenticityKey.length != AUTHENTICITY_KEY_BYTE_SIZE) {
-            Log.w(TAG, "Illegal authenticity key size");
-            return null;
-        }
-
-        // Generates a 32 bytes encryption key from authenticity_key
-        byte[] encryptionKey = Cryptor.computeHkdf(authenticityKey, EK_IV, ENCRYPTION_KEY_SIZE);
-        if (encryptionKey == null) {
-            Log.e(TAG, "Failed to generate encryption key.");
-            return null;
-        }
-
-        // Encrypts the data using the encryption key
-        SecretKey secretKey = new SecretKeySpec(encryptionKey, ENCRYPT_ALGORITHM);
-        Cipher cipher;
-        try {
-            cipher = Cipher.getInstance(CIPHER_ALGORITHM);
-        } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
-            Log.e(TAG, "Failed to encrypt with secret key.", e);
-            return null;
-        }
-        byte[] esalt = Cryptor.computeHkdf(salt, ESALT_IV, AES_CTR_IV_SIZE);
-        if (esalt == null) {
-            Log.e(TAG, "Failed to generate salt.");
-            return null;
-        }
-        try {
-            cipher.init(Cipher.ENCRYPT_MODE, secretKey, new IvParameterSpec(esalt));
-        } catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
-            Log.e(TAG, "Failed to initialize cipher.", e);
-            return null;
-        }
-        try {
-            return cipher.doFinal(data);
-        } catch (IllegalBlockSizeException | BadPaddingException e) {
-            Log.e(TAG, "Failed to encrypt with secret key.", e);
-            return null;
-        }
-    }
-
-    @Nullable
-    @Override
-    public byte[] decrypt(byte[] encryptedData, byte[] salt, byte[] authenticityKey) {
-        if (authenticityKey.length != AUTHENTICITY_KEY_BYTE_SIZE) {
-            Log.w(TAG, "Illegal authenticity key size");
-            return null;
-        }
-
-        // Generates a 32 bytes encryption key from authenticity_key
-        byte[] encryptionKey = Cryptor.computeHkdf(authenticityKey, EK_IV, ENCRYPTION_KEY_SIZE);
-        if (encryptionKey == null) {
-            Log.e(TAG, "Failed to generate encryption key.");
-            return null;
-        }
-
-        // Decrypts the data using the encryption key
-        SecretKey secretKey = new SecretKeySpec(encryptionKey, ENCRYPT_ALGORITHM);
-        Cipher cipher;
-        try {
-            cipher = Cipher.getInstance(CIPHER_ALGORITHM);
-        } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
-            Log.e(TAG, "Failed to get cipher instance.", e);
-            return null;
-        }
-        byte[] esalt = Cryptor.computeHkdf(salt, ESALT_IV, AES_CTR_IV_SIZE);
-        if (esalt == null) {
-            return null;
-        }
-        try {
-            cipher.init(Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(esalt));
-        } catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
-            Log.e(TAG, "Failed to initialize cipher.", e);
-            return null;
-        }
-
-        try {
-            return cipher.doFinal(encryptedData);
-        } catch (IllegalBlockSizeException | BadPaddingException e) {
-            Log.e(TAG, "Failed to decrypt bytes with secret key.", e);
-            return null;
-        }
-    }
-
-    /**
-     * Generates a digital signature for the data.
-     *
-     * @return signature {@code null} if failed to sign
-     */
-    @Nullable
-    @Override
-    public byte[] sign(byte[] data, byte[] salt) {
-        if (data == null) {
-            Log.e(TAG, "Not generate HMAC tag because of invalid data input.");
-            return null;
-        }
-
-        // Generates a 8 bytes HMAC tag
-        return Cryptor.computeHkdf(data, salt, HMAC_TAG_SIZE);
-    }
-
-    /**
-     * Generates a digital signature for the data.
-     * Uses KTAG_IV as salt value.
-     */
-    @Nullable
-    public byte[] sign(byte[] data) {
-        // Generates a 8 bytes HMAC tag
-        return sign(data, KTAG_IV);
-    }
-
-    @Override
-    public boolean verify(byte[] data, byte[] key, byte[] signature) {
-        return Arrays.equals(sign(data, key), signature);
-    }
-
-    /**
-     * Verifies the signature generated by data and key, with the original signed data. Uses
-     * KTAG_IV as salt value.
-     */
-    public boolean verify(byte[] data, byte[] signature) {
-        return verify(data, KTAG_IV, signature);
-    }
-}
diff --git a/nearby/service/java/com/android/server/nearby/util/encryption/CryptorImpV1.java b/nearby/service/java/com/android/server/nearby/util/encryption/CryptorImpV1.java
deleted file mode 100644
index 15073fb..0000000
--- a/nearby/service/java/com/android/server/nearby/util/encryption/CryptorImpV1.java
+++ /dev/null
@@ -1,212 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.nearby.util.encryption;
-
-import static com.android.server.nearby.NearbyService.TAG;
-
-import android.security.keystore.KeyProperties;
-import android.util.Log;
-
-import androidx.annotation.Nullable;
-
-import com.android.internal.annotations.VisibleForTesting;
-
-import java.security.InvalidAlgorithmParameterException;
-import java.security.InvalidKeyException;
-import java.security.NoSuchAlgorithmException;
-import java.util.Arrays;
-
-import javax.crypto.BadPaddingException;
-import javax.crypto.Cipher;
-import javax.crypto.IllegalBlockSizeException;
-import javax.crypto.NoSuchPaddingException;
-import javax.crypto.SecretKey;
-import javax.crypto.spec.IvParameterSpec;
-import javax.crypto.spec.SecretKeySpec;
-
-/**
- * {@link android.nearby.BroadcastRequest#PRESENCE_VERSION_V1} for encryption and decryption.
- */
-public class CryptorImpV1 extends Cryptor {
-
-    /**
-     * In the form of "algorithm/mode/padding". Must be the same across broadcast and scan devices.
-     */
-    private static final String CIPHER_ALGORITHM = "AES/CTR/NoPadding";
-
-    @VisibleForTesting
-    static final String ENCRYPT_ALGORITHM = KeyProperties.KEY_ALGORITHM_AES;
-
-    /** Length of encryption key required by AES/GCM encryption. */
-    private static final int ENCRYPTION_KEY_SIZE = 32;
-
-    /** Length of salt required by AES/GCM encryption. */
-    private static final int AES_CTR_IV_SIZE = 16;
-
-    /** Length HMAC tag */
-    public static final int HMAC_TAG_SIZE = 16;
-
-    // 3 16 byte arrays known by both the encryptor and decryptor.
-    private static final byte[] AK_IV =
-            new byte[] {12, -59, 19, 23, 96, 57, -59, 19, 117, -31, -116, -61, 86, -25, -33, -78};
-    private static final byte[] ASALT_IV =
-            new byte[] {111, 48, -83, -79, -10, -102, -16, 73, 43, 55, 102, -127, 58, -19, -113, 4};
-    private static final byte[] HK_IV =
-            new byte[] {12, -59, 19, 23, 96, 57, -59, 19, 117, -31, -116, -61, 86, -25, -33, -78};
-
-    // Lazily instantiated when {@link #getInstance()} is called.
-    @Nullable private static CryptorImpV1 sCryptor;
-
-    /** Returns an instance of CryptorImpV1. */
-    public static CryptorImpV1 getInstance() {
-        if (sCryptor == null) {
-            sCryptor = new CryptorImpV1();
-        }
-        return sCryptor;
-    }
-
-    private CryptorImpV1() {
-    }
-
-    @Nullable
-    @Override
-    public byte[] encrypt(byte[] data, byte[] salt, byte[] authenticityKey) {
-        if (authenticityKey.length != AUTHENTICITY_KEY_BYTE_SIZE) {
-            Log.w(TAG, "Illegal authenticity key size");
-            return null;
-        }
-
-        // Generates a 32 bytes encryption key from authenticity_key
-        byte[] encryptionKey = Cryptor.computeHkdf(authenticityKey, AK_IV, ENCRYPTION_KEY_SIZE);
-        if (encryptionKey == null) {
-            Log.e(TAG, "Failed to generate encryption key.");
-            return null;
-        }
-
-        // Encrypts the data using the encryption key
-        SecretKey secretKey = new SecretKeySpec(encryptionKey, ENCRYPT_ALGORITHM);
-        Cipher cipher;
-        try {
-            cipher = Cipher.getInstance(CIPHER_ALGORITHM);
-        } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
-            Log.e(TAG, "Failed to encrypt with secret key.", e);
-            return null;
-        }
-        byte[] asalt = Cryptor.computeHkdf(salt, ASALT_IV, AES_CTR_IV_SIZE);
-        if (asalt == null) {
-            Log.e(TAG, "Failed to generate salt.");
-            return null;
-        }
-        try {
-            cipher.init(Cipher.ENCRYPT_MODE, secretKey, new IvParameterSpec(asalt));
-        } catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
-            Log.e(TAG, "Failed to initialize cipher.", e);
-            return null;
-        }
-        try {
-            return cipher.doFinal(data);
-        } catch (IllegalBlockSizeException | BadPaddingException e) {
-            Log.e(TAG, "Failed to encrypt with secret key.", e);
-            return null;
-        }
-    }
-
-    @Nullable
-    @Override
-    public byte[] decrypt(byte[] encryptedData, byte[] salt, byte[] authenticityKey) {
-        if (authenticityKey.length != AUTHENTICITY_KEY_BYTE_SIZE) {
-            Log.w(TAG, "Illegal authenticity key size");
-            return null;
-        }
-
-        // Generates a 32 bytes encryption key from authenticity_key
-        byte[] encryptionKey = Cryptor.computeHkdf(authenticityKey, AK_IV, ENCRYPTION_KEY_SIZE);
-        if (encryptionKey == null) {
-            Log.e(TAG, "Failed to generate encryption key.");
-            return null;
-        }
-
-        // Decrypts the data using the encryption key
-        SecretKey secretKey = new SecretKeySpec(encryptionKey, ENCRYPT_ALGORITHM);
-        Cipher cipher;
-        try {
-            cipher = Cipher.getInstance(CIPHER_ALGORITHM);
-        } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
-            Log.e(TAG, "Failed to get cipher instance.", e);
-            return null;
-        }
-        byte[] asalt = Cryptor.computeHkdf(salt, ASALT_IV, AES_CTR_IV_SIZE);
-        if (asalt == null) {
-            return null;
-        }
-        try {
-            cipher.init(Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(asalt));
-        } catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
-            Log.e(TAG, "Failed to initialize cipher.", e);
-            return null;
-        }
-
-        try {
-            return cipher.doFinal(encryptedData);
-        } catch (IllegalBlockSizeException | BadPaddingException e) {
-            Log.e(TAG, "Failed to decrypt bytes with secret key.", e);
-            return null;
-        }
-    }
-
-    @Override
-    @Nullable
-    public byte[] sign(byte[] data, byte[] key) {
-        return generateHmacTag(data, key);
-    }
-
-    @Override
-    public int getSignatureLength() {
-        return HMAC_TAG_SIZE;
-    }
-
-    @Override
-    public boolean verify(byte[] data, byte[] key, byte[] signature) {
-        return Arrays.equals(sign(data, key), signature);
-    }
-
-    /** Generates a 16 bytes HMAC tag. This is used for decryptor to verify if the computed HMAC tag
-     * is equal to HMAC tag in advertisement to see data integrity. */
-    @Nullable
-    @VisibleForTesting
-    byte[] generateHmacTag(byte[] data, byte[] authenticityKey) {
-        if (data == null || authenticityKey == null) {
-            Log.e(TAG, "Not generate HMAC tag because of invalid data input.");
-            return null;
-        }
-
-        if (authenticityKey.length != AUTHENTICITY_KEY_BYTE_SIZE) {
-            Log.e(TAG, "Illegal authenticity key size");
-            return null;
-        }
-
-        // Generates a 32 bytes HMAC key from authenticity_key
-        byte[] hmacKey = Cryptor.computeHkdf(authenticityKey, HK_IV, AES_CTR_IV_SIZE);
-        if (hmacKey == null) {
-            Log.e(TAG, "Failed to generate HMAC key.");
-            return null;
-        }
-
-        // Generates a 16 bytes HMAC tag from authenticity_key
-        return Cryptor.computeHkdf(data, hmacKey, HMAC_TAG_SIZE);
-    }
-}
diff --git a/nearby/service/java/com/android/server/nearby/util/encryption/CryptorMicImp.java b/nearby/service/java/com/android/server/nearby/util/encryption/CryptorMicImp.java
new file mode 100644
index 0000000..3dbf85c
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/util/encryption/CryptorMicImp.java
@@ -0,0 +1,286 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.util.encryption;
+
+import static com.android.server.nearby.NearbyService.TAG;
+
+import android.security.keystore.KeyProperties;
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.nearby.util.ArrayUtils;
+
+import java.nio.charset.StandardCharsets;
+import java.security.GeneralSecurityException;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
+
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+
+/**
+ * MIC encryption and decryption for {@link android.nearby.BroadcastRequest#PRESENCE_VERSION_V1}
+ * advertisement
+ */
+public class CryptorMicImp extends Cryptor {
+
+    public static final int MIC_LENGTH = 16;
+
+    private static final String ENCRYPT_ALGORITHM = KeyProperties.KEY_ALGORITHM_AES;
+    private static final byte[] AES_KEY_INFO_BYTES = "Unsigned Section AES key".getBytes(
+            StandardCharsets.US_ASCII);
+    private static final byte[] ADV_NONCE_INFO_BYTES_SALT_DE = "Unsigned Section IV".getBytes(
+            StandardCharsets.US_ASCII);
+    private static final byte[] ADV_NONCE_INFO_BYTES_ENCRYPTION_INFO_DE =
+            "V1 derived salt".getBytes(StandardCharsets.US_ASCII);
+    private static final byte[] METADATA_KEY_HMAC_KEY_INFO_BYTES =
+            "Unsigned Section metadata key HMAC key".getBytes(StandardCharsets.US_ASCII);
+    private static final byte[] MIC_HMAC_KEY_INFO_BYTES = "Unsigned Section HMAC key".getBytes(
+            StandardCharsets.US_ASCII);
+    private static final int AES_KEY_SIZE = 16;
+    private static final int ADV_NONCE_SIZE_SALT_DE = 16;
+    private static final int ADV_NONCE_SIZE_ENCRYPTION_INFO_DE = 12;
+    private static final int HMAC_KEY_SIZE = 32;
+
+    // Lazily instantiated when {@link #getInstance()} is called.
+    @Nullable
+    private static CryptorMicImp sCryptor;
+
+    private CryptorMicImp() {
+    }
+
+    /** Returns an instance of CryptorImpV1. */
+    public static CryptorMicImp getInstance() {
+        if (sCryptor == null) {
+            sCryptor = new CryptorMicImp();
+        }
+        return sCryptor;
+    }
+
+    /**
+     * Generate the meta data encryption key tag
+     * @param metadataEncryptionKey used as identity
+     * @param keySeed authenticity key saved in local and shared credential
+     * @return bytes generated by hmac or {@code null} when there is an error
+     */
+    @Nullable
+    public static byte[] generateMetadataEncryptionKeyTag(byte[] metadataEncryptionKey,
+            byte[] keySeed) {
+        try {
+            byte[] metadataKeyHmacKey = generateMetadataKeyHmacKey(keySeed);
+            return Cryptor.generateHmac(/* algorithm= */ HMAC_SHA256_ALGORITHM, /* input= */
+                    metadataEncryptionKey, /* key= */ metadataKeyHmacKey);
+        } catch (GeneralSecurityException e) {
+            Log.e(TAG, "Failed to generate Metadata encryption key tag.", e);
+            return null;
+        }
+    }
+
+    /**
+     * @param salt from the 2 bytes Salt Data Element
+     */
+    @Nullable
+    public static byte[] generateAdvNonce(byte[] salt) throws GeneralSecurityException {
+        return Cryptor.computeHkdf(
+                /* macAlgorithm= */ HMAC_SHA256_ALGORITHM,
+                /* ikm = */ salt,
+                /* salt= */ NP_HKDF_SALT,
+                /* info= */ ADV_NONCE_INFO_BYTES_SALT_DE,
+                /* size= */ ADV_NONCE_SIZE_SALT_DE);
+    }
+
+    /** Generates the 12 bytes nonce with salt from the 2 bytes Salt Data Element */
+    @Nullable
+    public static byte[] generateAdvNonce(byte[] salt, int deIndex)
+            throws GeneralSecurityException {
+        // go/nearby-specs-working-doc
+        // Indices are encoded as big-endian unsigned 32-bit integers, starting at 1.
+        // Index 0 is reserved
+        byte[] indexBytes = new byte[4];
+        indexBytes[3] = (byte) deIndex;
+        byte[] info =
+                ArrayUtils.concatByteArrays(ADV_NONCE_INFO_BYTES_ENCRYPTION_INFO_DE, indexBytes);
+        return Cryptor.computeHkdf(
+                /* macAlgorithm= */ HMAC_SHA256_ALGORITHM,
+                /* ikm = */ salt,
+                /* salt= */ NP_HKDF_SALT,
+                /* info= */ info,
+                /* size= */ ADV_NONCE_SIZE_ENCRYPTION_INFO_DE);
+    }
+
+    @Nullable
+    @Override
+    public byte[] encrypt(byte[] input, byte[] iv, byte[] keySeed) {
+        if (input == null || iv == null || keySeed == null) {
+            return null;
+        }
+        Cipher cipher;
+        try {
+            cipher = Cipher.getInstance(CIPHER_ALGORITHM);
+        } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
+            Log.e(TAG, "Failed to encrypt with secret key.", e);
+            return null;
+        }
+
+        byte[] aesKey;
+        try {
+            aesKey = generateAesKey(keySeed);
+        } catch (GeneralSecurityException e) {
+            Log.e(TAG, "Encryption failed because failed to generate the AES key.", e);
+            return null;
+        }
+        if (aesKey == null) {
+            Log.i(TAG, "Failed to generate the AES key.");
+            return null;
+        }
+        SecretKey secretKey = new SecretKeySpec(aesKey, ENCRYPT_ALGORITHM);
+        try {
+            cipher.init(Cipher.ENCRYPT_MODE, secretKey, new IvParameterSpec(iv));
+        } catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
+            Log.e(TAG, "Failed to initialize cipher.", e);
+            return null;
+
+        }
+        try {
+            return cipher.doFinal(input);
+        } catch (IllegalBlockSizeException | BadPaddingException e) {
+            Log.e(TAG, "Failed to encrypt with secret key.", e);
+            return null;
+        }
+    }
+
+    @Nullable
+    @Override
+    public byte[] decrypt(byte[] encryptedData, byte[] iv, byte[] keySeed) {
+        if (encryptedData == null || iv == null || keySeed == null) {
+            return null;
+        }
+
+        Cipher cipher;
+        try {
+            cipher = Cipher.getInstance(CIPHER_ALGORITHM);
+        } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
+            Log.e(TAG, "Failed to get cipher instance.", e);
+            return null;
+        }
+        byte[] aesKey;
+        try {
+            aesKey = generateAesKey(keySeed);
+        } catch (GeneralSecurityException e) {
+            Log.e(TAG, "Decryption failed because failed to generate the AES key.", e);
+            return null;
+        }
+        SecretKey secretKey = new SecretKeySpec(aesKey, ENCRYPT_ALGORITHM);
+        try {
+            cipher.init(Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(iv));
+        } catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
+            Log.e(TAG, "Failed to initialize cipher.", e);
+            return null;
+        }
+
+        try {
+            return cipher.doFinal(encryptedData);
+        } catch (IllegalBlockSizeException | BadPaddingException e) {
+            Log.e(TAG, "Failed to decrypt bytes with secret key.", e);
+            return null;
+        }
+    }
+
+    @Override
+    @Nullable
+    public byte[] sign(byte[] data, byte[] key) {
+        byte[] res = generateHmacTag(data, key);
+        return res;
+    }
+
+    @Override
+    public int getSignatureLength() {
+        return MIC_LENGTH;
+    }
+
+    @Override
+    public boolean verify(byte[] data, byte[] key, byte[] signature) {
+        return Arrays.equals(sign(data, key), signature);
+    }
+
+    /**
+     * Generates a 16 bytes HMAC tag. This is used for decryptor to verify if the computed HMAC tag
+     * is equal to HMAC tag in advertisement to see data integrity.
+     *
+     * @param input   concatenated advertisement UUID, header, section header, derived salt, and
+     *                section content
+     * @param keySeed the MIC HMAC key is calculated using the derived key
+     * @return the first 16 bytes of HMAC-SHA256 result
+     */
+    @Nullable
+    @VisibleForTesting
+    byte[] generateHmacTag(byte[] input, byte[] keySeed) {
+        try {
+            if (input == null || keySeed == null) {
+                return null;
+            }
+            byte[] micHmacKey = generateMicHmacKey(keySeed);
+            byte[] hmac = Cryptor.generateHmac(/* algorithm= */ HMAC_SHA256_ALGORITHM, /* input= */
+                    input, /* key= */ micHmacKey);
+            if (ArrayUtils.isEmpty(hmac)) {
+                return null;
+            }
+            return Arrays.copyOf(hmac, MIC_LENGTH);
+        } catch (GeneralSecurityException e) {
+            Log.e(TAG, "Failed to generate mic hmac key.", e);
+            return null;
+        }
+    }
+
+    @Nullable
+    private static byte[] generateAesKey(byte[] keySeed) throws GeneralSecurityException {
+        return Cryptor.computeHkdf(
+                /* macAlgorithm= */ HMAC_SHA256_ALGORITHM,
+                /* ikm = */ keySeed,
+                /* salt= */ NP_HKDF_SALT,
+                /* info= */ AES_KEY_INFO_BYTES,
+                /* size= */ AES_KEY_SIZE);
+    }
+
+    private static byte[] generateMetadataKeyHmacKey(byte[] keySeed)
+            throws GeneralSecurityException {
+        return generateHmacKey(keySeed, METADATA_KEY_HMAC_KEY_INFO_BYTES);
+    }
+
+    private static byte[] generateMicHmacKey(byte[] keySeed) throws GeneralSecurityException {
+        return generateHmacKey(keySeed, MIC_HMAC_KEY_INFO_BYTES);
+    }
+
+    private static byte[] generateHmacKey(byte[] keySeed, byte[] info)
+            throws GeneralSecurityException {
+        return Cryptor.computeHkdf(
+                /* macAlgorithm= */ HMAC_SHA256_ALGORITHM,
+                /* ikm = */ keySeed,
+                /* salt= */ NP_HKDF_SALT,
+                /* info= */ info,
+                /* size= */ HMAC_KEY_SIZE);
+    }
+}
diff --git a/nearby/service/lint-baseline.xml b/nearby/service/lint-baseline.xml
index a4761ab..3477594 100644
--- a/nearby/service/lint-baseline.xml
+++ b/nearby/service/lint-baseline.xml
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 8.0.0-dev" type="baseline" dependencies="true" variant="all" version="8.0.0-dev">
+<issues format="6" by="lint 8.4.0-alpha01" type="baseline" client="" dependencies="true" name="" variant="all" version="8.4.0-alpha01">
 
     <issue
         id="NewApi"
@@ -8,7 +8,7 @@
         errorLine2="                                                     ~~~~~~~~~~~~">
         <location
             file="packages/modules/Connectivity/nearby/service/java/com/android/server/nearby/provider/ChreCommunication.java"
-            line="263"
+            line="289"
             column="54"/>
     </issue>
 
diff --git a/nearby/service/proto/Android.bp b/nearby/service/proto/Android.bp
index 1b00cf6..be5a0b3 100644
--- a/nearby/service/proto/Android.bp
+++ b/nearby/service/proto/Android.bp
@@ -12,6 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 package {
+    default_team: "trendy_team_fwk_core_networking",
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
@@ -41,4 +42,4 @@
     apex_available: [
         "com.android.tethering",
     ],
-}
\ No newline at end of file
+}
diff --git a/nearby/service/proto/src/presence/blefilter.proto b/nearby/service/proto/src/presence/blefilter.proto
index e1bf455..bf9357b 100644
--- a/nearby/service/proto/src/presence/blefilter.proto
+++ b/nearby/service/proto/src/presence/blefilter.proto
@@ -115,6 +115,9 @@
   repeated DataElement data_element = 7;
   optional bytes ble_service_data = 8;
   optional ResultType result_type = 9;
+  // Timestamp when the device is discovered, in nanoseconds,
+  // relative to Android SystemClock.elapsedRealtimeNanos().
+  optional uint64 timestamp_ns = 10;
 }
 
 message BleFilterResults {
diff --git a/nearby/tests/cts/fastpair/Android.bp b/nearby/tests/cts/fastpair/Android.bp
index 66a1ffe..aa2806d 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"],
 }
 
@@ -39,6 +40,7 @@
         "cts",
         "general-tests",
         "mts-tethering",
+        "mcts-tethering",
     ],
     certificate: "platform",
     sdk_version: "module_current",
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/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/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/NearbyConfigurationTest.java b/nearby/tests/unit/src/com/android/server/nearby/NearbyConfigurationTest.java
index 5ddfed3..644e178 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/NearbyConfigurationTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/NearbyConfigurationTest.java
@@ -25,6 +25,7 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import android.os.Build;
 import android.provider.DeviceConfig;
 
 import androidx.test.platform.app.InstrumentationRegistry;
@@ -68,7 +69,12 @@
                 "3", false);
         Thread.sleep(500);
 
-        assertThat(mNearbyConfiguration.isTestAppSupported()).isTrue();
+        // TestAppSupported Flag can only be set to true in user-debug devices.
+        if (Build.isDebuggable()) {
+            assertThat(mNearbyConfiguration.isTestAppSupported()).isTrue();
+        } else {
+            assertThat(mNearbyConfiguration.isTestAppSupported()).isFalse();
+        }
         assertThat(mNearbyConfiguration.isPresenceBroadcastLegacyEnabled()).isTrue();
         assertThat(mNearbyConfiguration.getNanoAppMinVersion()).isEqualTo(3);
     }
diff --git a/nearby/tests/unit/src/com/android/server/nearby/managers/BroadcastProviderManagerTest.java b/nearby/tests/unit/src/com/android/server/nearby/managers/BroadcastProviderManagerTest.java
index bc38210..7ff7b13 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/managers/BroadcastProviderManagerTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/managers/BroadcastProviderManagerTest.java
@@ -24,7 +24,9 @@
 
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
 import android.app.UiAutomation;
 import android.content.Context;
@@ -34,6 +36,7 @@
 import android.nearby.PresenceBroadcastRequest;
 import android.nearby.PresenceCredential;
 import android.nearby.PrivateCredential;
+import android.os.IBinder;
 import android.provider.DeviceConfig;
 
 import androidx.test.core.app.ApplicationProvider;
@@ -74,6 +77,8 @@
     IBroadcastListener mBroadcastListener;
     @Mock
     BleBroadcastProvider mBleBroadcastProvider;
+    @Mock
+    IBinder mBinder;
     private Context mContext;
     private BroadcastProviderManager mBroadcastProviderManager;
     private BroadcastRequest mBroadcastRequest;
@@ -82,9 +87,12 @@
 
     @Before
     public void setUp() {
+        when(mBroadcastListener.asBinder()).thenReturn(mBinder);
         mUiAutomation.adoptShellPermissionIdentity(WRITE_DEVICE_CONFIG, READ_DEVICE_CONFIG);
         DeviceConfig.setProperty(
                 NAMESPACE, NEARBY_ENABLE_PRESENCE_BROADCAST_LEGACY, "true", false);
+        DeviceConfig.setProperty(
+                NAMESPACE, NEARBY_SUPPORT_TEST_APP, "true", false);
 
         mContext = ApplicationProvider.getApplicationContext();
         mBroadcastProviderManager = new BroadcastProviderManager(
@@ -104,15 +112,28 @@
     }
 
     @Test
-    public void testStartAdvertising() {
+    public void testStartAdvertising() throws Exception {
         mBroadcastProviderManager.startBroadcast(mBroadcastRequest, mBroadcastListener);
         verify(mBleBroadcastProvider).start(eq(BroadcastRequest.PRESENCE_VERSION_V0),
                 any(byte[].class), any(BleBroadcastProvider.BroadcastListener.class));
+        verify(mBinder).linkToDeath(any(), eq(0));
     }
 
     @Test
     public void testStopAdvertising() {
+        mBroadcastProviderManager.startBroadcast(mBroadcastRequest, mBroadcastListener);
         mBroadcastProviderManager.stopBroadcast(mBroadcastListener);
+        verify(mBinder).unlinkToDeath(any(), eq(0));
+    }
+
+    @Test
+    public void testRegisterAdvertising_twoTimes_fail() throws Exception {
+        IBroadcastListener newListener = mock(IBroadcastListener.class);
+        IBinder newBinder = mock(IBinder.class);
+        when(newListener.asBinder()).thenReturn(newBinder);
+        mBroadcastProviderManager.startBroadcast(mBroadcastRequest, mBroadcastListener);
+        mBroadcastProviderManager.startBroadcast(mBroadcastRequest, newListener);
+        verify(newListener).onStatusChanged(eq(BroadcastCallback.STATUS_FAILURE));
     }
 
     @Test
diff --git a/nearby/tests/unit/src/com/android/server/nearby/managers/DiscoveryProviderManagerLegacyTest.java b/nearby/tests/unit/src/com/android/server/nearby/managers/DiscoveryProviderManagerLegacyTest.java
index aa0dad3..0ca571a 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/managers/DiscoveryProviderManagerLegacyTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/managers/DiscoveryProviderManagerLegacyTest.java
@@ -16,6 +16,7 @@
 
 package com.android.server.nearby.managers;
 
+import static android.Manifest.permission.READ_DEVICE_CONFIG;
 import static android.nearby.PresenceCredential.IDENTITY_TYPE_PRIVATE;
 import static android.nearby.ScanRequest.SCAN_TYPE_NEARBY_PRESENCE;
 
@@ -23,10 +24,12 @@
 
 import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 import android.app.AppOpsManager;
+import android.bluetooth.BluetoothAdapter;
 import android.content.Context;
 import android.nearby.DataElement;
 import android.nearby.IScanListener;
@@ -36,6 +39,8 @@
 import android.nearby.ScanRequest;
 import android.os.IBinder;
 
+import androidx.test.platform.app.InstrumentationRegistry;
+
 import com.android.server.nearby.injector.Injector;
 import com.android.server.nearby.provider.BleDiscoveryProvider;
 import com.android.server.nearby.provider.ChreCommunication;
@@ -86,6 +91,8 @@
     DiscoveryProviderManagerLegacy.ScanListenerDeathRecipient mScanListenerDeathRecipient;
     @Mock
     IBinder mIBinder;
+    @Mock
+    BluetoothAdapter mBluetoothAdapter;
     private DiscoveryProviderManagerLegacy mDiscoveryProviderManager;
     private Map<IBinder, DiscoveryProviderManagerLegacy.ScanListenerRecord>
             mScanTypeScanListenerRecordMap;
@@ -135,10 +142,15 @@
 
     @Before
     public void setup() {
+        InstrumentationRegistry.getInstrumentation().getUiAutomation().adoptShellPermissionIdentity(
+                READ_DEVICE_CONFIG);
         MockitoAnnotations.initMocks(this);
         when(mInjector.getAppOpsManager()).thenReturn(mAppOpsManager);
+        when(mInjector.getBluetoothAdapter()).thenReturn(mBluetoothAdapter);
         when(mBleDiscoveryProvider.getController()).thenReturn(mBluetoothController);
         when(mChreDiscoveryProvider.getController()).thenReturn(mChreController);
+        when(mBluetoothAdapter.isBleScanAlwaysAvailable()).thenReturn(true);
+        when(mBluetoothAdapter.isEnabled()).thenReturn(true);
 
         mScanTypeScanListenerRecordMap = new HashMap<>();
         mDiscoveryProviderManager =
@@ -164,6 +176,13 @@
     }
 
     @Test
+    public void test_enableBleWhenBleOff() throws Exception {
+        when(mBluetoothAdapter.isEnabled()).thenReturn(false);
+        mDiscoveryProviderManager.init();
+        verify(mBluetoothAdapter, times(1)).enableBLE();
+    }
+
+    @Test
     public void testStartProviders_chreOnlyChreAvailable_bleProviderNotStarted() {
         when(mChreDiscoveryProvider.available()).thenReturn(true);
 
@@ -375,4 +394,62 @@
                 .isTrue();
         assertThat(manager.mChreDiscoveryProvider.getFiltersLocked()).isNotNull();
     }
+
+    @Test
+    public void isBluetoothEnabledTest_bluetoothEnabled() {
+        when(mBluetoothAdapter.isEnabled()).thenReturn(true);
+        when(mBluetoothAdapter.isLeEnabled()).thenReturn(false);
+        when(mBluetoothAdapter.isBleScanAlwaysAvailable()).thenReturn(true);
+        when(mBluetoothAdapter.enableBLE()).thenReturn(true);
+
+        assertThat(mDiscoveryProviderManager.setBleScanEnabled()).isTrue();
+    }
+
+    @Test
+    public void isBluetoothEnabledTest_bleEnabled() {
+        when(mBluetoothAdapter.isEnabled()).thenReturn(false);
+        when(mBluetoothAdapter.isLeEnabled()).thenReturn(true);
+        when(mBluetoothAdapter.isBleScanAlwaysAvailable()).thenReturn(true);
+        when(mBluetoothAdapter.enableBLE()).thenReturn(true);
+
+        assertThat(mDiscoveryProviderManager.setBleScanEnabled()).isTrue();
+    }
+
+    @Test
+    public void enabledTest_enabled() {
+        when(mBluetoothAdapter.isEnabled()).thenReturn(false);
+        when(mBluetoothAdapter.isLeEnabled()).thenReturn(false);
+        when(mBluetoothAdapter.isBleScanAlwaysAvailable()).thenReturn(true);
+        when(mBluetoothAdapter.enableBLE()).thenReturn(true);
+
+        assertThat(mDiscoveryProviderManager.setBleScanEnabled()).isTrue();
+    }
+
+    @Test
+    public void enabledTest_enableFailed() {
+        when(mBluetoothAdapter.isEnabled()).thenReturn(false);
+        when(mBluetoothAdapter.isLeEnabled()).thenReturn(false);
+        when(mBluetoothAdapter.isBleScanAlwaysAvailable()).thenReturn(true);
+        when(mBluetoothAdapter.enableBLE()).thenReturn(false);
+
+        assertThat(mDiscoveryProviderManager.setBleScanEnabled()).isFalse();
+    }
+
+    @Test
+    public void enabledTest_scanIsOn() {
+        when(mBluetoothAdapter.isEnabled()).thenReturn(true);
+        when(mBluetoothAdapter.isLeEnabled()).thenReturn(false);
+        when(mBluetoothAdapter.isBleScanAlwaysAvailable()).thenReturn(false);
+
+        assertThat(mDiscoveryProviderManager.setBleScanEnabled()).isTrue();
+    }
+
+    @Test
+    public void enabledTest_failed() {
+        when(mBluetoothAdapter.isEnabled()).thenReturn(false);
+        when(mBluetoothAdapter.isLeEnabled()).thenReturn(false);
+        when(mBluetoothAdapter.isBleScanAlwaysAvailable()).thenReturn(false);
+
+        assertThat(mDiscoveryProviderManager.setBleScanEnabled()).isFalse();
+    }
 }
diff --git a/nearby/tests/unit/src/com/android/server/nearby/managers/DiscoveryProviderManagerTest.java b/nearby/tests/unit/src/com/android/server/nearby/managers/DiscoveryProviderManagerTest.java
index 7ecf631..7cea34a 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/managers/DiscoveryProviderManagerTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/managers/DiscoveryProviderManagerTest.java
@@ -16,6 +16,7 @@
 
 package com.android.server.nearby.managers;
 
+import static android.Manifest.permission.READ_DEVICE_CONFIG;
 import static android.nearby.PresenceCredential.IDENTITY_TYPE_PRIVATE;
 import static android.nearby.ScanRequest.SCAN_TYPE_NEARBY_PRESENCE;
 
@@ -24,10 +25,12 @@
 import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 import android.app.AppOpsManager;
+import android.bluetooth.BluetoothAdapter;
 import android.content.Context;
 import android.nearby.DataElement;
 import android.nearby.IScanListener;
@@ -37,6 +40,8 @@
 import android.nearby.ScanRequest;
 import android.os.IBinder;
 
+import androidx.test.platform.app.InstrumentationRegistry;
+
 import com.android.server.nearby.injector.Injector;
 import com.android.server.nearby.provider.BleDiscoveryProvider;
 import com.android.server.nearby.provider.ChreCommunication;
@@ -80,6 +85,8 @@
     CallerIdentity mCallerIdentity;
     @Mock
     IBinder mIBinder;
+    @Mock
+    BluetoothAdapter mBluetoothAdapter;
     private Executor mExecutor;
     private DiscoveryProviderManager mDiscoveryProviderManager;
 
@@ -128,12 +135,17 @@
 
     @Before
     public void setup() {
+        InstrumentationRegistry.getInstrumentation().getUiAutomation().adoptShellPermissionIdentity(
+                READ_DEVICE_CONFIG);
         MockitoAnnotations.initMocks(this);
         mExecutor = Executors.newSingleThreadExecutor();
         when(mInjector.getAppOpsManager()).thenReturn(mAppOpsManager);
         when(mBleDiscoveryProvider.getController()).thenReturn(mBluetoothController);
         when(mChreDiscoveryProvider.getController()).thenReturn(mChreController);
         when(mScanListener.asBinder()).thenReturn(mIBinder);
+        when(mInjector.getBluetoothAdapter()).thenReturn(mBluetoothAdapter);
+        when(mBluetoothAdapter.isBleScanAlwaysAvailable()).thenReturn(true);
+        when(mBluetoothAdapter.isEnabled()).thenReturn(true);
 
         mDiscoveryProviderManager =
                 new DiscoveryProviderManager(mContext, mExecutor, mInjector,
@@ -157,6 +169,13 @@
     }
 
     @Test
+    public void test_enableBleWhenBleOff() throws Exception {
+        when(mBluetoothAdapter.isEnabled()).thenReturn(false);
+        mDiscoveryProviderManager.init();
+        verify(mBluetoothAdapter, times(1)).enableBLE();
+    }
+
+    @Test
     public void testStartProviders_chreOnlyChreAvailable_bleProviderNotStarted() {
         reset(mBluetoothController);
         when(mChreDiscoveryProvider.available()).thenReturn(true);
@@ -336,4 +355,62 @@
                 .isTrue();
         assertThat(manager.mChreDiscoveryProvider.getFiltersLocked()).isNotNull();
     }
+
+    @Test
+    public void isBluetoothEnabledTest_bluetoothEnabled() {
+        when(mBluetoothAdapter.isEnabled()).thenReturn(true);
+        when(mBluetoothAdapter.isLeEnabled()).thenReturn(false);
+        when(mBluetoothAdapter.isBleScanAlwaysAvailable()).thenReturn(true);
+        when(mBluetoothAdapter.enableBLE()).thenReturn(true);
+
+        assertThat(mDiscoveryProviderManager.setBleScanEnabled()).isTrue();
+    }
+
+    @Test
+    public void isBluetoothEnabledTest_bleEnabled() {
+        when(mBluetoothAdapter.isEnabled()).thenReturn(false);
+        when(mBluetoothAdapter.isLeEnabled()).thenReturn(true);
+        when(mBluetoothAdapter.isBleScanAlwaysAvailable()).thenReturn(true);
+        when(mBluetoothAdapter.enableBLE()).thenReturn(true);
+
+        assertThat(mDiscoveryProviderManager.setBleScanEnabled()).isTrue();
+    }
+
+    @Test
+    public void enabledTest_enabled() {
+        when(mBluetoothAdapter.isEnabled()).thenReturn(false);
+        when(mBluetoothAdapter.isLeEnabled()).thenReturn(false);
+        when(mBluetoothAdapter.isBleScanAlwaysAvailable()).thenReturn(true);
+        when(mBluetoothAdapter.enableBLE()).thenReturn(true);
+
+        assertThat(mDiscoveryProviderManager.setBleScanEnabled()).isTrue();
+    }
+
+    @Test
+    public void enabledTest_enableFailed() {
+        when(mBluetoothAdapter.isEnabled()).thenReturn(false);
+        when(mBluetoothAdapter.isLeEnabled()).thenReturn(false);
+        when(mBluetoothAdapter.isBleScanAlwaysAvailable()).thenReturn(true);
+        when(mBluetoothAdapter.enableBLE()).thenReturn(false);
+
+        assertThat(mDiscoveryProviderManager.setBleScanEnabled()).isFalse();
+    }
+
+    @Test
+    public void enabledTest_scanIsOn() {
+        when(mBluetoothAdapter.isEnabled()).thenReturn(true);
+        when(mBluetoothAdapter.isLeEnabled()).thenReturn(false);
+        when(mBluetoothAdapter.isBleScanAlwaysAvailable()).thenReturn(false);
+
+        assertThat(mDiscoveryProviderManager.setBleScanEnabled()).isTrue();
+    }
+
+    @Test
+    public void enabledTest_failed() {
+        when(mBluetoothAdapter.isEnabled()).thenReturn(false);
+        when(mBluetoothAdapter.isLeEnabled()).thenReturn(false);
+        when(mBluetoothAdapter.isBleScanAlwaysAvailable()).thenReturn(false);
+
+        assertThat(mDiscoveryProviderManager.setBleScanEnabled()).isFalse();
+    }
 }
diff --git a/nearby/tests/unit/src/com/android/server/nearby/presence/EncryptionInfoTest.java b/nearby/tests/unit/src/com/android/server/nearby/presence/EncryptionInfoTest.java
new file mode 100644
index 0000000..6ec7c57
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/presence/EncryptionInfoTest.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.presence;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import com.android.server.nearby.presence.EncryptionInfo.EncodingScheme;
+import com.android.server.nearby.util.ArrayUtils;
+
+import org.junit.Test;
+
+
+/**
+ * Unit test for {@link EncryptionInfo}.
+ */
+public class EncryptionInfoTest {
+    private static final byte[] SALT =
+            new byte[]{25, -21, 35, -108, -26, -126, 99, 60, 110, 45, -116, 34, 91, 126, -23, 127};
+
+    @Test
+    public void test_illegalLength() {
+        byte[] data = new byte[]{1, 2};
+        assertThrows(IllegalArgumentException.class, () -> new EncryptionInfo(data));
+    }
+
+    @Test
+    public void test_illegalEncodingScheme() {
+        assertThrows(IllegalArgumentException.class,
+                () -> new EncryptionInfo(ArrayUtils.append((byte) 0b10110000, SALT)));
+        assertThrows(IllegalArgumentException.class,
+                () -> new EncryptionInfo(ArrayUtils.append((byte) 0b01101000, SALT)));
+    }
+
+    @Test
+    public void test_getMethods_signature() {
+        byte[] data = ArrayUtils.append((byte) 0b10001000, SALT);
+        EncryptionInfo info = new EncryptionInfo(data);
+        assertThat(info.getEncodingScheme()).isEqualTo(EncodingScheme.SIGNATURE);
+        assertThat(info.getSalt()).isEqualTo(SALT);
+    }
+
+    @Test
+    public void test_getMethods_mic() {
+        byte[] data = ArrayUtils.append((byte) 0b10000000, SALT);
+        EncryptionInfo info = new EncryptionInfo(data);
+        assertThat(info.getEncodingScheme()).isEqualTo(EncodingScheme.MIC);
+        assertThat(info.getSalt()).isEqualTo(SALT);
+    }
+    @Test
+    public void test_toBytes() {
+        byte[] data = EncryptionInfo.toByte(EncodingScheme.MIC, SALT);
+        EncryptionInfo info = new EncryptionInfo(data);
+        assertThat(info.getEncodingScheme()).isEqualTo(EncodingScheme.MIC);
+        assertThat(info.getSalt()).isEqualTo(SALT);
+    }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/presence/ExtendedAdvertisementTest.java b/nearby/tests/unit/src/com/android/server/nearby/presence/ExtendedAdvertisementTest.java
index 895df69..3f00a42 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/presence/ExtendedAdvertisementTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/presence/ExtendedAdvertisementTest.java
@@ -16,6 +16,8 @@
 
 package com.android.server.nearby.presence;
 
+import static com.android.server.nearby.presence.PresenceConstants.PRESENCE_UUID_BYTES;
+
 import static com.google.common.truth.Truth.assertThat;
 
 import android.nearby.BroadcastRequest;
@@ -25,19 +27,22 @@
 import android.nearby.PrivateCredential;
 import android.nearby.PublicCredential;
 
-import com.android.server.nearby.util.encryption.CryptorImpIdentityV1;
-import com.android.server.nearby.util.encryption.CryptorImpV1;
+import com.android.server.nearby.util.ArrayUtils;
+import com.android.server.nearby.util.encryption.CryptorMicImp;
 
 import org.junit.Before;
 import org.junit.Test;
 
 import java.nio.ByteBuffer;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
 
 public class ExtendedAdvertisementTest {
+    private static final int EXTENDED_ADVERTISEMENT_BYTE_LENGTH = 67;
     private static final int IDENTITY_TYPE = PresenceCredential.IDENTITY_TYPE_PRIVATE;
+    private static final int DATA_TYPE_ACTION = 6;
     private static final int DATA_TYPE_MODEL_ID = 7;
     private static final int DATA_TYPE_BLE_ADDRESS = 101;
     private static final int DATA_TYPE_PUBLIC_IDENTITY = 3;
@@ -49,18 +54,23 @@
     private static final DataElement BLE_ADDRESS_ELEMENT =
             new DataElement(DATA_TYPE_BLE_ADDRESS, BLE_ADDRESS);
 
-    private static final byte[] IDENTITY =
-            new byte[]{1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4};
+    private static final byte[] METADATA_ENCRYPTION_KEY =
+            new byte[]{-39, -55, 115, 78, -57, 40, 115, 0, -112, 86, -86, 7, -42, 68, 11, 12};
     private static final int MEDIUM_TYPE_BLE = 0;
     private static final byte[] SALT = {2, 3};
+
     private static final int PRESENCE_ACTION_1 = 1;
     private static final int PRESENCE_ACTION_2 = 2;
+    private static final DataElement PRESENCE_ACTION_DE_1 =
+            new DataElement(DATA_TYPE_ACTION, new byte[]{PRESENCE_ACTION_1});
+    private static final DataElement PRESENCE_ACTION_DE_2 =
+            new DataElement(DATA_TYPE_ACTION, new byte[]{PRESENCE_ACTION_2});
 
     private static final byte[] SECRET_ID = new byte[]{1, 2, 3, 4};
     private static final byte[] AUTHENTICITY_KEY =
             new byte[]{-97, 10, 107, -86, 25, 65, -54, -95, -72, 59, 54, 93, 9, 3, -24, -88};
     private static final byte[] PUBLIC_KEY =
-            new byte[] {
+            new byte[]{
                     48, 89, 48, 19, 6, 7, 42, -122, 72, -50, 61, 2, 1, 6, 8, 42, -122, 72, -50, 61,
                     66, 0, 4, -56, -39, -92, 69, 0, 52, 23, 67, 83, -14, 75, 52, -14, -5, -41, 48,
                     -83, 31, 42, -39, 102, -13, 22, -73, -73, 86, 30, -96, -84, -13, 4, 122, 104,
@@ -68,7 +78,7 @@
                     123, 41, -119, -25, 1, -112, 112
             };
     private static final byte[] ENCRYPTED_METADATA_BYTES =
-            new byte[] {
+            new byte[]{
                     -44, -25, -95, -124, -7, 90, 116, -8, 7, -120, -23, -22, -106, -44, -19, 61,
                     -18, 39, 29, 78, 108, -11, -39, 85, -30, 64, -99, 102, 65, 37, -42, 114, -37,
                     88, -112, 8, -75, -53, 23, -16, -104, 67, 49, 48, -53, 73, -109, 44, -23, -11,
@@ -78,23 +88,65 @@
                     -4, -46, -30, -85, -50, 100, 46, -66, -128, 7, 66, 9, 88, 95, 12, -13, 81, -91,
             };
     private static final byte[] METADATA_ENCRYPTION_KEY_TAG =
-            new byte[] {-126, -104, 1, -1, 26, -46, -68, -86};
+            new byte[]{-100, 102, -35, -99, 66, -85, -55, -58, -52, 11, -74, 102, 109, -89, 1, -34,
+                    45, 43, 107, -60, 99, -21, 28, 34, 31, -100, -96, 108, 108, -18, 107, 5};
+
+    private static final String ENCODED_ADVERTISEMENT_ENCRYPTION_INFO =
+            "2091911000DE2A89ED98474AF3E41E48487E8AEBDE90014C18BCB9F9AAC5C11A1BE00A10A5DCD2C49A74BE"
+                    + "BAF0FE72FD5053B9DF8B9976C80BE0DCE8FEE83F1BFA9A89EB176CA48EE4ED5D15C6CDAD6B9E"
+                    + "41187AA6316D7BFD8E454A53971AC00836F7AB0771FF0534050037D49C6AEB18CF9F8590E5CD"
+                    + "EE2FBC330FCDC640C63F0735B7E3F02FE61A0496EF976A158AD3455D";
+    private static final byte[] METADATA_ENCRYPTION_KEY_TAG_2 =
+            new byte[]{-54, -39, 41, 16, 61, 79, -116, 14, 94, 0, 84, 45, 26, -108, 66, -48, 124,
+                    -81, 61, 56, -98, -47, 14, -19, 116, 106, -27, 123, -81, 49, 83, -42};
+
     private static final String DEVICE_NAME = "test_device";
 
+    private static final byte[] SALT_16 =
+            ArrayUtils.stringToBytes("DE2A89ED98474AF3E41E48487E8AEBDE");
+    private static final byte[] AUTHENTICITY_KEY_2 =  ArrayUtils.stringToBytes(
+            "959D2F3CAB8EE4A2DEB0255C03762CF5D39EB919300420E75A089050FB025E20");
+    private static final byte[] METADATA_ENCRYPTION_KEY_2 =  ArrayUtils.stringToBytes(
+            "EF5E9A0867560E52AE1F05FCA7E48D29");
+
+    private static final DataElement DE1 = new DataElement(571, ArrayUtils.stringToBytes(
+            "537F96FD94E13BE589F0141145CFC0EEC4F86FBDB2"));
+    private static final DataElement DE2 = new DataElement(541, ArrayUtils.stringToBytes(
+            "D301FFB24B5B"));
+    private static final DataElement DE3 = new DataElement(51, ArrayUtils.stringToBytes(
+            "EA95F07C25B75C04E1B2B8731F6A55BA379FB141"));
+    private static final DataElement DE4 = new DataElement(729, ArrayUtils.stringToBytes(
+            "2EFD3101E2311BBB108F0A7503907EAF0C2EAAA60CDA8D33A294C4CEACE0"));
+    private static final DataElement DE5 = new DataElement(411, ArrayUtils.stringToBytes("B0"));
+
     private PresenceBroadcastRequest.Builder mBuilder;
+    private PresenceBroadcastRequest.Builder mBuilderCredentialInfo;
     private PrivateCredential mPrivateCredential;
+    private PrivateCredential mPrivateCredential2;
+
     private PublicCredential mPublicCredential;
+    private PublicCredential mPublicCredential2;
 
     @Before
     public void setUp() {
         mPrivateCredential =
-                new PrivateCredential.Builder(SECRET_ID, AUTHENTICITY_KEY, IDENTITY, DEVICE_NAME)
+                new PrivateCredential.Builder(
+                        SECRET_ID, AUTHENTICITY_KEY, METADATA_ENCRYPTION_KEY, DEVICE_NAME)
+                        .setIdentityType(PresenceCredential.IDENTITY_TYPE_PRIVATE)
+                        .build();
+        mPrivateCredential2 =
+                new PrivateCredential.Builder(
+                        SECRET_ID, AUTHENTICITY_KEY_2, METADATA_ENCRYPTION_KEY_2, DEVICE_NAME)
                         .setIdentityType(PresenceCredential.IDENTITY_TYPE_PRIVATE)
                         .build();
         mPublicCredential =
                 new PublicCredential.Builder(SECRET_ID, AUTHENTICITY_KEY, PUBLIC_KEY,
                         ENCRYPTED_METADATA_BYTES, METADATA_ENCRYPTION_KEY_TAG)
                         .build();
+        mPublicCredential2 =
+                new PublicCredential.Builder(SECRET_ID, AUTHENTICITY_KEY_2, PUBLIC_KEY,
+                        ENCRYPTED_METADATA_BYTES, METADATA_ENCRYPTION_KEY_TAG_2)
+                        .build();
         mBuilder =
                 new PresenceBroadcastRequest.Builder(Collections.singletonList(MEDIUM_TYPE_BLE),
                         SALT, mPrivateCredential)
@@ -103,6 +155,16 @@
                         .addAction(PRESENCE_ACTION_2)
                         .addExtendedProperty(new DataElement(DATA_TYPE_BLE_ADDRESS, BLE_ADDRESS))
                         .addExtendedProperty(new DataElement(DATA_TYPE_MODEL_ID, MODE_ID_DATA));
+
+        mBuilderCredentialInfo =
+                new PresenceBroadcastRequest.Builder(Collections.singletonList(MEDIUM_TYPE_BLE),
+                        SALT_16, mPrivateCredential2)
+                        .setVersion(BroadcastRequest.PRESENCE_VERSION_V1)
+                        .addExtendedProperty(DE1)
+                        .addExtendedProperty(DE2)
+                        .addExtendedProperty(DE3)
+                        .addExtendedProperty(DE4)
+                        .addExtendedProperty(DE5);
     }
 
     @Test
@@ -112,36 +174,50 @@
 
         assertThat(originalAdvertisement.getActions())
                 .containsExactly(PRESENCE_ACTION_1, PRESENCE_ACTION_2);
-        assertThat(originalAdvertisement.getIdentity()).isEqualTo(IDENTITY);
+        assertThat(originalAdvertisement.getIdentity()).isEqualTo(METADATA_ENCRYPTION_KEY);
         assertThat(originalAdvertisement.getIdentityType()).isEqualTo(IDENTITY_TYPE);
-        assertThat(originalAdvertisement.getLength()).isEqualTo(66);
         assertThat(originalAdvertisement.getVersion()).isEqualTo(
                 BroadcastRequest.PRESENCE_VERSION_V1);
         assertThat(originalAdvertisement.getSalt()).isEqualTo(SALT);
         assertThat(originalAdvertisement.getDataElements())
-                .containsExactly(MODE_ID_ADDRESS_ELEMENT, BLE_ADDRESS_ELEMENT);
+                .containsExactly(PRESENCE_ACTION_DE_1, PRESENCE_ACTION_DE_2,
+                        MODE_ID_ADDRESS_ELEMENT, BLE_ADDRESS_ELEMENT);
+        assertThat(originalAdvertisement.getLength()).isEqualTo(EXTENDED_ADVERTISEMENT_BYTE_LENGTH);
+    }
+
+    @Test
+    public void test_createFromRequest_credentialInfo() {
+        ExtendedAdvertisement originalAdvertisement = ExtendedAdvertisement.createFromRequest(
+                mBuilderCredentialInfo.build());
+
+        assertThat(originalAdvertisement.getIdentity()).isEqualTo(METADATA_ENCRYPTION_KEY_2);
+        assertThat(originalAdvertisement.getIdentityType()).isEqualTo(IDENTITY_TYPE);
+        assertThat(originalAdvertisement.getVersion()).isEqualTo(
+                BroadcastRequest.PRESENCE_VERSION_V1);
+        assertThat(originalAdvertisement.getSalt()).isEqualTo(SALT_16);
+        assertThat(originalAdvertisement.getDataElements())
+                .containsExactly(DE1, DE2, DE3, DE4, DE5);
     }
 
     @Test
     public void test_createFromRequest_encodeAndDecode() {
         ExtendedAdvertisement originalAdvertisement = ExtendedAdvertisement.createFromRequest(
                 mBuilder.build());
-
         byte[] generatedBytes = originalAdvertisement.toBytes();
-
         ExtendedAdvertisement newAdvertisement =
                 ExtendedAdvertisement.fromBytes(generatedBytes, mPublicCredential);
 
         assertThat(newAdvertisement.getActions())
                 .containsExactly(PRESENCE_ACTION_1, PRESENCE_ACTION_2);
-        assertThat(newAdvertisement.getIdentity()).isEqualTo(IDENTITY);
+        assertThat(newAdvertisement.getIdentity()).isEqualTo(METADATA_ENCRYPTION_KEY);
         assertThat(newAdvertisement.getIdentityType()).isEqualTo(IDENTITY_TYPE);
-        assertThat(newAdvertisement.getLength()).isEqualTo(66);
+        assertThat(newAdvertisement.getLength()).isEqualTo(EXTENDED_ADVERTISEMENT_BYTE_LENGTH);
         assertThat(newAdvertisement.getVersion()).isEqualTo(
                 BroadcastRequest.PRESENCE_VERSION_V1);
         assertThat(newAdvertisement.getSalt()).isEqualTo(SALT);
         assertThat(newAdvertisement.getDataElements())
-                .containsExactly(MODE_ID_ADDRESS_ELEMENT, BLE_ADDRESS_ELEMENT);
+                .containsExactly(MODE_ID_ADDRESS_ELEMENT, BLE_ADDRESS_ELEMENT,
+                        PRESENCE_ACTION_DE_1, PRESENCE_ACTION_DE_2);
     }
 
     @Test
@@ -170,45 +246,77 @@
                         .setVersion(BroadcastRequest.PRESENCE_VERSION_V1)
                         .addAction(PRESENCE_ACTION_1);
         assertThat(ExtendedAdvertisement.createFromRequest(builder2.build())).isNull();
-
-        // empty action
-        PresenceBroadcastRequest.Builder builder3 =
-                new PresenceBroadcastRequest.Builder(Collections.singletonList(MEDIUM_TYPE_BLE),
-                        SALT, mPrivateCredential)
-                        .setVersion(BroadcastRequest.PRESENCE_VERSION_V1);
-        assertThat(ExtendedAdvertisement.createFromRequest(builder3.build())).isNull();
     }
 
     @Test
-    public void test_toBytes() {
+    public void test_toBytesSalt() throws Exception {
         ExtendedAdvertisement adv = ExtendedAdvertisement.createFromRequest(mBuilder.build());
         assertThat(adv.toBytes()).isEqualTo(getExtendedAdvertisementByteArray());
     }
 
     @Test
-    public void test_fromBytes() {
+    public void test_fromBytesSalt() throws Exception {
         byte[] originalBytes = getExtendedAdvertisementByteArray();
         ExtendedAdvertisement adv =
                 ExtendedAdvertisement.fromBytes(originalBytes, mPublicCredential);
 
         assertThat(adv.getActions())
                 .containsExactly(PRESENCE_ACTION_1, PRESENCE_ACTION_2);
-        assertThat(adv.getIdentity()).isEqualTo(IDENTITY);
+        assertThat(adv.getIdentity()).isEqualTo(METADATA_ENCRYPTION_KEY);
         assertThat(adv.getIdentityType()).isEqualTo(IDENTITY_TYPE);
-        assertThat(adv.getLength()).isEqualTo(66);
+        assertThat(adv.getLength()).isEqualTo(EXTENDED_ADVERTISEMENT_BYTE_LENGTH);
         assertThat(adv.getVersion()).isEqualTo(
                 BroadcastRequest.PRESENCE_VERSION_V1);
         assertThat(adv.getSalt()).isEqualTo(SALT);
         assertThat(adv.getDataElements())
-                .containsExactly(MODE_ID_ADDRESS_ELEMENT, BLE_ADDRESS_ELEMENT);
+                .containsExactly(MODE_ID_ADDRESS_ELEMENT, BLE_ADDRESS_ELEMENT,
+                        PRESENCE_ACTION_DE_1, PRESENCE_ACTION_DE_2);
+    }
+
+    @Test
+    public void test_toBytesCredentialElement() {
+        ExtendedAdvertisement adv =
+                ExtendedAdvertisement.createFromRequest(mBuilderCredentialInfo.build());
+        assertThat(ArrayUtils.bytesToStringUppercase(adv.toBytes())).isEqualTo(
+                ENCODED_ADVERTISEMENT_ENCRYPTION_INFO);
+    }
+
+    @Test
+    public void test_fromBytesCredentialElement() {
+        ExtendedAdvertisement adv =
+                ExtendedAdvertisement.fromBytes(
+                        ArrayUtils.stringToBytes(ENCODED_ADVERTISEMENT_ENCRYPTION_INFO),
+                        mPublicCredential2);
+        assertThat(adv.getIdentity()).isEqualTo(METADATA_ENCRYPTION_KEY_2);
+        assertThat(adv.getIdentityType()).isEqualTo(IDENTITY_TYPE);
+        assertThat(adv.getVersion()).isEqualTo(BroadcastRequest.PRESENCE_VERSION_V1);
+        assertThat(adv.getSalt()).isEqualTo(SALT_16);
+        assertThat(adv.getDataElements()).containsExactly(DE1, DE2, DE3, DE4, DE5);
+    }
+
+    @Test
+    public void test_fromBytes_metadataTagNotMatched_fail() throws Exception {
+        byte[] originalBytes = getExtendedAdvertisementByteArray();
+        PublicCredential credential =
+                new PublicCredential.Builder(SECRET_ID, AUTHENTICITY_KEY, PUBLIC_KEY,
+                        ENCRYPTED_METADATA_BYTES,
+                        new byte[]{113, 90, -55, 73, 25, -9, 55, -44, 102, 44, 81, -68, 101, 21, 32,
+                                92, -107, 3, 108, 90, 28, -73, 16, 49, -95, -121, 8, -45, -27, 16,
+                                6, 108})
+                        .build();
+        ExtendedAdvertisement adv =
+                ExtendedAdvertisement.fromBytes(originalBytes, credential);
+        assertThat(adv).isNull();
     }
 
     @Test
     public void test_toString() {
         ExtendedAdvertisement adv = ExtendedAdvertisement.createFromRequest(mBuilder.build());
         assertThat(adv.toString()).isEqualTo("ExtendedAdvertisement:"
-                + "<VERSION: 1, length: 66, dataElementCount: 2, identityType: 1, "
-                + "identity: [1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4], salt: [2, 3],"
+                + "<VERSION: 1, length: " + EXTENDED_ADVERTISEMENT_BYTE_LENGTH
+                + ", dataElementCount: 4, identityType: 1, "
+                + "identity: " + Arrays.toString(METADATA_ENCRYPTION_KEY)
+                + ", salt: [2, 3],"
                 + " actions: [1, 2]>");
     }
 
@@ -222,26 +330,22 @@
         assertThat(adv.getDataElements(DATA_TYPE_PUBLIC_IDENTITY)).isEmpty();
     }
 
-    private static byte[] getExtendedAdvertisementByteArray() {
-        ByteBuffer buffer = ByteBuffer.allocate(66);
+    private static byte[] getExtendedAdvertisementByteArray() throws Exception {
+        CryptorMicImp cryptor = CryptorMicImp.getInstance();
+        ByteBuffer buffer = ByteBuffer.allocate(EXTENDED_ADVERTISEMENT_BYTE_LENGTH);
         buffer.put((byte) 0b00100000); // Header V1
-        buffer.put((byte) 0b00100000); // Salt header: length 2, type 0
+        buffer.put(
+                (byte) (EXTENDED_ADVERTISEMENT_BYTE_LENGTH - 2)); // Section header (section length)
+
         // Salt data
-        buffer.put(SALT);
+        // Salt header: length 2, type 0
+        byte[] saltBytes = ArrayUtils.concatByteArrays(new byte[]{(byte) 0b00100000}, SALT);
+        buffer.put(saltBytes);
         // Identity header: length 16, type 1 (private identity)
-        buffer.put(new byte[]{(byte) 0b10010000, (byte) 0b00000001});
-        // Identity data
-        buffer.put(CryptorImpIdentityV1.getInstance().encrypt(IDENTITY, SALT, AUTHENTICITY_KEY));
+        byte[] identityHeader = new byte[]{(byte) 0b10010000, (byte) 0b00000001};
+        buffer.put(identityHeader);
 
         ByteBuffer deBuffer = ByteBuffer.allocate(28);
-        // Action1 header: length 1, type 6
-        deBuffer.put(new byte[]{(byte) 0b00010110});
-        // Action1 data
-        deBuffer.put((byte) PRESENCE_ACTION_1);
-        // Action2 header: length 1, type 6
-        deBuffer.put(new byte[]{(byte) 0b00010110});
-        // Action2 data
-        deBuffer.put((byte) PRESENCE_ACTION_2);
         // Ble address header: length 7, type 102
         deBuffer.put(new byte[]{(byte) 0b10000111, (byte) 0b01100101});
         // Ble address data
@@ -250,11 +354,30 @@
         deBuffer.put(new byte[]{(byte) 0b10001101, (byte) 0b00000111});
         // model id data
         deBuffer.put(MODE_ID_DATA);
+        // Action1 header: length 1, type 6
+        deBuffer.put(new byte[]{(byte) 0b00010110});
+        // Action1 data
+        deBuffer.put((byte) PRESENCE_ACTION_1);
+        // Action2 header: length 1, type 6
+        deBuffer.put(new byte[]{(byte) 0b00010110});
+        // Action2 data
+        deBuffer.put((byte) PRESENCE_ACTION_2);
+        byte[] deBytes = deBuffer.array();
+        byte[] nonce = CryptorMicImp.generateAdvNonce(SALT);
+        byte[] ciphertext =
+                cryptor.encrypt(
+                        ArrayUtils.concatByteArrays(METADATA_ENCRYPTION_KEY, deBytes),
+                        nonce, AUTHENTICITY_KEY);
+        buffer.put(ciphertext);
 
-        byte[] data = deBuffer.array();
-        CryptorImpV1 cryptor = CryptorImpV1.getInstance();
-        buffer.put(cryptor.encrypt(data, SALT, AUTHENTICITY_KEY));
-        buffer.put(cryptor.sign(data, AUTHENTICITY_KEY));
+        byte[] dataToSign = ArrayUtils.concatByteArrays(
+                PRESENCE_UUID_BYTES, /* UUID */
+                new byte[]{(byte) 0b00100000}, /* header */
+                new byte[]{(byte) (EXTENDED_ADVERTISEMENT_BYTE_LENGTH - 2)} /* sectionHeader */,
+                saltBytes, /* salt */
+                nonce, identityHeader, ciphertext);
+        byte[] mic = cryptor.sign(dataToSign, AUTHENTICITY_KEY);
+        buffer.put(mic);
 
         return buffer.array();
     }
diff --git a/nearby/tests/unit/src/com/android/server/nearby/provider/BleBroadcastProviderTest.java b/nearby/tests/unit/src/com/android/server/nearby/provider/BleBroadcastProviderTest.java
index 05b556b..0b1a742 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/provider/BleBroadcastProviderTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/provider/BleBroadcastProviderTest.java
@@ -16,6 +16,7 @@
 
 package com.android.server.nearby.provider;
 
+import static org.mockito.Mockito.atLeast;
 import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.eq;
 import static org.mockito.Mockito.times;
@@ -69,7 +70,7 @@
 
         AdvertiseSettings settings = new AdvertiseSettings.Builder().build();
         mBleBroadcastProvider.onStartSuccess(settings);
-        verify(mBroadcastListener).onStatusChanged(eq(BroadcastCallback.STATUS_OK));
+        verify(mBroadcastListener, atLeast(1)).onStatusChanged(eq(BroadcastCallback.STATUS_OK));
     }
 
     @Test
@@ -81,7 +82,7 @@
         // advertising set can not be mocked, so we will allow nulls
         mBleBroadcastProvider.mAdvertisingSetCallback.onAdvertisingSetStarted(null, -30,
                 AdvertisingSetCallback.ADVERTISE_SUCCESS);
-        verify(mBroadcastListener).onStatusChanged(eq(BroadcastCallback.STATUS_OK));
+        verify(mBroadcastListener, atLeast(1)).onStatusChanged(eq(BroadcastCallback.STATUS_OK));
     }
 
     @Test
diff --git a/nearby/tests/unit/src/com/android/server/nearby/provider/ChreDiscoveryProviderTest.java b/nearby/tests/unit/src/com/android/server/nearby/provider/ChreDiscoveryProviderTest.java
index 154441b..590a46e 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/provider/ChreDiscoveryProviderTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/provider/ChreDiscoveryProviderTest.java
@@ -192,7 +192,12 @@
 
     @Test
     @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testOnNearbyDeviceDiscoveredWithDataElements() {
+    public void testOnNearbyDeviceDiscoveredWithDataElements_TIME() {
+        // The feature only supports user-debug builds.
+        if (!Build.isDebuggable()) {
+            return;
+        }
+
         // Disables the setting of test app support
         boolean isSupportedTestApp = getDeviceConfigBoolean(
                 NEARBY_SUPPORT_TEST_APP, false /* defaultValue */);
@@ -209,6 +214,7 @@
         // First byte is length of service data, padding zeros should be thrown away.
         final byte [] bleServiceData = new byte[] {5, 1, 2, 3, 4, 5, 0, 0, 0, 0};
         final byte [] testData = new byte[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
+        final long timestampNs = 1697765417070000000L;
 
         final List<DataElement> expectedExtendedProperties = new ArrayList<>();
         expectedExtendedProperties.add(new DataElement(DATA_TYPE_CONNECTION_STATUS_KEY,
@@ -262,6 +268,7 @@
                                 .setValue(ByteString.copyFrom(testData))
                                 .setValueLength(testData.length)
                         )
+                        .setTimestampNs(timestampNs)
                         .build();
         Blefilter.BleFilterResults results =
                 Blefilter.BleFilterResults.newBuilder().addResult(result).build();
@@ -285,11 +292,18 @@
             DeviceConfig.setProperty(NAMESPACE, NEARBY_SUPPORT_TEST_APP, "true", false);
             assertThat(new NearbyConfiguration().isTestAppSupported()).isTrue();
         }
+        // Nanoseconds to Milliseconds
+        assertThat((mNearbyDevice.getValue().getPresenceDevice())
+                .getDiscoveryTimestampMillis()).isEqualTo(timestampNs / 1000000);
     }
 
     @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    @SdkSuppress(minSdkVersion = 33, codeName = "T")
     public void testOnNearbyDeviceDiscoveredWithTestDataElements() {
+        // The feature only supports user-debug builds.
+        if (!Build.isDebuggable()) {
+            return;
+        }
         // Enables the setting of test app support
         boolean isSupportedTestApp = getDeviceConfigBoolean(
                 NEARBY_SUPPORT_TEST_APP, false /* defaultValue */);
diff --git a/nearby/tests/unit/src/com/android/server/nearby/util/ArrayUtilsTest.java b/nearby/tests/unit/src/com/android/server/nearby/util/ArrayUtilsTest.java
index a759baf..455c432 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/util/ArrayUtilsTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/util/ArrayUtilsTest.java
@@ -18,8 +18,6 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import androidx.test.filters.SdkSuppress;
-
 import org.junit.Test;
 
 public final class ArrayUtilsTest {
@@ -30,51 +28,58 @@
     private static final byte[] BYTES_ALL = new byte[] {7, 9, 8};
 
     @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
     public void testConcatByteArraysNoInput() {
         assertThat(ArrayUtils.concatByteArrays().length).isEqualTo(0);
     }
 
     @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
     public void testConcatByteArraysOneEmptyArray() {
         assertThat(ArrayUtils.concatByteArrays(BYTES_EMPTY).length).isEqualTo(0);
     }
 
     @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
     public void testConcatByteArraysOneNonEmptyArray() {
         assertThat(ArrayUtils.concatByteArrays(BYTES_ONE)).isEqualTo(BYTES_ONE);
     }
 
     @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
     public void testConcatByteArraysMultipleNonEmptyArrays() {
         assertThat(ArrayUtils.concatByteArrays(BYTES_ONE, BYTES_TWO)).isEqualTo(BYTES_ALL);
     }
 
     @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
     public void testConcatByteArraysMultipleArrays() {
         assertThat(ArrayUtils.concatByteArrays(BYTES_ONE, BYTES_EMPTY, BYTES_TWO))
                 .isEqualTo(BYTES_ALL);
     }
 
     @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
     public void testIsEmptyNull_returnsTrue() {
         assertThat(ArrayUtils.isEmpty(null)).isTrue();
     }
 
     @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
     public void testIsEmpty_returnsTrue() {
         assertThat(ArrayUtils.isEmpty(new byte[]{})).isTrue();
     }
 
     @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
     public void testIsEmpty_returnsFalse() {
         assertThat(ArrayUtils.isEmpty(BYTES_ALL)).isFalse();
     }
+
+    @Test
+    public void testAppendByte() {
+        assertThat(ArrayUtils.append((byte) 2, BYTES_ONE)).isEqualTo(new byte[]{2, 7, 9});
+    }
+
+    @Test
+    public void testAppendByteNull() {
+        assertThat(ArrayUtils.append((byte) 2, null)).isEqualTo(new byte[]{2});
+    }
+
+    @Test
+    public void testAppendByteToArray() {
+        assertThat(ArrayUtils.append(BYTES_ONE, (byte) 2)).isEqualTo(new byte[]{7, 9, 2});
+    }
 }
diff --git a/nearby/tests/unit/src/com/android/server/nearby/util/encryption/CryptorImpIdentityV1Test.java b/nearby/tests/unit/src/com/android/server/nearby/util/encryption/CryptorImpIdentityV1Test.java
deleted file mode 100644
index f0294fc..0000000
--- a/nearby/tests/unit/src/com/android/server/nearby/util/encryption/CryptorImpIdentityV1Test.java
+++ /dev/null
@@ -1,76 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.nearby.util.encryption;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import android.util.Log;
-
-import org.junit.Test;
-
-import java.util.Arrays;
-
-public class CryptorImpIdentityV1Test {
-    private static final String TAG = "CryptorImpIdentityV1Test";
-    private static final byte[] SALT = new byte[] {102, 22};
-    private static final byte[] DATA =
-            new byte[] {107, -102, 101, 107, 20, 62, 2, 73, 113, 59, 8, -14, -58, 122};
-    private static final byte[] AUTHENTICITY_KEY =
-            new byte[] {-89, 88, -50, -42, -99, 57, 84, -24, 121, 1, -104, -8, -26, -73, -36, 100};
-
-    @Test
-    public void test_encrypt_decrypt() {
-        Cryptor identityCryptor = CryptorImpIdentityV1.getInstance();
-        byte[] encryptedData = identityCryptor.encrypt(DATA, SALT, AUTHENTICITY_KEY);
-
-        assertThat(identityCryptor.decrypt(encryptedData, SALT, AUTHENTICITY_KEY)).isEqualTo(DATA);
-    }
-
-    @Test
-    public void test_encryption() {
-        Cryptor identityCryptor = CryptorImpIdentityV1.getInstance();
-        byte[] encryptedData = identityCryptor.encrypt(DATA, SALT, AUTHENTICITY_KEY);
-
-        // for debugging
-        Log.d(TAG, "encrypted data is: " + Arrays.toString(encryptedData));
-
-        assertThat(encryptedData).isEqualTo(getEncryptedData());
-    }
-
-    @Test
-    public void test_decryption() {
-        Cryptor identityCryptor = CryptorImpIdentityV1.getInstance();
-        byte[] decryptedData =
-                identityCryptor.decrypt(getEncryptedData(), SALT, AUTHENTICITY_KEY);
-        // for debugging
-        Log.d(TAG, "decrypted data is: " + Arrays.toString(decryptedData));
-
-        assertThat(decryptedData).isEqualTo(DATA);
-    }
-
-    @Test
-    public void generateHmacTag() {
-        CryptorImpIdentityV1 identityCryptor = CryptorImpIdentityV1.getInstance();
-        byte[] generatedTag = identityCryptor.sign(DATA);
-        byte[] expectedTag = new byte[]{50, 116, 95, -87, 63, 123, -79, -43};
-        assertThat(generatedTag).isEqualTo(expectedTag);
-    }
-
-    private static byte[] getEncryptedData() {
-        return new byte[]{6, -31, -32, -123, 43, -92, -47, -110, -65, 126, -15, -51, -19, -43};
-    }
-}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/util/encryption/CryptorImpV1Test.java b/nearby/tests/unit/src/com/android/server/nearby/util/encryption/CryptorImpV1Test.java
deleted file mode 100644
index 3ca2575..0000000
--- a/nearby/tests/unit/src/com/android/server/nearby/util/encryption/CryptorImpV1Test.java
+++ /dev/null
@@ -1,114 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.nearby.util.encryption;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import android.util.Log;
-
-import org.junit.Test;
-
-import java.util.Arrays;
-
-/**
- * Unit test for {@link CryptorImpV1}
- */
-public final class CryptorImpV1Test {
-    private static final String TAG = "CryptorImpV1Test";
-    private static final byte[] SALT = new byte[] {102, 22};
-    private static final byte[] DATA =
-            new byte[] {107, -102, 101, 107, 20, 62, 2, 73, 113, 59, 8, -14, -58, 122};
-    private static final byte[] AUTHENTICITY_KEY =
-            new byte[] {-89, 88, -50, -42, -99, 57, 84, -24, 121, 1, -104, -8, -26, -73, -36, 100};
-
-    @Test
-    public void test_encryption() {
-        Cryptor v1Cryptor = CryptorImpV1.getInstance();
-        byte[] encryptedData = v1Cryptor.encrypt(DATA, SALT, AUTHENTICITY_KEY);
-
-        // for debugging
-        Log.d(TAG, "encrypted data is: " + Arrays.toString(encryptedData));
-
-        assertThat(encryptedData).isEqualTo(getEncryptedData());
-    }
-
-    @Test
-    public void test_encryption_invalidInput() {
-        Cryptor v1Cryptor = CryptorImpV1.getInstance();
-        assertThat(v1Cryptor.encrypt(DATA, SALT, new byte[]{1, 2, 3, 4, 6})).isNull();
-    }
-
-    @Test
-    public void test_decryption() {
-        Cryptor v1Cryptor = CryptorImpV1.getInstance();
-        byte[] decryptedData =
-                v1Cryptor.decrypt(getEncryptedData(), SALT, AUTHENTICITY_KEY);
-        // for debugging
-        Log.d(TAG, "decrypted data is: " + Arrays.toString(decryptedData));
-
-        assertThat(decryptedData).isEqualTo(DATA);
-    }
-
-    @Test
-    public void test_decryption_invalidInput() {
-        Cryptor v1Cryptor = CryptorImpV1.getInstance();
-        assertThat(v1Cryptor.decrypt(getEncryptedData(), SALT, new byte[]{1, 2, 3, 4, 6})).isNull();
-    }
-
-    @Test
-    public void generateSign() {
-        CryptorImpV1 v1Cryptor = CryptorImpV1.getInstance();
-        byte[] generatedTag = v1Cryptor.sign(DATA, AUTHENTICITY_KEY);
-        byte[] expectedTag = new byte[]{
-                100, 88, -104, 80, -66, 107, -38, 95, 34, 40, -56, -23, -90, 90, -87, 12};
-        assertThat(generatedTag).isEqualTo(expectedTag);
-    }
-
-    @Test
-    public void test_verify() {
-        CryptorImpV1 v1Cryptor = CryptorImpV1.getInstance();
-        byte[] expectedTag = new byte[]{
-                100, 88, -104, 80, -66, 107, -38, 95, 34, 40, -56, -23, -90, 90, -87, 12};
-
-        assertThat(v1Cryptor.verify(DATA, AUTHENTICITY_KEY, expectedTag)).isTrue();
-        assertThat(v1Cryptor.verify(DATA, AUTHENTICITY_KEY, DATA)).isFalse();
-    }
-
-    @Test
-    public void test_generateHmacTag_sameResult() {
-        CryptorImpV1 v1Cryptor = CryptorImpV1.getInstance();
-        byte[] res1 = v1Cryptor.generateHmacTag(DATA, AUTHENTICITY_KEY);
-        assertThat(res1)
-                .isEqualTo(v1Cryptor.generateHmacTag(DATA, AUTHENTICITY_KEY));
-    }
-
-    @Test
-    public void test_generateHmacTag_nullData() {
-        CryptorImpV1 v1Cryptor = CryptorImpV1.getInstance();
-        assertThat(v1Cryptor.generateHmacTag(/* data= */ null, AUTHENTICITY_KEY)).isNull();
-    }
-
-    @Test
-    public void test_generateHmacTag_nullKey() {
-        CryptorImpV1 v1Cryptor = CryptorImpV1.getInstance();
-        assertThat(v1Cryptor.generateHmacTag(DATA, /* authenticityKey= */ null)).isNull();
-    }
-
-    private static byte[] getEncryptedData() {
-        return new byte[]{-92, 94, -99, -97, 81, -48, -7, 119, -64, -22, 45, -49, -50, 92};
-    }
-}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/util/encryption/CryptorMicImpTest.java b/nearby/tests/unit/src/com/android/server/nearby/util/encryption/CryptorMicImpTest.java
new file mode 100644
index 0000000..b6d2333
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/util/encryption/CryptorMicImpTest.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.util.encryption;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+/**
+ * Unit test for {@link CryptorMicImp}
+ */
+public final class CryptorMicImpTest {
+    private static final String TAG = "CryptorImpV1Test";
+    private static final byte[] SALT = new byte[]{102, 22};
+    private static final byte[] DATA =
+            new byte[]{107, -102, 101, 107, 20, 62, 2, 73, 113, 59, 8, -14, -58, 122};
+    private static final byte[] AUTHENTICITY_KEY =
+            new byte[]{-89, 88, -50, -42, -99, 57, 84, -24, 121, 1, -104, -8, -26, -73, -36, 100};
+
+    private static byte[] getEncryptedData() {
+        return new byte[]{112, 23, -111, 87, 122, -27, 45, -25, -35, 84, -89, 115, 61, 113};
+    }
+
+    @Test
+    public void test_encryption() throws Exception {
+        Cryptor v1Cryptor = CryptorMicImp.getInstance();
+        byte[] encryptedData =
+                v1Cryptor.encrypt(DATA,  CryptorMicImp.generateAdvNonce(SALT), AUTHENTICITY_KEY);
+        assertThat(encryptedData).isEqualTo(getEncryptedData());
+    }
+
+    @Test
+    public void test_decryption() throws Exception {
+        Cryptor v1Cryptor = CryptorMicImp.getInstance();
+        byte[] decryptedData =
+                v1Cryptor.decrypt(getEncryptedData(), CryptorMicImp.generateAdvNonce(SALT),
+                        AUTHENTICITY_KEY);
+        assertThat(decryptedData).isEqualTo(DATA);
+    }
+
+    @Test
+    public void test_verify() {
+        CryptorMicImp v1Cryptor = CryptorMicImp.getInstance();
+        byte[] expectedTag = new byte[]{
+                -80, -51, -101, -7, -65, 110, 37, 68, 122, -128, 57, -90, -115, -59, -61, 46};
+        assertThat(v1Cryptor.verify(DATA, AUTHENTICITY_KEY, expectedTag)).isTrue();
+        assertThat(v1Cryptor.verify(DATA, AUTHENTICITY_KEY, DATA)).isFalse();
+    }
+
+    @Test
+    public void test_generateHmacTag_sameResult() {
+        CryptorMicImp v1Cryptor = CryptorMicImp.getInstance();
+        byte[] res1 = v1Cryptor.generateHmacTag(DATA, AUTHENTICITY_KEY);
+        assertThat(res1)
+                .isEqualTo(v1Cryptor.generateHmacTag(DATA, AUTHENTICITY_KEY));
+    }
+
+    @Test
+    public void test_generateHmacTag_nullData() {
+        CryptorMicImp v1Cryptor = CryptorMicImp.getInstance();
+        assertThat(v1Cryptor.generateHmacTag(/* data= */ null, AUTHENTICITY_KEY)).isNull();
+    }
+
+    @Test
+    public void test_generateHmacTag_nullKey() {
+        CryptorMicImp v1Cryptor = CryptorMicImp.getInstance();
+        assertThat(v1Cryptor.generateHmacTag(DATA, /* authenticityKey= */ null)).isNull();
+    }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/util/encryption/CryptorTest.java b/nearby/tests/unit/src/com/android/server/nearby/util/encryption/CryptorTest.java
index ca612e3..1fb2236 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/util/encryption/CryptorTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/util/encryption/CryptorTest.java
@@ -42,7 +42,7 @@
         assertThat(res2).hasLength(outputSize);
         assertThat(res1).isNotEqualTo(res2);
         assertThat(res1)
-                .isEqualTo(CryptorImpV1.computeHkdf(DATA, AUTHENTICITY_KEY, outputSize));
+                .isEqualTo(CryptorMicImp.computeHkdf(DATA, AUTHENTICITY_KEY, outputSize));
     }
 
     @Test
diff --git a/netbpfload/Android.bp b/netbpfload/Android.bp
index 69bbf12..b5e4722 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",
 
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/BpfHandler.cpp b/netd/BpfHandler.cpp
index a7a4059..a00c363 100644
--- a/netd/BpfHandler.cpp
+++ b/netd/BpfHandler.cpp
@@ -53,14 +53,10 @@
                                     bpf_attach_type type) {
     unique_fd cgroupProg(retrieveProgram(programPath));
     if (!cgroupProg.ok()) {
-        const int err = errno;
-        ALOGE("Failed to get program from %s: %s", programPath, strerror(err));
-        return statusFromErrno(err, "cgroup program get failed");
+        return statusFromErrno(errno, fmt::format("Failed to get program from {}", programPath));
     }
     if (android::bpf::attachProgram(type, cgroupProg, cgroupFd)) {
-        const int err = errno;
-        ALOGE("Program from %s attach failed: %s", programPath, strerror(err));
-        return statusFromErrno(err, "program attach failed");
+        return statusFromErrno(errno, fmt::format("Program {} attach failed", programPath));
     }
     return netdutils::status::ok;
 }
@@ -68,31 +64,39 @@
 static Status checkProgramAccessible(const char* programPath) {
     unique_fd prog(retrieveProgram(programPath));
     if (!prog.ok()) {
-        const int err = errno;
-        ALOGE("Failed to get program from %s: %s", programPath, strerror(err));
-        return statusFromErrno(err, "program retrieve failed");
+        return statusFromErrno(errno, fmt::format("Failed to get program from {}", programPath));
     }
     return netdutils::status::ok;
 }
 
 static Status initPrograms(const char* cg2_path) {
+    if (!cg2_path) return Status("cg2_path is NULL");
+
     // This code was mainlined in T, so this should be trivially satisfied.
-    if (!modules::sdklevel::IsAtLeastT()) abort();
+    if (!modules::sdklevel::IsAtLeastT()) return Status("S- platform is unsupported");
 
     // S requires eBPF support which was only added in 4.9, so this should be satisfied.
-    if (!bpf::isAtLeastKernelVersion(4, 9, 0)) abort();
+    if (!bpf::isAtLeastKernelVersion(4, 9, 0)) {
+        return Status("kernel version < 4.9.0 is unsupported");
+    }
 
     // U bumps the kernel requirement up to 4.14
-    if (modules::sdklevel::IsAtLeastU() && !bpf::isAtLeastKernelVersion(4, 14, 0)) abort();
+    if (modules::sdklevel::IsAtLeastU() && !bpf::isAtLeastKernelVersion(4, 14, 0)) {
+        return Status("U+ platform with kernel version < 4.14.0 is unsupported");
+    }
 
     if (modules::sdklevel::IsAtLeastV()) {
         // V bumps the kernel requirement up to 4.19
         // see also: //system/netd/tests/kernel_test.cpp TestKernel419
-        if (!bpf::isAtLeastKernelVersion(4, 19, 0)) abort();
+        if (!bpf::isAtLeastKernelVersion(4, 19, 0)) {
+            return Status("V+ platform with kernel version < 4.19.0 is unsupported");
+        }
 
         // Technically already required by U, but only enforce on V+
         // see also: //system/netd/tests/kernel_test.cpp TestKernel64Bit
-        if (bpf::isKernel32Bit() && bpf::isAtLeastKernelVersion(5, 16, 0)) abort();
+        if (bpf::isKernel32Bit() && bpf::isAtLeastKernelVersion(5, 16, 0)) {
+            return Status("V+ platform with 32 bit kernel, version >= 5.16.0 is unsupported");
+        }
     }
 
     // Linux 6.1 is highest version supported by U, starting with V new kernels,
@@ -102,10 +106,14 @@
     // it does not affect unprivileged apps, the 32-on-64 compatibility
     // problems are AFAIK limited to various CAP_NET_ADMIN protected interfaces.
     // see also: //system/bpf/bpfloader/BpfLoader.cpp main()
-    if (bpf::isUserspace32bit() && bpf::isAtLeastKernelVersion(6, 2, 0)) abort();
+    if (bpf::isUserspace32bit() && bpf::isAtLeastKernelVersion(6, 2, 0)) {
+        return Status("32 bit userspace with Kernel version >= 6.2.0 is unsupported");
+    }
 
     // U mandates this mount point (though it should also be the case on T)
-    if (modules::sdklevel::IsAtLeastU() && !!strcmp(cg2_path, "/sys/fs/cgroup")) abort();
+    if (modules::sdklevel::IsAtLeastU() && !!strcmp(cg2_path, "/sys/fs/cgroup")) {
+        return Status("U+ platform with cg2_path != /sys/fs/cgroup is unsupported");
+    }
 
     unique_fd cg_fd(open(cg2_path, O_DIRECTORY | O_RDONLY | O_CLOEXEC));
     if (!cg_fd.ok()) {
diff --git a/netd/NetdUpdatable.cpp b/netd/NetdUpdatable.cpp
index 41b1fdb..3b15916 100644
--- a/netd/NetdUpdatable.cpp
+++ b/netd/NetdUpdatable.cpp
@@ -31,8 +31,8 @@
 
     android::netdutils::Status ret = sBpfHandler.init(cg2_path);
     if (!android::netdutils::isOk(ret)) {
-        LOG(ERROR) << __func__ << ": BPF handler init failed";
-        return -ret.code();
+        LOG(ERROR) << __func__ << ": Failed: (" << ret.code() << ") " << ret.msg();
+        abort();
     }
     return 0;
 }
diff --git a/remoteauth/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 98ed2b2..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"],
 }
 
@@ -33,6 +34,7 @@
     libs: [
         "androidx.annotation_annotation",
         "framework-bluetooth",
+        "framework-connectivity",
         "error_prone_annotations",
         "framework-configinfrastructure",
         "framework-connectivity-pre-jarjar",
diff --git a/remoteauth/service/java/com/android/server/remoteauth/connectivity/CdmConnectionInfo.java b/remoteauth/service/java/com/android/server/remoteauth/connectivity/CdmConnectionInfo.java
index 8bfdd36..d2a828c 100644
--- a/remoteauth/service/java/com/android/server/remoteauth/connectivity/CdmConnectionInfo.java
+++ b/remoteauth/service/java/com/android/server/remoteauth/connectivity/CdmConnectionInfo.java
@@ -34,11 +34,11 @@
 // TODO(b/295407748): Change to use @DataClass.
 // TODO(b/296625303): Change to VANILLA_ICE_CREAM when AssociationInfo is available in V.
 @TargetApi(Build.VERSION_CODES.TIRAMISU)
-public final class CdmConnectionInfo extends ConnectionInfo {
+public final class CdmConnectionInfo extends ConnectionInfo<AssociationInfo> {
     @NonNull private final AssociationInfo mAssociationInfo;
 
     public CdmConnectionInfo(int connectionId, @NonNull AssociationInfo associationInfo) {
-       super(connectionId);
+        super(connectionId);
         mAssociationInfo = associationInfo;
     }
 
@@ -78,10 +78,6 @@
         out.writeTypedObject(mAssociationInfo, 0);
     }
 
-    public AssociationInfo getAssociationInfo() {
-        return mAssociationInfo;
-    }
-
     /** Returns a string representation of ConnectionInfo. */
     @Override
     public String toString() {
@@ -98,11 +94,16 @@
         }
 
         CdmConnectionInfo other = (CdmConnectionInfo) o;
-        return mAssociationInfo.equals(other.getAssociationInfo());
+        return super.equals(o) && mAssociationInfo.equals(other.getConnectionParams());
     }
 
     @Override
     public int hashCode() {
         return Objects.hash(mAssociationInfo);
     }
+
+    @Override
+    public AssociationInfo getConnectionParams() {
+        return mAssociationInfo;
+    }
 }
diff --git a/remoteauth/service/java/com/android/server/remoteauth/connectivity/CdmConnectivityManager.java b/remoteauth/service/java/com/android/server/remoteauth/connectivity/CdmConnectivityManager.java
new file mode 100644
index 0000000..49745c0
--- /dev/null
+++ b/remoteauth/service/java/com/android/server/remoteauth/connectivity/CdmConnectivityManager.java
@@ -0,0 +1,201 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.remoteauth.connectivity;
+
+import static com.android.server.remoteauth.connectivity.DiscoveryFilter.DeviceType;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.TargetApi;
+import android.companion.AssociationInfo;
+import android.companion.AssociationRequest;
+import android.os.Build;
+import android.util.Log;
+
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.Preconditions;
+
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Future;
+import java.util.concurrent.RejectedExecutionException;
+
+/**
+ * Discovers devices associated with the companion device manager.
+ *
+ * TODO(b/296625303): Change to VANILLA_ICE_CREAM when AssociationInfo is available in V.
+ */
+@TargetApi(Build.VERSION_CODES.TIRAMISU)
+public class CdmConnectivityManager implements ConnectivityManager {
+    private static final String TAG = "CdmConnectivityManager";
+
+    private final CompanionDeviceManagerWrapper mCompanionDeviceManagerWrapper;
+
+    private ExecutorService mExecutor;
+    private Map<DiscoveredDeviceReceiver, Future> mPendingDiscoveryCallbacks =
+            new ConcurrentHashMap<>();
+
+    public CdmConnectivityManager(
+            @NonNull ExecutorService executor,
+            @NonNull CompanionDeviceManagerWrapper companionDeviceManagerWrapper) {
+        mExecutor = executor;
+        mCompanionDeviceManagerWrapper = companionDeviceManagerWrapper;
+    }
+
+    /**
+     * Runs associated discovery callbacks for discovered devices.
+     *
+     * @param discoveredDeviceReceiver callback.
+     * @param device discovered device.
+     */
+    private void notifyOnDiscovered(
+            @NonNull DiscoveredDeviceReceiver discoveredDeviceReceiver,
+            @NonNull DiscoveredDevice device) {
+        Preconditions.checkNotNull(discoveredDeviceReceiver);
+        Preconditions.checkNotNull(device);
+
+        Log.i(TAG, "Notifying discovered device");
+        discoveredDeviceReceiver.onDiscovered(device);
+    }
+
+    /**
+     * Posts an async task to discover CDM associations and run callback if device is discovered.
+     *
+     * @param discoveryFilter filter for association.
+     * @param discoveredDeviceReceiver callback.
+     */
+    private void startDiscoveryAsync(@NonNull DiscoveryFilter discoveryFilter,
+            @NonNull DiscoveredDeviceReceiver discoveredDeviceReceiver) {
+        Preconditions.checkNotNull(discoveryFilter);
+        Preconditions.checkNotNull(discoveredDeviceReceiver);
+
+        List<AssociationInfo> associations = mCompanionDeviceManagerWrapper.getAllAssociations();
+        Log.i(TAG, "Found associations: " + associations.size());
+        for (AssociationInfo association : associations) {
+            String deviceProfile = getDeviceProfileFromType(discoveryFilter.getDeviceType());
+            // TODO(b/297574984): Include device presence signal before notifying discovery result.
+            if (mCompanionDeviceManagerWrapper.getDeviceProfile(association)
+                    .equals(deviceProfile)) {
+                notifyOnDiscovered(
+                        discoveredDeviceReceiver,
+                        createDiscoveredDeviceFromAssociation(association));
+            }
+        }
+    }
+
+    /**
+     * Returns the device profile from association info.
+     *
+     * @param deviceType Discovery filter device type.
+     * @return Device profile string defined in {@link AssociationRequest}.
+     * @throws AssertionError if type cannot be mapped.
+     */
+    private String getDeviceProfileFromType(@DeviceType int deviceType) {
+        if (deviceType == DiscoveryFilter.WATCH) {
+            return AssociationRequest.DEVICE_PROFILE_WATCH;
+        } else {
+            // Should not reach here.
+            throw new AssertionError(deviceType);
+        }
+    }
+
+    /**
+     * Creates discovered device from association info.
+     *
+     * @param info Association info.
+     * @return discovered device object.
+     */
+    private @NonNull DiscoveredDevice createDiscoveredDeviceFromAssociation(
+            @NonNull AssociationInfo info) {
+        return new DiscoveredDevice(
+                new CdmConnectionInfo(info.getId(), info),
+                info.getDisplayName() == null ? "" : info.getDisplayName().toString());
+    }
+
+    /**
+     * Triggers the discovery for CDM associations.
+     *
+     * Runs discovery only if a callback has not been previously registered.
+     *
+     * @param discoveryFilter filter for associations.
+     * @param discoveredDeviceReceiver callback to be run on discovery result.
+     */
+    @Override
+    public void startDiscovery(
+            @NonNull DiscoveryFilter discoveryFilter,
+            @NonNull DiscoveredDeviceReceiver discoveredDeviceReceiver) {
+        Preconditions.checkNotNull(mCompanionDeviceManagerWrapper);
+        Preconditions.checkNotNull(discoveryFilter);
+        Preconditions.checkNotNull(discoveredDeviceReceiver);
+
+        try {
+            mPendingDiscoveryCallbacks.computeIfAbsent(
+                    discoveredDeviceReceiver,
+                    discoveryFuture -> mExecutor.submit(
+                        () -> startDiscoveryAsync(discoveryFilter, discoveryFuture),
+                        /* result= */ null));
+        } catch (RejectedExecutionException | NullPointerException e) {
+            Log.e(TAG, "Failed to start async discovery: " + e.getMessage());
+        }
+    }
+
+    /** Stops discovery. */
+    @Override
+    public void stopDiscovery(
+            @NonNull DiscoveryFilter discoveryFilter,
+            @NonNull DiscoveredDeviceReceiver discoveredDeviceReceiver) {
+        Preconditions.checkNotNull(discoveryFilter);
+        Preconditions.checkNotNull(discoveredDeviceReceiver);
+
+        Future<Void> discoveryFuture = mPendingDiscoveryCallbacks.remove(discoveredDeviceReceiver);
+        if (null != discoveryFuture && !discoveryFuture.cancel(/* mayInterruptIfRunning= */ true)) {
+            Log.d(TAG, "Discovery was possibly completed.");
+        }
+    }
+
+    @Nullable
+    @Override
+    public Connection connect(@NonNull ConnectionInfo connectionInfo,
+            @NonNull EventListener eventListener) {
+        // Not implemented.
+        return null;
+    }
+
+    @Override
+    public void startListening(MessageReceiver messageReceiver) {
+        // Not implemented.
+    }
+
+    @Override
+    public void stopListening(MessageReceiver messageReceiver) {
+        // Not implemented.
+    }
+
+    /**
+     * Returns whether the callback is already registered and pending.
+     *
+     * @param discoveredDeviceReceiver callback
+     * @return true if the callback is pending, false otherwise.
+     */
+    @VisibleForTesting
+    boolean hasPendingCallbacks(@NonNull DiscoveredDeviceReceiver discoveredDeviceReceiver) {
+        return mPendingDiscoveryCallbacks.containsKey(discoveredDeviceReceiver);
+    }
+}
diff --git a/remoteauth/service/java/com/android/server/remoteauth/connectivity/CompanionDeviceManagerWrapper.java b/remoteauth/service/java/com/android/server/remoteauth/connectivity/CompanionDeviceManagerWrapper.java
new file mode 100644
index 0000000..eaf3edb
--- /dev/null
+++ b/remoteauth/service/java/com/android/server/remoteauth/connectivity/CompanionDeviceManagerWrapper.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.remoteauth.connectivity;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.TargetApi;
+import android.companion.AssociationInfo;
+import android.companion.CompanionDeviceManager;
+import android.content.Context;
+import android.os.Build;
+import android.util.Log;
+
+import java.util.List;
+
+/** Wraps {@link android.companion.CompanionDeviceManager} for easier testing. */
+// TODO(b/296625303): Change to VANILLA_ICE_CREAM when AssociationInfo is available in V.
+@TargetApi(Build.VERSION_CODES.TIRAMISU)
+public class CompanionDeviceManagerWrapper {
+    private static final String TAG = "CompanionDeviceManagerWrapper";
+
+    private Context mContext;
+    private CompanionDeviceManager mCompanionDeviceManager;
+
+    public CompanionDeviceManagerWrapper(@NonNull Context context) {
+        mContext = context;
+    }
+
+    /**
+     * Returns device profile string from the association info.
+     *
+     * @param associationInfo the association info.
+     * @return String indicating device profile
+     */
+    @Nullable
+    public String getDeviceProfile(@NonNull AssociationInfo associationInfo) {
+        return associationInfo.getDeviceProfile();
+    }
+
+    /**
+     * Returns all associations.
+     *
+     * @return associations or null if no associated devices present.
+     */
+    @Nullable
+    public List<AssociationInfo> getAllAssociations() {
+        if (mCompanionDeviceManager == null) {
+            try {
+                mCompanionDeviceManager = mContext.getSystemService(CompanionDeviceManager.class);
+            } catch (NullPointerException e) {
+                Log.e(TAG, "CompanionDeviceManager service does not exist: " + e);
+                return null;
+            }
+        }
+
+        try {
+            return mCompanionDeviceManager.getAllAssociations();
+        } catch (RuntimeException e) {
+            Log.e(TAG, "Failed to get CompanionDeviceManager associations: " + e.getMessage());
+        }
+        return null;
+    }
+}
diff --git a/remoteauth/service/java/com/android/server/remoteauth/connectivity/Connection.java b/remoteauth/service/java/com/android/server/remoteauth/connectivity/Connection.java
index eb5458d..dca8b9f 100644
--- a/remoteauth/service/java/com/android/server/remoteauth/connectivity/Connection.java
+++ b/remoteauth/service/java/com/android/server/remoteauth/connectivity/Connection.java
@@ -25,7 +25,6 @@
  * A connection with the peer device.
  *
  * <p>Connections are used to exchange data with the peer device.
- *
  */
 public interface Connection {
     /** Unknown error. */
diff --git a/remoteauth/service/java/com/android/server/remoteauth/connectivity/ConnectionInfo.java b/remoteauth/service/java/com/android/server/remoteauth/connectivity/ConnectionInfo.java
index 39bfa8d..6e0edb4 100644
--- a/remoteauth/service/java/com/android/server/remoteauth/connectivity/ConnectionInfo.java
+++ b/remoteauth/service/java/com/android/server/remoteauth/connectivity/ConnectionInfo.java
@@ -28,9 +28,10 @@
  * <p>Connection information captures the details of underlying connection such as connection id,
  * type of connection and peer device mac address.
  *
+ * @param <T> connection params per connection type.
  */
 // TODO(b/295407748) Change to use @DataClass.
-public abstract class ConnectionInfo implements Parcelable {
+public abstract class ConnectionInfo<T> implements Parcelable {
     int mConnectionId;
 
     public ConnectionInfo(int connectionId) {
@@ -85,4 +86,9 @@
     public int hashCode() {
         return Objects.hash(mConnectionId);
     }
+
+    /**
+     * Returns connection related parameters.
+     */
+    public abstract T getConnectionParams();
 }
diff --git a/remoteauth/service/java/com/android/server/remoteauth/connectivity/ConnectivityManager.java b/remoteauth/service/java/com/android/server/remoteauth/connectivity/ConnectivityManager.java
index bc0d77e..7b30285 100644
--- a/remoteauth/service/java/com/android/server/remoteauth/connectivity/ConnectivityManager.java
+++ b/remoteauth/service/java/com/android/server/remoteauth/connectivity/ConnectivityManager.java
@@ -23,9 +23,7 @@
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 
-/**
- * Performs discovery and triggers a connection to an associated device.
- */
+/** Performs discovery and triggers a connection to an associated device. */
 public interface ConnectivityManager {
     /**
      * Starts device discovery.
@@ -63,8 +61,12 @@
 
     /** Reason codes for connect failure. */
     @Retention(RetentionPolicy.SOURCE)
-    @IntDef({ERROR_REASON_UNKNOWN, ERROR_CONNECTION_TIMED_OUT, ERROR_CONNECTION_REFUSED,
-            ERROR_DEVICE_UNAVAILABLE})
+    @IntDef({
+        ERROR_REASON_UNKNOWN,
+        ERROR_CONNECTION_TIMED_OUT,
+        ERROR_CONNECTION_REFUSED,
+        ERROR_DEVICE_UNAVAILABLE
+    })
     @interface ReasonCode {}
 
     /**
diff --git a/remoteauth/service/java/com/android/server/remoteauth/connectivity/ConnectivityManagerFactory.java b/remoteauth/service/java/com/android/server/remoteauth/connectivity/ConnectivityManagerFactory.java
new file mode 100644
index 0000000..3407fca
--- /dev/null
+++ b/remoteauth/service/java/com/android/server/remoteauth/connectivity/ConnectivityManagerFactory.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.remoteauth.connectivity;
+
+import android.annotation.NonNull;
+import android.content.Context;
+
+import com.android.internal.util.Preconditions;
+
+import java.lang.ref.WeakReference;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+/**
+ * Factory class to create different types of connectivity managers based on the underlying
+ * network transports (for example CompanionDeviceManager).
+ */
+public final class ConnectivityManagerFactory {
+    private static final ExecutorService EXECUTOR = Executors.newSingleThreadExecutor();
+
+    /**
+     * Creates and returns a ConnectivityManager object depending on connection type.
+     *
+     * @param context of the caller.
+     * @return ConnectivityManager object.
+     */
+    public static ConnectivityManager getConnectivityManager(@NonNull Context context) {
+        Preconditions.checkNotNull(context);
+
+        // For now, we only have one case, but ideally this should create a new type based on some
+        // feature flag.
+        return new CdmConnectivityManager(EXECUTOR, new CompanionDeviceManagerWrapper(
+                new WeakReference<>(context.getApplicationContext()).get()));
+    }
+
+    private ConnectivityManagerFactory() {}
+}
diff --git a/remoteauth/service/java/com/android/server/remoteauth/connectivity/DiscoveredDevice.java b/remoteauth/service/java/com/android/server/remoteauth/connectivity/DiscoveredDevice.java
index a3e1e58..8cad3eb 100644
--- a/remoteauth/service/java/com/android/server/remoteauth/connectivity/DiscoveredDevice.java
+++ b/remoteauth/service/java/com/android/server/remoteauth/connectivity/DiscoveredDevice.java
@@ -25,8 +25,7 @@
     private @NonNull ConnectionInfo mConnectionInfo;
     private @Nullable String mDisplayName;
 
-    public DiscoveredDevice(
-            @NonNull ConnectionInfo connectionInfo, @Nullable String displayName) {
+    public DiscoveredDevice(@NonNull ConnectionInfo connectionInfo, @Nullable String displayName) {
         this.mConnectionInfo = connectionInfo;
         this.mDisplayName = displayName;
     }
diff --git a/remoteauth/service/java/com/android/server/remoteauth/connectivity/DiscoveryFilter.java b/remoteauth/service/java/com/android/server/remoteauth/connectivity/DiscoveryFilter.java
index 36c4b60..3590f20 100644
--- a/remoteauth/service/java/com/android/server/remoteauth/connectivity/DiscoveryFilter.java
+++ b/remoteauth/service/java/com/android/server/remoteauth/connectivity/DiscoveryFilter.java
@@ -31,7 +31,6 @@
  * the filter criteria (device type, name or peer address).
  */
 public final class DiscoveryFilter {
-
     /** Device type WATCH. */
     public static final int WATCH = 0;
 
@@ -86,7 +85,7 @@
         private Builder() {}
 
         /** Static method to create a new builder */
-        public static Builder newInstance() {
+        public static Builder builder() {
             return new Builder();
         }
 
diff --git a/remoteauth/service/java/com/android/server/remoteauth/connectivity/EventListener.java b/remoteauth/service/java/com/android/server/remoteauth/connectivity/EventListener.java
index 8ec851a..eaf57e0 100644
--- a/remoteauth/service/java/com/android/server/remoteauth/connectivity/EventListener.java
+++ b/remoteauth/service/java/com/android/server/remoteauth/connectivity/EventListener.java
@@ -18,9 +18,7 @@
 
 import android.annotation.NonNull;
 
-/**
- * Listens to the events from underlying transport.
- */
+/** Listens to the events from underlying transport. */
 public interface EventListener {
     /** Called when remote device is disconnected from the underlying transport. */
     void onDisconnect(@NonNull ConnectionInfo connectionInfo);
diff --git a/remoteauth/service/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/tests/unit/Android.bp b/remoteauth/tests/unit/Android.bp
index a21c033..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"],
 }
 
@@ -32,17 +33,20 @@
         "android.test.base",
         "android.test.mock",
         "android.test.runner",
+        "framework-annotations-lib",
     ],
     compile_multilib: "both",
 
     static_libs: [
         "androidx.test.ext.junit",
+        "androidx.test.ext.truth",
         "androidx.test.rules",
         "com.uwb.support.generic",
         "framework-remoteauth-static",
         "junit",
         "libprotobuf-java-lite",
         "mockito-target-extended-minus-junit4",
+        "mockito-target-minus-junit4",
         "platform-test-annotations",
         "service-remoteauth-pre-jarjar",
         "truth",
diff --git a/remoteauth/tests/unit/AndroidManifest.xml b/remoteauth/tests/unit/AndroidManifest.xml
index 0449409..a5294c8 100644
--- a/remoteauth/tests/unit/AndroidManifest.xml
+++ b/remoteauth/tests/unit/AndroidManifest.xml
@@ -31,5 +31,6 @@
     <instrumentation
         android:name="androidx.test.runner.AndroidJUnitRunner"
         android:targetPackage="android.remoteauth.test"
-        android:label="RemoteAuth Mainline Module Tests" />
+        android:label="RemoteAuth Mainline Module Tests"
+        android:directBootAware="true"/>
 </manifest>
diff --git a/remoteauth/tests/unit/src/com/android/server/remoteauth/connectivity/CdmConnectivityManagerTest.java b/remoteauth/tests/unit/src/com/android/server/remoteauth/connectivity/CdmConnectivityManagerTest.java
new file mode 100644
index 0000000..824344a
--- /dev/null
+++ b/remoteauth/tests/unit/src/com/android/server/remoteauth/connectivity/CdmConnectivityManagerTest.java
@@ -0,0 +1,304 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.remoteauth.connectivity;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.companion.AssociationInfo;
+import android.companion.AssociationRequest;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.google.common.util.concurrent.SettableFuture;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+/** Unit tests for {@link CdmConnectivityManager}. */
+@RunWith(AndroidJUnit4.class)
+public class CdmConnectivityManagerTest {
+    @Rule public final MockitoRule mocks = MockitoJUnit.rule();
+
+    @Mock CompanionDeviceManagerWrapper mCompanionDeviceManagerWrapper;
+
+    private CdmConnectivityManager mCdmConnectivityManager;
+    private ExecutorService mTestExecutor = Executors.newSingleThreadExecutor();
+
+    @Before
+    public void setUp() {
+        mCdmConnectivityManager =
+                new CdmConnectivityManager(mTestExecutor, mCompanionDeviceManagerWrapper);
+    }
+
+    @After
+    public void tearDown() {
+        mTestExecutor.shutdown();
+    }
+
+    @Test
+    public void testStartDiscovery_callsGetAllAssociationsOnce() throws InterruptedException {
+        mCdmConnectivityManager.startDiscovery(
+                Utils.getFakeDiscoveryFilter(), Utils.getFakeDiscoveredDeviceReceiver());
+
+        mTestExecutor.awaitTermination(1, TimeUnit.SECONDS);
+
+        verify(mCompanionDeviceManagerWrapper, times(1)).getAllAssociations();
+    }
+
+    @Test
+    public void testStartDiscovery_fetchesNoAssociations() {
+        SettableFuture<Boolean> future = SettableFuture.create();
+
+        when(mCompanionDeviceManagerWrapper.getAllAssociations())
+                .thenReturn(Utils.getFakeAssociationInfoList(0));
+
+        DiscoveredDeviceReceiver discoveredDeviceReceiver =
+                new DiscoveredDeviceReceiver() {
+                    @Override
+                    public void onDiscovered(DiscoveredDevice unused) {
+                        future.set(true);
+                    }
+
+                    @Override
+                    public void onLost(DiscoveredDevice unused) {
+                        future.set(true);
+                    }
+                };
+
+        mCdmConnectivityManager.startDiscovery(
+                Utils.getFakeDiscoveryFilter(), discoveredDeviceReceiver);
+
+        assertThat(future.isDone()).isFalse();
+    }
+
+    @Test
+    public void testStartDiscovery_DoesNotReturnNonWatchAssociations() throws InterruptedException {
+        SettableFuture<Boolean> future = SettableFuture.create();
+        DiscoveredDeviceReceiver discoveredDeviceReceiver =
+                new DiscoveredDeviceReceiver() {
+                    @Override
+                    public void onDiscovered(DiscoveredDevice unused) {
+                        future.set(true);
+                    }
+
+                    @Override
+                    public void onLost(DiscoveredDevice unused) {
+                        future.set(true);
+                    }
+                };
+
+        when(mCompanionDeviceManagerWrapper.getDeviceProfile(any(AssociationInfo.class)))
+                .thenReturn(Utils.FAKE_DEVICE_PROFILE);
+
+        mCdmConnectivityManager.startDiscovery(
+                Utils.getFakeDiscoveryFilter(), discoveredDeviceReceiver);
+
+        mTestExecutor.awaitTermination(1, TimeUnit.SECONDS);
+
+        verify(mCompanionDeviceManagerWrapper, times(1)).getAllAssociations();
+        verify(mCompanionDeviceManagerWrapper, times(0))
+                .getDeviceProfile(any(AssociationInfo.class));
+        assertThat(future.isDone()).isFalse();
+    }
+
+    @Test
+    public void testStartDiscovery_returnsOneWatchAssociation() throws InterruptedException {
+        SettableFuture<Boolean> future = SettableFuture.create();
+        DiscoveredDeviceReceiver discoveredDeviceReceiver =
+                new DiscoveredDeviceReceiver() {
+                    @Override
+                    public void onDiscovered(DiscoveredDevice unused) {
+                        future.set(true);
+                    }
+                };
+
+        when(mCompanionDeviceManagerWrapper.getAllAssociations())
+                .thenReturn(Utils.getFakeAssociationInfoList(1));
+        when(mCompanionDeviceManagerWrapper.getDeviceProfile(any(AssociationInfo.class)))
+                .thenReturn(AssociationRequest.DEVICE_PROFILE_WATCH);
+
+        mCdmConnectivityManager.startDiscovery(
+                Utils.getFakeDiscoveryFilter(), discoveredDeviceReceiver);
+
+        mTestExecutor.awaitTermination(1, TimeUnit.SECONDS);
+
+        verify(mCompanionDeviceManagerWrapper, times(1)).getAllAssociations();
+        verify(mCompanionDeviceManagerWrapper, times(1))
+                .getDeviceProfile(any(AssociationInfo.class));
+        assertThat(future.isDone()).isTrue();
+    }
+
+    @Test
+    public void testStartDiscovery_returnsMultipleWatchAssociations() throws InterruptedException {
+        int numAssociations = 3;
+        SettableFuture<Boolean> future = SettableFuture.create();
+        DiscoveredDeviceReceiver discoveredDeviceReceiver =
+                new DiscoveredDeviceReceiver() {
+                    int mNumCallbacks = 0;
+
+                    @Override
+                    public void onDiscovered(DiscoveredDevice unused) {
+                        ++mNumCallbacks;
+                        if (mNumCallbacks == numAssociations) {
+                            future.set(true);
+                        }
+                    }
+                };
+
+        when(mCompanionDeviceManagerWrapper.getAllAssociations())
+                .thenReturn(Utils.getFakeAssociationInfoList(numAssociations));
+        when(mCompanionDeviceManagerWrapper.getDeviceProfile(any(AssociationInfo.class)))
+                .thenReturn(AssociationRequest.DEVICE_PROFILE_WATCH);
+
+        mCdmConnectivityManager.startDiscovery(
+                Utils.getFakeDiscoveryFilter(), discoveredDeviceReceiver);
+
+        mTestExecutor.awaitTermination(1, TimeUnit.SECONDS);
+
+        verify(mCompanionDeviceManagerWrapper, times(1)).getAllAssociations();
+        verify(mCompanionDeviceManagerWrapper, times(numAssociations))
+                .getDeviceProfile(any(AssociationInfo.class));
+        assertThat(future.isDone()).isTrue();
+    }
+
+    @Test
+    public void testMultipleStartDiscovery_runsAllCallbacks() throws InterruptedException {
+        SettableFuture<Boolean> future1 = SettableFuture.create();
+        SettableFuture<Boolean> future2 = SettableFuture.create();
+        DiscoveredDeviceReceiver discoveredDeviceReceiver1 =
+                new DiscoveredDeviceReceiver() {
+                    @Override
+                    public void onDiscovered(DiscoveredDevice unused) {
+                        future1.set(true);
+                    }
+                };
+        DiscoveredDeviceReceiver discoveredDeviceReceiver2 =
+                new DiscoveredDeviceReceiver() {
+                    @Override
+                    public void onDiscovered(DiscoveredDevice unused) {
+                        future2.set(true);
+                    }
+                };
+
+        when(mCompanionDeviceManagerWrapper.getAllAssociations())
+                .thenReturn(Utils.getFakeAssociationInfoList(1));
+        when(mCompanionDeviceManagerWrapper.getDeviceProfile(any(AssociationInfo.class)))
+                .thenReturn(AssociationRequest.DEVICE_PROFILE_WATCH);
+
+        // Start discovery twice with different callbacks.
+        mCdmConnectivityManager.startDiscovery(
+                Utils.getFakeDiscoveryFilter(), discoveredDeviceReceiver1);
+        mCdmConnectivityManager.startDiscovery(
+                Utils.getFakeDiscoveryFilter(), discoveredDeviceReceiver2);
+
+        mTestExecutor.awaitTermination(1, TimeUnit.SECONDS);
+
+        verify(mCompanionDeviceManagerWrapper, times(2)).getAllAssociations();
+        verify(mCompanionDeviceManagerWrapper, times(2))
+                .getDeviceProfile(any(AssociationInfo.class));
+        assertThat(future1.isDone()).isTrue();
+        assertThat(future2.isDone()).isTrue();
+    }
+
+    @Test
+    public void testStartDiscovery_returnsExpectedDiscoveredDevice() throws InterruptedException {
+        SettableFuture<Boolean> future = SettableFuture.create();
+        DiscoveredDeviceReceiver discoveredDeviceReceiver =
+                new DiscoveredDeviceReceiver() {
+                    @Override
+                    public void onDiscovered(DiscoveredDevice device) {
+                        assertThat(device.getConnectionInfo() instanceof CdmConnectionInfo)
+                                .isTrue();
+
+                        CdmConnectionInfo connectionInfo =
+                                (CdmConnectionInfo) device.getConnectionInfo();
+                        if (connectionInfo.getConnectionParams().getDeviceMacAddress().toString()
+                                .equals(Utils.FAKE_PEER_ADDRESS)
+                                && connectionInfo.getConnectionId() == Utils.FAKE_CONNECTION_ID) {
+                            future.set(true);
+                        }
+                    }
+                };
+
+        when(mCompanionDeviceManagerWrapper.getAllAssociations())
+                .thenReturn(Utils.getFakeAssociationInfoList(1));
+        when(mCompanionDeviceManagerWrapper.getDeviceProfile(any(AssociationInfo.class)))
+                .thenReturn(AssociationRequest.DEVICE_PROFILE_WATCH);
+
+        mCdmConnectivityManager.startDiscovery(
+                Utils.getFakeDiscoveryFilter(), discoveredDeviceReceiver);
+
+        mTestExecutor.awaitTermination(1, TimeUnit.SECONDS);
+
+        verify(mCompanionDeviceManagerWrapper, times(1)).getAllAssociations();
+        verify(mCompanionDeviceManagerWrapper, times(1))
+                .getDeviceProfile(any(AssociationInfo.class));
+        assertThat(future.isDone()).isTrue();
+    }
+
+    @Test
+    public void testStopDiscovery_removesCallback() {
+        DiscoveredDeviceReceiver discoveredDeviceReceiver =
+                new DiscoveredDeviceReceiver() {
+                    @Override
+                    public void onDiscovered(DiscoveredDevice unused) {}
+                };
+
+        DiscoveryFilter discoveryFilter = Utils.getFakeDiscoveryFilter();
+        mCdmConnectivityManager.startDiscovery(discoveryFilter, discoveredDeviceReceiver);
+
+        assertThat(mCdmConnectivityManager.hasPendingCallbacks(discoveredDeviceReceiver)).isTrue();
+
+        mCdmConnectivityManager.stopDiscovery(discoveryFilter, discoveredDeviceReceiver);
+
+        assertThat(mCdmConnectivityManager.hasPendingCallbacks(discoveredDeviceReceiver)).isFalse();
+    }
+
+    @Test
+    public void testStopDiscovery_DoesNotRunCallback() {
+        SettableFuture future = SettableFuture.create();
+        DiscoveredDeviceReceiver discoveredDeviceReceiver =
+                new DiscoveredDeviceReceiver() {
+                    @Override
+                    public void onDiscovered(DiscoveredDevice unused) {
+                        future.set(true);
+                    }
+                };
+
+        DiscoveryFilter discoveryFilter = Utils.getFakeDiscoveryFilter();
+        mCdmConnectivityManager.startDiscovery(discoveryFilter, discoveredDeviceReceiver);
+        mCdmConnectivityManager.stopDiscovery(discoveryFilter, discoveredDeviceReceiver);
+
+        assertThat(future.isDone()).isFalse();
+    }
+}
+
diff --git a/remoteauth/tests/unit/src/com/android/server/remoteauth/connectivity/ConnectivityManagerFactoryTest.java b/remoteauth/tests/unit/src/com/android/server/remoteauth/connectivity/ConnectivityManagerFactoryTest.java
new file mode 100644
index 0000000..42eff90
--- /dev/null
+++ b/remoteauth/tests/unit/src/com/android/server/remoteauth/connectivity/ConnectivityManagerFactoryTest.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.remoteauth.connectivity;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.Context;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Unit tests for {@link CdmConnectivityManager}. */
+@RunWith(AndroidJUnit4.class)
+public class ConnectivityManagerFactoryTest {
+
+    @Before
+    public void setUp() {}
+
+    @Test
+    public void testFactory_returnsConnectivityManager() {
+        final Context context = InstrumentationRegistry.getInstrumentation().getContext();
+        ConnectivityManager connectivityManager =
+                ConnectivityManagerFactory.getConnectivityManager(context);
+
+        assertThat(connectivityManager).isNotNull();
+    }
+}
diff --git a/remoteauth/tests/unit/src/com/android/server/remoteauth/connectivity/Utils.java b/remoteauth/tests/unit/src/com/android/server/remoteauth/connectivity/Utils.java
new file mode 100644
index 0000000..a5c992a
--- /dev/null
+++ b/remoteauth/tests/unit/src/com/android/server/remoteauth/connectivity/Utils.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.remoteauth.connectivity;
+
+import android.companion.AssociationInfo;
+import android.companion.AssociationRequest;
+import android.content.pm.PackageManager;
+import android.net.MacAddress;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public final class Utils {
+    public static final int FAKE_CONNECTION_ID = 1;
+    public static final int FAKE_USER_ID = 0;
+    public static final String FAKE_DISPLAY_NAME = "FAKE-DISPLAY-NAME";
+    public static final String FAKE_PEER_ADDRESS = "ff:ff:ff:ff:ff:ff";
+    public static final String FAKE_DEVICE_PROFILE = "FAKE-DEVICE-PROFILE";
+    public static final String FAKE_PACKAGE_NAME = "FAKE-PACKAGE-NAME";
+
+    public static DiscoveryFilter getFakeDiscoveryFilter() {
+        return DiscoveryFilter.Builder.builder()
+                .setDeviceName(FAKE_DISPLAY_NAME)
+                .setPeerAddress("FAKE-PEER-ADDRESS")
+                .setDeviceType(DiscoveryFilter.WATCH)
+                .build();
+    }
+
+    public static DiscoveredDeviceReceiver getFakeDiscoveredDeviceReceiver() {
+        return new DiscoveredDeviceReceiver() {
+            @Override
+            public void onDiscovered(DiscoveredDevice unused) {}
+
+            @Override
+            public void onLost(DiscoveredDevice unused) {}
+        };
+    }
+
+    /**
+     * Returns a fake CDM connection info.
+     *
+     * @return connection info.
+     */
+    public static CdmConnectionInfo getFakeCdmConnectionInfo()
+            throws PackageManager.NameNotFoundException {
+        return new CdmConnectionInfo(FAKE_CONNECTION_ID, getFakeAssociationInfoList(1).get(0));
+    }
+
+    /**
+     * Returns a fake discovered device.
+     *
+     * @return discovered device.
+     */
+    public static DiscoveredDevice getFakeCdmDiscoveredDevice()
+            throws PackageManager.NameNotFoundException {
+        return new DiscoveredDevice(getFakeCdmConnectionInfo(), FAKE_DISPLAY_NAME);
+    }
+
+    /**
+     * Returns fake association info array.
+     *
+     * <p> Creates an AssociationInfo object with fake values.
+     *
+     * @param associationsSize number of fake association info entries to return.
+     * @return list of {@link AssociationInfo} or null.
+     */
+    public static List<AssociationInfo> getFakeAssociationInfoList(int associationsSize) {
+        if (associationsSize > 0) {
+            List<AssociationInfo> associations = new ArrayList<>();
+            // Association id starts from 1.
+            for (int i = 1; i <= associationsSize; ++i) {
+                associations.add(
+                        (new AssociationInfo.Builder(i, FAKE_USER_ID, FAKE_PACKAGE_NAME))
+                                .setDeviceProfile(AssociationRequest.DEVICE_PROFILE_WATCH)
+                                .setDisplayName(FAKE_DISPLAY_NAME)
+                                .setDeviceMacAddress(MacAddress.fromString(FAKE_PEER_ADDRESS))
+                                .build());
+            }
+            return associations;
+        }
+        return new ArrayList<>();
+    }
+
+    private Utils() {}
+}
diff --git a/service-t/Android.bp b/service-t/Android.bp
index bc49f0e..19850fd 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.
@@ -78,6 +80,9 @@
         "//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 +99,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 +133,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/IpSecService.java b/service-t/src/com/android/server/IpSecService.java
index a884840..ea91e64 100644
--- a/service-t/src/com/android/server/IpSecService.java
+++ b/service-t/src/com/android/server/IpSecService.java
@@ -42,6 +42,7 @@
 import android.net.IpSecSpiResponse;
 import android.net.IpSecTransform;
 import android.net.IpSecTransformResponse;
+import android.net.IpSecTransformState;
 import android.net.IpSecTunnelInterfaceResponse;
 import android.net.IpSecUdpEncapResponse;
 import android.net.LinkAddress;
@@ -70,6 +71,7 @@
 import com.android.net.module.util.BinderUtils;
 import com.android.net.module.util.NetdUtils;
 import com.android.net.module.util.PermissionUtils;
+import com.android.net.module.util.netlink.xfrm.XfrmNetlinkNewSaMessage;
 
 import libcore.io.IoUtils;
 
@@ -109,6 +111,7 @@
     @VisibleForTesting static final int MAX_PORT_BIND_ATTEMPTS = 10;
 
     private final INetd mNetd;
+    private final IpSecXfrmController mIpSecXfrmCtrl;
 
     static {
         try {
@@ -152,6 +155,11 @@
             }
             return netd;
         }
+
+        /** Get a instance of IpSecXfrmController */
+        public IpSecXfrmController getIpSecXfrmController() {
+            return new IpSecXfrmController();
+        }
     }
 
     final UidFdTagger mUidFdTagger;
@@ -1111,6 +1119,7 @@
         mContext = context;
         mDeps = Objects.requireNonNull(deps, "Missing dependencies.");
         mUidFdTagger = uidFdTagger;
+        mIpSecXfrmCtrl = mDeps.getIpSecXfrmController();
         try {
             mNetd = mDeps.getNetdInstance(mContext);
         } catch (RemoteException e) {
@@ -1862,6 +1871,48 @@
         releaseResource(userRecord.mTransformRecords, resourceId);
     }
 
+    @Override
+    public synchronized IpSecTransformState getTransformState(int transformId)
+            throws IllegalStateException, RemoteException {
+        mContext.enforceCallingOrSelfPermission(
+                android.Manifest.permission.ACCESS_NETWORK_STATE, "IpsecService#getTransformState");
+
+        UserRecord userRecord = mUserResourceTracker.getUserRecord(Binder.getCallingUid());
+        TransformRecord transformInfo =
+                userRecord.mTransformRecords.getResourceOrThrow(transformId);
+
+        final int spi = transformInfo.getSpiRecord().getSpi();
+        final InetAddress destAddress =
+                InetAddresses.parseNumericAddress(
+                        transformInfo.getConfig().getDestinationAddress());
+        Log.d(TAG, "getTransformState for spi " + spi + " destAddress " + destAddress);
+
+        // Make netlink call
+        final XfrmNetlinkNewSaMessage xfrmNewSaMsg;
+        try {
+            xfrmNewSaMsg = mIpSecXfrmCtrl.ipSecGetSa(destAddress, Integer.toUnsignedLong(spi));
+        } catch (ErrnoException | IOException e) {
+            Log.e(TAG, "getTransformState: failed to get IpSecTransformState" + e.toString());
+            throw new IllegalStateException("Failed to get IpSecTransformState", e);
+        }
+
+        // Keep the netlink socket open to save time for the next call. It is cheap to have a
+        // persistent netlink socket in the system server
+
+        if (xfrmNewSaMsg == null) {
+            Log.e(TAG, "getTransformState: failed to get IpSecTransformState xfrmNewSaMsg is null");
+            throw new IllegalStateException("Failed to get IpSecTransformState");
+        }
+
+        return new IpSecTransformState.Builder()
+                .setTxHighestSequenceNumber(xfrmNewSaMsg.getTxSequenceNumber())
+                .setRxHighestSequenceNumber(xfrmNewSaMsg.getRxSequenceNumber())
+                .setPacketCount(xfrmNewSaMsg.getPacketCount())
+                .setByteCount(xfrmNewSaMsg.getByteCount())
+                .setReplayBitmap(xfrmNewSaMsg.getBitmap())
+                .build();
+    }
+
     /**
      * Apply an active transport mode transform to a socket, which will apply the IPsec security
      * association as a correspondent policy to the provided socket
diff --git a/service-t/src/com/android/server/IpSecXfrmController.java b/service-t/src/com/android/server/IpSecXfrmController.java
new file mode 100644
index 0000000..3cfbf83
--- /dev/null
+++ b/service-t/src/com/android/server/IpSecXfrmController.java
@@ -0,0 +1,200 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.server;
+
+import static com.android.net.module.util.netlink.NetlinkUtils.SOCKET_RECV_BUFSIZE;
+import static com.android.net.module.util.netlink.xfrm.XfrmNetlinkMessage.IPPROTO_ESP;
+import static com.android.net.module.util.netlink.xfrm.XfrmNetlinkMessage.NETLINK_XFRM;
+import static com.android.net.module.util.netlink.xfrm.XfrmNetlinkMessage.XFRM_MSG_NEWSA;
+
+import android.annotation.TargetApi;
+import android.os.Build;
+import android.system.ErrnoException;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.net.module.util.HexDump;
+import com.android.net.module.util.netlink.NetlinkConstants;
+import com.android.net.module.util.netlink.NetlinkErrorMessage;
+import com.android.net.module.util.netlink.NetlinkMessage;
+import com.android.net.module.util.netlink.NetlinkUtils;
+import com.android.net.module.util.netlink.xfrm.XfrmNetlinkGetSaMessage;
+import com.android.net.module.util.netlink.xfrm.XfrmNetlinkMessage;
+import com.android.net.module.util.netlink.xfrm.XfrmNetlinkNewSaMessage;
+
+import libcore.io.IoUtils;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.io.InterruptedIOException;
+import java.net.InetAddress;
+import java.net.SocketException;
+import java.nio.ByteBuffer;
+
+/**
+ * This class handles IPSec XFRM commands between IpSecService and the Linux kernel
+ *
+ * <p>Synchronization in IpSecXfrmController is done on all entrypoints due to potential race
+ * conditions at the kernel/xfrm level.
+ */
+public class IpSecXfrmController {
+    private static final String TAG = IpSecXfrmController.class.getSimpleName();
+
+    private static final boolean VDBG = false; // STOPSHIP: if true
+
+    private static final int TIMEOUT_MS = 500;
+    private static final int DEFAULT_RECV_BUFSIZE = 8 * 1024;
+
+    @NonNull private final Dependencies mDependencies;
+    @Nullable private FileDescriptor mNetlinkSocket;
+
+    @VisibleForTesting
+    public IpSecXfrmController(@NonNull Dependencies dependencies) {
+        mDependencies = dependencies;
+    }
+
+    public IpSecXfrmController() {
+        this(new Dependencies());
+    }
+
+    /**
+     * Start the XfrmController
+     *
+     * <p>The method is idempotent
+     */
+    public synchronized void openNetlinkSocketIfNeeded() throws ErrnoException, SocketException {
+        if (mNetlinkSocket == null) {
+            mNetlinkSocket = mDependencies.newNetlinkSocket();
+        }
+    }
+
+    /**
+     * Stop the XfrmController
+     *
+     * <p>The method is idempotent
+     */
+    public synchronized void closeNetlinkSocketIfNeeded() {
+        if (mNetlinkSocket != null) {
+            mDependencies.releaseNetlinkSocket(mNetlinkSocket);
+            mNetlinkSocket = null;
+        }
+    }
+
+    @VisibleForTesting
+    public synchronized FileDescriptor getNetlinkSocket() {
+        return mNetlinkSocket;
+    }
+
+    /** Dependencies of IpSecXfrmController, for injection in tests. */
+    @VisibleForTesting
+    public static class Dependencies {
+        /** Get a new XFRM netlink socket and connect it */
+        public FileDescriptor newNetlinkSocket() throws ErrnoException, SocketException {
+            final FileDescriptor fd =
+                    NetlinkUtils.netlinkSocketForProto(NETLINK_XFRM, SOCKET_RECV_BUFSIZE);
+            NetlinkUtils.connectToKernel(fd);
+            return fd;
+        }
+
+        /** Close the netlink socket */
+        // TODO: b/205923322 This annotation is to suppress the lint error complaining that
+        // #closeQuietly requires Android S. It can be removed when the infra supports setting
+        // service-connectivity min_sdk to 31
+        @TargetApi(Build.VERSION_CODES.S)
+        public void releaseNetlinkSocket(FileDescriptor fd) {
+            IoUtils.closeQuietly(fd);
+        }
+
+        /** Send a netlink message to a socket */
+        public void sendMessage(FileDescriptor fd, byte[] bytes)
+                throws ErrnoException, InterruptedIOException {
+            NetlinkUtils.sendMessage(fd, bytes, 0, bytes.length, TIMEOUT_MS);
+        }
+
+        /** Receive a netlink message from a socket */
+        public ByteBuffer recvMessage(FileDescriptor fd)
+                throws ErrnoException, InterruptedIOException {
+            return NetlinkUtils.recvMessage(fd, DEFAULT_RECV_BUFSIZE, TIMEOUT_MS);
+        }
+    }
+
+    @GuardedBy("IpSecXfrmController.this")
+    private NetlinkMessage sendRequestAndGetResponse(String methodTag, byte[] req)
+            throws ErrnoException, InterruptedIOException, IOException {
+        openNetlinkSocketIfNeeded();
+
+        logD(methodTag + ":  send request " + req.length + " bytes");
+        logV(HexDump.dumpHexString(req));
+        mDependencies.sendMessage(mNetlinkSocket, req);
+
+        final ByteBuffer response = mDependencies.recvMessage(mNetlinkSocket);
+        logD(methodTag + ": receive response " + response.limit() + " bytes");
+        logV(HexDump.dumpHexString(response.array(), 0 /* offset */, response.limit()));
+
+        final NetlinkMessage msg = XfrmNetlinkMessage.parse(response, NETLINK_XFRM);
+        if (msg == null) {
+            throw new IOException("Fail to parse the response message");
+        }
+
+        final int msgType = msg.getHeader().nlmsg_type;
+        if (msgType == NetlinkConstants.NLMSG_ERROR) {
+            final NetlinkErrorMessage errorMsg = (NetlinkErrorMessage) msg;
+            final int errorCode = errorMsg.getNlMsgError().error;
+            throw new ErrnoException(methodTag, errorCode);
+        }
+
+        return msg;
+    }
+
+    /** Get the state of an IPsec SA */
+    @NonNull
+    public synchronized XfrmNetlinkNewSaMessage ipSecGetSa(
+            @NonNull final InetAddress destAddress, long spi)
+            throws ErrnoException, InterruptedIOException, IOException {
+        logD("ipSecGetSa: destAddress=" + destAddress + " spi=" + spi);
+
+        final byte[] req =
+                XfrmNetlinkGetSaMessage.newXfrmNetlinkGetSaMessage(
+                        destAddress, spi, (short) IPPROTO_ESP);
+        try {
+            final NetlinkMessage msg = sendRequestAndGetResponse("ipSecGetSa", req);
+
+            final int messageType = msg.getHeader().nlmsg_type;
+            if (messageType != XFRM_MSG_NEWSA) {
+                throw new IOException("unexpected response type " + messageType);
+            }
+
+            return (XfrmNetlinkNewSaMessage) msg;
+        } catch (IllegalArgumentException exception) {
+            // Maybe thrown from Struct.parse
+            throw new IOException("Failed to parse the response " + exception);
+        }
+    }
+
+    private static void logV(String details) {
+        if (VDBG) {
+            Log.v(TAG, details);
+        }
+    }
+
+    private static void logD(String details) {
+        Log.d(TAG, details);
+    }
+}
diff --git a/service-t/src/com/android/server/NsdService.java b/service-t/src/com/android/server/NsdService.java
index 43357e4..ac794a1 100644
--- a/service-t/src/com/android/server/NsdService.java
+++ b/service-t/src/com/android/server/NsdService.java
@@ -16,6 +16,7 @@
 
 package com.android.server;
 
+import static android.Manifest.permission.DEVICE_POWER;
 import static android.Manifest.permission.NETWORK_SETTINGS;
 import static android.Manifest.permission.NETWORK_STACK;
 import static android.net.ConnectivityManager.NETID_UNSET;
@@ -25,6 +26,8 @@
 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.TYPE_REGEX;
+import static android.net.nsd.NsdManager.TYPE_SUBTYPE_LABEL_REGEX;
 import static android.provider.DeviceConfig.NAMESPACE_TETHERING;
 
 import static com.android.modules.utils.build.SdkLevel.isAtLeastU;
@@ -32,6 +35,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;
@@ -50,6 +55,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;
@@ -90,6 +97,7 @@
 import com.android.net.module.util.SharedLog;
 import com.android.server.connectivity.mdns.ExecutorProvider;
 import com.android.server.connectivity.mdns.MdnsAdvertiser;
+import com.android.server.connectivity.mdns.MdnsAdvertisingOptions;
 import com.android.server.connectivity.mdns.MdnsDiscoveryManager;
 import com.android.server.connectivity.mdns.MdnsFeatureFlags;
 import com.android.server.connectivity.mdns.MdnsInterfaceSocket;
@@ -109,7 +117,10 @@
 import java.net.UnknownHostException;
 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;
@@ -168,6 +179,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";
@@ -184,11 +197,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;
@@ -238,6 +253,8 @@
 
     private final RemoteCallbackList<IOffloadEngine> mOffloadEngines =
             new RemoteCallbackList<>();
+    @NonNull
+    private final MdnsFeatureFlags mMdnsFeatureFlags;
 
     private static class OffloadEngineInfo {
         @NonNull final String mInterfaceName;
@@ -259,15 +276,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;
         }
 
@@ -310,8 +323,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
@@ -340,8 +353,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
@@ -362,8 +375,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
@@ -534,6 +547,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;
@@ -546,6 +564,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;
@@ -686,18 +709,46 @@
                 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());
+            }
+
             @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.
@@ -712,55 +763,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,
@@ -771,7 +837,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
@@ -809,7 +875,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
@@ -824,36 +890,92 @@
                                     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;
+                            }
+
+                            serviceInfo.setSubtypes(subtypes);
                             maybeStartMonitoringSockets();
-                            // TODO: pass in the subtype as well. Including the subtype in the
-                            // service type would generate service instance names like
-                            // Name._subtype._sub._type._tcp, which is incorrect
-                            // (it should be Name._type._tcp).
-                            mAdvertiser.addService(transactionId, serviceInfo, typeSubtype.second);
+                            final MdnsAdvertisingOptions mdnsAdvertisingOptions =
+                                    MdnsAdvertisingOptions.newBuilder().setIsOnlyUpdate(
+                                            isUpdateOnly).build();
+                            mAdvertiser.addOrUpdateService(transactionId, serviceInfo,
+                                    mdnsAdvertisingOptions, clientInfo.mUid);
                             storeAdvertiserRequestMap(clientRequestId, transactionId, clientInfo,
                                     serviceInfo.getNetwork());
                         } else {
                             maybeStartDaemon();
+                            transactionId = getUniqueId();
                             if (registerService(transactionId, serviceInfo)) {
                                 if (DBG) {
                                     Log.d(TAG, "Register " + clientRequestId
@@ -873,7 +995,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
@@ -916,7 +1038,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
@@ -928,7 +1050,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;
@@ -944,10 +1066,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();
@@ -978,7 +1102,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
@@ -1017,7 +1141,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
@@ -1029,7 +1153,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;
@@ -1042,10 +1166,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();
@@ -1060,7 +1186,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
@@ -1384,6 +1510,7 @@
                         servInfo,
                         network == null ? INetd.LOCAL_NET_ID : network.netId,
                         serviceInfo.getInterfaceIndex());
+                servInfo.setSubtypes(dedupSubtypeLabels(serviceInfo.getSubtypes()));
                 return servInfo;
             }
 
@@ -1437,6 +1564,7 @@
                                 Log.e(TAG, "Invalid attribute", e);
                             }
                         }
+                        info.setHostname(getHostname(serviceInfo));
                         final List<InetAddress> addresses = getInetAddresses(serviceInfo);
                         if (addresses.size() != 0) {
                             info.setHostAddresses(addresses);
@@ -1473,6 +1601,7 @@
                             }
                         }
 
+                        info.setHostname(getHostname(serviceInfo));
                         final List<InetAddress> addresses = getInetAddresses(serviceInfo);
                         info.setHostAddresses(addresses);
                         clientInfo.onServiceUpdated(clientRequestId, info, request);
@@ -1519,6 +1648,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:
@@ -1577,34 +1716,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("^" + TYPE_SUBTYPE_LABEL_REGEX + "$").matcher(subtype).matches();
     }
 
     @VisibleForTesting
@@ -1618,7 +1774,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;
 
@@ -1640,24 +1797,34 @@
         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(
                         mContext, MdnsFeatureFlags.INCLUDE_INET_ADDRESS_RECORDS_IN_PROBING))
-                .setIsExpiredServicesRemovalEnabled(mDeps.isTrunkStableFeatureEnabled(
-                        MdnsFeatureFlags.NSD_EXPIRED_SERVICES_REMOVAL))
+                .setIsExpiredServicesRemovalEnabled(mDeps.isFeatureEnabled(
+                        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();
     }
 
@@ -1736,8 +1903,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);
         }
 
         /**
@@ -1923,9 +2090,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);
         }
@@ -2010,33 +2178,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
@@ -2048,8 +2239,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
@@ -2063,13 +2254,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
@@ -2104,20 +2295,25 @@
             if (!SdkLevel.isAtLeastT()) {
                 throw new SecurityException("API is not available in before API level 33");
             }
-            // REGISTER_NSD_OFFLOAD_ENGINE was only added to the SDK in V, but may
-            // be back ported to older builds: accept it as long as it's signature-protected
-            if (PermissionUtils.checkAnyPermissionOf(context, REGISTER_NSD_OFFLOAD_ENGINE)
-                    && (SdkLevel.isAtLeastV() || PermissionUtils.isSystemSignaturePermission(
-                    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);
             }
-            if (PermissionUtils.checkAnyPermissionOf(context, NETWORK_STACK,
-                    PERMISSION_MAINLINE_NETWORK_STACK, NETWORK_SETTINGS)) {
+
+            if (PermissionUtils.checkAnyPermissionOf(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) + ".");
         }
     }
 
@@ -2135,6 +2331,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);
         }
@@ -2152,13 +2353,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;
         }
@@ -2166,10 +2376,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);
@@ -2200,7 +2418,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;
@@ -2243,14 +2480,26 @@
     }
 
     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);
     }
 
@@ -2563,12 +2812,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/EnqueueMdnsQueryCallable.java b/service-t/src/com/android/server/connectivity/mdns/EnqueueMdnsQueryCallable.java
index 1582fb6..c4d3338 100644
--- a/service-t/src/com/android/server/connectivity/mdns/EnqueueMdnsQueryCallable.java
+++ b/service-t/src/com/android/server/connectivity/mdns/EnqueueMdnsQueryCallable.java
@@ -32,6 +32,7 @@
 import java.net.InetSocketAddress;
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.List;
 import java.util.concurrent.Callable;
 
@@ -122,17 +123,22 @@
                 return Pair.create(INVALID_TRANSACTION_ID, new ArrayList<>());
             }
 
-            int numQuestions = 0;
+            final List<MdnsRecord> questions = new ArrayList<>();
 
             if (sendDiscoveryQueries) {
-                numQuestions++; // Base service type
-                if (!subtypes.isEmpty()) {
-                    numQuestions += subtypes.size();
+                // Base service type
+                questions.add(new MdnsPointerRecord(serviceTypeLabels, expectUnicastResponse));
+                for (String subtype : subtypes) {
+                    final String[] labels = new String[serviceTypeLabels.length + 2];
+                    labels[0] = MdnsConstants.SUBTYPE_PREFIX + subtype;
+                    labels[1] = MdnsConstants.SUBTYPE_LABEL;
+                    System.arraycopy(serviceTypeLabels, 0, labels, 2, serviceTypeLabels.length);
+
+                    questions.add(new MdnsPointerRecord(labels, expectUnicastResponse));
                 }
             }
 
             // List of (name, type) to query
-            final ArrayList<Pair<String[], Integer>> missingKnownAnswerRecords = new ArrayList<>();
             final long now = clock.elapsedRealtime();
             for (MdnsResponse response : servicesToResolve) {
                 final String[] serviceName = response.getServiceName();
@@ -142,13 +148,13 @@
                 boolean renewSrv = !response.hasServiceRecord() || MdnsUtils.isRecordRenewalNeeded(
                         response.getServiceRecord(), now);
                 if (renewSrv && renewTxt) {
-                    missingKnownAnswerRecords.add(new Pair<>(serviceName, MdnsRecord.TYPE_ANY));
+                    questions.add(new MdnsAnyRecord(serviceName, expectUnicastResponse));
                 } else {
                     if (renewTxt) {
-                        missingKnownAnswerRecords.add(new Pair<>(serviceName, MdnsRecord.TYPE_TXT));
+                        questions.add(new MdnsTextRecord(serviceName, expectUnicastResponse));
                     }
                     if (renewSrv) {
-                        missingKnownAnswerRecords.add(new Pair<>(serviceName, MdnsRecord.TYPE_SRV));
+                        questions.add(new MdnsServiceRecord(serviceName, expectUnicastResponse));
                         // The hostname is not yet known, so queries for address records will be
                         // sent the next time the EnqueueMdnsQueryCallable is enqueued if the reply
                         // does not contain them. In practice, advertisers should include the
@@ -157,46 +163,27 @@
                     } else if (!response.hasInet4AddressRecord()
                             && !response.hasInet6AddressRecord()) {
                         final String[] host = response.getServiceRecord().getServiceHost();
-                        missingKnownAnswerRecords.add(new Pair<>(host, MdnsRecord.TYPE_A));
-                        missingKnownAnswerRecords.add(new Pair<>(host, MdnsRecord.TYPE_AAAA));
+                        questions.add(new MdnsInetAddressRecord(
+                                host, MdnsRecord.TYPE_A, expectUnicastResponse));
+                        questions.add(new MdnsInetAddressRecord(
+                                host, MdnsRecord.TYPE_AAAA, expectUnicastResponse));
                     }
                 }
             }
-            numQuestions += missingKnownAnswerRecords.size();
 
-            if (numQuestions == 0) {
+            if (questions.size() == 0) {
                 // No query to send
                 return Pair.create(INVALID_TRANSACTION_ID, new ArrayList<>());
             }
 
-            // Header.
-            packetWriter.writeUInt16(transactionId); // transaction ID
-            packetWriter.writeUInt16(MdnsConstants.FLAGS_QUERY); // flags
-            packetWriter.writeUInt16(numQuestions); // number of questions
-            packetWriter.writeUInt16(0); // number of answers (not yet known; will be written later)
-            packetWriter.writeUInt16(0); // number of authority entries
-            packetWriter.writeUInt16(0); // number of additional records
-
-            // Question(s) for missing records on known answers
-            for (Pair<String[], Integer> question : missingKnownAnswerRecords) {
-                writeQuestion(question.first, question.second);
-            }
-
-            // Question(s) for discovering other services with the type. There will be one question
-            // for each (fqdn+subtype, recordType) combination, as well as one for each (fqdn,
-            // recordType) combination.
-            if (sendDiscoveryQueries) {
-                for (String subtype : subtypes) {
-                    String[] labels = new String[serviceTypeLabels.length + 2];
-                    labels[0] = MdnsConstants.SUBTYPE_PREFIX + subtype;
-                    labels[1] = MdnsConstants.SUBTYPE_LABEL;
-                    System.arraycopy(serviceTypeLabels, 0, labels, 2, serviceTypeLabels.length);
-
-                    writeQuestion(labels, MdnsRecord.TYPE_PTR);
-                }
-                writeQuestion(serviceTypeLabels, MdnsRecord.TYPE_PTR);
-            }
-
+            final MdnsPacket queryPacket = new MdnsPacket(
+                    transactionId,
+                    MdnsConstants.FLAGS_QUERY,
+                    questions,
+                    Collections.emptyList(), /* answers */
+                    Collections.emptyList(), /* authorityRecords */
+                    Collections.emptyList() /* additionalRecords */);
+            MdnsUtils.writeMdnsPacket(packetWriter, queryPacket);
             sendPacketToIpv4AndIpv6(requestSender, MdnsConstants.MDNS_PORT);
             for (Integer emulatorPort : castShellEmulatorMdnsPorts) {
                 sendPacketToIpv4AndIpv6(requestSender, emulatorPort);
@@ -209,14 +196,6 @@
         }
     }
 
-    private void writeQuestion(String[] labels, int type) throws IOException {
-        packetWriter.writeLabels(labels);
-        packetWriter.writeUInt16(type);
-        packetWriter.writeUInt16(
-                MdnsConstants.QCLASS_INTERNET
-                        | (expectUnicastResponse ? MdnsConstants.QCLASS_UNICAST : 0));
-    }
-
     private void sendPacket(MdnsSocketClientBase requestSender, InetSocketAddress address)
             throws IOException {
         DatagramPacket packet = packetWriter.getPacket(address);
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 28e3924..0b60572 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,19 +33,24 @@
 import android.net.nsd.OffloadServiceInfo;
 import android.os.Build;
 import android.os.Looper;
+import android.text.TextUtils;
 import android.util.ArrayMap;
 import android.util.Log;
 import android.util.SparseArray;
 
+import com.android.connectivity.resources.R;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.net.module.util.CollectionUtils;
 import com.android.net.module.util.SharedLog;
+import com.android.server.connectivity.ConnectivityResources;
 import com.android.server.connectivity.mdns.util.MdnsUtils;
 
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
 import java.util.UUID;
 import java.util.function.BiPredicate;
 import java.util.function.Consumer;
@@ -82,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.
@@ -145,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,
@@ -173,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;
@@ -199,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
@@ -225,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) {
@@ -320,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(),
@@ -342,16 +416,40 @@
         }
 
         /**
-         * Add a service.
+         * Get the ID of a conflicting registration due to host, or -1 if none.
          *
-         * Conflicts must be checked via {@link #getConflictingService} before attempting to add.
+         * <p>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.
          */
-        void addService(int id, Registration registration) {
+        int getConflictingRegistrationDueToHost(@NonNull NsdServiceInfo info, int clientUid) {
+            if (TextUtils.isEmpty(info.getHostname())) {
+                return -1;
+            }
+            for (int i = 0; i < mPendingRegistrations.size(); i++) {
+                final Registration otherRegistration = mPendingRegistrations.valueAt(i);
+                final NsdServiceInfo otherInfo = otherRegistration.getServiceInfo();
+                if (clientUid != otherRegistration.mClientUid
+                        && MdnsUtils.equalsIgnoreDnsCase(
+                                info.getHostname(), otherInfo.getHostname())) {
+                    return mPendingRegistrations.keyAt(i);
+                }
+            }
+            return -1;
+        }
+
+        /**
+         * Add a service to advertise.
+         *
+         * <p>Conflicts must be checked via {@link #getConflictingRegistrationDueToService} and
+         * {@link #getConflictingRegistrationDueToHost} before attempting to add.
+         */
+        void addService(int id, @NonNull Registration registration) {
             mPendingRegistrations.put(id, registration);
             for (int i = 0; i < mAdvertisers.size(); i++) {
                 try {
-                    mAdvertisers.valueAt(i).addService(
-                            id, registration.getServiceInfo(), registration.getSubtype());
+                    mAdvertisers.valueAt(i).addService(id, registration.getServiceInfo());
                 } catch (NameConflictException e) {
                     mSharedLog.wtf("Name conflict adding services that should have unique names",
                             e);
@@ -359,6 +457,18 @@
             }
         }
 
+        /**
+         * Update an already registered service.
+         * The caller is expected to check that the service being updated doesn't change its name
+         */
+        void updateService(int id, @NonNull Registration registration) {
+            mPendingRegistrations.put(id, registration);
+            for (int i = 0; i < mAdvertisers.size(); i++) {
+                mAdvertisers.valueAt(i).updateService(
+                        id, registration.getServiceInfo().getSubtypes());
+            }
+        }
+
         void removeService(int id) {
             mPendingRegistrations.remove(id);
             for (int i = 0; i < mAdvertisers.size(); i++) {
@@ -405,7 +515,7 @@
                 final Registration registration = mPendingRegistrations.valueAt(i);
                 try {
                     advertiser.addService(mPendingRegistrations.keyAt(i),
-                            registration.getServiceInfo(), registration.getSubtype());
+                            registration.getServiceInfo());
                 } catch (NameConflictException e) {
                     mSharedLog.wtf("Name conflict adding services that should have unique names",
                             e);
@@ -467,21 +577,43 @@
     }
 
     private static class Registration {
-        @NonNull
-        final String mOriginalName;
+        @Nullable
+        final String mOriginalServiceName;
+        @Nullable
+        final String mOriginalHostname;
         boolean mNotifiedRegistrationSuccess;
-        private int mConflictCount;
+        private int mServiceNameConflictCount;
+        private int mHostnameConflictCount;
         @NonNull
         private NsdServiceInfo mServiceInfo;
-        @Nullable
-        private final String mSubtype;
+        final int mClientUid;
         int mConflictDuringProbingCount;
         int mConflictAfterProbingCount;
 
-        private Registration(@NonNull NsdServiceInfo serviceInfo, @Nullable String subtype) {
-            this.mOriginalName = serviceInfo.getServiceName();
+
+        private Registration(@NonNull NsdServiceInfo serviceInfo, int clientUid) {
+            this.mOriginalServiceName = serviceInfo.getServiceName();
+            this.mOriginalHostname = serviceInfo.getHostname();
             this.mServiceInfo = serviceInfo;
-            this.mSubtype = subtype;
+            this.mClientUid = clientUid;
+        }
+
+        /** Check if the new {@link NsdServiceInfo} doesn't update any data other than subtypes. */
+        public boolean isSubtypeOnlyUpdate(@NonNull NsdServiceInfo newInfo) {
+            return Objects.equals(newInfo.getServiceName(), mOriginalServiceName)
+                    && Objects.equals(newInfo.getServiceType(), mServiceInfo.getServiceType())
+                    && newInfo.getPort() == mServiceInfo.getPort()
+                    && Objects.equals(newInfo.getHostname(), mOriginalHostname)
+                    && Objects.equals(newInfo.getHostAddresses(), mServiceInfo.getHostAddresses())
+                    && Objects.equals(newInfo.getNetwork(), mServiceInfo.getNetwork());
+        }
+
+        /**
+         * Update subTypes for the registration.
+         */
+        public void updateSubtypes(@NonNull Set<String> subtypes) {
+            mServiceInfo = new NsdServiceInfo(mServiceInfo);
+            mServiceInfo.setSubtypes(subtypes);
         }
 
         /**
@@ -490,8 +622,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;
         }
 
@@ -507,40 +650,53 @@
          * @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;
-        }
     }
 
     /**
@@ -608,14 +764,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;
@@ -623,6 +781,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() {
@@ -632,42 +815,69 @@
     }
 
     /**
-     * Add a service to advertise.
+     * Add or update a service to advertise.
+     *
      * @param id A unique ID for the service.
      * @param service The service info to advertise.
-     * @param subtype An optional subtype to advertise the service with.
+     * @param advertisingOptions The advertising options.
+     * @param clientUid The UID who wants to advertise the service.
      */
-    public void addService(int id, NsdServiceInfo service, @Nullable String subtype) {
+    public void addOrUpdateService(int id, NsdServiceInfo service,
+            MdnsAdvertisingOptions advertisingOptions, int clientUid) {
         checkThread();
-        if (mRegistrations.get(id) != null) {
-            mSharedLog.e("Adding duplicate registration for " + service);
-            // TODO (b/264986328): add a more specific error code
-            mCb.onRegisterServiceFailed(id, NsdManager.FAILURE_INTERNAL_ERROR);
-            return;
-        }
-
-        mSharedLog.i("Adding service " + service + " with ID " + id + " and subtype " + subtype);
-
+        final Registration existingRegistration = mRegistrations.get(id);
         final Network network = service.getNetwork();
-        final Registration registration = new Registration(service, subtype);
-        final BiPredicate<Network, InterfaceAdvertiserRequest> checkConflictFilter;
-        if (network == null) {
-            // If registering on all networks, no advertiser must have conflicts
-            checkConflictFilter = (net, adv) -> true;
-        } else {
-            // If registering on one network, the matching network advertiser and the one for all
-            // networks must not have conflicts
-            checkConflictFilter = (net, adv) -> net == null || network.equals(net);
-        }
+        final Set<String> subtypes = service.getSubtypes();
+        Registration registration;
+        if (advertisingOptions.isOnlyUpdate()) {
+            if (existingRegistration == null) {
+                mSharedLog.e("Update non existing registration for " + service);
+                mCb.onRegisterServiceFailed(id, NsdManager.FAILURE_INTERNAL_ERROR);
+                return;
+            }
+            if (!(existingRegistration.isSubtypeOnlyUpdate(service))) {
+                mSharedLog.e("Update request can only update subType, serviceInfo: " + service
+                        + ", existing serviceInfo: " + existingRegistration.getServiceInfo());
+                mCb.onRegisterServiceFailed(id, NsdManager.FAILURE_INTERNAL_ERROR);
+                return;
 
-        updateRegistrationUntilNoConflict(checkConflictFilter, registration);
+            }
+            mSharedLog.i("Update service " + service + " with ID " + id + " and subtypes "
+                    + subtypes + " advertisingOptions " + advertisingOptions);
+            registration = existingRegistration;
+            registration.updateSubtypes(subtypes);
+        } else {
+            if (existingRegistration != null) {
+                mSharedLog.e("Adding duplicate registration for " + service);
+                // TODO (b/264986328): add a more specific error code
+                mCb.onRegisterServiceFailed(id, NsdManager.FAILURE_INTERNAL_ERROR);
+                return;
+            }
+            mSharedLog.i("Adding service " + service + " with ID " + id + " and subtypes "
+                    + subtypes + " advertisingOptions " + advertisingOptions);
+            registration = new Registration(service, clientUid);
+            final BiPredicate<Network, InterfaceAdvertiserRequest> checkConflictFilter;
+            if (network == null) {
+                // If registering on all networks, no advertiser must have conflicts
+                checkConflictFilter = (net, adv) -> true;
+            } else {
+                // If registering on one network, the matching network advertiser and the one
+                // for all networks must not have conflicts
+                checkConflictFilter = (net, adv) -> net == null || network.equals(net);
+            }
+            updateRegistrationUntilNoConflict(checkConflictFilter, registration);
+        }
 
         InterfaceAdvertiserRequest advertiser = mAdvertiserRequests.get(network);
         if (advertiser == null) {
             advertiser = new InterfaceAdvertiserRequest(network);
             mAdvertiserRequests.put(network, advertiser);
         }
-        advertiser.addService(id, registration);
+        if (advertisingOptions.isOnlyUpdate()) {
+            advertiser.updateService(id, registration);
+        } else {
+            advertiser.addService(id, registration);
+        }
         mRegistrations.put(id, registration);
     }
 
@@ -738,21 +948,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
new file mode 100644
index 0000000..e7a6ca7
--- /dev/null
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertisingOptions.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.mdns;
+
+/**
+ * API configuration parameters for advertising the mDNS service.
+ *
+ * <p>Use {@link MdnsAdvertisingOptions.Builder} to create {@link MdnsAdvertisingOptions}.
+ *
+ * @hide
+ */
+public class MdnsAdvertisingOptions {
+
+    private static MdnsAdvertisingOptions sDefaultOptions;
+    private final boolean mIsOnlyUpdate;
+
+    /**
+     * Parcelable constructs for a {@link MdnsAdvertisingOptions}.
+     */
+    MdnsAdvertisingOptions(
+            boolean isOnlyUpdate) {
+        this.mIsOnlyUpdate = isOnlyUpdate;
+    }
+
+    /**
+     * Returns a {@link Builder} for {@link MdnsAdvertisingOptions}.
+     */
+    public static Builder newBuilder() {
+        return new Builder();
+    }
+
+    /**
+     * Returns a default search options.
+     */
+    public static synchronized MdnsAdvertisingOptions getDefaultOptions() {
+        if (sDefaultOptions == null) {
+            sDefaultOptions = newBuilder().build();
+        }
+        return sDefaultOptions;
+    }
+
+    /**
+     * @return {@code true} if the advertising request is an update request.
+     */
+    public boolean isOnlyUpdate() {
+        return mIsOnlyUpdate;
+    }
+
+    @Override
+    public String toString() {
+        return "MdnsAdvertisingOptions{" + "mIsOnlyUpdate=" + mIsOnlyUpdate + '}';
+    }
+
+    /**
+     * A builder to create {@link MdnsAdvertisingOptions}.
+     */
+    public static final class Builder {
+        private boolean mIsOnlyUpdate = false;
+
+        private Builder() {
+        }
+
+        /**
+         * Sets if the advertising request is an update request.
+         */
+        public Builder setIsOnlyUpdate(boolean isOnlyUpdate) {
+            this.mIsOnlyUpdate = isOnlyUpdate;
+            return this;
+        }
+
+        /**
+         * Builds a {@link MdnsAdvertisingOptions} with the arguments supplied to this builder.
+         */
+        public MdnsAdvertisingOptions build() {
+            return new MdnsAdvertisingOptions(mIsOnlyUpdate);
+        }
+    }
+}
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 738c151..fe9bbba 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,68 @@
     // 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);
+    }
+
     /**
      * 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 +151,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}.
@@ -86,8 +162,12 @@
         public Builder() {
             mIsMdnsOffloadFeatureEnabled = false;
             mIncludeInetAddressRecordsInProbing = false;
-            mIsExpiredServicesRemovalEnabled = true; // Default enabled.
+            mIsExpiredServicesRemovalEnabled = false;
             mIsLabelCountLimitEnabled = true; // Default enabled.
+            mIsKnownAnswerSuppressionEnabled = false;
+            mIsUnicastReplyEnabled = true;
+            mIsAggressiveQueryModeEnabled = false;
+            mOverrideProvider = null;
         }
 
         /**
@@ -132,13 +212,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/MdnsInetAddressRecord.java b/service-t/src/com/android/server/connectivity/mdns/MdnsInetAddressRecord.java
index 973fd96..4399f2d 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsInetAddressRecord.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsInetAddressRecord.java
@@ -60,6 +60,12 @@
         super(name, type, reader, isQuestion);
     }
 
+    public MdnsInetAddressRecord(String[] name, int type, boolean isUnicast) {
+        super(name, type,
+                MdnsConstants.QCLASS_INTERNET | (isUnicast ? MdnsConstants.QCLASS_UNICAST : 0),
+                0L /* receiptTimeMillis */, false /* cacheFlush */, 0L /* ttlMillis */);
+    }
+
     public MdnsInetAddressRecord(String[] name, long receiptTimeMillis, boolean cacheFlush,
                     long ttlMillis, InetAddress address) {
         super(name, address instanceof Inet4Address ? TYPE_A : TYPE_AAAA,
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java b/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java
index 62c37ad..aa51c41 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);
@@ -229,13 +242,24 @@
     }
 
     /**
+     * Update an already registered service without sending exit/re-announcement packet.
+     *
+     * @param id An exiting service id
+     * @param subtypes New subtypes
+     */
+    public void updateService(int id, @NonNull Set<String> subtypes) {
+        // The current implementation is intended to be used in cases where subtypes don't get
+        // announced.
+        mRecordRepository.updateService(id, subtypes);
+    }
+
+    /**
      * Start advertising a service.
      *
      * @throws NameConflictException There is already a service being advertised with that name.
      */
-    public void addService(int id, NsdServiceInfo service, @Nullable String subtype)
-            throws NameConflictException {
-        final int replacedExitingService = mRecordRepository.addService(id, service, subtype);
+    public void addService(int id, NsdServiceInfo service) throws NameConflictException {
+        final int replacedExitingService = mRecordRepository.addService(id, service);
         // Cancel announcements for the existing service. This only happens for exiting services
         // (so cancelling exiting announcements), as per RecordRepository.addService.
         if (replacedExitingService >= 0) {
@@ -323,6 +347,7 @@
         final MdnsProber.ProbingInfo probingInfo = mRecordRepository.setServiceProbing(serviceId);
         if (probingInfo == null) return false;
 
+        mAnnouncer.stop(serviceId);
         mProber.restartForConflict(probingInfo);
         return true;
     }
@@ -361,23 +386,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/MdnsPointerRecord.java b/service-t/src/com/android/server/connectivity/mdns/MdnsPointerRecord.java
index 41cc380..e5c90a4 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsPointerRecord.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsPointerRecord.java
@@ -39,6 +39,12 @@
         super(name, TYPE_PTR, reader, isQuestion);
     }
 
+    public MdnsPointerRecord(String[] name, boolean isUnicast) {
+        super(name, TYPE_PTR,
+                MdnsConstants.QCLASS_INTERNET | (isUnicast ? MdnsConstants.QCLASS_UNICAST : 0),
+                0L /* receiptTimeMillis */, false /* cacheFlush */, 0L /* ttlMillis */);
+    }
+
     public MdnsPointerRecord(String[] name, long receiptTimeMillis, boolean cacheFlush,
                     long ttlMillis, String[] pointer) {
         super(name, TYPE_PTR, MdnsConstants.QCLASS_INTERNET, receiptTimeMillis, cacheFlush,
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 e34778f..fb45454 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;
 
@@ -46,6 +50,7 @@
 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 +58,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}.
@@ -74,8 +81,6 @@
 
     // 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 +97,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 +147,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,19 +164,19 @@
         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.
          */
-        public boolean exiting = false;
+        public boolean exiting;
 
         /**
          * The replied query packet count of this service.
@@ -185,88 +194,130 @@
         private boolean isProbing;
 
         /**
+         * 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);
+        }
+
+        /**
+         * Create a ServiceRegistration for dns-sd service registration (RFC6763).
+         */
+        ServiceRegistration(@NonNull String[] deviceHostname, @NonNull NsdServiceInfo serviceInfo,
+                int repliedServiceCount, int sentPacketCount, boolean exiting, boolean isProbing) {
+            this.serviceInfo = serviceInfo;
+
+            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);
+
+            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 */,
+                                NON_NAME_RECORDS_TTL_MILLIS,
+                                serviceName),
+                        true /* sharedName */));
+                for (String subtype : serviceInfo.getSubtypes()) {
+                    ptrRecords.add(new RecordInfo<>(
+                            serviceInfo,
+                            new MdnsPointerRecord(
+                                    MdnsUtils.constructFullSubtype(serviceType, subtype),
+                                    0L /* receiptTimeMillis */,
+                                    false /* cacheFlush */,
+                                    NON_NAME_RECORDS_TTL_MILLIS,
+                                    serviceName),
+                            true /* sharedName */));
+                }
+
+                srvRecord = new RecordInfo<>(
+                        serviceInfo,
+                        new MdnsServiceRecord(serviceName,
+                                0L /* receiptTimeMillis */,
+                                true /* cacheFlush */,
+                                NAME_RECORDS_TTL_MILLIS,
+                                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 */,
+                                NON_NAME_RECORDS_TTL_MILLIS,
+                                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 */,
+                                NON_NAME_RECORDS_TTL_MILLIS,
+                                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 */,
+                                            NAME_RECORDS_TTL_MILLIS,
+                                            address),
+                                    false /* sharedName */));
+                }
+                allRecords.addAll(addressRecords);
+            } else {
+                addressRecords = Collections.emptyList();
+            }
+
+            this.allRecords = Collections.unmodifiableList(allRecords);
+            this.repliedServiceCount = repliedServiceCount;
+            this.sentPacketCount = sentPacketCount;
+            this.isProbing = isProbing;
+            this.exiting = exiting;
+        }
+
+        /**
          * Create a ServiceRegistration for dns-sd service registration (RFC6763).
          *
          * @param deviceHostname Hostname of the device (for the interface used)
          * @param serviceInfo Service to advertise
          */
         ServiceRegistration(@NonNull String[] deviceHostname, @NonNull NsdServiceInfo serviceInfo,
-                @Nullable String subtype, int repliedServiceCount, int sentPacketCount) {
-            this.serviceInfo = serviceInfo;
-            this.subtype = subtype;
-
-            final String[] serviceType = splitServiceType(serviceInfo);
-            final String[] serviceName = splitFullyQualifiedName(serviceInfo, serviceType);
-
-            // Service PTR record
-            final RecordInfo<MdnsPointerRecord> ptrRecord = new RecordInfo<>(
-                    serviceInfo,
-                    new MdnsPointerRecord(
-                            serviceType,
-                            0L /* receiptTimeMillis */,
-                            false /* cacheFlush */,
-                            NON_NAME_RECORDS_TTL_MILLIS,
-                            serviceName),
-                    true /* sharedName */);
-
-            if (subtype == null) {
-                this.ptrRecords = Collections.singletonList(ptrRecord);
-            } else {
-                final String[] subtypeName = new String[serviceType.length + 2];
-                System.arraycopy(serviceType, 0, subtypeName, 2, serviceType.length);
-                subtypeName[0] = subtype;
-                subtypeName[1] = SUBTYPE_SEPARATOR;
-                final RecordInfo<MdnsPointerRecord> subtypeRecord = new RecordInfo<>(
-                        serviceInfo,
-                        new MdnsPointerRecord(
-                                subtypeName,
-                                0L /* receiptTimeMillis */,
-                                false /* cacheFlush */,
-                                NON_NAME_RECORDS_TTL_MILLIS,
-                                serviceName),
-                        true /* sharedName */);
-
-                this.ptrRecords = List.of(ptrRecord, subtypeRecord);
-            }
-
-            srvRecord = new RecordInfo<>(
-                    serviceInfo,
-                    new MdnsServiceRecord(serviceName,
-                            0L /* receiptTimeMillis */,
-                            true /* cacheFlush */,
-                            NAME_RECORDS_TTL_MILLIS, 0 /* servicePriority */, 0 /* serviceWeight */,
-                            serviceInfo.getPort(),
-                            deviceHostname),
-                    false /* sharedName */);
-
-            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 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 */));
-
-            this.allRecords = Collections.unmodifiableList(allRecords);
-            this.repliedServiceCount = repliedServiceCount;
-            this.sentPacketCount = sentPacketCount;
-            this.isProbing = true;
+                int repliedServiceCount, int sentPacketCount) {
+            this(deviceHostname, serviceInfo,repliedServiceCount, sentPacketCount,
+                    false /* exiting */, true /* isProbing */);
         }
 
         void setProbing(boolean probing) {
@@ -305,6 +356,23 @@
     }
 
     /**
+     * Update a service that already registered in the repository.
+     *
+     * @param serviceId An existing service ID.
+     * @param subtypes New subtypes
+     */
+    public void updateService(int serviceId, @NonNull Set<String> subtypes) {
+        final ServiceRegistration existingRegistration = mServices.get(serviceId);
+        if (existingRegistration == null) {
+            throw new IllegalArgumentException(
+                    "Service ID must already exist for an update request: " + serviceId);
+        }
+        final ServiceRegistration updatedRegistration = existingRegistration.withSubtypes(
+                subtypes);
+        mServices.put(serviceId, updatedRegistration);
+    }
+
+    /**
      * Add a service to the repository.
      *
      * This may remove/replace any existing service that used the name added but is exiting.
@@ -314,8 +382,7 @@
      *         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)
-            throws NameConflictException {
+    public int addService(int serviceId, NsdServiceInfo serviceInfo) throws NameConflictException {
         if (mServices.contains(serviceId)) {
             throw new IllegalArgumentException(
                     "Service ID must not be reused across registrations: " + serviceId);
@@ -328,7 +395,7 @@
         }
 
         final ServiceRegistration registration = new ServiceRegistration(
-                mDeviceHostname, serviceInfo, subtype, NO_PACKET /* repliedServiceCount */,
+                mDeviceHostname, serviceInfo, NO_PACKET /* repliedServiceCount */,
                 NO_PACKET /* sentPacketCount */);
         mServices.put(serviceId, registration);
 
@@ -340,32 +407,38 @@
     /**
      * @return The ID of the service identified by its name, or -1 if none.
      */
-    private int getServiceByName(@NonNull String serviceName) {
+    private int getServiceByName(@Nullable String serviceName) {
+        if (TextUtils.isEmpty(serviceName)) {
+            return -1;
+        }
         for (int i = 0; i < mServices.size(); i++) {
             final ServiceRegistration registration = mServices.valueAt(i);
-            if (MdnsUtils.equalsIgnoreDnsCase(serviceName,
-                    registration.serviceInfo.getServiceName())) {
+            if (MdnsUtils.equalsIgnoreDnsCase(
+                    serviceName, registration.serviceInfo.getServiceName())) {
                 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 */,
@@ -462,6 +535,16 @@
         return ret;
     }
 
+    private boolean isTruncatedKnownAnswerPacket(MdnsPacket packet) {
+        if (!mMdnsFeatureFlags.mIsKnownAnswerSuppressionEnabled
+                // 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.
      *
@@ -471,30 +554,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
@@ -518,6 +654,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;
@@ -533,10 +675,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;
     }
 
     /**
@@ -547,14 +702,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
@@ -577,19 +734,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.mIsKnownAnswerSuppressionEnabled
+                    && 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);
         }
 
@@ -598,7 +768,7 @@
         // 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 */,
@@ -606,13 +776,14 @@
                     // be the same as the TTL that the record would have had, had it existed."
                     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) {
@@ -625,21 +796,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;
     }
 
@@ -656,7 +814,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
@@ -672,6 +830,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) {
@@ -689,6 +853,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.
@@ -703,10 +888,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);
@@ -725,29 +910,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);
     }
 
     /**
@@ -766,8 +968,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);
@@ -782,70 +989,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.
@@ -856,8 +1174,8 @@
         if (registration == null) return null;
 
         registration.setProbing(true);
-        return makeProbingInfo(
-                serviceId, registration.srvRecord.record, makeProbingInetAddressRecords());
+
+        return makeProbingInfo(serviceId, registration);
     }
 
     /**
@@ -891,10 +1209,9 @@
         if (existing == null) return null;
 
         final ServiceRegistration newService = new ServiceRegistration(mDeviceHostname, newInfo,
-                existing.subtype, existing.repliedServiceCount, existing.sentPacketCount);
+                existing.repliedServiceCount, existing.sentPacketCount);
         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..a46be3b 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.mIsKnownAnswerSuppressionEnabled) {
+            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.mIsKnownAnswerSuppressionEnabled) {
+                // 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/MdnsResponse.java b/service-t/src/com/android/server/connectivity/mdns/MdnsResponse.java
index e2288c1..05ad1be 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsResponse.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsResponse.java
@@ -33,6 +33,7 @@
 
 /** An mDNS response. */
 public class MdnsResponse {
+    public static final long EXPIRATION_NEVER = Long.MAX_VALUE;
     private final List<MdnsRecord> records;
     private final List<MdnsPointerRecord> pointerRecords;
     private MdnsServiceRecord serviceRecord;
@@ -349,6 +350,21 @@
         return serviceName;
     }
 
+    /** Get the min remaining ttl time from received records */
+    public long getMinRemainingTtl(long now) {
+        long minRemainingTtl = EXPIRATION_NEVER;
+        // TODO: Check other records(A, AAAA, TXT) ttl time.
+        if (!hasServiceRecord()) {
+            return EXPIRATION_NEVER;
+        }
+        // Check ttl time.
+        long remainingTtl = serviceRecord.getRemainingTTL(now);
+        if (remainingTtl < minRemainingTtl) {
+            minRemainingTtl = remainingTtl;
+        }
+        return minRemainingTtl;
+    }
+
     /**
      * Tests if this response is a goodbye message. This will be true if a service record is present
      * and any of the records have a TTL of 0.
diff --git a/service-t/src/com/android/server/connectivity/mdns/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/MdnsServiceCache.java b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceCache.java
index d3493c7..e9a41d1 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceCache.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceCache.java
@@ -16,16 +16,22 @@
 
 package com.android.server.connectivity.mdns;
 
+import static com.android.server.connectivity.mdns.MdnsResponse.EXPIRATION_NEVER;
 import static com.android.server.connectivity.mdns.util.MdnsUtils.ensureRunningOnHandlerThread;
 import static com.android.server.connectivity.mdns.util.MdnsUtils.equalsIgnoreDnsCase;
 import static com.android.server.connectivity.mdns.util.MdnsUtils.toDnsLowerCase;
 
+import static java.lang.Math.min;
+
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.os.Handler;
 import android.os.Looper;
 import android.util.ArrayMap;
 
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.connectivity.mdns.util.MdnsUtils;
+
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Iterator;
@@ -67,8 +73,11 @@
         }
     }
     /**
-     * A map of cached services. Key is composed of service name, type and socket. Value is the
-     * service which use the service type to discover from each socket.
+     * A map of cached services. Key is composed of service type and socket. Value is the list of
+     * services which are discovered from the given CacheKey.
+     * When the MdnsFeatureFlags#NSD_EXPIRED_SERVICES_REMOVAL flag is enabled, the lists are sorted
+     * by expiration time, with the earliest entries appearing first. This sorting allows the
+     * removal process to progress through the expiration check efficiently.
      */
     @NonNull
     private final ArrayMap<CacheKey, List<MdnsResponse>> mCachedServices = new ArrayMap<>();
@@ -82,10 +91,20 @@
     private final Handler mHandler;
     @NonNull
     private final MdnsFeatureFlags mMdnsFeatureFlags;
+    @NonNull
+    private final MdnsUtils.Clock mClock;
+    private long mNextExpirationTime = EXPIRATION_NEVER;
 
     public MdnsServiceCache(@NonNull Looper looper, @NonNull MdnsFeatureFlags mdnsFeatureFlags) {
+        this(looper, mdnsFeatureFlags, new MdnsUtils.Clock());
+    }
+
+    @VisibleForTesting
+    MdnsServiceCache(@NonNull Looper looper, @NonNull MdnsFeatureFlags mdnsFeatureFlags,
+            @NonNull MdnsUtils.Clock clock) {
         mHandler = new Handler(looper);
         mMdnsFeatureFlags = mdnsFeatureFlags;
+        mClock = clock;
     }
 
     /**
@@ -97,6 +116,9 @@
     @NonNull
     public List<MdnsResponse> getCachedServices(@NonNull CacheKey cacheKey) {
         ensureRunningOnHandlerThread(mHandler);
+        if (mMdnsFeatureFlags.mIsExpiredServicesRemovalEnabled) {
+            maybeRemoveExpiredServices(cacheKey, mClock.elapsedRealtime());
+        }
         return mCachedServices.containsKey(cacheKey)
                 ? Collections.unmodifiableList(new ArrayList<>(mCachedServices.get(cacheKey)))
                 : Collections.emptyList();
@@ -129,6 +151,9 @@
     @Nullable
     public MdnsResponse getCachedService(@NonNull String serviceName, @NonNull CacheKey cacheKey) {
         ensureRunningOnHandlerThread(mHandler);
+        if (mMdnsFeatureFlags.mIsExpiredServicesRemovalEnabled) {
+            maybeRemoveExpiredServices(cacheKey, mClock.elapsedRealtime());
+        }
         final List<MdnsResponse> responses = mCachedServices.get(cacheKey);
         if (responses == null) {
             return null;
@@ -137,6 +162,16 @@
         return response != null ? new MdnsResponse(response) : null;
     }
 
+    static void insertResponseAndSortList(
+            List<MdnsResponse> responses, MdnsResponse response, long now) {
+        // binarySearch returns "the index of the search key, if it is contained in the list;
+        // otherwise, (-(insertion point) - 1)"
+        final int searchRes = Collections.binarySearch(responses, response,
+                // Sort the list by ttl.
+                (o1, o2) -> Long.compare(o1.getMinRemainingTtl(now), o2.getMinRemainingTtl(now)));
+        responses.add(searchRes >= 0 ? searchRes : (-searchRes - 1), response);
+    }
+
     /**
      * Add or update a service.
      *
@@ -151,7 +186,15 @@
         final MdnsResponse existing =
                 findMatchedResponse(responses, response.getServiceInstanceName());
         responses.remove(existing);
-        responses.add(response);
+        if (mMdnsFeatureFlags.mIsExpiredServicesRemovalEnabled) {
+            final long now = mClock.elapsedRealtime();
+            // Insert and sort service
+            insertResponseAndSortList(responses, response, now);
+            // Update the next expiration check time when a new service is added.
+            mNextExpirationTime = getNextExpirationTime(now);
+        } else {
+            responses.add(response);
+        }
     }
 
     /**
@@ -168,14 +211,25 @@
             return null;
         }
         final Iterator<MdnsResponse> iterator = responses.iterator();
+        MdnsResponse removedResponse = null;
         while (iterator.hasNext()) {
             final MdnsResponse response = iterator.next();
             if (equalsIgnoreDnsCase(serviceName, response.getServiceInstanceName())) {
                 iterator.remove();
-                return response;
+                removedResponse = response;
+                break;
             }
         }
-        return null;
+
+        if (mMdnsFeatureFlags.mIsExpiredServicesRemovalEnabled) {
+            // Remove the serviceType if no response.
+            if (responses.isEmpty()) {
+                mCachedServices.remove(cacheKey);
+            }
+            // Update the next expiration check time when a service is removed.
+            mNextExpirationTime = getNextExpirationTime(mClock.elapsedRealtime());
+        }
+        return removedResponse;
     }
 
     /**
@@ -203,6 +257,87 @@
         mCallbacks.remove(cacheKey);
     }
 
+    private void notifyServiceExpired(@NonNull CacheKey cacheKey,
+            @NonNull MdnsResponse previousResponse, @Nullable MdnsResponse newResponse) {
+        final ServiceExpiredCallback callback = mCallbacks.get(cacheKey);
+        if (callback == null) {
+            // The cached service is no listener.
+            return;
+        }
+        mHandler.post(()-> callback.onServiceRecordExpired(previousResponse, newResponse));
+    }
+
+    static List<MdnsResponse> removeExpiredServices(@NonNull List<MdnsResponse> responses,
+            long now) {
+        final List<MdnsResponse> removedResponses = new ArrayList<>();
+        final Iterator<MdnsResponse> iterator = responses.iterator();
+        while (iterator.hasNext()) {
+            final MdnsResponse response = iterator.next();
+            // TODO: Check other records (A, AAAA, TXT) ttl time and remove the record if it's
+            //  expired. Then send service update notification.
+            if (!response.hasServiceRecord() || response.getMinRemainingTtl(now) > 0) {
+                // The responses are sorted by the service record ttl time. Break out of loop
+                // early if service is not expired or no service record.
+                break;
+            }
+            // Remove the ttl expired service.
+            iterator.remove();
+            removedResponses.add(response);
+        }
+        return removedResponses;
+    }
+
+    private long getNextExpirationTime(long now) {
+        if (mCachedServices.isEmpty()) {
+            return EXPIRATION_NEVER;
+        }
+
+        long minRemainingTtl = EXPIRATION_NEVER;
+        for (int i = 0; i < mCachedServices.size(); i++) {
+            minRemainingTtl = min(minRemainingTtl,
+                    // The empty lists are not kept in the map, so there's always at least one
+                    // element in the list. Therefore, it's fine to get the first element without a
+                    // null check.
+                    mCachedServices.valueAt(i).get(0).getMinRemainingTtl(now));
+        }
+        return minRemainingTtl == EXPIRATION_NEVER ? EXPIRATION_NEVER : now + minRemainingTtl;
+    }
+
+    /**
+     * Check whether the ttl time is expired on each service and notify to the listeners
+     */
+    private void maybeRemoveExpiredServices(CacheKey cacheKey, long now) {
+        ensureRunningOnHandlerThread(mHandler);
+        if (now < mNextExpirationTime) {
+            // Skip the check if ttl time is not expired.
+            return;
+        }
+
+        final List<MdnsResponse> responses = mCachedServices.get(cacheKey);
+        if (responses == null) {
+            // No such services.
+            return;
+        }
+
+        final List<MdnsResponse> removedResponses = removeExpiredServices(responses, now);
+        if (removedResponses.isEmpty()) {
+            // No expired services.
+            return;
+        }
+
+        for (MdnsResponse previousResponse : removedResponses) {
+            notifyServiceExpired(cacheKey, previousResponse, null /* newResponse */);
+        }
+
+        // Remove the serviceType if no response.
+        if (responses.isEmpty()) {
+            mCachedServices.remove(cacheKey);
+        }
+
+        // Update next expiration time.
+        mNextExpirationTime = getNextExpirationTime(now);
+    }
+
     /*** Callbacks for listening service expiration */
     public interface ServiceExpiredCallback {
         /*** Notify the service is expired */
@@ -210,5 +345,5 @@
                 @Nullable MdnsResponse newResponse);
     }
 
-    // TODO: check ttl expiration for each service and notify to the clients.
+    // TODO: Schedule a job to check ttl expiration for all services and notify to the clients.
 }
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceRecord.java b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceRecord.java
index 4d407be..0d6a9ec 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceRecord.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceRecord.java
@@ -49,6 +49,12 @@
         super(name, TYPE_SRV, reader, isQuestion);
     }
 
+    public MdnsServiceRecord(String[] name, boolean isUnicast) {
+        super(name, TYPE_SRV,
+                MdnsConstants.QCLASS_INTERNET | (isUnicast ? MdnsConstants.QCLASS_UNICAST : 0),
+                0L /* receiptTimeMillis */, false /* cacheFlush */, 0L /* ttlMillis */);
+    }
+
     public MdnsServiceRecord(String[] name, long receiptTimeMillis, boolean cacheFlush,
                     long ttlMillis, int servicePriority, int serviceWeight, int servicePort,
                     String[] serviceHost) {
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 0a03186..3a69d67 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
@@ -81,7 +81,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 +95,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);
@@ -311,13 +337,16 @@
         ensureRunningOnHandlerThread(handler);
         this.searchOptions = searchOptions;
         boolean hadReply = false;
-        if (listeners.put(listener, searchOptions) == null) {
-            for (MdnsResponse existingResponse :
-                    serviceCache.getCachedServices(cacheKey)) {
+        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;
@@ -331,7 +360,7 @@
         // interested anymore.
         final QueryTaskConfig taskConfig = new QueryTaskConfig(
                 searchOptions.getSubtypes(),
-                searchOptions.isPassiveMode(),
+                searchOptions.getQueryMode(),
                 searchOptions.onlyUseIpv6OnIpv6OnlyNetworks(),
                 searchOptions.numOfQueriesBeforeBackoff(),
                 socketKey);
@@ -481,9 +510,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()) {
@@ -512,10 +542,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);
             }
@@ -526,16 +555,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 */);
@@ -577,7 +612,7 @@
     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;
             }
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsTextRecord.java b/service-t/src/com/android/server/connectivity/mdns/MdnsTextRecord.java
index cf6c8ac..92cf324 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsTextRecord.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsTextRecord.java
@@ -42,6 +42,12 @@
         super(name, TYPE_TXT, reader, isQuestion);
     }
 
+    public MdnsTextRecord(String[] name, boolean isUnicast) {
+        super(name, TYPE_TXT,
+                MdnsConstants.QCLASS_INTERNET | (isUnicast ? MdnsConstants.QCLASS_UNICAST : 0),
+                0L /* receiptTimeMillis */, false /* cacheFlush */, 0L /* ttlMillis */);
+    }
+
     public MdnsTextRecord(String[] name, long receiptTimeMillis, boolean cacheFlush, long ttlMillis,
             List<TextEntry> entries) {
         super(name, TYPE_TXT, MdnsConstants.QCLASS_INTERNET, receiptTimeMillis, cacheFlush,
diff --git a/service-t/src/com/android/server/connectivity/mdns/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..63524a6 100644
--- a/service-t/src/com/android/server/connectivity/mdns/QueryTaskConfig.java
+++ b/service-t/src/com/android/server/connectivity/mdns/QueryTaskConfig.java
@@ -16,6 +16,9 @@
 
 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;
 
@@ -33,19 +36,29 @@
 
     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;
+    @VisibleForTesting
+    // RFC 6762 5.2: The interval between the first two queries MUST be at least one second.
+    static final int INITIAL_AGGRESSIVE_TIME_BETWEEN_BURSTS_MS = 1000;
+    @VisibleForTesting
+    // Basically this tries to send one query per typical DTIM interval 100ms, to maximize the
+    // chances that a query will be received if devices are using a DTIM multiplier (in which case
+    // they only listen once every [multiplier] DTIM intervals).
+    static final int TIME_BETWEEN_RETRANSMISSION_QUERIES_IN_BURST_MS = 100;
+    static final int MAX_TIME_BETWEEN_AGGRESSIVE_BURSTS_MS = 60000;
     // The following fields are used by QueryTask so we need to test them.
     @VisibleForTesting
     final List<String> subtypes;
     private final boolean alwaysAskForUnicastResponse =
             MdnsConfigs.alwaysAskForUnicastResponseInEachBurst();
-    private final boolean usePassiveMode;
+    private final int queryMode;
     final boolean onlyUseIpv6OnIpv6OnlyNetworks;
     private final int numOfQueriesBeforeBackoff;
     @VisibleForTesting
@@ -66,7 +79,7 @@
             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,12 +92,13 @@
         this.queryCount = queryCount;
         this.socketKey = other.socketKey;
     }
+
     QueryTaskConfig(@NonNull Collection<String> subtypes,
-            boolean usePassiveMode,
+            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);
@@ -94,21 +108,58 @@
         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 +171,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 4d79f9d..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))) {
@@ -183,13 +186,10 @@
     }
 
     /**
-     * Create a raw DNS packet.
+     * Write the mdns packet from given MdnsPacket.
      */
-    public static byte[] createRawDnsPacket(@NonNull byte[] packetCreationBuffer,
-            @NonNull MdnsPacket packet) throws IOException {
-        // TODO: support packets over size (send in multiple packets with TC bit set)
-        final MdnsPacketWriter writer = new MdnsPacketWriter(packetCreationBuffer);
-
+    public static void writeMdnsPacket(@NonNull MdnsPacketWriter writer, @NonNull MdnsPacket packet)
+            throws IOException {
         writer.writeUInt16(packet.transactionId); // Transaction ID (advertisement: 0)
         writer.writeUInt16(packet.flags); // Response, authoritative (rfc6762 18.4)
         writer.writeUInt16(packet.questions.size()); // questions count
@@ -210,6 +210,16 @@
         for (MdnsRecord record : packet.additionalRecords) {
             record.write(writer, 0L);
         }
+    }
+
+    /**
+     * Create a raw DNS packet.
+     */
+    public static byte[] createRawDnsPacket(@NonNull byte[] packetCreationBuffer,
+            @NonNull MdnsPacket packet) throws IOException {
+        // TODO: support packets over size (send in multiple packets with TC bit set)
+        final MdnsPacketWriter writer = new MdnsPacketWriter(packetCreationBuffer);
+        writeMdnsPacket(writer, packet);
 
         final int len = writer.getWritePosition();
         return Arrays.copyOfRange(packetCreationBuffer, 0, len);
@@ -226,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/EthernetTracker.java b/service-t/src/com/android/server/ethernet/EthernetTracker.java
index 01b8de7..458d64f 100644
--- a/service-t/src/com/android/server/ethernet/EthernetTracker.java
+++ b/service-t/src/com/android/server/ethernet/EthernetTracker.java
@@ -241,11 +241,11 @@
         String ifaceMatchRegex = mDeps.getInterfaceRegexFromResource(mContext);
         // "*" is a magic string to indicate "pick the default".
         if (ifaceMatchRegex.equals("*")) {
-            if (SdkLevel.isAtLeastU()) {
-                // On U+, include both usb%d and eth%d interfaces.
+            if (SdkLevel.isAtLeastV()) {
+                // On V+, include both usb%d and eth%d interfaces.
                 ifaceMatchRegex = "(usb|eth)\\d+";
             } else {
-                // On T, include only eth%d interfaces.
+                // On T and U, include only eth%d interfaces.
                 ifaceMatchRegex = "eth\\d+";
             }
         }
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 804e709..ec10158 100644
--- a/service-t/src/com/android/server/net/NetworkStatsService.java
+++ b/service-t/src/com/android/server/net/NetworkStatsService.java
@@ -95,7 +95,6 @@
 import android.content.pm.PackageManager;
 import android.content.res.Resources;
 import android.database.ContentObserver;
-import android.net.ConnectivityManager;
 import android.net.DataUsageRequest;
 import android.net.INetd;
 import android.net.INetworkStatsService;
@@ -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);
@@ -1272,93 +1383,6 @@
         return true;
     }
 
-    private static String str(NetworkStatsCollection.Key key) {
-        StringBuilder sb = new StringBuilder()
-                .append(key.ident.toString())
-                .append(" uid=").append(key.uid);
-        if (key.set != SET_FOREGROUND) {
-            sb.append(" set=").append(key.set);
-        }
-        if (key.tag != 0) {
-            sb.append(" tag=").append(key.tag);
-        }
-        return sb.toString();
-    }
-
-    // The importer will modify some keys when importing them.
-    // In order to keep the comparison code simple, add such special cases here and simply
-    // ignore them. This should not impact fidelity much because the start/end checks and the total
-    // bytes check still need to pass.
-    private static boolean couldKeyChangeOnImport(NetworkStatsCollection.Key key) {
-        if (key.ident.isEmpty()) return false;
-        final NetworkIdentity firstIdent = key.ident.iterator().next();
-
-        // Non-mobile network with non-empty RAT type.
-        // This combination is invalid and the NetworkIdentity.Builder will throw if it is passed
-        // in, but it looks like it was previously possible to persist it to disk. The importer sets
-        // the RAT type to NETWORK_TYPE_ALL.
-        if (firstIdent.getType() != ConnectivityManager.TYPE_MOBILE
-                && firstIdent.getRatType() != NetworkTemplate.NETWORK_TYPE_ALL) {
-            return true;
-        }
-
-        return false;
-    }
-
-    @Nullable
-    private static String compareStats(
-            NetworkStatsCollection migrated, NetworkStatsCollection legacy) {
-        final Map<NetworkStatsCollection.Key, NetworkStatsHistory> migEntries =
-                migrated.getEntries();
-        final Map<NetworkStatsCollection.Key, NetworkStatsHistory> legEntries = legacy.getEntries();
-
-        final ArraySet<NetworkStatsCollection.Key> unmatchedLegKeys =
-                new ArraySet<>(legEntries.keySet());
-
-        for (NetworkStatsCollection.Key legKey : legEntries.keySet()) {
-            final NetworkStatsHistory legHistory = legEntries.get(legKey);
-            final NetworkStatsHistory migHistory = migEntries.get(legKey);
-
-            if (migHistory == null && couldKeyChangeOnImport(legKey)) {
-                unmatchedLegKeys.remove(legKey);
-                continue;
-            }
-
-            if (migHistory == null) {
-                return "Missing migrated history for legacy key " + str(legKey)
-                        + ", legacy history was " + legHistory;
-            }
-            if (!migHistory.isSameAs(legHistory)) {
-                return "Difference in history for key " + legKey + "; legacy history " + legHistory
-                        + ", migrated history " + migHistory;
-            }
-            unmatchedLegKeys.remove(legKey);
-        }
-
-        if (!unmatchedLegKeys.isEmpty()) {
-            final NetworkStatsHistory first = legEntries.get(unmatchedLegKeys.valueAt(0));
-            return "Found unmatched legacy keys: count=" + unmatchedLegKeys.size()
-                    + ", first unmatched collection " + first;
-        }
-
-        if (migrated.getStartMillis() != legacy.getStartMillis()
-                || migrated.getEndMillis() != legacy.getEndMillis()) {
-            return "Start / end of the collections "
-                    + migrated.getStartMillis() + "/" + legacy.getStartMillis() + " and "
-                    + migrated.getEndMillis() + "/" + legacy.getEndMillis()
-                    + " don't match";
-        }
-
-        if (migrated.getTotalBytes() != legacy.getTotalBytes()) {
-            return "Total bytes " + migrated.getTotalBytes() + " and " + legacy.getTotalBytes()
-                    + " don't match for collections with start/end "
-                    + migrated.getStartMillis()
-                    + "/" + legacy.getStartMillis();
-        }
-
-        return null;
-    }
-
     @GuardedBy("mStatsLock")
     @NonNull
     private NetworkStatsCollection readPlatformCollectionForRecorder(
@@ -1435,7 +1459,7 @@
         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,
                 NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK,
@@ -1452,7 +1476,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) {
@@ -1469,6 +1494,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);
@@ -1607,7 +1633,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.
@@ -1628,7 +1654,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);
     }
@@ -1955,36 +1981,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) {
@@ -1993,19 +2039,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);
     }
 
     /**
@@ -2175,6 +2209,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);
 
@@ -2246,6 +2281,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) {
@@ -2726,6 +2762,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();
 
@@ -2840,9 +2887,9 @@
             }
 
             pw.println();
-            pw.println("InterfaceMapUpdater:");
+            pw.println("InterfaceMapHelper:");
             pw.increaseIndent();
-            mInterfaceMapUpdater.dump(pw);
+            mInterfaceMapHelper.dump(pw);
             pw.decreaseIndent();
 
             pw.println();
@@ -2864,6 +2911,12 @@
             dumpStatsMapLocked(mStatsMapB, pw, "mStatsMapB");
             dumpIfaceStatsMapLocked(pw);
             pw.decreaseIndent();
+
+            pw.println();
+            pw.println("SkDestroyListener logs:");
+            pw.increaseIndent();
+            mSkDestroyListener.dump(pw);
+            pw.decreaseIndent();
         }
     }
 
@@ -2983,7 +3036,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) + " "
@@ -3001,7 +3054,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 + " "
@@ -3361,10 +3414,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/Android.bp b/service/Android.bp
index 82f64ba..4394f03 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"],
 }
@@ -107,10 +108,6 @@
         "-Werror",
         "-Wno-unused-parameter",
         "-Wthread-safety",
-
-        // AServiceManager_waitForService is available on only 31+, but it's still safe for Thread
-        // service because it's enabled on only 34+
-        "-Wno-unguarded-availability",
     ],
     srcs: [
         ":services.connectivity-netstats-jni-sources",
@@ -189,7 +186,7 @@
         "androidx.annotation_annotation",
         "connectivity-net-module-utils-bpf",
         "connectivity_native_aidl_interface-lateststable-java",
-        "dnsresolver_aidl_interface-V12-java",
+        "dnsresolver_aidl_interface-V13-java",
         "modules-utils-shell-command-handler",
         "net-utils-device-common",
         "net-utils-device-common-ip",
@@ -204,7 +201,11 @@
     apex_available: [
         "com.android.tethering",
     ],
-    lint: { strict_updatability_linting: true },
+    lint: {
+        strict_updatability_linting: true,
+        baseline_filename: "lint-baseline.xml",
+
+    },
     visibility: [
         "//packages/modules/Connectivity/service-t",
         "//packages/modules/Connectivity/tests:__subpackages__",
@@ -229,6 +230,7 @@
     ],
     lint: {
         strict_updatability_linting: true,
+
     },
 }
 
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-as/strings.xml b/service/ServiceConnectivityResources/res/values-as/strings.xml
index 7e4dd42..e753cb3 100644
--- a/service/ServiceConnectivityResources/res/values-as/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-as/strings.xml
@@ -18,7 +18,7 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="connectivityResourcesAppLabel" msgid="2476261877900882974">"ছিষ্টেম সংযোগৰ উৎস"</string>
-    <string name="wifi_available_sign_in" msgid="8041178343789805553">"Wi-Fi নেটৱৰ্কত ছাইন ইন কৰক"</string>
+    <string name="wifi_available_sign_in" msgid="8041178343789805553">"ৱাই-ফাই নেটৱৰ্কত ছাইন ইন কৰক"</string>
     <string name="network_available_sign_in" msgid="2622520134876355561">"নেটৱৰ্কত ছাইন ইন কৰক"</string>
     <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
diff --git a/service/ServiceConnectivityResources/res/values-kn/strings.xml b/service/ServiceConnectivityResources/res/values-kn/strings.xml
index 8046d0e..98a2d9c 100644
--- a/service/ServiceConnectivityResources/res/values-kn/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-kn/strings.xml
@@ -30,7 +30,7 @@
     <string name="network_partial_connectivity" msgid="5549503845834993258">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> ಸೀಮಿತ ಸಂಪರ್ಕ ಕಲ್ಪಿಸುವಿಕೆಯನ್ನು ಹೊಂದಿದೆ"</string>
     <string name="network_partial_connectivity_detailed" msgid="4732435946300249845">"ಹೇಗಾದರೂ ಸಂಪರ್ಕಿಸಲು ಟ್ಯಾಪ್ ಮಾಡಿ"</string>
     <string name="network_switch_metered" msgid="5016937523571166319">"<xliff:g id="NETWORK_TYPE">%1$s</xliff:g> ಗೆ ಬದಲಾಯಿಸಲಾಗಿದೆ"</string>
-    <string name="network_switch_metered_detail" msgid="1257300152739542096">"<xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> ಇಂಟರ್ನೆಟ್ ಆ್ಯಕ್ಸೆಸ್ ಹೊಂದಿಲ್ಲದಿರುವಾಗ, ಸಾಧನವು <xliff:g id="NEW_NETWORK">%1$s</xliff:g> ಬಳಸುತ್ತದೆ. ಶುಲ್ಕಗಳು ಅನ್ವಯವಾಗಬಹುದು."</string>
+    <string name="network_switch_metered_detail" msgid="1257300152739542096">"<xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> ಇಂಟರ್ನೆಟ್ ಪ್ರವೇಶ ಹೊಂದಿಲ್ಲದಿರುವಾಗ, ಸಾಧನವು <xliff:g id="NEW_NETWORK">%1$s</xliff:g> ಬಳಸುತ್ತದೆ. ಶುಲ್ಕಗಳು ಅನ್ವಯವಾಗಬಹುದು."</string>
     <string name="network_switch_metered_toast" msgid="70691146054130335">"<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> ರಿಂದ <xliff:g id="NEW_NETWORK">%2$s</xliff:g> ಗೆ ಬದಲಾಯಿಸಲಾಗಿದೆ"</string>
   <string-array name="network_switch_type_name">
     <item msgid="3004933964374161223">"ಮೊಬೈಲ್ ಡೇಟಾ"</item>
diff --git a/service/ServiceConnectivityResources/res/values/config.xml b/service/ServiceConnectivityResources/res/values/config.xml
index 045d707f..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>
@@ -196,8 +203,8 @@
 
     <!-- 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 U+ and eth\\d+ on
-         Android T. -->
+         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.
diff --git a/service/ServiceConnectivityResources/res/values/config_thread.xml b/service/ServiceConnectivityResources/res/values/config_thread.xml
new file mode 100644
index 0000000..14b5427
--- /dev/null
+++ b/service/ServiceConnectivityResources/res/values/config_thread.xml
@@ -0,0 +1,29 @@
+<?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">
+    <!-- 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..f2c4d91 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,9 @@
             <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_location_use_for_country_code_enabled" />
         </policy>
     </overlayable>
 </resources>
diff --git a/service/ServiceConnectivityResources/res/values/strings.xml b/service/ServiceConnectivityResources/res/values/strings.xml
index b2fa5f5..246155e 100644
--- a/service/ServiceConnectivityResources/res/values/strings.xml
+++ b/service/ServiceConnectivityResources/res/values/strings.xml
@@ -29,6 +29,15 @@
     <!-- A notification is shown when a captive portal network is detected.  This is the notification's message. -->
     <string name="network_available_sign_in_detailed"><xliff:g id="network_ssid">%1$s</xliff:g></string>
 
+    <!-- A notification is shown when the system detected no internet access on a mobile network, possibly because the user is out of data, and a webpage is available to get Internet access (possibly by topping up or getting a subscription).  This is the notification's title. -->
+    <string name="mobile_network_available_no_internet">No internet</string>
+
+    <!-- A notification is shown when the system detected no internet access on a mobile network, possibly because the user is out of data, and a webpage is available to get Internet access (possibly by topping up or getting a subscription).  This is the notification's message. -->
+    <string name="mobile_network_available_no_internet_detailed">You may be out of data from <xliff:g id="network_carrier" example="Android Mobile">%1$s</xliff:g>. Tap for options.</string>
+
+    <!-- A notification is shown when the system detected no internet access on a mobile network, possibly because the user is out of data, and a webpage is available to get Internet access (possibly by topping up or getting a subscription).  This is the notification's message when the carrier is unknown. -->
+    <string name="mobile_network_available_no_internet_detailed_unknown_carrier">You may be out of data. Tap for options.</string>
+
     <!-- A notification is shown when the user connects to a Wi-Fi network and the system detects that that network has no Internet access. This is the notification's title. -->
     <string name="wifi_no_internet"><xliff:g id="network_ssid" example="GoogleGuest">%1$s</xliff:g> has no internet access</string>
 
diff --git a/service/jni/com_android_server_ServiceManagerWrapper.cpp b/service/jni/com_android_server_ServiceManagerWrapper.cpp
index 0cd58f4..0e32726 100644
--- a/service/jni/com_android_server_ServiceManagerWrapper.cpp
+++ b/service/jni/com_android_server_ServiceManagerWrapper.cpp
@@ -25,7 +25,13 @@
 static jobject com_android_server_ServiceManagerWrapper_waitForService(
         JNIEnv* env, jobject clazz, jstring serviceName) {
     ScopedUtfChars name(env, serviceName);
+
+// AServiceManager_waitForService is available on only 31+, but it's still safe for Thread
+// service because it's enabled on only 34+
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wunguarded-availability"
     return AIBinder_toJavaBinder(env, AServiceManager_waitForService(name.c_str()));
+#pragma clang diagnostic pop
 }
 
 /*
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..b09589c 100644
--- a/service/lint-baseline.xml
+++ b/service/lint-baseline.xml
@@ -1,5 +1,71 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 8.0.0-dev" type="baseline" dependencies="true" variant="all" version="8.0.0-dev">
+<issues format="6" by="lint 8.4.0-alpha01" type="baseline" client="" dependencies="true" name="" variant="all" version="8.4.0-alpha01">
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `BpfBitmap`"
+        errorLine1="                return new BpfBitmap(BLOCKED_PORTS_MAP_PATH);"
+        errorLine2="                       ~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/connectivity/ConnectivityNativeService.java"
+            line="61"
+            column="24"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `set`"
+        errorLine1="            mBpfBlockedPortsMap.set(port);"
+        errorLine2="                                ~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/connectivity/ConnectivityNativeService.java"
+            line="96"
+            column="33"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `unset`"
+        errorLine1="            mBpfBlockedPortsMap.unset(port);"
+        errorLine2="                                ~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/connectivity/ConnectivityNativeService.java"
+            line="107"
+            column="33"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `clear`"
+        errorLine1="            mBpfBlockedPortsMap.clear();"
+        errorLine2="                                ~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/connectivity/ConnectivityNativeService.java"
+            line="118"
+            column="33"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `get`"
+        errorLine1="                if (mBpfBlockedPortsMap.get(i)) portMap.add(i);"
+        errorLine2="                                        ~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/connectivity/ConnectivityNativeService.java"
+            line="131"
+            column="41"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `android.os.BatteryStatsManager#reportNetworkInterfaceForTransports`"
+        errorLine1="            batteryStats.reportNetworkInterfaceForTransports(iface, transportTypes);"
+        errorLine2="                         ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+            line="1447"
+            column="26"/>
+    </issue>
 
     <issue
         id="NewApi"
@@ -8,23 +74,562 @@
         errorLine2="                     ~~~~~~~~~~~~~~~~~~">
         <location
             file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
-            line="1358"
+            line="1458"
             column="22"/>
     </issue>
 
     <issue
         id="NewApi"
+        message="Call requires API level 33 (current min is 30): `getProgramId`"
+        errorLine1="            return BpfUtils.getProgramId(attachType);"
+        errorLine2="                            ~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+            line="1572"
+            column="29"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Class requires API level 31 (current min is 30): `android.net.NetworkPolicyManager`"
+        errorLine1="        mPolicyManager = mContext.getSystemService(NetworkPolicyManager.class);"
+        errorLine2="                                                   ~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+            line="1740"
+            column="52"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `android.net.NetworkPolicyManager#registerNetworkPolicyCallback`"
+        errorLine1="        mPolicyManager.registerNetworkPolicyCallback(null, mPolicyCallback);"
+        errorLine2="                       ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+            line="1753"
+            column="24"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Cast to `UidFrozenStateChangedCallback` requires API level 34 (current min is 30)"
+        errorLine1="                    new UidFrozenStateChangedCallback() {"
+        errorLine2="                    ^">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+            line="1888"
+            column="21"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Class requires API level 34 (current min is 30): `android.app.ActivityManager.UidFrozenStateChangedCallback`"
+        errorLine1="                    new UidFrozenStateChangedCallback() {"
+        errorLine2="                        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+            line="1888"
+            column="25"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 34 (current min is 30): `android.app.ActivityManager#registerUidFrozenStateChangedCallback`"
+        errorLine1="            activityManager.registerUidFrozenStateChangedCallback("
+        errorLine2="                            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+            line="1907"
+            column="29"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `android.net.NetworkPolicyManager#isUidNetworkingBlocked`"
+        errorLine1="            return mPolicyManager.isUidNetworkingBlocked(uid, metered);"
+        errorLine2="                                  ~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+            line="2162"
+            column="35"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `android.net.NetworkPolicyManager#getRestrictBackgroundStatus`"
+        errorLine1="            return mPolicyManager.getRestrictBackgroundStatus(callerUid);"
+        errorLine2="                                  ~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+            line="2947"
+            column="35"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `android.net.NetworkStateSnapshot#getNetwork`"
+        errorLine1="            final NetworkAgentInfo nai = getNetworkAgentInfoForNetwork(snapshot.getNetwork());"
+        errorLine2="                                                                                ~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+            line="2963"
+            column="81"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `android.net.NetworkStateSnapshot#getLinkProperties`"
+        errorLine1="                        snapshot.getLinkProperties(), snapshot.getNetworkCapabilities(),"
+        errorLine2="                                 ~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+            line="2966"
+            column="34"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `android.net.NetworkStateSnapshot#getNetworkCapabilities`"
+        errorLine1="                        snapshot.getLinkProperties(), snapshot.getNetworkCapabilities(),"
+        errorLine2="                                                               ~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+            line="2966"
+            column="64"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `android.net.NetworkStateSnapshot#getNetwork`"
+        errorLine1="                        snapshot.getNetwork(), snapshot.getSubscriberId()));"
+        errorLine2="                                 ~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+            line="2967"
+            column="34"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `android.net.NetworkStateSnapshot#getSubscriberId`"
+        errorLine1="                        snapshot.getNetwork(), snapshot.getSubscriberId()));"
+        errorLine2="                                                        ~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+            line="2967"
+            column="57"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Class requires API level 31 (current min is 30): `android.net.NetworkPolicyManager.NetworkPolicyCallback`"
+        errorLine1="    private final NetworkPolicyCallback mPolicyCallback = new NetworkPolicyCallback() {"
+        errorLine2="                                                              ~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+            line="3210"
+            column="63"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 33 (current min is 30): `dump`"
+        errorLine1="            mBpfNetMaps.dump(pw, fd, verbose);"
+        errorLine2="                        ~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+            line="4155"
+            column="25"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `android.os.Build#isDebuggable`"
+        errorLine1="        if (!Build.isDebuggable()) {"
+        errorLine2="                   ~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+            line="5721"
+            column="20"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Class requires API level 31 (current min is 30): `android.net.NetworkPolicyManager`"
+        errorLine1="                 mContext.getSystemService(NetworkPolicyManager.class);"
+        errorLine2="                                           ~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+            line="6174"
+            column="44"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `android.net.NetworkPolicyManager#getMultipathPreference`"
+        errorLine1="            networkPreference = netPolicyManager.getMultipathPreference(network);"
+        errorLine2="                                                 ~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+            line="6179"
+            column="50"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `new android.net.UnderlyingNetworkInfo`"
+        errorLine1="        return new UnderlyingNetworkInfo(nai.networkCapabilities.getOwnerUid(),"
+        errorLine2="               ~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+            line="6819"
+            column="16"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `android.net.NetworkPolicyManager#isUidRestrictedOnMeteredNetworks`"
+        errorLine1="            if (mPolicyManager.isUidRestrictedOnMeteredNetworks(uid)) {"
+        errorLine2="                               ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+            line="7822"
+            column="32"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `android.os.Build#isDebuggable`"
+        errorLine1="            if (Build.isDebuggable()) {"
+        errorLine2="                      ~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+            line="9943"
+            column="23"/>
+    </issue>
+
+    <issue
+        id="NewApi"
         message="Call requires API level 31 (current min is 30): `android.app.usage.NetworkStatsManager#notifyNetworkStatus`"
         errorLine1="            mStatsManager.notifyNetworkStatus(getDefaultNetworks(),"
         errorLine2="                          ~~~~~~~~~~~~~~~~~~~">
         <location
             file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
-            line="9938"
+            line="10909"
             column="27"/>
     </issue>
 
     <issue
         id="NewApi"
+        message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
+        errorLine1="                IoUtils.closeQuietly(pfd);"
+        errorLine2="                        ~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+            line="10962"
+            column="25"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
+        errorLine1="                IoUtils.closeQuietly(pfd);"
+        errorLine2="                        ~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+            line="10979"
+            column="25"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Class requires API level 31 (current min is 30): `android.net.NetworkWatchlistManager`"
+        errorLine1="        NetworkWatchlistManager nwm = mContext.getSystemService(NetworkWatchlistManager.class);"
+        errorLine2="                                                                ~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+            line="11035"
+            column="65"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `android.net.NetworkWatchlistManager#getWatchlistConfigHash`"
+        errorLine1="        return nwm.getWatchlistConfigHash();"
+        errorLine2="                   ~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+            line="11041"
+            column="20"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 33 (current min is 30): `getProgramId`"
+        errorLine1="                        final int ret = BpfUtils.getProgramId(type);"
+        errorLine2="                                                 ~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+            line="11180"
+            column="50"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `android.os.BatteryStatsManager#reportMobileRadioPowerState`"
+        errorLine1="                    bs.reportMobileRadioPowerState(isActive, uid);"
+        errorLine2="                       ~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+            line="12254"
+            column="24"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `android.os.BatteryStatsManager#reportWifiRadioPowerState`"
+        errorLine1="                    bs.reportWifiRadioPowerState(isActive, uid);"
+        errorLine2="                       ~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+            line="12257"
+            column="24"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 33 (current min is 30): `addNiceApp`"
+        errorLine1="                mBpfNetMaps.addNiceApp(uid);"
+        errorLine2="                            ~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+            line="13079"
+            column="29"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 33 (current min is 30): `removeNiceApp`"
+        errorLine1="                mBpfNetMaps.removeNiceApp(uid);"
+        errorLine2="                            ~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+            line="13081"
+            column="29"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 33 (current min is 30): `addNaughtyApp`"
+        errorLine1="                mBpfNetMaps.addNaughtyApp(uid);"
+        errorLine2="                            ~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+            line="13094"
+            column="29"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 33 (current min is 30): `removeNaughtyApp`"
+        errorLine1="                mBpfNetMaps.removeNaughtyApp(uid);"
+        errorLine2="                            ~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+            line="13096"
+            column="29"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `android.os.UserHandle#getUid`"
+        errorLine1="            final int uid = uh.getUid(appId);"
+        errorLine2="                               ~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+            line="13112"
+            column="32"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 33 (current min is 30): `setUidRule`"
+        errorLine1="            mBpfNetMaps.setUidRule(chain, uid, firewallRule);"
+        errorLine2="                        ~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+            line="13130"
+            column="25"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 33 (current min is 30): `setChildChain`"
+        errorLine1="            mBpfNetMaps.setChildChain(chain, enable);"
+        errorLine2="                        ~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+            line="13195"
+            column="25"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 33 (current min is 30): `isChainEnabled`"
+        errorLine1="        return mBpfNetMaps.isChainEnabled(chain);"
+        errorLine2="                           ~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+            line="13213"
+            column="28"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 33 (current min is 30): `replaceUidChain`"
+        errorLine1="        mBpfNetMaps.replaceUidChain(chain, uids);"
+        errorLine2="                    ~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+            line="13220"
+            column="21"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `BpfMap`"
+        errorLine1="        mBpfDscpIpv4Policies = new BpfMap&lt;Struct.S32, DscpPolicyValue&gt;(IPV4_POLICY_MAP_PATH,"
+        errorLine2="                               ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/connectivity/DscpPolicyTracker.java"
+            line="88"
+            column="32"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `BpfMap`"
+        errorLine1="        mBpfDscpIpv6Policies = new BpfMap&lt;Struct.S32, DscpPolicyValue&gt;(IPV6_POLICY_MAP_PATH,"
+        errorLine2="                               ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/connectivity/DscpPolicyTracker.java"
+            line="90"
+            column="32"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `insertOrReplaceEntry`"
+        errorLine1="                mBpfDscpIpv4Policies.insertOrReplaceEntry("
+        errorLine2="                                     ~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/connectivity/DscpPolicyTracker.java"
+            line="183"
+            column="38"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `insertOrReplaceEntry`"
+        errorLine1="                mBpfDscpIpv6Policies.insertOrReplaceEntry("
+        errorLine2="                                     ~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/connectivity/DscpPolicyTracker.java"
+            line="194"
+            column="38"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `replaceEntry`"
+        errorLine1="            mBpfDscpIpv4Policies.replaceEntry(new Struct.S32(index), DscpPolicyValue.NONE);"
+        errorLine2="                                 ~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/connectivity/DscpPolicyTracker.java"
+            line="261"
+            column="34"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `replaceEntry`"
+        errorLine1="            mBpfDscpIpv6Policies.replaceEntry(new Struct.S32(index), DscpPolicyValue.NONE);"
+        errorLine2="                                 ~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/connectivity/DscpPolicyTracker.java"
+            line="262"
+            column="34"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `java.net.InetAddress#parseNumericAddress`"
+        errorLine1='            InetAddress.parseNumericAddress("::").getAddress();'
+        errorLine2="                        ~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/connectivity/DscpPolicyValue.java"
+            line="99"
+            column="25"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `new android.net.NetworkStateSnapshot`"
+        errorLine1="            return new NetworkStateSnapshot(network, new NetworkCapabilities(networkCapabilities),"
+        errorLine2="                   ~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/connectivity/NetworkAgentInfo.java"
+            line="1353"
+            column="20"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
+        errorLine1="            IoUtils.closeQuietly(mFileDescriptor);"
+        errorLine2="                    ~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/connectivity/NetworkDiagnostics.java"
+            line="570"
+            column="21"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Field requires API level 31 (current min is 30): `android.os.Build.VERSION#DEVICE_INITIAL_SDK_INT`"
+        errorLine1="            return Build.VERSION.DEVICE_INITIAL_SDK_INT;"
+        errorLine2="                   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/connectivity/PermissionMonitor.java"
+            line="212"
+            column="20"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `android.os.SystemConfigManager#getSystemPermissionUids`"
+        errorLine1="        for (final int uid : mSystemConfigManager.getSystemPermissionUids(INTERNET)) {"
+        errorLine2="                                                  ~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/connectivity/PermissionMonitor.java"
+            line="396"
+            column="51"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `android.os.SystemConfigManager#getSystemPermissionUids`"
+        errorLine1="        for (final int uid : mSystemConfigManager.getSystemPermissionUids(UPDATE_DEVICE_STATS)) {"
+        errorLine2="                                                  ~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/connectivity/PermissionMonitor.java"
+            line="404"
+            column="51"/>
+    </issue>
+
+    <issue
+        id="NewApi"
         message="Call requires API level 31 (current min is 30): `android.content.pm.ApplicationInfo#isOem`"
         errorLine1="        return appInfo.isVendor() || appInfo.isOem() || appInfo.isProduct();"
         errorLine2="                                             ~~~~~">
@@ -58,441 +663,34 @@
 
     <issue
         id="NewApi"
-        message="Call requires API level 31 (current min is 30): `android.net.NetworkPolicyManager#getMultipathPreference`"
-        errorLine1="            networkPreference = netPolicyManager.getMultipathPreference(network);"
-        errorLine2="                                                 ~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
-            line="5498"
-            column="50"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 30): `android.net.NetworkPolicyManager#getRestrictBackgroundStatus`"
-        errorLine1="            return mPolicyManager.getRestrictBackgroundStatus(callerUid);"
-        errorLine2="                                  ~~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
-            line="2565"
-            column="35"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 30): `android.net.NetworkPolicyManager#isUidNetworkingBlocked`"
-        errorLine1="            return mPolicyManager.isUidNetworkingBlocked(uid, metered);"
-        errorLine2="                                  ~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
-            line="1914"
-            column="35"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 30): `android.net.NetworkPolicyManager#isUidRestrictedOnMeteredNetworks`"
-        errorLine1="            if (mPolicyManager.isUidRestrictedOnMeteredNetworks(uid)) {"
-        errorLine2="                               ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
-            line="7094"
-            column="32"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 30): `android.net.NetworkPolicyManager#registerNetworkPolicyCallback`"
-        errorLine1="        mPolicyManager.registerNetworkPolicyCallback(null, mPolicyCallback);"
-        errorLine2="                       ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
-            line="1567"
-            column="24"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 30): `android.net.NetworkStateSnapshot#getLinkProperties`"
-        errorLine1="                        snapshot.getLinkProperties(), snapshot.getNetworkCapabilities(),"
-        errorLine2="                                 ~~~~~~~~~~~~~~~~~">
-        <location
-            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
-            line="2584"
-            column="34"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 30): `android.net.NetworkStateSnapshot#getNetworkCapabilities`"
-        errorLine1="                        snapshot.getLinkProperties(), snapshot.getNetworkCapabilities(),"
-        errorLine2="                                                               ~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
-            line="2584"
-            column="64"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 30): `android.net.NetworkStateSnapshot#getNetwork`"
-        errorLine1="                        snapshot.getNetwork(), snapshot.getSubscriberId()));"
-        errorLine2="                                 ~~~~~~~~~~">
-        <location
-            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
-            line="2585"
-            column="34"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 30): `android.net.NetworkStateSnapshot#getNetwork`"
-        errorLine1="            final NetworkAgentInfo nai = getNetworkAgentInfoForNetwork(snapshot.getNetwork());"
-        errorLine2="                                                                                ~~~~~~~~~~">
-        <location
-            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
-            line="2581"
-            column="81"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 30): `android.net.NetworkStateSnapshot#getSubscriberId`"
-        errorLine1="                        snapshot.getNetwork(), snapshot.getSubscriberId()));"
-        errorLine2="                                                        ~~~~~~~~~~~~~~~">
-        <location
-            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
-            line="2585"
-            column="57"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 30): `android.net.NetworkWatchlistManager#getWatchlistConfigHash`"
-        errorLine1="        return nwm.getWatchlistConfigHash();"
-        errorLine2="                   ~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
-            line="10060"
-            column="20"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 30): `android.net.PacProxyManager#addPacProxyInstalledListener`"
-        errorLine1="        mPacProxyManager.addPacProxyInstalledListener("
-        errorLine2="                         ~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="packages/modules/Connectivity/service/src/com/android/server/connectivity/ProxyTracker.java"
-            line="111"
-            column="26"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 30): `android.net.PacProxyManager#setCurrentProxyScriptUrl`"
-        errorLine1="                        () -&gt; mPacProxyManager.setCurrentProxyScriptUrl(proxyProperties));"
-        errorLine2="                                               ~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="packages/modules/Connectivity/service/src/com/android/server/connectivity/ProxyTracker.java"
-            line="208"
-            column="48"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 30): `android.net.PacProxyManager#setCurrentProxyScriptUrl`"
-        errorLine1="        mPacProxyManager.setCurrentProxyScriptUrl(proxyInfo);"
-        errorLine2="                         ~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="packages/modules/Connectivity/service/src/com/android/server/connectivity/ProxyTracker.java"
-            line="252"
-            column="26"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 30): `android.os.BatteryStatsManager#reportMobileRadioPowerState`"
-        errorLine1="                    bs.reportMobileRadioPowerState(isActive, NO_UID);"
-        errorLine2="                       ~~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
-            line="11006"
-            column="24"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 30): `android.os.BatteryStatsManager#reportNetworkInterfaceForTransports`"
-        errorLine1="            batteryStats.reportNetworkInterfaceForTransports(iface, transportTypes);"
-        errorLine2="                         ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
-            line="1347"
-            column="26"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 30): `android.os.BatteryStatsManager#reportWifiRadioPowerState`"
-        errorLine1="                    bs.reportWifiRadioPowerState(isActive, NO_UID);"
-        errorLine2="                       ~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
-            line="11009"
-            column="24"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 30): `android.os.Build#isDebuggable`"
-        errorLine1="            if (Build.isDebuggable()) {"
-        errorLine2="                      ~~~~~~~~~~~~">
-        <location
-            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
-            line="9074"
-            column="23"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 30): `android.os.Build#isDebuggable`"
-        errorLine1="        if (!Build.isDebuggable()) {"
-        errorLine2="                   ~~~~~~~~~~~~">
-        <location
-            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
-            line="5039"
-            column="20"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 30): `android.os.SystemConfigManager#getSystemPermissionUids`"
-        errorLine1="        for (final int uid : mSystemConfigManager.getSystemPermissionUids(INTERNET)) {"
-        errorLine2="                                                  ~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="packages/modules/Connectivity/service/src/com/android/server/connectivity/PermissionMonitor.java"
-            line="396"
-            column="51"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 30): `android.os.SystemConfigManager#getSystemPermissionUids`"
-        errorLine1="        for (final int uid : mSystemConfigManager.getSystemPermissionUids(UPDATE_DEVICE_STATS)) {"
-        errorLine2="                                                  ~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="packages/modules/Connectivity/service/src/com/android/server/connectivity/PermissionMonitor.java"
-            line="404"
-            column="51"/>
-    </issue>
-
-    <issue
-        id="NewApi"
         message="Call requires API level 31 (current min is 30): `android.os.UserHandle#getUid`"
         errorLine1="                    final int uid = handle.getUid(appId);"
         errorLine2="                                           ~~~~~~">
         <location
             file="packages/modules/Connectivity/service/src/com/android/server/connectivity/PermissionMonitor.java"
-            line="1069"
+            line="1070"
             column="44"/>
     </issue>
 
     <issue
         id="NewApi"
-        message="Call requires API level 31 (current min is 30): `android.system.Os#getsockoptInt`"
-        errorLine1="                tcpDetails.tos = Os.getsockoptInt(fd, IPPROTO_IP, IP_TOS);"
-        errorLine2="                                    ~~~~~~~~~~~~~">
+        message="Call requires API level 33 (current min is 30): `updateUidLockdownRule`"
+        errorLine1="            mBpfNetMaps.updateUidLockdownRule(uid, add);"
+        errorLine2="                        ~~~~~~~~~~~~~~~~~~~~~">
         <location
-            file="packages/modules/Connectivity/service/src/com/android/server/connectivity/TcpKeepaliveController.java"
-            line="285"
-            column="37"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 30): `android.system.Os#getsockoptInt`"
-        errorLine1="                tcpDetails.ttl = Os.getsockoptInt(fd, IPPROTO_IP, IP_TTL);"
-        errorLine2="                                    ~~~~~~~~~~~~~">
-        <location
-            file="packages/modules/Connectivity/service/src/com/android/server/connectivity/TcpKeepaliveController.java"
-            line="287"
-            column="37"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 30): `android.system.Os#getsockoptInt`"
-        errorLine1="            tcpDetails.ack = Os.getsockoptInt(fd, IPPROTO_TCP, TCP_QUEUE_SEQ);"
-        errorLine2="                                ~~~~~~~~~~~~~">
-        <location
-            file="packages/modules/Connectivity/service/src/com/android/server/connectivity/TcpKeepaliveController.java"
-            line="265"
-            column="33"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 30): `android.system.Os#getsockoptInt`"
-        errorLine1="            tcpDetails.seq = Os.getsockoptInt(fd, IPPROTO_TCP, TCP_QUEUE_SEQ);"
-        errorLine2="                                ~~~~~~~~~~~~~">
-        <location
-            file="packages/modules/Connectivity/service/src/com/android/server/connectivity/TcpKeepaliveController.java"
-            line="262"
-            column="33"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 30): `android.system.Os#ioctlInt`"
-        errorLine1="        final int result = Os.ioctlInt(fd, SIOCINQ);"
-        errorLine2="                              ~~~~~~~~">
-        <location
-            file="packages/modules/Connectivity/service/src/com/android/server/connectivity/TcpKeepaliveController.java"
-            line="392"
-            column="31"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 30): `android.system.Os#ioctlInt`"
-        errorLine1="        final int result = Os.ioctlInt(fd, SIOCOUTQ);"
-        errorLine2="                              ~~~~~~~~">
-        <location
-            file="packages/modules/Connectivity/service/src/com/android/server/connectivity/TcpKeepaliveController.java"
-            line="402"
-            column="31"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 30): `java.net.InetAddress#parseNumericAddress`"
-        errorLine1='            InetAddress.parseNumericAddress("::").getAddress();'
-        errorLine2="                        ~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="packages/modules/Connectivity/service/src/com/android/server/connectivity/DscpPolicyValue.java"
-            line="99"
+            file="packages/modules/Connectivity/service/src/com/android/server/connectivity/PermissionMonitor.java"
+            line="1123"
             column="25"/>
     </issue>
 
     <issue
         id="NewApi"
-        message="Call requires API level 31 (current min is 30): `java.net.InetAddress#parseNumericAddress`"
-        errorLine1='    private static final InetAddress GOOGLE_DNS_4 = InetAddress.parseNumericAddress("8.8.8.8");'
-        errorLine2="                                                                ~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="packages/modules/Connectivity/service/src/com/android/server/connectivity/ClatCoordinator.java"
-            line="89"
-            column="65"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
-        errorLine1="                IoUtils.closeQuietly(pfd);"
-        errorLine2="                        ~~~~~~~~~~~~">
-        <location
-            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
-            line="9991"
-            column="25"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
-        errorLine1="                IoUtils.closeQuietly(pfd);"
-        errorLine2="                        ~~~~~~~~~~~~">
-        <location
-            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
-            line="10008"
-            column="25"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
-        errorLine1="            IoUtils.closeQuietly(mFileDescriptor);"
-        errorLine2="                    ~~~~~~~~~~~~">
-        <location
-            file="packages/modules/Connectivity/service/src/com/android/server/connectivity/NetworkDiagnostics.java"
-            line="481"
-            column="21"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 30): `new android.net.NetworkStateSnapshot`"
-        errorLine1="            return new NetworkStateSnapshot(network, new NetworkCapabilities(networkCapabilities),"
-        errorLine2="                   ~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="packages/modules/Connectivity/service/src/com/android/server/connectivity/NetworkAgentInfo.java"
-            line="1269"
-            column="20"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 30): `new android.net.UnderlyingNetworkInfo`"
-        errorLine1="        return new UnderlyingNetworkInfo(nai.networkCapabilities.getOwnerUid(),"
-        errorLine2="               ~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
-            line="6123"
-            column="16"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Class requires API level 31 (current min is 30): `android.net.NetworkPolicyManager.NetworkPolicyCallback`"
-        errorLine1="    private final NetworkPolicyCallback mPolicyCallback = new NetworkPolicyCallback() {"
-        errorLine2="                                                              ~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
-            line="2827"
-            column="63"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Class requires API level 31 (current min is 30): `android.net.NetworkPolicyManager`"
-        errorLine1="                 mContext.getSystemService(NetworkPolicyManager.class);"
-        errorLine2="                                           ~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
-            line="5493"
-            column="44"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Class requires API level 31 (current min is 30): `android.net.NetworkPolicyManager`"
-        errorLine1="        mPolicyManager = mContext.getSystemService(NetworkPolicyManager.class);"
-        errorLine2="                                                   ~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
-            line="1554"
-            column="52"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Class requires API level 31 (current min is 30): `android.net.NetworkWatchlistManager`"
-        errorLine1="        NetworkWatchlistManager nwm = mContext.getSystemService(NetworkWatchlistManager.class);"
-        errorLine2="                                                                ~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
-            line="10054"
-            column="65"/>
-    </issue>
-
-    <issue
-        id="NewApi"
         message="Class requires API level 31 (current min is 30): `android.net.PacProxyManager.PacProxyInstalledListener`"
         errorLine1="    private class PacProxyInstalledListener implements PacProxyManager.PacProxyInstalledListener {"
         errorLine2="                                                       ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
         <location
             file="packages/modules/Connectivity/service/src/com/android/server/connectivity/ProxyTracker.java"
-            line="90"
+            line="92"
             column="56"/>
     </issue>
 
@@ -503,8 +701,107 @@
         errorLine2="                                                    ~~~~~~~~~~~~~~~">
         <location
             file="packages/modules/Connectivity/service/src/com/android/server/connectivity/ProxyTracker.java"
-            line="108"
+            line="111"
             column="53"/>
     </issue>
 
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `android.net.PacProxyManager#addPacProxyInstalledListener`"
+        errorLine1="            mPacProxyManager.addPacProxyInstalledListener("
+        errorLine2="                             ~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/connectivity/ProxyTracker.java"
+            line="115"
+            column="30"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `android.net.PacProxyManager#setCurrentProxyScriptUrl`"
+        errorLine1="                        () -&gt; mPacProxyManager.setCurrentProxyScriptUrl(proxyProperties));"
+        errorLine2="                                               ~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/connectivity/ProxyTracker.java"
+            line="213"
+            column="48"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `android.net.PacProxyManager#setCurrentProxyScriptUrl`"
+        errorLine1="            mPacProxyManager.setCurrentProxyScriptUrl(proxyInfo);"
+        errorLine2="                             ~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/connectivity/ProxyTracker.java"
+            line="259"
+            column="30"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `android.system.Os#getsockoptInt`"
+        errorLine1="            tcpDetails.seq = Os.getsockoptInt(fd, IPPROTO_TCP, TCP_QUEUE_SEQ);"
+        errorLine2="                                ~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/connectivity/TcpKeepaliveController.java"
+            line="269"
+            column="33"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `android.system.Os#getsockoptInt`"
+        errorLine1="            tcpDetails.ack = Os.getsockoptInt(fd, IPPROTO_TCP, TCP_QUEUE_SEQ);"
+        errorLine2="                                ~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/connectivity/TcpKeepaliveController.java"
+            line="272"
+            column="33"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `android.system.Os#getsockoptInt`"
+        errorLine1="                tcpDetails.tos = Os.getsockoptInt(fd, IPPROTO_IP, IP_TOS);"
+        errorLine2="                                    ~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/connectivity/TcpKeepaliveController.java"
+            line="292"
+            column="37"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `android.system.Os#getsockoptInt`"
+        errorLine1="                tcpDetails.ttl = Os.getsockoptInt(fd, IPPROTO_IP, IP_TTL);"
+        errorLine2="                                    ~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/connectivity/TcpKeepaliveController.java"
+            line="294"
+            column="37"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `android.system.Os#ioctlInt`"
+        errorLine1="        final int result = Os.ioctlInt(fd, SIOCINQ);"
+        errorLine2="                              ~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/connectivity/TcpKeepaliveController.java"
+            line="401"
+            column="31"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `android.system.Os#ioctlInt`"
+        errorLine1="        final int result = Os.ioctlInt(fd, SIOCOUTQ);"
+        errorLine2="                              ~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/connectivity/TcpKeepaliveController.java"
+            line="411"
+            column="31"/>
+    </issue>
+
 </issues>
\ No newline at end of file
diff --git a/service/native/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/BpfLoaderRcUtils.java b/service/src/com/android/server/BpfLoaderRcUtils.java
new file mode 100644
index 0000000..293e757
--- /dev/null
+++ b/service/src/com/android/server/BpfLoaderRcUtils.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server;
+
+import android.annotation.NonNull;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.modules.utils.build.SdkLevel;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * BpfRcUtils is responsible for comparing the bpf loader rc file.
+ *
+ * {@hide}
+ */
+public class BpfLoaderRcUtils {
+    public static final String TAG = BpfLoaderRcUtils.class.getSimpleName();
+
+    private static final List<String> BPF_LOADER_RC_S_T = List.of(
+            "service bpfloader /system/bin/bpfloader",
+            "capabilities CHOWN SYS_ADMIN NET_ADMIN",
+            "rlimit memlock 1073741824 1073741824",
+            "oneshot",
+            "reboot_on_failure reboot,bpfloader-failed",
+            "updatable"
+    );
+
+    private static final List<String> BPF_LOADER_RC_U = List.of(
+            "service bpfloader /system/bin/bpfloader",
+            "capabilities CHOWN SYS_ADMIN NET_ADMIN",
+            "group root graphics network_stack net_admin net_bw_acct net_bw_stats net_raw system",
+            "user root",
+            "rlimit memlock 1073741824 1073741824",
+            "oneshot",
+            "reboot_on_failure reboot,bpfloader-failed",
+            "updatable"
+    );
+
+    private static final List<String> BPF_LOADER_RC_UQPR2 = List.of(
+            "service bpfloader /system/bin/netbpfload",
+            "capabilities CHOWN SYS_ADMIN NET_ADMIN",
+            "group root graphics network_stack net_admin net_bw_acct net_bw_stats net_raw system",
+            "user root",
+            "rlimit memlock 1073741824 1073741824",
+            "oneshot",
+            "reboot_on_failure reboot,bpfloader-failed",
+            "updatable"
+    );
+
+
+    private static final String BPF_LOADER_RC_FILE_PATH = "/etc/init/bpfloader.rc";
+    private static final String NET_BPF_LOAD_RC_FILE_PATH = "/etc/init/netbpfload.rc";
+
+    private BpfLoaderRcUtils() {
+    }
+
+    /**
+     * Load the bpf rc file content from the input stream.
+     */
+    @VisibleForTesting
+    public static List<String> loadExistingBpfRcFile(@NonNull InputStream inputStream) {
+        List<String> contents = new ArrayList<>();
+        boolean bpfSectionFound = false;
+        try (BufferedReader br = new BufferedReader(
+                new InputStreamReader(inputStream, StandardCharsets.ISO_8859_1))) {
+            String line;
+            while ((line = br.readLine()) != null) {
+                line = line.trim();
+                if (line.isEmpty()) {
+                    continue;
+                }
+                if (line.startsWith("#")) {
+                    continue;
+                }
+                // If bpf service section was found and new service or action section start. The
+                // read should stop.
+                if (bpfSectionFound && (line.startsWith("service ") || (line.startsWith("on ")))) {
+                    break;
+                }
+                if (line.startsWith("service bpfloader ")) {
+                    bpfSectionFound = true;
+                }
+                if (bpfSectionFound) {
+                    contents.add(line);
+                }
+            }
+        } catch (IOException e) {
+            Log.wtf("read input stream failed.", e);
+            contents.clear();
+            return contents;
+        }
+        return contents;
+    }
+
+    /**
+     * Check the bpfLoader rc file on the system image matches any of the template files.
+     */
+    public static boolean checkBpfLoaderRc() {
+        File bpfRcFile = new File(BPF_LOADER_RC_FILE_PATH);
+        if (!bpfRcFile.exists()) {
+            if (SdkLevel.isAtLeastU()) {
+                bpfRcFile = new File(NET_BPF_LOAD_RC_FILE_PATH);
+            }
+            if (!bpfRcFile.exists()) {
+                Log.wtf(TAG,
+                        "neither " + BPF_LOADER_RC_FILE_PATH + " nor " + NET_BPF_LOAD_RC_FILE_PATH
+                                + " exist.");
+                return false;
+            }
+            // Check bpf rc file in U QPR2
+            return compareBpfLoaderRc(bpfRcFile, BPF_LOADER_RC_UQPR2);
+        }
+
+        if (SdkLevel.isAtLeastU()) {
+            // Check bpf rc file in U
+            return compareBpfLoaderRc(bpfRcFile, BPF_LOADER_RC_U);
+        }
+        // Check bpf rc file in S/T
+        return compareBpfLoaderRc(bpfRcFile, BPF_LOADER_RC_S_T);
+    }
+
+    private static boolean compareBpfLoaderRc(@NonNull File bpfRcFile,
+            @NonNull List<String> template) {
+        try {
+            List<String> actualContent = loadExistingBpfRcFile(new FileInputStream(bpfRcFile));
+            if (!actualContent.equals(template)) {
+                Log.wtf(TAG, "BPF rc file is not same as the template files " + actualContent);
+                return false;
+            }
+        } catch (FileNotFoundException e) {
+            Log.wtf(bpfRcFile.getPath() + " doesn't exist.", e);
+            return false;
+        }
+        return true;
+    }
+}
diff --git a/service/src/com/android/server/BpfNetMaps.java b/service/src/com/android/server/BpfNetMaps.java
index 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 7d1644e..a995439 100755
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -32,6 +32,7 @@
 import static android.net.ConnectivityDiagnosticsManager.DataStallReport.KEY_DNS_CONSECUTIVE_TIMEOUTS;
 import static android.net.ConnectivityDiagnosticsManager.DataStallReport.KEY_TCP_METRICS_COLLECTION_PERIOD_MILLIS;
 import static android.net.ConnectivityDiagnosticsManager.DataStallReport.KEY_TCP_PACKET_FAIL_RATE;
+import static android.net.ConnectivityManager.ACTION_RESTRICT_BACKGROUND_CHANGED;
 import static android.net.ConnectivityManager.BLOCKED_METERED_REASON_MASK;
 import static android.net.ConnectivityManager.BLOCKED_REASON_LOCKDOWN_VPN;
 import static android.net.ConnectivityManager.BLOCKED_REASON_NONE;
@@ -64,6 +65,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;
@@ -170,6 +172,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;
@@ -289,6 +292,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;
@@ -313,12 +317,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;
@@ -327,6 +331,7 @@
 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;
@@ -346,6 +351,7 @@
 import java.io.PrintWriter;
 import java.io.Writer;
 import java.net.Inet4Address;
+import java.net.Inet6Address;
 import java.net.InetAddress;
 import java.net.InetSocketAddress;
 import java.net.SocketException;
@@ -360,6 +366,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;
@@ -495,6 +502,7 @@
     @GuardedBy("mTNSLock")
     private TestNetworkService mTNS;
     private final CompanionDeviceManagerProxyService mCdmps;
+    private final MulticastRoutingCoordinatorService mMulticastRoutingCoordinatorService;
     private final RoutingCoordinatorService mRoutingCoordinatorService;
 
     private final Object mTNSLock = new Object();
@@ -940,6 +948,8 @@
 
     private final IpConnectivityLog mMetricsLog;
 
+    @Nullable private final NetworkRequestStateStatsMetrics mNetworkRequestStateStatsMetrics;
+
     @GuardedBy("mBandwidthRequests")
     private final SparseArray<Integer> mBandwidthRequests = new SparseArray<>(10);
 
@@ -969,6 +979,9 @@
     // Flag to optimize closing frozen app sockets by waiting for the cellular modem to wake up.
     private final boolean mDelayDestroyFrozenSockets;
 
+    // Flag to allow SysUI to receive connectivity reports for wifi picker UI.
+    private final boolean mAllowSysUiConnectivityReports;
+
     // Uids that ConnectivityService is pending to close sockets of.
     private final Set<Integer> mPendingFrozenUids = new ArraySet<>();
 
@@ -1273,7 +1286,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)) {
@@ -1282,7 +1295,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");
             }
@@ -1359,8 +1372,8 @@
         /**
          * Create a HandlerThread to use in ConnectivityService.
          */
-        public HandlerThread makeHandlerThread() {
-            return new HandlerThread("ConnectivityServiceThread");
+        public HandlerThread makeHandlerThread(@NonNull final String tag) {
+            return new HandlerThread(tag);
         }
 
         /**
@@ -1417,6 +1430,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
          */
@@ -1455,7 +1492,7 @@
         public CarrierPrivilegeAuthenticator makeCarrierPrivilegeAuthenticator(
                 @NonNull final Context context, @NonNull final TelephonyManager tm) {
             if (isAtLeastT()) {
-                return new CarrierPrivilegeAuthenticator(context, this, tm);
+                return new CarrierPrivilegeAuthenticator(context, tm);
             } else {
                 return null;
             }
@@ -1469,6 +1506,13 @@
         }
 
         /**
+         * @see DeviceConfigUtils#isTetheringFeatureNotChickenedOut
+         */
+        public boolean isFeatureNotChickenedOut(Context context, String name) {
+            return DeviceConfigUtils.isTetheringFeatureNotChickenedOut(context, name);
+        }
+
+        /**
          * Get the BpfNetMaps implementation to use in ConnectivityService.
          * @param netd a netd binder
          * @return BpfNetMaps implementation.
@@ -1643,6 +1687,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,
@@ -1696,7 +1741,7 @@
 
         mNetd = netd;
         mBpfNetMaps = mDeps.getBpfNetMaps(mContext, netd);
-        mHandlerThread = mDeps.makeHandlerThread();
+        mHandlerThread = mDeps.makeHandlerThread("ConnectivityServiceThread");
         mPermissionMonitor =
                 new PermissionMonitor(mContext, mNetd, mBpfNetMaps, mHandlerThread);
         mHandlerThread.start();
@@ -1760,7 +1805,25 @@
         mUserAllContext.registerReceiver(mPackageIntentReceiver, packageIntentFilter,
                 null /* broadcastPermission */, mHandler);
 
-        mNetworkActivityTracker = new LegacyNetworkActivityTracker(mContext, mNetd, mHandler);
+        // This is needed for pre-V devices to propagate the data saver status
+        // to the BPF map. This isn't supported before Android T because BPF maps are
+        // unsupported, and it's also unnecessary on Android V and later versions,
+        // as the platform code handles data saver bit updates. Additionally, checking
+        // the initial data saver status here is superfluous because the intent won't
+        // be sent until the system is ready.
+        if (mDeps.isAtLeastT() && !mDeps.isAtLeastV()) {
+            final IntentFilter dataSaverIntentFilter =
+                    new IntentFilter(ACTION_RESTRICT_BACKGROUND_CHANGED);
+            mUserAllContext.registerReceiver(mDataSaverReceiver, dataSaverIntentFilter,
+                    null /* broadcastPermission */, mHandler);
+        }
+
+        // TrackMultiNetworkActivities feature should be enabled by trunk stable flag.
+        // But reading the trunk stable flags from mainline modules is not supported yet.
+        // So enabling this feature on V+ release.
+        mTrackMultiNetworkActivities = mDeps.isAtLeastV();
+        mNetworkActivityTracker = new LegacyNetworkActivityTracker(mContext, mNetd, mHandler,
+                mTrackMultiNetworkActivities);
 
         final NetdCallback netdCallback = new NetdCallback();
         try {
@@ -1830,11 +1893,15 @@
         }
 
         mRoutingCoordinatorService = new RoutingCoordinatorService(netd);
+        mMulticastRoutingCoordinatorService =
+                mDeps.makeMulticastRoutingCoordinatorService(mHandler);
 
         mDestroyFrozenSockets = mDeps.isAtLeastU()
                 && mDeps.isFeatureEnabled(context, KEY_DESTROY_FROZEN_SOCKETS_VERSION);
         mDelayDestroyFrozenSockets = mDeps.isAtLeastU()
                 && mDeps.isFeatureEnabled(context, DELAY_DESTROY_FROZEN_SOCKETS_VERSION);
+        mAllowSysUiConnectivityReports = mDeps.isFeatureNotChickenedOut(
+                mContext, ALLOW_SYSUI_CONNECTIVITY_REPORTS);
         if (mDestroyFrozenSockets) {
             final UidFrozenStateChangedCallback frozenStateChangedCallback =
                     new UidFrozenStateChangedCallback() {
@@ -1859,6 +1926,10 @@
             activityManager.registerUidFrozenStateChangedCallback(
                     (Runnable r) -> r.run(), frozenStateChangedCallback);
         }
+
+        if (mDeps.isFeatureNotChickenedOut(mContext, LOG_BPF_RC)) {
+            mHandler.post(BpfLoaderRcUtils::checkBpfLoaderRc);
+        }
     }
 
     /**
@@ -2632,6 +2703,12 @@
         }
     }
 
+    private boolean canSeeAllowedUids(final int pid, final int uid, final int netOwnerUid) {
+        return Process.SYSTEM_UID == uid
+                || checkAnyPermissionOf(mContext, pid, uid,
+                        android.Manifest.permission.NETWORK_FACTORY);
+    }
+
     @VisibleForTesting
     NetworkCapabilities networkCapabilitiesRestrictedForCallerPermissions(
             NetworkCapabilities nc, int callerPid, int callerUid) {
@@ -2653,8 +2730,7 @@
                 NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK)) {
             newNc.setAdministratorUids(new int[0]);
         }
-        if (!checkAnyPermissionOf(mContext,
-                callerPid, callerUid, android.Manifest.permission.NETWORK_FACTORY)) {
+        if (!canSeeAllowedUids(callerPid, callerUid, newNc.getOwnerUid())) {
             newNc.setAllowedUids(new ArraySet<>());
             newNc.setSubscriptionIds(Collections.emptySet());
         }
@@ -2964,26 +3040,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.
@@ -2999,7 +3055,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 */);
@@ -3234,9 +3291,20 @@
     private void handleReportNetworkActivity(final NetworkActivityParams params) {
         mNetworkActivityTracker.handleReportNetworkActivity(params);
 
+        final boolean isCellNetworkActivity;
+        if (mTrackMultiNetworkActivities) {
+            final NetworkAgentInfo nai = getNetworkAgentInfoForNetId(params.label);
+            // nai could be null if netd receives a netlink message and calls the network
+            // activity change callback after the network is unregistered from ConnectivityService.
+            isCellNetworkActivity = nai != null
+                    && nai.networkCapabilities.hasTransport(TRANSPORT_CELLULAR);
+        } else {
+            isCellNetworkActivity = params.label == TRANSPORT_CELLULAR;
+        }
+
         if (mDelayDestroyFrozenSockets
                 && params.isActive
-                && params.label == TRANSPORT_CELLULAR
+                && isCellNetworkActivity
                 && !mPendingFrozenUids.isEmpty()) {
             closePendingFrozenSockets();
         }
@@ -3297,6 +3365,12 @@
     static final String DELAY_DESTROY_FROZEN_SOCKETS_VERSION =
             "delay_destroy_frozen_sockets_version";
 
+    @VisibleForTesting
+    public static final String ALLOW_SYSUI_CONNECTIVITY_REPORTS =
+            "allow_sysui_connectivity_reports";
+
+    public static final String LOG_BPF_RC = "log_bpf_rc_force_disable";
+
     private void enforceInternetPermission() {
         mContext.enforceCallingOrSelfPermission(
                 android.Manifest.permission.INTERNET,
@@ -3460,6 +3534,11 @@
                 NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK);
     }
 
+    private boolean checkSystemBarServicePermission(int pid, int uid) {
+        return checkAnyPermissionOf(mContext, pid, uid,
+                android.Manifest.permission.STATUS_BAR_SERVICE);
+    }
+
     private boolean checkNetworkSignalStrengthWakeupPermission(int pid, int uid) {
         return checkAnyPermissionOf(mContext, pid, uid,
                 android.Manifest.permission.NETWORK_SIGNAL_STRENGTH_WAKEUP,
@@ -3907,6 +3986,11 @@
         pw.println();
         dumpBpfProgramStatus(pw);
 
+        if (null != mCarrierPrivilegeAuthenticator) {
+            pw.println();
+            mCarrierPrivilegeAuthenticator.dump(pw);
+        }
+
         pw.println();
 
         if (!CollectionUtils.contains(args, SHORT_ARG)) {
@@ -3967,6 +4051,10 @@
         pw.increaseIndent();
         mNetworkActivityTracker.dump(pw);
         pw.decreaseIndent();
+
+        pw.println();
+        pw.println("Multicast routing supported: " +
+                (mMulticastRoutingCoordinatorService != null));
     }
 
     private void dumpNetworks(IndentingPrintWriter pw) {
@@ -4736,7 +4824,7 @@
         // If the Private DNS mode is opportunistic, reprogram the DNS servers
         // in order to restart a validation pass from within netd.
         final PrivateDnsConfig cfg = mDnsManager.getPrivateDnsConfig();
-        if (cfg.useTls && TextUtils.isEmpty(cfg.hostname)) {
+        if (cfg.inOpportunisticMode()) {
             updateDnses(nai.linkProperties, null, nai.network.getNetId());
         }
     }
@@ -4944,6 +5032,11 @@
         if (wasDefault) {
             mDefaultInetConditionPublished = 0;
         }
+        if (mTrackMultiNetworkActivities) {
+            // If trackMultiNetworkActivities is disabled, ActivityTracker removes idleTimer when
+            // the network becomes no longer the default network.
+            mNetworkActivityTracker.removeDataActivityTracking(nai);
+        }
         notifyIfacesChangedForNetworkStats();
         // If this was a local network forwarded to some upstream, or if some local network was
         // forwarded to this nai, then disable forwarding rules now.
@@ -4997,12 +5090,7 @@
                 }
 
                 if (mDefaultRequest == nri) {
-                    // TODO : make battery stats aware that since 2013 multiple interfaces may be
-                    //  active at the same time. For now keep calling this with the default
-                    //  network, because while incorrect this is the closest to the old (also
-                    //  incorrect) behavior.
-                    mNetworkActivityTracker.updateDataActivityTracking(
-                            null /* newNetwork */, nai);
+                    mNetworkActivityTracker.updateDefaultNetwork(null /* newNetwork */, nai);
                     maybeClosePendingFrozenSockets(null /* newNetwork */, nai);
                     ensureNetworkTransitionWakelock(nai.toShortString());
                 }
@@ -5108,9 +5196,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 "
@@ -5215,7 +5306,7 @@
     private boolean hasCarrierPrivilegeForNetworkCaps(final int callingUid,
             @NonNull final NetworkCapabilities caps) {
         if (mCarrierPrivilegeAuthenticator != null) {
-            return mCarrierPrivilegeAuthenticator.hasCarrierPrivilegeForNetworkCapabilities(
+            return mCarrierPrivilegeAuthenticator.isCarrierServiceUidForNetworkCapabilities(
                     callingUid, caps);
         }
         return false;
@@ -5257,6 +5348,8 @@
                             updateSignalStrengthThresholds(network, "REGISTER", req);
                         }
                     }
+                } else if (req.isRequest() && mNetworkRequestStateStatsMetrics != null) {
+                    mNetworkRequestStateStatsMetrics.onNetworkRequestReceived(req);
                 }
             }
 
@@ -5474,6 +5567,8 @@
             }
             if (req.isListen()) {
                 removeListenRequestFromNetworks(req);
+            } else if (req.isRequest() && mNetworkRequestStateStatsMetrics != null) {
+                mNetworkRequestStateStatsMetrics.onNetworkRequestRemoved(req);
             }
         }
         nri.unlinkDeathRecipient();
@@ -6996,6 +7091,32 @@
         }
     };
 
+    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+    private final BroadcastReceiver mDataSaverReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            if (mDeps.isAtLeastV()) {
+                throw new IllegalStateException(
+                        "data saver status should be updated from platform");
+            }
+            ensureRunningOnConnectivityServiceThread();
+            switch (intent.getAction()) {
+                case ACTION_RESTRICT_BACKGROUND_CHANGED:
+                    // If the uid is present in the deny list, the API will consistently
+                    // return ENABLED. To retrieve the global switch status, the system
+                    // uid is chosen because it will never be included in the deny list.
+                    final int dataSaverForSystemUid =
+                            mPolicyManager.getRestrictBackgroundStatus(Process.SYSTEM_UID);
+                    final boolean isDataSaverEnabled = (dataSaverForSystemUid
+                            != ConnectivityManager.RESTRICT_BACKGROUND_STATUS_DISABLED);
+                    mBpfNetMaps.setDataSaverEnabled(isDataSaverEnabled);
+                    break;
+                default:
+                    Log.wtf(TAG, "received unexpected intent: " + intent.getAction());
+            }
+        }
+    };
+
     private final HashMap<Messenger, NetworkProviderInfo> mNetworkProviderInfos = new HashMap<>();
     private final HashMap<NetworkRequest, NetworkRequestInfo> mNetworkRequests = new HashMap<>();
 
@@ -7599,7 +7720,7 @@
     }
 
     private void enforceRequestCapabilitiesDeclaration(@NonNull final String callerPackageName,
-            @NonNull final NetworkCapabilities networkCapabilities) {
+            @NonNull final NetworkCapabilities networkCapabilities, int callingUid) {
         // This check is added to fix the linter error for "current min is 30", which is not going
         // to happen because Connectivity service always run in S+.
         if (!mDeps.isAtLeastS()) {
@@ -7613,7 +7734,9 @@
                 applicationNetworkCapabilities = mSelfCertifiedCapabilityCache.get(
                         callerPackageName);
                 if (applicationNetworkCapabilities == null) {
-                    final PackageManager packageManager = mContext.getPackageManager();
+                    final PackageManager packageManager =
+                            mContext.createContextAsUser(UserHandle.getUserHandleForUid(
+                                    callingUid), 0 /* flags */).getPackageManager();
                     final PackageManager.Property networkSliceProperty = packageManager.getProperty(
                             ConstantsShim.PROPERTY_SELF_CERTIFIED_NETWORK_CAPABILITIES,
                             callerPackageName
@@ -7645,7 +7768,8 @@
             String callingPackageName, String callingAttributionTag, final int callingUid) {
         if (shouldCheckCapabilitiesDeclaration(networkCapabilities, callingUid,
                 callingPackageName)) {
-            enforceRequestCapabilitiesDeclaration(callingPackageName, networkCapabilities);
+            enforceRequestCapabilitiesDeclaration(callingPackageName, networkCapabilities,
+                    callingUid);
         }
         if (networkCapabilities.hasCapability(NET_CAPABILITY_NOT_RESTRICTED) == false) {
             // For T+ devices, callers with carrier privilege could request with CBS capabilities.
@@ -8997,6 +9121,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,
@@ -9010,7 +9199,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(
@@ -9069,6 +9257,7 @@
             configBuilder.setUpstreamSelector(oldRequest);
             nai.localNetworkConfig = configBuilder.build();
         }
+        maybeApplyMulticastRoutingConfig(nai, oldConfig, newConfig);
     }
 
     /**
@@ -9623,7 +9812,7 @@
         if (oldDefaultNetwork != null) {
             mLingerMonitor.noteLingerDefaultNetwork(oldDefaultNetwork, newDefaultNetwork);
         }
-        mNetworkActivityTracker.updateDataActivityTracking(newDefaultNetwork, oldDefaultNetwork);
+        mNetworkActivityTracker.updateDefaultNetwork(newDefaultNetwork, oldDefaultNetwork);
         maybeClosePendingFrozenSockets(newDefaultNetwork, oldDefaultNetwork);
         mProxyTracker.setDefaultProxy(null != newDefaultNetwork
                 ? newDefaultNetwork.linkProperties.getHttpProxy() : null);
@@ -10046,6 +10235,50 @@
         // Process default network changes if applicable.
         processDefaultNetworkChanges(changes);
 
+        // Update forwarding rules for the upstreams of local networks. Do this before sending
+        // onAvailable so that by the time onAvailable is sent the forwarding rules are set up.
+        // Don't send CALLBACK_LOCAL_NETWORK_INFO_CHANGED yet though : they should be sent after
+        // onAvailable so clients know what network the change is about. Store such changes in
+        // an array that's only allocated if necessary (because it's almost never necessary).
+        ArrayList<NetworkAgentInfo> localInfoChangedAgents = null;
+        for (final NetworkAgentInfo nai : mNetworkAgentInfos) {
+            if (!nai.isLocalNetwork()) continue;
+            final NetworkRequest nr = nai.localNetworkConfig.getUpstreamSelector();
+            if (null == nr) continue; // No upstream for this local network
+            final NetworkRequestInfo nri = mNetworkRequests.get(nr);
+            final NetworkReassignment.RequestReassignment change = changes.getReassignment(nri);
+            if (null == change) continue; // No change in upstreams for this network
+            final String fromIface = nai.linkProperties.getInterfaceName();
+            if (!hasSameInterfaceName(change.mOldNetwork, change.mNewNetwork)
+                    || change.mOldNetwork.isDestroyed()) {
+                // There can be a change with the same interface name if the new network is the
+                // replacement for the old network that was unregisteredAfterReplacement.
+                try {
+                    if (null != change.mOldNetwork) {
+                        mRoutingCoordinatorService.removeInterfaceForward(fromIface,
+                                change.mOldNetwork.linkProperties.getInterfaceName());
+                        disableMulticastRouting(fromIface,
+                                change.mOldNetwork.linkProperties.getInterfaceName());
+                    }
+                    // If the new upstream is already destroyed, there is no point in setting up
+                    // a forward (in fact, it might forward to the interface for some new network !)
+                    // Later when the upstream disconnects CS will try to remove the forward, which
+                    // is ignored with a benign log by RoutingCoordinatorService.
+                    if (null != change.mNewNetwork && !change.mNewNetwork.isDestroyed()) {
+                        mRoutingCoordinatorService.addInterfaceForward(fromIface,
+                                change.mNewNetwork.linkProperties.getInterfaceName());
+                        applyMulticastRoutingConfig(fromIface,
+                                change.mNewNetwork.linkProperties.getInterfaceName(),
+                                nai.localNetworkConfig);
+                    }
+                } catch (final RemoteException e) {
+                    loge("Can't update forwarding rules", e);
+                }
+            }
+            if (null == localInfoChangedAgents) localInfoChangedAgents = new ArrayList<>();
+            localInfoChangedAgents.add(nai);
+        }
+
         // Notify requested networks are available after the default net is switched, but
         // before LegacyTypeTracker sends legacy broadcasts
         for (final NetworkReassignment.RequestReassignment event :
@@ -10094,38 +10327,12 @@
             notifyNetworkLosing(nai, now);
         }
 
-        // Update forwarding rules for the upstreams of local networks. Do this after sending
-        // onAvailable so that clients understand what network this is about.
-        for (final NetworkAgentInfo nai : mNetworkAgentInfos) {
-            if (!nai.isLocalNetwork()) continue;
-            final NetworkRequest nr = nai.localNetworkConfig.getUpstreamSelector();
-            if (null == nr) continue; // No upstream for this local network
-            final NetworkRequestInfo nri = mNetworkRequests.get(nr);
-            final NetworkReassignment.RequestReassignment change = changes.getReassignment(nri);
-            if (null == change) continue; // No change in upstreams for this network
-            final String fromIface = nai.linkProperties.getInterfaceName();
-            if (!hasSameInterfaceName(change.mOldNetwork, change.mNewNetwork)
-                    || change.mOldNetwork.isDestroyed()) {
-                // There can be a change with the same interface name if the new network is the
-                // replacement for the old network that was unregisteredAfterReplacement.
-                try {
-                    if (null != change.mOldNetwork) {
-                        mRoutingCoordinatorService.removeInterfaceForward(fromIface,
-                                change.mOldNetwork.linkProperties.getInterfaceName());
-                    }
-                    // If the new upstream is already destroyed, there is no point in setting up
-                    // a forward (in fact, it might forward to the interface for some new network !)
-                    // Later when the upstream disconnects CS will try to remove the forward, which
-                    // is ignored with a benign log by RoutingCoordinatorService.
-                    if (null != change.mNewNetwork && !change.mNewNetwork.isDestroyed()) {
-                        mRoutingCoordinatorService.addInterfaceForward(fromIface,
-                                change.mNewNetwork.linkProperties.getInterfaceName());
-                    }
-                } catch (final RemoteException e) {
-                    loge("Can't update forwarding rules", e);
-                }
+        // Send LOCAL_NETWORK_INFO_CHANGED callbacks now that onAvailable and onLost have been sent.
+        if (null != localInfoChangedAgents) {
+            for (final NetworkAgentInfo nai : localInfoChangedAgents) {
+                notifyNetworkCallbacks(nai,
+                        ConnectivityManager.CALLBACK_LOCAL_NETWORK_INFO_CHANGED);
             }
-            notifyNetworkCallbacks(nai, ConnectivityManager.CALLBACK_LOCAL_NETWORK_INFO_CHANGED);
         }
 
         updateLegacyTypeTrackerAndVpnLockdownForRematch(changes, nais);
@@ -10523,6 +10730,15 @@
                     SystemClock.elapsedRealtime(), mNascentDelayMs);
             networkAgent.setInactive();
 
+            if (mTrackMultiNetworkActivities) {
+                // Start tracking activity of this network.
+                // This must be called before rematchAllNetworksAndRequests since the network
+                // should be tracked when the network becomes the default network.
+                // This method does not trigger any callbacks or broadcasts. Callbacks or broadcasts
+                // can be triggered later if this network becomes the default network.
+                mNetworkActivityTracker.setupDataActivityTracking(networkAgent);
+            }
+
             // Consider network even though it is not yet validated.
             rematchAllNetworksAndRequests();
 
@@ -11581,6 +11797,10 @@
         if (checkNetworkStackPermission(callbackPid, callbackUid)) {
             return true;
         }
+        if (mAllowSysUiConnectivityReports
+                && checkSystemBarServicePermission(callbackPid, callbackUid)) {
+            return true;
+        }
 
         // Administrator UIDs also contains the Owner UID
         final int[] administratorUids = nai.networkCapabilities.getAdministratorUids();
@@ -11710,8 +11930,8 @@
      */
     private static final class NetworkActivityParams {
         public final boolean isActive;
-        // Label used for idle timer. Transport type is used as label.
-        // label is int since NMS was using the identifier as int, and it has not been changed
+        // If TrackMultiNetworkActivities is enabled, idleTimer label is netid.
+        // If TrackMultiNetworkActivities is disabled, idleTimer label is transport type.
         public final int label;
         public final long timestampNs;
         // Uid represents the uid that was responsible for waking the radio.
@@ -11753,13 +11973,15 @@
         }
     }
 
+    private final boolean mTrackMultiNetworkActivities;
     private final LegacyNetworkActivityTracker mNetworkActivityTracker;
 
     /**
      * Class used for updating network activity tracking with netd and notify network activity
      * changes.
      */
-    private static final class LegacyNetworkActivityTracker {
+    @VisibleForTesting
+    public static final class LegacyNetworkActivityTracker {
         private static final int NO_UID = -1;
         private final Context mContext;
         private final INetd mNetd;
@@ -11771,8 +11993,14 @@
         // If there is no default network, default network is considered active to keep the existing
         // behavior. Initial value is used until first connect to the default network.
         private volatile boolean mIsDefaultNetworkActive = true;
+        private Network mDefaultNetwork;
         // Key is netId. Value is configured idle timer information.
         private final SparseArray<IdleTimerParams> mActiveIdleTimers = new SparseArray<>();
+        private final boolean mTrackMultiNetworkActivities;
+        // Store netIds of Wi-Fi networks whose idletimers report that they are active
+        private final Set<Integer> mActiveWifiNetworks = new ArraySet<>();
+        // Store netIds of cellular networks whose idletimers report that they are active
+        private final Set<Integer> mActiveCellularNetworks = new ArraySet<>();
 
         private static class IdleTimerParams {
             public final int timeout;
@@ -11785,10 +12013,11 @@
         }
 
         LegacyNetworkActivityTracker(@NonNull Context context, @NonNull INetd netd,
-                @NonNull Handler handler) {
+                @NonNull Handler handler, boolean trackMultiNetworkActivities) {
             mContext = context;
             mNetd = netd;
             mHandler = handler;
+            mTrackMultiNetworkActivities = trackMultiNetworkActivities;
         }
 
         private void ensureRunningOnConnectivityServiceThread() {
@@ -11798,19 +12027,97 @@
             }
         }
 
-        public void handleReportNetworkActivity(NetworkActivityParams activityParams) {
-            ensureRunningOnConnectivityServiceThread();
+        /**
+         * Update network activity and call BatteryStats to update radio power state if the
+         * mobile or Wi-Fi activity is changed.
+         * LegacyNetworkActivityTracker considers the mobile network is active if at least one
+         * mobile network is active since BatteryStatsService only maintains a single power state
+         * for the mobile network.
+         * The Wi-Fi network is also the same.
+         *
+         * {@link #setupDataActivityTracking} and {@link #removeDataActivityTracking} use
+         * TRANSPORT_CELLULAR as the transportType argument if the network has both cell and Wi-Fi
+         * transports.
+         */
+        private void maybeUpdateRadioPowerState(final int netId, final int transportType,
+                final boolean isActive, final int uid) {
+            if (transportType != TRANSPORT_WIFI && transportType != TRANSPORT_CELLULAR) {
+                Log.e(TAG, "Unexpected transportType in maybeUpdateRadioPowerState: "
+                        + transportType);
+                return;
+            }
+            final Set<Integer> activeNetworks = transportType == TRANSPORT_WIFI
+                    ? mActiveWifiNetworks : mActiveCellularNetworks;
+
+            final boolean wasEmpty = activeNetworks.isEmpty();
+            if (isActive) {
+                activeNetworks.add(netId);
+            } else {
+                activeNetworks.remove(netId);
+            }
+
+            if (wasEmpty != activeNetworks.isEmpty()) {
+                updateRadioPowerState(isActive, transportType, uid);
+            }
+        }
+
+        private void handleDefaultNetworkActivity(final int transportType,
+                final boolean isActive, final long timestampNs) {
+            mIsDefaultNetworkActive = isActive;
+            sendDataActivityBroadcast(transportTypeToLegacyType(transportType),
+                    isActive, timestampNs);
+            if (isActive) {
+                reportNetworkActive();
+            }
+        }
+
+        private void handleReportNetworkActivityWithNetIdLabel(
+                NetworkActivityParams activityParams) {
+            final int netId = activityParams.label;
+            final IdleTimerParams idleTimerParams = mActiveIdleTimers.get(netId);
+            if (idleTimerParams == null) {
+                // This network activity change is not tracked anymore
+                // This can happen if netd callback post activity change event message but idle
+                // timer is removed before processing this message.
+                return;
+            }
+            // TODO: if a network changes transports, storing the transport type in the
+            // IdleTimerParams is not correct. Consider getting it from the network's
+            // NetworkCapabilities instead.
+            final int transportType = idleTimerParams.transportType;
+            maybeUpdateRadioPowerState(netId, transportType,
+                    activityParams.isActive, activityParams.uid);
+
+            if (mDefaultNetwork == null || mDefaultNetwork.netId != netId) {
+                // This activity change is not for the default network.
+                return;
+            }
+
+            handleDefaultNetworkActivity(transportType, activityParams.isActive,
+                    activityParams.timestampNs);
+        }
+
+        private void handleReportNetworkActivityWithTransportTypeLabel(
+                NetworkActivityParams activityParams) {
             if (mActiveIdleTimers.size() == 0) {
                 // This activity change is not for the current default network.
                 // This can happen if netd callback post activity change event message but
                 // the default network is lost before processing this message.
                 return;
             }
-            sendDataActivityBroadcast(transportTypeToLegacyType(activityParams.label),
-                    activityParams.isActive, activityParams.timestampNs);
-            mIsDefaultNetworkActive = activityParams.isActive;
-            if (mIsDefaultNetworkActive) {
-                reportNetworkActive();
+            handleDefaultNetworkActivity(activityParams.label, activityParams.isActive,
+                    activityParams.timestampNs);
+        }
+
+        /**
+         * Handle network activity change
+         */
+        public void handleReportNetworkActivity(NetworkActivityParams activityParams) {
+            ensureRunningOnConnectivityServiceThread();
+            if (mTrackMultiNetworkActivities) {
+                handleReportNetworkActivityWithNetIdLabel(activityParams);
+            } else {
+                handleReportNetworkActivityWithTransportTypeLabel(activityParams);
             }
         }
 
@@ -11867,6 +12174,30 @@
         }
 
         /**
+         * Get idle timer label
+         */
+        @VisibleForTesting
+        public static int getIdleTimerLabel(final boolean trackMultiNetworkActivities,
+                final int netId, final int transportType) {
+            return trackMultiNetworkActivities ? netId : transportType;
+        }
+
+        private boolean maybeCreateIdleTimer(
+                String iface, int netId, int timeout, int transportType) {
+            if (timeout <= 0 || iface == null) return false;
+            try {
+                final String label = Integer.toString(getIdleTimerLabel(
+                        mTrackMultiNetworkActivities, netId, transportType));
+                mNetd.idletimerAddInterface(iface, timeout, label);
+                mActiveIdleTimers.put(netId, new IdleTimerParams(timeout, transportType));
+                return true;
+            } catch (Exception e) {
+                loge("Exception in createIdleTimer", e);
+                return false;
+            }
+        }
+
+        /**
          * Setup data activity tracking for the given network.
          *
          * Every {@code setupDataActivityTracking} should be paired with a
@@ -11875,13 +12206,17 @@
          * @return true if the idleTimer is added to the network, false otherwise
          */
         private boolean setupDataActivityTracking(NetworkAgentInfo networkAgent) {
+            ensureRunningOnConnectivityServiceThread();
             final String iface = networkAgent.linkProperties.getInterfaceName();
             final int netId = networkAgent.network().netId;
 
             final int timeout;
             final int type;
 
-            if (networkAgent.networkCapabilities.hasTransport(
+            if (!networkAgent.networkCapabilities.hasCapability(NET_CAPABILITY_NOT_VPN)) {
+                // Do not track VPN network.
+                return false;
+            } else if (networkAgent.networkCapabilities.hasTransport(
                     NetworkCapabilities.TRANSPORT_CELLULAR)) {
                 timeout = Settings.Global.getInt(mContext.getContentResolver(),
                         ConnectivitySettingsManager.DATA_ACTIVITY_TIMEOUT_MOBILE,
@@ -11897,25 +12232,21 @@
                 return false; // do not track any other networks
             }
 
-            updateRadioPowerState(true /* isActive */, type);
-
-            if (timeout > 0 && iface != null) {
-                try {
-                    mActiveIdleTimers.put(netId, new IdleTimerParams(timeout, type));
-                    mNetd.idletimerAddInterface(iface, timeout, Integer.toString(type));
-                    return true;
-                } catch (Exception e) {
-                    // You shall not crash!
-                    loge("Exception in setupDataActivityTracking " + e);
-                }
+            final boolean hasIdleTimer = maybeCreateIdleTimer(iface, netId, timeout, type);
+            if (hasIdleTimer || !mTrackMultiNetworkActivities) {
+                // If trackMultiNetwork is disabled, NetworkActivityTracker updates radio power
+                // state in all cases. If trackMultiNetwork is enabled, it updates radio power
+                // state only about a network that has an idletimer.
+                maybeUpdateRadioPowerState(netId, type, true /* isActive */, NO_UID);
             }
-            return false;
+            return hasIdleTimer;
         }
 
         /**
          * Remove data activity tracking when network disconnects.
          */
-        private void removeDataActivityTracking(NetworkAgentInfo networkAgent) {
+        public void removeDataActivityTracking(NetworkAgentInfo networkAgent) {
+            ensureRunningOnConnectivityServiceThread();
             final String iface = networkAgent.linkProperties.getInterfaceName();
             final int netId = networkAgent.network().netId;
             final NetworkCapabilities caps = networkAgent.networkCapabilities;
@@ -11923,7 +12254,10 @@
             if (iface == null) return;
 
             final int type;
-            if (caps.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) {
+            if (!networkAgent.networkCapabilities.hasCapability(NET_CAPABILITY_NOT_VPN)) {
+                // Do not track VPN network.
+                return;
+            } else if (caps.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) {
                 type = NetworkCapabilities.TRANSPORT_CELLULAR;
             } else if (caps.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) {
                 type = NetworkCapabilities.TRANSPORT_WIFI;
@@ -11932,16 +12266,17 @@
             }
 
             try {
-                updateRadioPowerState(false /* isActive */, type);
+                maybeUpdateRadioPowerState(netId, type, false /* isActive */, NO_UID);
                 final IdleTimerParams params = mActiveIdleTimers.get(netId);
                 if (params == null) {
                     // IdleTimer is not added if the configured timeout is 0 or negative value
                     return;
                 }
                 mActiveIdleTimers.remove(netId);
-                // The call fails silently if no idle timer setup for this interface
-                mNetd.idletimerRemoveInterface(iface, params.timeout,
-                        Integer.toString(params.transportType));
+                final String label = Integer.toString(getIdleTimerLabel(
+                        mTrackMultiNetworkActivities, netId, params.transportType));
+                        // The call fails silently if no idle timer setup for this interface
+                mNetd.idletimerRemoveInterface(iface, params.timeout, label);
             } catch (Exception e) {
                 // You shall not crash!
                 loge("Exception in removeDataActivityTracking " + e);
@@ -11951,12 +12286,15 @@
         private void updateDefaultNetworkActivity(NetworkAgentInfo defaultNetwork,
                 boolean hasIdleTimer) {
             if (defaultNetwork != null) {
+                mDefaultNetwork = defaultNetwork.network();
                 mIsDefaultNetworkActive = true;
-                // Callbacks are called only when the network has the idle timer.
-                if (hasIdleTimer) {
+                // If only the default network is tracked, callbacks are called only when the
+                // network has the idle timer.
+                if (mTrackMultiNetworkActivities || hasIdleTimer) {
                     reportNetworkActive();
                 }
             } else {
+                mDefaultNetwork = null;
                 // If there is no default network, default network is considered active to keep the
                 // existing behavior.
                 mIsDefaultNetworkActive = true;
@@ -11964,29 +12302,34 @@
         }
 
         /**
-         * Update data activity tracking when network state is updated.
+         * Update the default network this class tracks the activity of.
          */
-        public void updateDataActivityTracking(NetworkAgentInfo newNetwork,
+        public void updateDefaultNetwork(NetworkAgentInfo newNetwork,
                 NetworkAgentInfo oldNetwork) {
             ensureRunningOnConnectivityServiceThread();
+            // If TrackMultiNetworkActivities is enabled, devices add idleTimer when the network is
+            // first connected and remove when the network is disconnected.
+            // If TrackMultiNetworkActivities is disabled, devices add idleTimer when the network
+            // becomes the default network and remove when the network becomes no longer the default
+            // network.
             boolean hasIdleTimer = false;
-            if (newNetwork != null) {
+            if (!mTrackMultiNetworkActivities && newNetwork != null) {
                 hasIdleTimer = setupDataActivityTracking(newNetwork);
             }
             updateDefaultNetworkActivity(newNetwork, hasIdleTimer);
-            if (oldNetwork != null) {
+            if (!mTrackMultiNetworkActivities && oldNetwork != null) {
                 removeDataActivityTracking(oldNetwork);
             }
         }
 
-        private void updateRadioPowerState(boolean isActive, int transportType) {
+        private void updateRadioPowerState(boolean isActive, int transportType, int uid) {
             final BatteryStatsManager bs = mContext.getSystemService(BatteryStatsManager.class);
             switch (transportType) {
                 case NetworkCapabilities.TRANSPORT_CELLULAR:
-                    bs.reportMobileRadioPowerState(isActive, NO_UID);
+                    bs.reportMobileRadioPowerState(isActive, uid);
                     break;
                 case NetworkCapabilities.TRANSPORT_WIFI:
-                    bs.reportWifiRadioPowerState(isActive, NO_UID);
+                    bs.reportWifiRadioPowerState(isActive, uid);
                     break;
                 default:
                     logw("Untracked transport type:" + transportType);
@@ -12006,7 +12349,9 @@
         }
 
         public void dump(IndentingPrintWriter pw) {
+            pw.print("mTrackMultiNetworkActivities="); pw.println(mTrackMultiNetworkActivities);
             pw.print("mIsDefaultNetworkActive="); pw.println(mIsDefaultNetworkActive);
+            pw.print("mDefaultNetwork="); pw.println(mDefaultNetwork);
             pw.println("Idle timers:");
             try {
                 for (int i = 0; i < mActiveIdleTimers.size(); i++) {
@@ -12015,11 +12360,13 @@
                     pw.print("    timeout="); pw.print(params.timeout);
                     pw.print(" type="); pw.println(params.transportType);
                 }
+                pw.println("WiFi active networks: " + mActiveWifiNetworks);
+                pw.println("Cellular active networks: " + mActiveCellularNetworks);
             } catch (Exception e) {
-                // mActiveIdleTimers should only be accessed from handler thread, except dump().
-                // As dump() is never called in normal usage, it would be needlessly expensive
-                // to lock the collection only for its benefit.
-                // Also, mActiveIdleTimers is not expected to be updated frequently.
+                // mActiveIdleTimers, mActiveWifiNetworks, and mActiveCellularNetworks should only
+                // be accessed from handler thread, except dump(). As dump() is never called in
+                // normal usage, it would be needlessly expensive to lock the collection only for
+                // its benefit. Also, they are not expected to be updated frequently.
                 // So catching the exception and logging.
                 pw.println("Failed to dump NetworkActivityTracker: " + e);
             }
@@ -12777,7 +13124,7 @@
         }
     }
 
-    @TargetApi(Build.VERSION_CODES.TIRAMISU)
+    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
     @Override
     public void setDataSaverEnabled(final boolean enable) {
         enforceNetworkStackOrSettingsPermission();
diff --git a/service/src/com/android/server/NetIdManager.java b/service/src/com/android/server/NetIdManager.java
index 61925c8..27b6b9b 100644
--- a/service/src/com/android/server/NetIdManager.java
+++ b/service/src/com/android/server/NetIdManager.java
@@ -27,6 +27,16 @@
  * Class used to reserve and release net IDs.
  *
  * <p>Instances of this class are thread-safe.
+ *
+ * NetIds are currently 16 bits long and consume 16 bits in the fwmark.
+ * The reason they are large is that applications might get confused if the netId counter
+ * wraps - for example, Network#equals would return true for a current network
+ * and a long-disconnected network.
+ * We could in theory fix this by splitting the identifier in two, e.g., a 24-bit generation
+ * counter and an 8-bit netId. Java Network objects would be constructed from the full 32-bit
+ * number, but only the 8-bit number would be used by netd and the fwmark.
+ * We'd have to fix all code that assumes that it can take a netId or a mark and construct
+ * a Network object from it.
  */
 public class NetIdManager {
     // Sequence number for Networks; keep in sync with system/netd/NetworkController.cpp
diff --git a/service/src/com/android/server/connectivity/AutomaticOnOffKeepaliveTracker.java b/service/src/com/android/server/connectivity/AutomaticOnOffKeepaliveTracker.java
index bba132f..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;
@@ -88,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;
@@ -771,7 +769,8 @@
     }
 
     private boolean isAnyTcpSocketConnectedForFamily(FileDescriptor fd, int family, int networkMark,
-            int networkMask) throws ErrnoException, InterruptedIOException {
+            int networkMask)
+            throws ErrnoException, InterruptedIOException {
         ensureRunningOnHandlerThread();
         // Build SocketDiag messages and cache it.
         if (mSockDiagMsg.get(family) == null) {
@@ -789,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,
@@ -829,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 ab7b1a7..5705ebe 100644
--- a/service/src/com/android/server/connectivity/CarrierPrivilegeAuthenticator.java
+++ b/service/src/com/android/server/connectivity/CarrierPrivilegeAuthenticator.java
@@ -17,6 +17,7 @@
 package com.android.server.connectivity;
 
 import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
+import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
 
 import static com.android.server.connectivity.ConnectivityFlags.CARRIER_SERVICE_CHANGED_USE_CALLBACK;
 
@@ -31,6 +32,8 @@
 import android.net.NetworkCapabilities;
 import android.net.NetworkSpecifier;
 import android.net.TelephonyNetworkSpecifier;
+import android.net.TransportInfo;
+import android.net.wifi.WifiInfo;
 import android.os.Handler;
 import android.os.HandlerThread;
 import android.os.Process;
@@ -41,12 +44,13 @@
 
 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.net.module.util.DeviceConfigUtils;
 import com.android.networkstack.apishim.TelephonyManagerShimImpl;
 import com.android.networkstack.apishim.common.TelephonyManagerShim;
 import com.android.networkstack.apishim.common.TelephonyManagerShim.CarrierPrivilegesListenerShim;
 import com.android.networkstack.apishim.common.UnsupportedApiLevelException;
-import com.android.server.ConnectivityService;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -77,13 +81,13 @@
     private final boolean mUseCallbacksForServiceChanged;
 
     public CarrierPrivilegeAuthenticator(@NonNull final Context c,
-            @NonNull final ConnectivityService.Dependencies deps,
+            @NonNull final Dependencies deps,
             @NonNull final TelephonyManager t,
             @NonNull final TelephonyManagerShim telephonyManagerShim) {
         mContext = c;
         mTelephonyManager = t;
         mTelephonyManagerShim = telephonyManagerShim;
-        final HandlerThread thread = new HandlerThread(TAG);
+        final HandlerThread thread = deps.makeHandlerThread();
         thread.start();
         mHandler = new Handler(thread.getLooper());
         mUseCallbacksForServiceChanged = deps.isFeatureEnabled(
@@ -109,9 +113,24 @@
     }
 
     public CarrierPrivilegeAuthenticator(@NonNull final Context c,
-            @NonNull final ConnectivityService.Dependencies deps,
             @NonNull final TelephonyManager t) {
-        this(c, deps, t, TelephonyManagerShimImpl.newInstance(t));
+        this(c, new Dependencies(), t, TelephonyManagerShimImpl.newInstance(t));
+    }
+
+    public static class Dependencies {
+        /**
+         * Create a HandlerThread to use in CarrierPrivilegeAuthenticator.
+         */
+        public HandlerThread makeHandlerThread() {
+            return new HandlerThread(TAG);
+        }
+
+        /**
+         * @see DeviceConfigUtils#isTetheringFeatureEnabled
+         */
+        public boolean isFeatureEnabled(Context context, String name) {
+            return DeviceConfigUtils.isTetheringFeatureEnabled(context, name);
+        }
     }
 
     private void simConfigChanged() {
@@ -125,11 +144,13 @@
 
     private class PrivilegeListener implements CarrierPrivilegesListenerShim {
         public final int mLogicalSlot;
+
         PrivilegeListener(final int logicalSlot) {
             mLogicalSlot = logicalSlot;
         }
 
-        @Override public void onCarrierPrivilegesChanged(
+        @Override
+        public void onCarrierPrivilegesChanged(
                 @NonNull List<String> privilegedPackageNames,
                 @NonNull int[] privilegedUids) {
             if (mUseCallbacksForServiceChanged) return;
@@ -193,12 +214,13 @@
      *
      * This returns whether the passed UID is the carrier service package for the subscription ID
      * stored in the telephony network specifier in the passed network capabilities.
-     * If the capabilities don't code for a cellular network, or if they don't have the
+     * If the capabilities don't code for a cellular or Wi-Fi network, or if they don't have the
      * subscription ID in their specifier, this returns false.
      *
-     * This method can be used to check that a network request for {@link NET_CAPABILITY_CBS} is
-     * allowed for the UID of a caller, which must hold carrier privilege and provide the carrier
-     * config.
+     * This method can be used to check that a network request that requires the UID to be
+     * the carrier service UID is indeed called by such a UID. An example of such a network could
+     * be a network with the  {@link android.net.NetworkCapabilities#NET_CAPABILITY_CBS}
+     * capability.
      * It can also be used to check that a factory is entitled to grant access to a given network
      * to a given UID on grounds that it is the carrier service package.
      *
@@ -206,11 +228,28 @@
      * @param networkCapabilities the network capabilities for which carrier privilege is checked.
      * @return true if uid provides the relevant carrier config else false.
      */
-    public boolean hasCarrierPrivilegeForNetworkCapabilities(int callingUid,
+    public boolean isCarrierServiceUidForNetworkCapabilities(int callingUid,
             @NonNull NetworkCapabilities networkCapabilities) {
         if (callingUid == Process.INVALID_UID) return false;
-        if (!networkCapabilities.hasSingleTransport(TRANSPORT_CELLULAR)) return false;
-        final int subId = getSubIdFromNetworkSpecifier(networkCapabilities.getNetworkSpecifier());
+        final int subId;
+        if (networkCapabilities.hasSingleTransportBesidesTest(TRANSPORT_CELLULAR)) {
+            subId = getSubIdFromTelephonySpecifier(networkCapabilities.getNetworkSpecifier());
+        } else if (networkCapabilities.hasSingleTransportBesidesTest(TRANSPORT_WIFI)) {
+            subId = getSubIdFromWifiTransportInfo(networkCapabilities.getTransportInfo());
+        } else {
+            subId = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
+        }
+        if (subId != SubscriptionManager.INVALID_SUBSCRIPTION_ID
+                && !networkCapabilities.getSubscriptionIds().contains(subId)) {
+            // Ideally, the code above should just use networkCapabilities.getSubscriptionIds()
+            // for simplicity and future-proofing. However, this is not the historical behavior,
+            // and there is no enforcement that they do not differ, so log a terrible failure if
+            // they do not match to gain confidence this never happens.
+            // TODO : when there is confidence that this never happens, rewrite the code above
+            // with NetworkCapabilities#getSubscriptionIds.
+            Log.wtf(TAG, "NetworkCapabilities subIds are inconsistent between "
+                    + "specifier/transportInfo and mSubIds : " + networkCapabilities);
+        }
         if (SubscriptionManager.INVALID_SUBSCRIPTION_ID == subId) return false;
         return callingUid == getCarrierServiceUidForSubId(subId);
     }
@@ -239,14 +278,6 @@
     }
 
     @VisibleForTesting
-    int getSubIdFromNetworkSpecifier(NetworkSpecifier specifier) {
-        if (specifier instanceof TelephonyNetworkSpecifier) {
-            return ((TelephonyNetworkSpecifier) specifier).getSubscriptionId();
-        }
-        return SubscriptionManager.INVALID_SUBSCRIPTION_ID;
-    }
-
-    @VisibleForTesting
     int getUidForPackage(String pkgName) {
         if (pkgName == null) {
             return Process.INVALID_UID;
@@ -271,8 +302,22 @@
         return getUidForPackage(getCarrierServicePackageNameForLogicalSlot(slotId));
     }
 
-    // Helper methods to avoid having to deal with UnsupportedApiLevelException.
+    @VisibleForTesting
+    int getSubIdFromTelephonySpecifier(@Nullable final NetworkSpecifier specifier) {
+        if (specifier instanceof TelephonyNetworkSpecifier) {
+            return ((TelephonyNetworkSpecifier) specifier).getSubscriptionId();
+        }
+        return SubscriptionManager.INVALID_SUBSCRIPTION_ID;
+    }
 
+    int getSubIdFromWifiTransportInfo(@Nullable final TransportInfo info) {
+        if (info instanceof WifiInfo) {
+            return ((WifiInfo) info).getSubscriptionId();
+        }
+        return SubscriptionManager.INVALID_SUBSCRIPTION_ID;
+    }
+
+    // Helper methods to avoid having to deal with UnsupportedApiLevelException.
     private void addCarrierPrivilegesListener(@NonNull final Executor executor,
             @NonNull final PrivilegeListener listener) {
         try {
@@ -292,4 +337,16 @@
             Log.e(TAG, "removeCarrierPrivilegesListener API is not available");
         }
     }
+
+    public void dump(IndentingPrintWriter pw) {
+        pw.println("CarrierPrivilegeAuthenticator:");
+        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);
+            }
+        }
+    }
 }
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 5aac8f1..f8f76ef 100644
--- a/service/src/com/android/server/connectivity/ConnectivityFlags.java
+++ b/service/src/com/android/server/connectivity/ConnectivityFlags.java
@@ -33,7 +33,6 @@
     public static final String NO_REMATCH_ALL_REQUESTS_ON_REGISTER =
             "no_rematch_all_requests_on_register";
 
-    @VisibleForTesting
     public static final String CARRIER_SERVICE_CHANGED_USE_CALLBACK =
             "carrier_service_changed_use_callback_version";
 
diff --git a/service/src/com/android/server/connectivity/DnsManager.java b/service/src/com/android/server/connectivity/DnsManager.java
index 894bcc4..8e6854a 100644
--- a/service/src/com/android/server/connectivity/DnsManager.java
+++ b/service/src/com/android/server/connectivity/DnsManager.java
@@ -302,7 +302,7 @@
         final PrivateDnsConfig privateDnsCfg = mPrivateDnsMap.getOrDefault(netId,
                 PRIVATE_DNS_OFF);
 
-        final boolean useTls = privateDnsCfg.useTls;
+        final boolean useTls = privateDnsCfg.mode != PRIVATE_DNS_MODE_OFF;
         final PrivateDnsValidationStatuses statuses =
                 useTls ? mPrivateDnsValidationMap.get(netId) : null;
         final boolean validated = (null != statuses) && statuses.hasValidatedServer();
@@ -370,7 +370,7 @@
         // networks like IMS.
         final PrivateDnsConfig privateDnsCfg = mPrivateDnsMap.getOrDefault(netId,
                 PRIVATE_DNS_OFF);
-        final boolean useTls = privateDnsCfg.useTls;
+        final boolean useTls = privateDnsCfg.mode != PRIVATE_DNS_MODE_OFF;
         final boolean strictMode = privateDnsCfg.inStrictMode();
 
         paramsParcel.netId = netId;
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/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 7cd3cc8..50cad45 100644
--- a/service/src/com/android/server/connectivity/NetworkAgentInfo.java
+++ b/service/src/com/android/server/connectivity/NetworkAgentInfo.java
@@ -17,11 +17,13 @@
 package com.android.server.connectivity;
 
 import static android.net.ConnectivityDiagnosticsManager.ConnectivityReport;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED;
 import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
 import static android.net.NetworkCapabilities.TRANSPORT_ETHERNET;
 import static android.net.NetworkCapabilities.TRANSPORT_TEST;
+import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
 import static android.net.NetworkCapabilities.transportNamesOf;
 
 import android.annotation.NonNull;
@@ -1549,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 static void restrictCapabilitiesFromNetworkAgent(@NonNull final NetworkCapabilities nc,
+    public void restrictCapabilitiesFromNetworkAgent(@NonNull final NetworkCapabilities nc,
             final int creatorUid, final boolean hasAutomotiveFeature,
             @NonNull final ConnectivityService.Dependencies deps,
             @Nullable final CarrierPrivilegeAuthenticator authenticator) {
@@ -1562,7 +1564,7 @@
         }
     }
 
-    private static boolean areAllowedUidsAcceptableFromNetworkAgent(
+    private boolean areAllowedUidsAcceptableFromNetworkAgent(
             @NonNull final NetworkCapabilities nc, final boolean hasAutomotiveFeature,
             @NonNull final ConnectivityService.Dependencies deps,
             @Nullable final CarrierPrivilegeAuthenticator carrierPrivilegeAuthenticator) {
@@ -1575,21 +1577,28 @@
         // On a non-restricted network, access UIDs make no sense
         if (nc.hasCapability(NET_CAPABILITY_NOT_RESTRICTED)) return false;
 
-        // If this network has TRANSPORT_TEST, then the caller can do whatever they want to
-        // access UIDs
-        if (nc.hasTransport(TRANSPORT_TEST)) return true;
+        // If this network has TRANSPORT_TEST and nothing else, then the caller can do whatever
+        // they want to access UIDs
+        if (nc.hasSingleTransport(TRANSPORT_TEST)) return true;
 
-        // Factories that make ethernet networks can allow UIDs for automotive devices.
-        if (nc.hasSingleTransport(TRANSPORT_ETHERNET) && hasAutomotiveFeature) {
-            return true;
+        if (nc.hasTransport(TRANSPORT_ETHERNET)) {
+            // Factories that make ethernet networks can allow UIDs for automotive devices.
+            if (hasAutomotiveFeature) return true;
+            // It's also admissible if the ethernet network has TRANSPORT_TEST, as long as it
+            // doesn't have NET_CAPABILITY_INTERNET so it can't become the default network.
+            if (nc.hasTransport(TRANSPORT_TEST) && !nc.hasCapability(NET_CAPABILITY_INTERNET)) {
+                return true;
+            }
+            return false;
         }
 
-        // Factories that make cell networks can allow the UID for the carrier service package.
+        // Factories that make cell/wifi networks can allow the UID for the carrier service package.
         // This can only work in T where there is support for CarrierPrivilegeAuthenticator
         if (null != carrierPrivilegeAuthenticator
-                && nc.hasSingleTransport(TRANSPORT_CELLULAR)
+                && (nc.hasSingleTransportBesidesTest(TRANSPORT_CELLULAR)
+                        || nc.hasSingleTransportBesidesTest(TRANSPORT_WIFI))
                 && (1 == nc.getAllowedUidsNoCopy().size())
-                && (carrierPrivilegeAuthenticator.hasCarrierPrivilegeForNetworkCapabilities(
+                && (carrierPrivilegeAuthenticator.isCarrierServiceUidForNetworkCapabilities(
                         nc.getAllowedUidsNoCopy().valueAt(0), nc))) {
             return true;
         }
diff --git a/service/src/com/android/server/connectivity/NetworkNotificationManager.java b/service/src/com/android/server/connectivity/NetworkNotificationManager.java
index bc13592..7707122 100644
--- a/service/src/com/android/server/connectivity/NetworkNotificationManager.java
+++ b/service/src/com/android/server/connectivity/NetworkNotificationManager.java
@@ -243,7 +243,7 @@
                     details = r.getString(R.string.network_available_sign_in_detailed, name);
                     break;
                 case TRANSPORT_CELLULAR:
-                    title = r.getString(R.string.network_available_sign_in, 0);
+                    title = r.getString(R.string.mobile_network_available_no_internet);
                     // TODO: Change this to pull from NetworkInfo once a printable
                     // name has been added to it
                     NetworkSpecifier specifier = nai.networkCapabilities.getNetworkSpecifier();
@@ -252,8 +252,16 @@
                         subId = ((TelephonyNetworkSpecifier) specifier).getSubscriptionId();
                     }
 
-                    details = mTelephonyManager.createForSubscriptionId(subId)
+                    final String operatorName = mTelephonyManager.createForSubscriptionId(subId)
                             .getNetworkOperatorName();
+                    if (TextUtils.isEmpty(operatorName)) {
+                        details = r.getString(R.string
+                                .mobile_network_available_no_internet_detailed_unknown_carrier);
+                    } else {
+                        details = r.getString(
+                                R.string.mobile_network_available_no_internet_detailed,
+                                operatorName);
+                    }
                     break;
                 default:
                     title = r.getString(R.string.network_available_sign_in, 0);
diff --git a/service/src/com/android/server/connectivity/NetworkRanker.java b/service/src/com/android/server/connectivity/NetworkRanker.java
index c473444..d94c8dc 100644
--- a/service/src/com/android/server/connectivity/NetworkRanker.java
+++ b/service/src/com/android/server/connectivity/NetworkRanker.java
@@ -17,8 +17,6 @@
 package com.android.server.connectivity;
 
 import static android.net.NetworkCapabilities.NET_CAPABILITY_CAPTIVE_PORTAL;
-import static android.net.NetworkCapabilities.NET_CAPABILITY_PRIORITIZE_BANDWIDTH;
-import static android.net.NetworkCapabilities.NET_CAPABILITY_PRIORITIZE_LATENCY;
 import static android.net.NetworkCapabilities.TRANSPORT_BLUETOOTH;
 import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
 import static android.net.NetworkCapabilities.TRANSPORT_ETHERNET;
@@ -223,19 +221,6 @@
     }
 
     /**
-     * Returns whether the scorable has any of the PRIORITIZE_* capabilities.
-     *
-     * These capabilities code for customer slices, and a network that has one is a customer slice.
-     */
-    private boolean hasPrioritizedCapability(@NonNull final Scoreable nai) {
-        final NetworkCapabilities caps = nai.getCapsNoCopy();
-        final long anyPrioritizeCapability =
-                (1L << NET_CAPABILITY_PRIORITIZE_LATENCY)
-                | (1L << NET_CAPABILITY_PRIORITIZE_BANDWIDTH);
-        return 0 != (caps.getCapabilitiesInternal() & anyPrioritizeCapability);
-    }
-
-    /**
      * Get the best network among a list of candidates according to policy.
      * @param candidates the candidates
      * @param currentSatisfier the current satisfier, or null if none
@@ -339,12 +324,6 @@
         // change from the previous result. If there were, it's guaranteed candidates.size() > 0
         // because accepted.size() > 0 above.
 
-        // If any network is not a slice with prioritized bandwidth or latency, don't choose one
-        // that is.
-        partitionInto(candidates, nai -> !hasPrioritizedCapability(nai), accepted, rejected);
-        if (accepted.size() == 1) return accepted.get(0);
-        if (accepted.size() > 0 && rejected.size() > 0) candidates = new ArrayList<>(accepted);
-
         // If some of the networks have a better transport than others, keep only the ones with
         // the best transports.
         for (final int transport : PREFERRED_TRANSPORTS_ORDER) {
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/staticlibs/Android.bp b/staticlibs/Android.bp
index 6325b46..3cbabcc 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: [
@@ -191,7 +193,7 @@
 java_library {
     name: "net-utils-device-common-netlink",
     srcs: [
-        "device/com/android/net/module/util/netlink/*.java",
+        "device/com/android/net/module/util/netlink/**/*.java",
     ],
     sdk_version: "module_current",
     min_sdk_version: "30",
@@ -295,6 +297,10 @@
             "-Xep:NullablePrimitive:ERROR",
         ],
     },
+    apex_available: [
+        "//apex_available:platform",
+        "com.android.tethering",
+    ],
 }
 
 java_library {
@@ -459,10 +465,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 d2a5b65..b62a430 100644
--- a/staticlibs/device/com/android/net/module/util/BpfBitmap.java
+++ b/staticlibs/device/com/android/net/module/util/BpfBitmap.java
@@ -16,9 +16,11 @@
 
 package com.android.net.module.util;
 
+import android.os.Build;
 import android.system.ErrnoException;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
 
  /**
  *
@@ -26,6 +28,7 @@
  * array type with key->int and value->uint64_t defined in the bpf program.
  *
  */
+@RequiresApi(Build.VERSION_CODES.S)
 public class BpfBitmap {
     private BpfMap<Struct.S32, Struct.S64> mBpfMap;
 
@@ -35,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 d622427..da77ae8 100644
--- a/staticlibs/device/com/android/net/module/util/BpfMap.java
+++ b/staticlibs/device/com/android/net/module/util/BpfMap.java
@@ -18,14 +18,15 @@
 import static android.system.OsConstants.EEXIST;
 import static android.system.OsConstants.ENOENT;
 
+import android.os.Build;
 import android.os.ParcelFileDescriptor;
 import android.system.ErrnoException;
 import android.util.Pair;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
 
-import java.io.IOException;
 import java.nio.ByteBuffer;
 import java.nio.ByteOrder;
 import java.util.NoSuchElementException;
@@ -40,6 +41,7 @@
  * @param <K> the key of the map.
  * @param <V> the value of the map.
  */
+@RequiresApi(Build.VERSION_CODES.S)
 public class BpfMap<K extends Struct, V extends Struct> implements IBpfMap<K, V> {
     static {
         System.loadLibrary(JniUtil.getJniLibraryName(BpfMap.class.getPackage()));
@@ -107,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/BpfUtils.java b/staticlibs/device/com/android/net/module/util/BpfUtils.java
index 10a8f60..cdd6fd7 100644
--- a/staticlibs/device/com/android/net/module/util/BpfUtils.java
+++ b/staticlibs/device/com/android/net/module/util/BpfUtils.java
@@ -15,7 +15,10 @@
  */
 package com.android.net.module.util;
 
+import android.os.Build;
+
 import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
 
 import java.io.IOException;
 
@@ -24,6 +27,7 @@
  *
  * {@hide}
  */
+@RequiresApi(Build.VERSION_CODES.TIRAMISU)
 public class BpfUtils {
     static {
         System.loadLibrary(JniUtil.getJniLibraryName(BpfUtils.class.getPackage()));
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/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/NetlinkMessage.java b/staticlibs/device/com/android/net/module/util/netlink/NetlinkMessage.java
index 9e1e26e..781a04e 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/NetlinkMessage.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/NetlinkMessage.java
@@ -16,12 +16,17 @@
 
 package com.android.net.module.util.netlink;
 
+import static com.android.net.module.util.netlink.xfrm.XfrmNetlinkMessage.NETLINK_XFRM;
+
 import android.system.OsConstants;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
+import com.android.net.module.util.netlink.xfrm.XfrmNetlinkMessage;
+
 import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
 
 /**
  * NetlinkMessage base class for other, more specific netlink message types.
@@ -75,6 +80,8 @@
             parsed = parseInetDiagMessage(nlmsghdr, byteBuffer);
         } else if (nlFamily == OsConstants.NETLINK_NETFILTER) {
             parsed = parseNfMessage(nlmsghdr, byteBuffer);
+        } else if (nlFamily == NETLINK_XFRM) {
+            parsed = parseXfrmMessage(nlmsghdr, byteBuffer);
         } else {
             parsed = null;
         }
@@ -135,6 +142,7 @@
                 return (NetlinkMessage) RtNetlinkAddressMessage.parse(nlmsghdr, byteBuffer);
             case NetlinkConstants.RTM_NEWROUTE:
             case NetlinkConstants.RTM_DELROUTE:
+            case NetlinkConstants.RTM_GETROUTE:
                 return (NetlinkMessage) RtNetlinkRouteMessage.parse(nlmsghdr, byteBuffer);
             case NetlinkConstants.RTM_NEWNEIGH:
             case NetlinkConstants.RTM_DELNEIGH:
@@ -168,4 +176,19 @@
             default: return null;
         }
     }
+
+    @Nullable
+    private static NetlinkMessage parseXfrmMessage(
+            @NonNull final StructNlMsgHdr nlmsghdr, @NonNull final ByteBuffer byteBuffer) {
+        return (NetlinkMessage) XfrmNetlinkMessage.parseXfrmInternal(nlmsghdr, byteBuffer);
+    }
+
+    /** A convenient method to create a ByteBuffer for encoding a new message */
+    protected static ByteBuffer newNlMsgByteBuffer(int payloadLen) {
+        final int length = StructNlMsgHdr.STRUCT_SIZE + payloadLen;
+        final ByteBuffer byteBuffer = ByteBuffer.allocate(length);
+        byteBuffer.order(ByteOrder.nativeOrder());
+
+        return byteBuffer;
+    }
 }
diff --git a/staticlibs/device/com/android/net/module/util/netlink/NetlinkUtils.java b/staticlibs/device/com/android/net/module/util/netlink/NetlinkUtils.java
index f1f30d3..61d347c 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 {
@@ -220,12 +227,18 @@
     /**
      * Create netlink socket with the given netlink protocol type.
      *
+     * @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;
     }
 
@@ -233,7 +246,7 @@
      * 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, 0);
     }
 
     /**
@@ -308,4 +321,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..b2b1e93 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkRouteMessage.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkRouteMessage.java
@@ -19,11 +19,15 @@
 import static android.system.OsConstants.AF_INET;
 import static android.system.OsConstants.AF_INET6;
 
+import static android.system.OsConstants.NETLINK_ROUTE;
 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.hexify;
+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;
@@ -34,6 +38,9 @@
 import java.net.Inet6Address;
 import java.net.InetAddress;
 import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.IntBuffer;
+import java.util.Arrays;
 
 /**
  * A NetlinkMessage subclass for rtnetlink route messages.
@@ -49,31 +56,69 @@
  */
 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) {
+    public RtNetlinkRouteMessage(StructNlMsgHdr header, StructRtMsg rtMsg) {
         super(header);
-        mRtmsg = null;
+        mRtmsg = rtMsg;
+        mSource = null;
         mDestination = null;
         mGateway = null;
-        mIfindex = 0;
+        mIifIndex = 0;
+        mOifIndex = 0;
         mRtaCacheInfo = null;
+        mSinceLastUseMillis = -1;
+    }
+
+    /**
+     * 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 +131,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 +150,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 +172,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 +187,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 +205,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 +234,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 +253,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 +263,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 +323,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/StructNlMsgHdr.java b/staticlibs/device/com/android/net/module/util/netlink/StructNlMsgHdr.java
index 5052cb8..5272366 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/StructNlMsgHdr.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/StructNlMsgHdr.java
@@ -71,13 +71,17 @@
             }
             sb.append("NLM_F_ECHO");
         }
-        if ((flags & NLM_F_ROOT) != 0) {
+        if ((flags & NLM_F_DUMP) == NLM_F_DUMP) {
+            if (sb.length() > 0) {
+                sb.append("|");
+            }
+            sb.append("NLM_F_DUMP");
+        } else if ((flags & NLM_F_ROOT) != 0) { // NLM_F_DUMP = NLM_F_ROOT | NLM_F_MATCH
             if (sb.length() > 0) {
                 sb.append("|");
             }
             sb.append("NLM_F_ROOT");
-        }
-        if ((flags & NLM_F_MATCH) != 0) {
+        } else if ((flags & NLM_F_MATCH) != 0) {
             if (sb.length() > 0) {
                 sb.append("|");
             }
@@ -128,6 +132,14 @@
         nlmsg_pid = 0;
     }
 
+    public StructNlMsgHdr(int payloadLen, short type, short flags, int seq) {
+        nlmsg_len = StructNlMsgHdr.STRUCT_SIZE + payloadLen;
+        nlmsg_type = type;
+        nlmsg_flags = flags;
+        nlmsg_seq = seq;
+        nlmsg_pid = 0;
+    }
+
     /**
      * Write netlink message header to ByteBuffer.
      */
diff --git a/staticlibs/device/com/android/net/module/util/netlink/xfrm/OWNERS b/staticlibs/device/com/android/net/module/util/netlink/xfrm/OWNERS
new file mode 100644
index 0000000..fca70aa
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/netlink/xfrm/OWNERS
@@ -0,0 +1,2 @@
+# Bug component: 685852
+file:platform/frameworks/base:main:/services/core/java/com/android/server/vcn/OWNERS
diff --git a/staticlibs/device/com/android/net/module/util/netlink/xfrm/StructXfrmAddressT.java b/staticlibs/device/com/android/net/module/util/netlink/xfrm/StructXfrmAddressT.java
new file mode 100644
index 0000000..cef1f56
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/netlink/xfrm/StructXfrmAddressT.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.net.module.util.netlink.xfrm;
+
+import static com.android.net.module.util.NetworkStackConstants.IPV4_ADDR_LEN;
+
+import android.system.OsConstants;
+
+import androidx.annotation.NonNull;
+
+import com.android.net.module.util.Struct;
+import com.android.net.module.util.Struct.Field;
+import com.android.net.module.util.Struct.Type;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+
+/**
+ * Struct xfrm_address_t
+ *
+ * <p>see include/uapi/linux/xfrm.h
+ *
+ * <pre>
+ * typedef union {
+ *      __be32 a4;
+ *      __be32 a6[4];
+ *      struct in6_addr in6;
+ * } xfrm_address_t;
+ * </pre>
+ *
+ * @hide
+ */
+public class StructXfrmAddressT extends Struct {
+    public static final int STRUCT_SIZE = 16;
+
+    @Field(order = 0, type = Type.ByteArray, arraysize = STRUCT_SIZE)
+    public final byte[] address;
+
+    // Constructor that allows Strutc.parse(Class<T>, ByteBuffer) to work
+    public StructXfrmAddressT(@NonNull final byte[] address) {
+        this.address = address.clone();
+    }
+
+    // Constructor to build a new message
+    public StructXfrmAddressT(@NonNull final InetAddress inetAddress) {
+        this.address = new byte[STRUCT_SIZE];
+        final byte[] addressBytes = inetAddress.getAddress();
+        System.arraycopy(addressBytes, 0, address, 0, addressBytes.length);
+    }
+
+    /** Return the address in InetAddress */
+    public InetAddress getAddress(int family) {
+        final byte[] addressBytes;
+        if (family == OsConstants.AF_INET6) {
+            addressBytes = this.address;
+        } else if (family == OsConstants.AF_INET) {
+            addressBytes = new byte[IPV4_ADDR_LEN];
+            System.arraycopy(this.address, 0, addressBytes, 0, addressBytes.length);
+        } else {
+            throw new IllegalArgumentException("Invalid IP family " + family);
+        }
+
+        try {
+            return InetAddress.getByAddress(addressBytes);
+        } catch (UnknownHostException e) {
+            // This should never happen
+            throw new IllegalArgumentException(
+                    "Illegal length of IP address " + addressBytes.length, e);
+        }
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/netlink/xfrm/StructXfrmId.java b/staticlibs/device/com/android/net/module/util/netlink/xfrm/StructXfrmId.java
new file mode 100644
index 0000000..bdcdcd8
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/netlink/xfrm/StructXfrmId.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util.netlink.xfrm;
+
+import androidx.annotation.NonNull;
+
+import com.android.net.module.util.Struct;
+
+import java.net.InetAddress;
+
+/**
+ * Struct xfrm_id
+ *
+ * <p>see include/uapi/linux/xfrm.h
+ *
+ * <pre>
+ * struct xfrm_id {
+ *      xfrm_address_t daddr;
+ *      __be32 spi;
+ *      __u8 proto;
+ * };
+ * </pre>
+ *
+ * @hide
+ */
+public class StructXfrmId extends Struct {
+    public static final int STRUCT_SIZE = 24;
+
+    @Field(order = 0, type = Type.ByteArray, arraysize = 16)
+    public final byte[] nestedStructDAddr;
+
+    @Field(order = 1, type = Type.UBE32)
+    public final long spi;
+
+    @Field(order = 2, type = Type.U8, padding = 3)
+    public final short proto;
+
+    @Computed private final StructXfrmAddressT mDestXfrmAddressT;
+
+    // Constructor that allows Strutc.parse(Class<T>, ByteBuffer) to work
+    public StructXfrmId(@NonNull final byte[] nestedStructDAddr, long spi, short proto) {
+        this.nestedStructDAddr = nestedStructDAddr.clone();
+        this.spi = spi;
+        this.proto = proto;
+
+        mDestXfrmAddressT = new StructXfrmAddressT(this.nestedStructDAddr);
+    }
+
+    // Constructor to build a new message
+    public StructXfrmId(@NonNull final InetAddress destAddress, long spi, short proto) {
+        this(new StructXfrmAddressT(destAddress).writeToBytes(), spi, proto);
+    }
+
+    /** Return the destination address */
+    public InetAddress getDestAddress(int family) {
+        return mDestXfrmAddressT.getAddress(family);
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/netlink/xfrm/StructXfrmLifetimeCfg.java b/staticlibs/device/com/android/net/module/util/netlink/xfrm/StructXfrmLifetimeCfg.java
new file mode 100644
index 0000000..12f68c8
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/netlink/xfrm/StructXfrmLifetimeCfg.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util.netlink.xfrm;
+
+import static com.android.net.module.util.netlink.xfrm.XfrmNetlinkMessage.XFRM_INF;
+
+import androidx.annotation.NonNull;
+
+import com.android.net.module.util.Struct;
+
+import java.math.BigInteger;
+
+/**
+ * Struct xfrm_lifetime_cfg
+ *
+ * <p>see include/uapi/linux/xfrm.h
+ *
+ * <pre>
+ * struct xfrm_lifetime_cfg {
+ *      __u64 soft_byte_limit;
+ *      __u64 hard_byte_limit;
+ *      __u64 soft_packet_limit;
+ *      __u64 hard_packet_limit;
+ *      __u64 soft_add_expires_seconds;
+ *      __u64 hard_add_expires_seconds;
+ *      __u64 soft_use_expires_seconds;
+ *      __u64 hard_use_expires_seconds;
+ * };
+ * </pre>
+ *
+ * @hide
+ */
+public class StructXfrmLifetimeCfg extends Struct {
+    public static final int STRUCT_SIZE = 64;
+
+    @Field(order = 0, type = Type.U64)
+    public final BigInteger softByteLimit;
+
+    @Field(order = 1, type = Type.U64)
+    public final BigInteger hardByteLimit;
+
+    @Field(order = 2, type = Type.U64)
+    public final BigInteger softPacketLimit;
+
+    @Field(order = 3, type = Type.U64)
+    public final BigInteger hardPacketLimit;
+
+    @Field(order = 4, type = Type.U64)
+    public final BigInteger softAddExpiresSeconds;
+
+    @Field(order = 5, type = Type.U64)
+    public final BigInteger hardAddExpiresSeconds;
+
+    @Field(order = 6, type = Type.U64)
+    public final BigInteger softUseExpiresSeconds;
+
+    @Field(order = 7, type = Type.U64)
+    public final BigInteger hardUseExpiresSeconds;
+
+    // Constructor that allows Strutc.parse(Class<T>, ByteBuffer) to work
+    public StructXfrmLifetimeCfg(
+            @NonNull final BigInteger softByteLimit,
+            @NonNull final BigInteger hardByteLimit,
+            @NonNull final BigInteger softPacketLimit,
+            @NonNull final BigInteger hardPacketLimit,
+            @NonNull final BigInteger softAddExpiresSeconds,
+            @NonNull final BigInteger hardAddExpiresSeconds,
+            @NonNull final BigInteger softUseExpiresSeconds,
+            @NonNull final BigInteger hardUseExpiresSeconds) {
+        this.softByteLimit = softByteLimit;
+        this.hardByteLimit = hardByteLimit;
+        this.softPacketLimit = softPacketLimit;
+        this.hardPacketLimit = hardPacketLimit;
+        this.softAddExpiresSeconds = softAddExpiresSeconds;
+        this.hardAddExpiresSeconds = hardAddExpiresSeconds;
+        this.softUseExpiresSeconds = softUseExpiresSeconds;
+        this.hardUseExpiresSeconds = hardUseExpiresSeconds;
+    }
+
+    // Constructor to build a new message
+    public StructXfrmLifetimeCfg() {
+        this(
+                XFRM_INF,
+                XFRM_INF,
+                XFRM_INF,
+                XFRM_INF,
+                BigInteger.ZERO,
+                BigInteger.ZERO,
+                BigInteger.ZERO,
+                BigInteger.ZERO);
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/netlink/xfrm/StructXfrmLifetimeCur.java b/staticlibs/device/com/android/net/module/util/netlink/xfrm/StructXfrmLifetimeCur.java
new file mode 100644
index 0000000..6a539c7
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/netlink/xfrm/StructXfrmLifetimeCur.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util.netlink.xfrm;
+
+import androidx.annotation.NonNull;
+
+import com.android.net.module.util.Struct;
+
+import java.math.BigInteger;
+
+/**
+ * Struct xfrm_lifetime_cur
+ *
+ * <p>see include/uapi/linux/xfrm.h
+ *
+ * <pre>
+ * struct xfrm_lifetime_cur {
+ *      __u64 bytes;
+ *      __u64 packets;
+ *      __u64 add_time;
+ *      __u64 use_time;
+ * };
+ * </pre>
+ *
+ * @hide
+ */
+public class StructXfrmLifetimeCur extends Struct {
+    public static final int STRUCT_SIZE = 32;
+
+    @Field(order = 0, type = Type.U64)
+    public final BigInteger bytes;
+
+    @Field(order = 1, type = Type.U64)
+    public final BigInteger packets;
+
+    @Field(order = 2, type = Type.U64)
+    public final BigInteger addTime;
+
+    @Field(order = 3, type = Type.U64)
+    public final BigInteger useTime;
+
+    public StructXfrmLifetimeCur(
+            @NonNull final BigInteger bytes,
+            @NonNull final BigInteger packets,
+            @NonNull final BigInteger addTime,
+            @NonNull final BigInteger useTime) {
+        this.bytes = bytes;
+        this.packets = packets;
+        this.addTime = addTime;
+        this.useTime = useTime;
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/netlink/xfrm/StructXfrmReplayStateEsn.java b/staticlibs/device/com/android/net/module/util/netlink/xfrm/StructXfrmReplayStateEsn.java
new file mode 100644
index 0000000..a7bdcd9
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/netlink/xfrm/StructXfrmReplayStateEsn.java
@@ -0,0 +1,203 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util.netlink.xfrm;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+
+import com.android.net.module.util.Struct;
+import com.android.net.module.util.Struct.Field;
+import com.android.net.module.util.Struct.Type;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+/**
+ * Struct xfrm_replay_state_esn
+ *
+ * <p>see include/uapi/linux/xfrm.h
+ *
+ * <pre>
+ * struct xfrm_replay_state_esn {
+ *      unsigned int bmp_len;
+ *      __u32 oseq;
+ *      __u32 seq;
+ *      __u32 oseq_hi;
+ *      __u32 seq_hi;
+ *      __u32 replay_window;
+ *      __u32 bmp[];
+ * };
+ * </pre>
+ *
+ * @hide
+ */
+public class StructXfrmReplayStateEsn {
+    // include/uapi/linux/xfrm.h XFRMA_REPLAY_ESN_MAX
+    private static final int XFRMA_REPLAY_ESN_BMP_LEN_MAX = 128;
+
+    @NonNull private final StructXfrmReplayStateEsnWithoutBitmap mWithoutBitmap;
+    @NonNull private final byte[] mBitmap;
+
+    private StructXfrmReplayStateEsn(
+            @NonNull final StructXfrmReplayStateEsnWithoutBitmap withoutBitmap,
+            @NonNull final byte[] bitmap) {
+        mWithoutBitmap = withoutBitmap;
+        mBitmap = bitmap;
+        validate();
+    }
+
+    /** Constructor to build a new message */
+    public StructXfrmReplayStateEsn(
+            long bmpLen,
+            long oSeq,
+            long seq,
+            long oSeqHi,
+            long seqHi,
+            long replayWindow,
+            @NonNull final byte[] bitmap) {
+        mWithoutBitmap =
+                new StructXfrmReplayStateEsnWithoutBitmap(
+                        bmpLen, oSeq, seq, oSeqHi, seqHi, replayWindow);
+        mBitmap = bitmap.clone();
+        validate();
+    }
+
+    private void validate() {
+        if (mWithoutBitmap.mBmpLenInBytes != mBitmap.length) {
+            throw new IllegalArgumentException(
+                    "mWithoutBitmap.mBmpLenInBytes not aligned with bitmap."
+                            + " mWithoutBitmap.mBmpLenInBytes: "
+                            + mWithoutBitmap.mBmpLenInBytes
+                            + " bitmap.length "
+                            + mBitmap.length);
+        }
+    }
+
+    /** Parse IpSecStructXfrmReplayStateEsn from ByteBuffer. */
+    @Nullable
+    public static StructXfrmReplayStateEsn parse(@NonNull final ByteBuffer buf) {
+        final StructXfrmReplayStateEsnWithoutBitmap withoutBitmap =
+                Struct.parse(StructXfrmReplayStateEsnWithoutBitmap.class, buf);
+        if (withoutBitmap == null) {
+            return null;
+        }
+
+        final byte[] bitmap = new byte[withoutBitmap.mBmpLenInBytes];
+        buf.get(bitmap);
+
+        return new StructXfrmReplayStateEsn(withoutBitmap, bitmap);
+    }
+
+    /** Convert the parsed object to ByteBuffer. */
+    public void writeToByteBuffer(@NonNull final ByteBuffer buf) {
+        mWithoutBitmap.writeToByteBuffer(buf);
+        buf.put(mBitmap);
+    }
+
+    /** Return the struct size */
+    public int getStructSize() {
+        return StructXfrmReplayStateEsnWithoutBitmap.STRUCT_SIZE + mBitmap.length;
+    }
+
+    /** Return the bitmap */
+    public byte[] getBitmap() {
+        return mBitmap.clone();
+    }
+
+    /** Return the bmp_len */
+    public long getBmpLen() {
+        return mWithoutBitmap.bmpLen;
+    }
+
+    /** Return the replay_window */
+    public long getReplayWindow() {
+        return mWithoutBitmap.replayWindow;
+    }
+
+    /** Return the TX sequence number in unisgned long */
+    public long getTxSequenceNumber() {
+        return getSequenceNumber(mWithoutBitmap.oSeqHi, mWithoutBitmap.oSeq);
+    }
+
+    /** Return the RX sequence number in unisgned long */
+    public long getRxSequenceNumber() {
+        return getSequenceNumber(mWithoutBitmap.seqHi, mWithoutBitmap.seq);
+    }
+
+    @VisibleForTesting
+    static long getSequenceNumber(long hi, long low) {
+        final ByteBuffer buffer = ByteBuffer.allocate(8);
+        buffer.order(ByteOrder.BIG_ENDIAN);
+        buffer.putInt((int) hi).putInt((int) low);
+        buffer.rewind();
+
+        return buffer.getLong();
+    }
+
+    /** The xfrm_replay_state_esn struct without the bitmap */
+    // Because the size of the bitmap is decided at runtime, it cannot be included in a Struct
+    // subclass. Therefore, this nested class is defined to include all other fields supported by
+    // Struct for code reuse.
+    public static class StructXfrmReplayStateEsnWithoutBitmap extends Struct {
+        public static final int STRUCT_SIZE = 24;
+
+        @Field(order = 0, type = Type.U32)
+        public final long bmpLen; // replay bitmap length in 32-bit integers
+
+        @Field(order = 1, type = Type.U32)
+        public final long oSeq;
+
+        @Field(order = 2, type = Type.U32)
+        public final long seq;
+
+        @Field(order = 3, type = Type.U32)
+        public final long oSeqHi;
+
+        @Field(order = 4, type = Type.U32)
+        public final long seqHi;
+
+        @Field(order = 5, type = Type.U32)
+        public final long replayWindow; // replay bitmap length in bit
+
+        @Computed private final int mBmpLenInBytes; // replay bitmap length in bytes
+
+        public StructXfrmReplayStateEsnWithoutBitmap(
+                long bmpLen, long oSeq, long seq, long oSeqHi, long seqHi, long replayWindow) {
+            this.bmpLen = bmpLen;
+            this.oSeq = oSeq;
+            this.seq = seq;
+            this.oSeqHi = oSeqHi;
+            this.seqHi = seqHi;
+            this.replayWindow = replayWindow;
+
+            if (bmpLen > XFRMA_REPLAY_ESN_BMP_LEN_MAX) {
+                throw new IllegalArgumentException("Invalid bmpLen " + bmpLen);
+            }
+
+            if (bmpLen * 4 * 8 != replayWindow) {
+                throw new IllegalArgumentException(
+                        "bmpLen not aligned with replayWindow. bmpLen: "
+                                + bmpLen
+                                + " replayWindow "
+                                + replayWindow);
+            }
+
+            mBmpLenInBytes = (int) bmpLen * 4;
+        }
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/netlink/xfrm/StructXfrmSelector.java b/staticlibs/device/com/android/net/module/util/netlink/xfrm/StructXfrmSelector.java
new file mode 100644
index 0000000..7bd2ca1
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/netlink/xfrm/StructXfrmSelector.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util.netlink.xfrm;
+
+import androidx.annotation.NonNull;
+
+import com.android.net.module.util.Struct;
+import com.android.net.module.util.Struct.Field;
+import com.android.net.module.util.Struct.Type;
+
+/**
+ * Struct xfrm_selector
+ *
+ * <p>see include/uapi/linux/xfrm.h
+ *
+ * <pre>
+ * struct xfrm_selector {
+ *      xfrm_address_t daddr;
+ *      xfrm_address_t saddr;
+ *      __be16 dport;
+ *      __be16 dport_mask;
+ *      __be16 sport;
+ *      __be16 sport_mask;
+ *      __u16 family;
+ *      __u8 prefixlen_d;
+ *      __u8 prefixlen_s;
+ *      __u8 proto;
+ *      int ifindex;
+ *      __kernel_uid32_t user;
+ * };
+ * </pre>
+ *
+ * @hide
+ */
+public class StructXfrmSelector extends Struct {
+    public static final int STRUCT_SIZE = 56;
+
+    @Field(order = 0, type = Type.ByteArray, arraysize = 16)
+    public final byte[] nestedStructDAddr;
+
+    @Field(order = 1, type = Type.ByteArray, arraysize = 16)
+    public final byte[] nestedStructSAddr;
+
+    @Field(order = 2, type = Type.UBE16)
+    public final int dPort;
+
+    @Field(order = 3, type = Type.UBE16)
+    public final int dPortMask;
+
+    @Field(order = 4, type = Type.UBE16)
+    public final int sPort;
+
+    @Field(order = 5, type = Type.UBE16)
+    public final int sPortMask;
+
+    @Field(order = 6, type = Type.U16)
+    public final int selectorFamily;
+
+    @Field(order = 7, type = Type.U8, padding = 1)
+    public final short prefixlenD;
+
+    @Field(order = 8, type = Type.U8, padding = 1)
+    public final short prefixlenS;
+
+    @Field(order = 9, type = Type.U8, padding = 1)
+    public final short proto;
+
+    @Field(order = 10, type = Type.S32)
+    public final int ifIndex;
+
+    @Field(order = 11, type = Type.S32)
+    public final int user;
+
+    // Constructor that allows Strutc.parse(Class<T>, ByteBuffer) to work
+    public StructXfrmSelector(
+            @NonNull final byte[] nestedStructDAddr,
+            @NonNull final byte[] nestedStructSAddr,
+            int dPort,
+            int dPortMask,
+            int sPort,
+            int sPortMask,
+            int selectorFamily,
+            short prefixlenD,
+            short prefixlenS,
+            short proto,
+            int ifIndex,
+            int user) {
+        this.nestedStructDAddr = nestedStructDAddr.clone();
+        this.nestedStructSAddr = nestedStructSAddr.clone();
+        this.dPort = dPort;
+        this.dPortMask = dPortMask;
+        this.sPort = sPort;
+        this.sPortMask = sPortMask;
+        this.selectorFamily = selectorFamily;
+        this.prefixlenD = prefixlenD;
+        this.prefixlenS = prefixlenS;
+        this.proto = proto;
+        this.ifIndex = ifIndex;
+        this.user = user;
+    }
+
+    // Constructor to build a new message
+    public StructXfrmSelector(int selectorFamily) {
+        this(
+                new byte[StructXfrmAddressT.STRUCT_SIZE],
+                new byte[StructXfrmAddressT.STRUCT_SIZE],
+                0 /* dPort */,
+                0 /* dPortMask */,
+                0 /* sPort */,
+                0 /* sPortMask */,
+                selectorFamily,
+                (short) 0 /* prefixlenD */,
+                (short) 0 /* prefixlenS */,
+                (short) 0 /* proto */,
+                0 /* ifIndex */,
+                0 /* user */);
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/netlink/xfrm/StructXfrmStats.java b/staticlibs/device/com/android/net/module/util/netlink/xfrm/StructXfrmStats.java
new file mode 100644
index 0000000..be13293
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/netlink/xfrm/StructXfrmStats.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.net.module.util.netlink.xfrm;
+
+import com.android.net.module.util.Struct;
+
+/**
+ * Struct xfrm_lifetime_cur
+ *
+ * <p>see include/uapi/linux/xfrm.h
+ *
+ * <pre>
+ * struct xfrm_stats {
+ *      __u32 replay_window;
+ *      __u32 replay;
+ *      __u32 integrity_failed;
+ * };
+ * </pre>
+ *
+ * @hide
+ */
+public class StructXfrmStats extends Struct {
+    public static final int STRUCT_SIZE = 12;
+
+    /** Number of packets that fall out of the replay window */
+    @Field(order = 0, type = Type.U32)
+    public final long replayWindow;
+
+    /** Number of replayed packets */
+    @Field(order = 1, type = Type.U32)
+    public final long replay;
+
+    /** Number of packets that failed authentication */
+    @Field(order = 2, type = Type.U32)
+    public final long integrityFailed;
+
+    // Constructor that allows Strutc.parse(Class<T>, ByteBuffer) to work
+    public StructXfrmStats(long replayWindow, long replay, long integrityFailed) {
+        this.replayWindow = replayWindow;
+        this.replay = replay;
+        this.integrityFailed = integrityFailed;
+    }
+
+    // Constructor to build a new message
+    public StructXfrmStats() {
+        this(0, 0, 0);
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/netlink/xfrm/StructXfrmUsersaId.java b/staticlibs/device/com/android/net/module/util/netlink/xfrm/StructXfrmUsersaId.java
new file mode 100644
index 0000000..5ebc69c
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/netlink/xfrm/StructXfrmUsersaId.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util.netlink.xfrm;
+
+import androidx.annotation.NonNull;
+
+import com.android.net.module.util.Struct;
+import com.android.net.module.util.Struct.Field;
+import com.android.net.module.util.Struct.Type;
+
+import java.net.InetAddress;
+
+/**
+ * Struct xfrm_usersa_id
+ *
+ * <p>see include/uapi/linux/xfrm.h
+ *
+ * <pre>
+ * struct xfrm_usersa_id {
+ *      xfrm_address_t      daddr;
+ *      __be32              spi;
+ *      __u16               family;
+ *      __u8                proto;
+ * };
+ * </pre>
+ *
+ * @hide
+ */
+public class StructXfrmUsersaId extends Struct {
+    public static final int STRUCT_SIZE = 24;
+
+    @Field(order = 0, type = Type.ByteArray, arraysize = 16)
+    public final byte[] nestedStructDAddr; // xfrm_address_t
+
+    @Field(order = 1, type = Type.UBE32)
+    public final long spi;
+
+    @Field(order = 2, type = Type.U16)
+    public final int family;
+
+    @Field(order = 3, type = Type.U8, padding = 1)
+    public final short proto;
+
+    @Computed private final StructXfrmAddressT mDestXfrmAddressT;
+
+    // Constructor that allows Strutc.parse(Class<T>, ByteBuffer) to work
+    public StructXfrmUsersaId(
+            @NonNull final byte[] nestedStructDAddr, long spi, int family, short proto) {
+        this.nestedStructDAddr = nestedStructDAddr.clone();
+        this.spi = spi;
+        this.family = family;
+        this.proto = proto;
+
+        mDestXfrmAddressT = new StructXfrmAddressT(this.nestedStructDAddr);
+    }
+
+    // Constructor to build a new message
+    public StructXfrmUsersaId(
+            @NonNull final InetAddress destAddress, long spi, int family, short proto) {
+        this(new StructXfrmAddressT(destAddress).writeToBytes(), spi, family, proto);
+    }
+
+    /** Return the destination address */
+    public InetAddress getDestAddress() {
+        return mDestXfrmAddressT.getAddress(family);
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/netlink/xfrm/StructXfrmUsersaInfo.java b/staticlibs/device/com/android/net/module/util/netlink/xfrm/StructXfrmUsersaInfo.java
new file mode 100644
index 0000000..740a801
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/netlink/xfrm/StructXfrmUsersaInfo.java
@@ -0,0 +1,215 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util.netlink.xfrm;
+
+import android.system.OsConstants;
+
+import androidx.annotation.NonNull;
+
+import com.android.net.module.util.Struct;
+import com.android.net.module.util.Struct.Computed;
+import com.android.net.module.util.Struct.Field;
+import com.android.net.module.util.Struct.Type;
+
+import java.math.BigInteger;
+import java.net.Inet4Address;
+import java.net.InetAddress;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+/**
+ * Struct xfrm_usersa_info
+ *
+ * <p>see include/uapi/linux/xfrm.h
+ *
+ * <pre>
+ * struct xfrm_usersa_info {
+ *      struct xfrm_selector sel;
+ *      struct xfrm_id id;
+ *      xfrm_address_t saddr;
+ *      struct xfrm_lifetime_cfg lft;
+ *      struct xfrm_lifetime_cur curlft;
+ *      struct xfrm_stats stats;
+ *      __u32 seq;
+ *      __u32 reqid;
+ *      __u16 family;
+ *      __u8 mode;
+ *      __u8 replay_window;
+ *      __u8 flags;
+ * };
+ * </pre>
+ *
+ * @hide
+ */
+public class StructXfrmUsersaInfo extends Struct {
+    private static final int NESTED_STRUCTS_SIZE =
+            StructXfrmSelector.STRUCT_SIZE
+                    + StructXfrmId.STRUCT_SIZE
+                    + StructXfrmAddressT.STRUCT_SIZE
+                    + StructXfrmLifetimeCfg.STRUCT_SIZE
+                    + StructXfrmLifetimeCur.STRUCT_SIZE
+                    + StructXfrmStats.STRUCT_SIZE;
+
+    public static final int STRUCT_SIZE = NESTED_STRUCTS_SIZE + 20;
+
+    @Computed private final StructXfrmSelector mXfrmSelector;
+    @Computed private final StructXfrmId mXfrmId;
+    @Computed private final StructXfrmAddressT mSourceXfrmAddressT;
+    @Computed private final StructXfrmLifetimeCfg mXfrmLifetime;
+    @Computed private final StructXfrmLifetimeCur mXfrmCurrentLifetime;
+    @Computed private final StructXfrmStats mXfrmStats;
+
+    @Field(order = 0, type = Type.ByteArray, arraysize = NESTED_STRUCTS_SIZE)
+    public final byte[] nestedStructs;
+
+    @Field(order = 1, type = Type.U32)
+    public final long seq;
+
+    @Field(order = 2, type = Type.U32)
+    public final long reqId;
+
+    @Field(order = 3, type = Type.U16)
+    public final int family;
+
+    @Field(order = 4, type = Type.U8)
+    public final short mode;
+
+    @Field(order = 5, type = Type.U8)
+    public final short replayWindowLegacy;
+
+    @Field(order = 6, type = Type.U8, padding = 7)
+    public final short flags;
+
+    // Constructor that allows Strutc.parse(Class<T>, ByteBuffer) to work
+    public StructXfrmUsersaInfo(
+            @NonNull final byte[] nestedStructs,
+            long seq,
+            long reqId,
+            int family,
+            short mode,
+            short replayWindowLegacy,
+            short flags) {
+        this.nestedStructs = nestedStructs.clone();
+        this.seq = seq;
+        this.reqId = reqId;
+        this.family = family;
+        this.mode = mode;
+        this.replayWindowLegacy = replayWindowLegacy;
+        this.flags = flags;
+
+        final ByteBuffer nestedStructsBuff = ByteBuffer.wrap(nestedStructs);
+        nestedStructsBuff.order(ByteOrder.nativeOrder());
+
+        // The parsing order matters
+        mXfrmSelector = Struct.parse(StructXfrmSelector.class, nestedStructsBuff);
+        mXfrmId = Struct.parse(StructXfrmId.class, nestedStructsBuff);
+        mSourceXfrmAddressT = Struct.parse(StructXfrmAddressT.class, nestedStructsBuff);
+        mXfrmLifetime = Struct.parse(StructXfrmLifetimeCfg.class, nestedStructsBuff);
+        mXfrmCurrentLifetime = Struct.parse(StructXfrmLifetimeCur.class, nestedStructsBuff);
+        mXfrmStats = Struct.parse(StructXfrmStats.class, nestedStructsBuff);
+    }
+
+    // Constructor to build a new message for TESTING
+    StructXfrmUsersaInfo(
+            @NonNull final InetAddress dAddr,
+            @NonNull final InetAddress sAddr,
+            @NonNull final BigInteger addTime,
+            int selectorFamily,
+            long spi,
+            long seq,
+            long reqId,
+            short proto,
+            short mode,
+            short replayWindowLegacy,
+            short flags) {
+        this.seq = seq;
+        this.reqId = reqId;
+        this.family = dAddr instanceof Inet4Address ? OsConstants.AF_INET : OsConstants.AF_INET6;
+        this.mode = mode;
+        this.replayWindowLegacy = replayWindowLegacy;
+        this.flags = flags;
+
+        mXfrmSelector = new StructXfrmSelector(selectorFamily);
+        mXfrmId = new StructXfrmId(dAddr, spi, proto);
+        mSourceXfrmAddressT = new StructXfrmAddressT(sAddr);
+        mXfrmLifetime = new StructXfrmLifetimeCfg();
+        mXfrmCurrentLifetime =
+                new StructXfrmLifetimeCur(
+                        BigInteger.ZERO, BigInteger.ZERO, addTime, BigInteger.ZERO);
+        mXfrmStats = new StructXfrmStats();
+
+        final ByteBuffer nestedStructsBuff = ByteBuffer.allocate(NESTED_STRUCTS_SIZE);
+        nestedStructsBuff.order(ByteOrder.nativeOrder());
+
+        mXfrmSelector.writeToByteBuffer(nestedStructsBuff);
+        mXfrmId.writeToByteBuffer(nestedStructsBuff);
+        mSourceXfrmAddressT.writeToByteBuffer(nestedStructsBuff);
+        mXfrmLifetime.writeToByteBuffer(nestedStructsBuff);
+        mXfrmCurrentLifetime.writeToByteBuffer(nestedStructsBuff);
+        mXfrmStats.writeToByteBuffer(nestedStructsBuff);
+
+        this.nestedStructs = nestedStructsBuff.array();
+    }
+
+    // Constructor to build a new message
+    public StructXfrmUsersaInfo(
+            @NonNull final InetAddress dAddr,
+            @NonNull final InetAddress sAddr,
+            long spi,
+            long seq,
+            long reqId,
+            short proto,
+            short mode,
+            short replayWindowLegacy,
+            short flags) {
+        // Use AF_UNSPEC for all SAs selectors. In transport mode, kernel picks selector family
+        // based on usersa->family, while in tunnel mode, the XFRM_STATE_AF_UNSPEC flag allows
+        // dual-stack SAs.
+        this(
+                dAddr,
+                sAddr,
+                BigInteger.ZERO,
+                OsConstants.AF_UNSPEC,
+                spi,
+                seq,
+                reqId,
+                proto,
+                mode,
+                replayWindowLegacy,
+                flags);
+    }
+
+    /** Return the destination address */
+    public InetAddress getDestAddress() {
+        return mXfrmId.getDestAddress(family);
+    }
+
+    /** Return the source address */
+    public InetAddress getSrcAddress() {
+        return mSourceXfrmAddressT.getAddress(family);
+    }
+
+    /** Return the SPI */
+    public long getSpi() {
+        return mXfrmId.spi;
+    }
+
+    /** Return the current lifetime */
+    public StructXfrmLifetimeCur getCurrentLifetime() {
+        return mXfrmCurrentLifetime;
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/netlink/xfrm/XfrmNetlinkGetSaMessage.java b/staticlibs/device/com/android/net/module/util/netlink/xfrm/XfrmNetlinkGetSaMessage.java
new file mode 100644
index 0000000..680a7ca
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/netlink/xfrm/XfrmNetlinkGetSaMessage.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util.netlink.xfrm;
+
+import static com.android.net.module.util.netlink.StructNlMsgHdr.NLM_F_REQUEST;
+import static com.android.net.module.util.netlink.xfrm.XfrmNetlinkMessage.XFRM_MSG_GETSA;
+
+import android.system.OsConstants;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.net.module.util.Struct;
+import com.android.net.module.util.netlink.StructNlMsgHdr;
+
+import java.net.Inet4Address;
+import java.net.InetAddress;
+import java.nio.ByteBuffer;
+
+/**
+ * An XfrmNetlinkMessage subclass for XFRM_MSG_GETSA messages.
+ *
+ * <p>see include/uapi/linux/xfrm.h
+ *
+ * <p>XFRM_MSG_GETSA syntax
+ *
+ * <ul>
+ *   <li>TLV: xfrm_usersa_id
+ *   <li>Optional Attributes: XFRMA_MARK, XFRMA_SRCADDR
+ * </ul>
+ *
+ * @hide
+ */
+public class XfrmNetlinkGetSaMessage extends XfrmNetlinkMessage {
+    @NonNull private final StructXfrmUsersaId mXfrmUsersaId;
+
+    private XfrmNetlinkGetSaMessage(
+            @NonNull final StructNlMsgHdr header, @NonNull final StructXfrmUsersaId xfrmUsersaId) {
+        super(header);
+        mXfrmUsersaId = xfrmUsersaId;
+    }
+
+    private XfrmNetlinkGetSaMessage(
+            @NonNull final StructNlMsgHdr header,
+            @NonNull final InetAddress destAddress,
+            long spi,
+            short proto) {
+        super(header);
+
+        final int family =
+                destAddress instanceof Inet4Address ? OsConstants.AF_INET : OsConstants.AF_INET6;
+        mXfrmUsersaId = new StructXfrmUsersaId(destAddress, spi, family, proto);
+    }
+
+    @Override
+    protected void packPayload(@NonNull final ByteBuffer byteBuffer) {
+        mXfrmUsersaId.writeToByteBuffer(byteBuffer);
+    }
+
+    /**
+     * Parse XFRM_MSG_GETSA message from ByteBuffer.
+     *
+     * <p>This method should be called from NetlinkMessage#parse(ByteBuffer, int) for generic
+     * message validation and processing
+     *
+     * @param nlmsghdr netlink message header.
+     * @param byteBuffer the ByteBuffer instance that wraps the raw netlink message bytes. MUST be
+     *                   host order
+     */
+    @Nullable
+    static XfrmNetlinkGetSaMessage parseInternal(
+            @NonNull final StructNlMsgHdr nlmsghdr, @NonNull final ByteBuffer byteBuffer) {
+        final StructXfrmUsersaId xfrmUsersaId = Struct.parse(StructXfrmUsersaId.class, byteBuffer);
+        if (xfrmUsersaId == null) {
+            return null;
+        }
+
+        // Attributes not supported. Don't bother handling them.
+
+        return new XfrmNetlinkGetSaMessage(nlmsghdr, xfrmUsersaId);
+    }
+
+    /** A convenient method to create a XFRM_MSG_GETSA message. */
+    public static byte[] newXfrmNetlinkGetSaMessage(
+            @NonNull final InetAddress destAddress, long spi, short proto) {
+        final int payloadLen = StructXfrmUsersaId.STRUCT_SIZE;
+
+        final StructNlMsgHdr nlmsghdr =
+                new StructNlMsgHdr(payloadLen, XFRM_MSG_GETSA, NLM_F_REQUEST, 0);
+        final XfrmNetlinkGetSaMessage message =
+                new XfrmNetlinkGetSaMessage(nlmsghdr, destAddress, spi, proto);
+
+        final ByteBuffer byteBuffer = newNlMsgByteBuffer(payloadLen);
+        message.pack(byteBuffer);
+
+        return byteBuffer.array();
+    }
+
+    public StructXfrmUsersaId getStructXfrmUsersaId() {
+        return mXfrmUsersaId;
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/netlink/xfrm/XfrmNetlinkMessage.java b/staticlibs/device/com/android/net/module/util/netlink/xfrm/XfrmNetlinkMessage.java
new file mode 100644
index 0000000..72d02d4
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/netlink/xfrm/XfrmNetlinkMessage.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util.netlink.xfrm;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.net.module.util.netlink.NetlinkMessage;
+import com.android.net.module.util.netlink.StructNlMsgHdr;
+
+import java.math.BigInteger;
+import java.nio.ByteBuffer;
+
+/** Base calss for XFRM netlink messages */
+// Developer notes: The Linux kernel includes a number of XFRM structs that are not standard netlink
+// attributes (e.g., xfrm_usersa_id). These structs are unlikely to change size, so this XFRM
+// netlink message implementation assumes their sizes will remain stable. If any non-attribute
+// struct size changes, it should be caught by CTS and then developers should add
+// kernel-version-based behvaiours.
+public abstract class XfrmNetlinkMessage extends NetlinkMessage {
+    // TODO: b/312498032 Remove it when OsConstants.IPPROTO_ESP is stable
+    public static final int IPPROTO_ESP = 50;
+    // TODO: b/312498032 Remove it when OsConstants.NETLINK_XFRM is stable
+    public static final int NETLINK_XFRM = 6;
+
+    /* see include/uapi/linux/xfrm.h */
+    public static final short XFRM_MSG_NEWSA = 16;
+    public static final short XFRM_MSG_GETSA = 18;
+
+    public static final int XFRM_MODE_TRANSPORT = 0;
+    public static final int XFRM_MODE_TUNNEL = 1;
+
+    public static final short XFRMA_REPLAY_VAL = 10;
+    public static final short XFRMA_REPLAY_ESN_VAL = 23;
+
+    public static final BigInteger XFRM_INF = new BigInteger("FFFFFFFFFFFFFFFF", 16);
+
+    public XfrmNetlinkMessage(@NonNull final StructNlMsgHdr header) {
+        super(header);
+    }
+
+    /**
+     * Parse XFRM message from ByteBuffer.
+     *
+     * <p>This method should be called from NetlinkMessage#parse(ByteBuffer, int) for generic
+     * message validation and processing
+     *
+     * @param nlmsghdr netlink message header.
+     * @param byteBuffer the ByteBuffer instance that wraps the raw netlink message bytes. MUST be
+     *                   host order
+     */
+    @Nullable
+    public static XfrmNetlinkMessage parseXfrmInternal(
+            @NonNull final StructNlMsgHdr nlmsghdr, @NonNull final ByteBuffer byteBuffer) {
+        switch (nlmsghdr.nlmsg_type) {
+            case XFRM_MSG_NEWSA:
+                return XfrmNetlinkNewSaMessage.parseInternal(nlmsghdr, byteBuffer);
+            case XFRM_MSG_GETSA:
+                return XfrmNetlinkGetSaMessage.parseInternal(nlmsghdr, byteBuffer);
+            default:
+                return null;
+        }
+    }
+
+    protected abstract void packPayload(@NonNull final ByteBuffer byteBuffer);
+
+    /** Write a XFRM message to {@link ByteBuffer}. */
+    public void pack(@NonNull final ByteBuffer byteBuffer) {
+        getHeader().pack(byteBuffer);
+        packPayload(byteBuffer);
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/netlink/xfrm/XfrmNetlinkNewSaMessage.java b/staticlibs/device/com/android/net/module/util/netlink/xfrm/XfrmNetlinkNewSaMessage.java
new file mode 100644
index 0000000..2f374ba
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/netlink/xfrm/XfrmNetlinkNewSaMessage.java
@@ -0,0 +1,157 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util.netlink.xfrm;
+
+import static com.android.net.module.util.netlink.xfrm.XfrmNetlinkMessage.XFRMA_REPLAY_ESN_VAL;
+
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.net.module.util.Struct;
+import com.android.net.module.util.netlink.StructNlAttr;
+import com.android.net.module.util.netlink.StructNlMsgHdr;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+/**
+ * A NetlinkMessage subclass for XFRM_MSG_NEWSA messages.
+ *
+ * <p>see also: &lt;linux_src&gt;/include/uapi/linux/xfrm.h
+ *
+ * <p>XFRM_MSG_NEWSA syntax
+ *
+ * <ul>
+ *   <li>TLV: xfrm_usersa_info
+ *   <li>Attributes: XFRMA_ALG_CRYPT, XFRMA_ALG_AUTH, XFRMA_OUTPUT_MARK, XFRMA_IF_ID,
+ *       XFRMA_REPLAY_ESN_VAL,XFRMA_REPLAY_VAL
+ * </ul>
+ *
+ * @hide
+ */
+public class XfrmNetlinkNewSaMessage extends XfrmNetlinkMessage {
+    private static final String TAG = XfrmNetlinkNewSaMessage.class.getSimpleName();
+    @NonNull private final StructXfrmUsersaInfo mXfrmUsersaInfo;
+
+    @NonNull private final StructXfrmReplayStateEsn mXfrmReplayStateEsn;
+
+    private XfrmNetlinkNewSaMessage(
+            @NonNull final StructNlMsgHdr header,
+            @NonNull final StructXfrmUsersaInfo xfrmUsersaInfo,
+            @NonNull final StructXfrmReplayStateEsn xfrmReplayStateEsn) {
+        super(header);
+        mXfrmUsersaInfo = xfrmUsersaInfo;
+        mXfrmReplayStateEsn = xfrmReplayStateEsn;
+    }
+
+    @Override
+    protected void packPayload(@NonNull final ByteBuffer byteBuffer) {
+        mXfrmUsersaInfo.writeToByteBuffer(byteBuffer);
+        if (mXfrmReplayStateEsn != null) {
+            mXfrmReplayStateEsn.writeToByteBuffer(byteBuffer);
+        }
+    }
+
+    /**
+     * Parse XFRM_MSG_NEWSA message from ByteBuffer.
+     *
+     * <p>This method should be called from NetlinkMessage#parse(ByteBuffer, int) for generic
+     * message validation and processing
+     *
+     * @param nlmsghdr netlink message header.
+     * @param byteBuffer the ByteBuffer instance that wraps the raw netlink message bytes. MUST be
+     *                   host order
+     */
+    @Nullable
+    static XfrmNetlinkNewSaMessage parseInternal(
+            @NonNull final StructNlMsgHdr nlmsghdr, @NonNull final ByteBuffer byteBuffer) {
+        final StructXfrmUsersaInfo xfrmUsersaInfo =
+                Struct.parse(StructXfrmUsersaInfo.class, byteBuffer);
+        if (xfrmUsersaInfo == null) {
+            Log.d(TAG, "parse: fail to parse xfrmUsersaInfo");
+            return null;
+        }
+
+        StructXfrmReplayStateEsn xfrmReplayStateEsn = null;
+
+        final int payloadLen = nlmsghdr.nlmsg_len - StructNlMsgHdr.STRUCT_SIZE;
+        int parsedLength = StructXfrmUsersaInfo.STRUCT_SIZE;
+        while (parsedLength < payloadLen) {
+            final StructNlAttr attr = StructNlAttr.parse(byteBuffer);
+
+            if (attr == null) {
+                Log.d(TAG, "parse: fail to parse netlink attributes");
+                return null;
+            }
+
+            final ByteBuffer attrValueBuff = ByteBuffer.wrap(attr.nla_value);
+            attrValueBuff.order(ByteOrder.nativeOrder());
+
+            if (attr.nla_type == XFRMA_REPLAY_ESN_VAL) {
+                xfrmReplayStateEsn = StructXfrmReplayStateEsn.parse(attrValueBuff);
+            }
+
+            parsedLength += attr.nla_len;
+        }
+
+        // TODO: Add the support of XFRMA_REPLAY_VAL
+
+        if (xfrmReplayStateEsn == null) {
+            Log.d(TAG, "parse: xfrmReplayStateEsn not found");
+            return null;
+        }
+
+        final XfrmNetlinkNewSaMessage msg =
+                new XfrmNetlinkNewSaMessage(nlmsghdr, xfrmUsersaInfo, xfrmReplayStateEsn);
+
+        return msg;
+    }
+
+    /** Return the TX sequence number in unisgned long */
+    public long getTxSequenceNumber() {
+        return mXfrmReplayStateEsn.getTxSequenceNumber();
+    }
+
+    /** Return the RX sequence number in unisgned long */
+    public long getRxSequenceNumber() {
+        return mXfrmReplayStateEsn.getRxSequenceNumber();
+    }
+
+    /** Return the bitmap */
+    public byte[] getBitmap() {
+        return mXfrmReplayStateEsn.getBitmap();
+    }
+
+    /** Return the packet count in unsigned long */
+    public long getPacketCount() {
+        // It is safe because "packets" is a 64-bit value
+        return mXfrmUsersaInfo.getCurrentLifetime().packets.longValue();
+    }
+
+    /** Return the byte count in unsigned long */
+    public long getByteCount() {
+        // It is safe because "bytes" is a 64-bit value
+        return mXfrmUsersaInfo.getCurrentLifetime().bytes.longValue();
+    }
+
+    /** Return the xfrm_usersa_info */
+    public StructXfrmUsersaInfo getXfrmUsersaInfo() {
+        return mXfrmUsersaInfo;
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/structs/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/DnsSvcbPacket.java b/staticlibs/framework/com/android/net/module/util/DnsSvcbPacket.java
index c7ed3e6..d298599 100644
--- a/staticlibs/framework/com/android/net/module/util/DnsSvcbPacket.java
+++ b/staticlibs/framework/com/android/net/module/util/DnsSvcbPacket.java
@@ -21,7 +21,6 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
-import android.text.TextUtils;
 import android.util.Log;
 
 import java.net.InetAddress;
@@ -29,7 +28,6 @@
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
-import java.util.StringJoiner;
 
 /**
  * A class for a DNS SVCB response packet.
@@ -159,16 +157,6 @@
         return out;
     }
 
-    @Override
-    public String toString() {
-        final StringJoiner out = new StringJoiner(" ");
-        out.add("QUERY: [" + TextUtils.join(", ", mRecords[QDSECTION]) + "]");
-        out.add("ANSWER: [" + TextUtils.join(", ", mRecords[ANSECTION]) + "]");
-        out.add("AUTHORITY: [" + TextUtils.join(", ", mRecords[NSSECTION]) + "]");
-        out.add("ADDITIONAL: [" + TextUtils.join(", ", mRecords[ARSECTION]) + "]");
-        return out.toString();
-    }
-
     /**
      * Creates a DnsSvcbPacket object from the given wire-format DNS answer.
      */
diff --git a/staticlibs/framework/com/android/net/module/util/DnsSvcbRecord.java b/staticlibs/framework/com/android/net/module/util/DnsSvcbRecord.java
index 669725c..935cdf6 100644
--- a/staticlibs/framework/com/android/net/module/util/DnsSvcbRecord.java
+++ b/staticlibs/framework/com/android/net/module/util/DnsSvcbRecord.java
@@ -230,7 +230,7 @@
     /**
      * The base class for all SvcParam.
      */
-    private abstract static class SvcParam {
+    private abstract static class SvcParam<T> {
         private final int mKey;
 
         SvcParam(int key) {
@@ -240,9 +240,11 @@
         int getKey() {
             return mKey;
         }
+
+        abstract T getValue();
     }
 
-    private static class SvcParamMandatory extends SvcParam {
+    private static class SvcParamMandatory extends SvcParam<short[]> {
         private final short[] mValue;
 
         private SvcParamMandatory(@NonNull ByteBuffer buf) throws BufferUnderflowException,
@@ -258,6 +260,12 @@
         }
 
         @Override
+        short[] getValue() {
+            /* Not yet implemented */
+            return null;
+        }
+
+        @Override
         public String toString() {
             final StringJoiner valueJoiner = new StringJoiner(",");
             for (short key : mValue) {
@@ -267,7 +275,7 @@
         }
     }
 
-    private static class SvcParamAlpn extends SvcParam {
+    private static class SvcParamAlpn extends SvcParam<List<String>> {
         private final List<String> mValue;
 
         SvcParamAlpn(@NonNull ByteBuffer buf) throws BufferUnderflowException, ParseException {
@@ -281,6 +289,7 @@
             }
         }
 
+        @Override
         List<String> getValue() {
             return Collections.unmodifiableList(mValue);
         }
@@ -291,7 +300,7 @@
         }
     }
 
-    private static class SvcParamNoDefaultAlpn extends SvcParam {
+    private static class SvcParamNoDefaultAlpn extends SvcParam<Void> {
         SvcParamNoDefaultAlpn(@NonNull ByteBuffer buf) throws BufferUnderflowException,
                 ParseException {
             super(KEY_NO_DEFAULT_ALPN);
@@ -303,12 +312,17 @@
         }
 
         @Override
+        Void getValue() {
+            return null;
+        }
+
+        @Override
         public String toString() {
             return toKeyName(getKey());
         }
     }
 
-    private static class SvcParamPort extends SvcParam {
+    private static class SvcParamPort extends SvcParam<Integer> {
         private final int mValue;
 
         SvcParamPort(@NonNull ByteBuffer buf) throws BufferUnderflowException, ParseException {
@@ -321,7 +335,8 @@
             mValue = Short.toUnsignedInt(buf.getShort());
         }
 
-        int getValue() {
+        @Override
+        Integer getValue() {
             return mValue;
         }
 
@@ -331,7 +346,7 @@
         }
     }
 
-    private static class SvcParamIpHint extends SvcParam {
+    private static class SvcParamIpHint extends SvcParam<List<InetAddress>> {
         private final List<InetAddress> mValue;
 
         private SvcParamIpHint(int key, @NonNull ByteBuffer buf, int addrLen) throws
@@ -346,6 +361,7 @@
             }
         }
 
+        @Override
         List<InetAddress> getValue() {
             return Collections.unmodifiableList(mValue);
         }
@@ -378,7 +394,7 @@
         }
     }
 
-    private static class SvcParamDohPath extends SvcParam {
+    private static class SvcParamDohPath extends SvcParam<String> {
         private final String mValue;
 
         SvcParamDohPath(@NonNull ByteBuffer buf) throws BufferUnderflowException, ParseException {
@@ -390,6 +406,7 @@
             mValue = new String(value, StandardCharsets.UTF_8);
         }
 
+        @Override
         String getValue() {
             return mValue;
         }
@@ -401,7 +418,7 @@
     }
 
     // For other unrecognized and unimplemented SvcParams, they are stored as SvcParamGeneric.
-    private static class SvcParamGeneric extends SvcParam {
+    private static class SvcParamGeneric extends SvcParam<byte[]> {
         private final byte[] mValue;
 
         SvcParamGeneric(int key, @NonNull ByteBuffer buf) throws BufferUnderflowException,
@@ -414,6 +431,12 @@
         }
 
         @Override
+        byte[] getValue() {
+            /* Not yet implemented */
+            return null;
+        }
+
+        @Override
         public String toString() {
             final StringBuilder out = new StringBuilder();
             out.append(toKeyName(getKey()));
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/NetworkStackConstants.java b/staticlibs/framework/com/android/net/module/util/NetworkStackConstants.java
index f9895c6..7c4abe0 100644
--- a/staticlibs/framework/com/android/net/module/util/NetworkStackConstants.java
+++ b/staticlibs/framework/com/android/net/module/util/NetworkStackConstants.java
@@ -189,6 +189,7 @@
 
     public static final byte PIO_FLAG_ON_LINK = (byte) (1 << 7);
     public static final byte PIO_FLAG_AUTONOMOUS = (byte) (1 << 6);
+    public static final byte PIO_FLAG_DHCPV6_PD_PREFERRED = (byte) (1 << 4);
 
     /**
      * TCP constants.
diff --git a/staticlibs/framework/com/android/net/module/util/PermissionUtils.java b/staticlibs/framework/com/android/net/module/util/PermissionUtils.java
index d5b4c90..f167d3d 100644
--- a/staticlibs/framework/com/android/net/module/util/PermissionUtils.java
+++ b/staticlibs/framework/com/android/net/module/util/PermissionUtils.java
@@ -21,7 +21,6 @@
 import static android.Manifest.permission.NETWORK_STACK;
 import static android.content.pm.PackageInfo.REQUESTED_PERMISSION_GRANTED;
 import static android.content.pm.PackageManager.PERMISSION_GRANTED;
-import static android.content.pm.PermissionInfo.PROTECTION_SIGNATURE;
 import static android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK;
 
 import android.annotation.NonNull;
@@ -29,8 +28,8 @@
 import android.content.Context;
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
-import android.content.pm.PermissionInfo;
 import android.os.Binder;
+import android.os.UserHandle;
 
 import java.io.PrintWriter;
 import java.util.ArrayList;
@@ -57,25 +56,6 @@
     }
 
     /**
-     * Return true if the permission has system signature.
-     */
-    public static boolean isSystemSignaturePermission(@NonNull Context context,
-            @NonNull String permission) {
-        try {
-            PermissionInfo permissionInfo = context.getPackageManager().getPermissionInfo(
-                    permission, 0 /* flags */);
-            if (permissionInfo == null) {
-                return false;
-            }
-            return "android".equals(permissionInfo.packageName)
-                    && permissionInfo.getProtection() == PROTECTION_SIGNATURE;
-        } catch (PackageManager.NameNotFoundException ignored) {
-            // Ignored the NameNotFoundException and return false
-        }
-        return false;
-    }
-
-    /**
      * Return true if the context has one of give permission that is allowed
      * for a particular process and user ID running in the system.
      */
@@ -205,4 +185,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/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/BpfRingbufTest.cpp b/staticlibs/native/bpf_headers/BpfRingbufTest.cpp
index e4de812..e81fb92 100644
--- a/staticlibs/native/bpf_headers/BpfRingbufTest.cpp
+++ b/staticlibs/native/bpf_headers/BpfRingbufTest.cpp
@@ -74,11 +74,27 @@
     ASSERT_RESULT_OK(result);
     EXPECT_TRUE(result.value()->isEmpty());
 
+    struct timespec t1, t2;
+    EXPECT_EQ(0, clock_gettime(CLOCK_MONOTONIC, &t1));
+    EXPECT_FALSE(result.value()->wait(1000 /*ms*/));  // false because wait should timeout
+    EXPECT_EQ(0, clock_gettime(CLOCK_MONOTONIC, &t2));
+    long long time1 = t1.tv_sec * 1000000000LL + t1.tv_nsec;
+    long long time2 = t2.tv_sec * 1000000000LL + t2.tv_nsec;
+    EXPECT_GE(time2 - time1, 1000000000 /*ns*/);  // 1000 ms as ns
+
     for (int i = 0; i < n; i++) {
       RunProgram();
     }
 
     EXPECT_FALSE(result.value()->isEmpty());
+
+    EXPECT_EQ(0, clock_gettime(CLOCK_MONOTONIC, &t1));
+    EXPECT_TRUE(result.value()->wait());
+    EXPECT_EQ(0, clock_gettime(CLOCK_MONOTONIC, &t2));
+    time1 = t1.tv_sec * 1000000000LL + t1.tv_nsec;
+    time2 = t2.tv_sec * 1000000000LL + t2.tv_nsec;
+    EXPECT_LE(time2 - time1, 1000000 /*ns*/);  // in x86 CF testing < 5000 ns
+
     EXPECT_THAT(result.value()->ConsumeAll(callback), HasValue(n));
     EXPECT_TRUE(result.value()->isEmpty());
     EXPECT_EQ(output, TEST_RINGBUF_MAGIC_NUM);
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_headers/include/bpf/BpfRingbuf.h b/staticlibs/native/bpf_headers/include/bpf/BpfRingbuf.h
index 9aff790..d716358 100644
--- a/staticlibs/native/bpf_headers/include/bpf/BpfRingbuf.h
+++ b/staticlibs/native/bpf_headers/include/bpf/BpfRingbuf.h
@@ -19,6 +19,7 @@
 #include <android-base/result.h>
 #include <android-base/unique_fd.h>
 #include <linux/bpf.h>
+#include <poll.h>
 #include <sys/mman.h>
 #include <utils/Log.h>
 
@@ -41,6 +42,9 @@
 
   bool isEmpty(void);
 
+  // returns !isEmpty() for convenience
+  bool wait(int timeout_ms = -1);
+
  protected:
   // Non-initializing constructor, used by Create.
   BpfRingbufBase(size_t value_size) : mValueSize(value_size) {}
@@ -200,12 +204,21 @@
 }
 
 inline bool BpfRingbufBase::isEmpty(void) {
-  uint32_t prod_pos = mProducerPos->load(std::memory_order_acquire);
-  // Only userspace writes to mConsumerPos, so no need to use std::memory_order_acquire
+  uint32_t prod_pos = mProducerPos->load(std::memory_order_relaxed);
   uint64_t cons_pos = mConsumerPos->load(std::memory_order_relaxed);
   return (cons_pos & 0xFFFFFFFF) == prod_pos;
 }
 
+inline bool BpfRingbufBase::wait(int timeout_ms) {
+  // possible optimization: if (!isEmpty()) return true;
+  struct pollfd pfd = {  // 1-element array
+    .fd = mRingFd.get(),
+    .events = POLLIN,
+  };
+  (void)poll(&pfd, 1, timeout_ms);  // 'best effort' poll
+  return !isEmpty();
+}
+
 inline base::Result<int> BpfRingbufBase::ConsumeAll(
     const std::function<void(const void*)>& callback) {
   int64_t count = 0;
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 65b3b09..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"],
 }
 
@@ -21,7 +22,7 @@
     sdk_version: "system_current",
     min_sdk_version: "30",
     static_libs: [
-        "netd_aidl_interface-V13-java",
+        "netd_aidl_interface-V14-java",
     ],
     apex_available: [
         "//apex_available:platform", // used from services.net
@@ -44,7 +45,7 @@
 cc_library_static {
     name: "netd_aidl_interface-lateststable-ndk",
     whole_static_libs: [
-        "netd_aidl_interface-V13-ndk",
+        "netd_aidl_interface-V14-ndk",
     ],
     apex_available: [
         "com.android.resolv",
@@ -55,12 +56,12 @@
 
 cc_defaults {
     name: "netd_aidl_interface_lateststable_cpp_static",
-    static_libs: ["netd_aidl_interface-V13-cpp"],
+    static_libs: ["netd_aidl_interface-V14-cpp"],
 }
 
 cc_defaults {
     name: "netd_aidl_interface_lateststable_cpp_shared",
-    shared_libs: ["netd_aidl_interface-V13-cpp"],
+    shared_libs: ["netd_aidl_interface-V14-cpp"],
 }
 
 aidl_interface {
@@ -162,8 +163,13 @@
             version: "13",
             imports: [],
         },
+        {
+            version: "14",
+            imports: [],
+        },
 
     ],
+    frozen: true,
 
 }
 
@@ -220,19 +226,6 @@
 
 }
 
-java_library {
-    name: "mdns_aidl_interface-lateststable-java",
-    sdk_version: "module_current",
-    min_sdk_version: "30",
-    static_libs: [
-        "mdns_aidl_interface-V1-java",
-    ],
-    apex_available: [
-        "//apex_available:platform",
-        "com.android.tethering",
-    ],
-}
-
 aidl_interface {
     name: "mdns_aidl_interface",
     local_include_dir: "binder",
@@ -249,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/aidl_api/netd_aidl_interface/14/.hash b/staticlibs/netd/aidl_api/netd_aidl_interface/14/.hash
new file mode 100644
index 0000000..0bf7bde
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/14/.hash
@@ -0,0 +1 @@
+50bce96bc8d5811ed952950df30ec503f8a561ed
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/INetd.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/INetd.aidl
new file mode 100644
index 0000000..8ccefb2
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/INetd.aidl
@@ -0,0 +1,259 @@
+/**
+ * Copyright (c) 2016, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+/* @hide */
+interface INetd {
+  boolean isAlive();
+  /**
+   * @deprecated unimplemented on T+.
+   */
+  boolean firewallReplaceUidChain(in @utf8InCpp String chainName, boolean isAllowlist, in int[] uids);
+  boolean bandwidthEnableDataSaver(boolean enable);
+  /**
+   * @deprecated use networkCreate() instead.
+   */
+  void networkCreatePhysical(int netId, int permission);
+  /**
+   * @deprecated use networkCreate() instead.
+   */
+  void networkCreateVpn(int netId, boolean secure);
+  void networkDestroy(int netId);
+  void networkAddInterface(int netId, in @utf8InCpp String iface);
+  void networkRemoveInterface(int netId, in @utf8InCpp String iface);
+  void networkAddUidRanges(int netId, in android.net.UidRangeParcel[] uidRanges);
+  void networkRemoveUidRanges(int netId, in android.net.UidRangeParcel[] uidRanges);
+  void networkRejectNonSecureVpn(boolean add, in android.net.UidRangeParcel[] uidRanges);
+  void socketDestroy(in android.net.UidRangeParcel[] uidRanges, in int[] exemptUids);
+  boolean tetherApplyDnsInterfaces();
+  android.net.TetherStatsParcel[] tetherGetStats();
+  void interfaceAddAddress(in @utf8InCpp String ifName, in @utf8InCpp String addrString, int prefixLength);
+  void interfaceDelAddress(in @utf8InCpp String ifName, in @utf8InCpp String addrString, int prefixLength);
+  @utf8InCpp String getProcSysNet(int ipversion, int which, in @utf8InCpp String ifname, in @utf8InCpp String parameter);
+  void setProcSysNet(int ipversion, int which, in @utf8InCpp String ifname, in @utf8InCpp String parameter, in @utf8InCpp String value);
+  void ipSecSetEncapSocketOwner(in ParcelFileDescriptor socket, int newUid);
+  int ipSecAllocateSpi(int transformId, in @utf8InCpp String sourceAddress, in @utf8InCpp String destinationAddress, int spi);
+  void ipSecAddSecurityAssociation(int transformId, int mode, in @utf8InCpp String sourceAddress, in @utf8InCpp String destinationAddress, int underlyingNetId, int spi, int markValue, int markMask, in @utf8InCpp String authAlgo, in byte[] authKey, in int authTruncBits, in @utf8InCpp String cryptAlgo, in byte[] cryptKey, in int cryptTruncBits, in @utf8InCpp String aeadAlgo, in byte[] aeadKey, in int aeadIcvBits, int encapType, int encapLocalPort, int encapRemotePort, int interfaceId);
+  void ipSecDeleteSecurityAssociation(int transformId, in @utf8InCpp String sourceAddress, in @utf8InCpp String destinationAddress, int spi, int markValue, int markMask, int interfaceId);
+  void ipSecApplyTransportModeTransform(in ParcelFileDescriptor socket, int transformId, int direction, in @utf8InCpp String sourceAddress, in @utf8InCpp String destinationAddress, int spi);
+  void ipSecRemoveTransportModeTransform(in ParcelFileDescriptor socket);
+  void ipSecAddSecurityPolicy(int transformId, int selAddrFamily, int direction, in @utf8InCpp String tmplSrcAddress, in @utf8InCpp String tmplDstAddress, int spi, int markValue, int markMask, int interfaceId);
+  void ipSecUpdateSecurityPolicy(int transformId, int selAddrFamily, int direction, in @utf8InCpp String tmplSrcAddress, in @utf8InCpp String tmplDstAddress, int spi, int markValue, int markMask, int interfaceId);
+  void ipSecDeleteSecurityPolicy(int transformId, int selAddrFamily, int direction, int markValue, int markMask, int interfaceId);
+  void ipSecAddTunnelInterface(in @utf8InCpp String deviceName, in @utf8InCpp String localAddress, in @utf8InCpp String remoteAddress, int iKey, int oKey, int interfaceId);
+  void ipSecUpdateTunnelInterface(in @utf8InCpp String deviceName, in @utf8InCpp String localAddress, in @utf8InCpp String remoteAddress, int iKey, int oKey, int interfaceId);
+  void ipSecRemoveTunnelInterface(in @utf8InCpp String deviceName);
+  void wakeupAddInterface(in @utf8InCpp String ifName, in @utf8InCpp String prefix, int mark, int mask);
+  void wakeupDelInterface(in @utf8InCpp String ifName, in @utf8InCpp String prefix, int mark, int mask);
+  void setIPv6AddrGenMode(in @utf8InCpp String ifName, int mode);
+  void idletimerAddInterface(in @utf8InCpp String ifName, int timeout, in @utf8InCpp String classLabel);
+  void idletimerRemoveInterface(in @utf8InCpp String ifName, int timeout, in @utf8InCpp String classLabel);
+  void strictUidCleartextPenalty(int uid, int policyPenalty);
+  /**
+   * @deprecated This method has no effect and throws UnsupportedOperationException. The clatd control plane moved to the mainline module starting in T. See ClatCoordinator.
+   */
+  @utf8InCpp String clatdStart(in @utf8InCpp String ifName, in @utf8InCpp String nat64Prefix);
+  /**
+   * @deprecated This method has no effect and throws UnsupportedOperationException. The clatd control plane moved to the mainline module starting in T. See ClatCoordinator.
+   */
+  void clatdStop(in @utf8InCpp String ifName);
+  boolean ipfwdEnabled();
+  @utf8InCpp String[] ipfwdGetRequesterList();
+  void ipfwdEnableForwarding(in @utf8InCpp String requester);
+  void ipfwdDisableForwarding(in @utf8InCpp String requester);
+  void ipfwdAddInterfaceForward(in @utf8InCpp String fromIface, in @utf8InCpp String toIface);
+  void ipfwdRemoveInterfaceForward(in @utf8InCpp String fromIface, in @utf8InCpp String toIface);
+  void bandwidthSetInterfaceQuota(in @utf8InCpp String ifName, long bytes);
+  void bandwidthRemoveInterfaceQuota(in @utf8InCpp String ifName);
+  void bandwidthSetInterfaceAlert(in @utf8InCpp String ifName, long bytes);
+  void bandwidthRemoveInterfaceAlert(in @utf8InCpp String ifName);
+  void bandwidthSetGlobalAlert(long bytes);
+  /**
+   * @deprecated unimplemented on T+.
+   */
+  void bandwidthAddNaughtyApp(int uid);
+  /**
+   * @deprecated unimplemented on T+.
+   */
+  void bandwidthRemoveNaughtyApp(int uid);
+  /**
+   * @deprecated unimplemented on T+.
+   */
+  void bandwidthAddNiceApp(int uid);
+  /**
+   * @deprecated unimplemented on T+.
+   */
+  void bandwidthRemoveNiceApp(int uid);
+  void tetherStart(in @utf8InCpp String[] dhcpRanges);
+  void tetherStop();
+  boolean tetherIsEnabled();
+  void tetherInterfaceAdd(in @utf8InCpp String ifName);
+  void tetherInterfaceRemove(in @utf8InCpp String ifName);
+  @utf8InCpp String[] tetherInterfaceList();
+  void tetherDnsSet(int netId, in @utf8InCpp String[] dnsAddrs);
+  @utf8InCpp String[] tetherDnsList();
+  void networkAddRoute(int netId, in @utf8InCpp String ifName, in @utf8InCpp String destination, in @utf8InCpp String nextHop);
+  void networkRemoveRoute(int netId, in @utf8InCpp String ifName, in @utf8InCpp String destination, in @utf8InCpp String nextHop);
+  void networkAddLegacyRoute(int netId, in @utf8InCpp String ifName, in @utf8InCpp String destination, in @utf8InCpp String nextHop, int uid);
+  void networkRemoveLegacyRoute(int netId, in @utf8InCpp String ifName, in @utf8InCpp String destination, in @utf8InCpp String nextHop, int uid);
+  int networkGetDefault();
+  void networkSetDefault(int netId);
+  void networkClearDefault();
+  void networkSetPermissionForNetwork(int netId, int permission);
+  void networkSetPermissionForUser(int permission, in int[] uids);
+  void networkClearPermissionForUser(in int[] uids);
+  /**
+   * @deprecated unimplemented on T+.
+   */
+  void trafficSetNetPermForUids(int permission, in int[] uids);
+  void networkSetProtectAllow(int uid);
+  void networkSetProtectDeny(int uid);
+  boolean networkCanProtect(int uid);
+  void firewallSetFirewallType(int firewalltype);
+  void firewallSetInterfaceRule(in @utf8InCpp String ifName, int firewallRule);
+  /**
+   * @deprecated unimplemented on T+.
+   */
+  void firewallSetUidRule(int childChain, int uid, int firewallRule);
+  /**
+   * @deprecated unimplemented on T+.
+   */
+  void firewallEnableChildChain(int childChain, boolean enable);
+  @utf8InCpp String[] interfaceGetList();
+  android.net.InterfaceConfigurationParcel interfaceGetCfg(in @utf8InCpp String ifName);
+  void interfaceSetCfg(in android.net.InterfaceConfigurationParcel cfg);
+  void interfaceSetIPv6PrivacyExtensions(in @utf8InCpp String ifName, boolean enable);
+  void interfaceClearAddrs(in @utf8InCpp String ifName);
+  void interfaceSetEnableIPv6(in @utf8InCpp String ifName, boolean enable);
+  void interfaceSetMtu(in @utf8InCpp String ifName, int mtu);
+  void tetherAddForward(in @utf8InCpp String intIface, in @utf8InCpp String extIface);
+  void tetherRemoveForward(in @utf8InCpp String intIface, in @utf8InCpp String extIface);
+  void setTcpRWmemorySize(in @utf8InCpp String rmemValues, in @utf8InCpp String wmemValues);
+  void registerUnsolicitedEventListener(android.net.INetdUnsolicitedEventListener listener);
+  /**
+   * @deprecated unimplemented on T+.
+   */
+  void firewallAddUidInterfaceRules(in @utf8InCpp String ifName, in int[] uids);
+  /**
+   * @deprecated unimplemented on T+.
+   */
+  void firewallRemoveUidInterfaceRules(in int[] uids);
+  /**
+   * @deprecated unimplemented on T+.
+   */
+  void trafficSwapActiveStatsMap();
+  IBinder getOemNetd();
+  void tetherStartWithConfiguration(in android.net.TetherConfigParcel config);
+  android.net.MarkMaskParcel getFwmarkForNetwork(int netId);
+  void networkAddRouteParcel(int netId, in android.net.RouteInfoParcel routeInfo);
+  void networkUpdateRouteParcel(int netId, in android.net.RouteInfoParcel routeInfo);
+  void networkRemoveRouteParcel(int netId, in android.net.RouteInfoParcel routeInfo);
+  /**
+   * @deprecated This method has no effect and throws UnsupportedOperationException. The mainline module accesses the BPF map directly starting in S. See BpfCoordinator.
+   */
+  void tetherOffloadRuleAdd(in android.net.TetherOffloadRuleParcel rule);
+  /**
+   * @deprecated This method has no effect and throws UnsupportedOperationException. The mainline module accesses the BPF map directly starting in S. See BpfCoordinator.
+   */
+  void tetherOffloadRuleRemove(in android.net.TetherOffloadRuleParcel rule);
+  /**
+   * @deprecated This method has no effect and throws UnsupportedOperationException. The mainline module accesses the BPF map directly starting in S. See BpfCoordinator.
+   */
+  android.net.TetherStatsParcel[] tetherOffloadGetStats();
+  /**
+   * @deprecated This method has no effect and throws UnsupportedOperationException. The mainline module accesses the BPF map directly starting in S. See BpfCoordinator.
+   */
+  void tetherOffloadSetInterfaceQuota(int ifIndex, long quotaBytes);
+  /**
+   * @deprecated This method has no effect and throws UnsupportedOperationException. The mainline module accesses the BPF map directly starting in S. See BpfCoordinator.
+   */
+  android.net.TetherStatsParcel tetherOffloadGetAndClearStats(int ifIndex);
+  void networkCreate(in android.net.NativeNetworkConfig config);
+  void networkAddUidRangesParcel(in android.net.netd.aidl.NativeUidRangeConfig uidRangesConfig);
+  void networkRemoveUidRangesParcel(in android.net.netd.aidl.NativeUidRangeConfig uidRangesConfig);
+  void ipSecMigrate(in android.net.IpSecMigrateInfoParcel migrateInfo);
+  void setNetworkAllowlist(in android.net.netd.aidl.NativeUidRangeConfig[] allowedNetworks);
+  const int IPV4 = 4;
+  const int IPV6 = 6;
+  const int CONF = 1;
+  const int NEIGH = 2;
+  const String IPSEC_INTERFACE_PREFIX = "ipsec";
+  const int IPV6_ADDR_GEN_MODE_EUI64 = 0;
+  const int IPV6_ADDR_GEN_MODE_NONE = 1;
+  const int IPV6_ADDR_GEN_MODE_STABLE_PRIVACY = 2;
+  const int IPV6_ADDR_GEN_MODE_RANDOM = 3;
+  const int IPV6_ADDR_GEN_MODE_DEFAULT = 0;
+  const int PENALTY_POLICY_ACCEPT = 1;
+  const int PENALTY_POLICY_LOG = 2;
+  const int PENALTY_POLICY_REJECT = 3;
+  const int CLAT_MARK = 0xdeadc1a7;
+  const int LOCAL_NET_ID = 99;
+  const int DUMMY_NET_ID = 51;
+  const int UNREACHABLE_NET_ID = 52;
+  const String NEXTHOP_NONE = "";
+  const String NEXTHOP_UNREACHABLE = "unreachable";
+  const String NEXTHOP_THROW = "throw";
+  const int PERMISSION_NONE = 0;
+  const int PERMISSION_NETWORK = 1;
+  const int PERMISSION_SYSTEM = 2;
+  const int NO_PERMISSIONS = 0;
+  const int PERMISSION_INTERNET = 4;
+  const int PERMISSION_UPDATE_DEVICE_STATS = 8;
+  const int PERMISSION_UNINSTALLED = (-1) /* -1 */;
+  /**
+   * @deprecated use FIREWALL_ALLOWLIST.
+   */
+  const int FIREWALL_WHITELIST = 0;
+  const int FIREWALL_ALLOWLIST = 0;
+  /**
+   * @deprecated use FIREWALL_DENYLIST.
+   */
+  const int FIREWALL_BLACKLIST = 1;
+  const int FIREWALL_DENYLIST = 1;
+  const int FIREWALL_RULE_ALLOW = 1;
+  const int FIREWALL_RULE_DENY = 2;
+  const int FIREWALL_CHAIN_NONE = 0;
+  const int FIREWALL_CHAIN_DOZABLE = 1;
+  const int FIREWALL_CHAIN_STANDBY = 2;
+  const int FIREWALL_CHAIN_POWERSAVE = 3;
+  const int FIREWALL_CHAIN_RESTRICTED = 4;
+  const String IF_STATE_UP = "up";
+  const String IF_STATE_DOWN = "down";
+  const String IF_FLAG_BROADCAST = "broadcast";
+  const String IF_FLAG_LOOPBACK = "loopback";
+  const String IF_FLAG_POINTOPOINT = "point-to-point";
+  const String IF_FLAG_RUNNING = "running";
+  const String IF_FLAG_MULTICAST = "multicast";
+  const int IPSEC_DIRECTION_IN = 0;
+  const int IPSEC_DIRECTION_OUT = 1;
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/INetdUnsolicitedEventListener.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/INetdUnsolicitedEventListener.aidl
new file mode 100644
index 0000000..31775df
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/INetdUnsolicitedEventListener.aidl
@@ -0,0 +1,48 @@
+/**
+ * Copyright (c) 2018, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+/* @hide */
+interface INetdUnsolicitedEventListener {
+  oneway void onInterfaceClassActivityChanged(boolean isActive, int timerLabel, long timestampNs, int uid);
+  oneway void onQuotaLimitReached(@utf8InCpp String alertName, @utf8InCpp String ifName);
+  oneway void onInterfaceDnsServerInfo(@utf8InCpp String ifName, long lifetimeS, in @utf8InCpp String[] servers);
+  oneway void onInterfaceAddressUpdated(@utf8InCpp String addr, @utf8InCpp String ifName, int flags, int scope);
+  oneway void onInterfaceAddressRemoved(@utf8InCpp String addr, @utf8InCpp String ifName, int flags, int scope);
+  oneway void onInterfaceAdded(@utf8InCpp String ifName);
+  oneway void onInterfaceRemoved(@utf8InCpp String ifName);
+  oneway void onInterfaceChanged(@utf8InCpp String ifName, boolean up);
+  oneway void onInterfaceLinkStateChanged(@utf8InCpp String ifName, boolean up);
+  oneway void onRouteChanged(boolean updated, @utf8InCpp String route, @utf8InCpp String gateway, @utf8InCpp String ifName);
+  oneway void onStrictCleartextDetected(int uid, @utf8InCpp String hex);
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/InterfaceConfigurationParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/InterfaceConfigurationParcel.aidl
new file mode 100644
index 0000000..1869d8d
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/InterfaceConfigurationParcel.aidl
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+/* @hide */
+parcelable InterfaceConfigurationParcel {
+  @utf8InCpp String ifName;
+  @utf8InCpp String hwAddr;
+  @utf8InCpp String ipv4Addr;
+  int prefixLength;
+  @utf8InCpp String[] flags;
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/IpSecMigrateInfoParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/IpSecMigrateInfoParcel.aidl
new file mode 100644
index 0000000..975a261
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/IpSecMigrateInfoParcel.aidl
@@ -0,0 +1,45 @@
+/**
+ * Copyright (c) 2022, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+@JavaOnlyImmutable
+parcelable IpSecMigrateInfoParcel {
+  int requestId;
+  int selAddrFamily;
+  int direction;
+  @utf8InCpp String oldSourceAddress;
+  @utf8InCpp String oldDestinationAddress;
+  @utf8InCpp String newSourceAddress;
+  @utf8InCpp String newDestinationAddress;
+  int interfaceId;
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/MarkMaskParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/MarkMaskParcel.aidl
new file mode 100644
index 0000000..8ea20d1
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/MarkMaskParcel.aidl
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+/* @hide */
+parcelable MarkMaskParcel {
+  int mark;
+  int mask;
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/NativeNetworkConfig.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/NativeNetworkConfig.aidl
new file mode 100644
index 0000000..77d814b
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/NativeNetworkConfig.aidl
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+/* @hide */
+@JavaDerive(equals=true, toString=true) @JavaOnlyImmutable
+parcelable NativeNetworkConfig {
+  int netId;
+  android.net.NativeNetworkType networkType = android.net.NativeNetworkType.PHYSICAL;
+  int permission;
+  boolean secure;
+  android.net.NativeVpnType vpnType = android.net.NativeVpnType.PLATFORM;
+  boolean excludeLocalRoutes = false;
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/NativeNetworkType.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/NativeNetworkType.aidl
new file mode 100644
index 0000000..e77a143
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/NativeNetworkType.aidl
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+@Backing(type="int")
+enum NativeNetworkType {
+  PHYSICAL = 0,
+  VIRTUAL = 1,
+  PHYSICAL_LOCAL = 2,
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/NativeVpnType.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/NativeVpnType.aidl
new file mode 100644
index 0000000..8a8be83
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/NativeVpnType.aidl
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+@Backing(type="int")
+enum NativeVpnType {
+  SERVICE = 1,
+  PLATFORM = 2,
+  LEGACY = 3,
+  OEM = 4,
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/RouteInfoParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/RouteInfoParcel.aidl
new file mode 100644
index 0000000..5ef95e6
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/RouteInfoParcel.aidl
@@ -0,0 +1,40 @@
+/**
+ * Copyright (c) 2020, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+parcelable RouteInfoParcel {
+  @utf8InCpp String destination;
+  @utf8InCpp String ifName;
+  @utf8InCpp String nextHop;
+  int mtu;
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/TetherConfigParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/TetherConfigParcel.aidl
new file mode 100644
index 0000000..7b39c22
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/TetherConfigParcel.aidl
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+/* @hide */
+parcelable TetherConfigParcel {
+  boolean usingLegacyDnsProxy;
+  @utf8InCpp String[] dhcpRanges;
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/TetherOffloadRuleParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/TetherOffloadRuleParcel.aidl
new file mode 100644
index 0000000..983e986
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/TetherOffloadRuleParcel.aidl
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+/* @hide */
+parcelable TetherOffloadRuleParcel {
+  int inputInterfaceIndex;
+  int outputInterfaceIndex;
+  byte[] destination;
+  int prefixLength;
+  byte[] srcL2Address;
+  byte[] dstL2Address;
+  int pmtu = 1500;
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/TetherStatsParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/TetherStatsParcel.aidl
new file mode 100644
index 0000000..5f1b722
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/TetherStatsParcel.aidl
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+/* @hide */
+parcelable TetherStatsParcel {
+  @utf8InCpp String iface;
+  long rxBytes;
+  long rxPackets;
+  long txBytes;
+  long txPackets;
+  int ifIndex = 0;
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/UidRangeParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/UidRangeParcel.aidl
new file mode 100644
index 0000000..72e987a
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/UidRangeParcel.aidl
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+/* @hide */
+@JavaDerive(equals=true, toString=true) @JavaOnlyImmutable
+parcelable UidRangeParcel {
+  int start;
+  int stop;
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/netd/aidl/NativeUidRangeConfig.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/netd/aidl/NativeUidRangeConfig.aidl
new file mode 100644
index 0000000..9bb679f
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/netd/aidl/NativeUidRangeConfig.aidl
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.netd.aidl;
+/* @hide */
+@JavaDerive(equals=true, toString=true) @JavaOnlyImmutable
+parcelable NativeUidRangeConfig {
+  int netId;
+  android.net.UidRangeParcel[] uidRanges;
+  int subPriority;
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/current/android/net/INetd.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/current/android/net/INetd.aidl
index 3507784..8ccefb2 100644
--- a/staticlibs/netd/aidl_api/netd_aidl_interface/current/android/net/INetd.aidl
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/current/android/net/INetd.aidl
@@ -35,6 +35,9 @@
 /* @hide */
 interface INetd {
   boolean isAlive();
+  /**
+   * @deprecated unimplemented on T+.
+   */
   boolean firewallReplaceUidChain(in @utf8InCpp String chainName, boolean isAllowlist, in int[] uids);
   boolean bandwidthEnableDataSaver(boolean enable);
   /**
@@ -95,9 +98,21 @@
   void bandwidthSetInterfaceAlert(in @utf8InCpp String ifName, long bytes);
   void bandwidthRemoveInterfaceAlert(in @utf8InCpp String ifName);
   void bandwidthSetGlobalAlert(long bytes);
+  /**
+   * @deprecated unimplemented on T+.
+   */
   void bandwidthAddNaughtyApp(int uid);
+  /**
+   * @deprecated unimplemented on T+.
+   */
   void bandwidthRemoveNaughtyApp(int uid);
+  /**
+   * @deprecated unimplemented on T+.
+   */
   void bandwidthAddNiceApp(int uid);
+  /**
+   * @deprecated unimplemented on T+.
+   */
   void bandwidthRemoveNiceApp(int uid);
   void tetherStart(in @utf8InCpp String[] dhcpRanges);
   void tetherStop();
@@ -117,13 +132,22 @@
   void networkSetPermissionForNetwork(int netId, int permission);
   void networkSetPermissionForUser(int permission, in int[] uids);
   void networkClearPermissionForUser(in int[] uids);
+  /**
+   * @deprecated unimplemented on T+.
+   */
   void trafficSetNetPermForUids(int permission, in int[] uids);
   void networkSetProtectAllow(int uid);
   void networkSetProtectDeny(int uid);
   boolean networkCanProtect(int uid);
   void firewallSetFirewallType(int firewalltype);
   void firewallSetInterfaceRule(in @utf8InCpp String ifName, int firewallRule);
+  /**
+   * @deprecated unimplemented on T+.
+   */
   void firewallSetUidRule(int childChain, int uid, int firewallRule);
+  /**
+   * @deprecated unimplemented on T+.
+   */
   void firewallEnableChildChain(int childChain, boolean enable);
   @utf8InCpp String[] interfaceGetList();
   android.net.InterfaceConfigurationParcel interfaceGetCfg(in @utf8InCpp String ifName);
@@ -136,8 +160,17 @@
   void tetherRemoveForward(in @utf8InCpp String intIface, in @utf8InCpp String extIface);
   void setTcpRWmemorySize(in @utf8InCpp String rmemValues, in @utf8InCpp String wmemValues);
   void registerUnsolicitedEventListener(android.net.INetdUnsolicitedEventListener listener);
+  /**
+   * @deprecated unimplemented on T+.
+   */
   void firewallAddUidInterfaceRules(in @utf8InCpp String ifName, in int[] uids);
+  /**
+   * @deprecated unimplemented on T+.
+   */
   void firewallRemoveUidInterfaceRules(in int[] uids);
+  /**
+   * @deprecated unimplemented on T+.
+   */
   void trafficSwapActiveStatsMap();
   IBinder getOemNetd();
   void tetherStartWithConfiguration(in android.net.TetherConfigParcel config);
@@ -196,7 +229,7 @@
   const int NO_PERMISSIONS = 0;
   const int PERMISSION_INTERNET = 4;
   const int PERMISSION_UPDATE_DEVICE_STATS = 8;
-  const int PERMISSION_UNINSTALLED = (-1);
+  const int PERMISSION_UNINSTALLED = (-1) /* -1 */;
   /**
    * @deprecated use FIREWALL_ALLOWLIST.
    */
diff --git a/staticlibs/netd/binder/android/net/INetd.aidl b/staticlibs/netd/binder/android/net/INetd.aidl
index 27d9a03..ee27e84 100644
--- a/staticlibs/netd/binder/android/net/INetd.aidl
+++ b/staticlibs/netd/binder/android/net/INetd.aidl
@@ -47,6 +47,7 @@
      * @param isAllowlist Whether this is an allowlist or denylist chain.
      * @param uids The list of UIDs to allow/deny.
      * @return true if the chain was successfully replaced, false otherwise.
+     * @deprecated unimplemented on T+.
      */
     boolean firewallReplaceUidChain(in @utf8InCpp String chainName,
                                     boolean isAllowlist,
@@ -683,6 +684,7 @@
     * @param uid uid of target app
     * @throws ServiceSpecificException in case of failure, with an error code indicating the
     *         cause of the failure.
+    * @deprecated unimplemented on T+.
     */
     void bandwidthAddNaughtyApp(int uid);
 
@@ -692,6 +694,7 @@
     * @param uid uid of target app
     * @throws ServiceSpecificException in case of failure, with an error code indicating the
     *         cause of the failure.
+    * @deprecated unimplemented on T+.
     */
     void bandwidthRemoveNaughtyApp(int uid);
 
@@ -701,6 +704,7 @@
     * @param uid uid of target app
     * @throws ServiceSpecificException in case of failure, with an error code indicating the
     *         cause of the failure.
+    * @deprecated unimplemented on T+.
     */
     void bandwidthAddNiceApp(int uid);
 
@@ -710,6 +714,7 @@
     * @param uid uid of target app
     * @throws ServiceSpecificException in case of failure, with an error code indicating the
     *         cause of the failure.
+    * @deprecated unimplemented on T+.
     */
     void bandwidthRemoveNiceApp(int uid);
 
@@ -983,6 +988,7 @@
     *                   PERMISSION_UPDATE_DEVICE_STATS. If the permission is NO_PERMISSIONS, then
     *                   revoke all permissions for the uids.
     * @param uids uid of users to grant permission
+    * @deprecated unimplemented on T+.
     */
     void trafficSetNetPermForUids(int permission, in int[] uids);
 
@@ -1071,6 +1077,7 @@
     * @param firewallRule either FIREWALL_RULE_ALLOW or FIREWALL_RULE_DENY
     * @throws ServiceSpecificException in case of failure, with an error code indicating the
     *         cause of the failure.
+    * @deprecated unimplemented on T+.
     */
     void firewallSetUidRule(int childChain, int uid, int firewallRule);
 
@@ -1081,6 +1088,7 @@
     * @param enable whether to enable or disable child chain.
     * @throws ServiceSpecificException in case of failure, with an error code indicating the
     *         cause of the failure.
+    * @deprecated unimplemented on T+.
     */
     void firewallEnableChildChain(int childChain, boolean enable);
 
@@ -1212,6 +1220,7 @@
      * @param uids an array of UIDs which the filtering rules will be set
      * @throws ServiceSpecificException in case of failure, with an error code indicating the
      *         cause of the failure.
+     * @deprecated unimplemented on T+.
      */
     void firewallAddUidInterfaceRules(in @utf8InCpp String ifName, in int[] uids);
 
@@ -1224,6 +1233,7 @@
      * @param uids an array of UIDs from which the filtering rules will be removed
      * @throws ServiceSpecificException in case of failure, with an error code indicating the
      *         cause of the failure.
+     * @deprecated unimplemented on T+.
      */
     void firewallRemoveUidInterfaceRules(in int[] uids);
 
@@ -1231,6 +1241,7 @@
     * Request netd to change the current active network stats map.
     * @throws ServiceSpecificException in case of failure, with an error code indicating the
     *         cause of the failure.
+    * @deprecated unimplemented on T+.
     */
     void trafficSwapActiveStatsMap();
 
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/netd/libnetdutils/include/netdutils/Status.h b/staticlibs/netd/libnetdutils/include/netdutils/Status.h
index 7b0bd47..34f3bb2 100644
--- a/staticlibs/netd/libnetdutils/include/netdutils/Status.h
+++ b/staticlibs/netd/libnetdutils/include/netdutils/Status.h
@@ -41,6 +41,9 @@
     // Constructs an error Status, |code| must be non-zero.
     Status(int code, std::string msg) : mCode(code), mMsg(std::move(msg)) { assert(!ok()); }
 
+    // Constructs an error Status with message. Error |code| is unspecified.
+    explicit Status(std::string msg) : Status(std::numeric_limits<int>::max(), std::move(msg)) {}
+
     Status(android::base::Result<void> result)
         : mCode(result.ok() ? 0 : static_cast<int>(result.error().code())),
           mMsg(result.ok() ? "" : result.error().message()) {}
diff --git a/staticlibs/tests/unit/Android.bp b/staticlibs/tests/unit/Android.bp
index 031e52f..d203bc0 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: [
@@ -33,7 +37,10 @@
         "//packages/modules/Connectivity/Tethering/tests:__subpackages__",
         "//packages/modules/NetworkStack/tests/integration",
     ],
-    lint: { strict_updatability_linting: true },
+    lint: {
+        strict_updatability_linting: true,
+        test: true,
+    },
 }
 
 android_test {
@@ -49,5 +56,7 @@
     ],
     jarjar_rules: "jarjar-rules.txt",
     test_suites: ["device-tests"],
-    lint: { strict_updatability_linting: true },
+    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/DnsSvcbPacketTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/DnsSvcbPacketTest.java
index 6778f8a..d59795f 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/DnsSvcbPacketTest.java
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/DnsSvcbPacketTest.java
@@ -207,7 +207,7 @@
                 os.write(shortToByteArray((short) mRdataLen));
             } else {
                 final byte[] targetNameLabels =
-                                DnsPacketUtils.DnsRecordParser.domainNameToLabels(mTargetName);
+                        DnsPacketUtils.DnsRecordParser.domainNameToLabels(mTargetName);
                 mRdataLen += (Short.BYTES + targetNameLabels.length);
                 os.write(shortToByteArray((short) mRdataLen));
                 os.write(shortToByteArray(mSvcPriority));
@@ -251,7 +251,9 @@
         // Check the content returned from toString() for now because the getter function for
         // this SvcParam hasn't been implemented.
         // TODO(b/240259333): Consider adding DnsSvcbRecord.isMandatory(String alpn) when needed.
-        assertTrue(record.toString().contains("mandatory=ipv4hint,alpn,key333"));
+        assertTrue(record.toString().contains("ipv4hint"));
+        assertTrue(record.toString().contains("alpn"));
+        assertTrue(record.toString().contains("key333"));
     }
 
     @Test
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 028308b..d5b43fb 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
@@ -16,21 +16,20 @@
 
 package com.android.net.module.util
 
-import android.Manifest.permission.INTERNET
-import android.Manifest.permission.NETWORK_SETTINGS
 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 android.os.Build
 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.testutils.DevSdkIgnoreRule
 import com.android.testutils.DevSdkIgnoreRunner
@@ -45,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 */
@@ -56,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)
 
@@ -64,6 +69,7 @@
     @Before
     fun setup() {
         doReturn(mockPackageManager).`when`(mockContext).packageManager
+        doReturn(mockContext).`when`(mockContext).createContextAsUser(any(), anyInt())
     }
 
     @Test
@@ -146,25 +152,22 @@
     }
 
     @Test
-    @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
-    fun testIsSystemSignaturePermission() {
-        assertTrue(
-            PermissionUtils.isSystemSignaturePermission(
-                context,
-                NETWORK_SETTINGS
-            )
-        )
-        assertFalse(
-            PermissionUtils
-                .isSystemSignaturePermission(context, PERMISSION_MAINLINE_NETWORK_STACK)
-        )
-        assertFalse(
-            PermissionUtils
-                .isSystemSignaturePermission(context, "test_permission")
-        )
-        assertFalse(
-            PermissionUtils
-                .isSystemSignaturePermission(context, INTERNET)
-        )
+    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..b644a53 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, 0);
+        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/netlink/StructNlMsgHdrTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/StructNlMsgHdrTest.java
index b7f68c6..a0d8b8c 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/StructNlMsgHdrTest.java
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/StructNlMsgHdrTest.java
@@ -16,6 +16,7 @@
 
 package com.android.net.module.util.netlink;
 
+import static com.android.net.module.util.netlink.StructNlMsgHdr.NLM_F_DUMP;
 import static org.junit.Assert.fail;
 
 import android.system.OsConstants;
@@ -48,10 +49,14 @@
     public static final String TEST_NLMSG_PID_STR = "nlmsg_pid{5678}";
 
     private StructNlMsgHdr makeStructNlMsgHdr(short type) {
+        return makeStructNlMsgHdr(type, TEST_NLMSG_FLAGS);
+    }
+
+    private StructNlMsgHdr makeStructNlMsgHdr(short type, short flags) {
         final StructNlMsgHdr struct = new StructNlMsgHdr();
         struct.nlmsg_len = TEST_NLMSG_LEN;
         struct.nlmsg_type = type;
-        struct.nlmsg_flags = TEST_NLMSG_FLAGS;
+        struct.nlmsg_flags = flags;
         struct.nlmsg_seq = TEST_NLMSG_SEQ;
         struct.nlmsg_pid = TEST_NLMSG_PID;
         return struct;
@@ -62,6 +67,11 @@
         fail("\"" + actualValue + "\" does not contain \"" + expectedSubstring + "\"");
     }
 
+    private static void assertNotContains(String actualValue, String unexpectedSubstring) {
+        if (!actualValue.contains(unexpectedSubstring)) return;
+        fail("\"" + actualValue + "\" contains \"" + unexpectedSubstring + "\"");
+    }
+
     @Test
     public void testToString() {
         StructNlMsgHdr struct = makeStructNlMsgHdr(NetlinkConstants.RTM_NEWADDR);
@@ -99,4 +109,31 @@
         assertContains(s, TEST_NLMSG_PID_STR);
         assertContains(s, "nlmsg_type{20(SOCK_DIAG_BY_FAMILY)}");
     }
+
+    @Test
+    public void testToString_flags_dumpRequest() {
+        final short flags = StructNlMsgHdr.NLM_F_REQUEST | StructNlMsgHdr.NLM_F_DUMP;
+        StructNlMsgHdr struct = makeStructNlMsgHdr(NetlinkConstants.RTM_GETROUTE, flags);
+
+        String s = struct.toString(OsConstants.NETLINK_ROUTE);
+
+        assertContains(s, "RTM_GETROUTE");
+        assertContains(s, "NLM_F_REQUEST");
+        assertContains(s, "NLM_F_DUMP");
+        // NLM_F_DUMP = NLM_F_ROOT | NLM_F_MATCH;
+        assertNotContains(s, "NLM_F_MATCH");
+        assertNotContains(s, "NLM_F_ROOT");
+    }
+
+    @Test
+    public void testToString_flags_root() {
+        final short flags = StructNlMsgHdr.NLM_F_ROOT;
+        StructNlMsgHdr struct = makeStructNlMsgHdr(NetlinkConstants.RTM_GETROUTE, flags);
+
+        String s = struct.toString(OsConstants.NETLINK_ROUTE);
+
+        assertContains(s, "NLM_F_ROOT");
+        // NLM_F_DUMP = NLM_F_ROOT | NLM_F_MATCH;
+        assertNotContains(s, "NLM_F_DUMP");
+    }
 }
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/xfrm/OWNERS b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/xfrm/OWNERS
new file mode 100644
index 0000000..fca70aa
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/xfrm/OWNERS
@@ -0,0 +1,2 @@
+# Bug component: 685852
+file:platform/frameworks/base:main:/services/core/java/com/android/server/vcn/OWNERS
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/xfrm/StructXfrmIdTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/xfrm/StructXfrmIdTest.java
new file mode 100644
index 0000000..c9741cf
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/xfrm/StructXfrmIdTest.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util.netlink.xfrm;
+
+import static com.android.net.module.util.netlink.xfrm.XfrmNetlinkMessage.IPPROTO_ESP;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+
+import android.net.InetAddresses;
+import android.system.OsConstants;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.net.module.util.HexDump;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.net.InetAddress;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class StructXfrmIdTest {
+    private static final String EXPECTED_HEX_STRING =
+            "C0000201000000000000000000000000" + "53FA0FDD32000000";
+    private static final byte[] EXPECTED_HEX = HexDump.hexStringToByteArray(EXPECTED_HEX_STRING);
+    private static final InetAddress DEST_ADDRESS = InetAddresses.parseNumericAddress("192.0.2.1");
+    private static final long SPI = 0x53fa0fdd;
+    private static final short PROTO = IPPROTO_ESP;
+
+    @Test
+    public void testEncode() throws Exception {
+        final StructXfrmId struct = new StructXfrmId(DEST_ADDRESS, SPI, PROTO);
+
+        final ByteBuffer buffer = ByteBuffer.allocate(EXPECTED_HEX.length);
+        buffer.order(ByteOrder.nativeOrder());
+        struct.writeToByteBuffer(buffer);
+
+        assertArrayEquals(EXPECTED_HEX, buffer.array());
+    }
+
+    @Test
+    public void testDecode() throws Exception {
+        final ByteBuffer buffer = ByteBuffer.wrap(EXPECTED_HEX);
+        buffer.order(ByteOrder.nativeOrder());
+        final StructXfrmId struct = StructXfrmId.parse(StructXfrmId.class, buffer);
+
+        assertEquals(DEST_ADDRESS, struct.getDestAddress(OsConstants.AF_INET));
+        assertEquals(SPI, struct.spi);
+        assertEquals(PROTO, struct.proto);
+    }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/xfrm/StructXfrmLifetimeCfgTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/xfrm/StructXfrmLifetimeCfgTest.java
new file mode 100644
index 0000000..69360f6
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/xfrm/StructXfrmLifetimeCfgTest.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util.netlink.xfrm;
+
+import static com.android.net.module.util.netlink.xfrm.XfrmNetlinkMessage.XFRM_INF;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.net.module.util.HexDump;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.math.BigInteger;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class StructXfrmLifetimeCfgTest {
+    private static final String EXPECTED_HEX_STRING =
+            "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"
+                    + "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000";
+    private static final byte[] EXPECTED_HEX = HexDump.hexStringToByteArray(EXPECTED_HEX_STRING);
+
+    @Test
+    public void testEncode() throws Exception {
+        final StructXfrmLifetimeCfg struct = new StructXfrmLifetimeCfg();
+
+        final ByteBuffer buffer = ByteBuffer.allocate(EXPECTED_HEX.length);
+        buffer.order(ByteOrder.nativeOrder());
+        struct.writeToByteBuffer(buffer);
+
+        assertArrayEquals(EXPECTED_HEX, buffer.array());
+    }
+
+    @Test
+    public void testDecode() throws Exception {
+        final ByteBuffer buffer = ByteBuffer.wrap(EXPECTED_HEX);
+        buffer.order(ByteOrder.nativeOrder());
+        final StructXfrmLifetimeCfg struct =
+                StructXfrmLifetimeCfg.parse(StructXfrmLifetimeCfg.class, buffer);
+
+        assertEquals(XFRM_INF, struct.softByteLimit);
+        assertEquals(XFRM_INF, struct.hardByteLimit);
+        assertEquals(XFRM_INF, struct.softPacketLimit);
+        assertEquals(XFRM_INF, struct.hardPacketLimit);
+        assertEquals(BigInteger.ZERO, struct.softAddExpiresSeconds);
+        assertEquals(BigInteger.ZERO, struct.hardAddExpiresSeconds);
+        assertEquals(BigInteger.ZERO, struct.softUseExpiresSeconds);
+        assertEquals(BigInteger.ZERO, struct.hardUseExpiresSeconds);
+    }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/xfrm/StructXfrmLifetimeCurTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/xfrm/StructXfrmLifetimeCurTest.java
new file mode 100644
index 0000000..008c922
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/xfrm/StructXfrmLifetimeCurTest.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util.netlink.xfrm;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.net.module.util.HexDump;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.math.BigInteger;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.Calendar;
+import java.util.TimeZone;
+import java.util.concurrent.TimeUnit;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class StructXfrmLifetimeCurTest {
+    private static final String EXPECTED_HEX_STRING =
+            "00000000000000000000000000000000" + "8CFE4265000000000000000000000000";
+    private static final byte[] EXPECTED_HEX = HexDump.hexStringToByteArray(EXPECTED_HEX_STRING);
+    private static final BigInteger ADD_TIME;
+
+    static {
+        final Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
+        cal.set(2023, Calendar.NOVEMBER, 2, 1, 42, 36);
+        final long timestampSeconds = TimeUnit.MILLISECONDS.toSeconds(cal.getTimeInMillis());
+        ADD_TIME = BigInteger.valueOf(timestampSeconds);
+    }
+
+    @Test
+    public void testEncode() throws Exception {
+        final StructXfrmLifetimeCur struct =
+                new StructXfrmLifetimeCur(
+                        BigInteger.ZERO, BigInteger.ZERO, ADD_TIME, BigInteger.ZERO);
+
+        final ByteBuffer buffer = ByteBuffer.allocate(EXPECTED_HEX.length);
+        buffer.order(ByteOrder.nativeOrder());
+        struct.writeToByteBuffer(buffer);
+
+        assertArrayEquals(EXPECTED_HEX, buffer.array());
+    }
+
+    @Test
+    public void testDecode() throws Exception {
+        final ByteBuffer buffer = ByteBuffer.wrap(EXPECTED_HEX);
+        buffer.order(ByteOrder.nativeOrder());
+        final StructXfrmLifetimeCur struct =
+                StructXfrmLifetimeCur.parse(StructXfrmLifetimeCur.class, buffer);
+
+        assertEquals(BigInteger.ZERO, struct.bytes);
+        assertEquals(BigInteger.ZERO, struct.packets);
+        assertEquals(ADD_TIME, struct.addTime);
+        assertEquals(BigInteger.ZERO, struct.useTime);
+    }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/xfrm/StructXfrmReplayStateEsnTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/xfrm/StructXfrmReplayStateEsnTest.java
new file mode 100644
index 0000000..1eb968d
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/xfrm/StructXfrmReplayStateEsnTest.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util.netlink.xfrm;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.net.module.util.HexDump;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class StructXfrmReplayStateEsnTest {
+    private static final String EXPECTED_HEX_STRING =
+            "80000000000000000000000000000000"
+                    + "00000000001000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "0000000000000000";
+
+    private static final byte[] EXPECTED_HEX = HexDump.hexStringToByteArray(EXPECTED_HEX_STRING);
+
+    private static final long BMP_LEN = 128;
+    private static final long REPLAY_WINDOW = 4096;
+    private static final byte[] BITMAP = new byte[512];
+
+    @Test
+    public void testEncode() throws Exception {
+        final StructXfrmReplayStateEsn struct =
+                new StructXfrmReplayStateEsn(BMP_LEN, 0L, 0L, 0L, 0L, REPLAY_WINDOW, BITMAP);
+
+        final ByteBuffer buffer = ByteBuffer.allocate(struct.getStructSize());
+        buffer.order(ByteOrder.nativeOrder());
+        struct.writeToByteBuffer(buffer);
+
+        assertArrayEquals(EXPECTED_HEX, buffer.array());
+    }
+
+    @Test
+    public void testDecode() throws Exception {
+        final ByteBuffer buffer = ByteBuffer.wrap(EXPECTED_HEX);
+        buffer.order(ByteOrder.nativeOrder());
+
+        final StructXfrmReplayStateEsn struct = StructXfrmReplayStateEsn.parse(buffer);
+
+        assertEquals(BMP_LEN, struct.getBmpLen());
+        assertEquals(REPLAY_WINDOW, struct.getReplayWindow());
+        assertArrayEquals(BITMAP, struct.getBitmap());
+        assertEquals(0L, struct.getRxSequenceNumber());
+        assertEquals(0L, struct.getTxSequenceNumber());
+    }
+
+    @Test
+    public void testGetSequenceNumber() throws Exception {
+        final long low = 0x00ab112233L;
+        final long hi = 0x01L;
+
+        assertEquals(0x01ab112233L, StructXfrmReplayStateEsn.getSequenceNumber(hi, low));
+        assertEquals(0xab11223300000001L, StructXfrmReplayStateEsn.getSequenceNumber(low, hi));
+    }
+
+    // TODO: Add test cases that the test bitmap is not all zeros
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/xfrm/StructXfrmSelectorTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/xfrm/StructXfrmSelectorTest.java
new file mode 100644
index 0000000..99f3b2a
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/xfrm/StructXfrmSelectorTest.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util.netlink.xfrm;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+
+import android.system.OsConstants;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.net.module.util.HexDump;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class StructXfrmSelectorTest {
+    private static final String EXPECTED_HEX_STRING =
+            "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000200000000000000"
+                    + "0000000000000000";
+    private static final byte[] EXPECTED_HEX = HexDump.hexStringToByteArray(EXPECTED_HEX_STRING);
+
+    private static final byte[] XFRM_ADDRESS_T_ANY_BYTES = new byte[16];
+    private static final int FAMILY = OsConstants.AF_INET;
+
+    @Test
+    public void testEncode() throws Exception {
+        final StructXfrmSelector struct = new StructXfrmSelector(FAMILY);
+
+        final ByteBuffer buffer = ByteBuffer.allocate(EXPECTED_HEX.length);
+        buffer.order(ByteOrder.nativeOrder());
+        struct.writeToByteBuffer(buffer);
+
+        assertArrayEquals(EXPECTED_HEX, buffer.array());
+    }
+
+    @Test
+    public void testDecode() throws Exception {
+        final ByteBuffer buffer = ByteBuffer.wrap(EXPECTED_HEX);
+        buffer.order(ByteOrder.nativeOrder());
+        final StructXfrmSelector struct =
+                StructXfrmSelector.parse(StructXfrmSelector.class, buffer);
+
+        assertArrayEquals(XFRM_ADDRESS_T_ANY_BYTES, struct.nestedStructDAddr);
+        assertArrayEquals(XFRM_ADDRESS_T_ANY_BYTES, struct.nestedStructSAddr);
+        assertEquals(0, struct.dPort);
+        assertEquals(0, struct.dPortMask);
+        assertEquals(0, struct.sPort);
+        assertEquals(0, struct.sPortMask);
+        assertEquals(FAMILY, struct.selectorFamily);
+        assertEquals(0, struct.prefixlenD);
+        assertEquals(0, struct.prefixlenS);
+        assertEquals(0, struct.proto);
+        assertEquals(0, struct.ifIndex);
+        assertEquals(0, struct.user);
+    }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/xfrm/StructXfrmUsersaIdTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/xfrm/StructXfrmUsersaIdTest.java
new file mode 100644
index 0000000..b659f62
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/xfrm/StructXfrmUsersaIdTest.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util.netlink.xfrm;
+
+import static com.android.net.module.util.netlink.xfrm.XfrmNetlinkMessage.IPPROTO_ESP;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+
+import android.net.InetAddresses;
+import android.system.OsConstants;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.net.module.util.HexDump;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.net.InetAddress;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class StructXfrmUsersaIdTest {
+    private static final String EXPECTED_HEX_STRING =
+            "C0000201000000000000000000000000" + "7768440002003200";
+    private static final byte[] EXPECTED_HEX = HexDump.hexStringToByteArray(EXPECTED_HEX_STRING);
+
+    private static final InetAddress DEST_ADDRESS = InetAddresses.parseNumericAddress("192.0.2.1");
+    private static final long SPI = 0x77684400;
+    private static final int FAMILY = OsConstants.AF_INET;
+    private static final short PROTO = IPPROTO_ESP;
+
+    @Test
+    public void testEncode() throws Exception {
+        final StructXfrmUsersaId struct = new StructXfrmUsersaId(DEST_ADDRESS, SPI, FAMILY, PROTO);
+
+        final ByteBuffer buffer = ByteBuffer.allocate(EXPECTED_HEX.length);
+        buffer.order(ByteOrder.nativeOrder());
+        struct.writeToByteBuffer(buffer);
+
+        assertArrayEquals(EXPECTED_HEX, buffer.array());
+    }
+
+    @Test
+    public void testDecode() throws Exception {
+        final ByteBuffer buffer = ByteBuffer.wrap(EXPECTED_HEX);
+        buffer.order(ByteOrder.nativeOrder());
+
+        final StructXfrmUsersaId struct =
+                StructXfrmUsersaId.parse(StructXfrmUsersaId.class, buffer);
+
+        assertEquals(DEST_ADDRESS, struct.getDestAddress());
+        assertEquals(SPI, struct.spi);
+        assertEquals(FAMILY, struct.family);
+        assertEquals(PROTO, struct.proto);
+    }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/xfrm/StructXfrmUsersaInfoTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/xfrm/StructXfrmUsersaInfoTest.java
new file mode 100644
index 0000000..94161ff
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/xfrm/StructXfrmUsersaInfoTest.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util.netlink.xfrm;
+
+import static com.android.net.module.util.netlink.xfrm.XfrmNetlinkMessage.IPPROTO_ESP;
+import static com.android.net.module.util.netlink.xfrm.XfrmNetlinkMessage.XFRM_MODE_TRANSPORT;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+
+import android.net.InetAddresses;
+import android.system.OsConstants;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.net.module.util.HexDump;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.math.BigInteger;
+import java.net.InetAddress;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.Calendar;
+import java.util.TimeZone;
+import java.util.concurrent.TimeUnit;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class StructXfrmUsersaInfoTest {
+    private static final String EXPECTED_HEX_STRING =
+            "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000A00000000000000"
+                    + "000000000000000020010DB800000000"
+                    + "0000000000000111AABBCCDD32000000"
+                    + "20010DB8000000000000000000000222"
+                    + "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"
+                    + "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "FD464C65000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "024000000A0000000000000000000000";
+    private static final byte[] EXPECTED_HEX = HexDump.hexStringToByteArray(EXPECTED_HEX_STRING);
+
+    private static final InetAddress DEST_ADDRESS =
+            InetAddresses.parseNumericAddress("2001:db8::111");
+    private static final InetAddress SOURCE_ADDRESS =
+            InetAddresses.parseNumericAddress("2001:db8::222");
+    private static final BigInteger ADD_TIME;
+    private static final int SELECTOR_FAMILY = OsConstants.AF_INET6;
+    private static final int FAMILY = OsConstants.AF_INET6;
+    private static final long SPI = 0xaabbccddL;
+    private static final long SEQ = 0L;
+    private static final long REQ_ID = 16386L;
+    private static final short PROTO = IPPROTO_ESP;
+    private static final short MODE = XFRM_MODE_TRANSPORT;
+    private static final short REPLAY_WINDOW_LEGACY = 0;
+    private static final short FLAGS = 0;
+
+    static {
+        final Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
+        cal.set(2023, Calendar.NOVEMBER, 9, 2, 42, 05);
+        final long timestampSeconds = TimeUnit.MILLISECONDS.toSeconds(cal.getTimeInMillis());
+        ADD_TIME = BigInteger.valueOf(timestampSeconds);
+    }
+
+    @Test
+    public void testEncode() throws Exception {
+        final StructXfrmUsersaInfo struct =
+                new StructXfrmUsersaInfo(
+                        DEST_ADDRESS,
+                        SOURCE_ADDRESS,
+                        ADD_TIME,
+                        SELECTOR_FAMILY,
+                        SPI,
+                        SEQ,
+                        REQ_ID,
+                        PROTO,
+                        MODE,
+                        REPLAY_WINDOW_LEGACY,
+                        FLAGS);
+
+        final ByteBuffer buffer = ByteBuffer.allocate(EXPECTED_HEX.length);
+        buffer.order(ByteOrder.nativeOrder());
+        struct.writeToByteBuffer(buffer);
+
+        assertArrayEquals(EXPECTED_HEX, buffer.array());
+    }
+
+    @Test
+    public void testDecode() throws Exception {
+        final ByteBuffer buffer = ByteBuffer.wrap(EXPECTED_HEX);
+        buffer.order(ByteOrder.nativeOrder());
+
+        final StructXfrmUsersaInfo struct =
+                StructXfrmUsersaInfo.parse(StructXfrmUsersaInfo.class, buffer);
+
+        assertEquals(DEST_ADDRESS, struct.getDestAddress());
+        assertEquals(SOURCE_ADDRESS, struct.getSrcAddress());
+        assertEquals(SPI, struct.getSpi());
+        assertEquals(SEQ, struct.seq);
+        assertEquals(REQ_ID, struct.reqId);
+        assertEquals(FAMILY, struct.family);
+        assertEquals(MODE, struct.mode);
+        assertEquals(REPLAY_WINDOW_LEGACY, struct.replayWindowLegacy);
+        assertEquals(FLAGS, struct.flags);
+    }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/xfrm/XfrmNetlinkGetSaMessageTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/xfrm/XfrmNetlinkGetSaMessageTest.java
new file mode 100644
index 0000000..0ab36e7
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/xfrm/XfrmNetlinkGetSaMessageTest.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util.netlink.xfrm;
+
+import static com.android.net.module.util.netlink.xfrm.XfrmNetlinkMessage.IPPROTO_ESP;
+import static com.android.net.module.util.netlink.xfrm.XfrmNetlinkMessage.NETLINK_XFRM;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+
+import android.net.InetAddresses;
+import android.system.OsConstants;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.net.module.util.HexDump;
+import com.android.net.module.util.netlink.NetlinkMessage;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.net.InetAddress;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class XfrmNetlinkGetSaMessageTest {
+    private static final String EXPECTED_HEX_STRING =
+            "28000000120001000000000000000000"
+                    + "C0000201000000000000000000000000"
+                    + "7768440002003200";
+    private static final byte[] EXPECTED_HEX = HexDump.hexStringToByteArray(EXPECTED_HEX_STRING);
+    private static final InetAddress DEST_ADDRESS = InetAddresses.parseNumericAddress("192.0.2.1");
+    private static final long SPI = 0x77684400;
+    private static final int FAMILY = OsConstants.AF_INET;
+    private static final short PROTO = IPPROTO_ESP;
+
+    @Test
+    public void testEncode() throws Exception {
+        final byte[] result =
+                XfrmNetlinkGetSaMessage.newXfrmNetlinkGetSaMessage(DEST_ADDRESS, SPI, PROTO);
+        assertArrayEquals(EXPECTED_HEX, result);
+    }
+
+    @Test
+    public void testDecode() throws Exception {
+        final ByteBuffer buffer = ByteBuffer.wrap(EXPECTED_HEX);
+        buffer.order(ByteOrder.nativeOrder());
+        final XfrmNetlinkGetSaMessage message =
+                (XfrmNetlinkGetSaMessage) NetlinkMessage.parse(buffer, NETLINK_XFRM);
+        final StructXfrmUsersaId struct = message.getStructXfrmUsersaId();
+
+        assertEquals(DEST_ADDRESS, struct.getDestAddress());
+        assertEquals(SPI, struct.spi);
+        assertEquals(FAMILY, struct.family);
+        assertEquals(PROTO, struct.proto);
+        assertEquals(0, buffer.remaining());
+    }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/xfrm/XfrmNetlinkNewSaMessageTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/xfrm/XfrmNetlinkNewSaMessageTest.java
new file mode 100644
index 0000000..3d0ce2c
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/xfrm/XfrmNetlinkNewSaMessageTest.java
@@ -0,0 +1,150 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util.netlink.xfrm;
+
+import static com.android.net.module.util.netlink.xfrm.XfrmNetlinkMessage.NETLINK_XFRM;
+import static com.android.net.module.util.netlink.xfrm.XfrmNetlinkMessage.XFRM_MODE_TRANSPORT;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+
+import android.net.InetAddresses;
+import android.system.OsConstants;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.net.module.util.HexDump;
+import com.android.net.module.util.netlink.NetlinkMessage;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.net.InetAddress;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class XfrmNetlinkNewSaMessageTest {
+    private static final String EXPECTED_HEX_STRING =
+            "2004000010000000000000003FE1D4B6"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000A00000000000000"
+                    + "000000000000000020010DB800000000"
+                    + "0000000000000111AABBCCDD32000000"
+                    + "20010DB8000000000000000000000222"
+                    + "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"
+                    + "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "FD464C65000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "024000000A0000000000000000000000"
+                    + "5C000100686D61632873686131290000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000A000000055F01AC07E15E437"
+                    + "115DDE0AEDD18A822BA9F81E60001400"
+                    + "686D6163287368613129000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "A00000006000000055F01AC07E15E437"
+                    + "115DDE0AEDD18A822BA9F81E58000200"
+                    + "63626328616573290000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "800000006AED4975ADF006D65C76F639"
+                    + "23A6265B1C0217008000000000000000"
+                    + "00000000000000000000000000100000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000";
+
+    private static final byte[] EXPECTED_HEX = HexDump.hexStringToByteArray(EXPECTED_HEX_STRING);
+
+    private static final InetAddress DEST_ADDRESS =
+            InetAddresses.parseNumericAddress("2001:db8::111");
+    private static final InetAddress SOURCE_ADDRESS =
+            InetAddresses.parseNumericAddress("2001:db8::222");
+    private static final int FAMILY = OsConstants.AF_INET6;
+    private static final long SPI = 0xaabbccddL;
+    private static final long SEQ = 0L;
+    private static final long REQ_ID = 16386L;
+    private static final short MODE = XFRM_MODE_TRANSPORT;
+    private static final short REPLAY_WINDOW_LEGACY = 0;
+    private static final short FLAGS = 0;
+    private static final byte[] BITMAP = new byte[512];
+
+    @Test
+    public void testDecode() throws Exception {
+        final ByteBuffer buffer = ByteBuffer.wrap(EXPECTED_HEX);
+        buffer.order(ByteOrder.nativeOrder());
+        final XfrmNetlinkNewSaMessage message =
+                (XfrmNetlinkNewSaMessage) NetlinkMessage.parse(buffer, NETLINK_XFRM);
+        final StructXfrmUsersaInfo xfrmUsersaInfo = message.getXfrmUsersaInfo();
+
+        assertEquals(DEST_ADDRESS, xfrmUsersaInfo.getDestAddress());
+        assertEquals(SOURCE_ADDRESS, xfrmUsersaInfo.getSrcAddress());
+        assertEquals(SPI, xfrmUsersaInfo.getSpi());
+        assertEquals(SEQ, xfrmUsersaInfo.seq);
+        assertEquals(REQ_ID, xfrmUsersaInfo.reqId);
+        assertEquals(FAMILY, xfrmUsersaInfo.family);
+        assertEquals(MODE, xfrmUsersaInfo.mode);
+        assertEquals(REPLAY_WINDOW_LEGACY, xfrmUsersaInfo.replayWindowLegacy);
+        assertEquals(FLAGS, xfrmUsersaInfo.flags);
+
+        assertArrayEquals(BITMAP, message.getBitmap());
+        assertEquals(0L, message.getRxSequenceNumber());
+        assertEquals(0L, message.getTxSequenceNumber());
+    }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/structs/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/app/connectivitychecker/src/com/android/testutils/connectivitypreparer/ConnectivityCheckTest.kt b/staticlibs/testutils/app/connectivitychecker/src/com/android/testutils/connectivitypreparer/ConnectivityCheckTest.kt
index d75d9ca..df6067d 100644
--- a/staticlibs/testutils/app/connectivitychecker/src/com/android/testutils/connectivitypreparer/ConnectivityCheckTest.kt
+++ b/staticlibs/testutils/app/connectivitychecker/src/com/android/testutils/connectivitypreparer/ConnectivityCheckTest.kt
@@ -61,8 +61,8 @@
                     commonError)
         }
         assertTrue(tm.isDataConnectivityPossible,
-            "The device is not setup with a SIM card that supports data connectivity. " +
-                    commonError)
+            "The device has a SIM card, but it does not supports data connectivity. " +
+            "Check the data plan, and verify that mobile data is working. " + commonError)
         connectUtil.ensureCellularValidated()
     }
 }
diff --git a/staticlibs/testutils/devicetests/NSResponder.kt b/staticlibs/testutils/devicetests/NSResponder.kt
new file mode 100644
index 0000000..f7619cd
--- /dev/null
+++ b/staticlibs/testutils/devicetests/NSResponder.kt
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.testutils
+
+import android.net.MacAddress
+import android.util.Log
+import com.android.net.module.util.Ipv6Utils
+import com.android.net.module.util.NetworkStackConstants.ETHER_HEADER_LEN
+import com.android.net.module.util.NetworkStackConstants.ICMPV6_ND_OPTION_TLLA
+import com.android.net.module.util.NetworkStackConstants.NEIGHBOR_ADVERTISEMENT_FLAG_SOLICITED
+import com.android.net.module.util.Struct
+import com.android.net.module.util.structs.Icmpv6Header
+import com.android.net.module.util.structs.Ipv6Header
+import com.android.net.module.util.structs.LlaOption
+import com.android.net.module.util.structs.NsHeader
+import com.android.testutils.PacketReflector.IPV6_HEADER_LENGTH
+import java.lang.IllegalArgumentException
+import java.net.Inet6Address
+import java.nio.ByteBuffer
+
+private const val NS_TYPE = 135.toShort()
+
+/**
+ * A class that can be used to reply to Neighbor Solicitation packets on a [TapPacketReader].
+ */
+class NSResponder(
+    reader: TapPacketReader,
+    table: Map<Inet6Address, MacAddress>,
+    name: String = NSResponder::class.java.simpleName
+) : PacketResponder(reader, Icmpv6Filter(), name) {
+    companion object {
+        private val TAG = NSResponder::class.simpleName
+    }
+
+    // Copy the map if not already immutable (toMap) to make sure it is not modified
+    private val table = table.toMap()
+
+    override fun replyToPacket(packet: ByteArray, reader: TapPacketReader) {
+        if (packet.size < IPV6_HEADER_LENGTH) {
+            return
+        }
+        val buf = ByteBuffer.wrap(packet, ETHER_HEADER_LEN, packet.size - ETHER_HEADER_LEN)
+        val ipv6Header = parseOrLog(Ipv6Header::class.java, buf) ?: return
+        val icmpHeader = parseOrLog(Icmpv6Header::class.java, buf) ?: return
+        if (icmpHeader.type != NS_TYPE) {
+            return
+        }
+        val ns = parseOrLog(NsHeader::class.java, buf) ?: return
+        val replyMacAddr = table[ns.target] ?: return
+        val slla = parseOrLog(LlaOption::class.java, buf) ?: return
+        val requesterMac = slla.linkLayerAddress
+
+        val tlla = LlaOption.build(ICMPV6_ND_OPTION_TLLA.toByte(), replyMacAddr)
+        reader.sendResponse(Ipv6Utils.buildNaPacket(
+            replyMacAddr /* srcMac */,
+            requesterMac /* dstMac */,
+            ns.target /* srcIp */,
+            ipv6Header.srcIp /* dstIp */,
+            NEIGHBOR_ADVERTISEMENT_FLAG_SOLICITED,
+            ns.target,
+            tlla))
+    }
+
+    private fun <T> parseOrLog(clazz: Class<T>, buf: ByteBuffer): T? where T : Struct {
+        return try {
+            Struct.parse(clazz, buf)
+        } catch (e: IllegalArgumentException) {
+            Log.e(TAG, "Invalid ${clazz.simpleName} in ICMPv6 packet", e)
+            null
+        }
+    }
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/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 2d281fd..69fdbf8 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/DevSdkIgnoreRunner.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/DevSdkIgnoreRunner.kt
@@ -17,6 +17,7 @@
 package com.android.testutils
 
 import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.net.module.util.LinkPropertiesUtils.CompareOrUpdateResult
 import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
 import java.lang.reflect.Modifier
@@ -27,8 +28,10 @@
 import org.junit.runner.manipulation.NoTestsRemainException
 import org.junit.runner.manipulation.Sortable
 import org.junit.runner.manipulation.Sorter
+import org.junit.runner.notification.Failure
 import org.junit.runner.notification.RunNotifier
 import org.junit.runners.Parameterized
+import org.mockito.Mockito
 
 /**
  * A runner that can skip tests based on the development SDK as defined in [DevSdkIgnoreRule].
@@ -52,6 +55,9 @@
  *     class MyTestClass { ... }
  */
 class DevSdkIgnoreRunner(private val klass: Class<*>) : Runner(), Filterable, Sortable {
+    private val leakMonitorDesc = Description.createTestDescription(klass, "ThreadLeakMonitor")
+    private val shouldThreadLeakFailTest = klass.isAnnotationPresent(MonitorThreadLeak::class.java)
+
     // Inference correctly infers Runner & Filterable & Sortable for |baseRunner|, but the
     // Java bytecode doesn't have a way to express this. Give this type a name by wrapping it.
     private class RunnerWrapper<T>(private val wrapped: T) :
@@ -61,6 +67,10 @@
         override fun run(notifier: RunNotifier?) = wrapped.run(notifier)
     }
 
+    // Annotation for test classes to indicate the test runner should monitor thread leak.
+    // TODO(b/307693729): Remove this annotation and monitor thread leak by default.
+    annotation class MonitorThreadLeak
+
     private val baseRunner: RunnerWrapper<*>? = klass.let {
         val ignoreAfter = it.getAnnotation(IgnoreAfter::class.java)
         val ignoreUpTo = it.getAnnotation(IgnoreUpTo::class.java)
@@ -81,20 +91,68 @@
                 it.isAnnotationPresent(Parameterized.Parameters::class.java) }
 
     override fun run(notifier: RunNotifier) {
-        if (baseRunner != null) {
+        if (baseRunner == null) {
+            // Report a single, skipped placeholder test for this class, as the class is expected to
+            // report results when run. In practice runners that apply the Filterable implementation
+            // would see a NoTestsRemainException and not call the run method.
+            notifier.fireTestIgnored(
+                    Description.createTestDescription(klass, "skippedClassForDevSdkMismatch"))
+            return
+        }
+        if (!shouldThreadLeakFailTest) {
             baseRunner.run(notifier)
             return
         }
 
-        // Report a single, skipped placeholder test for this class, as the class is expected to
-        // report results when run. In practice runners that apply the Filterable implementation
-        // would see a NoTestsRemainException and not call the run method.
-        notifier.fireTestIgnored(
-                Description.createTestDescription(klass, "skippedClassForDevSdkMismatch"))
+        // Dump threads as a baseline to monitor thread leaks.
+        val threadCountsBeforeTest = getAllThreadNameCounts()
+
+        baseRunner.run(notifier)
+
+        notifier.fireTestStarted(leakMonitorDesc)
+        val threadCountsAfterTest = getAllThreadNameCounts()
+        // TODO : move CompareOrUpdateResult to its own util instead of LinkProperties.
+        val threadsDiff = CompareOrUpdateResult(
+                threadCountsBeforeTest.entries,
+                threadCountsAfterTest.entries
+        ) { it.key }
+        // Ignore removed threads, which typically are generated by previous tests.
+        // Because this is in the threadsDiff.updated member, for sure there is a
+        // corresponding key in threadCountsBeforeTest.
+        val increasedThreads = threadsDiff.updated
+                .filter { threadCountsBeforeTest[it.key]!! < it.value }
+        if (threadsDiff.added.isNotEmpty() || increasedThreads.isNotEmpty()) {
+            notifier.fireTestFailure(Failure(leakMonitorDesc,
+                    IllegalStateException("Unexpected thread changes: $threadsDiff")))
+        }
+        // Clears up internal state of all inline mocks.
+        // TODO: Call clearInlineMocks() at the end of each test.
+        Mockito.framework().clearInlineMocks()
+        notifier.fireTestFinished(leakMonitorDesc)
+    }
+
+    private fun getAllThreadNameCounts(): Map<String, Int> {
+        // Get the counts of threads in the group per name.
+        // Filter system thread groups.
+        // Also ignore threads with 1 count, this effectively filtered out threads created by the
+        // test runner or other system components. e.g. hwuiTask*, queued-work-looper,
+        // SurfaceSyncGroupTimer, RenderThread, Time-limited test, etc.
+        return Thread.getAllStackTraces().keys
+                .filter { it.threadGroup?.name != "system" }
+                .groupingBy { it.name }.eachCount()
+                .filter { it.value != 1 }
     }
 
     override fun getDescription(): Description {
-        return baseRunner?.description ?: Description.createSuiteDescription(klass)
+        if (baseRunner == null) {
+            return Description.createSuiteDescription(klass)
+        }
+
+        return baseRunner.description.also {
+            if (shouldThreadLeakFailTest) {
+                it.addChild(leakMonitorDesc)
+            }
+        }
     }
 
     /**
@@ -102,7 +160,9 @@
      */
     override fun testCount(): Int {
         // When ignoring the tests, a skipped placeholder test is reported, so test count is 1.
-        return baseRunner?.testCount() ?: 1
+        if (baseRunner == null) return 1
+
+        return baseRunner.testCount() + if (shouldThreadLeakFailTest) 1 else 0
     }
 
     @Throws(NoTestsRemainException::class)
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/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..1a2cc88 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,29 +32,26 @@
 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,
+    addresses: List<LinkAddress>,
     dnsAddr: InetAddress
 ) {
-    private val natMap = NatMap()
     private val binder = Binder()
 
     private val cm = context.getSystemService(ConnectivityManager::class.java)!!
     private val tnm = context.getSystemService(TestNetworkManager::class.java)!!
 
-    // Create test networks.
-    private val internalIface = tnm.createTunInterface(listOf(internalAddr))
-    private val externalIface = tnm.createTunInterface(listOf(externalAddr))
+    // 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
@@ -61,32 +59,20 @@
     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
     }
 
-    // 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(
-        internalFd,
-        1500,
-        externalFd,
-        externalAddr.address,
-        natMap
-    )
-    private val pr2 = NatExternalPacketForwarder(
-        externalFd,
-        1500,
-        internalFd,
-        externalAddr.address,
-        natMap
-    )
+    private val pr1 = PacketForwarder(internalFd, 1500, externalFd)
+    private val pr2 = PacketForwarder(externalFd, 1500, internalFd)
 
     fun start() {
         IoUtils.setBlocking(internalFd, true /* blocking */)
@@ -107,7 +93,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 +106,7 @@
         cm.requestNetwork(nr, testCb)
 
         val lp = LinkProperties().apply {
-            addLinkAddress(addr)
+            setLinkAddresses(addresses)
             interfaceName = testIface.interfaceName
             addDnsServer(dnsAddr)
         }
@@ -130,44 +116,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/PacketForwarder.java
similarity index 62%
rename from staticlibs/testutils/devicetests/com/android/testutils/NatPacketForwarderBase.java
rename to staticlibs/testutils/devicetests/com/android/testutils/PacketForwarder.java
index 0a2b5d4..d8efb7d 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/NatPacketForwarderBase.java
+++ b/staticlibs/testutils/devicetests/com/android/testutils/PacketForwarder.java
@@ -30,16 +30,13 @@
 import android.system.Os;
 import android.util.Log;
 
-import androidx.annotation.GuardedBy;
-
 import java.io.FileDescriptor;
 import java.io.IOException;
-import java.net.InetAddress;
 import java.util.Objects;
 
 /**
  * A class that forwards packets from a {@link TestNetworkInterface} to another
- * {@link TestNetworkInterface} with NAT.
+ * {@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,30 +51,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";
-    static final int DESTINATION_PORT_OFFSET = 2;
+public class PacketForwarder extends Thread {
+    private static final String TAG = "PacketForwarder";
 
     // The source fd to read packets from.
     @NonNull
@@ -88,27 +69,12 @@
     // The destination fd to write packets to.
     @NonNull
     final FileDescriptor mDstFd;
-    // The NAT mapping table shared between two NatPacketForwarder instances to map from
-    // the source port to the associated internal address. The map can be read/write from two
-    // different threads on any given time whenever receiving packets on the
-    // {@link TestNetworkInterface}. Thus, synchronize on the object when reading/writing is needed.
-    @GuardedBy("mNatMap")
-    @NonNull
-    final PacketBridge.NatMap mNatMap;
-    // The address of the external interface. See {@link NatPacketForwarder}.
-    @NonNull
-    final InetAddress mExtAddr;
 
     /**
-     * Construct a {@link NatPacketForwarderBase}.
+     * Construct a {@link PacketForwarder}.
      *
      * 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,29 +82,15 @@
      * @param srcFd   {@link FileDescriptor} to read packets from.
      * @param mtu     MTU of the test network.
      * @param dstFd   {@link FileDescriptor} to write packets to.
-     * @param extAddr the external address, which is the address of the external interface.
-     *                See {@link NatPacketForwarderBase}.
-     * @param natMap  the NAT mapping table shared between two {@link NatPacketForwarderBase}
-     *                instance.
      */
-    public NatPacketForwarderBase(@NonNull FileDescriptor srcFd, int mtu,
-            @NonNull FileDescriptor dstFd, @NonNull InetAddress extAddr,
-            @NonNull PacketBridge.NatMap natMap) {
+    public PacketForwarder(@NonNull FileDescriptor srcFd, int mtu,
+                           @NonNull FileDescriptor dstFd) {
         super(TAG);
         mSrcFd = Objects.requireNonNull(srcFd);
         mBuf = new byte[mtu];
         mDstFd = Objects.requireNonNull(dstFd);
-        mExtAddr = Objects.requireNonNull(extAddr);
-        mNatMap = Objects.requireNonNull(natMap);
     }
 
-    /**
-     * A method to prepare forwarding packets between two instances of {@link TestNetworkInterface},
-     * which includes re-write addresses, ports and fix up checksums.
-     * Subclasses should override this method to implement a simple NAT.
-     */
-    abstract void preparePacketForForwarding(@NonNull byte[] buf, int len, int version, int proto);
-
     private void forwardPacket(@NonNull byte[] buf, int len) {
         try {
             Os.write(mDstFd, buf, 0, len);
@@ -190,8 +142,9 @@
         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 addresses.
+        PacketReflectorUtil.swapAddresses(mBuf, version);
+
         // Send the packet to the destination fd.
         forwardPacket(mBuf, len);
     }
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/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/netstats/NetworkTemplateTest.kt b/tests/common/java/android/net/netstats/NetworkTemplateTest.kt
index fd7bd74..1b55be9 100644
--- a/tests/common/java/android/net/netstats/NetworkTemplateTest.kt
+++ b/tests/common/java/android/net/netstats/NetworkTemplateTest.kt
@@ -62,11 +62,6 @@
             }
         }
 
-        // Verify hidden match rules cannot construct templates.
-        assertFailsWith<IllegalArgumentException> {
-            NetworkTemplate.Builder(MATCH_PROXY).build()
-        }
-
         // Verify template which matches metered cellular and carrier networks with
         // the given IMSI. See buildTemplateMobileAll and buildTemplateCarrierMetered.
         listOf(MATCH_MOBILE, MATCH_CARRIER).forEach { matchRule ->
@@ -170,9 +165,9 @@
                     assertEquals(expectedTemplate, it)
                 }
 
-        // Verify template which matches ethernet and bluetooth networks.
+        // Verify template which matches ethernet, bluetooth and proxy networks.
         // See buildTemplateEthernet and buildTemplateBluetooth.
-        listOf(MATCH_ETHERNET, MATCH_BLUETOOTH).forEach { matchRule ->
+        listOf(MATCH_ETHERNET, MATCH_BLUETOOTH, MATCH_PROXY).forEach { matchRule ->
             NetworkTemplate.Builder(matchRule).build().let {
                 val expectedTemplate = NetworkTemplate(matchRule,
                         emptyArray<String>() /*subscriberIds*/, emptyArray<String>(),
diff --git a/tests/common/java/android/net/nsd/NsdServiceInfoTest.java b/tests/common/java/android/net/nsd/NsdServiceInfoTest.java
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..2688fb8 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"],
 }
 
@@ -43,7 +40,9 @@
     test_suites: [
         "cts",
         "general-tests",
-        "sts"
+        "mcts-tethering",
+        "mts-tethering",
+        "sts",
     ],
     data: [
         ":CtsHostsideNetworkTestsApp",
diff --git a/tests/cts/hostside/AndroidTest.xml b/tests/cts/hostside/AndroidTest.xml
index 90b7875..0ffe81e 100644
--- a/tests/cts/hostside/AndroidTest.xml
+++ b/tests/cts/hostside/AndroidTest.xml
@@ -36,6 +36,7 @@
     <target_preparer class="com.android.tradefed.targetprep.DeviceSetup">
         <option name="force-skip-system-props" value="true" />
         <option name="set-global-setting" key="verifier_verify_adb_installs" value="0" />
+        <option name="set-global-setting" key="low_power_standby_enabled" value="0" />
     </target_preparer>
 
     <test class="com.android.compatibility.common.tradefed.testtype.JarHostTest" >
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..d555491 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"],
 }
 
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/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..29aac3c 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,6 +16,9 @@
 
 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;
@@ -38,7 +41,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;
@@ -67,6 +69,7 @@
 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 org.junit.Rule;
 import org.junit.rules.RuleChain;
@@ -76,6 +79,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.
@@ -126,8 +130,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";
 
@@ -171,9 +173,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();
@@ -284,44 +283,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 +330,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 +923,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..4004789 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,8 @@
 package com.android.cts.net.hostside;
 
 
+import static android.app.ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE;
+
 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;
@@ -95,7 +97,7 @@
             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/NetworkCallbackTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkCallbackTest.java
index 82f4a65..eb2347d 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,7 @@
 
 package com.android.cts.net.hostside;
 
+import static android.app.ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED;
 import static android.net.NetworkCapabilities.SIGNAL_STRENGTH_UNSPECIFIED;
 
@@ -203,6 +204,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.
@@ -312,7 +314,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 +339,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);
             }
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..7aeca77 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,7 @@
 
 package com.android.cts.net.hostside;
 
+import static android.app.ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE;
 import static android.os.Process.SYSTEM_UID;
 
 import static com.android.cts.net.hostside.NetworkPolicyTestUtils.assertIsUidRestrictedOnMeteredNetworks;
@@ -137,13 +138,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 +220,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.
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/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/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 b86de25..98d5630 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,6 +55,7 @@
         "junit",
         "junit-params",
         "modules-utils-build",
+        "net-tests-utils",
         "net-utils-framework-common",
         "truth",
         "TetheringIntegrationTestsBaseLib",
@@ -62,10 +64,13 @@
     // uncomment when b/13249961 is fixed
     // sdk_version: "current",
     platform_apis: true,
-    data: [":ConnectivityTestPreparer"],
     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
@@ -128,6 +133,7 @@
         "cts",
         "general-tests",
         "mts-tethering",
+        "mcts-tethering",
     ],
 }
 
@@ -154,3 +160,17 @@
     package_name: "android.net.cts.maxtargetsdk30", // CTS package names must be unique.
     instrumentation_target_package: "android.net.cts.maxtargetsdk30",
 }
+
+android_test_helper_app {
+    name: "CtsCarrierServicePackage",
+    defaults: ["cts_defaults"],
+    package_name: "android.net.cts.carrierservicepackage",
+    manifest: "carrierservicepackage/AndroidManifest.xml",
+    srcs: ["carrierservicepackage/src/**/*.java"],
+    min_sdk_version: "30",
+    // Tag this module as a cts test artifact
+    test_suites: [
+        "cts",
+        "general-tests",
+    ],
+}
diff --git a/tests/cts/net/AndroidManifest.xml b/tests/cts/net/AndroidManifest.xml
index 68e36ff..098cc0a 100644
--- a/tests/cts/net/AndroidManifest.xml
+++ b/tests/cts/net/AndroidManifest.xml
@@ -36,6 +36,7 @@
     <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
     <uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />
     <uses-permission android:name="android.permission.PACKAGE_USAGE_STATS" />
+    <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
 
     <!-- This test also uses signature permissions through adopting the shell identity.
          The permissions acquired that way include (probably not exhaustive) :
@@ -46,6 +47,7 @@
                  android:usesCleartextTraffic="true">
         <uses-library android:name="android.test.runner" />
         <uses-library android:name="org.apache.http.legacy" android:required="false" />
+
     </application>
 
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
@@ -54,4 +56,3 @@
     </instrumentation>
 
 </manifest>
-
diff --git a/tests/cts/net/AndroidTestTemplate.xml b/tests/cts/net/AndroidTestTemplate.xml
index 8efa99f..38f26d8 100644
--- a/tests/cts/net/AndroidTestTemplate.xml
+++ b/tests/cts/net/AndroidTestTemplate.xml
@@ -27,6 +27,7 @@
     <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
         <option name="cleanup-apks" value="true" />
         <option name="test-file-name" value="{MODULE}.apk" />
+        <option name="test-file-name" value="CtsCarrierServicePackage.apk" />
     </target_preparer>
     <target_preparer class="com.android.testutils.ConnectivityTestTargetPreparer">
     </target_preparer>
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/carrierservicepackage/AndroidManifest.xml b/tests/cts/net/carrierservicepackage/AndroidManifest.xml
new file mode 100644
index 0000000..c2a45eb
--- /dev/null
+++ b/tests/cts/net/carrierservicepackage/AndroidManifest.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+     android:versionCode="1"
+     android:versionName="1.0.0"
+     package="android.net.cts.carrierservicepackage">
+    <uses-sdk android:minSdkVersion="30"
+              android:targetSdkVersion="33" />
+    <uses-permission android:name="android.permission.READ_PRIVILEGED_PHONE_STATE"/>
+    <application android:allowBackup="false"
+         android:directBootAware="true">
+        <service android:name=".DummyCarrierConfigService"
+                 android:permission="android.permission.BIND_CARRIER_SERVICES"
+                 android:exported="true">
+            <intent-filter>
+              <action android:name="android.service.carrier.CarrierService"/>
+            </intent-filter>
+        </service>
+    </application>
+
+</manifest>
diff --git a/tests/cts/net/carrierservicepackage/src/android/net/cts/carrierservicepackage/DummyCarrierConfigService.java b/tests/cts/net/carrierservicepackage/src/android/net/cts/carrierservicepackage/DummyCarrierConfigService.java
new file mode 100644
index 0000000..ca2015b
--- /dev/null
+++ b/tests/cts/net/carrierservicepackage/src/android/net/cts/carrierservicepackage/DummyCarrierConfigService.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.cts.carrierservicepackage;
+
+import android.content.Intent;
+import android.os.IBinder;
+import android.os.PersistableBundle;
+import android.service.carrier.CarrierIdentifier;
+import android.service.carrier.CarrierService;
+
+public class DummyCarrierConfigService extends CarrierService {
+    private static final String TAG = "DummyCarrierConfigService";
+
+    public DummyCarrierConfigService() {}
+
+    @Override
+    public PersistableBundle onLoadConfig(CarrierIdentifier id) {
+        return new PersistableBundle(); // Do nothing
+    }
+
+    @Override
+    public IBinder onBind(Intent intent) {
+        return super.onBind(intent);
+    }
+
+    @Override
+    public boolean onUnbind(Intent intent) {
+        return super.onUnbind(intent);
+    }
+}
+
diff --git a/tests/cts/net/jni/Android.bp b/tests/cts/net/jni/Android.bp
index 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 0a143c5..2646b60 100644
--- a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
@@ -278,9 +278,7 @@
     private static final int MIN_KEEPALIVE_INTERVAL = 10;
 
     private static final int NETWORK_CALLBACK_TIMEOUT_MS = 30_000;
-    // Timeout for waiting network to be validated. Set the timeout to 30s, which is more than
-    // DNS timeout.
-    // TODO(b/252972908): reset the original timer when aosp/2188755 is ramped up.
+    // Timeout for waiting network to be validated.
     private static final int LISTEN_ACTIVITY_TIMEOUT_MS = 30_000;
     private static final int NO_CALLBACK_TIMEOUT_MS = 100;
     private static final int NETWORK_REQUEST_TIMEOUT_MS = 3000;
@@ -728,6 +726,7 @@
         return mCm.getRedactedNetworkCapabilitiesForPackage(nc, uid, packageName);
     }
 
+    @ConnectivityModuleTest
     @DevSdkIgnoreRule.IgnoreUpTo(SC_V2)
     @AppModeFull(reason = "Cannot get installed packages in instant app mode")
     @Test
@@ -796,7 +795,7 @@
             // Make sure that the NC is null if the package doesn't hold ACCESS_NETWORK_STATE.
             assertNull(redactNc(nc, groundedUid, groundedPkg));
 
-            // Uids, ssid, underlying networks & subscriptionIds will be redacted if the given uid
+            // Uids, ssid & underlying networks will be redacted if the given uid
             // doesn't hold the associated permissions. The wifi transport info is also suitably
             // redacted.
             final NetworkCapabilities redactedNormal = redactNc(nc, normalUid, normalPkg);
@@ -1354,9 +1353,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. */
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 308aead..9ff0f2f 100644
--- a/tests/cts/net/src/android/net/cts/DnsResolverTest.java
+++ b/tests/cts/net/src/android/net/cts/DnsResolverTest.java
@@ -860,4 +860,9 @@
             assertEquals(DnsResolver.ERROR_SYSTEM, e.code);
         }
     }
+
+    @Test
+    public void testNoRawBinderAccess() {
+        assertNull(mContext.getSystemService("dnsresolver"));
+    }
 }
diff --git a/tests/cts/net/src/android/net/cts/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/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/NetworkAgentTest.kt b/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
index 392cba9..84b6745 100644
--- a/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
+++ b/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
@@ -15,9 +15,12 @@
  */
 package android.net.cts
 
+import android.Manifest.permission.MODIFY_PHONE_STATE
 import android.Manifest.permission.NETWORK_SETTINGS
+import android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE
 import android.app.Instrumentation
 import android.content.Context
+import android.content.pm.PackageManager
 import android.net.ConnectivityManager
 import android.net.EthernetNetworkSpecifier
 import android.net.INetworkAgent
@@ -44,7 +47,9 @@
 import android.net.NetworkCapabilities.NET_CAPABILITY_TEMPORARILY_NOT_METERED
 import android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED
 import android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED
+import android.net.NetworkCapabilities.TRANSPORT_BLUETOOTH
 import android.net.NetworkCapabilities.TRANSPORT_CELLULAR
+import android.net.NetworkCapabilities.TRANSPORT_ETHERNET
 import android.net.NetworkCapabilities.TRANSPORT_TEST
 import android.net.NetworkCapabilities.TRANSPORT_VPN
 import android.net.NetworkCapabilities.TRANSPORT_WIFI
@@ -53,6 +58,7 @@
 import android.net.NetworkReleasedException
 import android.net.NetworkRequest
 import android.net.NetworkScore
+import android.net.NetworkSpecifier
 import android.net.QosCallback
 import android.net.QosCallback.QosCallbackRegistrationException
 import android.net.QosCallbackException
@@ -61,6 +67,7 @@
 import android.net.QosSocketInfo
 import android.net.RouteInfo
 import android.net.SocketKeepalive
+import android.net.TelephonyNetworkSpecifier
 import android.net.TestNetworkInterface
 import android.net.TestNetworkManager
 import android.net.Uri
@@ -69,21 +76,31 @@
 import android.net.cts.NetworkAgentTest.TestableQosCallback.CallbackEntry.OnError
 import android.net.cts.NetworkAgentTest.TestableQosCallback.CallbackEntry.OnQosSessionAvailable
 import android.net.cts.NetworkAgentTest.TestableQosCallback.CallbackEntry.OnQosSessionLost
+import android.net.wifi.WifiInfo
 import android.os.Build
+import android.os.ConditionVariable
 import android.os.Handler
 import android.os.HandlerThread
 import android.os.Message
+import android.os.PersistableBundle
 import android.os.Process
 import android.os.SystemClock
 import android.platform.test.annotations.AppModeFull
 import android.system.OsConstants.IPPROTO_TCP
 import android.system.OsConstants.IPPROTO_UDP
+import android.telephony.CarrierConfigManager
+import android.telephony.SubscriptionManager
 import android.telephony.TelephonyManager
+import android.telephony.TelephonyManager.CarrierPrivilegesCallback
 import android.telephony.data.EpsBearerQosSessionAttributes
+import android.util.ArraySet
 import android.util.DebugUtils.valueToString
+import android.util.Log
 import androidx.test.InstrumentationRegistry
+import com.android.compatibility.common.util.SystemUtil.runShellCommand
 import com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity
 import com.android.compatibility.common.util.ThrowingSupplier
+import com.android.compatibility.common.util.UiccUtil
 import com.android.modules.utils.build.SdkLevel
 import com.android.net.module.util.ArrayTrackRecord
 import com.android.testutils.CompatUtil
@@ -112,12 +129,15 @@
 import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnValidationStatus
 import com.android.testutils.TestableNetworkCallback
 import com.android.testutils.assertThrows
+import com.android.testutils.runAsShell
+import com.android.testutils.tryTest
 import java.io.Closeable
 import java.io.IOException
 import java.net.DatagramSocket
 import java.net.InetAddress
 import java.net.InetSocketAddress
 import java.net.Socket
+import java.security.MessageDigest
 import java.time.Duration
 import java.util.Arrays
 import java.util.UUID
@@ -141,6 +161,7 @@
 import org.mockito.Mockito.timeout
 import org.mockito.Mockito.verify
 
+private const val TAG = "NetworkAgentTest"
 // This test doesn't really have a constraint on how fast the methods should return. If it's
 // going to fail, it will simply wait forever, so setting a high timeout lowers the flake ratio
 // without affecting the run time of successful runs. Thus, set a very high timeout.
@@ -261,17 +282,18 @@
         callbacksToCleanUp.add(callback)
     }
 
-    private fun makeTestNetworkRequest(specifier: String? = null): NetworkRequest {
-        return NetworkRequest.Builder()
-                .clearCapabilities()
-                .addTransportType(TRANSPORT_TEST)
-                .also {
-                    if (specifier != null) {
-                        it.setNetworkSpecifier(CompatUtil.makeEthernetNetworkSpecifier(specifier))
-                    }
-                }
-                .build()
-    }
+    private fun String?.asEthSpecifier(): NetworkSpecifier? =
+            if (null == this) null else CompatUtil.makeEthernetNetworkSpecifier(this)
+    private fun makeTestNetworkRequest(specifier: NetworkSpecifier? = null) =
+            NetworkRequest.Builder().run {
+                clearCapabilities()
+                addTransportType(TRANSPORT_TEST)
+                if (specifier != null) setNetworkSpecifier(specifier)
+                build()
+            }
+
+    private fun makeTestNetworkRequest(specifier: String?) =
+            makeTestNetworkRequest(specifier.asEthSpecifier())
 
     private fun makeTestNetworkCapabilities(
         specifier: String? = null,
@@ -322,7 +344,7 @@
     ): Pair<TestableNetworkAgent, TestableNetworkCallback> {
         val callback = TestableNetworkCallback()
         // Ensure this NetworkAgent is never unneeded by filing a request with its specifier.
-        requestNetwork(makeTestNetworkRequest(specifier = specifier), callback)
+        requestNetwork(makeTestNetworkRequest(specifier), callback)
         val nc = makeTestNetworkCapabilities(specifier, transports)
         val agent = createNetworkAgent(context, initialConfig = initialConfig, initialNc = nc)
         agent.setTeardownDelayMillis(0)
@@ -543,6 +565,234 @@
                 .addTransportType(TRANSPORT_TEST)
                 .setAllowedUids(uids.toSet()).build()
 
+    /**
+     * Get the single element from this ArraySet, or fail() if doesn't contain exactly 1 element.
+     */
+    fun <T> ArraySet<T>.getSingleElement(): T {
+        if (size != 1) fail("Expected exactly one element, contained $size")
+        return iterator().next()
+    }
+
+    private fun doTestAllowedUids(
+            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
+    ) {
+        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) }
+            addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+            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())
+            }
+            setAllowedUids(setOf(uid))
+            setOwnerUid(Process.myUid())
+            setAdministratorUids(intArrayOf(Process.myUid()))
+            build()
+        })
+        runWithShellPermissionIdentity {
+            agent.register()
+        }
+        agent.markConnected()
+
+        registerNetworkCallback(makeTestNetworkRequest(specifier), callback)
+        callback.expect<Available>(agent.network!!)
+        callback.expect<CapabilitiesChanged>(agent.network!!) {
+            if (expectUidsPresent) {
+                it.caps.allowedUidsNoCopy.getSingleElement() == uid
+            } else {
+                it.caps.allowedUidsNoCopy.isEmpty()
+            }
+        }
+        agent.unregister()
+        callback.eventuallyExpect<Lost> { it.network == agent.network }
+        // callback will be unregistered in tearDown()
+    }
+
+    private fun setHoldCarrierPrivilege(hold: Boolean, subId: Int) {
+        fun getCertHash(): String {
+            val pkgInfo = realContext.packageManager.getPackageInfo(realContext.opPackageName,
+                    PackageManager.GET_SIGNATURES)
+            val digest = MessageDigest.getInstance("SHA-256")
+            val certHash = digest.digest(pkgInfo.signatures!![0]!!.toByteArray())
+            return UiccUtil.bytesToHexString(certHash)!!
+        }
+
+        val tm = realContext.getSystemService(TelephonyManager::class.java)!!
+        val ccm = realContext.getSystemService(CarrierConfigManager::class.java)!!
+
+        val cv = ConditionVariable()
+        val cpb = PrivilegeWaiterCallback(cv)
+        tryTest {
+            val slotIndex = SubscriptionManager.getSlotIndex(subId)!!
+            runAsShell(READ_PRIVILEGED_PHONE_STATE) {
+                tm.registerCarrierPrivilegesCallback(slotIndex, { it.run() }, cpb)
+            }
+            // Wait for the callback to be registered
+            assertTrue(cv.block(DEFAULT_TIMEOUT_MS), "Can't register CarrierPrivilegesCallback")
+            if (cpb.hasPrivilege == hold) {
+                if (hold) {
+                    Log.w(TAG, "Package ${realContext.opPackageName} already is privileged")
+                } else {
+                    Log.w(TAG, "Package ${realContext.opPackageName} already isn't privileged")
+                }
+                return@tryTest
+            }
+            cv.close()
+            runAsShell(MODIFY_PHONE_STATE) {
+                val carrierConfigs = if (hold) {
+                    PersistableBundle().also {
+                        it.putStringArray(CarrierConfigManager.KEY_CARRIER_CERTIFICATE_STRING_ARRAY,
+                                arrayOf(getCertHash()))
+                    }
+                } else {
+                    null
+                }
+                ccm.overrideConfig(subId, carrierConfigs)
+            }
+            assertTrue(cv.block(DEFAULT_TIMEOUT_MS), "Can't change carrier privilege")
+        } cleanup {
+            runAsShell(READ_PRIVILEGED_PHONE_STATE) {
+                tm.unregisterCarrierPrivilegesCallback(cpb)
+            }
+        }
+    }
+
+    private fun acquireCarrierPrivilege(subId: Int) = setHoldCarrierPrivilege(true, subId)
+    private fun dropCarrierPrivilege(subId: Int) = setHoldCarrierPrivilege(false, subId)
+
+    private fun setCarrierServicePackageOverride(subId: Int, pkg: String?) {
+        val tm = realContext.getSystemService(TelephonyManager::class.java)!!
+
+        val cv = ConditionVariable()
+        val cpb = CarrierServiceChangedWaiterCallback(cv)
+        tryTest {
+            val slotIndex = SubscriptionManager.getSlotIndex(subId)!!
+            runAsShell(READ_PRIVILEGED_PHONE_STATE) {
+                tm.registerCarrierPrivilegesCallback(slotIndex, { it.run() }, cpb)
+            }
+            // Wait for the callback to be registered
+            assertTrue(cv.block(DEFAULT_TIMEOUT_MS), "Can't register CarrierPrivilegesCallback")
+            if (cpb.pkgName == pkg) {
+                Log.w(TAG, "Carrier service package was already $pkg")
+                return@tryTest
+            }
+            cv.close()
+            runAsShell(MODIFY_PHONE_STATE) {
+                if (null == pkg) {
+                    // There is a bug is clear-carrier-service-package-override where not adding
+                    // the -s argument will use the wrong slot index : b/299604822
+                    runShellCommand("cmd phone clear-carrier-service-package-override" +
+                            " -s $subId")
+                } else {
+                    // -s could set the subId, but this test works with the default subId.
+                    runShellCommand("cmd phone set-carrier-service-package-override $pkg")
+                }
+            }
+            assertTrue(cv.block(DEFAULT_TIMEOUT_MS), "Can't modify carrier service package")
+        } cleanup {
+            runAsShell(READ_PRIVILEGED_PHONE_STATE) {
+                tm.unregisterCarrierPrivilegesCallback(cpb)
+            }
+        }
+    }
+
+    private fun String.execute() = runShellCommand(this).trim()
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.S)
+    fun testAllowedUids() {
+        // Use a different package than this one to make sure that a package that doesn't hold
+        // carrier service permission can be set as an allowed UID.
+        val servicePackage = "android.net.cts.carrierservicepackage"
+        val uid = try {
+            realContext.packageManager.getApplicationInfo(servicePackage, 0).uid
+        } catch (e: PackageManager.NameNotFoundException) {
+            fail("$servicePackage could not be installed, please check the SuiteApkInstaller" +
+                    " installed CtsCarrierServicePackage.apk", e)
+        }
+
+        val tm = realContext.getSystemService(TelephonyManager::class.java)!!
+        val defaultSubId = SubscriptionManager.getDefaultSubscriptionId()
+        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)
+
+            // The tools to set the carrier service package override do not exist before U,
+            // so there is no way to test the rest of this test on < U.
+            if (!SdkLevel.isAtLeastU()) return@tryTest
+            // Acquiring carrier privilege is necessary to override the carrier service package.
+            val defaultSlotIndex = SubscriptionManager.getSlotIndex(defaultSubId)
+            acquireCarrierPrivilege(defaultSubId)
+            setCarrierServicePackageOverride(defaultSubId, servicePackage)
+            val actualServicePackage: String? = runAsShell(READ_PRIVILEGED_PHONE_STATE) {
+                tm.getCarrierServicePackageNameForLogicalSlot(defaultSlotIndex)
+            }
+            assertEquals(servicePackage, actualServicePackage)
+
+            // Wait for CarrierServiceAuthenticator to have seen the update of the service package
+            val timeout = SystemClock.elapsedRealtime() + DEFAULT_TIMEOUT_MS
+            while (true) {
+                if (SystemClock.elapsedRealtime() > timeout) {
+                    fail("Couldn't make $servicePackage the service package for $defaultSubId: " +
+                            "dumpsys connectivity".execute().split("\n")
+                                    .filter { it.contains("Logical slot = $defaultSlotIndex.*") })
+                }
+                if ("dumpsys connectivity"
+                        .execute()
+                        .split("\n")
+                        .filter { it.contains("Logical slot = $defaultSlotIndex : uid = $uid") }
+                        .isNotEmpty()) {
+                    // Found the configuration
+                    break
+                }
+                Thread.sleep(500)
+            }
+
+            // Cell and WiFi are allowed to set UIDs, but not Bluetooth or agents with multiple
+            // transports.
+            // TODO(b/315136340): Allow ownerUid to see allowedUids and enable below test case
+            // doTestAllowedUids(defaultSubId, TRANSPORT_CELLULAR, uid, expectUidsPresent = true)
+            if (SdkLevel.isAtLeastV()) {
+                // Cannot be tested before V because WifiInfo.Builder#setSubscriptionId doesn't
+                // exist
+                // TODO(b/315136340): Allow ownerUid to see allowedUids and enable below test case
+                // doTestAllowedUids(defaultSubId, TRANSPORT_WIFI, uid, expectUidsPresent = true)
+            }
+            doTestAllowedUids(defaultSubId, TRANSPORT_BLUETOOTH, uid, expectUidsPresent = false)
+            doTestAllowedUids(defaultSubId, intArrayOf(TRANSPORT_CELLULAR, TRANSPORT_WIFI), uid,
+                    expectUidsPresent = false)
+        } cleanupStep {
+            if (SdkLevel.isAtLeastU()) setCarrierServicePackageOverride(defaultSubId, null)
+        } cleanup {
+            if (SdkLevel.isAtLeastU()) dropCarrierPrivilege(defaultSubId)
+        }
+    }
+
     @Test
     fun testRejectedUpdates() {
         val callback = TestableNetworkCallback(DEFAULT_TIMEOUT_MS)
@@ -704,7 +954,6 @@
                 argThat<NetworkInfo> { it.detailedState == NetworkInfo.DetailedState.CONNECTING },
                 any(LinkProperties::class.java),
                 any(NetworkCapabilities::class.java),
-                any(), // LocalNetworkConfig TODO : specify when it's public
                 any(NetworkScore::class.java),
                 any(NetworkAgentConfig::class.java),
                 eq(NetworkProvider.ID_NONE))
@@ -1497,3 +1746,25 @@
         doTestNativeNetworkCreation(expectCreatedImmediately = true, intArrayOf(TRANSPORT_VPN))
     }
 }
+
+// Subclasses of CarrierPrivilegesCallback can't be inline, or they'll be compiled as
+// inner classes of the test class and will fail resolution on R as the test harness
+// uses reflection to list all methods and classes
+class PrivilegeWaiterCallback(private val cv: ConditionVariable) :
+        CarrierPrivilegesCallback {
+    var hasPrivilege = false
+    override fun onCarrierPrivilegesChanged(p: MutableSet<String>, uids: MutableSet<Int>) {
+        hasPrivilege = uids.contains(Process.myUid())
+        cv.open()
+    }
+}
+
+class CarrierServiceChangedWaiterCallback(private val cv: ConditionVariable) :
+        CarrierPrivilegesCallback {
+    var pkgName: String? = null
+    override fun onCarrierPrivilegesChanged(p: MutableSet<String>, u: MutableSet<Int>) {}
+    override fun onCarrierServiceChanged(pkgName: String?, uid: Int) {
+        this.pkgName = pkgName
+        cv.open()
+    }
+}
diff --git a/tests/cts/net/src/android/net/cts/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 9c44a3e..9aa3c84 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,11 +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.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
@@ -107,6 +114,7 @@
 import kotlin.math.min
 import kotlin.test.assertEquals
 import kotlin.test.assertFailsWith
+import kotlin.test.assertNotEquals
 import kotlin.test.assertNotNull
 import kotlin.test.assertNull
 import kotlin.test.fail
@@ -119,6 +127,7 @@
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
+import kotlin.test.assertNotEquals
 
 private const val TAG = "NsdManagerTest"
 private const val TIMEOUT_MS = 2000L
@@ -131,6 +140,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)
@@ -142,6 +152,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")
@@ -150,7 +163,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) }
 
@@ -676,11 +693,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)
@@ -843,6 +861,8 @@
         checkConnectSocketToMdnsd(shouldFail = false)
     }
 
+    // Native mdns powered by Netd is removed after U.
+    @DevSdkIgnoreRule.IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
     @Test @CtsNetTestCasesMaxTargetSdk30("Socket is started with the service up to target SDK 30")
     fun testManagerCreatesLegacySocket() {
         nsdManager // Ensure the lazy-init member is initialized, so NsdManager is created
@@ -989,6 +1009,154 @@
     }
 
     @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 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())
@@ -1026,6 +1194,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())
@@ -1100,6 +1345,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.
@@ -1158,7 +1518,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)
 
@@ -1221,6 +1582,274 @@
                 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>()
+        }
+    }
+
+    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:
@@ -1237,6 +1866,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.
@@ -1253,6 +1898,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)
@@ -1265,7 +1923,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)
@@ -1280,7 +1941,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)
@@ -1302,6 +1963,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)
@@ -1336,3 +2009,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/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/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/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt b/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt
index 9b082a4..9b1bf6e 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,34 @@
 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.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,12 +80,10 @@
 import org.mockito.Mock
 import org.mockito.Mockito.any
 import org.mockito.Mockito.anyInt
-import org.mockito.Mockito.doAnswer
 import org.mockito.Mockito.doNothing
 import org.mockito.Mockito.doReturn
 import org.mockito.Mockito.eq
 import org.mockito.Mockito.mock
-import org.mockito.Mockito.spy
 import org.mockito.MockitoAnnotations
 import org.mockito.Spy
 
@@ -90,7 +94,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 +122,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 +204,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 +215,51 @@
     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
+        ): CarrierPrivilegeAuthenticator {
+            return CarrierPrivilegeAuthenticator(context,
+                object : CarrierPrivilegeAuthenticator.Dependencies() {
+                    override fun makeHandlerThread(): HandlerThread =
+                        super.makeHandlerThread().also { handlerThreads.add(it) }
+                },
+                tm, TelephonyManagerShimImpl.newInstance(tm))
+        }
     }
 
     @After
     fun tearDown() {
         nsInstrumentation.clearAllState()
         ConnectivityResources.setResourcesContextForTest(null)
+        handlerThreads.forEach {
+            it.quitSafely()
+            it.join()
+        }
+        handlerThreads.clear()
     }
 
     @Test
@@ -254,8 +281,18 @@
         na.addCapability(NET_CAPABILITY_INTERNET)
         na.connect()
 
-        testCallback.expectAvailableThenValidatedCallbacks(na.network, TEST_TIMEOUT_MS)
-        assertEquals(2, nsInstrumentation.getRequestUrls().size)
+        tryTest {
+            testCallback.expectAvailableThenValidatedCallbacks(na.network, TEST_TIMEOUT_MS)
+            val requestedSize = nsInstrumentation.getRequestUrls().size
+            if (requestedSize == 2 || (requestedSize == 1 &&
+                        nsInstrumentation.getRequestUrls()[0] == httpsProbeUrl)
+            ) {
+                return@tryTest
+            }
+            fail("Unexpected request urls: ${nsInstrumentation.getRequestUrls()}")
+        } cleanup {
+            na.destroy()
+        }
     }
 
     @Test
@@ -287,24 +324,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/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..2f761d7 100644
--- a/tests/native/utilities/Android.bp
+++ b/tests/native/utilities/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"],
 }
 
@@ -21,7 +22,7 @@
     name: "libconnectivity_native_test_utils",
     defaults: [
         "netd_defaults",
-        "resolv_test_defaults"
+        "resolv_test_defaults",
     ],
     srcs: [
         "firewall.cpp",
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..4a1298f 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"
@@ -73,7 +74,7 @@
         "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 +116,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/BpfNetMapsReaderTest.kt b/tests/unit/java/android/net/BpfNetMapsReaderTest.kt
index 258e422..8919666 100644
--- a/tests/unit/java/android/net/BpfNetMapsReaderTest.kt
+++ b/tests/unit/java/android/net/BpfNetMapsReaderTest.kt
@@ -16,6 +16,9 @@
 
 package android.net
 
+import android.net.BpfNetMapsConstants.DATA_SAVER_DISABLED
+import android.net.BpfNetMapsConstants.DATA_SAVER_ENABLED
+import android.net.BpfNetMapsConstants.DATA_SAVER_ENABLED_KEY
 import android.net.BpfNetMapsConstants.DOZABLE_MATCH
 import android.net.BpfNetMapsConstants.HAPPY_BOX_MATCH
 import android.net.BpfNetMapsConstants.PENALTY_BOX_MATCH
@@ -26,6 +29,8 @@
 import com.android.net.module.util.IBpfMap
 import com.android.net.module.util.Struct.S32
 import com.android.net.module.util.Struct.U32
+import com.android.net.module.util.Struct.U8
+import com.android.testutils.DevSdkIgnoreRule
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
 import com.android.testutils.DevSdkIgnoreRunner
 import com.android.testutils.TestBpfMap
@@ -33,6 +38,7 @@
 import kotlin.test.assertEquals
 import kotlin.test.assertFalse
 import kotlin.test.assertTrue
+import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 
@@ -45,17 +51,24 @@
 @RunWith(DevSdkIgnoreRunner::class)
 @IgnoreUpTo(VERSION_CODES.S_V2)
 class BpfNetMapsReaderTest {
+    @Rule
+    @JvmField
+    val ignoreRule = DevSdkIgnoreRule()
+
     private val testConfigurationMap: IBpfMap<S32, U32> = TestBpfMap()
     private val testUidOwnerMap: IBpfMap<S32, UidOwnerValue> = TestBpfMap()
+    private val testDataSaverEnabledMap: IBpfMap<S32, U8> = TestBpfMap()
     private val bpfNetMapsReader = BpfNetMapsReader(
-        TestDependencies(testConfigurationMap, testUidOwnerMap))
+        TestDependencies(testConfigurationMap, testUidOwnerMap, testDataSaverEnabledMap))
 
     class TestDependencies(
         private val configMap: IBpfMap<S32, U32>,
-        private val uidOwnerMap: IBpfMap<S32, UidOwnerValue>
+        private val uidOwnerMap: IBpfMap<S32, UidOwnerValue>,
+        private val dataSaverEnabledMap: IBpfMap<S32, U8>
     ) : BpfNetMapsReader.Dependencies() {
         override fun getConfigurationMap() = configMap
         override fun getUidOwnerMap() = uidOwnerMap
+        override fun getDataSaverEnabledMap() = dataSaverEnabledMap
     }
 
     private fun doTestIsChainEnabled(chain: Int) {
@@ -199,4 +212,12 @@
         assertFalse(isUidNetworkingBlocked(TEST_UID2))
         assertFalse(isUidNetworkingBlocked(TEST_UID3))
     }
+
+    @Test
+    fun testGetDataSaverEnabled() {
+        testDataSaverEnabledMap.updateEntry(DATA_SAVER_ENABLED_KEY, U8(DATA_SAVER_DISABLED))
+        assertFalse(bpfNetMapsReader.dataSaverEnabled)
+        testDataSaverEnabledMap.updateEntry(DATA_SAVER_ENABLED_KEY, U8(DATA_SAVER_ENABLED))
+        assertTrue(bpfNetMapsReader.dataSaverEnabled)
+    }
 }
diff --git a/tests/unit/java/android/net/ConnectivityManagerTest.java b/tests/unit/java/android/net/ConnectivityManagerTest.java
index b8c5447..b71a46f 100644
--- a/tests/unit/java/android/net/ConnectivityManagerTest.java
+++ b/tests/unit/java/android/net/ConnectivityManagerTest.java
@@ -16,13 +16,6 @@
 
 package android.net;
 
-import static android.content.Context.RECEIVER_NOT_EXPORTED;
-import static android.content.pm.ApplicationInfo.FLAG_PERSISTENT;
-import static android.content.pm.ApplicationInfo.FLAG_SYSTEM;
-import static android.net.ConnectivityManager.ACTION_RESTRICT_BACKGROUND_CHANGED;
-import static android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_DISABLED;
-import static android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED;
-import static android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_WHITELISTED;
 import static android.net.ConnectivityManager.TYPE_NONE;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_CBS;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_DUN;
@@ -46,7 +39,6 @@
 
 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;
@@ -70,10 +62,7 @@
 
 import android.app.PendingIntent;
 import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
 import android.content.pm.ApplicationInfo;
-import android.net.ConnectivityManager.DataSaverStatusTracker;
 import android.net.ConnectivityManager.NetworkCallback;
 import android.os.Build.VERSION_CODES;
 import android.os.Bundle;
@@ -90,6 +79,7 @@
 import com.android.testutils.DevSdkIgnoreRunner;
 
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
@@ -102,6 +92,8 @@
 @SmallTest
 @DevSdkIgnoreRule.IgnoreUpTo(VERSION_CODES.R)
 public class ConnectivityManagerTest {
+    @Rule
+    public final DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule();
     private static final int TIMEOUT_MS = 30_000;
     private static final int SHORT_TIMEOUT_MS = 150;
 
@@ -524,43 +516,6 @@
                     + " attempts", ref.get());
     }
 
-    @Test
-    public void testDataSaverStatusTracker() {
-        mockService(NetworkPolicyManager.class, Context.NETWORK_POLICY_SERVICE, mNpm);
-        // Mock proper application info.
-        doReturn(mCtx).when(mCtx).getApplicationContext();
-        final ApplicationInfo mockAppInfo = new ApplicationInfo();
-        mockAppInfo.flags = FLAG_PERSISTENT | FLAG_SYSTEM;
-        doReturn(mockAppInfo).when(mCtx).getApplicationInfo();
-        // Enable data saver.
-        doReturn(RESTRICT_BACKGROUND_STATUS_ENABLED).when(mNpm)
-                .getRestrictBackgroundStatus(anyInt());
-
-        final DataSaverStatusTracker tracker = new DataSaverStatusTracker(mCtx);
-        // Verify the data saver status is correct right after initialization.
-        assertTrue(tracker.getDataSaverEnabled());
-
-        // Verify the tracker register receiver with expected intent filter.
-        final ArgumentCaptor<IntentFilter> intentFilterCaptor =
-                ArgumentCaptor.forClass(IntentFilter.class);
-        verify(mCtx).registerReceiver(
-                any(), intentFilterCaptor.capture(), eq(RECEIVER_NOT_EXPORTED));
-        assertEquals(ACTION_RESTRICT_BACKGROUND_CHANGED,
-                intentFilterCaptor.getValue().getAction(0));
-
-        // Mock data saver status changed event and verify the tracker tracks the
-        // status accordingly.
-        doReturn(RESTRICT_BACKGROUND_STATUS_DISABLED).when(mNpm)
-                .getRestrictBackgroundStatus(anyInt());
-        tracker.onReceive(mCtx, new Intent(ACTION_RESTRICT_BACKGROUND_CHANGED));
-        assertFalse(tracker.getDataSaverEnabled());
-
-        doReturn(RESTRICT_BACKGROUND_STATUS_WHITELISTED).when(mNpm)
-                .getRestrictBackgroundStatus(anyInt());
-        tracker.onReceive(mCtx, new Intent(ACTION_RESTRICT_BACKGROUND_CHANGED));
-        assertTrue(tracker.getDataSaverEnabled());
-    }
-
     private <T> void mockService(Class<T> clazz, String name, T service) {
         doReturn(service).when(mCtx).getSystemService(name);
         doReturn(name).when(mCtx).getSystemServiceName(clazz);
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 a28245d..e453c02 100644
--- a/tests/unit/java/android/net/NetworkUtilsTest.java
+++ b/tests/unit/java/android/net/NetworkUtilsTest.java
@@ -16,19 +16,37 @@
 
 package android.net;
 
+import static android.system.OsConstants.AF_INET6;
+import static android.system.OsConstants.IPPROTO_ICMPV6;
+import static android.system.OsConstants.SOCK_DGRAM;
+import static android.system.OsConstants.SOL_SOCKET;
+import static android.system.OsConstants.SO_RCVTIMEO;
+
+import static com.android.compatibility.common.util.PropertyUtil.getVsrApiLevel;
+
 import static junit.framework.Assert.assertEquals;
 
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeTrue;
+
 import android.os.Build;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.system.StructTimeval;
 
 import androidx.test.filters.SmallTest;
 
+import com.android.net.module.util.SocketUtils;
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRunner;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.io.FileDescriptor;
 import java.math.BigInteger;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
 import java.util.TreeSet;
 
 @RunWith(DevSdkIgnoreRunner.class)
@@ -131,4 +149,42 @@
         assertEquals(BigInteger.valueOf(7l - 4 + 4 + 16 + 65536),
                 NetworkUtils.routedIPv6AddressCount(set));
     }
+
+    private byte[] getTimevalBytes(StructTimeval tv) {
+        byte[] timeval = new byte[16];
+        ByteBuffer buf = ByteBuffer.wrap(timeval);
+        buf.order(ByteOrder.nativeOrder());
+        buf.putLong(tv.tv_sec);
+        buf.putLong(tv.tv_usec);
+        return timeval;
+    }
+
+    private void testSetSockOptBytes(FileDescriptor sock, long timeValMillis)
+            throws ErrnoException {
+        final StructTimeval writeTimeval = StructTimeval.fromMillis(timeValMillis);
+        byte[] timeval = getTimevalBytes(writeTimeval);
+        final StructTimeval readTimeval;
+
+        NetworkUtils.setsockoptBytes(sock, SOL_SOCKET, SO_RCVTIMEO, timeval);
+        readTimeval = Os.getsockoptTimeval(sock, SOL_SOCKET, SO_RCVTIMEO);
+
+        assertEquals(writeTimeval, readTimeval);
+    }
+
+    @Test
+    public void testSetSockOptBytes() throws ErrnoException {
+        final FileDescriptor sock = Os.socket(AF_INET6, SOCK_DGRAM, IPPROTO_ICMPV6);
+
+        testSetSockOptBytes(sock, 3000);
+
+        testSetSockOptBytes(sock, 5000);
+
+        SocketUtils.closeSocketQuietly(sock);
+    }
+
+    @Test
+    public void testIsKernel64Bit() {
+        assumeTrue(getVsrApiLevel() > Build.VERSION_CODES.TIRAMISU);
+        assertTrue(NetworkUtils.isKernel64Bit());
+    }
 }
diff --git a/tests/unit/java/android/net/nsd/NsdManagerTest.java b/tests/unit/java/android/net/nsd/NsdManagerTest.java
index 0965193..951675c 100644
--- a/tests/unit/java/android/net/nsd/NsdManagerTest.java
+++ b/tests/unit/java/android/net/nsd/NsdManagerTest.java
@@ -38,6 +38,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 +52,10 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
+import java.net.InetAddress;
+import java.util.List;
+
+@DevSdkIgnoreRunner.MonitorThreadLeak
 @RunWith(DevSdkIgnoreRunner.class)
 @SmallTest
 @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
@@ -85,73 +90,81 @@
     @Test
     @EnableCompatChanges(ConnectivityCompatChanges.RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER)
     public void testResolveServiceS() throws Exception {
-        verify(mServiceConn, never()).startDaemon();
+        verifyDaemonStarted(/* targetSdkPreS= */ false);
         doTestResolveService();
     }
 
     @Test
     @DisableCompatChanges(ConnectivityCompatChanges.RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER)
     public void testResolveServicePreS() throws Exception {
-        verify(mServiceConn).startDaemon();
+        verifyDaemonStarted(/* targetSdkPreS= */ true);
         doTestResolveService();
     }
 
     @Test
     @EnableCompatChanges(ConnectivityCompatChanges.RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER)
     public void testDiscoverServiceS() throws Exception {
-        verify(mServiceConn, never()).startDaemon();
+        verifyDaemonStarted(/* targetSdkPreS= */ false);
         doTestDiscoverService();
     }
 
     @Test
     @DisableCompatChanges(ConnectivityCompatChanges.RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER)
     public void testDiscoverServicePreS() throws Exception {
-        verify(mServiceConn).startDaemon();
+        verifyDaemonStarted(/* targetSdkPreS= */ true);
         doTestDiscoverService();
     }
 
     @Test
     @EnableCompatChanges(ConnectivityCompatChanges.RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER)
     public void testParallelResolveServiceS() throws Exception {
-        verify(mServiceConn, never()).startDaemon();
+        verifyDaemonStarted(/* targetSdkPreS= */ false);
         doTestParallelResolveService();
     }
 
     @Test
     @DisableCompatChanges(ConnectivityCompatChanges.RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER)
     public void testParallelResolveServicePreS() throws Exception {
-        verify(mServiceConn).startDaemon();
+        verifyDaemonStarted(/* targetSdkPreS= */ true);
         doTestParallelResolveService();
     }
 
     @Test
     @EnableCompatChanges(ConnectivityCompatChanges.RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER)
     public void testInvalidCallsS() throws Exception {
-        verify(mServiceConn, never()).startDaemon();
+        verifyDaemonStarted(/* targetSdkPreS= */ false);
         doTestInvalidCalls();
     }
 
     @Test
     @DisableCompatChanges(ConnectivityCompatChanges.RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER)
     public void testInvalidCallsPreS() throws Exception {
-        verify(mServiceConn).startDaemon();
+        verifyDaemonStarted(/* targetSdkPreS= */ true);
         doTestInvalidCalls();
     }
 
     @Test
     @EnableCompatChanges(ConnectivityCompatChanges.RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER)
     public void testRegisterServiceS() throws Exception {
-        verify(mServiceConn, never()).startDaemon();
+        verifyDaemonStarted(/* targetSdkPreS= */ false);
         doTestRegisterService();
     }
 
     @Test
     @DisableCompatChanges(ConnectivityCompatChanges.RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER)
     public void testRegisterServicePreS() throws Exception {
-        verify(mServiceConn).startDaemon();
+        verifyDaemonStarted(/* targetSdkPreS= */ true);
         doTestRegisterService();
     }
 
+    private void verifyDaemonStarted(boolean targetSdkPreS) throws Exception {
+        if (targetSdkPreS && !SdkLevel.isAtLeastV()) {
+            verify(mServiceConn).startDaemon();
+        } else {
+            verify(mServiceConn, never()).startDaemon();
+        }
+    }
+
     private void doTestResolveService() throws Exception {
         NsdManager manager = mManager;
 
@@ -195,6 +208,22 @@
         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);
+    }
+
     private void doTestRegisterService() throws Exception {
         NsdManager manager = mManager;
 
@@ -259,6 +288,7 @@
     private void doTestDiscoverService() throws Exception {
         NsdManager manager = mManager;
 
+        DiscoveryRequest request1 = new DiscoveryRequest.Builder("a_type").build();
         NsdServiceInfo reply1 = new NsdServiceInfo("a_name", "a_type");
         NsdServiceInfo reply2 = new NsdServiceInfo("another_name", "a_type");
         NsdServiceInfo reply3 = new NsdServiceInfo("a_third_name", "a_type");
@@ -279,7 +309,7 @@
         int key2 = getRequestKey(req ->
                 verify(mServiceConn, times(2)).discoverServices(req.capture(), any()));
 
-        mCallback.onDiscoverServicesStarted(key2, reply1);
+        mCallback.onDiscoverServicesStarted(key2, request1);
         verify(listener, timeout(mTimeoutMs).times(1)).onDiscoveryStarted("a_type");
 
 
@@ -319,7 +349,7 @@
         int key3 = getRequestKey(req ->
                 verify(mServiceConn, times(3)).discoverServices(req.capture(), any()));
 
-        mCallback.onDiscoverServicesStarted(key3, reply1);
+        mCallback.onDiscoverServicesStarted(key3, request1);
         verify(listener, timeout(mTimeoutMs).times(1)).onDiscoveryStarted("a_type");
 
         // Client unregisters immediately, it fails
@@ -343,10 +373,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
@@ -356,13 +428,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
diff --git a/tests/unit/java/com/android/internal/net/VpnProfileTest.java b/tests/unit/java/com/android/internal/net/VpnProfileTest.java
index b2dff2e..acae7d2 100644
--- a/tests/unit/java/com/android/internal/net/VpnProfileTest.java
+++ b/tests/unit/java/com/android/internal/net/VpnProfileTest.java
@@ -26,6 +26,7 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotSame;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 
@@ -311,4 +312,12 @@
         decoded.password = profile.password;
         assertEquals(profile, decoded);
     }
+
+    @Test
+    public void testClone() {
+        final VpnProfile profile = getSampleIkev2Profile(DUMMY_PROFILE_KEY);
+        final VpnProfile clone = profile.clone();
+        assertEquals(profile, clone);
+        assertNotSame(profile, clone);
+    }
 }
diff --git a/tests/unit/java/com/android/server/BpfLoaderRcUtilsTest.kt b/tests/unit/java/com/android/server/BpfLoaderRcUtilsTest.kt
new file mode 100644
index 0000000..2cf6b17
--- /dev/null
+++ b/tests/unit/java/com/android/server/BpfLoaderRcUtilsTest.kt
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server
+
+import android.os.Build
+import androidx.test.filters.SmallTest
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.DevSdkIgnoreRunner
+import kotlin.test.assertEquals
+import kotlin.test.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(DevSdkIgnoreRunner::class)
+@SmallTest
+@IgnoreUpTo(Build.VERSION_CODES.S)
+class BpfLoaderRcUtilsTest {
+    @Test
+    fun testLoadExistingBpfRcFile() {
+
+        val inputString = """
+            service a
+            # test comment
+            service bpfloader /system/bin/bpfloader
+                capabilities CHOWN SYS_ADMIN NET_ADMIN
+                group root graphics network_stack net_admin net_bw_acct net_bw_stats net_raw system
+                user root
+                rlimit memlock 1073741824 1073741824
+                oneshot
+                # comment 漢字
+                reboot_on_failure reboot,bpfloader-failed
+                updatable
+            
+            #test comment
+            on b 
+              oneshot 
+              # test comment
+        """.trimIndent()
+        val expectedResult = listOf(
+            "service bpfloader /system/bin/bpfloader",
+            "capabilities CHOWN SYS_ADMIN NET_ADMIN",
+            "group root graphics network_stack net_admin net_bw_acct net_bw_stats net_raw system",
+            "user root",
+            "rlimit memlock 1073741824 1073741824",
+            "oneshot",
+            "reboot_on_failure reboot,bpfloader-failed",
+            "updatable"
+        )
+
+        assertEquals(expectedResult,
+                BpfLoaderRcUtils.loadExistingBpfRcFile(inputString.byteInputStream()))
+    }
+
+    @Test
+    fun testCheckBpfRcFile() {
+        assertTrue(BpfLoaderRcUtils.checkBpfLoaderRc())
+    }
+}
diff --git a/tests/unit/java/com/android/server/ConnectivityServiceTest.java b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
index bafd450..7f821dd 100755
--- a/tests/unit/java/com/android/server/ConnectivityServiceTest.java
+++ b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
@@ -31,6 +31,7 @@
 import static android.Manifest.permission.NETWORK_STACK;
 import static android.Manifest.permission.PACKET_KEEPALIVE_OFFLOAD;
 import static android.Manifest.permission.READ_DEVICE_CONFIG;
+import static android.Manifest.permission.STATUS_BAR_SERVICE;
 import static android.app.ActivityManager.UidFrozenStateChangedCallback.UID_FROZEN_STATE_FROZEN;
 import static android.app.ActivityManager.UidFrozenStateChangedCallback.UID_FROZEN_STATE_UNFROZEN;
 import static android.app.PendingIntent.FLAG_IMMUTABLE;
@@ -78,7 +79,6 @@
 import static android.net.ConnectivityManager.TYPE_ETHERNET;
 import static android.net.ConnectivityManager.TYPE_MOBILE;
 import static android.net.ConnectivityManager.TYPE_MOBILE_SUPL;
-import static android.net.ConnectivityManager.TYPE_NONE;
 import static android.net.ConnectivityManager.TYPE_VPN;
 import static android.net.ConnectivityManager.TYPE_WIFI;
 import static android.net.ConnectivitySettingsManager.PRIVATE_DNS_MODE_OFF;
@@ -154,10 +154,14 @@
 import static android.net.resolv.aidl.IDnsResolverUnsolicitedEventListener.VALIDATION_RESULT_SUCCESS;
 import static android.os.Process.INVALID_UID;
 import static android.system.OsConstants.IPPROTO_TCP;
+import static android.telephony.DataConnectionRealTimeInfo.DC_POWER_STATE_HIGH;
+import static android.telephony.DataConnectionRealTimeInfo.DC_POWER_STATE_LOW;
 
 import static com.android.server.ConnectivityService.DELAY_DESTROY_FROZEN_SOCKETS_VERSION;
 import static com.android.net.module.util.DeviceConfigUtils.TETHERING_MODULE_NAME;
+import static com.android.server.ConnectivityService.ALLOW_SYSUI_CONNECTIVITY_REPORTS;
 import static com.android.server.ConnectivityService.KEY_DESTROY_FROZEN_SOCKETS_VERSION;
+import static com.android.server.ConnectivityService.LOG_BPF_RC;
 import static com.android.server.ConnectivityService.MAX_NETWORK_REQUESTS_PER_SYSTEM_UID;
 import static com.android.server.ConnectivityService.PREFERENCE_ORDER_MOBILE_DATA_PREFERERRED;
 import static com.android.server.ConnectivityService.PREFERENCE_ORDER_OEM;
@@ -498,6 +502,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";
@@ -585,6 +590,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;
@@ -638,8 +644,8 @@
 
     // BatteryStatsManager is final and cannot be mocked with regular mockito, so just mock the
     // underlying binder calls.
-    final BatteryStatsManager mBatteryStatsManager =
-            new BatteryStatsManager(mock(IBatteryStats.class));
+    final IBatteryStats mIBatteryStats = mock(IBatteryStats.class);
+    final BatteryStatsManager mBatteryStatsManager = new BatteryStatsManager(mIBatteryStats);
 
     private ArgumentCaptor<ResolverParamsParcel> mResolverParamsParcelCaptor =
             ArgumentCaptor.forClass(ResolverParamsParcel.class);
@@ -1088,6 +1094,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.
@@ -1655,8 +1662,7 @@
             waitForIdle();
         }
 
-        public void startLegacyVpnPrivileged(VpnProfile profile,
-                @Nullable Network underlying, @NonNull LinkProperties egress) {
+        public void startLegacyVpnPrivileged(VpnProfile profile) {
             switch (profile.type) {
                 case VpnProfile.TYPE_IKEV2_IPSEC_RSA:
                 case VpnProfile.TYPE_IKEV2_IPSEC_USER_PASS:
@@ -1988,7 +1994,7 @@
         }
 
         @Override
-        public HandlerThread makeHandlerThread() {
+        public HandlerThread makeHandlerThread(@NonNull final String tag) {
             return mCsHandlerThread;
         }
 
@@ -2150,6 +2156,18 @@
             }
         }
 
+        @Override
+        public boolean isFeatureNotChickenedOut(Context context, String name) {
+            switch (name) {
+                case ALLOW_SYSUI_CONNECTIVITY_REPORTS:
+                    return true;
+                case LOG_BPF_RC:
+                    return true;
+                default:
+                    return super.isFeatureNotChickenedOut(context, name);
+            }
+        }
+
         public void setChangeIdEnabled(final boolean enabled, final long changeId, final int uid) {
             final Pair<Long, Integer> data = new Pair<>(changeId, uid);
             // mEnabledChangeIds is read on the handler thread and maybe the test thread, so
@@ -2389,6 +2407,11 @@
         FakeSettingsProvider.clearSettingsProvider();
         ConnectivityResources.setResourcesContextForTest(null);
 
+        for (TestNetworkAgentWrapper agent : mCreatedAgents) {
+            agent.destroy();
+        }
+        mCreatedAgents.clear();
+
         mCsHandlerThread.quitSafely();
         mCsHandlerThread.join();
         mAlarmManagerThread.quitSafely();
@@ -10238,7 +10261,7 @@
         b.expectBroadcast();
         // Simulate LockdownVpnTracker attempting to start the VPN since it received the
         // systemDefault callback.
-        mMockVpn.startLegacyVpnPrivileged(profile, mCellAgent.getNetwork(), cellLp);
+        mMockVpn.startLegacyVpnPrivileged(profile);
         if (expectSetVpnDefaultForUids) {
             // setVpnDefaultForUids() releases the original network request and creates a VPN
             // request so LOST callback is received.
@@ -10309,7 +10332,7 @@
         // callback with different network.
         final ExpectedBroadcast b6 = expectConnectivityAction(TYPE_VPN, DetailedState.DISCONNECTED);
         mMockVpn.stopVpnRunnerPrivileged();
-        mMockVpn.startLegacyVpnPrivileged(profile, mWiFiAgent.getNetwork(), wifiLp);
+        mMockVpn.startLegacyVpnPrivileged(profile);
         // VPN network is disconnected (to restart)
         callback.expect(LOST, mMockVpn);
         defaultCallback.expect(LOST, mMockVpn);
@@ -10795,6 +10818,11 @@
         expectNativeNetworkCreated(netId, permission, iface, null /* inOrder */);
     }
 
+    private int getIdleTimerLabel(int netId, int transportType) {
+        return ConnectivityService.LegacyNetworkActivityTracker.getIdleTimerLabel(
+                mDeps.isAtLeastV(), netId, transportType);
+    }
+
     @Test
     public void testStackedLinkProperties() throws Exception {
         final LinkAddress myIpv4 = new LinkAddress("1.2.3.4/24");
@@ -11036,7 +11064,7 @@
         networkCallback.expect(LOST, mCellAgent);
         networkCallback.assertNoCallback();
         verify(mMockNetd, times(1)).idletimerRemoveInterface(eq(MOBILE_IFNAME), anyInt(),
-                eq(Integer.toString(TRANSPORT_CELLULAR)));
+                eq(Integer.toString(getIdleTimerLabel(cellNetId, TRANSPORT_CELLULAR))));
         verify(mMockNetd).networkDestroy(cellNetId);
         if (mDeps.isAtLeastU()) {
             verify(mMockNetd).setNetworkAllowlist(any());
@@ -11095,7 +11123,7 @@
         }
 
         verify(mMockNetd).idletimerRemoveInterface(eq(MOBILE_IFNAME), anyInt(),
-                eq(Integer.toString(TRANSPORT_CELLULAR)));
+                eq(Integer.toString(getIdleTimerLabel(cellNetId, TRANSPORT_CELLULAR))));
         verify(mMockNetd).networkDestroy(cellNetId);
         if (mDeps.isAtLeastU()) {
             verify(mMockNetd).setNetworkAllowlist(any());
@@ -11350,8 +11378,21 @@
         final ConditionVariable onNetworkActiveCv = new ConditionVariable();
         final ConnectivityManager.OnNetworkActiveListener listener = onNetworkActiveCv::open;
 
+        TestNetworkCallback defaultCallback = new TestNetworkCallback();
+
         testAndCleanup(() -> {
+            mCm.registerDefaultNetworkCallback(defaultCallback);
             agent.connect(true);
+            defaultCallback.expectAvailableThenValidatedCallbacks(agent);
+            if (transportType == TRANSPORT_CELLULAR) {
+                verify(mIBatteryStats).noteMobileRadioPowerState(eq(DC_POWER_STATE_HIGH),
+                        anyLong() /* timestampNs */, eq(NETWORK_ACTIVITY_NO_UID));
+            } else if (transportType == TRANSPORT_WIFI) {
+                verify(mIBatteryStats).noteWifiRadioPowerState(eq(DC_POWER_STATE_HIGH),
+                        anyLong() /* timestampNs */, eq(NETWORK_ACTIVITY_NO_UID));
+            }
+            clearInvocations(mIBatteryStats);
+            final int idleTimerLabel = getIdleTimerLabel(agent.getNetwork().netId, transportType);
 
             // Network is considered active when the network becomes the default network.
             assertTrue(mCm.isDefaultNetworkActive());
@@ -11360,19 +11401,57 @@
 
             // Interface goes to inactive state
             netdUnsolicitedEventListener.onInterfaceClassActivityChanged(false /* isActive */,
-                    transportType, TIMESTAMP, NETWORK_ACTIVITY_NO_UID);
+                    idleTimerLabel, TIMESTAMP, NETWORK_ACTIVITY_NO_UID);
             mServiceContext.expectDataActivityBroadcast(legacyType, false /* isActive */,
                     TIMESTAMP);
             assertFalse(onNetworkActiveCv.block(TEST_CALLBACK_TIMEOUT_MS));
             assertFalse(mCm.isDefaultNetworkActive());
+            if (mDeps.isAtLeastV()) {
+                if (transportType == TRANSPORT_CELLULAR) {
+                    verify(mIBatteryStats).noteMobileRadioPowerState(eq(DC_POWER_STATE_LOW),
+                            anyLong() /* timestampNs */, eq(NETWORK_ACTIVITY_NO_UID));
+                } else if (transportType == TRANSPORT_WIFI) {
+                    verify(mIBatteryStats).noteWifiRadioPowerState(eq(DC_POWER_STATE_LOW),
+                            anyLong() /* timestampNs */, eq(NETWORK_ACTIVITY_NO_UID));
+                }
+            } else {
+                // If TrackMultiNetworks is disabled, LegacyNetworkActivityTracker does not call
+                // BatteryStats API by the netd activity change callback since BatteryStatsService
+                // listen to netd callback via NetworkManagementService and update battery stats by
+                // itself.
+                verify(mIBatteryStats, never())
+                        .noteMobileRadioPowerState(anyInt(), anyLong(), anyInt());
+                verify(mIBatteryStats, never())
+                        .noteWifiRadioPowerState(anyInt(), anyLong(), anyInt());
+            }
 
             // Interface goes to active state
             netdUnsolicitedEventListener.onInterfaceClassActivityChanged(true /* isActive */,
-                    transportType, TIMESTAMP, TEST_PACKAGE_UID);
+                    idleTimerLabel, TIMESTAMP, TEST_PACKAGE_UID);
             mServiceContext.expectDataActivityBroadcast(legacyType, true /* isActive */, TIMESTAMP);
             assertTrue(onNetworkActiveCv.block(TEST_CALLBACK_TIMEOUT_MS));
             assertTrue(mCm.isDefaultNetworkActive());
+            if (mDeps.isAtLeastV()) {
+                if (transportType == TRANSPORT_CELLULAR) {
+                    verify(mIBatteryStats).noteMobileRadioPowerState(eq(DC_POWER_STATE_HIGH),
+                            anyLong() /* timestampNs */, eq(TEST_PACKAGE_UID));
+                } else if (transportType == TRANSPORT_WIFI) {
+                    verify(mIBatteryStats).noteWifiRadioPowerState(eq(DC_POWER_STATE_HIGH),
+                            anyLong() /* timestampNs */, eq(TEST_PACKAGE_UID));
+                }
+            } else {
+                // If TrackMultiNetworks is disabled, LegacyNetworkActivityTracker does not call
+                // BatteryStats API by the netd activity change callback since BatteryStatsService
+                // listen to netd callback via NetworkManagementService and update battery stats by
+                // itself.
+                verify(mIBatteryStats, never())
+                        .noteMobileRadioPowerState(anyInt(), anyLong(), anyInt());
+                verify(mIBatteryStats, never())
+                        .noteWifiRadioPowerState(anyInt(), anyLong(), anyInt());
+            }
         }, () -> { // Cleanup
+                mCm.unregisterNetworkCallback(defaultCallback);
+            }, () -> { // Cleanup
                 mCm.removeDefaultNetworkActiveListener(listener);
             }, () -> { // Cleanup
                 agent.disconnect();
@@ -11420,12 +11499,13 @@
     }
 
     @Test
-    public void testOnNetworkActive_NewEthernetConnects_CallbackNotCalled() throws Exception {
-        // LegacyNetworkActivityTracker calls onNetworkActive callback only for networks that
-        // tracker adds the idle timer to. And the tracker does not set the idle timer for the
-        // ethernet network.
+    public void testOnNetworkActive_NewEthernetConnects_Callback() throws Exception {
+        // On pre-V devices, LegacyNetworkActivityTracker calls onNetworkActive callback only for
+        // networks that tracker adds the idle timer to. And the tracker does not set the idle timer
+        // for the ethernet network.
         // So onNetworkActive is not called when the ethernet becomes the default network
-        doTestOnNetworkActive_NewNetworkConnects(TRANSPORT_ETHERNET, false /* expectCallback */);
+        final boolean expectCallback = mDeps.isAtLeastV();
+        doTestOnNetworkActive_NewNetworkConnects(TRANSPORT_ETHERNET, expectCallback);
     }
 
     @Test
@@ -11455,15 +11535,19 @@
         mCm.registerNetworkCallback(networkRequest, networkCallback);
 
         mCellAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
+        final String cellIdleTimerLabel = Integer.toString(getIdleTimerLabel(
+                mCellAgent.getNetwork().netId, TRANSPORT_CELLULAR));
         final LinkProperties cellLp = new LinkProperties();
         cellLp.setInterfaceName(MOBILE_IFNAME);
         mCellAgent.sendLinkProperties(cellLp);
         mCellAgent.connect(true);
         networkCallback.expectAvailableThenValidatedCallbacks(mCellAgent);
         verify(mMockNetd, times(1)).idletimerAddInterface(eq(MOBILE_IFNAME), anyInt(),
-                eq(Integer.toString(TRANSPORT_CELLULAR)));
+                eq(cellIdleTimerLabel));
 
         mWiFiAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        String wifiIdleTimerLabel = Integer.toString(getIdleTimerLabel(
+                mWiFiAgent.getNetwork().netId, TRANSPORT_WIFI));
         final LinkProperties wifiLp = new LinkProperties();
         wifiLp.setInterfaceName(WIFI_IFNAME);
         mWiFiAgent.sendLinkProperties(wifiLp);
@@ -11474,9 +11558,18 @@
         networkCallback.expectLosing(mCellAgent);
         networkCallback.expectCaps(mWiFiAgent, c -> c.hasCapability(NET_CAPABILITY_VALIDATED));
         verify(mMockNetd, times(1)).idletimerAddInterface(eq(WIFI_IFNAME), anyInt(),
-                eq(Integer.toString(TRANSPORT_WIFI)));
-        verify(mMockNetd, times(1)).idletimerRemoveInterface(eq(MOBILE_IFNAME), anyInt(),
-                eq(Integer.toString(TRANSPORT_CELLULAR)));
+                eq(wifiIdleTimerLabel));
+        if (mDeps.isAtLeastV()) {
+            // V+ devices add idleTimer when the network is first connected and remove when the
+            // network is disconnected.
+            verify(mMockNetd, never()).idletimerRemoveInterface(eq(MOBILE_IFNAME), anyInt(),
+                    eq(Integer.toString(mCellAgent.getNetwork().netId)));
+        } else {
+            // pre V devices add idleTimer when the network becomes the default network and remove
+            // when the network becomes no longer the default network.
+            verify(mMockNetd, times(1)).idletimerRemoveInterface(eq(MOBILE_IFNAME), anyInt(),
+                    eq(Integer.toString(TRANSPORT_CELLULAR)));
+        }
 
         // Disconnect wifi and switch back to cell
         reset(mMockNetd);
@@ -11484,13 +11577,20 @@
         networkCallback.expect(LOST, mWiFiAgent);
         assertNoCallbacks(networkCallback);
         verify(mMockNetd, times(1)).idletimerRemoveInterface(eq(WIFI_IFNAME), anyInt(),
-                eq(Integer.toString(TRANSPORT_WIFI)));
-        verify(mMockNetd, times(1)).idletimerAddInterface(eq(MOBILE_IFNAME), anyInt(),
-                eq(Integer.toString(TRANSPORT_CELLULAR)));
+                eq(wifiIdleTimerLabel));
+        if (mDeps.isAtLeastV()) {
+            verify(mMockNetd, never()).idletimerAddInterface(eq(MOBILE_IFNAME), anyInt(),
+                    eq(Integer.toString(mCellAgent.getNetwork().netId)));
+        } else {
+            verify(mMockNetd, times(1)).idletimerAddInterface(eq(MOBILE_IFNAME), anyInt(),
+                    eq(Integer.toString(TRANSPORT_CELLULAR)));
+        }
 
         // reconnect wifi
         reset(mMockNetd);
         mWiFiAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        wifiIdleTimerLabel = Integer.toString(getIdleTimerLabel(
+                mWiFiAgent.getNetwork().netId, TRANSPORT_WIFI));
         wifiLp.setInterfaceName(WIFI_IFNAME);
         mWiFiAgent.sendLinkProperties(wifiLp);
         mWiFiAgent.connect(true);
@@ -11498,20 +11598,30 @@
         networkCallback.expectLosing(mCellAgent);
         networkCallback.expectCaps(mWiFiAgent, c -> c.hasCapability(NET_CAPABILITY_VALIDATED));
         verify(mMockNetd, times(1)).idletimerAddInterface(eq(WIFI_IFNAME), anyInt(),
-                eq(Integer.toString(TRANSPORT_WIFI)));
-        verify(mMockNetd, times(1)).idletimerRemoveInterface(eq(MOBILE_IFNAME), anyInt(),
-                eq(Integer.toString(TRANSPORT_CELLULAR)));
+                eq(wifiIdleTimerLabel));
+        if (mDeps.isAtLeastV()) {
+            verify(mMockNetd, never()).idletimerRemoveInterface(eq(MOBILE_IFNAME), anyInt(),
+                    eq(Integer.toString(mCellAgent.getNetwork().netId)));
+        } else {
+            verify(mMockNetd, times(1)).idletimerRemoveInterface(eq(MOBILE_IFNAME), anyInt(),
+                    eq(Integer.toString(TRANSPORT_CELLULAR)));
+        }
 
         // Disconnect cell
         reset(mMockNetd);
         mCellAgent.disconnect();
         networkCallback.expect(LOST, mCellAgent);
-        // LOST callback is triggered earlier than removing idle timer. Broadcast should also be
-        // sent as network being switched. Ensure rule removal for cell will not be triggered
-        // unexpectedly before network being removed.
         waitForIdle();
-        verify(mMockNetd, times(0)).idletimerRemoveInterface(eq(MOBILE_IFNAME), anyInt(),
-                eq(Integer.toString(TRANSPORT_CELLULAR)));
+        if (mDeps.isAtLeastV()) {
+            verify(mMockNetd, times(1)).idletimerRemoveInterface(eq(MOBILE_IFNAME), anyInt(),
+                    eq(Integer.toString(mCellAgent.getNetwork().netId)));
+        }  else {
+            // LOST callback is triggered earlier than removing idle timer. Broadcast should also be
+            // sent as network being switched. Ensure rule removal for cell will not be triggered
+            // unexpectedly before network being removed.
+            verify(mMockNetd, times(0)).idletimerRemoveInterface(eq(MOBILE_IFNAME), anyInt(),
+                    eq(Integer.toString(TRANSPORT_CELLULAR)));
+        }
         verify(mMockNetd, times(1)).networkDestroy(eq(mCellAgent.getNetwork().netId));
         verify(mMockDnsResolver, times(1)).destroyNetworkCache(eq(mCellAgent.getNetwork().netId));
 
@@ -11520,12 +11630,27 @@
         mWiFiAgent.disconnect();
         b.expectBroadcast();
         verify(mMockNetd, times(1)).idletimerRemoveInterface(eq(WIFI_IFNAME), anyInt(),
-                eq(Integer.toString(TRANSPORT_WIFI)));
+                eq(wifiIdleTimerLabel));
 
         // Clean up
         mCm.unregisterNetworkCallback(networkCallback);
     }
 
+    @Test
+    public void testDataActivityTracking_VpnNetwork() throws Exception {
+        mWiFiAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        mWiFiAgent.connect(true /* validated */);
+        mMockVpn.setUnderlyingNetworks(new Network[] { mWiFiAgent.getNetwork() });
+
+        final LinkProperties lp = new LinkProperties();
+        lp.setInterfaceName(VPN_IFNAME);
+        mMockVpn.establishForMyUid(lp);
+
+        // NetworkActivityTracker should not track the VPN network since VPN can change the
+        // underlying network without disconnect.
+        verify(mMockNetd, never()).idletimerAddInterface(eq(VPN_IFNAME), anyInt(), any());
+    }
+
     private void verifyTcpBufferSizeChange(String tcpBufferSizes) throws Exception {
         String[] values = tcpBufferSizes.split(",");
         String rmemValues = String.join(" ", values[0], values[1], values[2]);
@@ -12810,6 +12935,18 @@
     }
 
     @Test
+    public void testCheckConnectivityDiagnosticsPermissionsSysUi() throws Exception {
+        final NetworkAgentInfo naiWithoutUid = fakeMobileNai(new NetworkCapabilities());
+
+        mServiceContext.setPermission(STATUS_BAR_SERVICE, PERMISSION_GRANTED);
+        assertTrue(
+                "SysUi permission (STATUS_BAR_SERVICE) not applied",
+                mService.checkConnectivityDiagnosticsPermissions(
+                        Process.myPid(), Process.myUid(), naiWithoutUid,
+                        mContext.getOpPackageName()));
+    }
+
+    @Test
     public void testCheckConnectivityDiagnosticsPermissionsWrongUidPackageName() throws Exception {
         final int wrongUid = Process.myUid() + 1;
 
@@ -17258,6 +17395,7 @@
         mCm.requestNetwork(new NetworkRequest.Builder()
                         .clearCapabilities()
                         .addTransportType(TRANSPORT_TEST)
+                        .addTransportType(TRANSPORT_CELLULAR)
                         .build(),
                 cb);
 
@@ -17403,7 +17541,7 @@
 
         // In this test TEST_PACKAGE_UID will be the UID of the carrier service UID.
         doReturn(true).when(mCarrierPrivilegeAuthenticator)
-                .hasCarrierPrivilegeForNetworkCapabilities(eq(TEST_PACKAGE_UID), any());
+                .isCarrierServiceUidForNetworkCapabilities(eq(TEST_PACKAGE_UID), any());
 
         // Simulate a restricted telephony network. The telephony factory is entitled to set
         // the access UID to the service package on any of its restricted networks.
@@ -17468,17 +17606,18 @@
         // TODO : fix the builder
         ncb.setNetworkSpecifier(null);
         ncb.removeTransportType(TRANSPORT_CELLULAR);
-        ncb.addTransportType(TRANSPORT_WIFI);
+        ncb.addTransportType(TRANSPORT_BLUETOOTH);
         // Wifi does not get to set access UID, even to the correct UID
         mCm.requestNetwork(new NetworkRequest.Builder()
-                .addTransportType(TRANSPORT_WIFI)
+                .addTransportType(TRANSPORT_BLUETOOTH)
                 .removeCapability(NET_CAPABILITY_NOT_RESTRICTED)
                 .build(), cb);
-        mWiFiAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI, new LinkProperties(), ncb.build());
-        mWiFiAgent.connect(true);
-        cb.expectAvailableThenValidatedCallbacks(mWiFiAgent);
+        final TestNetworkAgentWrapper bluetoothAgent = new TestNetworkAgentWrapper(
+                TRANSPORT_BLUETOOTH, new LinkProperties(), ncb.build());
+        bluetoothAgent.connect(true);
+        cb.expectAvailableThenValidatedCallbacks(bluetoothAgent);
         ncb.setAllowedUids(serviceUidSet);
-        mWiFiAgent.setNetworkCapabilities(ncb.build(), true /* sendToCS */);
+        bluetoothAgent.setNetworkCapabilities(ncb.build(), true /* sendToCS */);
         cb.assertNoCallback(TEST_CALLBACK_TIMEOUT_MS);
         mCm.unregisterNetworkCallback(cb);
     }
@@ -18704,6 +18843,7 @@
         final LinkProperties lp = new LinkProperties();
         lp.setInterfaceName(transportToTestIfaceName(transportType));
         final TestNetworkAgentWrapper agent = new TestNetworkAgentWrapper(transportType, lp);
+        final int idleTimerLabel = getIdleTimerLabel(agent.getNetwork().netId, transportType);
         testAndCleanup(() -> {
             final UidFrozenStateChangedCallback uidFrozenStateChangedCallback =
                     getUidFrozenStateChangedCallback().get();
@@ -18716,7 +18856,7 @@
             if (freezeWithNetworkInactive) {
                 // Make network inactive
                 netdUnsolicitedEventListener.onInterfaceClassActivityChanged(false /* isActive */,
-                        transportType, TIMESTAMP, NETWORK_ACTIVITY_NO_UID);
+                        idleTimerLabel, TIMESTAMP, NETWORK_ACTIVITY_NO_UID);
             }
 
             // Freeze TEST_FROZEN_UID and TEST_UNFROZEN_UID
@@ -18740,7 +18880,7 @@
 
             // Make network active
             netdUnsolicitedEventListener.onInterfaceClassActivityChanged(true /* isActive */,
-                    transportType, TIMESTAMP, TEST_PACKAGE_UID);
+                    idleTimerLabel, TIMESTAMP, TEST_PACKAGE_UID);
             waitForIdle();
 
             if (expectDelay) {
@@ -18759,8 +18899,8 @@
     @Test
     @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
     public void testDelayFrozenUidSocketDestroy_ActiveCellular() throws Exception {
-        doTestDelayFrozenUidSocketDestroy(TRANSPORT_CELLULAR,
-                false /* freezeWithNetworkInactive */, false /* expectDelay */);
+        doTestDelayFrozenUidSocketDestroy(TRANSPORT_CELLULAR, false /* freezeWithNetworkInactive */,
+                false /* expectDelay */);
     }
 
     @Test
@@ -18768,22 +18908,22 @@
     public void testDelayFrozenUidSocketDestroy_InactiveCellular() throws Exception {
         // When the default network is cellular and cellular network is inactive, closing socket
         // is delayed.
-        doTestDelayFrozenUidSocketDestroy(TRANSPORT_CELLULAR,
-                true /* freezeWithNetworkInactive */, true /* expectDelay */);
+        doTestDelayFrozenUidSocketDestroy(TRANSPORT_CELLULAR, true /* freezeWithNetworkInactive */,
+                true /* expectDelay */);
     }
 
     @Test
     @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
     public void testDelayFrozenUidSocketDestroy_ActiveWifi() throws Exception {
-        doTestDelayFrozenUidSocketDestroy(TRANSPORT_WIFI,
-                false /* freezeWithNetworkInactive */, false /* expectDelay */);
+        doTestDelayFrozenUidSocketDestroy(TRANSPORT_WIFI, false /* freezeWithNetworkInactive */,
+                false /* expectDelay */);
     }
 
     @Test
     @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
     public void testDelayFrozenUidSocketDestroy_InactiveWifi() throws Exception {
-        doTestDelayFrozenUidSocketDestroy(TRANSPORT_WIFI,
-                true /* freezeWithNetworkInactive */, false /* expectDelay */);
+        doTestDelayFrozenUidSocketDestroy(TRANSPORT_WIFI, true /* freezeWithNetworkInactive */,
+                false /* expectDelay */);
     }
 
     /**
@@ -18804,6 +18944,8 @@
         final LinkProperties cellLp = new LinkProperties();
         cellLp.setInterfaceName(MOBILE_IFNAME);
         mCellAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR, cellLp);
+        final int idleTimerLabel =
+                getIdleTimerLabel(mCellAgent.getNetwork().netId, TRANSPORT_CELLULAR);
 
         final TestNetworkCallback defaultCallback = new TestNetworkCallback();
         mCm.registerDefaultNetworkCallback(defaultCallback);
@@ -18813,7 +18955,7 @@
 
             // Make cell network inactive
             netdUnsolicitedEventListener.onInterfaceClassActivityChanged(false /* isActive */,
-                    TRANSPORT_CELLULAR, TIMESTAMP, NETWORK_ACTIVITY_NO_UID);
+                    idleTimerLabel, TIMESTAMP, NETWORK_ACTIVITY_NO_UID);
 
             // Freeze TEST_FROZEN_UID
             final int[] uids = {TEST_FROZEN_UID};
diff --git a/tests/unit/java/com/android/server/IpSecServiceParameterizedTest.java b/tests/unit/java/com/android/server/IpSecServiceParameterizedTest.java
index 1618a62..8037542 100644
--- a/tests/unit/java/com/android/server/IpSecServiceParameterizedTest.java
+++ b/tests/unit/java/com/android/server/IpSecServiceParameterizedTest.java
@@ -33,6 +33,7 @@
 import static org.junit.Assert.fail;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.argThat;
 import static org.mockito.ArgumentMatchers.eq;
@@ -74,6 +75,7 @@
 
 import androidx.test.filters.SmallTest;
 
+import com.android.net.module.util.netlink.xfrm.XfrmNetlinkNewSaMessage;
 import com.android.server.IpSecService.TunnelInterfaceRecord;
 import com.android.testutils.DevSdkIgnoreRule;
 
@@ -85,6 +87,7 @@
 import org.junit.runners.Parameterized;
 
 import java.net.Inet4Address;
+import java.net.InetAddress;
 import java.net.Socket;
 import java.util.Arrays;
 import java.util.Collection;
@@ -149,6 +152,7 @@
         private Set<String> mAllowedPermissions = new ArraySet<>(Arrays.asList(
                 android.Manifest.permission.MANAGE_IPSEC_TUNNELS,
                 android.Manifest.permission.NETWORK_STACK,
+                android.Manifest.permission.ACCESS_NETWORK_STATE,
                 PERMISSION_MAINLINE_NETWORK_STACK));
 
         private void setAllowedPermissions(String... permissions) {
@@ -202,11 +206,13 @@
     private IpSecService.Dependencies makeDependencies() throws RemoteException {
         final IpSecService.Dependencies deps = mock(IpSecService.Dependencies.class);
         when(deps.getNetdInstance(mTestContext)).thenReturn(mMockNetd);
+        when(deps.getIpSecXfrmController()).thenReturn(mMockXfrmCtrl);
         return deps;
     }
 
     INetd mMockNetd;
     PackageManager mMockPkgMgr;
+    IpSecXfrmController mMockXfrmCtrl;
     IpSecService.Dependencies mDeps;
     IpSecService mIpSecService;
     Network fakeNetwork = new Network(0xAB);
@@ -235,6 +241,7 @@
     @Before
     public void setUp() throws Exception {
         mMockNetd = mock(INetd.class);
+        mMockXfrmCtrl = mock(IpSecXfrmController.class);
         mMockPkgMgr = mock(PackageManager.class);
         mDeps = makeDependencies();
         mIpSecService = new IpSecService(mTestContext, mDeps);
@@ -506,6 +513,32 @@
     }
 
     @Test
+    public void getTransformState() throws Exception {
+        XfrmNetlinkNewSaMessage mockXfrmNewSaMsg = mock(XfrmNetlinkNewSaMessage.class);
+        when(mockXfrmNewSaMsg.getBitmap()).thenReturn(new byte[512]);
+        when(mMockXfrmCtrl.ipSecGetSa(any(InetAddress.class), anyLong()))
+                .thenReturn(mockXfrmNewSaMsg);
+
+        // Create transform
+        IpSecConfig ipSecConfig = new IpSecConfig();
+        addDefaultSpisAndRemoteAddrToIpSecConfig(ipSecConfig);
+        addAuthAndCryptToIpSecConfig(ipSecConfig);
+
+        IpSecTransformResponse createTransformResp =
+                mIpSecService.createTransform(ipSecConfig, new Binder(), BLESSED_PACKAGE);
+        assertEquals(IpSecManager.Status.OK, createTransformResp.status);
+
+        // Get transform state
+        mIpSecService.getTransformState(createTransformResp.resourceId);
+
+        // Verifications
+        verify(mMockXfrmCtrl)
+                .ipSecGetSa(
+                        eq(InetAddresses.parseNumericAddress(mDestinationAddr)),
+                        eq(Integer.toUnsignedLong(TEST_SPI)));
+    }
+
+    @Test
     public void testReleaseOwnedSpi() throws Exception {
         IpSecConfig ipSecConfig = new IpSecConfig();
         addDefaultSpisAndRemoteAddrToIpSecConfig(ipSecConfig);
diff --git a/tests/unit/java/com/android/server/IpSecXfrmControllerTest.java b/tests/unit/java/com/android/server/IpSecXfrmControllerTest.java
new file mode 100644
index 0000000..8c1f47f
--- /dev/null
+++ b/tests/unit/java/com/android/server/IpSecXfrmControllerTest.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server;
+
+import static com.android.server.IpSecXfrmControllerTestHex.XFRM_ESRCH_HEX;
+import static com.android.server.IpSecXfrmControllerTestHex.XFRM_NEW_SA_HEX;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.net.InetAddresses;
+import android.system.ErrnoException;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.net.module.util.netlink.NetlinkMessage;
+import com.android.net.module.util.netlink.xfrm.XfrmNetlinkNewSaMessage;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.io.FileDescriptor;
+import java.net.InetAddress;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class IpSecXfrmControllerTest {
+    private static final InetAddress DEST_ADDRESS =
+            InetAddresses.parseNumericAddress("2001:db8::111");
+    private static final long SPI = 0xaabbccddL;
+    private static final int ESRCH = -3;
+
+    private IpSecXfrmController mXfrmController;
+    private FileDescriptor mDummyNetlinkSocket;
+
+    @Mock private IpSecXfrmController.Dependencies mMockDeps;
+
+    @Captor private ArgumentCaptor<byte[]> mRequestByteArrayCaptor;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        mDummyNetlinkSocket = new FileDescriptor();
+
+        when(mMockDeps.newNetlinkSocket()).thenReturn(mDummyNetlinkSocket);
+        mXfrmController = new IpSecXfrmController(mMockDeps);
+    }
+
+    @Test
+    public void testStartStop() throws Exception {
+        mXfrmController.openNetlinkSocketIfNeeded();
+
+        verify(mMockDeps).newNetlinkSocket();
+        assertNotNull(mXfrmController.getNetlinkSocket());
+
+        mXfrmController.closeNetlinkSocketIfNeeded();
+        verify(mMockDeps).releaseNetlinkSocket(eq(mDummyNetlinkSocket));
+        assertNull(mXfrmController.getNetlinkSocket());
+    }
+
+    private static void injectRxMessage(IpSecXfrmController.Dependencies mockDeps, byte[] bytes)
+            throws Exception {
+        final ByteBuffer buff = ByteBuffer.wrap(bytes);
+        buff.order(ByteOrder.nativeOrder());
+
+        when(mockDeps.recvMessage(any(FileDescriptor.class))).thenReturn(buff);
+    }
+
+    @Test
+    public void testIpSecGetSa() throws Exception {
+        final int expectedReqLen = 40;
+        injectRxMessage(mMockDeps, XFRM_NEW_SA_HEX);
+
+        final NetlinkMessage netlinkMessage = mXfrmController.ipSecGetSa(DEST_ADDRESS, SPI);
+        final XfrmNetlinkNewSaMessage message = (XfrmNetlinkNewSaMessage) netlinkMessage;
+
+        // Verifications
+        assertEquals(SPI, message.getXfrmUsersaInfo().getSpi());
+        assertEquals(DEST_ADDRESS, message.getXfrmUsersaInfo().getDestAddress());
+
+        verify(mMockDeps).sendMessage(eq(mDummyNetlinkSocket), mRequestByteArrayCaptor.capture());
+        final byte[] request = mRequestByteArrayCaptor.getValue();
+        assertEquals(expectedReqLen, request.length);
+
+        verify(mMockDeps).recvMessage(eq(mDummyNetlinkSocket));
+    }
+
+    @Test
+    public void testIpSecGetSa_NlErrorMsg() throws Exception {
+        injectRxMessage(mMockDeps, XFRM_ESRCH_HEX);
+
+        try {
+            mXfrmController.ipSecGetSa(DEST_ADDRESS, SPI);
+            fail("Expected to fail with ESRCH ");
+        } catch (ErrnoException e) {
+            assertEquals(ESRCH, e.errno);
+        }
+    }
+}
diff --git a/tests/unit/java/com/android/server/IpSecXfrmControllerTestHex.java b/tests/unit/java/com/android/server/IpSecXfrmControllerTestHex.java
new file mode 100644
index 0000000..a2082c4
--- /dev/null
+++ b/tests/unit/java/com/android/server/IpSecXfrmControllerTestHex.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server;
+
+import com.android.net.module.util.HexDump;
+
+public class IpSecXfrmControllerTestHex {
+    private static final String XFRM_NEW_SA_HEX_STRING =
+            "2003000010000000000000003FE1D4B6"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000A00000000000000"
+                    + "000000000000000020010DB800000000"
+                    + "0000000000000111AABBCCDD32000000"
+                    + "20010DB8000000000000000000000222"
+                    + "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"
+                    + "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "FD464C65000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "024000000A0000000000000000000000"
+                    + "5C000100686D61632873686131290000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000A000000055F01AC07E15E437"
+                    + "115DDE0AEDD18A822BA9F81E60001400"
+                    + "686D6163287368613129000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "A00000006000000055F01AC07E15E437"
+                    + "115DDE0AEDD18A822BA9F81E58000200"
+                    + "63626328616573290000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "800000006AED4975ADF006D65C76F639"
+                    + "23A6265B1C0117004000000000000000"
+                    + "00000000000000000000000000080000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000";
+    public static final byte[] XFRM_NEW_SA_HEX =
+            HexDump.hexStringToByteArray(XFRM_NEW_SA_HEX_STRING);
+
+    private static final String XFRM_ESRCH_HEX_STRING =
+            "3C0000000200000000000000A5060000"
+                    + "FDFFFFFF280000001200010000000000"
+                    + "0000000020010DB80000000000000000"
+                    + "00000111AABBCCDD0A003200";
+    public static final byte[] XFRM_ESRCH_HEX = HexDump.hexStringToByteArray(XFRM_ESRCH_HEX_STRING);
+}
diff --git a/tests/unit/java/com/android/server/NsdServiceTest.java b/tests/unit/java/com/android/server/NsdServiceTest.java
index 771edb2..b60f0b4 100644
--- a/tests/unit/java/com/android/server/NsdServiceTest.java
+++ b/tests/unit/java/com/android/server/NsdServiceTest.java
@@ -16,6 +16,7 @@
 
 package com.android.server;
 
+import static android.Manifest.permission.DEVICE_POWER;
 import static android.Manifest.permission.NETWORK_SETTINGS;
 import static android.Manifest.permission.NETWORK_STACK;
 import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_CACHED;
@@ -24,7 +25,6 @@
 import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_VISIBLE;
 import static android.content.pm.PackageManager.PERMISSION_DENIED;
 import static android.content.pm.PackageManager.PERMISSION_GRANTED;
-import static android.content.pm.PermissionInfo.PROTECTION_SIGNATURE;
 import static android.net.InetAddresses.parseNumericAddress;
 import static android.net.NetworkCapabilities.TRANSPORT_ETHERNET;
 import static android.net.NetworkCapabilities.TRANSPORT_VPN;
@@ -75,7 +75,6 @@
 import android.content.ContentResolver;
 import android.content.Context;
 import android.content.pm.PackageManager;
-import android.content.pm.PermissionInfo;
 import android.net.INetd;
 import android.net.Network;
 import android.net.mdns.aidl.DiscoveryInfo;
@@ -141,14 +140,19 @@
 import java.util.List;
 import java.util.Objects;
 import java.util.Queue;
+import java.util.Set;
 
 // TODOs:
 //  - test client can send requests and receive replies
 //  - test NSD_ON ENABLE/DISABLED listening
+@DevSdkIgnoreRunner.MonitorThreadLeak
 @RunWith(DevSdkIgnoreRunner.class)
 @SmallTest
 @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
 public class NsdServiceTest {
+    @Rule
+    public final DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule();
+
     static final int PROTOCOL = NsdManager.PROTOCOL_DNS_SD;
     private static final long CLEANUP_DELAY_MS = 500;
     private static final long TIMEOUT_MS = 500;
@@ -169,6 +173,8 @@
 
     @Rule
     public TestRule compatChangeRule = new PlatformCompatChangeRule();
+    @Rule
+    public TestRule ignoreRule = new DevSdkIgnoreRule();
     @Mock Context mContext;
     @Mock PackageManager mPackageManager;
     @Mock ContentResolver mResolver;
@@ -228,7 +234,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,6 +259,8 @@
         }
     }
 
+    // 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,
@@ -284,6 +293,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);
@@ -319,6 +329,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();
@@ -363,6 +374,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);
 
@@ -399,6 +411,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;
@@ -497,6 +510,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);
@@ -529,6 +543,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);
@@ -583,6 +598,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);
@@ -615,6 +631,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);
@@ -650,6 +667,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,6 +719,7 @@
 
     @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();
@@ -721,6 +740,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);
@@ -747,6 +767,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);
@@ -772,6 +793,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);
@@ -953,6 +975,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);
@@ -1113,9 +1136,10 @@
         final RegistrationListener regListener = mock(RegistrationListener.class);
         client.registerService(regInfo, NsdManager.PROTOCOL_DNS_SD, Runnable::run, regListener);
         waitForIdle();
-        verify(mAdvertiser).addService(anyInt(), argThat(s ->
+        verify(mAdvertiser).addOrUpdateService(anyInt(), argThat(s ->
                 "Instance".equals(s.getServiceName())
-                        && SERVICE_TYPE.equals(s.getServiceType())), eq("_subtype"));
+                        && SERVICE_TYPE.equals(s.getServiceType())
+                        && s.getSubtypes().equals(Set.of("_subtype"))), any(), anyInt());
 
         final DiscoveryListener discListener = mock(DiscoveryListener.class);
         client.discoverServices(typeWithSubtype, PROTOCOL, network, Runnable::run, discListener);
@@ -1199,6 +1223,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);
@@ -1220,8 +1245,8 @@
         waitForIdle();
 
         final ArgumentCaptor<Integer> serviceIdCaptor = ArgumentCaptor.forClass(Integer.class);
-        verify(mAdvertiser).addService(serviceIdCaptor.capture(),
-                argThat(info -> matches(info, regInfo)), eq(null) /* subtype */);
+        verify(mAdvertiser).addOrUpdateService(serviceIdCaptor.capture(),
+                argThat(info -> matches(info, regInfo)), any(), anyInt());
 
         client.unregisterService(regListenerWithoutFeature);
         waitForIdle();
@@ -1237,6 +1262,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(),
@@ -1249,7 +1275,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(),
@@ -1280,10 +1306,10 @@
         waitForIdle();
 
         // The advertiser is enabled for _type2 but not _type1
-        verify(mAdvertiser, never()).addService(
-                anyInt(), argThat(info -> matches(info, service1)), eq(null) /* subtype */);
-        verify(mAdvertiser).addService(
-                anyInt(), argThat(info -> matches(info, service2)), eq(null) /* subtype */);
+        verify(mAdvertiser, never()).addOrUpdateService(anyInt(),
+                argThat(info -> matches(info, service1)), any(), anyInt());
+        verify(mAdvertiser).addOrUpdateService(anyInt(), argThat(info -> matches(info, service2)),
+                any(), anyInt());
     }
 
     @Test
@@ -1295,7 +1321,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"));
@@ -1307,8 +1333,8 @@
         waitForIdle();
         verify(mSocketProvider).startMonitoringSockets();
         final ArgumentCaptor<Integer> idCaptor = ArgumentCaptor.forClass(Integer.class);
-        verify(mAdvertiser).addService(idCaptor.capture(), argThat(info ->
-                matches(info, regInfo)), eq(null) /* subtype */);
+        verify(mAdvertiser).addOrUpdateService(idCaptor.capture(), argThat(info ->
+                matches(info, regInfo)), any(), anyInt());
 
         // Verify onServiceRegistered callback
         final MdnsAdvertiser.AdvertiserCallback cb = cbCaptor.getValue();
@@ -1346,7 +1372,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"));
@@ -1356,7 +1382,7 @@
 
         client.registerService(regInfo, NsdManager.PROTOCOL_DNS_SD, Runnable::run, regListener);
         waitForIdle();
-        verify(mAdvertiser, never()).addService(anyInt(), any(), any());
+        verify(mAdvertiser, never()).addOrUpdateService(anyInt(), any(), any(), anyInt());
 
         verify(regListener, timeout(TIMEOUT_MS)).onRegistrationFailed(
                 argThat(info -> matches(info, regInfo)), eq(FAILURE_INTERNAL_ERROR));
@@ -1373,7 +1399,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"));
@@ -1385,9 +1411,12 @@
         waitForIdle();
         final ArgumentCaptor<Integer> idCaptor = ArgumentCaptor.forClass(Integer.class);
         // Service name is truncated to 63 characters
-        verify(mAdvertiser).addService(idCaptor.capture(),
-                argThat(info -> info.getServiceName().equals("a".repeat(63))),
-                eq(null) /* subtype */);
+        verify(mAdvertiser)
+                .addOrUpdateService(
+                        idCaptor.capture(),
+                        argThat(info -> info.getServiceName().equals("a".repeat(63))),
+                        any(),
+                        anyInt());
 
         // Verify onServiceRegistered callback
         final MdnsAdvertiser.AdvertiserCallback cb = cbCaptor.getValue();
@@ -1451,14 +1480,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
@@ -1477,7 +1514,7 @@
         client.registerService(regInfo, NsdManager.PROTOCOL_DNS_SD, Runnable::run, regListener);
         waitForIdle();
         verify(mSocketProvider).startMonitoringSockets();
-        verify(mAdvertiser).addService(anyInt(), any(), any());
+        verify(mAdvertiser).addOrUpdateService(anyInt(), any(), any(), anyInt());
 
         // Verify the discovery uses MdnsDiscoveryManager
         final DiscoveryListener discListener = mock(DiscoveryListener.class);
@@ -1510,7 +1547,7 @@
         client.registerService(regInfo, NsdManager.PROTOCOL_DNS_SD, Runnable::run, regListener);
         waitForIdle();
         verify(mSocketProvider).startMonitoringSockets();
-        verify(mAdvertiser).addService(anyInt(), any(), any());
+        verify(mAdvertiser).addOrUpdateService(anyInt(), any(), any(), anyInt());
 
         final Network wifiNetwork1 = new Network(123);
         final Network wifiNetwork2 = new Network(124);
@@ -1696,8 +1733,8 @@
 
     @Test
     @EnableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
-    public void testRegisterOffloadEngine_checkPermission()
-            throws PackageManager.NameNotFoundException {
+    @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    public void testRegisterOffloadEngine_checkPermission_V() {
         final NsdManager client = connectClient(mService);
         final OffloadEngine offloadEngine = mock(OffloadEngine.class);
         doReturn(PERMISSION_DENIED).when(mContext).checkCallingOrSelfPermission(NETWORK_STACK);
@@ -1707,17 +1744,41 @@
         doReturn(PERMISSION_GRANTED).when(mContext).checkCallingOrSelfPermission(
                 REGISTER_NSD_OFFLOAD_ENGINE);
 
-        PermissionInfo permissionInfo = new PermissionInfo("");
-        permissionInfo.packageName = "android";
-        permissionInfo.protectionLevel = PROTECTION_SIGNATURE;
-        doReturn(permissionInfo).when(mPackageManager).getPermissionInfo(
-                REGISTER_NSD_OFFLOAD_ENGINE, 0);
-        client.registerOffloadEngine("iface1", OffloadEngine.OFFLOAD_TYPE_REPLY,
-                OffloadEngine.OFFLOAD_CAPABILITY_BYPASS_MULTICAST_LOCK,
-                Runnable::run, offloadEngine);
-        client.unregisterOffloadEngine(offloadEngine);
+        doReturn(PERMISSION_DENIED).when(mContext).checkCallingOrSelfPermission(
+                REGISTER_NSD_OFFLOAD_ENGINE);
+        doReturn(PERMISSION_GRANTED).when(mContext).checkCallingOrSelfPermission(DEVICE_POWER);
+        assertThrows(SecurityException.class,
+                () -> client.registerOffloadEngine("iface1", OffloadEngine.OFFLOAD_TYPE_REPLY,
+                        OffloadEngine.OFFLOAD_CAPABILITY_BYPASS_MULTICAST_LOCK, Runnable::run,
+                        offloadEngine));
+        doReturn(PERMISSION_GRANTED).when(mContext).checkCallingOrSelfPermission(
+                REGISTER_NSD_OFFLOAD_ENGINE);
+        final OffloadEngine offloadEngine2 = mock(OffloadEngine.class);
+        client.registerOffloadEngine("iface2", OffloadEngine.OFFLOAD_TYPE_REPLY,
+                OffloadEngine.OFFLOAD_CAPABILITY_BYPASS_MULTICAST_LOCK, Runnable::run,
+                offloadEngine2);
+        client.unregisterOffloadEngine(offloadEngine2);
+    }
 
-        // TODO: add checks to test the packageName other than android
+    @Test
+    @EnableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
+    @DevSdkIgnoreRule.IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
+    public void testRegisterOffloadEngine_checkPermission_U() {
+        final NsdManager client = connectClient(mService);
+        final OffloadEngine offloadEngine = mock(OffloadEngine.class);
+        doReturn(PERMISSION_DENIED).when(mContext).checkCallingOrSelfPermission(NETWORK_STACK);
+        doReturn(PERMISSION_DENIED).when(mContext).checkCallingOrSelfPermission(
+                PERMISSION_MAINLINE_NETWORK_STACK);
+        doReturn(PERMISSION_DENIED).when(mContext).checkCallingOrSelfPermission(NETWORK_SETTINGS);
+        doReturn(PERMISSION_GRANTED).when(mContext).checkCallingOrSelfPermission(
+                REGISTER_NSD_OFFLOAD_ENGINE);
+
+        doReturn(PERMISSION_GRANTED).when(mContext).checkCallingOrSelfPermission(DEVICE_POWER);
+        client.registerOffloadEngine("iface2", OffloadEngine.OFFLOAD_TYPE_REPLY,
+                OffloadEngine.OFFLOAD_CAPABILITY_BYPASS_MULTICAST_LOCK, Runnable::run,
+                offloadEngine);
+        client.unregisterOffloadEngine(offloadEngine);
     }
 
 
diff --git a/tests/unit/java/com/android/server/connectivity/AutomaticOnOffKeepaliveTrackerTest.java b/tests/unit/java/com/android/server/connectivity/AutomaticOnOffKeepaliveTrackerTest.java
index 8e19c01..6cc301d 100644
--- a/tests/unit/java/com/android/server/connectivity/AutomaticOnOffKeepaliveTrackerTest.java
+++ b/tests/unit/java/com/android/server/connectivity/AutomaticOnOffKeepaliveTrackerTest.java
@@ -138,81 +138,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
diff --git a/tests/unit/java/com/android/server/connectivity/CarrierPrivilegeAuthenticatorTest.java b/tests/unit/java/com/android/server/connectivity/CarrierPrivilegeAuthenticatorTest.java
index 8113626..f07593e 100644
--- a/tests/unit/java/com/android/server/connectivity/CarrierPrivilegeAuthenticatorTest.java
+++ b/tests/unit/java/com/android/server/connectivity/CarrierPrivilegeAuthenticatorTest.java
@@ -45,6 +45,7 @@
 import android.net.NetworkCapabilities;
 import android.net.TelephonyNetworkSpecifier;
 import android.os.Build;
+import android.os.HandlerThread;
 import android.telephony.SubscriptionManager;
 import android.telephony.TelephonyManager;
 
@@ -52,10 +53,11 @@
 import com.android.networkstack.apishim.TelephonyManagerShimImpl;
 import com.android.networkstack.apishim.common.TelephonyManagerShim.CarrierPrivilegesListenerShim;
 import com.android.networkstack.apishim.common.UnsupportedApiLevelException;
-import com.android.server.ConnectivityService;
+import com.android.server.connectivity.CarrierPrivilegeAuthenticator.Dependencies;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
 import com.android.testutils.DevSdkIgnoreRunner;
 
+import org.junit.After;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -86,10 +88,11 @@
     private final int mCarrierConfigPkgUid = 12345;
     private final String mTestPkg = "com.android.server.connectivity.test";
     private final BroadcastReceiver mMultiSimBroadcastReceiver;
+    @NonNull private final HandlerThread mHandlerThread;
 
     public class TestCarrierPrivilegeAuthenticator extends CarrierPrivilegeAuthenticator {
         TestCarrierPrivilegeAuthenticator(@NonNull final Context c,
-                @NonNull final ConnectivityService.Dependencies deps,
+                @NonNull final Dependencies deps,
                 @NonNull final TelephonyManager t) {
             super(c, deps, t, mTelephonyManagerShim);
         }
@@ -100,6 +103,11 @@
         }
     }
 
+    @After
+    public void tearDown() {
+        mHandlerThread.quit();
+    }
+
     /** Parameters to test both using callbacks or the old broadcast */
     @Parameterized.Parameters
     public static Collection<Boolean> shouldUseCallbacks() {
@@ -111,9 +119,11 @@
         mTelephonyManager = mock(TelephonyManager.class);
         mTelephonyManagerShim = mock(TelephonyManagerShimImpl.class);
         mPackageManager = mock(PackageManager.class);
-        final ConnectivityService.Dependencies deps = mock(ConnectivityService.Dependencies.class);
+        mHandlerThread = new HandlerThread(CarrierPrivilegeAuthenticatorTest.class.getSimpleName());
+        final Dependencies deps = mock(Dependencies.class);
         doReturn(useCallbacks).when(deps).isFeatureEnabled(any() /* context */,
                 eq(CARRIER_SERVICE_CHANGED_USE_CALLBACK));
+        doReturn(mHandlerThread).when(deps).makeHandlerThread();
         doReturn(SUBSCRIPTION_COUNT).when(mTelephonyManager).getActiveModemCount();
         doReturn(mTestPkg).when(mTelephonyManagerShim)
                 .getCarrierServicePackageNameForLogicalSlot(anyInt());
@@ -164,9 +174,9 @@
                 .addTransportType(TRANSPORT_CELLULAR)
                 .setNetworkSpecifier(new TelephonyNetworkSpecifier(0));
 
-        assertTrue(mCarrierPrivilegeAuthenticator.hasCarrierPrivilegeForNetworkCapabilities(
+        assertTrue(mCarrierPrivilegeAuthenticator.isCarrierServiceUidForNetworkCapabilities(
                 mCarrierConfigPkgUid, ncBuilder.build()));
-        assertFalse(mCarrierPrivilegeAuthenticator.hasCarrierPrivilegeForNetworkCapabilities(
+        assertFalse(mCarrierPrivilegeAuthenticator.isCarrierServiceUidForNetworkCapabilities(
                 mCarrierConfigPkgUid + 1, ncBuilder.build()));
     }
 
@@ -203,9 +213,9 @@
                 .addTransportType(TRANSPORT_CELLULAR)
                 .setNetworkSpecifier(specifier)
                 .build();
-        assertTrue(mCarrierPrivilegeAuthenticator.hasCarrierPrivilegeForNetworkCapabilities(
+        assertTrue(mCarrierPrivilegeAuthenticator.isCarrierServiceUidForNetworkCapabilities(
                 mCarrierConfigPkgUid, nc));
-        assertFalse(mCarrierPrivilegeAuthenticator.hasCarrierPrivilegeForNetworkCapabilities(
+        assertFalse(mCarrierPrivilegeAuthenticator.isCarrierServiceUidForNetworkCapabilities(
                 mCarrierConfigPkgUid + 1, nc));
     }
 
@@ -225,9 +235,9 @@
         listener.onCarrierPrivilegesChanged(Collections.emptyList(), new int[] {});
         listener.onCarrierServiceChanged(null, applicationInfo.uid);
 
-        assertFalse(mCarrierPrivilegeAuthenticator.hasCarrierPrivilegeForNetworkCapabilities(
+        assertFalse(mCarrierPrivilegeAuthenticator.isCarrierServiceUidForNetworkCapabilities(
                 mCarrierConfigPkgUid, nc));
-        assertTrue(mCarrierPrivilegeAuthenticator.hasCarrierPrivilegeForNetworkCapabilities(
+        assertTrue(mCarrierPrivilegeAuthenticator.isCarrierServiceUidForNetworkCapabilities(
                 mCarrierConfigPkgUid + 1, nc));
     }
 
@@ -238,11 +248,11 @@
 
         final NetworkCapabilities.Builder ncBuilder = new NetworkCapabilities.Builder();
         ncBuilder.addTransportType(TRANSPORT_CELLULAR);
-        assertFalse(mCarrierPrivilegeAuthenticator.hasCarrierPrivilegeForNetworkCapabilities(
+        assertFalse(mCarrierPrivilegeAuthenticator.isCarrierServiceUidForNetworkCapabilities(
                 mCarrierConfigPkgUid, ncBuilder.build()));
 
         ncBuilder.setNetworkSpecifier(new TelephonyNetworkSpecifier(0));
-        assertTrue(mCarrierPrivilegeAuthenticator.hasCarrierPrivilegeForNetworkCapabilities(
+        assertTrue(mCarrierPrivilegeAuthenticator.isCarrierServiceUidForNetworkCapabilities(
                 mCarrierConfigPkgUid, ncBuilder.build()));
 
         // The builder for NetworkCapabilities doesn't allow removing the transport as long as a
@@ -251,7 +261,7 @@
         ncBuilder.removeTransportType(TRANSPORT_CELLULAR);
         ncBuilder.addTransportType(TRANSPORT_WIFI);
         ncBuilder.setNetworkSpecifier(new TelephonyNetworkSpecifier(0));
-        assertFalse(mCarrierPrivilegeAuthenticator.hasCarrierPrivilegeForNetworkCapabilities(
+        assertFalse(mCarrierPrivilegeAuthenticator.isCarrierServiceUidForNetworkCapabilities(
                 mCarrierConfigPkgUid, ncBuilder.build()));
     }
 }
diff --git a/tests/unit/java/com/android/server/connectivity/DnsManagerTest.java b/tests/unit/java/com/android/server/connectivity/DnsManagerTest.java
index 545ed16..44512bb 100644
--- a/tests/unit/java/com/android/server/connectivity/DnsManagerTest.java
+++ b/tests/unit/java/com/android/server/connectivity/DnsManagerTest.java
@@ -19,6 +19,7 @@
 import static android.net.ConnectivitySettingsManager.PRIVATE_DNS_DEFAULT_MODE;
 import static android.net.ConnectivitySettingsManager.PRIVATE_DNS_MODE;
 import static android.net.ConnectivitySettingsManager.PRIVATE_DNS_MODE_OFF;
+import static android.net.ConnectivitySettingsManager.PRIVATE_DNS_MODE_OPPORTUNISTIC;
 import static android.net.ConnectivitySettingsManager.PRIVATE_DNS_MODE_PROVIDER_HOSTNAME;
 import static android.net.ConnectivitySettingsManager.PRIVATE_DNS_SPECIFIER;
 import static android.net.NetworkCapabilities.MAX_TRANSPORT;
@@ -140,7 +141,8 @@
         assertResolverOptionsEquals(actual.resolverOptions, expected.resolverOptions);
         assertContainsExactly(actual.transportTypes, expected.transportTypes);
         assertEquals(actual.meteredNetwork, expected.meteredNetwork);
-        assertFieldCountEquals(17, ResolverParamsParcel.class);
+        assertEquals(actual.dohParams, expected.dohParams);
+        assertFieldCountEquals(18, ResolverParamsParcel.class);
     }
 
     @Before
@@ -328,14 +330,14 @@
     public void testOverrideDefaultMode() throws Exception {
         // Hard-coded default is opportunistic mode.
         final PrivateDnsConfig cfgAuto = DnsManager.getPrivateDnsConfig(mCtx);
-        assertTrue(cfgAuto.useTls);
+        assertEquals(PRIVATE_DNS_MODE_OPPORTUNISTIC, cfgAuto.mode);
         assertEquals("", cfgAuto.hostname);
         assertEquals(new InetAddress[0], cfgAuto.ips);
 
         // Pretend a gservices push sets the default to "off".
         ConnectivitySettingsManager.setPrivateDnsDefaultMode(mCtx, PRIVATE_DNS_MODE_OFF);
         final PrivateDnsConfig cfgOff = DnsManager.getPrivateDnsConfig(mCtx);
-        assertFalse(cfgOff.useTls);
+        assertEquals(PRIVATE_DNS_MODE_OFF, cfgOff.mode);
         assertEquals("", cfgOff.hostname);
         assertEquals(new InetAddress[0], cfgOff.ips);
 
@@ -343,7 +345,7 @@
         ConnectivitySettingsManager.setPrivateDnsMode(mCtx, PRIVATE_DNS_MODE_PROVIDER_HOSTNAME);
         ConnectivitySettingsManager.setPrivateDnsHostname(mCtx, "strictmode.com");
         final PrivateDnsConfig cfgStrict = DnsManager.getPrivateDnsConfig(mCtx);
-        assertTrue(cfgStrict.useTls);
+        assertEquals(PRIVATE_DNS_MODE_PROVIDER_HOSTNAME, cfgStrict.mode);
         assertEquals("strictmode.com", cfgStrict.hostname);
         assertEquals(new InetAddress[0], cfgStrict.ips);
     }
@@ -381,6 +383,7 @@
         expectedParams.transportTypes = TEST_TRANSPORT_TYPES;
         expectedParams.resolverOptions = null;
         expectedParams.meteredNetwork = true;
+        expectedParams.dohParams = null;
         assertResolverParamsEquals(actualParams, expectedParams);
     }
 
@@ -417,7 +420,7 @@
 
         // The PrivateDnsConfig map is empty, so the default PRIVATE_DNS_OFF is returned.
         PrivateDnsConfig privateDnsCfg = mDnsManager.getPrivateDnsConfig(network);
-        assertFalse(privateDnsCfg.useTls);
+        assertEquals(PRIVATE_DNS_MODE_OFF, privateDnsCfg.mode);
         assertEquals("", privateDnsCfg.hostname);
         assertEquals(new InetAddress[0], privateDnsCfg.ips);
 
@@ -429,7 +432,7 @@
                         VALIDATION_RESULT_SUCCESS));
         mDnsManager.updatePrivateDnsStatus(TEST_NETID, lp);
         privateDnsCfg = mDnsManager.getPrivateDnsConfig(network);
-        assertTrue(privateDnsCfg.useTls);
+        assertEquals(PRIVATE_DNS_MODE_OPPORTUNISTIC, privateDnsCfg.mode);
         assertEquals("", privateDnsCfg.hostname);
         assertEquals(new InetAddress[0], privateDnsCfg.ips);
 
@@ -437,14 +440,14 @@
         mDnsManager.updatePrivateDns(network, new PrivateDnsConfig(tlsName, tlsAddrs));
         mDnsManager.updatePrivateDnsStatus(TEST_NETID, lp);
         privateDnsCfg = mDnsManager.getPrivateDnsConfig(network);
-        assertTrue(privateDnsCfg.useTls);
+        assertEquals(PRIVATE_DNS_MODE_PROVIDER_HOSTNAME, privateDnsCfg.mode);
         assertEquals(tlsName, privateDnsCfg.hostname);
         assertEquals(tlsAddrs, privateDnsCfg.ips);
 
         // The network is removed, so the PrivateDnsConfig map becomes empty again.
         mDnsManager.removeNetwork(network);
         privateDnsCfg = mDnsManager.getPrivateDnsConfig(network);
-        assertFalse(privateDnsCfg.useTls);
+        assertEquals(PRIVATE_DNS_MODE_OFF, privateDnsCfg.mode);
         assertEquals("", privateDnsCfg.hostname);
         assertEquals(new InetAddress[0], privateDnsCfg.ips);
     }
diff --git a/tests/unit/java/com/android/server/connectivity/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/NetworkNotificationManagerTest.java b/tests/unit/java/com/android/server/connectivity/NetworkNotificationManagerTest.java
index b319c30..7121ed4 100644
--- a/tests/unit/java/com/android/server/connectivity/NetworkNotificationManagerTest.java
+++ b/tests/unit/java/com/android/server/connectivity/NetworkNotificationManagerTest.java
@@ -54,6 +54,7 @@
 import android.content.res.Resources;
 import android.net.NetworkCapabilities;
 import android.net.NetworkInfo;
+import android.net.TelephonyNetworkSpecifier;
 import android.os.Build;
 import android.os.Bundle;
 import android.os.PowerManager;
@@ -107,12 +108,16 @@
     private static final long TEST_TIMEOUT_MS = 10_000L;
     private static final long UI_AUTOMATOR_WAIT_TIME_MILLIS = TEST_TIMEOUT_MS;
 
-    static final NetworkCapabilities CELL_CAPABILITIES = new NetworkCapabilities();
-    static final NetworkCapabilities WIFI_CAPABILITIES = new NetworkCapabilities();
-    static final NetworkCapabilities VPN_CAPABILITIES = new NetworkCapabilities();
+    private static final int TEST_SUB_ID = 43;
+    private static final String TEST_OPERATOR_NAME = "Test Operator";
+    private static final NetworkCapabilities CELL_CAPABILITIES = new NetworkCapabilities();
+    private static final NetworkCapabilities WIFI_CAPABILITIES = new NetworkCapabilities();
+    private static final NetworkCapabilities VPN_CAPABILITIES = new NetworkCapabilities();
     static {
         CELL_CAPABILITIES.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR);
         CELL_CAPABILITIES.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
+        CELL_CAPABILITIES.setNetworkSpecifier(new TelephonyNetworkSpecifier.Builder()
+                .setSubscriptionId(TEST_SUB_ID).build());
 
         WIFI_CAPABILITIES.addTransportType(NetworkCapabilities.TRANSPORT_WIFI);
         WIFI_CAPABILITIES.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
@@ -149,6 +154,7 @@
     @Mock DisplayMetrics mDisplayMetrics;
     @Mock PackageManager mPm;
     @Mock TelephonyManager mTelephonyManager;
+    @Mock TelephonyManager mTestSubIdTelephonyManager;
     @Mock NotificationManager mNotificationManager;
     @Mock NetworkAgentInfo mWifiNai;
     @Mock NetworkAgentInfo mCellNai;
@@ -170,18 +176,21 @@
         mVpnNai.networkInfo = mNetworkInfo;
         mDisplayMetrics.density = 2.275f;
         doReturn(true).when(mVpnNai).isVPN();
-        when(mCtx.getResources()).thenReturn(mResources);
-        when(mCtx.getPackageManager()).thenReturn(mPm);
-        when(mCtx.getApplicationInfo()).thenReturn(new ApplicationInfo());
+        doReturn(mResources).when(mCtx).getResources();
+        doReturn(mPm).when(mCtx).getPackageManager();
+        doReturn(new ApplicationInfo()).when(mCtx).getApplicationInfo();
         final Context asUserCtx = mock(Context.class, AdditionalAnswers.delegatesTo(mCtx));
         doReturn(UserHandle.ALL).when(asUserCtx).getUser();
-        when(mCtx.createContextAsUser(eq(UserHandle.ALL), anyInt())).thenReturn(asUserCtx);
-        when(mCtx.getSystemService(eq(Context.NOTIFICATION_SERVICE)))
-                .thenReturn(mNotificationManager);
-        when(mNetworkInfo.getExtraInfo()).thenReturn(TEST_EXTRA_INFO);
+        doReturn(asUserCtx).when(mCtx).createContextAsUser(eq(UserHandle.ALL), anyInt());
+        doReturn(mNotificationManager).when(mCtx)
+                .getSystemService(eq(Context.NOTIFICATION_SERVICE));
+        doReturn(TEST_EXTRA_INFO).when(mNetworkInfo).getExtraInfo();
         ConnectivityResources.setResourcesContextForTest(mCtx);
-        when(mResources.getColor(anyInt(), any())).thenReturn(0xFF607D8B);
-        when(mResources.getDisplayMetrics()).thenReturn(mDisplayMetrics);
+        doReturn(0xFF607D8B).when(mResources).getColor(anyInt(), any());
+        doReturn(mDisplayMetrics).when(mResources).getDisplayMetrics();
+        doReturn(mTestSubIdTelephonyManager).when(mTelephonyManager)
+                .createForSubscriptionId(TEST_SUB_ID);
+        doReturn(TEST_OPERATOR_NAME).when(mTestSubIdTelephonyManager).getNetworkOperatorName();
 
         // Come up with some credible-looking transport names. The actual values do not matter.
         String[] transportNames = new String[NetworkCapabilities.MAX_TRANSPORT + 1];
@@ -532,4 +541,44 @@
                 R.string.wifi_no_internet, TEST_EXTRA_INFO,
                 R.string.wifi_no_internet_detailed);
     }
+
+    private void runTelephonySignInNotificationTest(String testTitle, String testContents) {
+        final int id = 101;
+        final String tag = NetworkNotificationManager.tagFor(id);
+        mManager.showNotification(id, SIGN_IN, mCellNai, null, null, false);
+
+        final ArgumentCaptor<Notification> noteCaptor = ArgumentCaptor.forClass(Notification.class);
+        verify(mNotificationManager).notify(eq(tag), eq(SIGN_IN.eventId), noteCaptor.capture());
+        final Bundle noteExtras = noteCaptor.getValue().extras;
+        assertEquals(testTitle, noteExtras.getString(Notification.EXTRA_TITLE));
+        assertEquals(testContents, noteExtras.getString(Notification.EXTRA_TEXT));
+    }
+
+    @Test
+    public void testTelephonySignInNotification() {
+        final String testTitle = "Telephony no internet title";
+        final String testContents = "Add data for " + TEST_OPERATOR_NAME;
+        // The test does not use real resources as they are in the ConnectivityResources package,
+        // which is tricky to use (requires resolving the package, QUERY_ALL_PACKAGES permission).
+        doReturn(testTitle).when(mResources).getString(
+                R.string.mobile_network_available_no_internet);
+        doReturn(testContents).when(mResources).getString(
+                R.string.mobile_network_available_no_internet_detailed, TEST_OPERATOR_NAME);
+
+        runTelephonySignInNotificationTest(testTitle, testContents);
+    }
+
+    @Test
+    public void testTelephonySignInNotification_NoOperator() {
+        doReturn("").when(mTestSubIdTelephonyManager).getNetworkOperatorName();
+
+        final String testTitle = "Telephony no internet title";
+        final String testContents = "Add data";
+        doReturn(testTitle).when(mResources).getString(
+                R.string.mobile_network_available_no_internet);
+        doReturn(testContents).when(mResources).getString(
+                R.string.mobile_network_available_no_internet_detailed_unknown_carrier);
+
+        runTelephonySignInNotificationTest(testTitle, testContents);
+    }
 }
diff --git a/tests/unit/java/com/android/server/connectivity/NetworkRankerTest.kt b/tests/unit/java/com/android/server/connectivity/NetworkRankerTest.kt
index 87f7369..1e3f389 100644
--- a/tests/unit/java/com/android/server/connectivity/NetworkRankerTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/NetworkRankerTest.kt
@@ -18,12 +18,9 @@
 
 import android.net.NetworkCapabilities
 import android.net.NetworkCapabilities.NET_CAPABILITY_CAPTIVE_PORTAL as NET_CAP_PORTAL
-import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET as NET_CAP_INTERNET
-import android.net.NetworkCapabilities.NET_CAPABILITY_PRIORITIZE_BANDWIDTH as NET_CAP_PRIO_BW
 import android.net.NetworkCapabilities.TRANSPORT_CELLULAR
 import android.net.NetworkCapabilities.TRANSPORT_WIFI
 import android.net.NetworkScore.KEEP_CONNECTED_NONE
-import android.net.NetworkScore.POLICY_TRANSPORT_PRIMARY
 import android.net.NetworkScore.POLICY_EXITING as EXITING
 import android.net.NetworkScore.POLICY_TRANSPORT_PRIMARY as PRIMARY
 import android.net.NetworkScore.POLICY_YIELD_TO_BAD_WIFI as YIELD_TO_BAD_WIFI
@@ -53,8 +50,8 @@
 class NetworkRankerTest(private val activelyPreferBadWifi: Boolean) {
     private val mRanker = NetworkRanker(NetworkRanker.Configuration(activelyPreferBadWifi))
 
-    private class TestScore(private val sc: FullScore, private val nc: NetworkCapabilities) :
-            NetworkRanker.Scoreable {
+    private class TestScore(private val sc: FullScore, private val nc: NetworkCapabilities)
+            : NetworkRanker.Scoreable {
         override fun getScore() = sc
         override fun getCapsNoCopy(): NetworkCapabilities = nc
     }
@@ -199,41 +196,4 @@
         val badExitingWifi = TestScore(score(EVER_EVALUATED, EVER_VALIDATED, EXITING), CAPS_WIFI)
         assertEquals(cell, rank(cell, badExitingWifi))
     }
-
-    @Test
-    fun testValidatedPolicyStrongerThanSlice() {
-        val unvalidatedNonslice = TestScore(score(EVER_EVALUATED),
-                caps(TRANSPORT_CELLULAR, NET_CAP_INTERNET))
-        val slice = TestScore(score(EVER_EVALUATED, IS_VALIDATED),
-                caps(TRANSPORT_CELLULAR, NET_CAP_INTERNET, NET_CAP_PRIO_BW))
-        assertEquals(slice, rank(slice, unvalidatedNonslice))
-    }
-
-    @Test
-    fun testPrimaryPolicyStrongerThanSlice() {
-        val nonslice = TestScore(score(EVER_EVALUATED),
-                caps(TRANSPORT_CELLULAR, NET_CAP_INTERNET))
-        val primarySlice = TestScore(score(EVER_EVALUATED, POLICY_TRANSPORT_PRIMARY),
-                caps(TRANSPORT_CELLULAR, NET_CAP_INTERNET, NET_CAP_PRIO_BW))
-        assertEquals(primarySlice, rank(nonslice, primarySlice))
-    }
-
-    @Test
-    fun testPreferNonSlices() {
-        // Slices lose to non-slices for general ranking
-        val nonslice = TestScore(score(EVER_EVALUATED, IS_VALIDATED),
-                caps(TRANSPORT_CELLULAR, NET_CAP_INTERNET))
-        val slice = TestScore(score(EVER_EVALUATED, IS_VALIDATED),
-                caps(TRANSPORT_CELLULAR, NET_CAP_INTERNET, NET_CAP_PRIO_BW))
-        assertEquals(nonslice, rank(slice, nonslice))
-    }
-
-    @Test
-    fun testSlicePolicyStrongerThanTransport() {
-        val nonSliceCell = TestScore(score(EVER_EVALUATED, IS_VALIDATED),
-                caps(TRANSPORT_CELLULAR, NET_CAP_INTERNET))
-        val sliceWifi = TestScore(score(EVER_EVALUATED, IS_VALIDATED),
-                caps(TRANSPORT_WIFI, NET_CAP_INTERNET, NET_CAP_PRIO_BW))
-        assertEquals(nonSliceCell, rank(nonSliceCell, sliceWifi))
-    }
 }
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/VpnTest.java b/tests/unit/java/com/android/server/connectivity/VpnTest.java
index ff801e5..c9cece0 100644
--- a/tests/unit/java/com/android/server/connectivity/VpnTest.java
+++ b/tests/unit/java/com/android/server/connectivity/VpnTest.java
@@ -74,7 +74,9 @@
 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;
@@ -116,7 +118,6 @@
 import android.net.IpSecTunnelInterfaceResponse;
 import android.net.LinkAddress;
 import android.net.LinkProperties;
-import android.net.LocalSocket;
 import android.net.Network;
 import android.net.NetworkAgent;
 import android.net.NetworkAgentConfig;
@@ -188,14 +189,12 @@
 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.BufferedWriter;
-import java.io.File;
 import java.io.FileDescriptor;
-import java.io.FileWriter;
 import java.io.IOException;
 import java.io.StringWriter;
 import java.net.Inet4Address;
@@ -204,14 +203,13 @@
 import java.net.UnknownHostException;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collection;
 import java.util.Collections;
-import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import java.util.SortedSet;
 import java.util.TreeSet;
-import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ScheduledFuture;
 import java.util.concurrent.ScheduledThreadPoolExecutor;
 import java.util.concurrent.TimeUnit;
@@ -314,6 +312,8 @@
     @Mock DeviceIdleInternal mDeviceIdleInternal;
     private final VpnProfile mVpnProfile;
 
+    @Captor private ArgumentCaptor<Collection<Range<Integer>>> mUidRangesCaptor;
+
     private IpSecManager mIpSecManager;
     private TestDeps mTestDeps;
 
@@ -1093,37 +1093,53 @@
         }
     }
 
-    private Vpn prepareVpnForVerifyAppExclusionList() throws Exception {
-        final Vpn vpn = createVpn(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
+    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));
-
-        vpn.startVpnProfile(TEST_VPN_PKG);
+        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 = prepareVpnForVerifyAppExclusionList();
+        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)));
-        assertEquals(vpn.createUserAndRestrictedProfilesRanges(
-                PRIMARY_USER.id, null, Arrays.asList(PKGS)),
-                vpn.mNetworkCapabilities.getUids());
+        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 = prepareVpnForVerifyAppExclusionList();
+        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));
 
@@ -1132,33 +1148,36 @@
         // 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(makeVpnUidRangeSet(PRIMARY_USER.id, newExcludedUids),
-                vpn.mNetworkCapabilities.getUids());
+        assertEquals(newUidRanges, vpn.mNetworkCapabilities.getUids());
         ArgumentCaptor<NetworkCapabilities> ncCaptor =
                 ArgumentCaptor.forClass(NetworkCapabilities.class);
         verify(mMockNetworkAgent).doSendNetworkCapabilities(ncCaptor.capture());
-        assertEquals(makeVpnUidRangeSet(PRIMARY_USER.id, newExcludedUids),
-                ncCaptor.getValue().getUids());
+        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(makeVpnUidRangeSet(PRIMARY_USER.id, newExcludedUids),
-                vpn.mNetworkCapabilities.getUids());
+        assertEquals(newUidRanges, vpn.mNetworkCapabilities.getUids());
         verify(mMockNetworkAgent).doSendNetworkCapabilities(ncCaptor.capture());
-        assertEquals(makeVpnUidRangeSet(PRIMARY_USER.id, newExcludedUids),
-                ncCaptor.getValue().getUids());
+        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) {
@@ -1784,6 +1803,9 @@
                 .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);
@@ -1792,6 +1814,8 @@
         // 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.
@@ -1811,16 +1835,43 @@
                 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;
-            final IkeSessionCallback ikeCb2 = verifyRetryAndGetNewIkeCb(retryIndex++);
+            // 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++);
 
-            mExecutor.execute(() -> ikeCb2.onClosedWithException(exception));
+            // 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));
         }
     }
 
@@ -1982,16 +2033,7 @@
 
     private Vpn startLegacyVpn(final Vpn vpn, final VpnProfile vpnProfile) throws Exception {
         setMockedUsers(PRIMARY_USER);
-
-        // Dummy egress interface
-        final LinkProperties lp = new LinkProperties();
-        lp.setInterfaceName(EGRESS_IFACE);
-
-        final RouteInfo defaultRoute = new RouteInfo(new IpPrefix(Inet4Address.ANY, 0),
-                        InetAddresses.parseNumericAddress("192.0.2.0"), EGRESS_IFACE);
-        lp.addRoute(defaultRoute);
-
-        vpn.startLegacyVpn(vpnProfile, EGRESS_NETWORK, lp);
+        vpn.startLegacyVpn(vpnProfile);
         return vpn;
     }
 
@@ -2103,7 +2145,9 @@
         when(mVpnProfileStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG)))
                 .thenReturn(vpnProfile.encode());
 
-        vpn.startVpnProfile(TEST_VPN_PKG);
+        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
@@ -2196,6 +2240,7 @@
         final PlatformVpnSnapshot vpnSnapShot = verifySetupPlatformVpn(
                 createIkeConfig(createIkeConnectInfo(), true /* isMobikeEnabled */));
         vpnSnapShot.vpn.mVpnRunner.exitVpnRunner();
+        verify(mConnectivityManager).setVpnDefaultForUids(anyString(), eq(Collections.EMPTY_LIST));
     }
 
     @Test
@@ -2934,7 +2979,7 @@
                         null /* iface */, RTN_UNREACHABLE));
         assertEquals(expectedRoutes, lp.getRoutes());
 
-        verify(mMockNetworkAgent).unregister();
+        verify(mMockNetworkAgent, timeout(TEST_TIMEOUT_MS)).unregister();
     }
 
     @Test
@@ -3104,6 +3149,20 @@
         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();
@@ -3113,20 +3172,7 @@
 
     // Make it public and un-final so as to spy it
     public class TestDeps extends Vpn.Dependencies {
-        public final CompletableFuture<String[]> racoonArgs = new CompletableFuture();
-        public final CompletableFuture<String[]> mtpdArgs = new CompletableFuture();
-        public final File mStateFile;
-
-        private final HashMap<String, Boolean> mRunningServices = new HashMap<>();
-
-        TestDeps() {
-            try {
-                mStateFile = File.createTempFile("vpnTest", ".tmp");
-                mStateFile.deleteOnExit();
-            } catch (final IOException e) {
-                throw new RuntimeException(e);
-            }
-        }
+        TestDeps() {}
 
         @Override
         public boolean isCallerSystem() {
@@ -3134,89 +3180,11 @@
         }
 
         @Override
-        public void startService(final String serviceName) {
-            mRunningServices.put(serviceName, true);
-        }
-
-        @Override
-        public void stopService(final String serviceName) {
-            mRunningServices.put(serviceName, false);
-        }
-
-        @Override
-        public boolean isServiceRunning(final String serviceName) {
-            return mRunningServices.getOrDefault(serviceName, false);
-        }
-
-        @Override
-        public boolean isServiceStopped(final String serviceName) {
-            return !isServiceRunning(serviceName);
-        }
-
-        @Override
-        public File getStateFile() {
-            return mStateFile;
-        }
-
-        @Override
         public PendingIntent getIntentForStatusPanel(Context context) {
             return null;
         }
 
         @Override
-        public void sendArgumentsToDaemon(
-                final String daemon, final LocalSocket socket, final String[] arguments,
-                final Vpn.RetryScheduler interruptChecker) throws IOException {
-            if ("racoon".equals(daemon)) {
-                racoonArgs.complete(arguments);
-            } else if ("mtpd".equals(daemon)) {
-                writeStateFile(arguments);
-                mtpdArgs.complete(arguments);
-            } else {
-                throw new UnsupportedOperationException("Unsupported daemon : " + daemon);
-            }
-        }
-
-        private void writeStateFile(final String[] arguments) throws IOException {
-            mStateFile.delete();
-            mStateFile.createNewFile();
-            mStateFile.deleteOnExit();
-            final BufferedWriter writer = new BufferedWriter(
-                    new FileWriter(mStateFile, false /* append */));
-            writer.write(EGRESS_IFACE);
-            writer.write("\n");
-            // addresses
-            writer.write("10.0.0.1/24\n");
-            // routes
-            writer.write("192.168.6.0/24\n");
-            // dns servers
-            writer.write("192.168.6.1\n");
-            // search domains
-            writer.write("vpn.searchdomains.com\n");
-            // endpoint - intentionally empty
-            writer.write("\n");
-            writer.flush();
-            writer.close();
-        }
-
-        @Override
-        @NonNull
-        public InetAddress resolve(final String endpoint) {
-            try {
-                // If a numeric IP address, return it.
-                return InetAddress.parseNumericAddress(endpoint);
-            } catch (IllegalArgumentException e) {
-                // Otherwise, return some token IP to test for.
-                return InetAddress.parseNumericAddress("5.6.7.8");
-            }
-        }
-
-        @Override
-        public boolean isInterfacePresent(final Vpn vpn, final String iface) {
-            return true;
-        }
-
-        @Override
         public ParcelFileDescriptor adoptFd(Vpn vpn, int mtu) {
             return new ParcelFileDescriptor(new FileDescriptor());
         }
@@ -3248,12 +3216,6 @@
         }
 
         @Override
-        public long getNextRetryDelayMs(int retryCount) {
-            // Simply return retryCount as the delay seconds for retrying.
-            return retryCount * 1000;
-        }
-
-        @Override
         public long getValidationFailRecoveryMs(int retryCount) {
             // Simply return retryCount as the delay seconds for retrying.
             return retryCount * 100L;
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 a86f923..f753c93 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsAdvertiserTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsAdvertiserTest.kt
@@ -16,17 +16,23 @@
 
 package com.android.server.connectivity.mdns
 
+import android.content.Context
+import android.content.res.Resources
 import android.net.InetAddresses.parseNumericAddress
 import android.net.LinkAddress
 import android.net.Network
+import android.net.nsd.NsdManager
 import android.net.nsd.NsdServiceInfo
 import android.net.nsd.OffloadEngine
 import android.net.nsd.OffloadServiceInfo
 import android.os.Build
 import android.os.Handler
 import android.os.HandlerThread
+import com.android.connectivity.resources.R
 import com.android.net.module.util.SharedLog
+import com.android.server.connectivity.ConnectivityResources
 import com.android.server.connectivity.mdns.MdnsAdvertiser.AdvertiserCallback
+import com.android.server.connectivity.mdns.MdnsInterfaceAdvertiser.CONFLICT_SERVICE
 import com.android.server.connectivity.mdns.MdnsSocketProvider.SocketCallback
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
 import com.android.testutils.DevSdkIgnoreRunner
@@ -54,9 +60,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")
@@ -67,10 +74,13 @@
 private val TEST_SOCKETKEY_2 = SocketKey(1002 /* interfaceIndex */)
 private val TEST_HOSTNAME = arrayOf("Android_test", "local")
 private const val TEST_SUBTYPE = "_subtype"
+private const val TEST_SUBTYPE2 = "_subtype2"
 private val TEST_INTERFACE1 = "test_iface1"
 private val TEST_INTERFACE2 = "test_iface2"
+private val TEST_CLIENT_UID_1 = 10010
 private val TEST_OFFLOAD_PACKET1 = byteArrayOf(0x01, 0x02, 0x03)
 private val TEST_OFFLOAD_PACKET2 = byteArrayOf(0x02, 0x03, 0x04)
+private val DEFAULT_ADVERTISING_OPTION = MdnsAdvertisingOptions.getDefaultOptions()
 
 private val SERVICE_1 = NsdServiceInfo("TestServiceName", "_advertisertest._tcp").apply {
     port = 12345
@@ -78,6 +88,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
@@ -91,6 +108,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
@@ -132,6 +157,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 {
@@ -146,6 +177,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
@@ -166,12 +199,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()
     }
@@ -185,8 +227,9 @@
     @Test
     fun testAddService_OneNetwork() {
         val advertiser =
-            MdnsAdvertiser(thread.looper, socketProvider, cb, mockDeps, sharedlog, flags)
-        postSync { advertiser.addService(SERVICE_ID_1, SERVICE_1, null /* subtype */) }
+            MdnsAdvertiser(thread.looper, socketProvider, cb, mockDeps, sharedlog, flags, context)
+        postSync { advertiser.addOrUpdateService(SERVICE_ID_1, SERVICE_1,
+                DEFAULT_ADVERTISING_OPTION, TEST_CLIENT_UID_1) }
 
         val socketCbCaptor = ArgumentCaptor.forClass(SocketCallback::class.java)
         verify(socketProvider).requestSocket(eq(TEST_NETWORK_1), socketCbCaptor.capture())
@@ -213,7 +256,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)
@@ -244,13 +290,14 @@
     }
 
     @Test
-    fun testAddService_AllNetworks() {
+    fun testAddService_AllNetworksWithSubType() {
         val advertiser =
-            MdnsAdvertiser(thread.looper, socketProvider, cb, mockDeps, sharedlog, flags)
-        postSync { advertiser.addService(SERVICE_ID_1, ALL_NETWORKS_SERVICE, TEST_SUBTYPE) }
+            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
@@ -266,9 +313,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))
         verify(mockInterfaceAdvertiser2).addService(
-                anyInt(), eq(ALL_NETWORKS_SERVICE), eq(TEST_SUBTYPE))
+                anyInt(), eq(ALL_NETWORKS_SERVICE_SUBTYPE))
 
         doReturn(false).`when`(mockInterfaceAdvertiser1).isProbing(SERVICE_ID_1)
         postSync { intAdvCbCaptor1.value.onServiceProbingSucceeded(
@@ -282,12 +329,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)
@@ -315,27 +371,89 @@
     }
 
     @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)
-        postSync { advertiser.addService(SERVICE_ID_1, SERVICE_1, null /* subtype */) }
+            MdnsAdvertiser(thread.looper, socketProvider, cb, mockDeps, sharedlog, flags, context)
+        postSync { advertiser.addOrUpdateService(SERVICE_ID_1, SERVICE_1,
+                DEFAULT_ADVERTISING_OPTION, TEST_CLIENT_UID_1) }
 
         val oneNetSocketCbCaptor = ArgumentCaptor.forClass(SocketCallback::class.java)
         verify(socketProvider).requestSocket(eq(TEST_NETWORK_1), oneNetSocketCbCaptor.capture())
         val oneNetSocketCb = oneNetSocketCbCaptor.value
 
         // Register a service with the same name on all networks (name conflict)
-        postSync { advertiser.addService(SERVICE_ID_2, ALL_NETWORKS_SERVICE, null /* subtype */) }
+        postSync { advertiser.addOrUpdateService(SERVICE_ID_2, ALL_NETWORKS_SERVICE,
+                DEFAULT_ADVERTISING_OPTION, TEST_CLIENT_UID_1) }
         val allNetSocketCbCaptor = ArgumentCaptor.forClass(SocketCallback::class.java)
         verify(socketProvider).requestSocket(eq(null), allNetSocketCbCaptor.capture())
         val allNetSocketCb = allNetSocketCbCaptor.value
 
-        postSync { advertiser.addService(LONG_SERVICE_ID_1, LONG_SERVICE_1, null /* subtype */) }
-        postSync { advertiser.addService(LONG_SERVICE_ID_2, LONG_ALL_NETWORKS_SERVICE,
-                null /* subtype */) }
+        postSync { advertiser.addOrUpdateService(LONG_SERVICE_ID_1, LONG_SERVICE_1,
+                DEFAULT_ADVERTISING_OPTION, TEST_CLIENT_UID_1) }
+        postSync { advertiser.addOrUpdateService(LONG_SERVICE_ID_2, LONG_ALL_NETWORKS_SERVICE,
+                DEFAULT_ADVERTISING_OPTION, TEST_CLIENT_UID_1) }
 
-        postSync { advertiser.addService(CASE_INSENSITIVE_TEST_SERVICE_ID, ALL_NETWORKS_SERVICE_2,
-                null /* subtype */) }
+        postSync { advertiser.addOrUpdateService(CASE_INSENSITIVE_TEST_SERVICE_ID,
+                ALL_NETWORKS_SERVICE_2, DEFAULT_ADVERTISING_OPTION, TEST_CLIENT_UID_1) }
 
         // Callbacks for matching network and all networks both get the socket
         postSync {
@@ -371,15 +489,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) })
         verify(mockInterfaceAdvertiser1).addService(eq(SERVICE_ID_2),
-                argThat { it.matches(expectedRenamed) }, eq(null))
+                argThat { it.matches(expectedRenamed) })
         verify(mockInterfaceAdvertiser1).addService(eq(LONG_SERVICE_ID_1),
-                argThat { it.matches(LONG_SERVICE_1) }, eq(null))
+                argThat { it.matches(LONG_SERVICE_1) })
         verify(mockInterfaceAdvertiser1).addService(eq(LONG_SERVICE_ID_2),
-            argThat { it.matches(expectedLongRenamed) }, eq(null))
+            argThat { it.matches(expectedLongRenamed) })
         verify(mockInterfaceAdvertiser1).addService(eq(CASE_INSENSITIVE_TEST_SERVICE_ID),
-            argThat { it.matches(expectedCaseInsensitiveRenamed) }, eq(null))
+            argThat { it.matches(expectedCaseInsensitiveRenamed) })
 
         doReturn(false).`when`(mockInterfaceAdvertiser1).isProbing(SERVICE_ID_1)
         postSync { intAdvCbCaptor.value.onServiceProbingSucceeded(
@@ -400,11 +518,52 @@
     }
 
     @Test
+    fun testAddOrUpdateService_Updates() {
+        val advertiser =
+                MdnsAdvertiser(thread.looper, socketProvider, cb, mockDeps, sharedlog, flags,
+                    context)
+        postSync { advertiser.addOrUpdateService(SERVICE_ID_1, ALL_NETWORKS_SERVICE,
+                DEFAULT_ADVERTISING_OPTION, TEST_CLIENT_UID_1) }
+
+        val socketCbCaptor = ArgumentCaptor.forClass(SocketCallback::class.java)
+        verify(socketProvider).requestSocket(eq(null), socketCbCaptor.capture())
+
+        val socketCb = socketCbCaptor.value
+        postSync { socketCb.onSocketCreated(TEST_SOCKETKEY_1, mockSocket1, listOf(TEST_LINKADDR)) }
+
+        verify(mockInterfaceAdvertiser1).addService(eq(SERVICE_ID_1),
+                argThat { it.matches(ALL_NETWORKS_SERVICE) })
+
+        val updateOptions = MdnsAdvertisingOptions.newBuilder().setIsOnlyUpdate(true).build()
+
+        // Update with serviceId that is not registered yet should fail
+        postSync { advertiser.addOrUpdateService(SERVICE_ID_2, ALL_NETWORKS_SERVICE_SUBTYPE,
+                updateOptions, TEST_CLIENT_UID_1) }
+        verify(cb).onRegisterServiceFailed(SERVICE_ID_2, NsdManager.FAILURE_INTERNAL_ERROR)
+
+        // Update service with different NsdServiceInfo should fail
+        postSync { advertiser.addOrUpdateService(SERVICE_ID_1, SERVICE_1_SUBTYPE, updateOptions,
+                TEST_CLIENT_UID_1) }
+        verify(cb).onRegisterServiceFailed(SERVICE_ID_1, NsdManager.FAILURE_INTERNAL_ERROR)
+
+        // Update service with same NsdServiceInfo but different subType should succeed
+        postSync { advertiser.addOrUpdateService(SERVICE_ID_1, ALL_NETWORKS_SERVICE_SUBTYPE,
+                updateOptions, TEST_CLIENT_UID_1) }
+        verify(mockInterfaceAdvertiser1).updateService(eq(SERVICE_ID_1), eq(setOf(TEST_SUBTYPE)))
+
+        // Newly created MdnsInterfaceAdvertiser will get addService() call.
+        postSync { socketCb.onSocketCreated(TEST_SOCKETKEY_2, mockSocket2, listOf(TEST_LINKADDR2)) }
+        verify(mockInterfaceAdvertiser2).addService(eq(SERVICE_ID_1),
+                argThat { it.matches(ALL_NETWORKS_SERVICE_SUBTYPE) })
+    }
+
+    @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.addService(SERVICE_ID_1, SERVICE_1, null /* subtype */) }
+        postSync { advertiser.addOrUpdateService(SERVICE_ID_1, SERVICE_1,
+                DEFAULT_ADVERTISING_OPTION, TEST_CLIENT_UID_1) }
         postSync { advertiser.removeService(SERVICE_ID_1) }
         verify(mockDeps, times(2)).generateHostname()
     }
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/MdnsDiscoveryManagerTests.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsDiscoveryManagerTests.java
index 331a5b6..5251e2a 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsDiscoveryManagerTests.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsDiscoveryManagerTests.java
@@ -56,6 +56,7 @@
 import java.util.concurrent.ScheduledExecutorService;
 
 /** Tests for {@link MdnsDiscoveryManager}. */
+@DevSdkIgnoreRunner.MonitorThreadLeak
 @RunWith(DevSdkIgnoreRunner.class)
 @DevSdkIgnoreRule.IgnoreUpTo(SC_V2)
 public class MdnsDiscoveryManagerTests {
@@ -134,9 +135,10 @@
     }
 
     @After
-    public void tearDown() {
+    public void tearDown() throws Exception {
         if (thread != null) {
             thread.quitSafely();
+            thread.join();
         }
     }
 
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 db41a6a..0637ad1 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,10 +47,12 @@
 import org.mockito.Mockito.any
 import org.mockito.Mockito.anyInt
 import org.mockito.Mockito.anyString
+import org.mockito.Mockito.argThat
 import org.mockito.Mockito.doAnswer
 import org.mockito.Mockito.doReturn
 import org.mockito.Mockito.eq
 import org.mockito.Mockito.mock
+import org.mockito.Mockito.never
 import org.mockito.Mockito.times
 import org.mockito.Mockito.verify
 
@@ -59,12 +64,20 @@
 private val TEST_HOSTNAME = arrayOf("Android_test", "local")
 
 private const val TEST_SERVICE_ID_1 = 42
+private const val TEST_SERVICE_ID_DUPLICATE = 43
 private val TEST_SERVICE_1 = NsdServiceInfo().apply {
     serviceType = "_testservice._tcp"
     serviceName = "MyTestService"
     port = 12345
 }
 
+private val TEST_SERVICE_1_SUBTYPE = NsdServiceInfo().apply {
+    subtypes = setOf("_sub")
+    serviceType = "_testservice._tcp"
+    serviceName = "MyTestService"
+    port = 12345
+}
+
 @RunWith(DevSdkIgnoreRunner::class)
 @IgnoreUpTo(Build.VERSION_CODES.S_V2)
 class MdnsInterfaceAdvertiserTest {
@@ -77,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>>
@@ -108,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())
 
@@ -117,7 +132,7 @@
             knownServices.add(inv.getArgument(0))
 
             -1
-        }.`when`(repository).addService(anyInt(), any(), any())
+        }.`when`(repository).addService(anyInt(), any())
         doAnswer { inv ->
             knownServices.remove(inv.getArgument(0))
             null
@@ -190,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:
@@ -204,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)
@@ -220,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(
@@ -248,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
@@ -272,14 +400,35 @@
         verify(prober).restartForConflict(mockProbingInfo)
     }
 
+    @Test
+    fun testReplaceExitingService() {
+        doReturn(TEST_SERVICE_ID_DUPLICATE).`when`(repository)
+                .addService(eq(TEST_SERVICE_ID_DUPLICATE), any())
+        advertiser.addService(TEST_SERVICE_ID_DUPLICATE, TEST_SERVICE_1_SUBTYPE)
+        verify(repository).addService(eq(TEST_SERVICE_ID_DUPLICATE), any())
+        verify(announcer).stop(TEST_SERVICE_ID_DUPLICATE)
+        verify(prober).startProbing(any())
+    }
+
+    @Test
+    fun testUpdateExistingService() {
+        doReturn(TEST_SERVICE_ID_DUPLICATE).`when`(repository)
+                .addService(eq(TEST_SERVICE_ID_DUPLICATE), any())
+        val subTypes = setOf("_sub")
+        advertiser.updateService(TEST_SERVICE_ID_DUPLICATE, subTypes)
+        verify(repository).updateService(eq(TEST_SERVICE_ID_DUPLICATE), any())
+        verify(announcer, never()).stop(TEST_SERVICE_ID_DUPLICATE)
+        verify(prober, never()).startProbing(any())
+    }
+
     private fun addServiceAndFinishProbing(serviceId: Int, serviceInfo: NsdServiceInfo):
             AnnouncementInfo {
         val testProbingInfo = mock(ProbingInfo::class.java)
         doReturn(serviceId).`when`(testProbingInfo).serviceId
         doReturn(testProbingInfo).`when`(repository).setServiceProbing(serviceId)
 
-        advertiser.addService(serviceId, serviceInfo, null /* subtype */)
-        verify(repository).addService(serviceId, serviceInfo, null /* subtype */)
+        advertiser.addService(serviceId, serviceInfo)
+        verify(repository).addService(serviceId, serviceInfo)
         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 f26f7e1..fd8d98b 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,21 @@
         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))
         assertEquals(1, repository.servicesCount)
 
         val probingInfo = repository.setServiceProbing(TEST_SERVICE_ID_1)
@@ -119,7 +168,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 +177,65 @@
 
     @Test
     fun testAddAndConflicts() {
-        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.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)
         }
         assertFailsWith(NameConflictException::class) {
-            repository.addService(TEST_SERVICE_ID_3, TEST_SERVICE_3, null /* subtype */)
+            repository.addService(TEST_SERVICE_ID_3, TEST_SERVICE_3)
         }
     }
 
     @Test
-    fun testInvalidReuseOfServiceId() {
-        val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
-        repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* subtype */)
+    fun testAddAndUpdates() {
+        val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+        repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
+
         assertFailsWith(IllegalArgumentException::class) {
-            repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_2, null /* subtype */)
+            repository.updateService(TEST_SERVICE_ID_2, emptySet() /* subtype */)
+        }
+
+        repository.updateService(TEST_SERVICE_ID_1, setOf(TEST_SUBTYPE))
+
+        val queriedName = arrayOf(TEST_SUBTYPE, "_sub", "_testservice", "_tcp", "local")
+        val questions = listOf(MdnsPointerRecord(queriedName, false /* isUnicast */))
+        val query = MdnsPacket(0 /* flags */, questions, emptyList() /* answers */,
+                emptyList() /* authorityRecords */, emptyList() /* additionalRecords */)
+        val src = InetSocketAddress(parseNumericAddress("192.0.2.123"), 5353)
+        val reply = repository.getReply(query, src)
+
+        assertNotNull(reply)
+
+        // TTLs as per RFC6762 10.
+        val longTtl = 4_500_000L
+        val serviceName = arrayOf("MyTestService", "_testservice", "_tcp", "local")
+
+        assertEquals(listOf(
+                MdnsPointerRecord(
+                        queriedName,
+                        0L /* receiptTimeMillis */,
+                        false /* cacheFlush */,
+                        longTtl,
+                        serviceName),
+        ), reply.answers)
+    }
+
+    @Test
+    fun testInvalidReuseOfServiceId() {
+        val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+        repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
+        assertFailsWith(IllegalArgumentException::class) {
+            repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_2)
         }
     }
 
     @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)
         assertTrue(repository.hasActiveService(TEST_SERVICE_ID_1))
 
         val probingInfo = repository.setServiceProbing(TEST_SERVICE_ID_1)
@@ -166,7 +249,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 */)
 
@@ -195,9 +278,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)
@@ -211,7 +295,7 @@
         assertEquals(0, packet.authorityRecords.size)
         assertEquals(0, packet.additionalRecords.size)
 
-        assertContentEquals(listOf(
+        assertThat(packet.answers).containsExactly(
                 MdnsPointerRecord(
                         arrayOf("_testservice", "_tcp", "local"),
                         0L /* receiptTimeMillis */,
@@ -224,7 +308,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)
@@ -232,13 +321,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))
         assertEquals(1, repository.servicesCount)
 
         repository.removeService(TEST_SERVICE_ID_2)
@@ -247,9 +336,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)
 
@@ -260,12 +349,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 */,
@@ -312,6 +402,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 */,
@@ -333,8 +430,7 @@
                         0L /* receiptTimeMillis */,
                         false /* cacheFlush */,
                         4500000L /* ttlMillis */,
-                        serviceType)
-        ), packet.answers)
+                        serviceType))
 
         assertContentEquals(listOf(
                 MdnsNsecRecord(v4AddrRev,
@@ -342,37 +438,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")
@@ -434,18 +530,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"),
-                0L /* receiptTimeMillis */,
-                false /* cacheFlush */,
-                // TTL and data is empty for a question
-                0L /* ttlMillis */,
-                null /* pointer */))
+        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)
@@ -453,108 +544,453 @@
         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,
-                0L /* receiptTimeMillis */,
-                false /* cacheFlush */,
-                // TTL and data is empty for a question
-                0L /* ttlMillis */,
-                null /* pointer */))
-        val query = MdnsPacket(0 /* flags */, questions, listOf() /* answers */,
-                listOf() /* authorityRecords */, listOf() /* additionalRecords */)
+    fun testGetReply_singlePtrQuestion_returnsSrvTxtAddressNsecRecords() {
+        val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+        repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1, setOf(TEST_SUBTYPE))
         val src = InetSocketAddress(parseNumericAddress("192.0.2.123"), 5353)
+        val serviceName = arrayOf("MyTestService", "_testservice", "_tcp", "local")
+
+        val query = makeQuery(TYPE_PTR to arrayOf("_testservice", "_tcp", "local"))
         val reply = repository.getReply(query, src)
 
         assertNotNull(reply)
-        // Source address is IPv4
-        assertEquals(MdnsConstants.getMdnsIPv4Address(), reply.destination.address)
-        assertEquals(MdnsConstants.MDNS_PORT, reply.destination.port)
-
-        // TTLs as per RFC6762 10.
-        val longTtl = 4_500_000L
-        val shortTtl = 120_000L
-        val serviceName = arrayOf("MyTestService", "_testservice", "_tcp", "local")
-
         assertEquals(listOf(
                 MdnsPointerRecord(
-                        queriedName,
-                        0L /* receiptTimeMillis */,
-                        false /* cacheFlush */,
-                        longTtl,
-                        serviceName),
-        ), reply.answers)
-
+                    arrayOf("_testservice", "_tcp", "local"), 0L, false, LONG_TTL, serviceName)),
+            reply.answers)
         assertEquals(listOf(
-            MdnsTextRecord(
-                    serviceName,
-                    0L /* receiptTimeMillis */,
-                    true /* cacheFlush */,
-                    longTtl,
-                    listOf() /* entries */),
-            MdnsServiceRecord(
-                    serviceName,
-                    0L /* receiptTimeMillis */,
-                    true /* cacheFlush */,
-                    shortTtl,
-                    0 /* servicePriority */,
-                    0 /* serviceWeight */,
-                    TEST_PORT,
-                    TEST_HOSTNAME),
-            MdnsInetAddressRecord(
-                    TEST_HOSTNAME,
-                    0L /* receiptTimeMillis */,
-                    true /* cacheFlush */,
-                    shortTtl,
-                    TEST_ADDRESSES[0].address),
-            MdnsInetAddressRecord(
-                    TEST_HOSTNAME,
-                    0L /* receiptTimeMillis */,
-                    true /* cacheFlush */,
-                    shortTtl,
-                    TEST_ADDRESSES[1].address),
-            MdnsInetAddressRecord(
-                    TEST_HOSTNAME,
-                    0L /* receiptTimeMillis */,
-                    true /* cacheFlush */,
-                    shortTtl,
-                    TEST_ADDRESSES[2].address),
-            MdnsNsecRecord(
-                    serviceName,
-                    0L /* receiptTimeMillis */,
-                    true /* cacheFlush */,
-                    longTtl,
-                    serviceName /* nextDomain */,
-                    intArrayOf(MdnsRecord.TYPE_TXT, MdnsRecord.TYPE_SRV)),
-            MdnsNsecRecord(
-                    TEST_HOSTNAME,
-                    0L /* receiptTimeMillis */,
-                    true /* cacheFlush */,
-                    shortTtl,
-                    TEST_HOSTNAME /* nextDomain */,
-                    intArrayOf(MdnsRecord.TYPE_A, MdnsRecord.TYPE_AAAA)),
+                MdnsTextRecord(serviceName, 0L, true, LONG_TTL, emptyList()),
+                MdnsServiceRecord(serviceName, 0L, true, SHORT_TTL, 0, 0, TEST_PORT, TEST_HOSTNAME),
+                MdnsInetAddressRecord(
+                    TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_ADDRESSES[0].address),
+                MdnsInetAddressRecord(
+                    TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_ADDRESSES[1].address),
+                MdnsInetAddressRecord(
+                    TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_ADDRESSES[2].address),
+                MdnsNsecRecord(serviceName, 0L, true, LONG_TTL, serviceName /* nextDomain */,
+                        intArrayOf(TYPE_TXT, TYPE_SRV)),
+                MdnsNsecRecord(TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_HOSTNAME /* nextDomain */,
+                        intArrayOf(TYPE_A, TYPE_AAAA)),
+            ), reply.additionalAnswers)
+    }
+
+
+    @Test
+    fun testGetReply_ptrQuestionForServiceWithCustomHost_customHostUsedInAdditionalAnswers() {
+        val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+        repository.initWithService(TEST_SERVICE_CUSTOM_HOST_ID_1, TEST_SERVICE_CUSTOM_HOST_1,
+                setOf(TEST_SUBTYPE, TEST_SUBTYPE2))
+        val src = InetSocketAddress(parseNumericAddress("fe80::1234"), 5353)
+        val serviceName = arrayOf("TestService", "_testservice", "_tcp", "local")
+
+        val query = makeQuery(TYPE_PTR to arrayOf("_testservice", "_tcp", "local"))
+        val reply = repository.getReply(query, src)
+
+        assertNotNull(reply)
+        assertEquals(listOf(
+                MdnsPointerRecord(
+                        arrayOf("_testservice", "_tcp", "local"),
+                        0L, false, LONG_TTL, serviceName)),
+                reply.answers)
+        assertEquals(listOf(
+                MdnsTextRecord(serviceName, 0L, true, LONG_TTL, listOf()),
+                MdnsServiceRecord(serviceName, 0L, true, SHORT_TTL,
+                        0, 0, TEST_PORT, TEST_CUSTOM_HOST_1_NAME),
+                MdnsInetAddressRecord(
+                        TEST_CUSTOM_HOST_1_NAME, 0L, true, SHORT_TTL,
+                        parseNumericAddress("2001:db8::1")),
+                MdnsNsecRecord(serviceName, 0L, true, LONG_TTL, serviceName /* nextDomain */,
+                        intArrayOf(TYPE_TXT, TYPE_SRV)),
+                MdnsNsecRecord(TEST_CUSTOM_HOST_1_NAME, 0L, true, SHORT_TTL,
+                        TEST_CUSTOM_HOST_1_NAME /* nextDomain */,
+                        intArrayOf(TYPE_AAAA)),
         ), reply.additionalAnswers)
     }
 
     @Test
+    fun testGetReply_ptrQuestionForServicesWithSameCustomHost_customHostUsedInAdditionalAnswers() {
+        val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+        val serviceWithCustomHost1 = NsdServiceInfo().apply {
+            hostname = "TestHost"
+            hostAddresses = listOf(
+                    parseNumericAddress("2001:db8::1"),
+                    parseNumericAddress("192.0.2.1"))
+            serviceType = "_testservice._tcp"
+            serviceName = "TestService1"
+            port = TEST_PORT
+        }
+        val serviceWithCustomHost2 = NsdServiceInfo().apply {
+            hostname = "TestHost"
+            hostAddresses = listOf(
+                    parseNumericAddress("2001:db8::1"),
+                    parseNumericAddress("2001:db8::3"))
+        }
+        repository.addServiceAndFinishProbing(TEST_SERVICE_ID_1, serviceWithCustomHost1)
+        repository.addServiceAndFinishProbing(TEST_SERVICE_ID_2, serviceWithCustomHost2)
+        val src = InetSocketAddress(parseNumericAddress("fe80::1234"), 5353)
+        val serviceName = arrayOf("TestService1", "_testservice", "_tcp", "local")
+
+        val query = makeQuery(TYPE_PTR to arrayOf("_testservice", "_tcp", "local"))
+        val reply = repository.getReply(query, src)
+
+        assertNotNull(reply)
+        assertEquals(listOf(
+                MdnsPointerRecord(
+                        arrayOf("_testservice", "_tcp", "local"),
+                        0L, false, LONG_TTL, serviceName)),
+                reply.answers)
+        assertEquals(listOf(
+                MdnsTextRecord(serviceName, 0L, true, LONG_TTL, listOf()),
+                MdnsServiceRecord(serviceName, 0L, true, SHORT_TTL,
+                        0, 0, TEST_PORT, TEST_CUSTOM_HOST_1_NAME),
+                MdnsInetAddressRecord(
+                        TEST_CUSTOM_HOST_1_NAME, 0L, true, SHORT_TTL,
+                        parseNumericAddress("2001:db8::1")),
+                MdnsInetAddressRecord(
+                        TEST_CUSTOM_HOST_1_NAME, 0L, true, SHORT_TTL,
+                        parseNumericAddress("192.0.2.1")),
+                MdnsInetAddressRecord(
+                        TEST_CUSTOM_HOST_1_NAME, 0L, true, SHORT_TTL,
+                        parseNumericAddress("2001:db8::3")),
+                MdnsNsecRecord(serviceName, 0L, true, LONG_TTL, serviceName /* nextDomain */,
+                        intArrayOf(TYPE_TXT, TYPE_SRV)),
+                MdnsNsecRecord(TEST_CUSTOM_HOST_1_NAME, 0L, true, SHORT_TTL,
+                        TEST_CUSTOM_HOST_1_NAME /* nextDomain */,
+                        intArrayOf(TYPE_A, TYPE_AAAA)),
+        ), reply.additionalAnswers)
+    }
+
+    @Test
+    fun testGetReply_singleSubtypePtrQuestion_returnsSrvTxtAddressNsecRecords() {
+        val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+        repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1, setOf(TEST_SUBTYPE))
+        val src = InetSocketAddress(parseNumericAddress("192.0.2.123"), 5353)
+        val serviceName = arrayOf("MyTestService", "_testservice", "_tcp", "local")
+
+        val query = makeQuery(
+                TYPE_PTR to arrayOf(TEST_SUBTYPE, "_sub", "_testservice", "_tcp", "local"))
+        val reply = repository.getReply(query, src)
+
+        assertNotNull(reply)
+        assertEquals(listOf(
+                MdnsPointerRecord(
+                    arrayOf(TEST_SUBTYPE, "_sub", "_testservice", "_tcp", "local"), 0L, false,
+                    LONG_TTL, serviceName)),
+            reply.answers)
+        assertEquals(listOf(
+                MdnsTextRecord(serviceName, 0L, true, LONG_TTL, emptyList()),
+                MdnsServiceRecord(serviceName, 0L, true, SHORT_TTL, 0, 0, TEST_PORT, TEST_HOSTNAME),
+                MdnsInetAddressRecord(
+                    TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_ADDRESSES[0].address),
+                MdnsInetAddressRecord(
+                    TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_ADDRESSES[1].address),
+                MdnsInetAddressRecord(
+                    TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_ADDRESSES[2].address),
+                MdnsNsecRecord(serviceName, 0L, true, LONG_TTL, serviceName /* nextDomain */,
+                        intArrayOf(TYPE_TXT, TYPE_SRV)),
+                MdnsNsecRecord(TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_HOSTNAME /* nextDomain */,
+                        intArrayOf(TYPE_A, TYPE_AAAA)),
+            ), reply.additionalAnswers)
+    }
+
+    @Test
+    fun testGetReply_duplicatePtrQuestions_doesNotReturnDuplicateRecords() {
+        val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+        repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1, setOf(TEST_SUBTYPE))
+        val src = InetSocketAddress(parseNumericAddress("192.0.2.123"), 5353)
+        val serviceName = arrayOf("MyTestService", "_testservice", "_tcp", "local")
+
+        val query = makeQuery(
+                TYPE_PTR to arrayOf("_testservice", "_tcp", "local"),
+                TYPE_PTR to arrayOf("_testservice", "_tcp", "local"))
+        val reply = repository.getReply(query, src)
+
+        assertNotNull(reply)
+        assertEquals(listOf(
+                MdnsPointerRecord(
+                    arrayOf("_testservice", "_tcp", "local"), 0L, false, LONG_TTL, serviceName)),
+            reply.answers)
+        assertEquals(listOf(
+                MdnsTextRecord(serviceName, 0L, true, LONG_TTL, emptyList()),
+                MdnsServiceRecord(serviceName, 0L, true, SHORT_TTL, 0, 0, TEST_PORT, TEST_HOSTNAME),
+                MdnsInetAddressRecord(
+                    TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_ADDRESSES[0].address),
+                MdnsInetAddressRecord(
+                    TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_ADDRESSES[1].address),
+                MdnsInetAddressRecord(
+                    TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_ADDRESSES[2].address),
+                MdnsNsecRecord(serviceName, 0L, true, LONG_TTL, serviceName /* nextDomain */,
+                        intArrayOf(TYPE_TXT, TYPE_SRV)),
+                MdnsNsecRecord(TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_HOSTNAME /* nextDomain */,
+                        intArrayOf(TYPE_A, TYPE_AAAA)),
+            ), reply.additionalAnswers)
+    }
+
+    @Test
+    fun testGetReply_multiplePtrQuestionsWithSubtype_doesNotReturnDuplicateRecords() {
+        val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+        repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1, setOf(TEST_SUBTYPE))
+        val src = InetSocketAddress(parseNumericAddress("192.0.2.123"), 5353)
+        val serviceName = arrayOf("MyTestService", "_testservice", "_tcp", "local")
+
+        val query = makeQuery(
+                TYPE_PTR to arrayOf("_testservice", "_tcp", "local"),
+                TYPE_PTR to arrayOf(TEST_SUBTYPE, "_sub", "_testservice", "_tcp", "local"))
+        val reply = repository.getReply(query, src)
+
+        assertNotNull(reply)
+        assertEquals(listOf(
+                MdnsPointerRecord(
+                    arrayOf("_testservice", "_tcp", "local"), 0L, false, LONG_TTL, serviceName),
+                MdnsPointerRecord(
+                    arrayOf(TEST_SUBTYPE, "_sub", "_testservice", "_tcp", "local"),
+                    0L, false, LONG_TTL, serviceName)),
+            reply.answers)
+        assertEquals(listOf(
+                MdnsTextRecord(serviceName, 0L, true, LONG_TTL, emptyList()),
+                MdnsServiceRecord(serviceName, 0L, true, SHORT_TTL, 0, 0, TEST_PORT, TEST_HOSTNAME),
+                MdnsInetAddressRecord(
+                    TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_ADDRESSES[0].address),
+                MdnsInetAddressRecord(
+                    TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_ADDRESSES[1].address),
+                MdnsInetAddressRecord(
+                    TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_ADDRESSES[2].address),
+                MdnsNsecRecord(serviceName, 0L, true, LONG_TTL, serviceName /* nextDomain */,
+                        intArrayOf(TYPE_TXT, TYPE_SRV)),
+                MdnsNsecRecord(TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_HOSTNAME /* nextDomain */,
+                        intArrayOf(TYPE_A, TYPE_AAAA)),
+            ), reply.additionalAnswers)
+    }
+
+    @Test
+    fun testGetReply_txtQuestion_returnsNoNsecRecord() {
+        val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+        repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1, setOf(TEST_SUBTYPE))
+        val src = InetSocketAddress(parseNumericAddress("192.0.2.123"), 5353)
+        val serviceName = arrayOf("MyTestService", "_testservice", "_tcp", "local")
+
+        val query = makeQuery(TYPE_TXT to serviceName)
+        val reply = repository.getReply(query, src)
+
+        assertNotNull(reply)
+        assertEquals(listOf(MdnsTextRecord(serviceName, 0L, true, LONG_TTL, emptyList())),
+                reply.answers)
+        // No NSEC records because the reply doesn't include the SRV record
+        assertTrue(reply.additionalAnswers.isEmpty())
+    }
+
+    @Test
+    fun testGetReply_AAAAQuestionButNoIpv6Address_returnsNsecRecord() {
+        val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+        repository.initWithService(
+                TEST_SERVICE_ID_1, TEST_SERVICE_1, setOf(TEST_SUBTYPE),
+                listOf(LinkAddress(parseNumericAddress("192.0.2.111"), 24)))
+        val src = InetSocketAddress(parseNumericAddress("192.0.2.123"), 5353)
+
+        val query = makeQuery(TYPE_AAAA to TEST_HOSTNAME)
+        val reply = repository.getReply(query, src)
+
+        assertNotNull(reply)
+        assertTrue(reply.answers.isEmpty())
+        assertEquals(listOf(
+                MdnsNsecRecord(TEST_HOSTNAME, 0L, true, LONG_TTL, TEST_HOSTNAME /* nextDomain */,
+                        intArrayOf(TYPE_AAAA))),
+            reply.additionalAnswers)
+    }
+
+    @Test
+    fun testGetReply_AAAAQuestionForCustomHost_returnsAAAARecords() {
+        val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+        repository.initWithService(
+                TEST_CUSTOM_HOST_ID_1, TEST_CUSTOM_HOST_1, subtypes = setOf(),
+                listOf(LinkAddress(parseNumericAddress("192.0.2.111"), 24)))
+        repository.addService(TEST_CUSTOM_HOST_ID_2, TEST_CUSTOM_HOST_2)
+        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)
+        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)
+        repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_2)
 
         val packet = MdnsPacket(
                 0 /* flags */,
@@ -574,15 +1010,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)
+        repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_2)
 
         val packet = MdnsPacket(
             0 /* flags */,
@@ -602,15 +1041,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)
+        repository.addService(TEST_CUSTOM_HOST_ID_2, TEST_CUSTOM_HOST_2)
+
+        val packet = MdnsPacket(
+                0, /* flags */
+                emptyList(), /* questions */
+                listOf(
+                        MdnsInetAddressRecord(arrayOf("TestHost", "local"),
+                                0L /* receiptTimeMillis */, true /* cacheFlush */,
+                                0L /* ttlMillis */, parseNumericAddress("2001:db8::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)
+        repository.addService(TEST_CUSTOM_HOST_ID_2, TEST_CUSTOM_HOST_2)
+
+        val packet = MdnsPacket(
+                0, /* flags */
+                emptyList(), /* questions */
+                listOf(
+                        MdnsInetAddressRecord(arrayOf("TestHost", "local"),
+                                0L /* receiptTimeMillis */, true /* cacheFlush */,
+                                0L /* ttlMillis */, parseNumericAddress("2001:db8::1")),
+                        MdnsInetAddressRecord(arrayOf("TestHost", "local"),
+                                0L /* receiptTimeMillis */, true /* cacheFlush */,
+                                0L /* ttlMillis */, parseNumericAddress("2001:db8::2")),
+                        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)
+        repository.addService(TEST_CUSTOM_HOST_ID_2, TEST_CUSTOM_HOST_2)
+
+        val packet = MdnsPacket(
+                0, /* flags */
+                emptyList(), /* questions */
+                listOf(
+                        MdnsInetAddressRecord(arrayOf("TestHost", "local"),
+                                0L /* receiptTimeMillis */, true /* cacheFlush */,
+                                0L /* ttlMillis */, parseNumericAddress("2001:db8::2")),
+                ) /* answers */,
+                emptyList() /* authorityRecords */,
+                emptyList() /* additionalRecords */)
+
+        assertEquals(emptyMap(),
+                repository.getConflictingServices(packet))
+    }
+
+    @Test
+    fun testGetConflictingServices_customHostsReplyHasIdenticalHosts_noConflict() {
+        val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+        repository.addService(TEST_CUSTOM_HOST_ID_1, TEST_CUSTOM_HOST_1)
+        repository.addService(TEST_CUSTOM_HOST_ID_2, TEST_CUSTOM_HOST_2)
+
+        val packet = MdnsPacket(
+                0, /* flags */
+                emptyList(), /* questions */
+                listOf(
+                        MdnsInetAddressRecord(arrayOf("TestHost", "local"),
+                                0L /* receiptTimeMillis */, true /* cacheFlush */,
+                                0L /* ttlMillis */, parseNumericAddress("2001:db8::1")),
+                        MdnsInetAddressRecord(arrayOf("TestHost", "local"),
+                                0L /* receiptTimeMillis */, true /* cacheFlush */,
+                                0L /* ttlMillis */, parseNumericAddress("2001:db8::2")),
+                ) /* answers */,
+                emptyList() /* authorityRecords */,
+                emptyList() /* additionalRecords */)
+
+        assertEquals(emptyMap(),
+                repository.getConflictingServices(packet))
+    }
+
+
+    @Test
+    fun testGetConflictingServices_customHostsCaseInsensitiveReplyHasIdenticalHosts_noConflict() {
+        val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+        repository.addService(TEST_CUSTOM_HOST_ID_1, TEST_CUSTOM_HOST_1)
+        repository.addService(TEST_CUSTOM_HOST_ID_2, TEST_CUSTOM_HOST_2)
+
+        val packet = MdnsPacket(
+                0, /* flags */
+                emptyList(), /* questions */
+                listOf(
+                        MdnsInetAddressRecord(arrayOf("TESTHOST", "local"),
+                                0L /* receiptTimeMillis */, true /* cacheFlush */,
+                                0L /* ttlMillis */, parseNumericAddress("2001:db8::1")),
+                        MdnsInetAddressRecord(arrayOf("testhost", "local"),
+                                0L /* receiptTimeMillis */, true /* cacheFlush */,
+                                0L /* ttlMillis */, parseNumericAddress("2001:db8::2")),
+                ) /* answers */,
+                emptyList() /* authorityRecords */,
+                emptyList() /* additionalRecords */)
+
+        assertEquals(emptyMap(),
+                repository.getConflictingServices(packet))
     }
 
     @Test
     fun testGetConflictingServices_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)
+        repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_2)
 
         val otherTtlMillis = 1234L
         val packet = MdnsPacket(
@@ -632,14 +1194,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)
+        repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_2)
 
         val otherTtlMillis = 1234L
         val packet = MdnsPacket(
@@ -661,25 +1223,21 @@
                 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,
                 repository.getServiceRepliedRequestsCount(TEST_SERVICE_ID_1))
 
-        val questions = listOf(MdnsPointerRecord(arrayOf("_testservice", "_tcp", "local"),
-                0L /* receiptTimeMillis */,
-                false /* cacheFlush */,
-                // TTL and data is empty for a question
-                0L /* ttlMillis */,
-                null /* pointer */))
-        val query = MdnsPacket(0 /* flags */, questions, listOf() /* answers */,
-                listOf() /* authorityRecords */, listOf() /* additionalRecords */)
+        val questions = listOf(
+                MdnsPointerRecord(arrayOf("_testservice", "_tcp", "local"), false /* isUnicast */))
+        val query = MdnsPacket(0 /* flags */, questions, emptyList() /* answers */,
+                emptyList() /* authorityRecords */, emptyList() /* additionalRecords */)
         val src = InetSocketAddress(parseNumericAddress("192.0.2.123"), 5353)
 
         // Reply to the question and verify there is one packet replied.
@@ -695,11 +1253,10 @@
     @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))
         assertEquals(1, repository.servicesCount)
 
         val probingInfo = repository.setServiceProbing(TEST_SERVICE_ID_1)
@@ -726,7 +1283,7 @@
                 expectedName,
                 0L /* receiptTimeMillis */,
                 false /* cacheFlush */,
-                120_000L /* ttlMillis */,
+                SHORT_TTL /* ttlMillis */,
                 0 /* servicePriority */,
                 0 /* serviceWeight */,
                 TEST_PORT,
@@ -735,33 +1292,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)
     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/MdnsServiceCacheTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceCacheTest.kt
index 2b3b834..b040ab6 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceCacheTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceCacheTest.kt
@@ -19,7 +19,10 @@
 import android.os.Build
 import android.os.Handler
 import android.os.HandlerThread
+import com.android.net.module.util.ArrayTrackRecord
 import com.android.server.connectivity.mdns.MdnsServiceCache.CacheKey
+import com.android.server.connectivity.mdns.MdnsServiceCacheTest.ExpiredRecord.ExpiredEvent.ServiceRecordExpired
+import com.android.server.connectivity.mdns.util.MdnsUtils
 import com.android.testutils.DevSdkIgnoreRule
 import com.android.testutils.DevSdkIgnoreRunner
 import java.util.concurrent.CompletableFuture
@@ -32,14 +35,21 @@
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.mock
 
 private const val SERVICE_NAME_1 = "service-instance-1"
 private const val SERVICE_NAME_2 = "service-instance-2"
+private const val SERVICE_NAME_3 = "service-instance-3"
 private const val SERVICE_TYPE_1 = "_test1._tcp.local"
 private const val SERVICE_TYPE_2 = "_test2._tcp.local"
 private const val INTERFACE_INDEX = 999
 private const val DEFAULT_TIMEOUT_MS = 2000L
+private const val NO_CALLBACK_TIMEOUT_MS = 200L
+private const val TEST_ELAPSED_REALTIME_MS = 123L
+private const val DEFAULT_TTL_TIME_MS = 120000L
 
+@DevSdkIgnoreRunner.MonitorThreadLeak
 @RunWith(DevSdkIgnoreRunner::class)
 @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
 class MdnsServiceCacheTest {
@@ -47,10 +57,46 @@
     private val cacheKey1 = CacheKey(SERVICE_TYPE_1, socketKey)
     private val cacheKey2 = CacheKey(SERVICE_TYPE_2, socketKey)
     private val thread = HandlerThread(MdnsServiceCacheTest::class.simpleName)
+    private val clock = mock(MdnsUtils.Clock::class.java)
     private val handler by lazy {
         Handler(thread.looper)
     }
 
+    private class ExpiredRecord : MdnsServiceCache.ServiceExpiredCallback {
+        val history = ArrayTrackRecord<ExpiredEvent>().newReadHead()
+
+        sealed class ExpiredEvent {
+            abstract val previousResponse: MdnsResponse
+            abstract val newResponse: MdnsResponse?
+            data class ServiceRecordExpired(
+                    override val previousResponse: MdnsResponse,
+                    override val newResponse: MdnsResponse?
+            ) : ExpiredEvent()
+        }
+
+        override fun onServiceRecordExpired(
+                previousResponse: MdnsResponse,
+                newResponse: MdnsResponse?
+        ) {
+            history.add(ServiceRecordExpired(previousResponse, newResponse))
+        }
+
+        fun expectedServiceRecordExpired(
+                serviceName: String,
+                timeoutMs: Long = DEFAULT_TIMEOUT_MS
+        ) {
+            val event = history.poll(timeoutMs)
+            assertNotNull(event)
+            assertTrue(event is ServiceRecordExpired)
+            assertEquals(serviceName, event.previousResponse.serviceInstanceName)
+        }
+
+        fun assertNoCallback() {
+            val cb = history.poll(NO_CALLBACK_TIMEOUT_MS)
+            assertNull("Expected no callback but got $cb", cb)
+        }
+    }
+
     @Before
     fun setUp() {
         thread.start()
@@ -59,6 +105,7 @@
     @After
     fun tearDown() {
         thread.quitSafely()
+        thread.join()
     }
 
     private fun makeFlags(isExpiredServicesRemovalEnabled: Boolean = false) =
@@ -89,19 +136,27 @@
     private fun getService(
             serviceCache: MdnsServiceCache,
             serviceName: String,
-            cacheKey: CacheKey,
+            cacheKey: CacheKey
     ): MdnsResponse? = runningOnHandlerAndReturn {
         serviceCache.getCachedService(serviceName, cacheKey)
     }
 
     private fun getServices(
             serviceCache: MdnsServiceCache,
-            cacheKey: CacheKey,
+            cacheKey: CacheKey
     ): List<MdnsResponse> = runningOnHandlerAndReturn { serviceCache.getCachedServices(cacheKey) }
 
+    private fun registerServiceExpiredCallback(
+            serviceCache: MdnsServiceCache,
+            cacheKey: CacheKey,
+            callback: MdnsServiceCache.ServiceExpiredCallback
+    ) = runningOnHandlerAndReturn {
+        serviceCache.registerServiceExpiredCallback(cacheKey, callback)
+    }
+
     @Test
     fun testAddAndRemoveService() {
-        val serviceCache = MdnsServiceCache(thread.looper, makeFlags())
+        val serviceCache = MdnsServiceCache(thread.looper, makeFlags(), clock)
         addOrUpdateService(serviceCache, cacheKey1, createResponse(SERVICE_NAME_1, SERVICE_TYPE_1))
         var response = getService(serviceCache, SERVICE_NAME_1, cacheKey1)
         assertNotNull(response)
@@ -113,7 +168,7 @@
 
     @Test
     fun testGetCachedServices_multipleServiceTypes() {
-        val serviceCache = MdnsServiceCache(thread.looper, makeFlags())
+        val serviceCache = MdnsServiceCache(thread.looper, makeFlags(), clock)
         addOrUpdateService(serviceCache, cacheKey1, createResponse(SERVICE_NAME_1, SERVICE_TYPE_1))
         addOrUpdateService(serviceCache, cacheKey1, createResponse(SERVICE_NAME_2, SERVICE_TYPE_1))
         addOrUpdateService(serviceCache, cacheKey2, createResponse(SERVICE_NAME_2, SERVICE_TYPE_2))
@@ -145,7 +200,127 @@
         })
     }
 
-    private fun createResponse(serviceInstanceName: String, serviceType: String) = MdnsResponse(
-            0 /* now */, "$serviceInstanceName.$serviceType".split(".").toTypedArray(),
-            socketKey.interfaceIndex, socketKey.network)
+    @Test
+    fun testServiceExpiredAndSendCallbacks() {
+        val serviceCache = MdnsServiceCache(
+                thread.looper, makeFlags(isExpiredServicesRemovalEnabled = true), clock)
+        // Register service expired callbacks
+        val callback1 = ExpiredRecord()
+        val callback2 = ExpiredRecord()
+        registerServiceExpiredCallback(serviceCache, cacheKey1, callback1)
+        registerServiceExpiredCallback(serviceCache, cacheKey2, callback2)
+
+        doReturn(TEST_ELAPSED_REALTIME_MS).`when`(clock).elapsedRealtime()
+
+        // Add multiple services with different ttl time.
+        addOrUpdateService(serviceCache, cacheKey1, createResponse(SERVICE_NAME_1, SERVICE_TYPE_1,
+                DEFAULT_TTL_TIME_MS))
+        addOrUpdateService(serviceCache, cacheKey1, createResponse(SERVICE_NAME_2, SERVICE_TYPE_1,
+                DEFAULT_TTL_TIME_MS + 20L))
+        addOrUpdateService(serviceCache, cacheKey2, createResponse(SERVICE_NAME_3, SERVICE_TYPE_2,
+                DEFAULT_TTL_TIME_MS + 10L))
+
+        // Check the service expiration immediately. Should be no callback.
+        assertEquals(2, getServices(serviceCache, cacheKey1).size)
+        assertEquals(1, getServices(serviceCache, cacheKey2).size)
+        callback1.assertNoCallback()
+        callback2.assertNoCallback()
+
+        // Simulate the case where the response is after TTL then check expired services.
+        // Expect SERVICE_NAME_1 expired.
+        doReturn(TEST_ELAPSED_REALTIME_MS + DEFAULT_TTL_TIME_MS).`when`(clock).elapsedRealtime()
+        assertEquals(1, getServices(serviceCache, cacheKey1).size)
+        assertEquals(1, getServices(serviceCache, cacheKey2).size)
+        callback1.expectedServiceRecordExpired(SERVICE_NAME_1)
+        callback2.assertNoCallback()
+
+        // Simulate the case where the response is after TTL then check expired services.
+        // Expect SERVICE_NAME_3 expired.
+        doReturn(TEST_ELAPSED_REALTIME_MS + DEFAULT_TTL_TIME_MS + 11L)
+                .`when`(clock).elapsedRealtime()
+        assertEquals(1, getServices(serviceCache, cacheKey1).size)
+        assertEquals(0, getServices(serviceCache, cacheKey2).size)
+        callback1.assertNoCallback()
+        callback2.expectedServiceRecordExpired(SERVICE_NAME_3)
+    }
+
+    @Test
+    fun testRemoveExpiredServiceWhenGetting() {
+        val serviceCache = MdnsServiceCache(
+                thread.looper, makeFlags(isExpiredServicesRemovalEnabled = true), clock)
+
+        doReturn(TEST_ELAPSED_REALTIME_MS).`when`(clock).elapsedRealtime()
+        addOrUpdateService(serviceCache, cacheKey1,
+                createResponse(SERVICE_NAME_1, SERVICE_TYPE_1, 1L /* ttlTime */))
+        doReturn(TEST_ELAPSED_REALTIME_MS + 2L).`when`(clock).elapsedRealtime()
+        assertNull(getService(serviceCache, SERVICE_NAME_1, cacheKey1))
+
+        addOrUpdateService(serviceCache, cacheKey2,
+                createResponse(SERVICE_NAME_2, SERVICE_TYPE_2, 3L /* ttlTime */))
+        doReturn(TEST_ELAPSED_REALTIME_MS + 4L).`when`(clock).elapsedRealtime()
+        assertEquals(0, getServices(serviceCache, cacheKey2).size)
+    }
+
+    @Test
+    fun testInsertResponseAndSortList() {
+        val responses = ArrayList<MdnsResponse>()
+        val response1 = createResponse(SERVICE_NAME_1, SERVICE_TYPE_1, 100L /* ttlTime */)
+        MdnsServiceCache.insertResponseAndSortList(responses, response1, TEST_ELAPSED_REALTIME_MS)
+        assertEquals(1, responses.size)
+        assertEquals(response1, responses[0])
+
+        val response2 = createResponse(SERVICE_NAME_2, SERVICE_TYPE_1, 50L /* ttlTime */)
+        MdnsServiceCache.insertResponseAndSortList(responses, response2, TEST_ELAPSED_REALTIME_MS)
+        assertEquals(2, responses.size)
+        assertEquals(response2, responses[0])
+        assertEquals(response1, responses[1])
+
+        val response3 = createResponse(SERVICE_NAME_3, SERVICE_TYPE_1, 75L /* ttlTime */)
+        MdnsServiceCache.insertResponseAndSortList(responses, response3, TEST_ELAPSED_REALTIME_MS)
+        assertEquals(3, responses.size)
+        assertEquals(response2, responses[0])
+        assertEquals(response3, responses[1])
+        assertEquals(response1, responses[2])
+
+        val response4 = createResponse("service-instance-4", SERVICE_TYPE_1, 125L /* ttlTime */)
+        MdnsServiceCache.insertResponseAndSortList(responses, response4, TEST_ELAPSED_REALTIME_MS)
+        assertEquals(4, responses.size)
+        assertEquals(response2, responses[0])
+        assertEquals(response3, responses[1])
+        assertEquals(response1, responses[2])
+        assertEquals(response4, responses[3])
+    }
+
+    private fun createResponse(
+            serviceInstanceName: String,
+            serviceType: String,
+            ttlTime: Long = 120000L
+    ): MdnsResponse {
+        val serviceName = "$serviceInstanceName.$serviceType".split(".").toTypedArray()
+        val response = MdnsResponse(
+                0 /* now */, "$serviceInstanceName.$serviceType".split(".").toTypedArray(),
+                socketKey.interfaceIndex, socketKey.network)
+
+        // Set PTR record
+        val pointerRecord = MdnsPointerRecord(
+                serviceType.split(".").toTypedArray(),
+                TEST_ELAPSED_REALTIME_MS /* receiptTimeMillis */,
+                false /* cacheFlush */,
+                ttlTime /* ttlMillis */,
+                serviceName)
+        response.addPointerRecord(pointerRecord)
+
+        // Set SRV record.
+        val serviceRecord = MdnsServiceRecord(
+                serviceName,
+                TEST_ELAPSED_REALTIME_MS /* receiptTimeMillis */,
+                false /* cacheFlush */,
+                ttlTime /* ttlMillis */,
+                0 /* servicePriority */,
+                0 /* serviceWeight */,
+                12345 /* port */,
+                arrayOf("hostname"))
+        response.serviceRecord = serviceRecord
+        return response
+    }
 }
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java
index ce154dd..dab3b42 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;
@@ -87,6 +93,7 @@
 import java.util.stream.Stream;
 
 /** Tests for {@link MdnsServiceTypeClient}. */
+@DevSdkIgnoreRunner.MonitorThreadLeak
 @RunWith(DevSdkIgnoreRunner.class)
 @DevSdkIgnoreRule.IgnoreUpTo(SC_V2)
 public class MdnsServiceTypeClientTests {
@@ -144,8 +151,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) {
@@ -170,7 +177,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])
@@ -188,13 +203,23 @@
                 .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();
         handler = new Handler(thread.getLooper());
         serviceCache = new MdnsServiceCache(
-                thread.getLooper(), MdnsFeatureFlags.newBuilder().build());
+                thread.getLooper(),
+                MdnsFeatureFlags.newBuilder().setIsExpiredServicesRemovalEnabled(false).build(),
+                mockDecoderClock);
 
         doAnswer(inv -> {
             latestDelayMs = 0;
@@ -228,9 +253,10 @@
     }
 
     @After
-    public void tearDown() {
+    public void tearDown() throws Exception {
         if (thread != null) {
             thread.quitSafely();
+            thread.join();
         }
     }
 
@@ -263,8 +289,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));
@@ -315,8 +341,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));
@@ -329,7 +355,7 @@
                 MdnsSearchOptions.newBuilder()
                         .addSubtype(SUBTYPE)
                         .addSubtype("_subtype2")
-                        .setIsPassiveMode(false)
+                        .setQueryMode(ACTIVE_QUERY_MODE)
                         .build();
         startSendAndReceive(mockListenerOne, searchOptions);
         // The previous scheduled task should be canceled.
@@ -349,8 +375,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));
@@ -376,8 +402,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));
@@ -435,8 +463,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));
@@ -493,8 +523,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));
@@ -507,7 +537,7 @@
                 MdnsSearchOptions.newBuilder()
                         .addSubtype(SUBTYPE)
                         .addSubtype("_subtype2")
-                        .setIsPassiveMode(true)
+                        .setQueryMode(PASSIVE_QUERY_MODE)
                         .build();
         startSendAndReceive(mockListenerOne, searchOptions);
         // The previous scheduled task should be canceled.
@@ -529,10 +559,10 @@
     @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.getSubtypes(), searchOptions.getQueryMode(),
                 false /* onlyUseIpv6OnIpv6OnlyNetworks */, 3 /* numOfQueriesBeforeBackoff */,
                 socketKey);
 
@@ -560,10 +590,10 @@
 
     @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.getSubtypes(), searchOptions.getQueryMode(),
                 false /* onlyUseIpv6OnIpv6OnlyNetworks */, 3 /* numOfQueriesBeforeBackoff */,
                 socketKey);
 
@@ -591,8 +621,8 @@
 
     @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();
 
@@ -601,7 +631,7 @@
                 MdnsSearchOptions.newBuilder()
                         .addSubtype(SUBTYPE)
                         .addSubtype("_subtype2")
-                        .setIsPassiveMode(true)
+                        .setQueryMode(PASSIVE_QUERY_MODE)
                         .build();
         startSendAndReceive(mockListenerOne, searchOptions);
 
@@ -620,8 +650,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);
@@ -1488,6 +1518,91 @@
     }
 
     @Test
+    public void testProcessResponse_SubtypeChange() {
+        client = new MdnsServiceTypeClient(SERVICE_TYPE, mockSocketClient, currentThreadExecutor,
+                mockDecoderClock, socketKey, mockSharedLog, thread.getLooper(), mockDeps,
+                serviceCache);
+
+        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,
@@ -1662,6 +1777,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));
     }
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/internal/SocketNetlinkMonitorTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/internal/SocketNetlinkMonitorTest.kt
index c62a081..3e1dab8 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/internal/SocketNetlinkMonitorTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/internal/SocketNetlinkMonitorTest.kt
@@ -27,6 +27,7 @@
 private val LINKADDRV4 = LinkAddress("192.0.2.0/24")
 private val IFACE_IDX = 32
 
+@DevSdkIgnoreRunner.MonitorThreadLeak
 @RunWith(DevSdkIgnoreRunner::class)
 @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
 internal class SocketNetlinkMonitorTest {
@@ -43,6 +44,7 @@
     @After
     fun tearDown() {
         thread.quitSafely()
+        thread.join()
     }
 
     @Test
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
new file mode 100644
index 0000000..8155fd0
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivityservice/CSBpfNetMapsTest.kt
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server
+
+import android.content.Intent
+import android.net.ConnectivityManager.ACTION_RESTRICT_BACKGROUND_CHANGED
+import android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_DISABLED
+import android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED
+import android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_WHITELISTED
+import android.os.Build
+import androidx.test.filters.SmallTest
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.visibleOnHandlerThread
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.inOrder
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+
+@DevSdkIgnoreRunner.MonitorThreadLeak
+@RunWith(DevSdkIgnoreRunner::class)
+@SmallTest
+@IgnoreUpTo(Build.VERSION_CODES.S_V2) // Bpf only supports in T+.
+class CSBpfNetMapsTest : CSTest() {
+    @get:Rule
+    val ignoreRule = DevSdkIgnoreRule()
+
+    @IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @Test
+    fun testCSTrackDataSaverBeforeV() {
+        val inOrder = inOrder(bpfNetMaps)
+        mockDataSaverStatus(RESTRICT_BACKGROUND_STATUS_WHITELISTED)
+        inOrder.verify(bpfNetMaps).setDataSaverEnabled(true)
+        mockDataSaverStatus(RESTRICT_BACKGROUND_STATUS_DISABLED)
+        inOrder.verify(bpfNetMaps).setDataSaverEnabled(false)
+        mockDataSaverStatus(RESTRICT_BACKGROUND_STATUS_ENABLED)
+        inOrder.verify(bpfNetMaps).setDataSaverEnabled(true)
+    }
+
+    // Data Saver Status is updated from platform code in V+.
+    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @Test
+    fun testCSTrackDataSaverAboveU() {
+        listOf(RESTRICT_BACKGROUND_STATUS_WHITELISTED, RESTRICT_BACKGROUND_STATUS_ENABLED,
+            RESTRICT_BACKGROUND_STATUS_DISABLED).forEach {
+            mockDataSaverStatus(it)
+            verify(bpfNetMaps, never()).setDataSaverEnabled(anyBoolean())
+        }
+    }
+
+    private fun mockDataSaverStatus(status: Int) {
+        doReturn(status).`when`(context.networkPolicyManager).getRestrictBackgroundStatus(anyInt())
+        // While the production code dispatches the intent on the handler thread,
+        // The test would dispatch the intent in the caller thread. Make it dispatch
+        // on the handler thread to match production behavior.
+        visibleOnHandlerThread(csHandler) {
+            context.sendBroadcast(Intent(ACTION_RESTRICT_BACKGROUND_CHANGED))
+        }
+        waitForIdle()
+    }
+}
diff --git a/tests/unit/java/com/android/server/connectivityservice/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 2126a09..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)
@@ -47,8 +48,9 @@
         val keepConnectedAgent = Agent(nc = nc, score = FromS(NetworkScore.Builder()
                 .setKeepConnectedReason(KEEP_CONNECTED_LOCAL_NETWORK)
                 .build()),
-                lnc = LocalNetworkConfig.Builder().build())
-        val dontKeepConnectedAgent = Agent(nc = nc, lnc = LocalNetworkConfig.Builder().build())
+                lnc = FromS(LocalNetworkConfig.Builder().build()))
+        val dontKeepConnectedAgent = Agent(nc = nc,
+                lnc = FromS(LocalNetworkConfig.Builder().build()))
         doTestKeepConnected(keepConnectedAgent, dontKeepConnectedAgent)
     }
 
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentCreationTests.kt b/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentCreationTests.kt
index cfc3a3d..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
@@ -49,8 +49,9 @@
 private fun keepConnectedScore() =
         FromS(NetworkScore.Builder().setKeepConnectedReason(KEEP_CONNECTED_FOR_TEST).build())
 
-private fun defaultLnc() = LocalNetworkConfig.Builder().build()
+private fun defaultLnc() = FromS(LocalNetworkConfig.Builder().build())
 
+@DevSdkIgnoreRunner.MonitorThreadLeak
 @RunWith(DevSdkIgnoreRunner::class)
 @SmallTest
 @IgnoreUpTo(Build.VERSION_CODES.R)
@@ -124,8 +125,7 @@
                     lnc = null)
         }
         assertFailsWith<IllegalArgumentException> {
-            Agent(nc = NetworkCapabilities.Builder().build(),
-                    lnc = LocalNetworkConfig.Builder().build())
+            Agent(nc = NetworkCapabilities.Builder().build(), lnc = defaultLnc())
         }
     }
 }
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentTests.kt b/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentTests.kt
index 1dab548..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)
@@ -94,7 +116,7 @@
         }
         assertFailsWith<IllegalArgumentException> {
             Agent(nc = NetworkCapabilities.Builder().build(),
-                    lnc = LocalNetworkConfig.Builder().build())
+                    lnc = FromS(LocalNetworkConfig.Builder().build()))
         }
     }
 
@@ -110,7 +132,7 @@
         val agent = Agent(nc = NetworkCapabilities.Builder()
                 .addCapability(NET_CAPABILITY_LOCAL_NETWORK)
                 .build(),
-                lnc = LocalNetworkConfig.Builder().build())
+                lnc = FromS(LocalNetworkConfig.Builder().build()))
         agent.connect()
         cb.expectAvailableCallbacks(agent.network, validated = false)
         agent.sendNetworkCapabilities(NetworkCapabilities.Builder().build())
@@ -141,7 +163,7 @@
         val localAgent = Agent(
                 nc = nc(TRANSPORT_WIFI, NET_CAPABILITY_LOCAL_NETWORK),
                 lp = lp("local0"),
-                lnc = LocalNetworkConfig.Builder().build(),
+                lnc = FromS(LocalNetworkConfig.Builder().build()),
         )
         localAgent.connect()
 
@@ -154,6 +176,7 @@
         val newLnc = LocalNetworkConfig.Builder()
                 .setUpstreamSelector(NetworkRequest.Builder()
                         .addTransportType(TRANSPORT_WIFI)
+                        .addForbiddenCapability(NET_CAPABILITY_LOCAL_NETWORK)
                         .build())
                 .build()
         localAgent.sendLocalNetworkConfig(newLnc)
@@ -176,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)
@@ -194,11 +477,11 @@
         // Set up a local agent that should forward its traffic to the best wifi upstream.
         val localAgent = Agent(nc = nc(TRANSPORT_WIFI, NET_CAPABILITY_LOCAL_NETWORK),
                 lp = lp("local0"),
-                lnc = LocalNetworkConfig.Builder()
-                .setUpstreamSelector(NetworkRequest.Builder()
-                        .addTransportType(TRANSPORT_WIFI)
-                        .build())
-                .build(),
+                lnc = FromS(LocalNetworkConfig.Builder()
+                        .setUpstreamSelector(upstreamSelectorWifi)
+                        .setUpstreamMulticastRoutingConfig(multicastRoutingConfigMinScope)
+                        .setDownstreamMulticastRoutingConfig(multicastRoutingConfigSelected)
+                        .build()),
                 score = FromS(NetworkScore.Builder()
                         .setKeepConnectedReason(KEEP_CONNECTED_LOCAL_NETWORK)
                         .build())
@@ -217,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"
@@ -233,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
@@ -247,11 +542,12 @@
         // Set up a local agent that should forward its traffic to the best wifi upstream.
         val localAgent = Agent(nc = nc(TRANSPORT_WIFI, NET_CAPABILITY_LOCAL_NETWORK),
                 lp = lp("local0"),
-                lnc = LocalNetworkConfig.Builder()
+                lnc = FromS(LocalNetworkConfig.Builder()
                         .setUpstreamSelector(NetworkRequest.Builder()
                                 .addTransportType(TRANSPORT_WIFI)
+                                .addForbiddenCapability(NET_CAPABILITY_LOCAL_NETWORK)
                                 .build())
-                        .build(),
+                        .build()),
                 score = FromS(NetworkScore.Builder()
                         .setKeepConnectedReason(KEEP_CONNECTED_LOCAL_NETWORK)
                         .build())
@@ -293,11 +589,12 @@
         cm.registerNetworkCallback(NetworkRequest.Builder().clearCapabilities().build(), cb)
 
         val localNc = nc(TRANSPORT_WIFI, NET_CAPABILITY_LOCAL_NETWORK)
-        val lnc = LocalNetworkConfig.Builder()
+        val lnc = FromS(LocalNetworkConfig.Builder()
                 .setUpstreamSelector(NetworkRequest.Builder()
                         .addTransportType(TRANSPORT_WIFI)
+                        .addForbiddenCapability(NET_CAPABILITY_LOCAL_NETWORK)
                         .build())
-                .build()
+                .build())
         val localScore = FromS(NetworkScore.Builder().build())
 
         // Set up a local agent that should forward its traffic to the best wifi upstream.
@@ -346,11 +643,12 @@
         wifiAgent.unregisterAfterReplacement(LONG_TIMEOUT_MS)
         val localAgent = Agent(nc = nc(TRANSPORT_WIFI, NET_CAPABILITY_LOCAL_NETWORK),
                 lp = lp("local0"),
-                lnc = LocalNetworkConfig.Builder()
+                lnc = FromS(LocalNetworkConfig.Builder()
                         .setUpstreamSelector(NetworkRequest.Builder()
+                                .addForbiddenCapability(NET_CAPABILITY_LOCAL_NETWORK)
                                 .addTransportType(TRANSPORT_WIFI)
                                 .build())
-                        .build(),
+                        .build()),
                 score = FromS(NetworkScore.Builder()
                         .setKeepConnectedReason(KEEP_CONNECTED_LOCAL_NETWORK)
                         .build())
@@ -374,11 +672,12 @@
     fun testForwardingRules() {
         deps.setBuildSdk(VERSION_V)
         // Set up a local agent that should forward its traffic to the best DUN upstream.
-        val lnc = LocalNetworkConfig.Builder()
+        val lnc = FromS(LocalNetworkConfig.Builder()
                 .setUpstreamSelector(NetworkRequest.Builder()
                         .addCapability(NET_CAPABILITY_DUN)
+                        .addForbiddenCapability(NET_CAPABILITY_LOCAL_NETWORK)
                         .build())
-                .build()
+                .build())
         val localAgent = Agent(nc = nc(TRANSPORT_WIFI, NET_CAPABILITY_LOCAL_NETWORK),
                 lp = lp("local0"),
                 lnc = lnc,
@@ -426,7 +725,7 @@
 
         // Make sure sending the same config again doesn't do anything
         repeat(5) {
-            localAgent.sendLocalNetworkConfig(lnc)
+            localAgent.sendLocalNetworkConfig(lnc.value)
         }
         inOrder.verifyNoMoreInteractions()
 
@@ -501,13 +800,14 @@
         }
 
         // Set up a local agent.
-        val lnc = LocalNetworkConfig.Builder().apply {
+        val lnc = FromS(LocalNetworkConfig.Builder().apply {
             if (haveUpstream) {
                 setUpstreamSelector(NetworkRequest.Builder()
+                        .addForbiddenCapability(NET_CAPABILITY_LOCAL_NETWORK)
                         .addTransportType(TRANSPORT_WIFI)
                         .build())
             }
-        }.build()
+        }.build())
         val localAgent = Agent(nc = nc(TRANSPORT_THREAD, NET_CAPABILITY_LOCAL_NETWORK),
                 lp = lp("local0"),
                 lnc = lnc,
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSNetworkActivityTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSNetworkActivityTest.kt
new file mode 100644
index 0000000..df0a2cc
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivityservice/CSNetworkActivityTest.kt
@@ -0,0 +1,268 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server
+
+import android.net.ConnectivityManager
+import android.net.ConnectivityManager.ACTION_DATA_ACTIVITY_CHANGE
+import android.net.ConnectivityManager.EXTRA_DEVICE_TYPE
+import android.net.ConnectivityManager.EXTRA_IS_ACTIVE
+import android.net.ConnectivityManager.EXTRA_REALTIME_NS
+import android.net.LinkProperties
+import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.NET_CAPABILITY_IMS
+import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED
+import android.net.NetworkCapabilities.TRANSPORT_CELLULAR
+import android.net.NetworkCapabilities.TRANSPORT_WIFI
+import android.net.NetworkRequest
+import android.os.Build
+import android.os.ConditionVariable
+import android.telephony.DataConnectionRealTimeInfo.DC_POWER_STATE_HIGH
+import android.telephony.DataConnectionRealTimeInfo.DC_POWER_STATE_LOW
+import androidx.test.filters.SmallTest
+import com.android.net.module.util.BaseNetdUnsolicitedEventListener
+import com.android.server.CSTest.CSContext
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.RecorderCallback.CallbackEntry.Lost
+import com.android.testutils.TestableNetworkCallback
+import kotlin.test.assertNotNull
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Mockito.anyInt
+import org.mockito.Mockito.anyLong
+import org.mockito.Mockito.inOrder
+import org.mockito.Mockito.never
+import org.mockito.Mockito.timeout
+import org.mockito.Mockito.verify
+
+private const val DATA_CELL_IFNAME = "rmnet_data"
+private const val IMS_CELL_IFNAME = "rmnet_ims"
+private const val WIFI_IFNAME = "wlan0"
+private const val TIMESTAMP = 1234L
+private const val NETWORK_ACTIVITY_NO_UID = -1
+private const val PACKAGE_UID = 123
+private const val TIMEOUT_MS = 250L
+
+@DevSdkIgnoreRunner.MonitorThreadLeak
+@RunWith(DevSdkIgnoreRunner::class)
+@SmallTest
+@IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+class CSNetworkActivityTest : CSTest() {
+
+    private fun getRegisteredNetdUnsolicitedEventListener(): BaseNetdUnsolicitedEventListener {
+        val captor = ArgumentCaptor.forClass(BaseNetdUnsolicitedEventListener::class.java)
+        verify(netd).registerUnsolicitedEventListener(captor.capture())
+        return captor.value
+    }
+
+    @Test
+    fun testInterfaceClassActivityChanged_NonDefaultNetwork() {
+        val netdUnsolicitedEventListener = getRegisteredNetdUnsolicitedEventListener()
+        val batteryStatsInorder = inOrder(batteryStats)
+
+        val cellNr = NetworkRequest.Builder()
+                .clearCapabilities()
+                .addTransportType(TRANSPORT_CELLULAR)
+                .addCapability(NET_CAPABILITY_INTERNET)
+                .build()
+        val cellCb = TestableNetworkCallback()
+        // Request cell network to keep cell network up
+        cm.requestNetwork(cellNr, cellCb)
+
+        val defaultCb = TestableNetworkCallback()
+        cm.registerDefaultNetworkCallback(defaultCb)
+
+        val cellNc = NetworkCapabilities.Builder()
+                .addTransportType(TRANSPORT_CELLULAR)
+                .addCapability(NET_CAPABILITY_INTERNET)
+                .addCapability(NET_CAPABILITY_NOT_SUSPENDED)
+                .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+                .build()
+        val cellLp = LinkProperties().apply {
+            interfaceName = DATA_CELL_IFNAME
+        }
+        // Connect Cellular network
+        val cellAgent = Agent(nc = cellNc, lp = cellLp)
+        cellAgent.connect()
+        defaultCb.expectAvailableCallbacks(cellAgent.network, validated = false)
+
+        val wifiNc = NetworkCapabilities.Builder()
+                .addTransportType(TRANSPORT_WIFI)
+                .addCapability(NET_CAPABILITY_INTERNET)
+                .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+                .build()
+        val wifiLp = LinkProperties().apply {
+            interfaceName = WIFI_IFNAME
+        }
+        // Connect Wi-Fi network, Wi-Fi network should be the default network.
+        val wifiAgent = Agent(nc = wifiNc, lp = wifiLp)
+        wifiAgent.connect()
+        defaultCb.expectAvailableCallbacks(wifiAgent.network, validated = false)
+        batteryStatsInorder.verify(batteryStats).noteWifiRadioPowerState(eq(DC_POWER_STATE_HIGH),
+                anyLong() /* timestampNs */, eq(NETWORK_ACTIVITY_NO_UID))
+
+        val onNetworkActiveCv = ConditionVariable()
+        val listener = ConnectivityManager.OnNetworkActiveListener { onNetworkActiveCv::open }
+        cm.addDefaultNetworkActiveListener(listener)
+
+        // Cellular network (non default network) goes to inactive state.
+        netdUnsolicitedEventListener.onInterfaceClassActivityChanged(false /* isActive */,
+                cellAgent.network.netId, TIMESTAMP, NETWORK_ACTIVITY_NO_UID)
+        // Non-default network activity change does not change default network activity
+        // But cellular radio power state is updated
+        assertFalse(onNetworkActiveCv.block(TIMEOUT_MS))
+        context.expectNoDataActivityBroadcast(0 /* timeoutMs */)
+        assertTrue(cm.isDefaultNetworkActive)
+        batteryStatsInorder.verify(batteryStats).noteMobileRadioPowerState(eq(DC_POWER_STATE_LOW),
+                anyLong() /* timestampNs */, eq(NETWORK_ACTIVITY_NO_UID))
+
+        // Cellular network (non default network) goes to active state.
+        netdUnsolicitedEventListener.onInterfaceClassActivityChanged(true /* isActive */,
+                cellAgent.network.netId, TIMESTAMP, PACKAGE_UID)
+        // Non-default network activity change does not change default network activity
+        // But cellular radio power state is updated
+        assertFalse(onNetworkActiveCv.block(TIMEOUT_MS))
+        context.expectNoDataActivityBroadcast(0 /* timeoutMs */)
+        assertTrue(cm.isDefaultNetworkActive)
+        batteryStatsInorder.verify(batteryStats).noteMobileRadioPowerState(eq(DC_POWER_STATE_HIGH),
+                anyLong() /* timestampNs */, eq(PACKAGE_UID))
+
+        cm.unregisterNetworkCallback(cellCb)
+        cm.unregisterNetworkCallback(defaultCb)
+        cm.removeDefaultNetworkActiveListener(listener)
+    }
+
+    @Test
+    fun testDataActivityTracking_MultiCellNetwork() {
+        val netdUnsolicitedEventListener = getRegisteredNetdUnsolicitedEventListener()
+        val batteryStatsInorder = inOrder(batteryStats)
+
+        val dataNetworkNc = NetworkCapabilities.Builder()
+                .addTransportType(TRANSPORT_CELLULAR)
+                .addCapability(NET_CAPABILITY_INTERNET)
+                .addCapability(NET_CAPABILITY_NOT_SUSPENDED)
+                .build()
+        val dataNetworkNr = NetworkRequest.Builder()
+                .clearCapabilities()
+                .addTransportType(TRANSPORT_CELLULAR)
+                .addCapability(NET_CAPABILITY_INTERNET)
+                .build()
+        val dataNetworkLp = LinkProperties().apply {
+            interfaceName = DATA_CELL_IFNAME
+        }
+        val dataNetworkCb = TestableNetworkCallback()
+        cm.requestNetwork(dataNetworkNr, dataNetworkCb)
+        val dataNetworkAgent = Agent(nc = dataNetworkNc, lp = dataNetworkLp)
+        val dataNetworkNetId = dataNetworkAgent.network.netId.toString()
+
+        val imsNetworkNc = NetworkCapabilities.Builder()
+                .addTransportType(TRANSPORT_CELLULAR)
+                .addCapability(NET_CAPABILITY_IMS)
+                .addCapability(NET_CAPABILITY_NOT_SUSPENDED)
+                .build()
+        val imsNetworkNr = NetworkRequest.Builder()
+                .clearCapabilities()
+                .addTransportType(TRANSPORT_CELLULAR)
+                .addCapability(NET_CAPABILITY_IMS)
+                .build()
+        val imsNetworkLp = LinkProperties().apply {
+            interfaceName = IMS_CELL_IFNAME
+        }
+        val imsNetworkCb = TestableNetworkCallback()
+        cm.requestNetwork(imsNetworkNr, imsNetworkCb)
+        val imsNetworkAgent = Agent(nc = imsNetworkNc, lp = imsNetworkLp)
+        val imsNetworkNetId = imsNetworkAgent.network.netId.toString()
+
+        dataNetworkAgent.connect()
+        dataNetworkCb.expectAvailableCallbacks(dataNetworkAgent.network, validated = false)
+
+        imsNetworkAgent.connect()
+        imsNetworkCb.expectAvailableCallbacks(imsNetworkAgent.network, validated = false)
+
+        // Both cell networks have idleTimers
+        verify(netd).idletimerAddInterface(eq(DATA_CELL_IFNAME), anyInt(), eq(dataNetworkNetId))
+        verify(netd).idletimerAddInterface(eq(IMS_CELL_IFNAME), anyInt(), eq(imsNetworkNetId))
+        verify(netd, never()).idletimerRemoveInterface(eq(DATA_CELL_IFNAME), anyInt(),
+                eq(dataNetworkNetId))
+        verify(netd, never()).idletimerRemoveInterface(eq(IMS_CELL_IFNAME), anyInt(),
+                eq(imsNetworkNetId))
+
+        // Both cell networks go to inactive state
+        netdUnsolicitedEventListener.onInterfaceClassActivityChanged(false /* isActive */,
+                imsNetworkAgent.network.netId, TIMESTAMP, NETWORK_ACTIVITY_NO_UID)
+        netdUnsolicitedEventListener.onInterfaceClassActivityChanged(false /* isActive */,
+                dataNetworkAgent.network.netId, TIMESTAMP, NETWORK_ACTIVITY_NO_UID)
+
+        // Data cell network goes to active state. This should update the cellular radio power state
+        netdUnsolicitedEventListener.onInterfaceClassActivityChanged(true /* isActive */,
+                dataNetworkAgent.network.netId, TIMESTAMP, PACKAGE_UID)
+        batteryStatsInorder.verify(batteryStats, timeout(TIMEOUT_MS)).noteMobileRadioPowerState(
+                eq(DC_POWER_STATE_HIGH), anyLong() /* timestampNs */, eq(PACKAGE_UID))
+        // Ims cell network goes to active state. But this should not update the cellular radio
+        // power state since cellular radio power state is already high
+        netdUnsolicitedEventListener.onInterfaceClassActivityChanged(true /* isActive */,
+                imsNetworkAgent.network.netId, TIMESTAMP, PACKAGE_UID)
+        waitForIdle()
+        batteryStatsInorder.verify(batteryStats, never()).noteMobileRadioPowerState(anyInt(),
+                anyLong() /* timestampNs */, anyInt())
+
+        // Data cell network goes to inactive state. But this should not update the cellular radio
+        // power state ims cell network is still active state
+        netdUnsolicitedEventListener.onInterfaceClassActivityChanged(false /* isActive */,
+                dataNetworkAgent.network.netId, TIMESTAMP, NETWORK_ACTIVITY_NO_UID)
+        waitForIdle()
+        batteryStatsInorder.verify(batteryStats, never()).noteMobileRadioPowerState(anyInt(),
+                anyLong() /* timestampNs */, anyInt())
+
+        // Ims cell network goes to inactive state.
+        // This should update the cellular radio power state
+        netdUnsolicitedEventListener.onInterfaceClassActivityChanged(false /* isActive */,
+                imsNetworkAgent.network.netId, TIMESTAMP, NETWORK_ACTIVITY_NO_UID)
+        batteryStatsInorder.verify(batteryStats, timeout(TIMEOUT_MS)).noteMobileRadioPowerState(
+                eq(DC_POWER_STATE_LOW), anyLong() /* timestampNs */, eq(NETWORK_ACTIVITY_NO_UID))
+
+        dataNetworkAgent.disconnect()
+        dataNetworkCb.expect<Lost>(dataNetworkAgent.network)
+        verify(netd).idletimerRemoveInterface(eq(DATA_CELL_IFNAME), anyInt(), eq(dataNetworkNetId))
+
+        imsNetworkAgent.disconnect()
+        imsNetworkCb.expect<Lost>(imsNetworkAgent.network)
+        verify(netd).idletimerRemoveInterface(eq(IMS_CELL_IFNAME), anyInt(), eq(imsNetworkNetId))
+
+        cm.unregisterNetworkCallback(dataNetworkCb)
+        cm.unregisterNetworkCallback(imsNetworkCb)
+    }
+}
+
+internal fun CSContext.expectDataActivityBroadcast(
+        deviceType: Int,
+        isActive: Boolean,
+        tsNanos: Long
+) {
+    assertNotNull(orderedBroadcastAsUserHistory.poll(BROADCAST_TIMEOUT_MS) {
+        intent -> intent.action.equals(ACTION_DATA_ACTIVITY_CHANGE) &&
+            intent.getIntExtra(EXTRA_DEVICE_TYPE, -1) == deviceType &&
+            intent.getBooleanExtra(EXTRA_IS_ACTIVE, !isActive) == isActive &&
+            intent.getLongExtra(EXTRA_REALTIME_NS, -1) == tsNanos
+    })
+}
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSNetworkRequestStateStatsMetricsTests.kt b/tests/unit/java/com/android/server/connectivityservice/CSNetworkRequestStateStatsMetricsTests.kt
new file mode 100644
index 0000000..35f8ae5
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivityservice/CSNetworkRequestStateStatsMetricsTests.kt
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server
+
+import android.net.NetworkCapabilities
+import android.net.NetworkRequest
+import android.os.Build
+import android.os.Process
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.TestableNetworkCallback
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.argThat
+import org.mockito.Mockito.clearInvocations
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+
+@RunWith(DevSdkIgnoreRunner::class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+class CSNetworkRequestStateStatsMetricsTests : CSTest() {
+    private val CELL_INTERNET_NOT_METERED_NC = NetworkCapabilities.Builder()
+            .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
+            .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
+            .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)
+            .build().setRequestorUidAndPackageName(Process.myUid(), context.getPackageName())
+
+    private val CELL_INTERNET_NOT_METERED_NR = NetworkRequest.Builder()
+            .setCapabilities(CELL_INTERNET_NOT_METERED_NC).build()
+
+    @Before
+    fun setup() {
+        waitForIdle()
+        clearInvocations(networkRequestStateStatsMetrics)
+    }
+
+    @Test
+    fun testRequestTypeNRProduceMetrics() {
+        cm.requestNetwork(CELL_INTERNET_NOT_METERED_NR, TestableNetworkCallback())
+        waitForIdle()
+
+        verify(networkRequestStateStatsMetrics).onNetworkRequestReceived(
+                argThat{req -> req.networkCapabilities.equals(
+                        CELL_INTERNET_NOT_METERED_NR.networkCapabilities)})
+    }
+
+    @Test
+    fun testListenTypeNRProduceNoMetrics() {
+        cm.registerNetworkCallback(CELL_INTERNET_NOT_METERED_NR, TestableNetworkCallback())
+        waitForIdle()
+        verify(networkRequestStateStatsMetrics, never()).onNetworkRequestReceived(any())
+    }
+
+    @Test
+    fun testRemoveRequestTypeNRProduceMetrics() {
+        val cb = TestableNetworkCallback()
+        cm.requestNetwork(CELL_INTERNET_NOT_METERED_NR, cb)
+
+        waitForIdle()
+        clearInvocations(networkRequestStateStatsMetrics)
+
+        cm.unregisterNetworkCallback(cb)
+        waitForIdle()
+        verify(networkRequestStateStatsMetrics).onNetworkRequestRemoved(
+                argThat{req -> req.networkCapabilities.equals(
+                        CELL_INTERNET_NOT_METERED_NR.networkCapabilities)})
+    }
+}
diff --git a/tests/unit/java/com/android/server/connectivityservice/base/CSAgentWrapper.kt b/tests/unit/java/com/android/server/connectivityservice/base/CSAgentWrapper.kt
index 013a749..d41c742 100644
--- a/tests/unit/java/com/android/server/connectivityservice/base/CSAgentWrapper.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/base/CSAgentWrapper.kt
@@ -69,7 +69,7 @@
         nac: NetworkAgentConfig,
         val nc: NetworkCapabilities,
         val lp: LinkProperties,
-        val lnc: LocalNetworkConfig?,
+        val lnc: FromS<LocalNetworkConfig>?,
         val score: FromS<NetworkScore>,
         val provider: NetworkProvider?
 ) : TestableNetworkCallback.HasNetwork {
@@ -101,7 +101,7 @@
         // Create the actual agent. NetworkAgent is abstract, so make an anonymous subclass.
         if (deps.isAtLeastS()) {
             agent = object : NetworkAgent(context, csHandlerThread.looper, TAG,
-                    nc, lp, lnc, score.value, nac, provider) {}
+                    nc, lp, lnc?.value, score.value, nac, provider) {}
         } else {
             agent = object : NetworkAgent(context, csHandlerThread.looper, TAG,
                     nc, lp, 50 /* score */, nac, provider) {}
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 0ccbfc3..0708669 100644
--- a/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt
@@ -43,6 +43,7 @@
 import android.net.PacProxyManager
 import android.net.networkstack.NetworkStackClientBase
 import android.os.BatteryStatsManager
+import android.os.Bundle
 import android.os.Handler
 import android.os.HandlerThread
 import android.os.UserHandle
@@ -54,24 +55,31 @@
 import com.android.internal.app.IBatteryStats
 import com.android.internal.util.test.BroadcastInterceptingContext
 import com.android.modules.utils.build.SdkLevel
+import com.android.net.module.util.ArrayTrackRecord
 import com.android.networkstack.apishim.common.UnsupportedApiLevelException
 import com.android.server.connectivity.AutomaticOnOffKeepaliveTracker
 import com.android.server.connectivity.CarrierPrivilegeAuthenticator
 import com.android.server.connectivity.ClatCoordinator
 import com.android.server.connectivity.ConnectivityFlags
+import com.android.server.connectivity.MulticastRoutingCoordinatorService
 import com.android.server.connectivity.MultinetworkPolicyTracker
 import com.android.server.connectivity.MultinetworkPolicyTrackerTestDependencies
+import com.android.server.connectivity.NetworkRequestStateStatsMetrics
 import com.android.server.connectivity.ProxyTracker
+import com.android.server.connectivity.RoutingCoordinatorService
 import com.android.testutils.visibleOnHandlerThread
 import com.android.testutils.waitForIdle
+import java.util.concurrent.Executors
+import kotlin.test.assertNull
+import kotlin.test.fail
+import org.junit.After
 import org.mockito.AdditionalAnswers.delegatesTo
 import org.mockito.Mockito.doAnswer
 import org.mockito.Mockito.doReturn
 import org.mockito.Mockito.mock
-import java.util.concurrent.Executors
-import kotlin.test.fail
 
 internal const val HANDLER_TIMEOUT_MS = 2_000
+internal const val BROADCAST_TIMEOUT_MS = 3_000L
 internal const val TEST_PACKAGE_NAME = "com.android.test.package"
 internal const val WIFI_WOL_IFNAME = "test_wlan_wol"
 internal val LOCAL_IPV4_ADDRESS = InetAddresses.parseNumericAddress("192.0.2.1")
@@ -127,6 +135,8 @@
         it[ConnectivityFlags.NO_REMATCH_ALL_REQUESTS_ON_REGISTER] = 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
     }
     fun enableFeature(f: String) = enabledFeatures.set(f, true)
     fun disableFeature(f: String) = enabledFeatures.set(f, false)
@@ -151,27 +161,42 @@
     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 alrmHandlerThread = HandlerThread("TestAlarmManager").also { it.start() }
+    val alarmManager = makeMockAlarmManager(alrmHandlerThread)
     val systemConfigManager = makeMockSystemConfigManager()
-    val batteryManager = BatteryStatsManager(mock<IBatteryStats>())
+    val batteryStats = mock<IBatteryStats>()
+    val batteryManager = BatteryStatsManager(batteryStats)
     val telephonyManager = mock<TelephonyManager>().also {
         doReturn(true).`when`(it).isDataCapable()
     }
 
+    val multicastRoutingCoordinatorService = mock<MulticastRoutingCoordinatorService>()
+
     val deps = CSDeps()
     val service = makeConnectivityService(context, netd, deps).also { it.systemReadyInternal() }
     val cm = ConnectivityManager(context, service)
     val csHandler = Handler(csHandlerThread.looper)
 
+    @After
+    fun tearDown() {
+        csHandlerThread.quitSafely()
+        csHandlerThread.join()
+        alrmHandlerThread.quitSafely()
+        alrmHandlerThread.join()
+    }
+
     inner class CSDeps : ConnectivityService.Dependencies() {
         override fun getResources(ctx: Context) = connResources
         override fun getBpfNetMaps(context: Context, netd: INetd) = this@CSTest.bpfNetMaps
         override fun getClatCoordinator(netd: INetd?) = this@CSTest.clatCoordinator
         override fun getNetworkStack() = this@CSTest.networkStack
 
-        override fun makeHandlerThread() = csHandlerThread
+        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,
@@ -190,11 +215,16 @@
                 MultinetworkPolicyTracker(c, h, r,
                         MultinetworkPolicyTrackerTestDependencies(connResources.get()))
 
+        override fun makeNetworkRequestStateStatsMetrics(c: Context) =
+                this@CSTest.networkRequestStateStatsMetrics
+
         // All queried features must be mocked, because the test cannot hold the
         // READ_DEVICE_CONFIG permission and device config utils use static methods for
         // checking permissions.
         override fun isFeatureEnabled(context: Context?, name: String?) =
                 enabledFeatures[name] ?: fail("Unmocked feature $name, see CSTest.enabledFeatures")
+        override fun isFeatureNotChickenedOut(context: Context?, name: String?) =
+                enabledFeatures[name] ?: fail("Unmocked feature $name, see CSTest.enabledFeatures")
 
         // Mocked change IDs
         private val enabledChangeIds = ArraySet<Long>()
@@ -282,6 +312,26 @@
             Context.STATS_MANAGER -> null // Stats manager is final and can't be mocked
             else -> super.getSystemService(serviceName)
         }
+
+        internal val orderedBroadcastAsUserHistory = ArrayTrackRecord<Intent>().newReadHead()
+
+        fun expectNoDataActivityBroadcast(timeoutMs: Int) {
+            assertNull(orderedBroadcastAsUserHistory.poll(
+                    timeoutMs.toLong()) { intent -> true })
+        }
+
+        override fun sendOrderedBroadcastAsUser(
+                intent: Intent,
+                user: UserHandle,
+                receiverPermission: String?,
+                resultReceiver: BroadcastReceiver?,
+                scheduler: Handler?,
+                initialCode: Int,
+                initialData: String?,
+                initialExtras: Bundle?
+        ) {
+            orderedBroadcastAsUserHistory.add(intent)
+        }
     }
 
     // Utility methods for subclasses to use
@@ -293,7 +343,7 @@
             nc: NetworkCapabilities = defaultNc(),
             nac: NetworkAgentConfig = emptyAgentConfig(nc.getLegacyType()),
             lp: LinkProperties = defaultLp(),
-            lnc: LocalNetworkConfig? = null,
+            lnc: FromS<LocalNetworkConfig>? = null,
             score: FromS<NetworkScore> = defaultScore(),
             provider: NetworkProvider? = null
     ) = CSAgentWrapper(context, deps, csHandlerThread, networkStack,
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 92a5b64..3ed51bc 100644
--- a/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
+++ b/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
@@ -67,6 +67,8 @@
 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 +85,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;
@@ -128,6 +131,7 @@
 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 +173,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;
@@ -193,6 +196,7 @@
  * TODO: This test used to be really brittle because it used Easymock - it uses Mockito now, but
  * still uses the Easymock structure, which could be simplified.
  */
+@DevSdkIgnoreRunner.MonitorThreadLeak
 @RunWith(DevSdkIgnoreRunner.class)
 @SmallTest
 // NetworkStatsService is not updatable before T, so tests do not need to be backwards compatible
@@ -250,7 +254,7 @@
     private @Mock AlarmManager mAlarmManager;
     @Mock
     private NetworkStatsSubscriptionsMonitor mNetworkStatsSubscriptionsMonitor;
-    private @Mock BpfInterfaceMapUpdater mBpfInterfaceMapUpdater;
+    private @Mock BpfInterfaceMapHelper mBpfInterfaceMapHelper;
     private HandlerThread mHandlerThread;
     @Mock
     private LocationPermissionChecker mLocationPermissionChecker;
@@ -283,9 +287,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;
@@ -368,7 +377,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
@@ -383,7 +391,7 @@
             }
         };
         mService = new NetworkStatsService(mServiceContext, mNetd, mAlarmManager, wakeLock,
-                mClock, mSettings, mStatsFactory, statsObservers, deps);
+                mClock, mSettings, mStatsFactory, statsObservers, mDeps);
 
         mElapsedRealtime = 0L;
 
@@ -422,132 +430,149 @@
         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
@@ -2165,6 +2190,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();
@@ -2229,7 +2319,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) {
@@ -2672,13 +2763,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");
     }
 
@@ -2693,13 +2784,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");
     }
 
@@ -2712,4 +2803,16 @@
         final String dump = getDump();
         assertDumpContains(dump, pollReasonNameOf(POLL_REASON_RAT_CHANGED));
     }
+
+    @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/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/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
new file mode 100644
index 0000000..fcfd469
--- /dev/null
+++ b/thread/demoapp/Android.bp
@@ -0,0 +1,40 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+    default_team: "trendy_team_fwk_thread_network",
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_app {
+    name: "ThreadNetworkDemoApp",
+    srcs: ["java/**/*.java"],
+    min_sdk_version: "34",
+    resource_dirs: ["res"],
+    static_libs: [
+        "androidx-constraintlayout_constraintlayout",
+        "androidx.appcompat_appcompat",
+        "androidx.navigation_navigation-common",
+        "androidx.navigation_navigation-fragment",
+        "androidx.navigation_navigation-ui",
+        "com.google.android.material_material",
+        "guava",
+    ],
+    libs: [
+        "framework-connectivity-t",
+    ],
+    certificate: "platform",
+    privileged: true,
+    platform_apis: true,
+}
diff --git a/thread/demoapp/AndroidManifest.xml b/thread/demoapp/AndroidManifest.xml
new file mode 100644
index 0000000..c31bb71
--- /dev/null
+++ b/thread/demoapp/AndroidManifest.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2023 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.threadnetwork.demoapp">
+
+    <uses-sdk android:minSdkVersion="34" android:targetSdkVersion="35"/>
+    <uses-feature android:name="android.hardware.threadnetwork" android:required="true" />
+    <uses-permission android:name="android.permission.INTERNET" />
+    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+    <uses-permission android:name="android.permission.THREAD_NETWORK_PRIVILEGED" />
+
+    <application
+        android:label="ThreadNetworkDemoApp"
+        android:theme="@style/Theme.ThreadNetworkDemoApp"
+        android:icon="@mipmap/ic_launcher"
+        android:roundIcon="@mipmap/ic_launcher_round"
+        android:testOnly="true">
+        <activity android:name=".MainActivity" android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+    </application>
+
+</manifest>
diff --git a/thread/demoapp/java/com/android/threadnetwork/demoapp/ConnectivityToolsFragment.java b/thread/demoapp/java/com/android/threadnetwork/demoapp/ConnectivityToolsFragment.java
new file mode 100644
index 0000000..6f616eb
--- /dev/null
+++ b/thread/demoapp/java/com/android/threadnetwork/demoapp/ConnectivityToolsFragment.java
@@ -0,0 +1,317 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.threadnetwork.demoapp;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import android.net.ConnectivityManager;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.net.NetworkRequest;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.AutoCompleteTextView;
+import android.widget.Button;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import androidx.core.content.ContextCompat;
+import androidx.fragment.app.Fragment;
+
+import com.android.threadnetwork.demoapp.concurrent.BackgroundExecutorProvider;
+
+import com.google.android.material.switchmaterial.SwitchMaterial;
+import com.google.android.material.textfield.TextInputEditText;
+import com.google.common.io.CharStreams;
+import com.google.common.net.InetAddresses;
+import com.google.common.util.concurrent.FluentFuture;
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningScheduledExecutorService;
+
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.net.DatagramPacket;
+import java.net.DatagramSocket;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.SocketAddress;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
+
+public final class ConnectivityToolsFragment extends Fragment {
+    private static final String TAG = "ConnectivityTools";
+
+    // This is a mirror of NetworkCapabilities#NET_CAPABILITY_LOCAL_NETWORK which is @hide for now
+    private static final int NET_CAPABILITY_LOCAL_NETWORK = 36;
+
+    private static final Duration PING_TIMEOUT = Duration.ofSeconds(10L);
+    private static final Duration UDP_TIMEOUT = Duration.ofSeconds(10L);
+    private final ListeningScheduledExecutorService mBackgroundExecutor =
+            BackgroundExecutorProvider.getBackgroundExecutor();
+    private final ArrayList<String> mServerIpCandidates = new ArrayList<>();
+    private final ArrayList<String> mServerPortCandidates = new ArrayList<>();
+    private Executor mMainExecutor;
+
+    private ListenableFuture<String> mPingFuture;
+    private ListenableFuture<String> mUdpFuture;
+    private ArrayAdapter<String> mPingServerIpAdapter;
+    private ArrayAdapter<String> mUdpServerIpAdapter;
+    private ArrayAdapter<String> mUdpServerPortAdapter;
+
+    private Network mThreadNetwork;
+    private boolean mBindThreadNetwork = false;
+
+    private void subscribeToThreadNetwork() {
+        ConnectivityManager cm = getActivity().getSystemService(ConnectivityManager.class);
+        cm.registerNetworkCallback(
+                new NetworkRequest.Builder()
+                        .addTransportType(NetworkCapabilities.TRANSPORT_THREAD)
+                        .addCapability(NET_CAPABILITY_LOCAL_NETWORK)
+                        .build(),
+                new ConnectivityManager.NetworkCallback() {
+                    @Override
+                    public void onAvailable(Network network) {
+                        mThreadNetwork = network;
+                    }
+
+                    @Override
+                    public void onLost(Network network) {
+                        mThreadNetwork = network;
+                    }
+                },
+                new Handler(Looper.myLooper()));
+    }
+
+    private static String getPingCommand(String serverIp) {
+        try {
+            InetAddress serverAddress = InetAddresses.forString(serverIp);
+            return (serverAddress instanceof Inet6Address)
+                    ? "/system/bin/ping6"
+                    : "/system/bin/ping";
+        } catch (IllegalArgumentException e) {
+            // The ping command can handle the illegal argument and output error message
+            return "/system/bin/ping6";
+        }
+    }
+
+    @Override
+    public View onCreateView(
+            LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+        return inflater.inflate(R.layout.connectivity_tools_fragment, container, false);
+    }
+
+    @Override
+    public void onViewCreated(View view, Bundle savedInstanceState) {
+        super.onViewCreated(view, savedInstanceState);
+
+        mMainExecutor = ContextCompat.getMainExecutor(getActivity());
+
+        subscribeToThreadNetwork();
+
+        AutoCompleteTextView pingServerIpText = view.findViewById(R.id.ping_server_ip_address_text);
+        mPingServerIpAdapter =
+                new ArrayAdapter<String>(
+                        getActivity(), R.layout.list_server_ip_address_view, mServerIpCandidates);
+        pingServerIpText.setAdapter(mPingServerIpAdapter);
+        TextView pingOutputText = view.findViewById(R.id.ping_output_text);
+        Button pingButton = view.findViewById(R.id.ping_button);
+
+        pingButton.setOnClickListener(
+                v -> {
+                    if (mPingFuture != null) {
+                        mPingFuture.cancel(/* mayInterruptIfRunning= */ true);
+                        mPingFuture = null;
+                    }
+
+                    String serverIp = pingServerIpText.getText().toString().strip();
+                    updateServerIpCandidates(serverIp);
+                    pingOutputText.setText("Sending ping message to " + serverIp + "\n");
+
+                    mPingFuture = sendPing(serverIp);
+                    Futures.addCallback(
+                            mPingFuture,
+                            new FutureCallback<String>() {
+                                @Override
+                                public void onSuccess(String result) {
+                                    pingOutputText.append(result + "\n");
+                                }
+
+                                @Override
+                                public void onFailure(Throwable t) {
+                                    if (t instanceof CancellationException) {
+                                        // Ignore the cancellation error
+                                        return;
+                                    }
+                                    pingOutputText.append("Failed: " + t.getMessage() + "\n");
+                                }
+                            },
+                            mMainExecutor);
+                });
+
+        AutoCompleteTextView udpServerIpText = view.findViewById(R.id.udp_server_ip_address_text);
+        mUdpServerIpAdapter =
+                new ArrayAdapter<String>(
+                        getActivity(), R.layout.list_server_ip_address_view, mServerIpCandidates);
+        udpServerIpText.setAdapter(mUdpServerIpAdapter);
+        AutoCompleteTextView udpServerPortText = view.findViewById(R.id.udp_server_port_text);
+        mUdpServerPortAdapter =
+                new ArrayAdapter<String>(
+                        getActivity(), R.layout.list_server_port_view, mServerPortCandidates);
+        udpServerPortText.setAdapter(mUdpServerPortAdapter);
+        TextInputEditText udpMsgText = view.findViewById(R.id.udp_message_text);
+        TextView udpOutputText = view.findViewById(R.id.udp_output_text);
+
+        SwitchMaterial switchBindThreadNetwork = view.findViewById(R.id.switch_bind_thread_network);
+        switchBindThreadNetwork.setChecked(mBindThreadNetwork);
+        switchBindThreadNetwork.setOnCheckedChangeListener(
+                (buttonView, isChecked) -> {
+                    if (isChecked) {
+                        Log.i(TAG, "Binding to the Thread network");
+
+                        if (mThreadNetwork == null) {
+                            Log.e(TAG, "Thread network is not available");
+                            Toast.makeText(
+                                    getActivity().getApplicationContext(),
+                                    "Thread network is not available",
+                                    Toast.LENGTH_LONG);
+                            switchBindThreadNetwork.setChecked(false);
+                        } else {
+                            mBindThreadNetwork = true;
+                        }
+                    } else {
+                        mBindThreadNetwork = false;
+                    }
+                });
+
+        Button sendUdpButton = view.findViewById(R.id.send_udp_button);
+        sendUdpButton.setOnClickListener(
+                v -> {
+                    if (mUdpFuture != null) {
+                        mUdpFuture.cancel(/* mayInterruptIfRunning= */ true);
+                        mUdpFuture = null;
+                    }
+
+                    String serverIp = udpServerIpText.getText().toString().strip();
+                    String serverPort = udpServerPortText.getText().toString().strip();
+                    String udpMsg = udpMsgText.getText().toString().strip();
+                    updateServerIpCandidates(serverIp);
+                    updateServerPortCandidates(serverPort);
+                    udpOutputText.setText(
+                            String.format(
+                                    "Sending UDP message \"%s\" to [%s]:%s",
+                                    udpMsg, serverIp, serverPort));
+
+                    mUdpFuture = sendUdpMessage(serverIp, serverPort, udpMsg);
+                    Futures.addCallback(
+                            mUdpFuture,
+                            new FutureCallback<String>() {
+                                @Override
+                                public void onSuccess(String result) {
+                                    udpOutputText.append("\n" + result);
+                                }
+
+                                @Override
+                                public void onFailure(Throwable t) {
+                                    if (t instanceof CancellationException) {
+                                        // Ignore the cancellation error
+                                        return;
+                                    }
+                                    udpOutputText.append("\nFailed: " + t.getMessage());
+                                }
+                            },
+                            mMainExecutor);
+                });
+    }
+
+    private void updateServerIpCandidates(String newServerIp) {
+        if (!mServerIpCandidates.contains(newServerIp)) {
+            mServerIpCandidates.add(0, newServerIp);
+            mPingServerIpAdapter.notifyDataSetChanged();
+            mUdpServerIpAdapter.notifyDataSetChanged();
+        }
+    }
+
+    private void updateServerPortCandidates(String newServerPort) {
+        if (!mServerPortCandidates.contains(newServerPort)) {
+            mServerPortCandidates.add(0, newServerPort);
+            mUdpServerPortAdapter.notifyDataSetChanged();
+        }
+    }
+
+    private ListenableFuture<String> sendPing(String serverIp) {
+        return FluentFuture.from(Futures.submit(() -> doSendPing(serverIp), mBackgroundExecutor))
+                .withTimeout(PING_TIMEOUT.getSeconds(), TimeUnit.SECONDS, mBackgroundExecutor);
+    }
+
+    private String doSendPing(String serverIp) throws IOException {
+        String pingCommand = getPingCommand(serverIp);
+        Process process =
+                new ProcessBuilder()
+                        .command(pingCommand, "-c 1", serverIp)
+                        .redirectErrorStream(true)
+                        .start();
+
+        return CharStreams.toString(new InputStreamReader(process.getInputStream()));
+    }
+
+    private ListenableFuture<String> sendUdpMessage(
+            String serverIp, String serverPort, String msg) {
+        return FluentFuture.from(
+                        Futures.submit(
+                                () -> doSendUdpMessage(serverIp, serverPort, msg),
+                                mBackgroundExecutor))
+                .withTimeout(UDP_TIMEOUT.getSeconds(), TimeUnit.SECONDS, mBackgroundExecutor);
+    }
+
+    private String doSendUdpMessage(String serverIp, String serverPort, String msg)
+            throws IOException {
+        SocketAddress serverAddr = new InetSocketAddress(serverIp, Integer.parseInt(serverPort));
+
+        try (DatagramSocket socket = new DatagramSocket()) {
+            if (mBindThreadNetwork && mThreadNetwork != null) {
+                mThreadNetwork.bindSocket(socket);
+                Log.i(TAG, "Successfully bind the socket to the Thread network");
+            }
+
+            socket.connect(serverAddr);
+            Log.d(TAG, "connected " + serverAddr);
+
+            byte[] msgBytes = msg.getBytes();
+            DatagramPacket packet = new DatagramPacket(msgBytes, msgBytes.length);
+
+            Log.d(TAG, String.format("Sending message to server %s: %s", serverAddr, msg));
+            socket.send(packet);
+            Log.d(TAG, "Send done");
+
+            Log.d(TAG, "Waiting for server reply");
+            socket.receive(packet);
+            return new String(packet.getData(), packet.getOffset(), packet.getLength(), UTF_8);
+        }
+    }
+}
diff --git a/thread/demoapp/java/com/android/threadnetwork/demoapp/MainActivity.java b/thread/demoapp/java/com/android/threadnetwork/demoapp/MainActivity.java
new file mode 100644
index 0000000..ef97a6c
--- /dev/null
+++ b/thread/demoapp/java/com/android/threadnetwork/demoapp/MainActivity.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.threadnetwork.demoapp;
+
+import android.content.Intent;
+import android.os.Bundle;
+
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.appcompat.widget.Toolbar;
+import androidx.drawerlayout.widget.DrawerLayout;
+import androidx.navigation.NavController;
+import androidx.navigation.fragment.NavHostFragment;
+import androidx.navigation.ui.AppBarConfiguration;
+import androidx.navigation.ui.NavigationUI;
+
+import com.google.android.material.navigation.NavigationView;
+
+public final class MainActivity extends AppCompatActivity {
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.main_activity);
+
+        NavHostFragment navHostFragment =
+                (NavHostFragment)
+                        getSupportFragmentManager().findFragmentById(R.id.nav_host_fragment);
+
+        NavController navController = navHostFragment.getNavController();
+
+        DrawerLayout drawerLayout = findViewById(R.id.drawer_layout);
+        Toolbar topAppBar = findViewById(R.id.top_app_bar);
+        AppBarConfiguration appBarConfig =
+                new AppBarConfiguration.Builder(navController.getGraph())
+                        .setOpenableLayout(drawerLayout)
+                        .build();
+
+        NavigationUI.setupWithNavController(topAppBar, navController, appBarConfig);
+
+        NavigationView navView = findViewById(R.id.nav_view);
+        NavigationUI.setupWithNavController(navView, navController);
+    }
+
+    @Override
+    protected void onActivityResult(int request, int result, Intent data) {
+        super.onActivityResult(request, result, data);
+    }
+}
diff --git a/thread/demoapp/java/com/android/threadnetwork/demoapp/ThreadNetworkSettingsFragment.java b/thread/demoapp/java/com/android/threadnetwork/demoapp/ThreadNetworkSettingsFragment.java
new file mode 100644
index 0000000..e95feaf
--- /dev/null
+++ b/thread/demoapp/java/com/android/threadnetwork/demoapp/ThreadNetworkSettingsFragment.java
@@ -0,0 +1,277 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.threadnetwork.demoapp;
+
+import static com.google.common.io.BaseEncoding.base16;
+
+import android.net.ConnectivityManager;
+import android.net.LinkAddress;
+import android.net.LinkProperties;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.net.NetworkRequest;
+import android.net.RouteInfo;
+import android.net.thread.ActiveOperationalDataset;
+import android.net.thread.OperationalDatasetTimestamp;
+import android.net.thread.PendingOperationalDataset;
+import android.net.thread.ThreadNetworkController;
+import android.net.thread.ThreadNetworkException;
+import android.net.thread.ThreadNetworkManager;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.OutcomeReceiver;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.TextView;
+
+import androidx.core.content.ContextCompat;
+import androidx.fragment.app.Fragment;
+
+import java.time.Duration;
+import java.time.Instant;
+import java.util.concurrent.Executor;
+
+public final class ThreadNetworkSettingsFragment extends Fragment {
+    private static final String TAG = "ThreadNetworkSettings";
+
+    // This is a mirror of NetworkCapabilities#NET_CAPABILITY_LOCAL_NETWORK which is @hide for now
+    private static final int NET_CAPABILITY_LOCAL_NETWORK = 36;
+
+    private ThreadNetworkController mThreadController;
+    private TextView mTextState;
+    private TextView mTextNetworkInfo;
+    private TextView mMigrateNetworkState;
+    private Executor mMainExecutor;
+
+    private int mDeviceRole;
+    private long mPartitionId;
+    private ActiveOperationalDataset mActiveDataset;
+
+    private static final byte[] DEFAULT_ACTIVE_DATASET_TLVS =
+            base16().lowerCase()
+                    .decode(
+                            "0e080000000000010000000300001235060004001fffe00208dae21bccb8c321c40708fdc376ead74396bb0510c52f56cd2d38a9eb7a716954f8efd939030f4f70656e5468726561642d646231390102db190410fcb737e6fd6bb1b0fed524a4496363110c0402a0f7f8");
+    private static final ActiveOperationalDataset DEFAULT_ACTIVE_DATASET =
+            ActiveOperationalDataset.fromThreadTlvs(DEFAULT_ACTIVE_DATASET_TLVS);
+
+    private static String deviceRoleToString(int mDeviceRole) {
+        switch (mDeviceRole) {
+            case ThreadNetworkController.DEVICE_ROLE_STOPPED:
+                return "Stopped";
+            case ThreadNetworkController.DEVICE_ROLE_DETACHED:
+                return "Detached";
+            case ThreadNetworkController.DEVICE_ROLE_CHILD:
+                return "Child";
+            case ThreadNetworkController.DEVICE_ROLE_ROUTER:
+                return "Router";
+            case ThreadNetworkController.DEVICE_ROLE_LEADER:
+                return "Leader";
+            default:
+                return "Unknown";
+        }
+    }
+
+    @Override
+    public View onCreateView(
+            LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+        return inflater.inflate(R.layout.thread_network_settings_fragment, container, false);
+    }
+
+    @Override
+    public void onViewCreated(View view, Bundle savedInstanceState) {
+        super.onViewCreated(view, savedInstanceState);
+
+        ConnectivityManager cm = getActivity().getSystemService(ConnectivityManager.class);
+        cm.registerNetworkCallback(
+                new NetworkRequest.Builder()
+                        .addTransportType(NetworkCapabilities.TRANSPORT_THREAD)
+                        .addCapability(NET_CAPABILITY_LOCAL_NETWORK)
+                        .build(),
+                new ConnectivityManager.NetworkCallback() {
+                    @Override
+                    public void onAvailable(Network network) {
+                        Log.i(TAG, "New Thread network is available");
+                    }
+
+                    @Override
+                    public void onLinkPropertiesChanged(
+                            Network network, LinkProperties linkProperties) {
+                        updateNetworkInfo(linkProperties);
+                    }
+
+                    @Override
+                    public void onLost(Network network) {
+                        Log.i(TAG, "Thread network " + network + " is lost");
+                        updateNetworkInfo(null /* linkProperties */);
+                    }
+                },
+                new Handler(Looper.myLooper()));
+
+        mMainExecutor = ContextCompat.getMainExecutor(getActivity());
+        ThreadNetworkManager threadManager =
+                getActivity().getSystemService(ThreadNetworkManager.class);
+        if (threadManager != null) {
+            mThreadController = threadManager.getAllThreadNetworkControllers().get(0);
+            mThreadController.registerStateCallback(
+                    mMainExecutor,
+                    new ThreadNetworkController.StateCallback() {
+                        @Override
+                        public void onDeviceRoleChanged(int mDeviceRole) {
+                            ThreadNetworkSettingsFragment.this.mDeviceRole = mDeviceRole;
+                            updateState();
+                        }
+
+                        @Override
+                        public void onPartitionIdChanged(long mPartitionId) {
+                            ThreadNetworkSettingsFragment.this.mPartitionId = mPartitionId;
+                            updateState();
+                        }
+                    });
+            mThreadController.registerOperationalDatasetCallback(
+                    mMainExecutor,
+                    newActiveDataset -> {
+                        this.mActiveDataset = newActiveDataset;
+                        updateState();
+                    });
+        }
+
+        mTextState = (TextView) view.findViewById(R.id.text_state);
+        mTextNetworkInfo = (TextView) view.findViewById(R.id.text_network_info);
+
+        if (mThreadController == null) {
+            mTextState.setText("Thread not supported!");
+            return;
+        }
+
+        ((Button) view.findViewById(R.id.button_join_network)).setOnClickListener(v -> doJoin());
+        ((Button) view.findViewById(R.id.button_leave_network)).setOnClickListener(v -> doLeave());
+
+        mMigrateNetworkState = view.findViewById(R.id.text_migrate_network_state);
+        ((Button) view.findViewById(R.id.button_migrate_network))
+                .setOnClickListener(v -> doMigration());
+
+        updateState();
+    }
+
+    private void doJoin() {
+        mThreadController.join(
+                DEFAULT_ACTIVE_DATASET,
+                mMainExecutor,
+                new OutcomeReceiver<Void, ThreadNetworkException>() {
+                    @Override
+                    public void onError(ThreadNetworkException error) {
+                        Log.e(TAG, "Failed to join network " + DEFAULT_ACTIVE_DATASET, error);
+                    }
+
+                    @Override
+                    public void onResult(Void v) {
+                        Log.i(TAG, "Successfully Joined");
+                    }
+                });
+    }
+
+    private void doLeave() {
+        mThreadController.leave(
+                mMainExecutor,
+                new OutcomeReceiver<>() {
+                    @Override
+                    public void onError(ThreadNetworkException error) {
+                        Log.e(TAG, "Failed to leave network " + DEFAULT_ACTIVE_DATASET, error);
+                    }
+
+                    @Override
+                    public void onResult(Void v) {
+                        Log.i(TAG, "Successfully Left");
+                    }
+                });
+    }
+
+    private void doMigration() {
+        var newActiveDataset =
+                new ActiveOperationalDataset.Builder(DEFAULT_ACTIVE_DATASET)
+                        .setNetworkName("NewThreadNet")
+                        .setActiveTimestamp(OperationalDatasetTimestamp.fromInstant(Instant.now()))
+                        .build();
+        var pendingDataset =
+                new PendingOperationalDataset(
+                        newActiveDataset,
+                        OperationalDatasetTimestamp.fromInstant(Instant.now()),
+                        Duration.ofSeconds(30));
+        mThreadController.scheduleMigration(
+                pendingDataset,
+                mMainExecutor,
+                new OutcomeReceiver<Void, ThreadNetworkException>() {
+                    @Override
+                    public void onResult(Void v) {
+                        mMigrateNetworkState.setText(
+                                "Scheduled migration to network \"NewThreadNet\" in 30s");
+                        // TODO: update Pending Dataset state
+                    }
+
+                    @Override
+                    public void onError(ThreadNetworkException e) {
+                        mMigrateNetworkState.setText(
+                                "Failed to schedule migration: " + e.getMessage());
+                    }
+                });
+    }
+
+    private void updateState() {
+        Log.i(
+                TAG,
+                String.format(
+                        "Updating Thread states (mDeviceRole: %s)",
+                        deviceRoleToString(mDeviceRole)));
+
+        String state =
+                String.format(
+                        "Role             %s\n"
+                                + "Partition ID     %d\n"
+                                + "Network Name     %s\n"
+                                + "Extended PAN ID  %s",
+                        deviceRoleToString(mDeviceRole),
+                        mPartitionId,
+                        mActiveDataset != null ? mActiveDataset.getNetworkName() : null,
+                        mActiveDataset != null
+                                ? base16().encode(mActiveDataset.getExtendedPanId())
+                                : null);
+        mTextState.setText(state);
+    }
+
+    private void updateNetworkInfo(LinkProperties linProperties) {
+        if (linProperties == null) {
+            mTextNetworkInfo.setText("");
+            return;
+        }
+
+        StringBuilder sb = new StringBuilder("Interface name:\n");
+        sb.append(linProperties.getInterfaceName() + "\n");
+        sb.append("Addresses:\n");
+        for (LinkAddress la : linProperties.getLinkAddresses()) {
+            sb.append(la + "\n");
+        }
+        sb.append("Routes:\n");
+        for (RouteInfo route : linProperties.getRoutes()) {
+            sb.append(route + "\n");
+        }
+        mTextNetworkInfo.setText(sb.toString());
+    }
+}
diff --git a/thread/demoapp/java/com/android/threadnetwork/demoapp/concurrent/BackgroundExecutorProvider.java b/thread/demoapp/java/com/android/threadnetwork/demoapp/concurrent/BackgroundExecutorProvider.java
new file mode 100644
index 0000000..d05ba73
--- /dev/null
+++ b/thread/demoapp/java/com/android/threadnetwork/demoapp/concurrent/BackgroundExecutorProvider.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.threadnetwork.demoapp.concurrent;
+
+import androidx.annotation.GuardedBy;
+
+import com.google.common.util.concurrent.ListeningScheduledExecutorService;
+import com.google.common.util.concurrent.MoreExecutors;
+
+import java.util.concurrent.Executors;
+
+/** Provides executors for executing tasks in background. */
+public final class BackgroundExecutorProvider {
+    private static final int CONCURRENCY = 4;
+
+    @GuardedBy("BackgroundExecutorProvider.class")
+    private static ListeningScheduledExecutorService backgroundExecutor;
+
+    private BackgroundExecutorProvider() {}
+
+    public static synchronized ListeningScheduledExecutorService getBackgroundExecutor() {
+        if (backgroundExecutor == null) {
+            backgroundExecutor =
+                    MoreExecutors.listeningDecorator(
+                            Executors.newScheduledThreadPool(/* maxConcurrency= */ CONCURRENCY));
+        }
+        return backgroundExecutor;
+    }
+}
diff --git a/thread/demoapp/res/drawable/ic_launcher_foreground.xml b/thread/demoapp/res/drawable/ic_launcher_foreground.xml
new file mode 100644
index 0000000..4dd8163
--- /dev/null
+++ b/thread/demoapp/res/drawable/ic_launcher_foreground.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2023 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<vector
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:width="108dp"
+    android:height="108dp"
+    android:viewportWidth="108"
+    android:viewportHeight="108">
+  <group android:scaleX="0.0612"
+      android:scaleY="0.0612"
+      android:translateX="23.4"
+      android:translateY="23.683332">
+    <path
+        android:pathData="M0,0h1000v1000h-1000z"
+        android:fillColor="#00FFCEC7"/>
+    <path
+        android:pathData="m630.6,954.5l-113.5,0l0,-567.2l-170.5,0c-50.6,0 -92,41.2 -92,91.9c0,50.6 41.4,91.8 92,91.8l0,113.5c-113.3,0 -205.5,-92.1 -205.5,-205.4c0,-113.3 92.2,-205.5 205.5,-205.5l170.5,0l0,-57.5c0,-94.2 76.7,-171 171.1,-171c94.2,0 170.8,76.7 170.8,171c0,94.2 -76.6,171 -170.8,171l-57.6,0l0,567.2zM630.6,273.9l57.6,0c31.7,0 57.3,-25.8 57.3,-57.5c0,-31.7 -25.7,-57.5 -57.3,-57.5c-31.8,0 -57.6,25.8 -57.6,57.5l0,57.5z"
+        android:strokeLineJoin="miter"
+        android:strokeWidth="0"
+        android:fillColor="#000000"
+        android:fillType="nonZero"
+        android:strokeColor="#00000000"
+        android:strokeLineCap="butt"/>
+  </group>
+</vector>
diff --git a/thread/demoapp/res/drawable/ic_menu_24dp.xml b/thread/demoapp/res/drawable/ic_menu_24dp.xml
new file mode 100644
index 0000000..8a4cf80
--- /dev/null
+++ b/thread/demoapp/res/drawable/ic_menu_24dp.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2023 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="24.0dp"
+        android:height="24.0dp"
+        android:viewportWidth="24.0"
+        android:viewportHeight="24.0"
+        android:tint="?android:attr/colorControlNormal">
+    <path
+        android:fillColor="#FFFFFFFF"
+        android:pathData="M3.0,18.0l18.0,0.0l0.0,-2.0L3.0,16.0l0.0,2.0zm0.0,-5.0l18.0,0.0l0.0,-2.0L3.0,11.0l0.0,2.0zm0.0,-7.0l0.0,2.0l18.0,0.0L21.0,6.0L3.0,6.0z"/>
+</vector>
diff --git a/thread/demoapp/res/drawable/ic_thread_wordmark.xml b/thread/demoapp/res/drawable/ic_thread_wordmark.xml
new file mode 100644
index 0000000..babaf54
--- /dev/null
+++ b/thread/demoapp/res/drawable/ic_thread_wordmark.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2023 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="167dp"
+    android:height="31dp"
+    android:viewportWidth="167"
+    android:viewportHeight="31">
+  <path
+      android:pathData="m32.413,7.977 l3.806,0 0,9.561 11.48,0 0,-9.561 3.837,0 0,22.957 -3.837,0 0,-9.558 -11.48,0 0,9.558 -3.806,0 0,-22.957z"
+      android:fillColor="#ffffff"/>
+  <path
+      android:pathData="m76.761,30.934 l-4.432,-7.641 -6.483,0 0,7.641 -3.807,0 0,-22.957 11.48,0c2.095,0 3.894,0.75 5.392,2.246 1.501,1.504 2.249,3.298 2.249,5.392 0,1.591 -0.453,3.034 -1.356,4.335 -0.885,1.279 -2.006,2.193 -3.376,2.747l4.732,8.236 -4.4,0zM73.519,11.812l-7.673,0 0,7.645 7.673,0c1.034,0 1.928,-0.379 2.678,-1.124 0.75,-0.752 1.126,-1.657 1.126,-2.717 0,-1.034 -0.376,-1.926 -1.126,-2.678C75.448,12.188 74.554,11.812 73.519,11.812Z"
+      android:fillColor="#ffffff"/>
+  <path
+      android:pathData="m106.945,7.977 l0,3.835 -11.478,0 0,5.757 11.478,0 0,3.807 -11.478,0 0,5.722 11.478,0 0,3.836 -15.277,0 0,-22.957 15.277,0z"
+      android:fillColor="#ffffff"/>
+  <path
+      android:pathData="m132.325,27.08 l-10.586,0 -1.958,3.854 -4.283,0 11.517,-23.519 11.522,23.519 -4.283,0 -1.928,-3.854zM123.627,23.267 L130.404,23.267 127.014,16.013 123.627,23.267z"
+      android:fillColor="#ffffff"/>
+  <path
+      android:pathData="m146.606,7.977 l7.638,0c1.569,0 3.044,0.304 4.436,0.907 1.387,0.609 2.608,1.437 3.656,2.485 1.047,1.047 1.869,2.266 2.479,3.653 0.609,1.391 0.909,2.866 0.909,4.435 0,1.563 -0.299,3.041 -0.909,4.432 -0.61,1.391 -1.425,2.608 -2.464,3.654 -1.037,1.05 -2.256,1.874 -3.656,2.48 -1.401,0.607 -2.882,0.91 -4.451,0.91l-7.638,0 0,-22.956zM154.244,27.098c1.06,0 2.054,-0.199 2.978,-0.599 0.925,-0.394 1.737,-0.945 2.432,-1.654 0.696,-0.702 1.241,-1.521 1.638,-2.446 0.397,-0.925 0.597,-1.907 0.597,-2.942 0,-1.037 -0.201,-2.02 -0.597,-2.948 -0.397,-0.925 -0.946,-1.737 -1.651,-2.447 -0.709,-0.703 -1.524,-1.256 -2.45,-1.653 -0.925,-0.397 -1.907,-0.597 -2.948,-0.597l-3.834,0 0,15.286 3.834,0z"
+      android:fillColor="#ffffff"/>
+  <path
+      android:pathData="m16.491,30.934 l-3.828,0 0,-19.128 -5.749,0c-1.705,0 -3.102,1.391 -3.102,3.1 0,1.706 1.397,3.097 3.102,3.097l0,3.83c-3.821,0 -6.931,-3.106 -6.931,-6.926 0,-3.822 3.111,-6.929 6.931,-6.929l5.749,0 0,-1.938c0,-3.179 2.587,-5.766 5.77,-5.766 3.175,0 5.76,2.588 5.76,5.766 0,3.179 -2.584,5.766 -5.76,5.766l-1.942,0 0,19.128zM16.491,7.977 L18.433,7.977c1.069,0 1.934,-0.869 1.934,-1.938 0,-1.069 -0.865,-1.938 -1.934,-1.938 -1.072,0 -1.942,0.869 -1.942,1.938l0,1.938z"
+      android:fillColor="#ffffff"/>
+</vector>
diff --git a/thread/demoapp/res/layout/connectivity_tools_fragment.xml b/thread/demoapp/res/layout/connectivity_tools_fragment.xml
new file mode 100644
index 0000000..a1aa0d4
--- /dev/null
+++ b/thread/demoapp/res/layout/connectivity_tools_fragment.xml
@@ -0,0 +1,137 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2023 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<ScrollView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    tools:context=".ConnectivityToolsFragment" >
+
+<LinearLayout
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:padding="8dp"
+    android:paddingBottom="16dp"
+    android:orientation="vertical">
+
+    <com.google.android.material.textfield.TextInputLayout
+        android:id="@+id/ping_server_ip_address_layout"
+        style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="8dp"
+        android:hint="Server IP Address">
+        <AutoCompleteTextView
+            android:id="@+id/ping_server_ip_address_text"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="fdde:ad00:beef::ff:fe00:7400"
+            android:textSize="14sp"/>
+    </com.google.android.material.textfield.TextInputLayout>
+
+    <Button
+        android:id="@+id/ping_button"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="8dp"
+        android:text="Ping"
+        android:textSize="20dp"/>
+
+    <TextView
+        android:id="@+id/ping_output_text"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="8dp"
+        android:scrollbars="vertical"
+        android:textIsSelectable="true"/>
+
+    <LinearLayout
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="16dp"
+        android:orientation="horizontal" >
+
+        <com.google.android.material.textfield.TextInputLayout
+            android:id="@+id/udp_server_ip_address_layout"
+            style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:hint="Server IP Address">
+            <AutoCompleteTextView
+                android:id="@+id/udp_server_ip_address_text"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:text="fdde:ad00:beef::ff:fe00:7400"
+                android:textSize="14sp"/>
+        </com.google.android.material.textfield.TextInputLayout>
+
+        <com.google.android.material.textfield.TextInputLayout
+            android:id="@+id/udp_server_port_layout"
+            style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginLeft="2dp"
+            android:hint="Server Port">
+            <AutoCompleteTextView
+                android:id="@+id/udp_server_port_text"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:inputType="number"
+                android:text="12345"
+                android:textSize="14sp"/>
+        </com.google.android.material.textfield.TextInputLayout>
+    </LinearLayout>
+
+    <com.google.android.material.textfield.TextInputLayout
+        android:id="@+id/udp_message_layout"
+        style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="8dp"
+        android:hint="UDP Message">
+        <com.google.android.material.textfield.TextInputEditText
+            android:id="@+id/udp_message_text"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="Hello Thread!"
+            android:textSize="14sp"/>
+    </com.google.android.material.textfield.TextInputLayout>
+
+    <com.google.android.material.switchmaterial.SwitchMaterial
+        android:id="@+id/switch_bind_thread_network"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:checked="true"
+        android:text="Bind to Thread network" />
+
+    <Button
+        android:id="@+id/send_udp_button"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="8dp"
+        android:text="Send UDP Message"
+        android:textSize="20dp"/>
+
+    <TextView
+        android:id="@+id/udp_output_text"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="8dp"
+        android:scrollbars="vertical"
+        android:textIsSelectable="true"/>
+</LinearLayout>
+</ScrollView>
diff --git a/thread/demoapp/res/layout/list_server_ip_address_view.xml b/thread/demoapp/res/layout/list_server_ip_address_view.xml
new file mode 100644
index 0000000..1a8f02e
--- /dev/null
+++ b/thread/demoapp/res/layout/list_server_ip_address_view.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2023 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<TextView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:padding="8dp"
+    android:ellipsize="end"
+    android:maxLines="1"
+    android:textAppearance="?attr/textAppearanceBody2"
+    />
diff --git a/thread/demoapp/res/layout/list_server_port_view.xml b/thread/demoapp/res/layout/list_server_port_view.xml
new file mode 100644
index 0000000..1a8f02e
--- /dev/null
+++ b/thread/demoapp/res/layout/list_server_port_view.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2023 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<TextView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:padding="8dp"
+    android:ellipsize="end"
+    android:maxLines="1"
+    android:textAppearance="?attr/textAppearanceBody2"
+    />
diff --git a/thread/demoapp/res/layout/main_activity.xml b/thread/demoapp/res/layout/main_activity.xml
new file mode 100644
index 0000000..12072e5
--- /dev/null
+++ b/thread/demoapp/res/layout/main_activity.xml
@@ -0,0 +1,63 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2023 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<androidx.drawerlayout.widget.DrawerLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:id="@+id/drawer_layout"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    tools:context=".MainActivity">
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:orientation="vertical">
+
+        <com.google.android.material.appbar.AppBarLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content">
+
+            <com.google.android.material.appbar.MaterialToolbar
+                android:id="@+id/top_app_bar"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                app:navigationIcon="@drawable/ic_menu_24dp" />
+
+        </com.google.android.material.appbar.AppBarLayout>
+
+        <androidx.fragment.app.FragmentContainerView
+            android:id="@+id/nav_host_fragment"
+            android:name="androidx.navigation.fragment.NavHostFragment"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            app:defaultNavHost="true"
+            app:navGraph="@navigation/nav_graph" />
+
+    </LinearLayout>
+
+    <com.google.android.material.navigation.NavigationView
+        android:id="@+id/nav_view"
+        android:layout_width="wrap_content"
+        android:layout_height="match_parent"
+        android:padding="16dp"
+        android:layout_gravity="start"
+        android:fitsSystemWindows="true"
+        app:headerLayout="@layout/nav_header"
+        app:menu="@menu/nav_menu" />
+
+</androidx.drawerlayout.widget.DrawerLayout>
diff --git a/thread/demoapp/res/layout/nav_header.xml b/thread/demoapp/res/layout/nav_header.xml
new file mode 100644
index 0000000..b91fb9c
--- /dev/null
+++ b/thread/demoapp/res/layout/nav_header.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2023 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:gravity="bottom"
+    android:orientation="vertical" >
+
+  <ImageView
+      android:id="@+id/nav_header_image"
+      android:layout_width="wrap_content"
+      android:layout_height="wrap_content"
+      android:paddingTop="@dimen/nav_header_vertical_spacing"
+      android:src="@drawable/ic_thread_wordmark" />
+</LinearLayout>
diff --git a/thread/demoapp/res/layout/thread_network_settings_fragment.xml b/thread/demoapp/res/layout/thread_network_settings_fragment.xml
new file mode 100644
index 0000000..cae46a3
--- /dev/null
+++ b/thread/demoapp/res/layout/thread_network_settings_fragment.xml
@@ -0,0 +1,71 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2023 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:padding="8dp"
+    android:orientation="vertical"
+    tools:context=".ThreadNetworkSettingsFragment" >
+
+    <Button android:id="@+id/button_join_network"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="Join Network" />
+    <Button android:id="@+id/button_leave_network"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="Leave Network" />
+
+    <TextView
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:textSize="16dp"
+        android:textStyle="bold"
+        android:text="State" />
+    <TextView
+        android:id="@+id/text_state"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:textSize="12dp"
+        android:typeface="monospace" />
+
+    <TextView
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="10dp"
+        android:textSize="16dp"
+        android:textStyle="bold"
+        android:text="Network Info" />
+    <TextView
+        android:id="@+id/text_network_info"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:textSize="12dp" />
+
+    <Button android:id="@+id/button_migrate_network"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="Migrate Network" />
+    <TextView
+        android:id="@+id/text_migrate_network_state"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:textSize="12dp" />
+</LinearLayout>
diff --git a/thread/demoapp/res/menu/nav_menu.xml b/thread/demoapp/res/menu/nav_menu.xml
new file mode 100644
index 0000000..8d036c2
--- /dev/null
+++ b/thread/demoapp/res/menu/nav_menu.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2023 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+    <item
+        android:id="@+id/thread_network_settings"
+        android:title="Thread Network Settings" />
+    <item
+        android:id="@+id/connectivity_tools"
+        android:title="Connectivity Tools" />
+</menu>
diff --git a/thread/demoapp/res/mipmap-anydpi-v26/ic_launcher.xml b/thread/demoapp/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..b111e91
--- /dev/null
+++ b/thread/demoapp/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools">
+    <background android:drawable="@color/white"/>
+    <foreground android:drawable="@drawable/ic_launcher_foreground"/>
+</adaptive-icon>
diff --git a/thread/demoapp/res/mipmap-anydpi-v26/ic_launcher_round.xml b/thread/demoapp/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 0000000..b111e91
--- /dev/null
+++ b/thread/demoapp/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools">
+    <background android:drawable="@color/white"/>
+    <foreground android:drawable="@drawable/ic_launcher_foreground"/>
+</adaptive-icon>
diff --git a/thread/demoapp/res/mipmap-hdpi/ic_launcher.png b/thread/demoapp/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..94e778f
--- /dev/null
+++ b/thread/demoapp/res/mipmap-hdpi/ic_launcher.png
Binary files differ
diff --git a/thread/demoapp/res/mipmap-hdpi/ic_launcher_round.png b/thread/demoapp/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 0000000..074a671
--- /dev/null
+++ b/thread/demoapp/res/mipmap-hdpi/ic_launcher_round.png
Binary files differ
diff --git a/thread/demoapp/res/mipmap-mdpi/ic_launcher.png b/thread/demoapp/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..674e51f
--- /dev/null
+++ b/thread/demoapp/res/mipmap-mdpi/ic_launcher.png
Binary files differ
diff --git a/thread/demoapp/res/mipmap-mdpi/ic_launcher_round.png b/thread/demoapp/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 0000000..4e35c29
--- /dev/null
+++ b/thread/demoapp/res/mipmap-mdpi/ic_launcher_round.png
Binary files differ
diff --git a/thread/demoapp/res/mipmap-xhdpi/ic_launcher.png b/thread/demoapp/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..2ee5d92
--- /dev/null
+++ b/thread/demoapp/res/mipmap-xhdpi/ic_launcher.png
Binary files differ
diff --git a/thread/demoapp/res/mipmap-xhdpi/ic_launcher_round.png b/thread/demoapp/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..78a3b7d
--- /dev/null
+++ b/thread/demoapp/res/mipmap-xhdpi/ic_launcher_round.png
Binary files differ
diff --git a/thread/demoapp/res/mipmap-xxhdpi/ic_launcher.png b/thread/demoapp/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..ffb6261
--- /dev/null
+++ b/thread/demoapp/res/mipmap-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/thread/demoapp/res/mipmap-xxhdpi/ic_launcher_round.png b/thread/demoapp/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..80fa037
--- /dev/null
+++ b/thread/demoapp/res/mipmap-xxhdpi/ic_launcher_round.png
Binary files differ
diff --git a/thread/demoapp/res/mipmap-xxxhdpi/ic_launcher.png b/thread/demoapp/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..5ca1bfe
--- /dev/null
+++ b/thread/demoapp/res/mipmap-xxxhdpi/ic_launcher.png
Binary files differ
diff --git a/thread/demoapp/res/mipmap-xxxhdpi/ic_launcher_round.png b/thread/demoapp/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..2fd92e3
--- /dev/null
+++ b/thread/demoapp/res/mipmap-xxxhdpi/ic_launcher_round.png
Binary files differ
diff --git a/thread/demoapp/res/navigation/nav_graph.xml b/thread/demoapp/res/navigation/nav_graph.xml
new file mode 100644
index 0000000..472d1bb
--- /dev/null
+++ b/thread/demoapp/res/navigation/nav_graph.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2023 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<navigation xmlns:android="http://schemas.android.com/apk/res/android"
+            xmlns:tools="http://schemas.android.com/tools"
+            xmlns:app="http://schemas.android.com/apk/res-auto"
+            android:id="@+id/nav_graph"
+            app:startDestination="@+id/thread_network_settings" >
+    <fragment
+        android:id="@+id/thread_network_settings"
+        android:name=".ThreadNetworkSettingsFragment"
+        android:label="Thread Network Settings"
+        tools:layout="@layout/thread_network_settings_fragment">
+    </fragment>
+
+    <fragment
+        android:id="@+id/connectivity_tools"
+        android:name=".ConnectivityToolsFragment"
+        android:label="Connectivity Tools"
+        tools:layout="@layout/connectivity_tools_fragment">
+    </fragment>
+</navigation>
diff --git a/thread/demoapp/res/values/colors.xml b/thread/demoapp/res/values/colors.xml
new file mode 100644
index 0000000..6a65937
--- /dev/null
+++ b/thread/demoapp/res/values/colors.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2023 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<resources>
+    <color name="purple_200">#FFBB86FC</color>
+    <color name="purple_500">#FF6200EE</color>
+    <color name="purple_700">#FF3700B3</color>
+    <color name="teal_200">#FF03DAC5</color>
+    <color name="teal_700">#FF018786</color>
+    <color name="black">#FF000000</color>
+    <color name="white">#FFFFFFFF</color>
+</resources>
diff --git a/thread/demoapp/res/values/dimens.xml b/thread/demoapp/res/values/dimens.xml
new file mode 100644
index 0000000..5165951
--- /dev/null
+++ b/thread/demoapp/res/values/dimens.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2023 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<resources>
+  <!-- Default screen margins, per the Android Design guidelines. -->
+  <dimen name="activity_horizontal_margin">16dp</dimen>
+  <dimen name="activity_vertical_margin">16dp</dimen>
+  <dimen name="nav_header_vertical_spacing">8dp</dimen>
+  <dimen name="nav_header_height">176dp</dimen>
+  <dimen name="fab_margin">16dp</dimen>
+</resources>
diff --git a/thread/demoapp/res/values/themes.xml b/thread/demoapp/res/values/themes.xml
new file mode 100644
index 0000000..9cb3403
--- /dev/null
+++ b/thread/demoapp/res/values/themes.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2023 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<resources xmlns:tools="http://schemas.android.com/tools">
+    <!-- Base application theme. -->
+    <style name="Theme.ThreadNetworkDemoApp" parent="Theme.MaterialComponents.DayNight.NoActionBar">
+        <!-- Primary brand color. -->
+        <item name="colorPrimary">@color/purple_500</item>
+        <item name="colorPrimaryVariant">@color/purple_700</item>
+        <item name="colorOnPrimary">@color/white</item>
+        <!-- Secondary brand color. -->
+        <item name="colorSecondary">@color/teal_200</item>
+        <item name="colorSecondaryVariant">@color/teal_700</item>
+        <item name="colorOnSecondary">@color/black</item>
+        <!-- Status bar color. -->
+        <item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
+        <!-- Customize your theme here. -->
+    </style>
+</resources>
diff --git a/thread/framework/Android.bp b/thread/framework/Android.bp
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 51e4d88..485e25d 100644
--- a/thread/framework/java/android/net/thread/IThreadNetworkController.aidl
+++ b/thread/framework/java/android/net/thread/IThreadNetworkController.aidl
@@ -16,7 +16,6 @@
 
 package android.net.thread;
 
-import android.net.Network;
 import android.net.thread.ActiveOperationalDataset;
 import android.net.thread.IActiveOperationalDatasetReceiver;
 import android.net.thread.IOperationalDatasetCallback;
@@ -39,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/PendingOperationalDataset.java b/thread/framework/java/android/net/thread/PendingOperationalDataset.java
index 9cfd0b8..c1351af 100644
--- a/thread/framework/java/android/net/thread/PendingOperationalDataset.java
+++ b/thread/framework/java/android/net/thread/PendingOperationalDataset.java
@@ -39,6 +39,7 @@
  * a given delay. This is typically used to deploy new network parameters (e.g. Network Key or
  * Channel) to all devices in the network.
  *
+ * @see ThreadNetworkController#scheduleMigration
  * @hide
  */
 @FlaggedApi(ThreadNetworkFlags.FLAG_THREAD_ENABLED)
@@ -76,7 +77,8 @@
      * @param pendingTimestamp the Pending Timestamp which represents the version of this Pending
      *     Dataset
      * @param delayTimer the delay after when {@code activeOpDataset} will be committed on this
-     *     device
+     *     device; use {@link Duration#ZERO} to tell the system to choose a reasonable value
+     *     automatically
      */
     public PendingOperationalDataset(
             @NonNull ActiveOperationalDataset activeOpDataset,
diff --git a/thread/framework/java/android/net/thread/ThreadNetworkController.java b/thread/framework/java/android/net/thread/ThreadNetworkController.java
index ec39db4..db761a3 100644
--- a/thread/framework/java/android/net/thread/ThreadNetworkController.java
+++ b/thread/framework/java/android/net/thread/ThreadNetworkController.java
@@ -26,10 +26,12 @@
 import android.annotation.Nullable;
 import android.annotation.RequiresPermission;
 import android.annotation.SystemApi;
+import android.os.Binder;
 import android.os.OutcomeReceiver;
 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;
@@ -66,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({
@@ -77,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;
 
@@ -98,11 +116,46 @@
     private final Map<OperationalDatasetCallback, OperationalDatasetCallbackProxy>
             mOpDatasetCallbackMap = new HashMap<>();
 
-    ThreadNetworkController(@NonNull IThreadNetworkController controllerService) {
+    /** @hide */
+    public ThreadNetworkController(@NonNull IThreadNetworkController controllerService) {
         requireNonNull(controllerService, "controllerService cannot be null");
         mControllerService = controllerService;
     }
 
+    /**
+     * Enables/Disables the radio of this ThreadNetworkController. The requested enabled state will
+     * be persistent and survives device reboots.
+     *
+     * <p>When Thread is in {@code STATE_DISABLED}, {@link ThreadNetworkController} APIs which
+     * require the Thread radio will fail with error code {@link
+     * ThreadNetworkException#ERROR_THREAD_DISABLED}. When Thread is in {@code STATE_DISABLING},
+     * {@link ThreadNetworkController} APIs that return a {@link ThreadNetworkException} will fail
+     * with error code {@link ThreadNetworkException#ERROR_BUSY}.
+     *
+     * <p>On success, {@link OutcomeReceiver#onResult} of {@code receiver} is called. It indicates
+     * the operation has completed. But there maybe subsequent calls to update the enabled state,
+     * callers of this method should use {@link #registerStateCallback} to subscribe to the Thread
+     * enabled state changes.
+     *
+     * <p>On failure, {@link OutcomeReceiver#onError} of {@code receiver} will be invoked with a
+     * specific error in {@link ThreadNetworkException#ERROR_}.
+     *
+     * @param enabled {@code true} for enabling Thread
+     * @param executor the executor to execute {@code receiver}
+     * @param receiver the receiver to receive result of this operation
+     */
+    @RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED")
+    public void setEnabled(
+            boolean enabled,
+            @NonNull @CallbackExecutor Executor executor,
+            @NonNull OutcomeReceiver<Void, ThreadNetworkException> receiver) {
+        try {
+            mControllerService.setEnabled(enabled, new OperationReceiverProxy(executor, receiver));
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
     /** Returns the Thread version this device is operating on. */
     @ThreadVersion
     public int getThreadVersion() {
@@ -167,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 {
@@ -180,12 +243,32 @@
 
         @Override
         public void onDeviceRoleChanged(@DeviceRole int deviceRole) {
-            mExecutor.execute(() -> mCallback.onDeviceRoleChanged(deviceRole));
+            final long identity = Binder.clearCallingIdentity();
+            try {
+                mExecutor.execute(() -> mCallback.onDeviceRoleChanged(deviceRole));
+            } finally {
+                Binder.restoreCallingIdentity(identity);
+            }
         }
 
         @Override
         public void onPartitionIdChanged(long partitionId) {
-            mExecutor.execute(() -> mCallback.onPartitionIdChanged(partitionId));
+            final long identity = Binder.clearCallingIdentity();
+            try {
+                mExecutor.execute(() -> mCallback.onPartitionIdChanged(partitionId));
+            } finally {
+                Binder.restoreCallingIdentity(identity);
+            }
+        }
+
+        @Override
+        public void onThreadEnableStateChanged(@EnabledState int enabled) {
+            final long identity = Binder.clearCallingIdentity();
+            try {
+                mExecutor.execute(() -> mCallback.onThreadEnableStateChanged(enabled));
+            } finally {
+                Binder.restoreCallingIdentity(identity);
+            }
         }
     }
 
@@ -226,15 +309,17 @@
      * @param callback the callback which has been registered with {@link #registerStateCallback}
      * @throws IllegalArgumentException if {@code callback} hasn't been registered
      */
+    @RequiresPermission(permission.ACCESS_NETWORK_STATE)
     public void unregisterStateCallback(@NonNull StateCallback callback) {
         requireNonNull(callback, "callback cannot be null");
         synchronized (mStateCallbackMapLock) {
-            StateCallbackProxy callbackProxy = mStateCallbackMap.remove(callback);
+            StateCallbackProxy callbackProxy = mStateCallbackMap.get(callback);
             if (callbackProxy == null) {
                 throw new IllegalArgumentException("callback hasn't been registered");
             }
             try {
                 mControllerService.unregisterStateCallback(callbackProxy);
+                mStateCallbackMap.remove(callback);
             } catch (RemoteException e) {
                 e.rethrowFromSystemServer();
             }
@@ -280,13 +365,24 @@
         @Override
         public void onActiveOperationalDatasetChanged(
                 @Nullable ActiveOperationalDataset activeDataset) {
-            mExecutor.execute(() -> mCallback.onActiveOperationalDatasetChanged(activeDataset));
+            final long identity = Binder.clearCallingIdentity();
+            try {
+                mExecutor.execute(() -> mCallback.onActiveOperationalDatasetChanged(activeDataset));
+            } finally {
+                Binder.restoreCallingIdentity(identity);
+            }
         }
 
         @Override
         public void onPendingOperationalDatasetChanged(
                 @Nullable PendingOperationalDataset pendingDataset) {
-            mExecutor.execute(() -> mCallback.onPendingOperationalDatasetChanged(pendingDataset));
+            final long identity = Binder.clearCallingIdentity();
+            try {
+                mExecutor.execute(
+                        () -> mCallback.onPendingOperationalDatasetChanged(pendingDataset));
+            } finally {
+                Binder.restoreCallingIdentity(identity);
+            }
         }
     }
 
@@ -334,15 +430,21 @@
      *     #registerOperationalDatasetCallback}
      * @throws IllegalArgumentException if {@code callback} hasn't been registered
      */
+    @RequiresPermission(
+            allOf = {
+                permission.ACCESS_NETWORK_STATE,
+                "android.permission.THREAD_NETWORK_PRIVILEGED"
+            })
     public void unregisterOperationalDatasetCallback(@NonNull OperationalDatasetCallback callback) {
         requireNonNull(callback, "callback cannot be null");
         synchronized (mOpDatasetCallbackMapLock) {
-            OperationalDatasetCallbackProxy callbackProxy = mOpDatasetCallbackMap.remove(callback);
+            OperationalDatasetCallbackProxy callbackProxy = mOpDatasetCallbackMap.get(callback);
             if (callbackProxy == null) {
                 throw new IllegalArgumentException("callback hasn't been registered");
             }
             try {
                 mControllerService.unregisterOperationalDatasetCallback(callbackProxy);
+                mOpDatasetCallbackMap.remove(callback);
             } catch (RemoteException e) {
                 e.rethrowFromSystemServer();
             }
@@ -468,12 +570,44 @@
         }
     }
 
+    /**
+     * 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,
             int errorCode,
             String errorMsg) {
-        executor.execute(() -> receiver.onError(new ThreadNetworkException(errorCode, errorMsg)));
+        final long identity = Binder.clearCallingIdentity();
+        try {
+            executor.execute(
+                    () -> receiver.onError(new ThreadNetworkException(errorCode, errorMsg)));
+        } finally {
+            Binder.restoreCallingIdentity(identity);
+        }
     }
 
     private static final class ActiveDatasetReceiverProxy
@@ -490,7 +624,12 @@
 
         @Override
         public void onSuccess(ActiveOperationalDataset dataset) {
-            mExecutor.execute(() -> mResultReceiver.onResult(dataset));
+            final long identity = Binder.clearCallingIdentity();
+            try {
+                mExecutor.execute(() -> mResultReceiver.onResult(dataset));
+            } finally {
+                Binder.restoreCallingIdentity(identity);
+            }
         }
 
         @Override
@@ -512,7 +651,12 @@
 
         @Override
         public void onSuccess() {
-            mExecutor.execute(() -> mResultReceiver.onResult(null));
+            final long identity = Binder.clearCallingIdentity();
+            try {
+                mExecutor.execute(() -> mResultReceiver.onResult(null));
+            } finally {
+                Binder.restoreCallingIdentity(identity);
+            }
         }
 
         @Override
diff --git a/thread/framework/java/android/net/thread/ThreadNetworkException.java b/thread/framework/java/android/net/thread/ThreadNetworkException.java
index c5e1e97..23ed53e 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 {}
 
@@ -122,6 +124,19 @@
      */
     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 final int mErrorCode;
 
     /** Creates a new {@link ThreadNetworkException} object with given error code and message. */
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 bd265e6..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,9 +36,13 @@
     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",
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/ThreadNetworkControllerService.java b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
index 6c9a775..847a224 100644
--- a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
+++ b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
@@ -14,6 +14,11 @@
 
 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;
 import static android.net.thread.ActiveOperationalDataset.LENGTH_EXTENDED_PAN_ID;
 import static android.net.thread.ActiveOperationalDataset.LENGTH_MESH_LOCAL_PREFIX_BITS;
@@ -22,6 +27,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;
@@ -30,9 +38,9 @@
 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.PERMISSION_THREAD_NETWORK_PRIVILEGED;
 
 import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_ABORT;
@@ -44,20 +52,35 @@
 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.Context;
 import android.net.ConnectivityManager;
+import android.net.IpPrefix;
 import android.net.LinkAddress;
 import android.net.LinkProperties;
+import android.net.LocalNetworkConfig;
+import android.net.LocalNetworkInfo;
+import android.net.MulticastRoutingConfig;
+import android.net.Network;
 import android.net.NetworkAgent;
 import android.net.NetworkAgentConfig;
 import android.net.NetworkCapabilities;
 import android.net.NetworkProvider;
+import android.net.NetworkRequest;
 import android.net.NetworkScore;
+import android.net.RouteInfo;
+import android.net.TestNetworkSpecifier;
 import android.net.thread.ActiveOperationalDataset;
 import android.net.thread.ActiveOperationalDataset.SecurityPolicy;
 import android.net.thread.IActiveOperationalDatasetReceiver;
@@ -69,6 +92,8 @@
 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;
@@ -80,6 +105,7 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.ServiceManagerWrapper;
+import com.android.server.thread.openthread.BorderRouterConfigurationParcel;
 import com.android.server.thread.openthread.IOtDaemon;
 import com.android.server.thread.openthread.IOtDaemonCallback;
 import com.android.server.thread.openthread.IOtStatusReceiver;
@@ -87,6 +113,7 @@
 import com.android.server.thread.openthread.OtDaemonState;
 
 import java.io.IOException;
+import java.net.Inet6Address;
 import java.net.InetAddress;
 import java.net.UnknownHostException;
 import java.security.SecureRandom;
@@ -102,10 +129,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";
 
@@ -114,42 +142,61 @@
     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 InfraInterfaceController mInfraIfController;
     private final LinkProperties mLinkProperties = new LinkProperties();
     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
 
-    private IOtDaemon mOtDaemon;
-    private NetworkAgent mNetworkAgent;
+    @Nullable private IOtDaemon mOtDaemon;
+    @Nullable private NetworkAgent mNetworkAgent;
+    @Nullable private NetworkAgent mTestNetworkAgent;
+
+    private MulticastRoutingConfig mUpstreamMulticastRoutingConfig = CONFIG_FORWARD_NONE;
+    private MulticastRoutingConfig mDownstreamMulticastRoutingConfig = CONFIG_FORWARD_NONE;
+    private Network mUpstreamNetwork;
+    private NetworkRequest mUpstreamNetworkRequest;
+    private UpstreamNetworkCallback mUpstreamNetworkCallback;
+    private TestNetworkSpecifier mUpstreamTestNetworkSpecifier;
+    private final HashMap<Network, String> mNetworkToInterface;
+    private final ThreadPersistentSettings mPersistentSettings;
+
+    private 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) {
         mContext = context;
-        mHandlerThread = handlerThread;
-        mHandler = new Handler(handlerThread.getLooper());
+        mHandler = handler;
         mNetworkProvider = networkProvider;
         mOtDaemonSupplier = otDaemonSupplier;
         mConnectivityManager = connectivityManager;
         mTunIfController = tunIfController;
+        mInfraIfController = infraIfController;
+        mUpstreamNetworkRequest = newUpstreamNetworkRequest();
+        mNetworkToInterface = new HashMap<Network, String>();
+        mBorderRouterConfig = new BorderRouterConfigurationParcel();
+        mPersistentSettings = persistentSettings;
     }
 
-    public static ThreadNetworkControllerService newInstance(Context context) {
+    public static ThreadNetworkControllerService newInstance(
+            Context context, ThreadPersistentSettings persistentSettings) {
         HandlerThread handlerThread = new HandlerThread("ThreadHandlerThread");
         handlerThread.start();
         NetworkProvider networkProvider =
@@ -157,27 +204,26 @@
 
         return new ThreadNetworkControllerService(
                 context,
-                handlerThread,
+                new Handler(handlerThread.getLooper()),
                 networkProvider,
                 () -> IOtDaemon.Stub.asInterface(ServiceManagerWrapper.waitForService("ot_daemon")),
                 context.getSystemService(ConnectivityManager.class),
-                new TunInterfaceController(TUN_IF_NAME));
+                new TunInterfaceController(TUN_IF_NAME),
+                new InfraInterfaceController(),
+                persistentSettings);
     }
 
-    private static NetworkCapabilities newNetworkCapabilities() {
-        return new NetworkCapabilities.Builder()
-                .addTransportType(NetworkCapabilities.TRANSPORT_THREAD)
-                .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED)
-                .build();
+    private static Inet6Address bytesToInet6Address(byte[] addressBytes) {
+        try {
+            return (Inet6Address) Inet6Address.getByAddress(addressBytes);
+        } catch (UnknownHostException e) {
+            // This is unlikely to happen unless the Thread daemon is critically broken
+            return null;
+        }
     }
 
     private static InetAddress addressInfoToInetAddress(Ipv6AddressInfo addressInfo) {
-        try {
-            return InetAddress.getByAddress(addressInfo.address);
-        } catch (UnknownHostException e) {
-            // This is impossible unless the Thread daemon is critically broken
-            return null;
-        }
+        return bytesToInet6Address(addressInfo.address);
     }
 
     private static LinkAddress newLinkAddress(Ipv6AddressInfo addressInfo) {
@@ -199,6 +245,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();
@@ -208,6 +276,8 @@
     }
 
     private IOtDaemon getOtDaemon() throws RemoteException {
+        checkOnHandlerThread();
+
         if (mOtDaemon != null) {
             return mOtDaemon;
         }
@@ -216,13 +286,16 @@
         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(),
+                mPersistentSettings.get(ThreadPersistentSettings.THREAD_ENABLED));
         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...");
         OperationReceiverWrapper.onOtDaemonDied();
@@ -243,30 +316,168 @@
                     mLinkProperties.setInterfaceName(TUN_IF_NAME);
                     mLinkProperties.setMtu(TunInterfaceController.MTU);
                     mConnectivityManager.registerNetworkProvider(mNetworkProvider);
+                    requestUpstreamNetwork();
+                    requestThreadNetwork();
 
                     initializeOtDaemon();
                 });
     }
 
+    public void setEnabled(@NonNull boolean isEnabled, @NonNull IOperationReceiver receiver) {
+        enforceAllPermissionsGranted(PERMISSION_THREAD_NETWORK_PRIVILEGED);
+
+        mHandler.post(() -> setEnabledInternal(isEnabled, new OperationReceiverWrapper(receiver)));
+    }
+
+    private void setEnabledInternal(
+            @NonNull boolean isEnabled, @Nullable OperationReceiverWrapper receiver) {
+        // 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 requestUpstreamNetwork() {
+        if (mUpstreamNetworkCallback != null) {
+            throw new AssertionError("The upstream network request is already there.");
+        }
+        mUpstreamNetworkCallback = new UpstreamNetworkCallback();
+        mConnectivityManager.registerNetworkCallback(
+                mUpstreamNetworkRequest, mUpstreamNetworkCallback, mHandler);
+    }
+
+    private void cancelRequestUpstreamNetwork() {
+        if (mUpstreamNetworkCallback == null) {
+            throw new AssertionError("The upstream network request null.");
+        }
+        mNetworkToInterface.clear();
+        mConnectivityManager.unregisterNetworkCallback(mUpstreamNetworkCallback);
+        mUpstreamNetworkCallback = null;
+    }
+
+    private final class UpstreamNetworkCallback extends ConnectivityManager.NetworkCallback {
+        @Override
+        public void onAvailable(@NonNull Network network) {
+            checkOnHandlerThread();
+            Log.i(TAG, "Upstream network available: " + network);
+        }
+
+        @Override
+        public void onLost(@NonNull Network network) {
+            checkOnHandlerThread();
+            Log.i(TAG, "Upstream network lost: " + network);
+
+            // TODO: disable border routing when upsteam network disconnected
+        }
+
+        @Override
+        public void onLinkPropertiesChanged(
+                @NonNull Network network, @NonNull LinkProperties linkProperties) {
+            checkOnHandlerThread();
+
+            String existingIfName = mNetworkToInterface.get(network);
+            String newIfName = linkProperties.getInterfaceName();
+            if (Objects.equals(existingIfName, newIfName)) {
+                return;
+            }
+            Log.i(TAG, "Upstream network changed: " + existingIfName + " -> " + newIfName);
+            mNetworkToInterface.put(network, newIfName);
+
+            // TODO: disable border routing if netIfName is null
+            if (network.equals(mUpstreamNetwork)) {
+                enableBorderRouting(mNetworkToInterface.get(mUpstreamNetwork));
+            }
+        }
+    }
+
+    private final class ThreadNetworkCallback extends ConnectivityManager.NetworkCallback {
+        @Override
+        public void onAvailable(@NonNull Network network) {
+            checkOnHandlerThread();
+            Log.i(TAG, "Thread network available: " + network);
+        }
+
+        @Override
+        public void onLocalNetworkInfoChanged(
+                @NonNull Network network, @NonNull LocalNetworkInfo localNetworkInfo) {
+            checkOnHandlerThread();
+            Log.i(
+                    TAG,
+                    "LocalNetworkInfo of Thread network changed: {threadNetwork: "
+                            + network
+                            + ", localNetworkInfo: "
+                            + localNetworkInfo
+                            + "}");
+            if (localNetworkInfo.getUpstreamNetwork() == null) {
+                mUpstreamNetwork = null;
+                return;
+            }
+            if (!localNetworkInfo.getUpstreamNetwork().equals(mUpstreamNetwork)) {
+                mUpstreamNetwork = localNetworkInfo.getUpstreamNetwork();
+                if (mNetworkToInterface.containsKey(mUpstreamNetwork)) {
+                    enableBorderRouting(mNetworkToInterface.get(mUpstreamNetwork));
+                }
+            }
+        }
+    }
+
+    private void requestThreadNetwork() {
+        mConnectivityManager.registerNetworkCallback(
+                new NetworkRequest.Builder()
+                        // clearCapabilities() is needed to remove forbidden capabilities and UID
+                        // requirement.
+                        .clearCapabilities()
+                        .addTransportType(NetworkCapabilities.TRANSPORT_THREAD)
+                        .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,
+                mLinkProperties,
+                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();
-        mNetworkAgent =
-                new NetworkAgent(
-                        mContext,
-                        mHandlerThread.getLooper(),
-                        TAG,
-                        netCaps,
-                        mLinkProperties,
-                        score,
-                        new NetworkAgentConfig.Builder().build(),
-                        mNetworkProvider) {};
+
+        mNetworkAgent = newNetworkAgent();
         mNetworkAgent.register();
         mNetworkAgent.markConnected();
         Log.i(TAG, "Registered Thread network");
@@ -303,10 +514,19 @@
     }
 
     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
@@ -408,29 +628,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 {
+        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));
     }
@@ -438,11 +658,13 @@
     @Override
     public void unregisterOperationalDatasetCallback(IOperationalDatasetCallback callback)
             throws RemoteException {
+        enforceAllPermissionsGranted(
+                permission.ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED);
         mHandler.post(() -> mOtDaemonCallbackProxy.unregisterDatasetCallback(callback));
     }
 
     private void checkOnHandlerThread() {
-        if (Looper.myLooper() != mHandlerThread.getLooper()) {
+        if (Looper.myLooper() != mHandler.getLooper()) {
             Log.wtf(TAG, "Must be on the handler thread!");
         }
     }
@@ -483,6 +705,8 @@
                 return ERROR_REJECTED_BY_PEER;
             case OT_ERROR_UNSUPPORTED_CHANNEL:
                 return ERROR_UNSUPPORTED_CHANNEL;
+            case OT_ERROR_THREAD_DISABLED:
+                return ERROR_THREAD_DISABLED;
             default:
                 return ERROR_INTERNAL_ERROR;
         }
@@ -491,7 +715,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));
@@ -515,7 +739,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));
@@ -538,7 +762,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)));
     }
@@ -554,6 +778,97 @@
         }
     }
 
+    /**
+     * 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, "Enable border routing on AIL: " + infraIfName);
+        try {
+            mBorderRouterConfig.infraInterfaceName = infraIfName;
+            mBorderRouterConfig.infraInterfaceIcmp6Socket =
+                    mInfraIfController.createIcmp6Socket(infraIfName);
+            mBorderRouterConfig.isBorderRoutingEnabled = true;
+
+            mOtDaemon.configureBorderRouter(
+                    mBorderRouterConfig,
+                    new IOtStatusReceiver.Stub() {
+                        @Override
+                        public void onSuccess() {
+                            Log.i(TAG, "configure border router successfully");
+                        }
+
+                        @Override
+                        public void onError(int i, String s) {
+                            Log.w(
+                                    TAG,
+                                    String.format(
+                                            "failed to configure border router: %d %s", i, s));
+                        }
+                    });
+        } catch (Exception e) {
+            Log.w(TAG, "enableBorderRouting failed: " + e);
+        }
+    }
+
     private void handleThreadInterfaceStateChanged(boolean isUp) {
         try {
             mTunIfController.setInterfaceUp(isUp);
@@ -566,9 +881,15 @@
     private void handleDeviceRoleChanged(@DeviceRole int deviceRole) {
         if (ThreadNetworkController.isAttached(deviceRole)) {
             Log.d(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");
+
+            // This is an idempotent method which can be called for multiple times when the device
+            // is already detached or stopped
             unregisterThreadNetwork();
         }
     }
@@ -588,6 +909,88 @@
         updateNetworkLinkProperties(linkAddress, isAdded);
     }
 
+    private boolean isMulticastForwardingEnabled() {
+        return !(mUpstreamMulticastRoutingConfig.getForwardingMode() == FORWARD_NONE
+                && mDownstreamMulticastRoutingConfig.getForwardingMode() == FORWARD_NONE);
+    }
+
+    private void sendLocalNetworkConfig() {
+        if (mNetworkAgent == null) {
+            return;
+        }
+        final LocalNetworkConfig localNetworkConfig = newLocalNetworkConfig();
+        mNetworkAgent.sendLocalNetworkConfig(localNetworkConfig);
+        Log.d(TAG, "Sent localNetworkConfig: " + localNetworkConfig);
+    }
+
+    private void handleMulticastForwardingStateChanged(boolean isEnabled) {
+        if (isMulticastForwardingEnabled() == isEnabled) {
+            return;
+        }
+
+        Log.i(TAG, "Multicast forwaring is " + (isEnabled ? "enabled" : "disabled"));
+
+        if (isEnabled) {
+            // 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 =
+                    new MulticastRoutingConfig.Builder(FORWARD_WITH_MIN_SCOPE, 4).build();
+            mDownstreamMulticastRoutingConfig =
+                    new MulticastRoutingConfig.Builder(FORWARD_SELECTED).build();
+        } else {
+            // When multicast forwarding is disabled, set both upstream and downstream
+            // forwarding config to FORWARD_NONE.
+            mUpstreamMulticastRoutingConfig = CONFIG_FORWARD_NONE;
+            mDownstreamMulticastRoutingConfig = CONFIG_FORWARD_NONE;
+        }
+        sendLocalNetworkConfig();
+    }
+
+    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) {
+            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();
+        }
+    }
+
     private static final class CallbackMetadata {
         private static long gId = 0;
 
@@ -610,8 +1013,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<>();
@@ -625,7 +1028,7 @@
         public void registerStateCallback(IStateCallback callback) {
             checkOnHandlerThread();
             if (mStateCallbacks.containsKey(callback)) {
-                return;
+                throw new IllegalStateException("Registering the same IStateCallback twice");
             }
 
             IBinder.DeathRecipient deathRecipient =
@@ -646,6 +1049,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)) {
@@ -657,7 +1069,8 @@
         public void registerDatasetCallback(IOperationalDatasetCallback callback) {
             checkOnHandlerThread();
             if (mOpDatasetCallbacks.containsKey(callback)) {
-                return;
+                throw new IllegalStateException(
+                        "Registering the same IOperationalDatasetCallback twice");
             }
 
             IBinder.DeathRecipient deathRecipient =
@@ -709,6 +1122,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));
         }
@@ -718,6 +1156,7 @@
             onInterfaceStateChanged(newState.isInterfaceUp);
             onDeviceRoleChanged(newState.deviceRole, listenerId);
             onPartitionIdChanged(newState.partitionId, listenerId);
+            onMulticastForwardingStateChanged(newState.multicastForwardingEnabled);
             mState = newState;
 
             ActiveOperationalDataset newActiveDataset;
@@ -732,7 +1171,7 @@
                 mActiveDataset = newActiveDataset;
             } catch (IllegalArgumentException e) {
                 // Is unlikely that OT will generate invalid Operational Dataset
-                Log.w(TAG, "Ignoring invalid Active Operational Dataset changes", e);
+                Log.wtf(TAG, "Invalid Active Operational Dataset from OpenThread", e);
             }
 
             PendingOperationalDataset newPendingDataset;
@@ -746,7 +1185,8 @@
                 onPendingOperationalDatasetChanged(newPendingDataset, listenerId);
                 mPendingDataset = newPendingDataset;
             } catch (IllegalArgumentException e) {
-                Log.w(TAG, "Ignoring invalid Pending Operational Dataset changes", e);
+                // Is unlikely that OT will generate invalid Operational Dataset
+                Log.wtf(TAG, "Invalid Pending Operational Dataset from OpenThread", e);
             }
         }
 
@@ -825,9 +1265,19 @@
             }
         }
 
+        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));
+        }
     }
 }
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..5cf27f7 100644
--- a/thread/service/java/com/android/server/thread/ThreadNetworkService.java
+++ b/thread/service/java/com/android/server/thread/ThreadNetworkService.java
@@ -16,13 +16,25 @@
 
 package com.android.server.thread;
 
+import static android.content.pm.PackageManager.PERMISSION_GRANTED;
+
+import static com.android.net.module.util.DeviceConfigUtils.TETHERING_MODULE_NAME;
+
+import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.content.ApexEnvironment;
 import android.content.Context;
 import android.net.thread.IThreadNetworkController;
 import android.net.thread.IThreadNetworkManager;
+import android.os.Binder;
+import android.os.ParcelFileDescriptor;
+import android.util.AtomicFile;
 
 import com.android.server.SystemService;
 
+import java.io.File;
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
 import java.util.Collections;
 import java.util.List;
 
@@ -31,22 +43,40 @@
  */
 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 =
+                new ThreadPersistentSettings(
+                        new AtomicFile(
+                                new File(
+                                        getOrCreateThreadnetworkDir(),
+                                        ThreadPersistentSettings.FILE_NAME)));
     }
 
     /**
-     * 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 +87,55 @@
         }
         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();
+    }
+
+    /** 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/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..d32f0bf
--- /dev/null
+++ b/thread/service/java/com/android/server/thread/ThreadPersistentSettings.java
@@ -0,0 +1,243 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.thread;
+
+import android.annotation.Nullable;
+import android.os.PersistableBundle;
+import android.util.AtomicFile;
+import android.util.Log;
+
+import com.android.internal.annotations.GuardedBy;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+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. */
+    public 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();
+
+    public ThreadPersistentSettings(AtomicFile atomicFile) {
+        mAtomicFile = atomicFile;
+    }
+
+    /** Initialize the settings by reading from the settings file. */
+    public void initialize() {
+        readFromStoreFile();
+        synchronized (mLock) {
+            if (mSettings.isEmpty()) {
+                put(THREAD_ENABLED.key, THREAD_ENABLED.defaultValue);
+            }
+        }
+    }
+
+    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;
+        }
+    }
+}
diff --git a/thread/service/java/com/android/server/thread/TunInterfaceController.java b/thread/service/java/com/android/server/thread/TunInterfaceController.java
index ac65b11..619a510 100644
--- a/thread/service/java/com/android/server/thread/TunInterfaceController.java
+++ b/thread/service/java/com/android/server/thread/TunInterfaceController.java
@@ -16,6 +16,7 @@
 
 package com.android.server.thread;
 
+import android.annotation.Nullable;
 import android.net.LinkAddress;
 import android.net.util.SocketUtils;
 import android.os.ParcelFileDescriptor;
@@ -34,6 +35,7 @@
 /** Controller for virtual/tunnel network interfaces. */
 public class TunInterfaceController {
     private static final String TAG = "TunIfController";
+    private static final long INFINITE_LIFETIME = 0xffffffffL;
     static final int MTU = 1280;
 
     static {
@@ -58,7 +60,8 @@
     public void createTunInterface() throws IOException {
         mParcelTunFd = ParcelFileDescriptor.adoptFd(nativeCreateTunInterface(mIfName, MTU));
         try {
-            mNetlinkSocket = NetlinkUtils.netlinkSocketForProto(OsConstants.NETLINK_ROUTE);
+            mNetlinkSocket =
+                    NetlinkUtils.netlinkSocketForProto(OsConstants.NETLINK_ROUTE, 0);
         } catch (ErrnoException e) {
             throw new IOException("Failed to create netlink socket", e);
         }
@@ -76,6 +79,7 @@
     }
 
     /** Returns the FD of the tunnel interface. */
+    @Nullable
     public ParcelFileDescriptor getTunFd() {
         return mParcelTunFd;
     }
@@ -98,7 +102,7 @@
 
         if (address.getDeprecationTime() == LinkAddress.LIFETIME_PERMANENT
                 || address.getDeprecationTime() == LinkAddress.LIFETIME_UNKNOWN) {
-            validLifetimeSeconds = 0xffffffffL;
+            validLifetimeSeconds = INFINITE_LIFETIME;
         } else {
             validLifetimeSeconds =
                     Math.max(
@@ -108,7 +112,7 @@
 
         if (address.getExpirationTime() == LinkAddress.LIFETIME_PERMANENT
                 || address.getExpirationTime() == LinkAddress.LIFETIME_UNKNOWN) {
-            preferredLifetimeSeconds = 0xffffffffL;
+            preferredLifetimeSeconds = INFINITE_LIFETIME;
         } else {
             preferredLifetimeSeconds =
                     Math.max(
diff --git a/thread/service/jni/com_android_server_thread_TunInterfaceController.cpp b/thread/service/jni/com_android_server_thread_TunInterfaceController.cpp
index ed39fab..c56bc0b 100644
--- a/thread/service/jni/com_android_server_thread_TunInterfaceController.cpp
+++ b/thread/service/jni/com_android_server_thread_TunInterfaceController.cpp
@@ -53,25 +53,25 @@
     strlcpy(ifr.ifr_name, ifName.c_str(), sizeof(ifr.ifr_name));
 
     if (ioctl(fd, TUNSETIFF, &ifr, sizeof(ifr)) != 0) {
-        close(fd);
         jniThrowExceptionFmt(env, "java/io/IOException", "ioctl(TUNSETIFF) failed (%s)",
                              strerror(errno));
+        close(fd);
         return -1;
     }
 
     int inet6 = socket(AF_INET6, SOCK_DGRAM | SOCK_CLOEXEC | SOCK_NONBLOCK, IPPROTO_IP);
     if (inet6 == -1) {
-        close(fd);
         jniThrowExceptionFmt(env, "java/io/IOException", "create inet6 socket failed (%s)",
                              strerror(errno));
+        close(fd);
         return -1;
     }
     ifr.ifr_mtu = mtu;
     if (ioctl(inet6, SIOCSIFMTU, &ifr) != 0) {
-        close(fd);
-        close(inet6);
         jniThrowExceptionFmt(env, "java/io/IOException", "ioctl(SIOCSIFMTU) failed (%s)",
                              strerror(errno));
+        close(fd);
+        close(inet6);
         return -1;
     }
 
@@ -94,7 +94,6 @@
     }
 
     if (ioctl(inet6, SIOCSIFFLAGS, &ifr) != 0) {
-        close(inet6);
         jniThrowExceptionFmt(env, "java/io/IOException", "ioctl(SIOCSIFFLAGS) failed (%s)",
                              strerror(errno));
     }
diff --git a/thread/tests/cts/Android.bp b/thread/tests/cts/Android.bp
index b75b8e6..81e24da 100644
--- a/thread/tests/cts/Android.bp
+++ b/thread/tests/cts/Android.bp
@@ -15,6 +15,7 @@
 //
 
 package {
+    default_team: "trendy_team_fwk_thread_network",
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
@@ -37,6 +38,7 @@
         "androidx.test.ext.junit",
         "compatibility-device-util-axt",
         "ctstestrunner-axt",
+        "guava",
         "guava-android-testlib",
         "net-tests-utils",
         "truth",
@@ -44,7 +46,7 @@
     libs: [
         "android.test.base",
         "android.test.runner",
-        "framework-connectivity-flagged-apis"
+        "framework-connectivity-module-api-stubs-including-flagged",
     ],
     // Test coverage system runs on different devices. Need to
     // compile for all architectures.
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 e17dd02..aab4b2e 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,39 @@
 
 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.thread.ActiveOperationalDataset;
 import android.net.thread.OperationalDatasetTimestamp;
 import android.net.thread.PendingOperationalDataset;
@@ -48,11 +63,11 @@
 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;
@@ -62,9 +77,11 @@
 
 import java.time.Duration;
 import java.time.Instant;
+import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
+import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
@@ -74,51 +91,715 @@
 @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 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 Set<String> mGrantedPermissions;
 
     @Before
-    public void setUp() {
-        mExecutor = Executors.newSingleThreadExecutor();
-        mManager = mContext.getSystemService(ThreadNetworkManager.class);
+    public void setUp() throws Exception {
+
         mGrantedPermissions = new HashSet<String>();
+        mExecutor = Executors.newSingleThreadExecutor();
+        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);
+
+        setEnabledAndWait(mController, true);
     }
 
     @After
     public void tearDown() throws Exception {
-        if (mManager != null) {
-            leaveAndWait();
-            dropPermissions();
+        if (mController != null) {
+            grantPermissions(THREAD_NETWORK_PRIVILEGED);
+            CompletableFuture<Void> future = new CompletableFuture<>();
+            mController.leave(mExecutor, future::complete);
+            future.get(LEAVE_TIMEOUT_MILLIS, MILLISECONDS);
+        }
+        dropAllPermissions();
+    }
+
+    @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));
         }
     }
 
-    private List<ThreadNetworkController> getAllControllers() {
-        return mManager.getAllThreadNetworkControllers();
+    @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();
+        }
     }
 
-    private void leaveAndWait() throws Exception {
-        grantPermissions(PERMISSION_THREAD_NETWORK_PRIVILEGED);
+    @Test
+    public void registerStateCallback_noPermissions_throwsSecurityException() throws Exception {
+        dropAllPermissions();
 
-        for (ThreadNetworkController controller : getAllControllers()) {
-            SettableFuture<Void> future = SettableFuture.create();
-            controller.leave(mExecutor, future::set);
-            future.get();
+        assertThrows(
+                SecurityException.class,
+                () -> mController.registerStateCallback(mExecutor, role -> {}));
+    }
+
+    @Test
+    public void registerStateCallback_alreadyRegistered_throwsIllegalArgumentException()
+            throws Exception {
+        grantPermissions(ACCESS_NETWORK_STATE);
+        CompletableFuture<Integer> deviceRole = new CompletableFuture<>();
+        StateCallback callback = role -> deviceRole.complete(role);
+
+        mController.registerStateCallback(mExecutor, callback);
+
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> mController.registerStateCallback(mExecutor, callback));
+    }
+
+    @Test
+    public void unregisterStateCallback_noPermissions_throwsSecurityException() throws Exception {
+        CompletableFuture<Integer> deviceRole = new CompletableFuture<>();
+        StateCallback callback = role -> deviceRole.complete(role);
+        runAsShell(
+                ACCESS_NETWORK_STATE, () -> mController.registerStateCallback(mExecutor, callback));
+
+        try {
+            dropAllPermissions();
+            assertThrows(
+                    SecurityException.class, () -> mController.unregisterStateCallback(callback));
+        } finally {
+            runAsShell(ACCESS_NETWORK_STATE, () -> mController.unregisterStateCallback(callback));
         }
     }
 
+    @Test
+    public void unregisterStateCallback_callbackRegistered_success() throws Exception {
+        grantPermissions(ACCESS_NETWORK_STATE);
+        CompletableFuture<Integer> deviceRole = new CompletableFuture<>();
+        StateCallback callback = role -> deviceRole.complete(role);
+
+        assertDoesNotThrow(() -> mController.registerStateCallback(mExecutor, callback));
+        mController.unregisterStateCallback(callback);
+    }
+
+    @Test
+    public void unregisterStateCallback_callbackNotRegistered_throwsIllegalArgumentException()
+            throws Exception {
+        CompletableFuture<Integer> deviceRole = new CompletableFuture<>();
+        StateCallback callback = role -> deviceRole.complete(role);
+
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> mController.unregisterStateCallback(callback));
+    }
+
+    @Test
+    public void unregisterStateCallback_alreadyUnregistered_throwsIllegalArgumentException()
+            throws Exception {
+        grantPermissions(ACCESS_NETWORK_STATE);
+        CompletableFuture<Integer> deviceRole = new CompletableFuture<>();
+        StateCallback callback = deviceRole::complete;
+        mController.registerStateCallback(mExecutor, callback);
+        mController.unregisterStateCallback(callback);
+
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> mController.unregisterStateCallback(callback));
+    }
+
+    @Test
+    public void registerOperationalDatasetCallback_permissionsGranted_returnsCurrentStates()
+            throws Exception {
+        grantPermissions(ACCESS_NETWORK_STATE, THREAD_NETWORK_PRIVILEGED);
+        CompletableFuture<ActiveOperationalDataset> activeFuture = new CompletableFuture<>();
+        CompletableFuture<PendingOperationalDataset> pendingFuture = new CompletableFuture<>();
+        var callback = newDatasetCallback(activeFuture, pendingFuture);
+
+        try {
+            mController.registerOperationalDatasetCallback(mExecutor, callback);
+
+            assertThat(activeFuture.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS)).isNull();
+            assertThat(pendingFuture.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS)).isNull();
+        } finally {
+            mController.unregisterOperationalDatasetCallback(callback);
+        }
+    }
+
+    @Test
+    public void registerOperationalDatasetCallback_noPermissions_throwsSecurityException()
+            throws Exception {
+        dropAllPermissions();
+        CompletableFuture<ActiveOperationalDataset> activeFuture = new CompletableFuture<>();
+        CompletableFuture<PendingOperationalDataset> pendingFuture = new CompletableFuture<>();
+        var callback = newDatasetCallback(activeFuture, pendingFuture);
+
+        assertThrows(
+                SecurityException.class,
+                () -> mController.registerOperationalDatasetCallback(mExecutor, callback));
+    }
+
+    @Test
+    public void unregisterOperationalDatasetCallback_callbackRegistered_success() throws Exception {
+        grantPermissions(ACCESS_NETWORK_STATE, THREAD_NETWORK_PRIVILEGED);
+        CompletableFuture<ActiveOperationalDataset> activeFuture = new CompletableFuture<>();
+        CompletableFuture<PendingOperationalDataset> pendingFuture = new CompletableFuture<>();
+        var callback = newDatasetCallback(activeFuture, pendingFuture);
+        mController.registerOperationalDatasetCallback(mExecutor, callback);
+
+        assertDoesNotThrow(() -> mController.unregisterOperationalDatasetCallback(callback));
+    }
+
+    @Test
+    public void unregisterOperationalDatasetCallback_noPermissions_throwsSecurityException()
+            throws Exception {
+        CompletableFuture<ActiveOperationalDataset> activeFuture = new CompletableFuture<>();
+        CompletableFuture<PendingOperationalDataset> pendingFuture = new CompletableFuture<>();
+        var callback = newDatasetCallback(activeFuture, pendingFuture);
+        runAsShell(
+                ACCESS_NETWORK_STATE,
+                THREAD_NETWORK_PRIVILEGED,
+                () -> mController.registerOperationalDatasetCallback(mExecutor, callback));
+
+        try {
+            dropAllPermissions();
+            assertThrows(
+                    SecurityException.class,
+                    () -> mController.unregisterOperationalDatasetCallback(callback));
+        } finally {
+            runAsShell(
+                    ACCESS_NETWORK_STATE,
+                    THREAD_NETWORK_PRIVILEGED,
+                    () -> mController.unregisterOperationalDatasetCallback(callback));
+        }
+    }
+
+    private static <V> OutcomeReceiver<V, ThreadNetworkException> newOutcomeReceiver(
+            CompletableFuture<V> future) {
+        return new OutcomeReceiver<V, ThreadNetworkException>() {
+            @Override
+            public void onResult(V result) {
+                future.complete(result);
+            }
+
+            @Override
+            public void onError(ThreadNetworkException e) {
+                future.completeExceptionally(e);
+            }
+        };
+    }
+
+    @Test
+    public void join_withPrivilegedPermission_success() throws Exception {
+        ActiveOperationalDataset activeDataset = newRandomizedDataset("TestNet", mController);
+        CompletableFuture<Void> joinFuture = new CompletableFuture<>();
+
+        runAsShell(
+                THREAD_NETWORK_PRIVILEGED,
+                () -> mController.join(activeDataset, mExecutor, newOutcomeReceiver(joinFuture)));
+        joinFuture.get(JOIN_TIMEOUT_MILLIS, MILLISECONDS);
+
+        assertThat(isAttached(mController)).isTrue();
+        assertThat(getActiveOperationalDataset(mController)).isEqualTo(activeDataset);
+    }
+
+    @Test
+    public void join_withoutPrivilegedPermission_throwsSecurityException() throws Exception {
+        dropAllPermissions();
+        ActiveOperationalDataset activeDataset = newRandomizedDataset("TestNet", mController);
+
+        assertThrows(
+                SecurityException.class, () -> mController.join(activeDataset, mExecutor, v -> {}));
+    }
+
+    @Test
+    public void join_threadDisabled_failsWithErrorThreadDisabled() throws Exception {
+        setEnabledAndWait(mController, false);
+        ActiveOperationalDataset activeDataset = newRandomizedDataset("TestNet", mController);
+        CompletableFuture<Void> joinFuture = new CompletableFuture<>();
+
+        runAsShell(
+                THREAD_NETWORK_PRIVILEGED,
+                () -> mController.join(activeDataset, mExecutor, newOutcomeReceiver(joinFuture)));
+
+        var thrown =
+                assertThrows(
+                        ExecutionException.class,
+                        () -> joinFuture.get(JOIN_TIMEOUT_MILLIS, MILLISECONDS));
+        var threadException = (ThreadNetworkException) thrown.getCause();
+        assertThat(threadException.getErrorCode()).isEqualTo(ERROR_THREAD_DISABLED);
+    }
+
+    @Test
+    public void join_concurrentRequests_firstOneIsAborted() throws Exception {
+        final byte[] KEY_1 = new byte[] {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1};
+        final byte[] KEY_2 = new byte[] {2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2};
+        ActiveOperationalDataset activeDataset1 =
+                new ActiveOperationalDataset.Builder(newRandomizedDataset("TestNet", mController))
+                        .setNetworkKey(KEY_1)
+                        .build();
+        ActiveOperationalDataset activeDataset2 =
+                new ActiveOperationalDataset.Builder(activeDataset1).setNetworkKey(KEY_2).build();
+        CompletableFuture<Void> joinFuture1 = new CompletableFuture<>();
+        CompletableFuture<Void> joinFuture2 = new CompletableFuture<>();
+
+        runAsShell(
+                THREAD_NETWORK_PRIVILEGED,
+                () -> {
+                    mController.join(activeDataset1, mExecutor, newOutcomeReceiver(joinFuture1));
+                    mController.join(activeDataset2, mExecutor, newOutcomeReceiver(joinFuture2));
+                });
+
+        var thrown =
+                assertThrows(
+                        ExecutionException.class,
+                        () -> joinFuture1.get(JOIN_TIMEOUT_MILLIS, MILLISECONDS));
+        var threadException = (ThreadNetworkException) thrown.getCause();
+        assertThat(threadException.getErrorCode()).isEqualTo(ERROR_ABORTED);
+        joinFuture2.get(JOIN_TIMEOUT_MILLIS, MILLISECONDS);
+        assertThat(isAttached(mController)).isTrue();
+        assertThat(getActiveOperationalDataset(mController)).isEqualTo(activeDataset2);
+    }
+
+    @Test
+    public void leave_withPrivilegedPermission_success() throws Exception {
+        CompletableFuture<Void> leaveFuture = new CompletableFuture<>();
+        joinRandomizedDatasetAndWait(mController);
+
+        runAsShell(
+                THREAD_NETWORK_PRIVILEGED,
+                () -> mController.leave(mExecutor, newOutcomeReceiver(leaveFuture)));
+        leaveFuture.get(LEAVE_TIMEOUT_MILLIS, MILLISECONDS);
+
+        assertThat(getDeviceRole(mController)).isEqualTo(DEVICE_ROLE_STOPPED);
+    }
+
+    @Test
+    public void leave_withoutPrivilegedPermission_throwsSecurityException() {
+        dropAllPermissions();
+
+        assertThrows(SecurityException.class, () -> mController.leave(mExecutor, v -> {}));
+    }
+
+    @Test
+    public void leave_threadDisabled_success() throws Exception {
+        setEnabledAndWait(mController, false);
+        CompletableFuture<Void> leaveFuture = new CompletableFuture<>();
+
+        leave(mController, newOutcomeReceiver(leaveFuture));
+        leaveFuture.get(LEAVE_TIMEOUT_MILLIS, MILLISECONDS);
+
+        assertThat(getDeviceRole(mController)).isEqualTo(DEVICE_ROLE_STOPPED);
+    }
+
+    @Test
+    public void leave_concurrentRequests_bothSuccess() throws Exception {
+        CompletableFuture<Void> leaveFuture1 = new CompletableFuture<>();
+        CompletableFuture<Void> leaveFuture2 = new CompletableFuture<>();
+        joinRandomizedDatasetAndWait(mController);
+
+        runAsShell(
+                THREAD_NETWORK_PRIVILEGED,
+                () -> {
+                    mController.leave(mExecutor, newOutcomeReceiver(leaveFuture1));
+                    mController.leave(mExecutor, newOutcomeReceiver(leaveFuture2));
+                });
+
+        leaveFuture1.get(LEAVE_TIMEOUT_MILLIS, MILLISECONDS);
+        leaveFuture2.get(LEAVE_TIMEOUT_MILLIS, MILLISECONDS);
+        assertThat(getDeviceRole(mController)).isEqualTo(DEVICE_ROLE_STOPPED);
+    }
+
+    @Test
+    public void scheduleMigration_withPrivilegedPermission_newDatasetApplied() throws Exception {
+        grantPermissions(ACCESS_NETWORK_STATE, THREAD_NETWORK_PRIVILEGED);
+        ActiveOperationalDataset activeDataset1 =
+                new ActiveOperationalDataset.Builder(newRandomizedDataset("TestNet", mController))
+                        .setActiveTimestamp(new OperationalDatasetTimestamp(1L, 0, false))
+                        .setExtendedPanId(new byte[] {1, 1, 1, 1, 1, 1, 1, 1})
+                        .build();
+        ActiveOperationalDataset activeDataset2 =
+                new ActiveOperationalDataset.Builder(activeDataset1)
+                        .setActiveTimestamp(new OperationalDatasetTimestamp(2L, 0, false))
+                        .setNetworkName("ThreadNet2")
+                        .build();
+        PendingOperationalDataset pendingDataset2 =
+                new PendingOperationalDataset(
+                        activeDataset2,
+                        OperationalDatasetTimestamp.fromInstant(Instant.now()),
+                        Duration.ofSeconds(30));
+        CompletableFuture<Void> joinFuture = new CompletableFuture<>();
+        CompletableFuture<Void> migrateFuture = new CompletableFuture<>();
+        mController.join(activeDataset1, mExecutor, newOutcomeReceiver(joinFuture));
+        joinFuture.get(JOIN_TIMEOUT_MILLIS, MILLISECONDS);
+
+        mController.scheduleMigration(
+                pendingDataset2, mExecutor, newOutcomeReceiver(migrateFuture));
+        migrateFuture.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS);
+
+        CompletableFuture<Boolean> dataset2IsApplied = new CompletableFuture<>();
+        CompletableFuture<Boolean> pendingDatasetIsRemoved = new CompletableFuture<>();
+        OperationalDatasetCallback datasetCallback =
+                new OperationalDatasetCallback() {
+                    @Override
+                    public void onActiveOperationalDatasetChanged(
+                            ActiveOperationalDataset activeDataset) {
+                        if (activeDataset.equals(activeDataset2)) {
+                            dataset2IsApplied.complete(true);
+                        }
+                    }
+
+                    @Override
+                    public void onPendingOperationalDatasetChanged(
+                            PendingOperationalDataset pendingDataset) {
+                        if (pendingDataset == null) {
+                            pendingDatasetIsRemoved.complete(true);
+                        }
+                    }
+                };
+        mController.registerOperationalDatasetCallback(directExecutor(), datasetCallback);
+        try {
+            assertThat(dataset2IsApplied.get(MIGRATION_TIMEOUT_MILLIS, MILLISECONDS)).isTrue();
+            assertThat(pendingDatasetIsRemoved.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS)).isTrue();
+        } finally {
+            mController.unregisterOperationalDatasetCallback(datasetCallback);
+        }
+    }
+
+    @Test
+    public void scheduleMigration_whenNotAttached_failWithPreconditionError() throws Exception {
+        grantPermissions(ACCESS_NETWORK_STATE, THREAD_NETWORK_PRIVILEGED);
+        PendingOperationalDataset pendingDataset =
+                new PendingOperationalDataset(
+                        newRandomizedDataset("TestNet", mController),
+                        OperationalDatasetTimestamp.fromInstant(Instant.now()),
+                        Duration.ofSeconds(30));
+        CompletableFuture<Void> migrateFuture = new CompletableFuture<>();
+
+        mController.scheduleMigration(pendingDataset, mExecutor, newOutcomeReceiver(migrateFuture));
+
+        ThreadNetworkException thrown =
+                (ThreadNetworkException)
+                        assertThrows(ExecutionException.class, migrateFuture::get).getCause();
+        assertThat(thrown.getErrorCode()).isEqualTo(ERROR_FAILED_PRECONDITION);
+    }
+
+    @Test
+    public void scheduleMigration_secondRequestHasSmallerTimestamp_rejectedByLeader()
+            throws Exception {
+        grantPermissions(ACCESS_NETWORK_STATE, THREAD_NETWORK_PRIVILEGED);
+        final ActiveOperationalDataset activeDataset =
+                new ActiveOperationalDataset.Builder(newRandomizedDataset("testNet", mController))
+                        .setActiveTimestamp(new OperationalDatasetTimestamp(1L, 0, false))
+                        .build();
+        ActiveOperationalDataset activeDataset1 =
+                new ActiveOperationalDataset.Builder(activeDataset)
+                        .setActiveTimestamp(new OperationalDatasetTimestamp(2L, 0, false))
+                        .setNetworkName("testNet1")
+                        .build();
+        PendingOperationalDataset pendingDataset1 =
+                new PendingOperationalDataset(
+                        activeDataset1,
+                        new OperationalDatasetTimestamp(100, 0, false),
+                        Duration.ofSeconds(30));
+        ActiveOperationalDataset activeDataset2 =
+                new ActiveOperationalDataset.Builder(activeDataset)
+                        .setActiveTimestamp(new OperationalDatasetTimestamp(3L, 0, false))
+                        .setNetworkName("testNet2")
+                        .build();
+        PendingOperationalDataset pendingDataset2 =
+                new PendingOperationalDataset(
+                        activeDataset2,
+                        new OperationalDatasetTimestamp(20, 0, false),
+                        Duration.ofSeconds(30));
+        CompletableFuture<Void> joinFuture = new CompletableFuture<>();
+        CompletableFuture<Void> migrateFuture1 = new CompletableFuture<>();
+        CompletableFuture<Void> migrateFuture2 = new CompletableFuture<>();
+        mController.join(activeDataset, mExecutor, newOutcomeReceiver(joinFuture));
+        joinFuture.get(JOIN_TIMEOUT_MILLIS, MILLISECONDS);
+
+        mController.scheduleMigration(
+                pendingDataset1, mExecutor, newOutcomeReceiver(migrateFuture1));
+        migrateFuture1.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS);
+        mController.scheduleMigration(
+                pendingDataset2, mExecutor, newOutcomeReceiver(migrateFuture2));
+
+        ThreadNetworkException thrown =
+                (ThreadNetworkException)
+                        assertThrows(ExecutionException.class, migrateFuture2::get).getCause();
+        assertThat(thrown.getErrorCode()).isEqualTo(ERROR_REJECTED_BY_PEER);
+    }
+
+    @Test
+    public void scheduleMigration_secondRequestHasLargerTimestamp_newDatasetApplied()
+            throws Exception {
+        grantPermissions(ACCESS_NETWORK_STATE, THREAD_NETWORK_PRIVILEGED);
+        final ActiveOperationalDataset activeDataset =
+                new ActiveOperationalDataset.Builder(newRandomizedDataset("validName", mController))
+                        .setActiveTimestamp(new OperationalDatasetTimestamp(1L, 0, false))
+                        .build();
+        ActiveOperationalDataset activeDataset1 =
+                new ActiveOperationalDataset.Builder(activeDataset)
+                        .setActiveTimestamp(new OperationalDatasetTimestamp(2L, 0, false))
+                        .setNetworkName("testNet1")
+                        .build();
+        PendingOperationalDataset pendingDataset1 =
+                new PendingOperationalDataset(
+                        activeDataset1,
+                        new OperationalDatasetTimestamp(100, 0, false),
+                        Duration.ofSeconds(30));
+        ActiveOperationalDataset activeDataset2 =
+                new ActiveOperationalDataset.Builder(activeDataset)
+                        .setActiveTimestamp(new OperationalDatasetTimestamp(3L, 0, false))
+                        .setNetworkName("testNet2")
+                        .build();
+        PendingOperationalDataset pendingDataset2 =
+                new PendingOperationalDataset(
+                        activeDataset2,
+                        new OperationalDatasetTimestamp(200, 0, false),
+                        Duration.ofSeconds(30));
+        CompletableFuture<Void> joinFuture = new CompletableFuture<>();
+        CompletableFuture<Void> migrateFuture1 = new CompletableFuture<>();
+        CompletableFuture<Void> migrateFuture2 = new CompletableFuture<>();
+        mController.join(activeDataset, mExecutor, newOutcomeReceiver(joinFuture));
+        joinFuture.get(JOIN_TIMEOUT_MILLIS, MILLISECONDS);
+
+        mController.scheduleMigration(
+                pendingDataset1, mExecutor, newOutcomeReceiver(migrateFuture1));
+        migrateFuture1.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS);
+        mController.scheduleMigration(
+                pendingDataset2, mExecutor, newOutcomeReceiver(migrateFuture2));
+        migrateFuture2.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS);
+
+        CompletableFuture<Boolean> dataset2IsApplied = new CompletableFuture<>();
+        CompletableFuture<Boolean> pendingDatasetIsRemoved = new CompletableFuture<>();
+        OperationalDatasetCallback datasetCallback =
+                new OperationalDatasetCallback() {
+                    @Override
+                    public void onActiveOperationalDatasetChanged(
+                            ActiveOperationalDataset activeDataset) {
+                        if (activeDataset.equals(activeDataset2)) {
+                            dataset2IsApplied.complete(true);
+                        }
+                    }
+
+                    @Override
+                    public void onPendingOperationalDatasetChanged(
+                            PendingOperationalDataset pendingDataset) {
+                        if (pendingDataset == null) {
+                            pendingDatasetIsRemoved.complete(true);
+                        }
+                    }
+                };
+        mController.registerOperationalDatasetCallback(directExecutor(), datasetCallback);
+        try {
+            assertThat(dataset2IsApplied.get(MIGRATION_TIMEOUT_MILLIS, MILLISECONDS)).isTrue();
+            assertThat(pendingDatasetIsRemoved.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS)).isTrue();
+        } finally {
+            mController.unregisterOperationalDatasetCallback(datasetCallback);
+        }
+    }
+
+    @Test
+    public void scheduleMigration_threadDisabled_failsWithErrorThreadDisabled() throws Exception {
+        ActiveOperationalDataset activeDataset = newRandomizedDataset("TestNet", mController);
+        PendingOperationalDataset pendingDataset =
+                new PendingOperationalDataset(
+                        activeDataset,
+                        OperationalDatasetTimestamp.fromInstant(Instant.now()),
+                        Duration.ofSeconds(30));
+        joinRandomizedDatasetAndWait(mController);
+        CompletableFuture<Void> migrationFuture = new CompletableFuture<>();
+
+        setEnabledAndWait(mController, false);
+
+        scheduleMigration(mController, pendingDataset, newOutcomeReceiver(migrationFuture));
+
+        ThreadNetworkException thrown =
+                (ThreadNetworkException)
+                        assertThrows(ExecutionException.class, migrationFuture::get).getCause();
+        assertThat(thrown.getErrorCode()).isEqualTo(ERROR_THREAD_DISABLED);
+    }
+
+    @Test
+    public void createRandomizedDataset_wrongNetworkNameLength_throwsIllegalArgumentException() {
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> mController.createRandomizedDataset("", mExecutor, dataset -> {}));
+
+        assertThrows(
+                IllegalArgumentException.class,
+                () ->
+                        mController.createRandomizedDataset(
+                                "ANetNameIs17Bytes", mExecutor, dataset -> {}));
+    }
+
+    @Test
+    public void createRandomizedDataset_validNetworkName_success() throws Exception {
+        ActiveOperationalDataset dataset = newRandomizedDataset("validName", mController);
+
+        assertThat(dataset.getNetworkName()).isEqualTo("validName");
+        assertThat(dataset.getPanId()).isLessThan(0xffff);
+        assertThat(dataset.getChannelMask().size()).isAtLeast(1);
+        assertThat(dataset.getExtendedPanId()).hasLength(8);
+        assertThat(dataset.getNetworkKey()).hasLength(16);
+        assertThat(dataset.getPskc()).hasLength(16);
+        assertThat(dataset.getMeshLocalPrefix().getPrefixLength()).isEqualTo(64);
+        assertThat(dataset.getMeshLocalPrefix().getRawAddress()[0]).isEqualTo((byte) 0xfd);
+    }
+
+    @Test
+    public void setEnabled_permissionsGranted_succeeds() throws Exception {
+        CompletableFuture<Void> setFuture1 = new CompletableFuture<>();
+        CompletableFuture<Void> setFuture2 = new CompletableFuture<>();
+
+        runAsShell(
+                THREAD_NETWORK_PRIVILEGED,
+                () -> mController.setEnabled(false, mExecutor, newOutcomeReceiver(setFuture1)));
+        setFuture1.get(ENABLED_TIMEOUT_MILLIS, MILLISECONDS);
+        waitForEnabledState(mController, booleanToEnabledState(false));
+
+        runAsShell(
+                THREAD_NETWORK_PRIVILEGED,
+                () -> mController.setEnabled(true, mExecutor, newOutcomeReceiver(setFuture2)));
+        setFuture2.get(ENABLED_TIMEOUT_MILLIS, MILLISECONDS);
+        waitForEnabledState(mController, booleanToEnabledState(true));
+    }
+
+    @Test
+    public void setEnabled_noPermissions_throwsSecurityException() throws Exception {
+        CompletableFuture<Void> setFuture = new CompletableFuture<>();
+        assertThrows(
+                SecurityException.class,
+                () -> mController.setEnabled(false, mExecutor, newOutcomeReceiver(setFuture)));
+    }
+
+    @Test
+    public void setEnabled_disable_leavesThreadNetwork() throws Exception {
+        joinRandomizedDatasetAndWait(mController);
+        setEnabledAndWait(mController, false);
+        assertThat(getDeviceRole(mController)).isEqualTo(DEVICE_ROLE_STOPPED);
+    }
+
+    @Test
+    public void setEnabled_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 +809,14 @@
         getInstrumentation().getUiAutomation().adoptShellPermissionIdentity(allPermissions);
     }
 
-    private static void dropPermissions() {
+    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,484 +825,189 @@
     }
 
     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 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)
+            throws Exception {
+        ActiveOperationalDataset activeDataset = newRandomizedDataset("TestNet", 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 {
+        CompletableFuture<Void> joinFuture = joinRandomizedDataset(controller);
+        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);
-        }
-    }
-
-    @Test
-    public void registerStateCallback_permissionsGranted_returnsCurrentStates() throws Exception {
-        grantPermissions(permission.ACCESS_NETWORK_STATE);
-
-        for (ThreadNetworkController controller : getAllControllers()) {
-            SettableFuture<Integer> deviceRole = SettableFuture.create();
-
-            controller.registerStateCallback(mExecutor, role -> deviceRole.set(role));
-
-            assertThat(deviceRole.get()).isEqualTo(DEVICE_ROLE_STOPPED);
-        }
-    }
-
-    @Test
-    public void registerStateCallback_noPermissions_throwsSecurityException() throws Exception {
-        dropPermissions();
-
-        for (ThreadNetworkController controller : getAllControllers()) {
-            assertThrows(
-                    SecurityException.class,
-                    () -> controller.registerStateCallback(mExecutor, role -> {}));
-        }
-    }
-
-    @Test
-    public void registerStateCallback_alreadyRegistered_throwsIllegalArgumentException()
-            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));
-        }
-    }
-
-    @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);
-        }
-    }
-
-    @Test
-    public void unregisterStateCallback_callbackNotRegistered_throwsIllegalArgumentException()
-            throws Exception {
-        for (ThreadNetworkController controller : getAllControllers()) {
-            SettableFuture<Integer> deviceRole = SettableFuture.create();
-            StateCallback callback = role -> deviceRole.set(role);
-
-            assertThrows(
-                    IllegalArgumentException.class,
-                    () -> controller.unregisterStateCallback(callback));
-        }
-    }
-
-    @Test
-    public void unregisterStateCallback_alreadyUnregistered_throwsIllegalArgumentException()
-            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);
-
-            assertThrows(
-                    IllegalArgumentException.class,
-                    () -> controller.unregisterStateCallback(callback));
-        }
-    }
-
-    @Test
-    public void registerOperationalDatasetCallback_permissionsGranted_returnsCurrentStates()
-            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();
-
-            controller.registerOperationalDatasetCallback(
-                    mExecutor, newDatasetCallback(activeFuture, pendingFuture));
-
-            assertThat(activeFuture.get()).isNull();
-            assertThat(pendingFuture.get()).isNull();
-        }
-    }
-
-    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 {
-        dropPermissions();
-
-        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() {
-        dropPermissions();
-
-        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);
+    private static void assertDoesNotThrow(ThrowingRunnable runnable) {
+        try {
+            runnable.run();
+        } catch (Throwable e) {
+            fail("Should not have thrown " + e);
         }
     }
 }
diff --git a/thread/tests/integration/Android.bp b/thread/tests/integration/Android.bp
new file mode 100644
index 0000000..5989db5
--- /dev/null
+++ b/thread/tests/integration/Android.bp
@@ -0,0 +1,58 @@
+//
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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",
+        "guava",
+        "mockito-target-minus-junit4",
+        "net-tests-utils",
+        "net-utils-device-common",
+        "net-utils-device-common-bpf",
+        "testables",
+    ],
+    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..ec9b5f3
--- /dev/null
+++ b/thread/tests/integration/AndroidTest.xml
@@ -0,0 +1,44 @@
+<?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>
+
+    <!-- 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..5d9f084
--- /dev/null
+++ b/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java
@@ -0,0 +1,186 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.IntegrationTestUtils.isExpectedIcmpv6Packet;
+import static android.net.thread.IntegrationTestUtils.isSimulatedThreadRadioSupported;
+import static android.net.thread.IntegrationTestUtils.newPacketReader;
+import static android.net.thread.IntegrationTestUtils.readPacketFrom;
+import static android.net.thread.IntegrationTestUtils.waitFor;
+import static android.net.thread.IntegrationTestUtils.waitForStateAnyOf;
+import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_LEADER;
+import static android.net.thread.ThreadNetworkManager.PERMISSION_THREAD_NETWORK_PRIVILEGED;
+
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ECHO_REPLY_TYPE;
+import static com.android.testutils.TestNetworkTrackerKt.initTestNetwork;
+import static com.android.testutils.TestPermissionUtil.runAsShell;
+
+import static com.google.common.io.BaseEncoding.base16;
+import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assume.assumeNotNull;
+import static org.junit.Assume.assumeTrue;
+
+import android.content.Context;
+import android.net.LinkProperties;
+import android.net.MacAddress;
+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.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/** 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;
+
+    // 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);
+
+        mHandlerThread = new HandlerThread(getClass().getSimpleName());
+        mHandlerThread.start();
+        mHandler = new Handler(mHandlerThread.getLooper());
+
+        mInfraNetworkTracker =
+                runAsShell(
+                        MANAGE_TEST_NETWORKS,
+                        () ->
+                                initTestNetwork(
+                                        mContext, new LinkProperties(), 5000 /* timeoutMs */));
+        runAsShell(
+                PERMISSION_THREAD_NETWORK_PRIVILEGED,
+                NETWORK_SETTINGS,
+                () -> {
+                    CountDownLatch latch = new CountDownLatch(1);
+                    mController.setTestNetworkAsUpstream(
+                            mInfraNetworkTracker.getTestIface().getInterfaceName(),
+                            directExecutor(),
+                            v -> latch.countDown());
+                    latch.await();
+                });
+    }
+
+    @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);
+                });
+        runAsShell(MANAGE_TEST_NETWORKS, () -> mInfraNetworkTracker.teardown());
+
+        mHandlerThread.quitSafely();
+        mHandlerThread.join();
+    }
+
+    @Test
+    public void unicastRouting_infraDevicePingTheadDeviceOmr_replyReceived() throws Exception {
+        assumeTrue(isSimulatedThreadRadioSupported());
+
+        /*
+         * <pre>
+         * Topology:
+         *                 infra network                       Thread
+         * infra device -------------------- Border Router -------------- Full Thread device
+         *                                   (Cuttlefish)
+         * </pre>
+         */
+
+        // BR forms a network.
+        runAsShell(
+                PERMISSION_THREAD_NETWORK_PRIVILEGED,
+                () -> mController.join(DEFAULT_DATASET, directExecutor(), result -> {}));
+        waitForStateAnyOf(mController, List.of(DEVICE_ROLE_LEADER), 30 /* timeoutSeconds */);
+
+        // Creates a Full Thread Device (FTD) and lets it join the network.
+        FullThreadDevice ftd = new FullThreadDevice(5 /* node ID */);
+        ftd.factoryReset();
+        ftd.joinNetwork(DEFAULT_DATASET);
+        ftd.waitForStateAnyOf(List.of("router", "child"), 10 /* timeoutSeconds */);
+        waitFor(() -> ftd.getOmrAddress() != null, 60 /* timeoutSeconds */);
+        Inet6Address ftdOmr = ftd.getOmrAddress();
+        assertNotNull(ftdOmr);
+
+        // Creates a infra network device.
+        TapPacketReader infraNetworkReader =
+                newPacketReader(mInfraNetworkTracker.getTestIface(), mHandler);
+        InfraNetworkDevice infraDevice =
+                new InfraNetworkDevice(MacAddress.fromString("1:2:3:4:5:6"), infraNetworkReader);
+        infraDevice.runSlaac(60 /* timeoutSeconds */);
+        assertNotNull(infraDevice.ipv6Addr);
+
+        // Infra device sends an echo request to FTD's OMR.
+        infraDevice.sendEchoRequest(ftdOmr);
+
+        // Infra device receives an echo reply sent by FTD.
+        assertNotNull(
+                readPacketFrom(
+                        infraNetworkReader,
+                        p -> isExpectedIcmpv6Packet(p, ICMPV6_ECHO_REPLY_TYPE)));
+    }
+}
diff --git a/thread/tests/integration/src/android/net/thread/FullThreadDevice.java b/thread/tests/integration/src/android/net/thread/FullThreadDevice.java
new file mode 100644
index 0000000..01638f3
--- /dev/null
+++ b/thread/tests/integration/src/android/net/thread/FullThreadDevice.java
@@ -0,0 +1,180 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.net.thread;
+
+import static android.net.thread.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 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.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * 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;
+    }
+
+    /**
+     * 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;
+    }
+
+    /**
+     * 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 timeoutSeconds the number of seconds to wait for.
+     */
+    public void waitForStateAnyOf(List<String> states, int timeoutSeconds) throws TimeoutException {
+        waitFor(() -> states.contains(getState()), timeoutSeconds);
+    }
+
+    /**
+     * Gets the state of the Thread device.
+     *
+     * @return a string representing the state.
+     */
+    public String getState() {
+        return executeCommand("state").get(0);
+    }
+
+    /** 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);
+        }
+    }
+
+    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/InfraNetworkDevice.java b/thread/tests/integration/src/android/net/thread/InfraNetworkDevice.java
new file mode 100644
index 0000000..43a800d
--- /dev/null
+++ b/thread/tests/integration/src/android/net/thread/InfraNetworkDevice.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.net.thread;
+
+import static android.net.thread.IntegrationTestUtils.getRaPios;
+import static android.net.thread.IntegrationTestUtils.readPacketFrom;
+import static android.net.thread.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.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(int timeoutSeconds) throws TimeoutException {
+        waitFor(() -> (ipv6Addr = runSlaac()) != null, timeoutSeconds, 5 /* intervalSeconds */);
+    }
+
+    private Inet6Address runSlaac() {
+        try {
+            sendRsPacket();
+
+            final byte[] raPacket = readPacketFrom(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/IntegrationTestUtils.java b/thread/tests/integration/src/android/net/thread/IntegrationTestUtils.java
new file mode 100644
index 0000000..c465d57
--- /dev/null
+++ b/thread/tests/integration/src/android/net/thread/IntegrationTestUtils.java
@@ -0,0 +1,228 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.net.thread;
+
+import static android.system.OsConstants.IPPROTO_ICMPV6;
+
+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.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.nio.ByteBuffer;
+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 {
+    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.
+     *
+     * <p>It checks the condition once every second.
+     *
+     * @param condition the condition to check.
+     * @param timeoutSeconds the number of seconds to wait for.
+     * @throws TimeoutException if the condition is not met after the timeout.
+     */
+    public static void waitFor(Supplier<Boolean> condition, int timeoutSeconds)
+            throws TimeoutException {
+        waitFor(condition, timeoutSeconds, 1);
+    }
+
+    /**
+     * Waits for the given {@link Supplier} to be true until given timeout.
+     *
+     * <p>It checks the condition once every {@code intervalSeconds}.
+     *
+     * @param condition the condition to check.
+     * @param timeoutSeconds the number of seconds to wait for.
+     * @param intervalSeconds the period to check the {@code condition}.
+     * @throws TimeoutException if the condition is still not met when the timeout expires.
+     */
+    public static void waitFor(Supplier<Boolean> condition, int timeoutSeconds, int intervalSeconds)
+            throws TimeoutException {
+        for (int i = 0; i < timeoutSeconds; i += intervalSeconds) {
+            if (condition.get()) {
+                return;
+            }
+            SystemClock.sleep(intervalSeconds * 1000L);
+        }
+        if (condition.get()) {
+            return;
+        }
+        throw new TimeoutException(
+                String.format(
+                        "The condition failed to become true in %d seconds.", timeoutSeconds));
+    }
+
+    /**
+     * 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 timeoutSeconds the number of seconds ot wait for.
+     * @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, int timeoutSeconds)
+            throws TimeoutException {
+        SettableFuture<Integer> future = SettableFuture.create();
+        ThreadNetworkController.StateCallback callback =
+                newRole -> {
+                    if (deviceRoles.contains(newRole)) {
+                        future.set(newRole);
+                    }
+                };
+        controller.registerStateCallback(directExecutor(), callback);
+        try {
+            int role = future.get(timeoutSeconds, TimeUnit.SECONDS);
+            controller.unregisterStateCallback(callback);
+            return role;
+        } catch (InterruptedException | ExecutionException e) {
+            throw new TimeoutException(
+                    String.format(
+                            "The device didn't become an expected role in %d seconds.",
+                            timeoutSeconds));
+        }
+    }
+
+    /**
+     * Reads 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[] readPacketFrom(TapPacketReader packetReader, Predicate<byte[]> filter) {
+        byte[] packet;
+        while ((packet = packetReader.poll(3000 /* timeoutMs */)) != null) {
+            if (filter.test(packet)) 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;
+    }
+
+    /** 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;
+    }
+}
diff --git a/thread/tests/unit/Android.bp b/thread/tests/unit/Android.bp
index 3a087c7..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,21 +30,39 @@
     ],
     test_suites: [
         "general-tests",
+        "mts-tethering",
     ],
     static_libs: [
-        "androidx.test.ext.junit",
-        "compatibility-device-util-axt",
-        "ctstestrunner-axt",
+        "frameworks-base-testutils",
         "framework-connectivity-pre-jarjar",
         "framework-connectivity-t-pre-jarjar",
+        "framework-location.stubs.module_lib",
+        "guava",
         "guava-android-testlib",
+        "mockito-target-extended-minus-junit4",
         "net-tests-utils",
+        "ot-daemon-aidl-java",
+        "ot-daemon-testing",
+        "service-connectivity-pre-jarjar",
+        "service-thread-pre-jarjar",
         "truth",
+        "service-thread-pre-jarjar",
     ],
     libs: [
         "android.test.base",
         "android.test.runner",
+        "ServiceConnectivityResources",
+        "framework-wifi",
     ],
+    jni_libs: [
+        "libservice-thread-jni",
+
+        // these are needed for Extended Mockito
+        "libdexmakerjvmtiagent",
+        "libstaticjvmtiagent",
+    ],
+    jni_uses_platform_apis: true,
+    jarjar_rules: ":connectivity-jarjar-rules",
     // Test coverage system runs on different devices. Need to
     // compile for all architectures.
     compile_multilib: "both",
diff --git a/thread/tests/unit/AndroidTest.xml b/thread/tests/unit/AndroidTest.xml
index 663ff74..26813c1 100644
--- a/thread/tests/unit/AndroidTest.xml
+++ b/thread/tests/unit/AndroidTest.xml
@@ -27,7 +27,11 @@
 
     <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
         <option name="package" value="android.net.thread.unittests" />
+        <option name="hidden-api-checks" value="false"/>
         <!-- Ignores tests introduced by guava-android-testlib -->
         <option name="exclude-annotation" value="org.junit.Ignore"/>
+        <!-- Ignores tests introduced by frameworks-base-testutils -->
+        <option name="exclude-filter" value="android.os.test.TestLooperTest"/>
+        <option name="exclude-filter" value="com.android.test.filters.SelectTestTests"/>
     </test>
 </configuration>
diff --git a/thread/tests/unit/src/android/net/thread/ActiveOperationalDatasetTest.java b/thread/tests/unit/src/android/net/thread/ActiveOperationalDatasetTest.java
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
new file mode 100644
index 0000000..75eb043
--- /dev/null
+++ b/thread/tests/unit/src/android/net/thread/ThreadNetworkControllerTest.java
@@ -0,0 +1,385 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.thread;
+
+import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_CHILD;
+import static android.net.thread.ThreadNetworkException.ERROR_UNAVAILABLE;
+import static android.net.thread.ThreadNetworkException.ERROR_UNSUPPORTED_CHANNEL;
+import static android.os.Process.SYSTEM_UID;
+
+import static com.google.common.io.BaseEncoding.base16;
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.doAnswer;
+
+import android.net.thread.ThreadNetworkController.OperationalDatasetCallback;
+import android.net.thread.ThreadNetworkController.StateCallback;
+import android.os.Binder;
+import android.os.OutcomeReceiver;
+import android.os.Process;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.invocation.InvocationOnMock;
+
+import java.time.Duration;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/** Unit tests for {@link ThreadNetworkController}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public final class ThreadNetworkControllerTest {
+
+    @Mock private IThreadNetworkController mMockService;
+    private ThreadNetworkController mController;
+
+    // A valid Thread Active Operational Dataset generated from OpenThread CLI "dataset new":
+    // Active Timestamp: 1
+    // Channel: 19
+    // Channel Mask: 0x07FFF800
+    // Ext PAN ID: ACC214689BC40BDF
+    // Mesh Local Prefix: fd64:db12:25f4:7e0b::/64
+    // Network Key: F26B3153760F519A63BAFDDFFC80D2AF
+    // Network Name: OpenThread-d9a0
+    // PAN ID: 0xD9A0
+    // PSKc: A245479C836D551B9CA557F7B9D351B4
+    // Security Policy: 672 onrcb
+    private static final byte[] DEFAULT_DATASET_TLVS =
+            base16().decode(
+                            "0E080000000000010000000300001335060004001FFFE002"
+                                    + "08ACC214689BC40BDF0708FD64DB1225F47E0B0510F26B31"
+                                    + "53760F519A63BAFDDFFC80D2AF030F4F70656E5468726561"
+                                    + "642D643961300102D9A00410A245479C836D551B9CA557F7"
+                                    + "B9D351B40C0402A0FFF8");
+
+    private static final ActiveOperationalDataset DEFAULT_DATASET =
+            ActiveOperationalDataset.fromThreadTlvs(DEFAULT_DATASET_TLVS);
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        mController = new ThreadNetworkController(mMockService);
+    }
+
+    private static void setBinderUid(int uid) {
+        // TODO: generally, it's not a good practice to depend on the implementation detail to set
+        // a custom UID, but Connectivity, Wifi, UWB and etc modules are using this trick. Maybe
+        // define a interface (e.b. CallerIdentityInjector) for easier mocking.
+        Binder.restoreCallingIdentity((((long) uid) << 32) | Binder.getCallingPid());
+    }
+
+    private static IStateCallback getStateCallback(InvocationOnMock invocation) {
+        return (IStateCallback) invocation.getArguments()[0];
+    }
+
+    private static IOperationReceiver getOperationReceiver(InvocationOnMock invocation) {
+        return (IOperationReceiver) invocation.getArguments()[0];
+    }
+
+    private static IOperationReceiver getJoinReceiver(InvocationOnMock invocation) {
+        return (IOperationReceiver) invocation.getArguments()[1];
+    }
+
+    private static IOperationReceiver getScheduleMigrationReceiver(InvocationOnMock invocation) {
+        return (IOperationReceiver) invocation.getArguments()[1];
+    }
+
+    private static IOperationReceiver getSetTestNetworkAsUpstreamReceiver(
+            InvocationOnMock invocation) {
+        return (IOperationReceiver) invocation.getArguments()[1];
+    }
+
+    private static IActiveOperationalDatasetReceiver getCreateDatasetReceiver(
+            InvocationOnMock invocation) {
+        return (IActiveOperationalDatasetReceiver) invocation.getArguments()[1];
+    }
+
+    private static IOperationalDatasetCallback getOperationalDatasetCallback(
+            InvocationOnMock invocation) {
+        return (IOperationalDatasetCallback) invocation.getArguments()[0];
+    }
+
+    @Test
+    public void registerStateCallback_callbackIsInvokedWithCallingAppIdentity() throws Exception {
+        setBinderUid(SYSTEM_UID);
+        doAnswer(
+                        invoke -> {
+                            getStateCallback(invoke).onDeviceRoleChanged(DEVICE_ROLE_CHILD);
+                            return null;
+                        })
+                .when(mMockService)
+                .registerStateCallback(any(IStateCallback.class));
+        AtomicInteger callbackUid = new AtomicInteger(0);
+        StateCallback callback = state -> callbackUid.set(Binder.getCallingUid());
+
+        try {
+            mController.registerStateCallback(Runnable::run, callback);
+
+            assertThat(callbackUid.get()).isNotEqualTo(SYSTEM_UID);
+            assertThat(callbackUid.get()).isEqualTo(Process.myUid());
+        } finally {
+            mController.unregisterStateCallback(callback);
+        }
+    }
+
+    @Test
+    public void registerOperationalDatasetCallback_callbackIsInvokedWithCallingAppIdentity()
+            throws Exception {
+        setBinderUid(SYSTEM_UID);
+        doAnswer(
+                        invoke -> {
+                            getOperationalDatasetCallback(invoke)
+                                    .onActiveOperationalDatasetChanged(null);
+                            getOperationalDatasetCallback(invoke)
+                                    .onPendingOperationalDatasetChanged(null);
+                            return null;
+                        })
+                .when(mMockService)
+                .registerOperationalDatasetCallback(any(IOperationalDatasetCallback.class));
+        AtomicInteger activeCallbackUid = new AtomicInteger(0);
+        AtomicInteger pendingCallbackUid = new AtomicInteger(0);
+        OperationalDatasetCallback callback =
+                new OperationalDatasetCallback() {
+                    @Override
+                    public void onActiveOperationalDatasetChanged(
+                            ActiveOperationalDataset dataset) {
+                        activeCallbackUid.set(Binder.getCallingUid());
+                    }
+
+                    @Override
+                    public void onPendingOperationalDatasetChanged(
+                            PendingOperationalDataset dataset) {
+                        pendingCallbackUid.set(Binder.getCallingUid());
+                    }
+                };
+
+        try {
+            mController.registerOperationalDatasetCallback(Runnable::run, callback);
+
+            assertThat(activeCallbackUid.get()).isNotEqualTo(SYSTEM_UID);
+            assertThat(activeCallbackUid.get()).isEqualTo(Process.myUid());
+            assertThat(pendingCallbackUid.get()).isNotEqualTo(SYSTEM_UID);
+            assertThat(pendingCallbackUid.get()).isEqualTo(Process.myUid());
+        } finally {
+            mController.unregisterOperationalDatasetCallback(callback);
+        }
+    }
+
+    @Test
+    public void createRandomizedDataset_callbackIsInvokedWithCallingAppIdentity() throws Exception {
+        setBinderUid(SYSTEM_UID);
+        AtomicInteger successCallbackUid = new AtomicInteger(0);
+        AtomicInteger errorCallbackUid = new AtomicInteger(0);
+
+        doAnswer(
+                        invoke -> {
+                            getCreateDatasetReceiver(invoke).onSuccess(DEFAULT_DATASET);
+                            return null;
+                        })
+                .when(mMockService)
+                .createRandomizedDataset(anyString(), any(IActiveOperationalDatasetReceiver.class));
+        mController.createRandomizedDataset(
+                "TestNet",
+                Runnable::run,
+                dataset -> successCallbackUid.set(Binder.getCallingUid()));
+        doAnswer(
+                        invoke -> {
+                            getCreateDatasetReceiver(invoke).onError(ERROR_UNSUPPORTED_CHANNEL, "");
+                            return null;
+                        })
+                .when(mMockService)
+                .createRandomizedDataset(anyString(), any(IActiveOperationalDatasetReceiver.class));
+        mController.createRandomizedDataset(
+                "TestNet",
+                Runnable::run,
+                new OutcomeReceiver<>() {
+                    @Override
+                    public void onResult(ActiveOperationalDataset dataset) {}
+
+                    @Override
+                    public void onError(ThreadNetworkException e) {
+                        errorCallbackUid.set(Binder.getCallingUid());
+                    }
+                });
+
+        assertThat(successCallbackUid.get()).isNotEqualTo(SYSTEM_UID);
+        assertThat(successCallbackUid.get()).isEqualTo(Process.myUid());
+        assertThat(errorCallbackUid.get()).isNotEqualTo(SYSTEM_UID);
+        assertThat(errorCallbackUid.get()).isEqualTo(Process.myUid());
+    }
+
+    @Test
+    public void join_callbackIsInvokedWithCallingAppIdentity() throws Exception {
+        setBinderUid(SYSTEM_UID);
+        AtomicInteger successCallbackUid = new AtomicInteger(0);
+        AtomicInteger errorCallbackUid = new AtomicInteger(0);
+
+        doAnswer(
+                        invoke -> {
+                            getJoinReceiver(invoke).onSuccess();
+                            return null;
+                        })
+                .when(mMockService)
+                .join(any(ActiveOperationalDataset.class), any(IOperationReceiver.class));
+        mController.join(
+                DEFAULT_DATASET,
+                Runnable::run,
+                v -> successCallbackUid.set(Binder.getCallingUid()));
+        doAnswer(
+                        invoke -> {
+                            getJoinReceiver(invoke).onError(ERROR_UNAVAILABLE, "");
+                            return null;
+                        })
+                .when(mMockService)
+                .join(any(ActiveOperationalDataset.class), any(IOperationReceiver.class));
+        mController.join(
+                DEFAULT_DATASET,
+                Runnable::run,
+                new OutcomeReceiver<>() {
+                    @Override
+                    public void onResult(Void unused) {}
+
+                    @Override
+                    public void onError(ThreadNetworkException e) {
+                        errorCallbackUid.set(Binder.getCallingUid());
+                    }
+                });
+
+        assertThat(successCallbackUid.get()).isNotEqualTo(SYSTEM_UID);
+        assertThat(successCallbackUid.get()).isEqualTo(Process.myUid());
+        assertThat(errorCallbackUid.get()).isNotEqualTo(SYSTEM_UID);
+        assertThat(errorCallbackUid.get()).isEqualTo(Process.myUid());
+    }
+
+    @Test
+    public void scheduleMigration_callbackIsInvokedWithCallingAppIdentity() throws Exception {
+        setBinderUid(SYSTEM_UID);
+        final PendingOperationalDataset pendingDataset =
+                new PendingOperationalDataset(
+                        DEFAULT_DATASET,
+                        new OperationalDatasetTimestamp(100, 0, false),
+                        Duration.ZERO);
+        AtomicInteger successCallbackUid = new AtomicInteger(0);
+        AtomicInteger errorCallbackUid = new AtomicInteger(0);
+
+        doAnswer(
+                        invoke -> {
+                            getScheduleMigrationReceiver(invoke).onSuccess();
+                            return null;
+                        })
+                .when(mMockService)
+                .scheduleMigration(
+                        any(PendingOperationalDataset.class), any(IOperationReceiver.class));
+        mController.scheduleMigration(
+                pendingDataset, Runnable::run, v -> successCallbackUid.set(Binder.getCallingUid()));
+        doAnswer(
+                        invoke -> {
+                            getScheduleMigrationReceiver(invoke).onError(ERROR_UNAVAILABLE, "");
+                            return null;
+                        })
+                .when(mMockService)
+                .scheduleMigration(
+                        any(PendingOperationalDataset.class), any(IOperationReceiver.class));
+        mController.scheduleMigration(
+                pendingDataset,
+                Runnable::run,
+                new OutcomeReceiver<>() {
+                    @Override
+                    public void onResult(Void unused) {}
+
+                    @Override
+                    public void onError(ThreadNetworkException e) {
+                        errorCallbackUid.set(Binder.getCallingUid());
+                    }
+                });
+
+        assertThat(successCallbackUid.get()).isNotEqualTo(SYSTEM_UID);
+        assertThat(successCallbackUid.get()).isEqualTo(Process.myUid());
+        assertThat(errorCallbackUid.get()).isNotEqualTo(SYSTEM_UID);
+        assertThat(errorCallbackUid.get()).isEqualTo(Process.myUid());
+    }
+
+    @Test
+    public void leave_callbackIsInvokedWithCallingAppIdentity() throws Exception {
+        setBinderUid(SYSTEM_UID);
+        AtomicInteger successCallbackUid = new AtomicInteger(0);
+        AtomicInteger errorCallbackUid = new AtomicInteger(0);
+
+        doAnswer(
+                        invoke -> {
+                            getOperationReceiver(invoke).onSuccess();
+                            return null;
+                        })
+                .when(mMockService)
+                .leave(any(IOperationReceiver.class));
+        mController.leave(Runnable::run, v -> successCallbackUid.set(Binder.getCallingUid()));
+        doAnswer(
+                        invoke -> {
+                            getOperationReceiver(invoke).onError(ERROR_UNAVAILABLE, "");
+                            return null;
+                        })
+                .when(mMockService)
+                .leave(any(IOperationReceiver.class));
+        mController.leave(
+                Runnable::run,
+                new OutcomeReceiver<>() {
+                    @Override
+                    public void onResult(Void unused) {}
+
+                    @Override
+                    public void onError(ThreadNetworkException e) {
+                        errorCallbackUid.set(Binder.getCallingUid());
+                    }
+                });
+
+        assertThat(successCallbackUid.get()).isNotEqualTo(SYSTEM_UID);
+        assertThat(successCallbackUid.get()).isEqualTo(Process.myUid());
+        assertThat(errorCallbackUid.get()).isNotEqualTo(SYSTEM_UID);
+        assertThat(errorCallbackUid.get()).isEqualTo(Process.myUid());
+    }
+
+    @Test
+    public void 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/ThreadPersistentSettingsTest.java b/thread/tests/unit/src/android/net/thread/ThreadPersistentSettingsTest.java
new file mode 100644
index 0000000..11aabb8
--- /dev/null
+++ b/thread/tests/unit/src/android/net/thread/ThreadPersistentSettingsTest.java
@@ -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.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.mock;
+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.PersistableBundle;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.util.AtomicFile;
+
+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.ByteArrayOutputStream;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+
+/** Unit tests for {@link ThreadPersistentSettings}. */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class ThreadPersistentSettingsTest {
+    @Mock private AtomicFile mAtomicFile;
+
+    private ThreadPersistentSettings mThreadPersistentSetting;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+
+        FileOutputStream fos = mock(FileOutputStream.class);
+        when(mAtomicFile.startWrite()).thenReturn(fos);
+        mThreadPersistentSetting = new ThreadPersistentSettings(mAtomicFile);
+    }
+
+    /** Called after each test */
+    @After
+    public void tearDown() {
+        validateMockitoUsage();
+    }
+
+    @Test
+    public void put_ThreadFeatureEnabledTrue_returnsTrue() throws Exception {
+        mThreadPersistentSetting.put(THREAD_ENABLED.key, true);
+
+        assertThat(mThreadPersistentSetting.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 {
+        mThreadPersistentSetting.put(THREAD_ENABLED.key, false);
+
+        assertThat(mThreadPersistentSetting.get(THREAD_ENABLED)).isFalse();
+        // Confirm that file writes have been triggered.
+        verify(mAtomicFile).startWrite();
+        verify(mAtomicFile).finishWrite(any());
+    }
+
+    @Test
+    public void initialize_readsFromFile() throws Exception {
+        byte[] data = createXmlForParsing(THREAD_ENABLED.key, false);
+        setupAtomicFileMockForRead(data);
+
+        // Trigger file read.
+        mThreadPersistentSetting.initialize();
+
+        assertThat(mThreadPersistentSetting.get(THREAD_ENABLED)).isFalse();
+        verify(mAtomicFile, never()).startWrite();
+    }
+
+    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/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/ThreadNetworkControllerServiceTest.java b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java
new file mode 100644
index 0000000..1d83abc
--- /dev/null
+++ b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.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.server.thread;
+
+import static android.net.thread.ThreadNetworkException.ERROR_INTERNAL_ERROR;
+import static android.net.thread.ThreadNetworkManager.PERMISSION_THREAD_NETWORK_PRIVILEGED;
+
+import static com.android.testutils.TestPermissionUtil.runAsShell;
+
+import static com.google.common.io.BaseEncoding.base16;
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.NetworkAgent;
+import android.net.NetworkProvider;
+import android.net.thread.ActiveOperationalDataset;
+import android.net.thread.IOperationReceiver;
+import android.os.Handler;
+import android.os.ParcelFileDescriptor;
+import android.os.RemoteException;
+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.Mock;
+import org.mockito.MockitoAnnotations;
+
+/** 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);
+
+    @Mock private ConnectivityManager mMockConnectivityManager;
+    @Mock private NetworkAgent mMockNetworkAgent;
+    @Mock private TunInterfaceController mMockTunIfController;
+    @Mock private ParcelFileDescriptor mMockTunFd;
+    @Mock private InfraInterfaceController mMockInfraIfController;
+    @Mock private ThreadPersistentSettings mMockPersistentSettings;
+    private Context mContext;
+    private TestLooper mTestLooper;
+    private FakeOtDaemon mFakeOtDaemon;
+    private ThreadNetworkControllerService mService;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+
+        mContext = ApplicationProvider.getApplicationContext();
+        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);
+
+        mService =
+                new ThreadNetworkControllerService(
+                        ApplicationProvider.getApplicationContext(),
+                        handler,
+                        networkProvider,
+                        () -> mFakeOtDaemon,
+                        mMockConnectivityManager,
+                        mMockTunIfController,
+                        mMockInfraIfController,
+                        mMockPersistentSettings);
+        mService.setTestNetworkAgent(mMockNetworkAgent);
+    }
+
+    @Test
+    public void initialize_tunInterfaceSetToOtDaemon() throws Exception {
+        when(mMockTunIfController.getTunFd()).thenReturn(mMockTunFd);
+
+        mService.initialize();
+        mTestLooper.dispatchAll();
+
+        verify(mMockTunIfController, times(1)).createTunInterface();
+        assertThat(mFakeOtDaemon.getTunFd()).isEqualTo(mMockTunFd);
+    }
+
+    @Test
+    public void join_otDaemonRemoteFailure_returnsInternalError() throws Exception {
+        mService.initialize();
+        final IOperationReceiver mockReceiver = mock(IOperationReceiver.class);
+        mFakeOtDaemon.setJoinException(new RemoteException("ot-daemon join() throws"));
+
+        runAsShell(
+                PERMISSION_THREAD_NETWORK_PRIVILEGED,
+                () -> 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);
+
+        runAsShell(
+                PERMISSION_THREAD_NETWORK_PRIVILEGED,
+                () -> 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();
+    }
+}
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/tools/Android.bp b/tools/Android.bp
index 3ce76f6..b7b2aaa 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"],
 }