Merge "Rename check*Permission* to has*Permission" into main
diff --git a/Cronet/tests/common/Android.bp b/Cronet/tests/common/Android.bp
index a484adb..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"],
 }
diff --git a/Cronet/tests/cts/Android.bp b/Cronet/tests/cts/Android.bp
index 7b52694..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 {
diff --git a/Cronet/tests/mts/Android.bp b/Cronet/tests/mts/Android.bp
index 743a1ca..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"],
 }
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/Tethering/Android.bp b/Tethering/Android.bp
index c30e251..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"],
 }
 
diff --git a/Tethering/apex/Android.bp b/Tethering/apex/Android.bp
index de9017a..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",
             ],
         },
     },
@@ -117,8 +121,9 @@
         "ServiceConnectivityResources",
     ],
     prebuilts: [
-        "ot-daemon.init.34rc",
         "current_sdkinfo",
+        "netbpfload.mainline.rc",
+        "ot-daemon.init.34rc",
     ],
     manifest: "manifest.json",
     key: "com.android.tethering.key",
diff --git a/Tethering/apex/permissions/Android.bp b/Tethering/apex/permissions/Android.bp
index 69c1aa2..20772a8 100644
--- a/Tethering/apex/permissions/Android.bp
+++ b/Tethering/apex/permissions/Android.bp
@@ -15,6 +15,7 @@
 //
 
 package {
+    default_team: "trendy_team_fwk_core_networking",
     default_applicable_licenses: ["Android-Apache-2.0"],
     default_visibility: ["//packages/modules/Connectivity/Tethering:__subpackages__"],
 }
@@ -22,4 +23,4 @@
 filegroup {
     name: "privapp_allowlist_com.android.tethering",
     srcs: ["permissions.xml"],
-}
\ No newline at end of file
+}
diff --git a/Tethering/common/TetheringLib/Android.bp b/Tethering/common/TetheringLib/Android.bp
index bcea425..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"],
 }
 
@@ -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/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/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/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/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/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 b9a3b63..f4b4cae 100644
--- a/common/Android.bp
+++ b/common/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/framework-t/Android.bp b/framework-t/Android.bp
index b90d99f..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.
 
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/Android.bp b/framework/Android.bp
index b7ff04f..31ddb53 100644
--- a/framework/Android.bp
+++ b/framework/Android.bp
@@ -15,6 +15,7 @@
 //
 
 package {
+    default_team: "trendy_team_fwk_core_networking",
     // See: http://go/android-license-faq
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
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/service/Android.bp b/nearby/service/Android.bp
index 17b80b0..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"],
 }
 
diff --git a/nearby/service/proto/Android.bp b/nearby/service/proto/Android.bp
index 1b00cf6..be5a0b3 100644
--- a/nearby/service/proto/Android.bp
+++ b/nearby/service/proto/Android.bp
@@ -12,6 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 package {
+    default_team: "trendy_team_fwk_core_networking",
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
@@ -41,4 +42,4 @@
     apex_available: [
         "com.android.tethering",
     ],
-}
\ No newline at end of file
+}
diff --git a/nearby/tests/cts/fastpair/Android.bp b/nearby/tests/cts/fastpair/Android.bp
index 4309d7e..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"],
 }
 
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 bbf42c7..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"],
 }
 
diff --git a/netbpfload/Android.bp b/netbpfload/Android.bp
index 1f92374..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",
 
@@ -49,3 +53,16 @@
     init_rc: ["netbpfload.rc"],
     required: ["bpfloader"],
 }
+
+// Versioned netbpfload init rc: init system will process it only on api V/35+ devices
+// (TODO: consider reducing to T/33+ - adjust the comment up above in line 43 as well)
+// Note: S[31] Sv2[32] T[33] U[34] V[35])
+//
+// For details of versioned rc files see:
+// https://android.googlesource.com/platform/system/core/+/HEAD/init/README.md#versioned-rc-files-within-apexs
+prebuilt_etc {
+    name: "netbpfload.mainline.rc",
+    src: "netbpfload.mainline.rc",
+    filename: "netbpfload.35rc",
+    installable: false,
+}
diff --git a/netbpfload/NetBpfLoad.cpp b/netbpfload/NetBpfLoad.cpp
index 6152287..cbd14ec 100644
--- a/netbpfload/NetBpfLoad.cpp
+++ b/netbpfload/NetBpfLoad.cpp
@@ -173,11 +173,40 @@
     (void)argc;
     android::base::InitLogging(argv, &android::base::KernelLogger);
 
-    const int device_api_level = android_get_device_api_level();
-    const bool isAtLeastU = (device_api_level >= __ANDROID_API_U__);
+    ALOGI("NetBpfLoad '%s' starting...", argv[0]);
 
-    if (!android::bpf::isAtLeastKernelVersion(4, 19, 0)) {
-        ALOGE("Android U QPR2 requires kernel 4.19.");
+    // true iff we are running from the module
+    const bool is_mainline = !strcmp(argv[0], "/apex/com.android.tethering/bin/netbpfload");
+
+    // true iff we are running from the platform
+    const bool is_platform = !strcmp(argv[0], "/system/bin/netbpfload");
+
+    const int device_api_level = android_get_device_api_level();
+    const bool isAtLeastT = (device_api_level >= __ANDROID_API_T__);
+    const bool isAtLeastU = (device_api_level >= __ANDROID_API_U__);
+    const bool isAtLeastV = (device_api_level >= __ANDROID_API_V__);
+
+    ALOGI("NetBpfLoad api:%d/%d kver:%07x platform:%d mainline:%d",
+          android_get_application_target_sdk_version(), device_api_level,
+          android::bpf::kernelVersion(), is_platform, is_mainline);
+
+    if (!is_platform && !is_mainline) {
+        ALOGE("Unable to determine if we're platform or mainline netbpfload.");
+        return 1;
+    }
+
+    if (isAtLeastT && !android::bpf::isAtLeastKernelVersion(4, 9, 0)) {
+        ALOGE("Android T requires kernel 4.9.");
+        return 1;
+    }
+
+    if (isAtLeastU && !android::bpf::isAtLeastKernelVersion(4, 14, 0)) {
+        ALOGE("Android U requires kernel 4.14.");
+        return 1;
+    }
+
+    if (isAtLeastV && !android::bpf::isAtLeastKernelVersion(4, 19, 0)) {
+        ALOGE("Android V requires kernel 4.19.");
         return 1;
     }
 
diff --git a/netbpfload/netbpfload.mainline.rc b/netbpfload/netbpfload.mainline.rc
new file mode 100644
index 0000000..0ac5de8
--- /dev/null
+++ b/netbpfload/netbpfload.mainline.rc
@@ -0,0 +1,8 @@
+service bpfloader /apex/com.android.tethering/bin/netbpfload
+    capabilities CHOWN SYS_ADMIN NET_ADMIN
+    group root graphics network_stack net_admin net_bw_acct net_bw_stats net_raw system
+    user root
+    rlimit memlock 1073741824 1073741824
+    oneshot
+    reboot_on_failure reboot,bpfloader-failed
+    override
diff --git a/netd/Android.bp b/netd/Android.bp
index 3cdbc97..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,15 +59,18 @@
 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",
diff --git a/remoteauth/framework/Android.bp b/remoteauth/framework/Android.bp
index 71b621a..2f1737f 100644
--- a/remoteauth/framework/Android.bp
+++ b/remoteauth/framework/Android.bp
@@ -13,6 +13,7 @@
 // limitations under the License.
 
 package {
+    default_team: "trendy_team_fwk_core_networking",
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
diff --git a/remoteauth/service/Android.bp b/remoteauth/service/Android.bp
index 8330efc..32ae54f 100644
--- a/remoteauth/service/Android.bp
+++ b/remoteauth/service/Android.bp
@@ -13,6 +13,7 @@
 // limitations under the License.
 
 package {
+    default_team: "trendy_team_fwk_core_networking",
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
diff --git a/remoteauth/service/jni/Android.bp b/remoteauth/service/jni/Android.bp
index a95a8fb..c0ac779 100644
--- a/remoteauth/service/jni/Android.bp
+++ b/remoteauth/service/jni/Android.bp
@@ -1,4 +1,5 @@
 package {
+    default_team: "trendy_team_fwk_core_networking",
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
diff --git a/remoteauth/tests/unit/Android.bp b/remoteauth/tests/unit/Android.bp
index 77e6f19..47b9e31 100644
--- a/remoteauth/tests/unit/Android.bp
+++ b/remoteauth/tests/unit/Android.bp
@@ -13,6 +13,7 @@
 // limitations under the License.
 
 package {
+    default_team: "trendy_team_fwk_core_networking",
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
diff --git a/service-t/Android.bp b/service-t/Android.bp
index 78c7d35..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"],
 }
diff --git a/service-t/native/libs/libnetworkstats/Android.bp b/service-t/native/libs/libnetworkstats/Android.bp
index b9f3adb..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",
diff --git a/service-t/src/com/android/server/IpSecXfrmController.java b/service-t/src/com/android/server/IpSecXfrmController.java
index c8abd40..3cfbf83 100644
--- a/service-t/src/com/android/server/IpSecXfrmController.java
+++ b/service-t/src/com/android/server/IpSecXfrmController.java
@@ -15,6 +15,7 @@
  */
 package com.android.server;
 
+import static com.android.net.module.util.netlink.NetlinkUtils.SOCKET_RECV_BUFSIZE;
 import static com.android.net.module.util.netlink.xfrm.XfrmNetlinkMessage.IPPROTO_ESP;
 import static com.android.net.module.util.netlink.xfrm.XfrmNetlinkMessage.NETLINK_XFRM;
 import static com.android.net.module.util.netlink.xfrm.XfrmNetlinkMessage.XFRM_MSG_NEWSA;
@@ -106,7 +107,8 @@
     public static class Dependencies {
         /** Get a new XFRM netlink socket and connect it */
         public FileDescriptor newNetlinkSocket() throws ErrnoException, SocketException {
-            final FileDescriptor fd = NetlinkUtils.netlinkSocketForProto(NETLINK_XFRM);
+            final FileDescriptor fd =
+                    NetlinkUtils.netlinkSocketForProto(NETLINK_XFRM, SOCKET_RECV_BUFSIZE);
             NetlinkUtils.connectToKernel(fd);
             return fd;
         }
diff --git a/service-t/src/com/android/server/NsdService.java b/service-t/src/com/android/server/NsdService.java
index db5f1c0..34927a6 100644
--- a/service-t/src/com/android/server/NsdService.java
+++ b/service-t/src/com/android/server/NsdService.java
@@ -35,6 +35,7 @@
 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;
 
@@ -252,6 +253,8 @@
 
     private final RemoteCallbackList<IOffloadEngine> mOffloadEngines =
             new RemoteCallbackList<>();
+    @NonNull
+    private final MdnsFeatureFlags mMdnsFeatureFlags;
 
     private static class OffloadEngineInfo {
         @NonNull final String mInterfaceName;
@@ -794,7 +797,10 @@
                                     MdnsSearchOptions.newBuilder()
                                             .setNetwork(discoveryRequest.getNetwork())
                                             .setRemoveExpiredService(true)
-                                            .setQueryMode(PASSIVE_QUERY_MODE);
+                                            .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.
@@ -1063,7 +1069,9 @@
                                     transactionId, resolveServiceType);
                             final MdnsSearchOptions options = MdnsSearchOptions.newBuilder()
                                     .setNetwork(info.getNetwork())
-                                    .setQueryMode(PASSIVE_QUERY_MODE)
+                                    .setQueryMode(mMdnsFeatureFlags.isAggressiveQueryModeEnabled()
+                                            ? AGGRESSIVE_QUERY_MODE
+                                            : PASSIVE_QUERY_MODE)
                                     .setResolveInstanceName(info.getServiceName())
                                     .setRemoveExpiredService(true)
                                     .build();
@@ -1161,7 +1169,9 @@
                                 transactionId, resolveServiceType);
                         final MdnsSearchOptions options = MdnsSearchOptions.newBuilder()
                                 .setNetwork(info.getNetwork())
-                                .setQueryMode(PASSIVE_QUERY_MODE)
+                                .setQueryMode(mMdnsFeatureFlags.isAggressiveQueryModeEnabled()
+                                        ? AGGRESSIVE_QUERY_MODE
+                                        : PASSIVE_QUERY_MODE)
                                 .setResolveInstanceName(info.getServiceName())
                                 .setRemoveExpiredService(true)
                                 .build();
@@ -1787,7 +1797,7 @@
         am.addOnUidImportanceListener(new UidImportanceListener(handler),
                 mRunningAppActiveImportanceCutoff);
 
-        final MdnsFeatureFlags flags = new MdnsFeatureFlags.Builder()
+        mMdnsFeatureFlags = new MdnsFeatureFlags.Builder()
                 .setIsMdnsOffloadFeatureEnabled(mDeps.isTetheringFeatureNotChickenedOut(
                         mContext, MdnsFeatureFlags.NSD_FORCE_DISABLE_MDNS_OFFLOAD))
                 .setIncludeInetAddressRecordsInProbing(mDeps.isFeatureEnabled(
@@ -1800,18 +1810,21 @@
                         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,
-                mContext);
+                new AdvertiserCallback(), LOGGER.forSubComponent("MdnsAdvertiser"),
+                mMdnsFeatureFlags, mContext);
         mClock = deps.makeClock();
     }
 
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsDiscoveryManager.java b/service-t/src/com/android/server/connectivity/mdns/MdnsDiscoveryManager.java
index 766f999..1d6039c 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsDiscoveryManager.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsDiscoveryManager.java
@@ -16,18 +16,19 @@
 
 package com.android.server.connectivity.mdns;
 
-import static com.android.server.connectivity.mdns.util.MdnsUtils.ensureRunningOnHandlerThread;
-
 import android.Manifest.permission;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.RequiresPermission;
 import android.os.Handler;
 import android.os.HandlerThread;
+import android.os.Looper;
 import android.util.ArrayMap;
 import android.util.Log;
 import android.util.Pair;
 
+import androidx.annotation.GuardedBy;
+
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.net.module.util.SharedLog;
 import com.android.server.connectivity.mdns.util.MdnsUtils;
@@ -36,6 +37,7 @@
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Objects;
+import java.util.concurrent.Executor;
 
 /**
  * This class keeps tracking the set of registered {@link MdnsServiceBrowserListener} instances, and
@@ -50,11 +52,13 @@
     @NonNull private final SharedLog sharedLog;
 
     @NonNull private final PerSocketServiceTypeClients perSocketServiceTypeClients;
-    @NonNull private final Handler handler;
-    @Nullable private final HandlerThread handlerThread;
-    @NonNull private final MdnsServiceCache serviceCache;
+    @NonNull private final DiscoveryExecutor discoveryExecutor;
     @NonNull private final MdnsFeatureFlags mdnsFeatureFlags;
 
+    // Only accessed on the handler thread, initialized before first use
+    @Nullable
+    private MdnsServiceCache serviceCache;
+
     private static class PerSocketServiceTypeClients {
         private final ArrayMap<Pair<String, SocketKey>, MdnsServiceTypeClient> clients =
                 new ArrayMap<>();
@@ -125,33 +129,82 @@
         this.sharedLog = sharedLog;
         this.perSocketServiceTypeClients = new PerSocketServiceTypeClients();
         this.mdnsFeatureFlags = mdnsFeatureFlags;
-        if (socketClient.getLooper() != null) {
-            this.handlerThread = null;
-            this.handler = new Handler(socketClient.getLooper());
-            this.serviceCache = new MdnsServiceCache(socketClient.getLooper(), mdnsFeatureFlags);
-        } else {
-            this.handlerThread = new HandlerThread(MdnsDiscoveryManager.class.getSimpleName());
-            this.handlerThread.start();
-            this.handler = new Handler(handlerThread.getLooper());
-            this.serviceCache = new MdnsServiceCache(handlerThread.getLooper(), mdnsFeatureFlags);
-        }
+        this.discoveryExecutor = new DiscoveryExecutor(socketClient.getLooper());
     }
 
-    private void checkAndRunOnHandlerThread(@NonNull Runnable function) {
-        if (this.handlerThread == null) {
-            function.run();
-        } else {
+    private static class DiscoveryExecutor implements Executor {
+        private final HandlerThread handlerThread;
+
+        @GuardedBy("pendingTasks")
+        @Nullable private Handler handler;
+        @GuardedBy("pendingTasks")
+        @NonNull private final ArrayList<Runnable> pendingTasks = new ArrayList<>();
+
+        DiscoveryExecutor(@Nullable Looper defaultLooper) {
+            if (defaultLooper != null) {
+                this.handlerThread = null;
+                synchronized (pendingTasks) {
+                    this.handler = new Handler(defaultLooper);
+                }
+            } else {
+                this.handlerThread = new HandlerThread(MdnsDiscoveryManager.class.getSimpleName()) {
+                    @Override
+                    protected void onLooperPrepared() {
+                        synchronized (pendingTasks) {
+                            handler = new Handler(getLooper());
+                            for (Runnable pendingTask : pendingTasks) {
+                                handler.post(pendingTask);
+                            }
+                            pendingTasks.clear();
+                        }
+                    }
+                };
+                this.handlerThread.start();
+            }
+        }
+
+        public void checkAndRunOnHandlerThread(@NonNull Runnable function) {
+            if (this.handlerThread == null) {
+                // Callers are expected to already be running on the handler when a defaultLooper
+                // was provided
+                function.run();
+            } else {
+                execute(function);
+            }
+        }
+
+        @Override
+        public void execute(Runnable function) {
+            final Handler handler;
+            synchronized (pendingTasks) {
+                if (this.handler == null) {
+                    pendingTasks.add(function);
+                    return;
+                } else {
+                    handler = this.handler;
+                }
+            }
             handler.post(function);
         }
+
+        void shutDown() {
+            if (this.handlerThread != null) {
+                this.handlerThread.quitSafely();
+            }
+        }
+
+        void ensureRunningOnHandlerThread() {
+            synchronized (pendingTasks) {
+                MdnsUtils.ensureRunningOnHandlerThread(handler);
+            }
+        }
     }
 
     /**
      * Do the cleanup of the MdnsDiscoveryManager
      */
     public void shutDown() {
-        if (this.handlerThread != null) {
-            this.handlerThread.quitSafely();
-        }
+        discoveryExecutor.shutDown();
     }
 
     /**
@@ -169,7 +222,7 @@
             @NonNull MdnsServiceBrowserListener listener,
             @NonNull MdnsSearchOptions searchOptions) {
         sharedLog.i("Registering listener for serviceType: " + serviceType);
-        checkAndRunOnHandlerThread(() ->
+        discoveryExecutor.checkAndRunOnHandlerThread(() ->
                 handleRegisterListener(serviceType, listener, searchOptions));
     }
 
@@ -191,7 +244,7 @@
                 new MdnsSocketClientBase.SocketCreationCallback() {
                     @Override
                     public void onSocketCreated(@NonNull SocketKey socketKey) {
-                        ensureRunningOnHandlerThread(handler);
+                        discoveryExecutor.ensureRunningOnHandlerThread();
                         // All listeners of the same service types shares the same
                         // MdnsServiceTypeClient.
                         MdnsServiceTypeClient serviceTypeClient =
@@ -206,7 +259,7 @@
 
                     @Override
                     public void onSocketDestroyed(@NonNull SocketKey socketKey) {
-                        ensureRunningOnHandlerThread(handler);
+                        discoveryExecutor.ensureRunningOnHandlerThread();
                         final MdnsServiceTypeClient serviceTypeClient =
                                 perSocketServiceTypeClients.get(serviceType, socketKey);
                         if (serviceTypeClient == null) return;
@@ -229,7 +282,8 @@
     public void unregisterListener(
             @NonNull String serviceType, @NonNull MdnsServiceBrowserListener listener) {
         sharedLog.i("Unregistering listener for serviceType:" + serviceType);
-        checkAndRunOnHandlerThread(() -> handleUnregisterListener(serviceType, listener));
+        discoveryExecutor.checkAndRunOnHandlerThread(() ->
+                handleUnregisterListener(serviceType, listener));
     }
 
     private void handleUnregisterListener(
@@ -260,7 +314,7 @@
 
     @Override
     public void onResponseReceived(@NonNull MdnsPacket packet, @NonNull SocketKey socketKey) {
-        checkAndRunOnHandlerThread(() ->
+        discoveryExecutor.checkAndRunOnHandlerThread(() ->
                 handleOnResponseReceived(packet, socketKey));
     }
 
@@ -282,7 +336,7 @@
     @Override
     public void onFailedToParseMdnsResponse(int receivedPacketNumber, int errorCode,
             @NonNull SocketKey socketKey) {
-        checkAndRunOnHandlerThread(() ->
+        discoveryExecutor.checkAndRunOnHandlerThread(() ->
                 handleOnFailedToParseMdnsResponse(receivedPacketNumber, errorCode, socketKey));
     }
 
@@ -296,12 +350,17 @@
     @VisibleForTesting
     MdnsServiceTypeClient createServiceTypeClient(@NonNull String serviceType,
             @NonNull SocketKey socketKey) {
+        discoveryExecutor.ensureRunningOnHandlerThread();
         sharedLog.log("createServiceTypeClient for type:" + serviceType + " " + socketKey);
         final String tag = serviceType + "-" + socketKey.getNetwork()
                 + "/" + socketKey.getInterfaceIndex();
+        final Looper looper = Looper.myLooper();
+        if (serviceCache == null) {
+            serviceCache = new MdnsServiceCache(looper, mdnsFeatureFlags);
+        }
         return new MdnsServiceTypeClient(
                 serviceType, socketClient,
                 executorProvider.newServiceTypeClientSchedulerExecutor(), socketKey,
-                sharedLog.forSubComponent(tag), handler.getLooper(), serviceCache);
+                sharedLog.forSubComponent(tag), looper, serviceCache);
     }
 }
\ No newline at end of file
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsFeatureFlags.java b/service-t/src/com/android/server/connectivity/mdns/MdnsFeatureFlags.java
index 9466162..fe9bbba 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsFeatureFlags.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsFeatureFlags.java
@@ -57,6 +57,11 @@
      */
     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;
 
@@ -75,6 +80,9 @@
     // 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;
 
@@ -103,6 +111,13 @@
     }
 
     /**
+     * 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,
@@ -111,6 +126,7 @@
             boolean isLabelCountLimitEnabled,
             boolean isKnownAnswerSuppressionEnabled,
             boolean isUnicastReplyEnabled,
+            boolean isAggressiveQueryModeEnabled,
             @Nullable FlagOverrideProvider overrideProvider) {
         mIsMdnsOffloadFeatureEnabled = isOffloadFeatureEnabled;
         mIncludeInetAddressRecordsInProbing = includeInetAddressRecordsInProbing;
@@ -118,6 +134,7 @@
         mIsLabelCountLimitEnabled = isLabelCountLimitEnabled;
         mIsKnownAnswerSuppressionEnabled = isKnownAnswerSuppressionEnabled;
         mIsUnicastReplyEnabled = isUnicastReplyEnabled;
+        mIsAggressiveQueryModeEnabled = isAggressiveQueryModeEnabled;
         mOverrideProvider = overrideProvider;
     }
 
@@ -136,6 +153,7 @@
         private boolean mIsLabelCountLimitEnabled;
         private boolean mIsKnownAnswerSuppressionEnabled;
         private boolean mIsUnicastReplyEnabled;
+        private boolean mIsAggressiveQueryModeEnabled;
         private FlagOverrideProvider mOverrideProvider;
 
         /**
@@ -148,6 +166,7 @@
             mIsLabelCountLimitEnabled = true; // Default enabled.
             mIsKnownAnswerSuppressionEnabled = false;
             mIsUnicastReplyEnabled = true;
+            mIsAggressiveQueryModeEnabled = false;
             mOverrideProvider = null;
         }
 
@@ -224,6 +243,16 @@
         }
 
         /**
+         * 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() {
@@ -233,6 +262,7 @@
                     mIsLabelCountLimitEnabled,
                     mIsKnownAnswerSuppressionEnabled,
                     mIsUnicastReplyEnabled,
+                    mIsAggressiveQueryModeEnabled,
                     mOverrideProvider);
         }
     }
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 e52c995..086094b 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsSearchOptions.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsSearchOptions.java
@@ -44,6 +44,10 @@
     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 =
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 0779498..4cb88b4 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
@@ -39,6 +39,7 @@
 import java.net.Inet6Address;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.Iterator;
 import java.util.List;
@@ -81,7 +82,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 +96,32 @@
     private long currentSessionId = 0;
     private long lastSentTime;
 
+    private static class ListenerInfo {
+        @NonNull
+        final MdnsSearchOptions searchOptions;
+        final Set<String> discoveredServiceNames;
+
+        ListenerInfo(@NonNull MdnsSearchOptions searchOptions,
+                @Nullable ListenerInfo previousInfo) {
+            this.searchOptions = searchOptions;
+            this.discoveredServiceNames = previousInfo == null
+                    ? MdnsUtils.newSet() : previousInfo.discoveredServiceNames;
+        }
+
+        /**
+         * Set the given service name as discovered.
+         *
+         * @return true if the service name was not discovered before.
+         */
+        boolean setServiceDiscovered(@NonNull String serviceName) {
+            return discoveredServiceNames.add(MdnsUtils.toDnsLowerCase(serviceName));
+        }
+
+        void unsetServiceDiscovered(@NonNull String serviceName) {
+            discoveredServiceNames.remove(MdnsUtils.toDnsLowerCase(serviceName));
+        }
+    }
+
     private class QueryTaskHandler extends Handler {
         QueryTaskHandler(Looper looper) {
             super(looper);
@@ -113,6 +140,7 @@
                     // before sending the query, it needs to be called just before sending it.
                     final List<MdnsResponse> servicesToResolve = makeResponsesForResolve(socketKey);
                     final QueryTask queryTask = new QueryTask(taskArgs, servicesToResolve,
+                            getAllDiscoverySubtypes(),
                             servicesToResolve.size() < listeners.size() /* sendDiscoveryQueries */);
                     executor.submit(queryTask);
                     break;
@@ -311,12 +339,16 @@
         ensureRunningOnHandlerThread(handler);
         this.searchOptions = searchOptions;
         boolean hadReply = false;
-        if (listeners.put(listener, searchOptions) == null) {
+        final ListenerInfo existingInfo = listeners.get(listener);
+        final ListenerInfo listenerInfo = new ListenerInfo(searchOptions, existingInfo);
+        listeners.put(listener, listenerInfo);
+        if (existingInfo == null) {
             for (MdnsResponse existingResponse : serviceCache.getCachedServices(cacheKey)) {
                 if (!responseMatchesOptions(existingResponse, searchOptions)) continue;
                 final MdnsServiceInfo info =
                         buildMdnsServiceInfoFromResponse(existingResponse, serviceTypeLabels);
                 listener.onServiceNameDiscovered(info, true /* isServiceFromCache */);
+                listenerInfo.setServiceDiscovered(info.getServiceInstanceName());
                 if (existingResponse.isComplete()) {
                     listener.onServiceFound(info, true /* isServiceFromCache */);
                     hadReply = true;
@@ -329,7 +361,6 @@
         // Keep tracking the ScheduledFuture for the task so we can cancel it if caller is not
         // interested anymore.
         final QueryTaskConfig taskConfig = new QueryTaskConfig(
-                searchOptions.getSubtypes(),
                 searchOptions.getQueryMode(),
                 searchOptions.onlyUseIpv6OnIpv6OnlyNetworks(),
                 searchOptions.numOfQueriesBeforeBackoff(),
@@ -357,6 +388,7 @@
             final QueryTask queryTask = new QueryTask(
                     mdnsQueryScheduler.scheduleFirstRun(taskConfig, now,
                             minRemainingTtl, currentSessionId), servicesToResolve,
+                    getAllDiscoverySubtypes(),
                     servicesToResolve.size() < listeners.size() /* sendDiscoveryQueries */);
             executor.submit(queryTask);
         }
@@ -364,6 +396,15 @@
         serviceCache.registerServiceExpiredCallback(cacheKey, serviceExpiredCallback);
     }
 
+    private Set<String> getAllDiscoverySubtypes() {
+        final Set<String> subtypes = MdnsUtils.newSet();
+        for (int i = 0; i < listeners.size(); i++) {
+            final MdnsSearchOptions listenerOptions = listeners.valueAt(i).searchOptions;
+            subtypes.addAll(listenerOptions.getSubtypes());
+        }
+        return subtypes;
+    }
+
     /**
      * Get the executor service.
      */
@@ -480,9 +521,10 @@
     private void notifyRemovedServiceToListeners(@NonNull MdnsResponse response,
             @NonNull String message) {
         for (int i = 0; i < listeners.size(); i++) {
-            if (!responseMatchesOptions(response, listeners.valueAt(i))) continue;
+            if (!responseMatchesOptions(response, listeners.valueAt(i).searchOptions)) continue;
             final MdnsServiceBrowserListener listener = listeners.keyAt(i);
             if (response.getServiceInstanceName() != null) {
+                listeners.valueAt(i).unsetServiceDiscovered(response.getServiceInstanceName());
                 final MdnsServiceInfo serviceInfo = buildMdnsServiceInfoFromResponse(
                         response, serviceTypeLabels);
                 if (response.isComplete()) {
@@ -511,10 +553,9 @@
         final MdnsResponse currentResponse =
                 serviceCache.getCachedService(serviceInstanceName, cacheKey);
 
-        boolean newServiceFound = false;
+        final boolean newInCache = currentResponse == null;
         boolean serviceBecomesComplete = false;
-        if (currentResponse == null) {
-            newServiceFound = true;
+        if (newInCache) {
             if (serviceInstanceName != null) {
                 serviceCache.addOrUpdateService(cacheKey, response);
             }
@@ -525,25 +566,28 @@
             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 */);
             }
 
             if (response.isComplete()) {
-                // There is a bug here: the newServiceFound is global right now. The state needs
-                // to be per listener because of the  responseMatchesOptions() filter.
-                // Otherwise, it won't handle the subType update properly.
                 if (newServiceFound || serviceBecomesComplete) {
                     sharedLog.log("onServiceFound: " + serviceInfo);
                     listener.onServiceFound(serviceInfo, false /* isServiceFromCache */);
@@ -579,7 +623,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;
             }
@@ -631,11 +675,15 @@
     private class QueryTask implements Runnable {
         private final MdnsQueryScheduler.ScheduledQueryTaskArgs taskArgs;
         private final List<MdnsResponse> servicesToResolve = new ArrayList<>();
+        private final List<String> subtypes = new ArrayList<>();
         private final boolean sendDiscoveryQueries;
         QueryTask(@NonNull MdnsQueryScheduler.ScheduledQueryTaskArgs taskArgs,
-                @NonNull List<MdnsResponse> servicesToResolve, boolean sendDiscoveryQueries) {
+                @NonNull Collection<MdnsResponse> servicesToResolve,
+                @NonNull Collection<String> subtypes,
+                boolean sendDiscoveryQueries) {
             this.taskArgs = taskArgs;
             this.servicesToResolve.addAll(servicesToResolve);
+            this.subtypes.addAll(subtypes);
             this.sendDiscoveryQueries = sendDiscoveryQueries;
         }
 
@@ -648,7 +696,7 @@
                                 socketClient,
                                 createMdnsPacketWriter(),
                                 serviceType,
-                                taskArgs.config.subtypes,
+                                subtypes,
                                 taskArgs.config.expectUnicastResponse,
                                 taskArgs.config.transactionId,
                                 taskArgs.config.socketKey,
@@ -660,7 +708,7 @@
                                 .call();
             } catch (RuntimeException e) {
                 sharedLog.e(String.format("Failed to run EnqueueMdnsQueryCallable for subtype: %s",
-                        TextUtils.join(",", taskArgs.config.subtypes)), e);
+                        TextUtils.join(",", subtypes)), e);
                 result = Pair.create(INVALID_TRANSACTION_ID, new ArrayList<>());
             }
             dependencies.sendMessage(
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 6d92bd2..0894166 100644
--- a/service-t/src/com/android/server/connectivity/mdns/QueryTaskConfig.java
+++ b/service-t/src/com/android/server/connectivity/mdns/QueryTaskConfig.java
@@ -16,6 +16,7 @@
 
 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;
@@ -23,10 +24,6 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.List;
-
 /**
  * A configuration for the PeriodicalQueryTask that contains parameters to build a query packet.
  * Call to getConfigForNextRun returns a config that can be used to build the next query task.
@@ -35,16 +32,23 @@
 
     private static final int INITIAL_TIME_BETWEEN_BURSTS_MS =
             (int) MdnsConfigs.initialTimeBetweenBurstsMs();
-    private static final int TIME_BETWEEN_BURSTS_MS = (int) MdnsConfigs.timeBetweenBurstsMs();
+    private static final int MAX_TIME_BETWEEN_ACTIVE_PASSIVE_BURSTS_MS =
+            (int) MdnsConfigs.timeBetweenBurstsMs();
     private static final int QUERIES_PER_BURST = (int) MdnsConfigs.queriesPerBurst();
     private static final int TIME_BETWEEN_QUERIES_IN_BURST_MS =
             (int) MdnsConfigs.timeBetweenQueriesInBurstMs();
     private static final int QUERIES_PER_BURST_PASSIVE_MODE =
             (int) MdnsConfigs.queriesPerBurstPassive();
     private static final int UNSIGNED_SHORT_MAX_VALUE = 65536;
-    // The following fields are used by QueryTask so we need to test them.
     @VisibleForTesting
-    final List<String> subtypes;
+    // RFC 6762 5.2: The interval between the first two queries MUST be at least one second.
+    static final int INITIAL_AGGRESSIVE_TIME_BETWEEN_BURSTS_MS = 1000;
+    @VisibleForTesting
+    // Basically this tries to send one query per typical DTIM interval 100ms, to maximize the
+    // chances that a query will be received if devices are using a DTIM multiplier (in which case
+    // they only listen once every [multiplier] DTIM intervals).
+    static final int TIME_BETWEEN_RETRANSMISSION_QUERIES_IN_BURST_MS = 100;
+    static final int MAX_TIME_BETWEEN_AGGRESSIVE_BURSTS_MS = 60000;
     private final boolean alwaysAskForUnicastResponse =
             MdnsConfigs.alwaysAskForUnicastResponseInEachBurst();
     private final int queryMode;
@@ -67,7 +71,6 @@
             boolean expectUnicastResponse, boolean isFirstBurst, int burstCounter,
             int queriesPerBurst, int timeBetweenBurstsInMs,
             long delayUntilNextTaskWithoutBackoffMs) {
-        this.subtypes = new ArrayList<>(other.subtypes);
         this.queryMode = other.queryMode;
         this.onlyUseIpv6OnIpv6OnlyNetworks = other.onlyUseIpv6OnIpv6OnlyNetworks;
         this.numOfQueriesBeforeBackoff = other.numOfQueriesBeforeBackoff;
@@ -81,36 +84,72 @@
         this.queryCount = queryCount;
         this.socketKey = other.socketKey;
     }
-    QueryTaskConfig(@NonNull Collection<String> subtypes,
-            int queryMode,
+
+    QueryTaskConfig(int queryMode,
             boolean onlyUseIpv6OnIpv6OnlyNetworks,
             int numOfQueriesBeforeBackoff,
             @Nullable SocketKey socketKey) {
         this.queryMode = queryMode;
         this.onlyUseIpv6OnIpv6OnlyNetworks = onlyUseIpv6OnIpv6OnlyNetworks;
         this.numOfQueriesBeforeBackoff = numOfQueriesBeforeBackoff;
-        this.subtypes = new ArrayList<>(subtypes);
         this.queriesPerBurst = QUERIES_PER_BURST;
         this.burstCounter = 0;
         this.transactionId = 1;
         this.expectUnicastResponse = true;
         this.isFirstBurst = true;
         // Config the scan frequency based on the scan mode.
-        if (this.queryMode == PASSIVE_QUERY_MODE) {
+        if (this.queryMode == AGGRESSIVE_QUERY_MODE) {
+            this.timeBetweenBurstsInMs = INITIAL_AGGRESSIVE_TIME_BETWEEN_BURSTS_MS;
+            this.delayUntilNextTaskWithoutBackoffMs =
+                    TIME_BETWEEN_RETRANSMISSION_QUERIES_IN_BURST_MS;
+        } else if (this.queryMode == PASSIVE_QUERY_MODE) {
             // In passive scan mode, sends a single burst of QUERIES_PER_BURST queries, and then
             // in each TIME_BETWEEN_BURSTS interval, sends QUERIES_PER_BURST_PASSIVE_MODE
             // queries.
-            this.timeBetweenBurstsInMs = 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);
     }
 
     /**
@@ -122,43 +161,26 @@
         if (newTransactionId > UNSIGNED_SHORT_MAX_VALUE) {
             newTransactionId = 1;
         }
-        boolean newExpectUnicastResponse = false;
-        boolean newIsFirstBurst = isFirstBurst;
+
         int newQueriesPerBurst = queriesPerBurst;
         int newBurstCounter = burstCounter + 1;
-        long newDelayUntilNextTaskWithoutBackoffMs = delayUntilNextTaskWithoutBackoffMs;
-        int newTimeBetweenBurstsInMs = timeBetweenBurstsInMs;
-        // Only the first query expects uni-cast response.
-        if (newBurstCounter == queriesPerBurst) {
+        final boolean isFirstQueryInBurst = newBurstCounter == 1;
+        final boolean isLastQueryInBurst = newBurstCounter == queriesPerBurst;
+        boolean newIsFirstBurst = isFirstBurst && !isLastQueryInBurst;
+        if (isLastQueryInBurst) {
             newBurstCounter = 0;
-
-            if (alwaysAskForUnicastResponse) {
-                newExpectUnicastResponse = true;
-            }
             // In passive scan mode, sends a single burst of QUERIES_PER_BURST queries, and
             // then in each TIME_BETWEEN_BURSTS interval, sends QUERIES_PER_BURST_PASSIVE_MODE
             // queries.
-            if (isFirstBurst) {
-                newIsFirstBurst = false;
-                if (queryMode == PASSIVE_QUERY_MODE) {
-                    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/Android.bp b/service/Android.bp
index 0d7e8d0..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"],
 }
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/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/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/connectivity/KeepaliveStatsTracker.java b/service/src/com/android/server/connectivity/KeepaliveStatsTracker.java
index 7a8b41b..48af9fa 100644
--- a/service/src/com/android/server/connectivity/KeepaliveStatsTracker.java
+++ b/service/src/com/android/server/connectivity/KeepaliveStatsTracker.java
@@ -74,9 +74,10 @@
 public class KeepaliveStatsTracker {
     private static final String TAG = KeepaliveStatsTracker.class.getSimpleName();
     private static final int INVALID_KEEPALIVE_ID = -1;
-    // 1 hour acceptable deviation in metrics collection duration time.
+    // 2 hour acceptable deviation in metrics collection duration time to account for the 1 hour
+    // window of AlarmManager.
     private static final long MAX_EXPECTED_DURATION_MS =
-            AutomaticOnOffKeepaliveTracker.METRICS_COLLECTION_DURATION_MS + 1 * 60 * 60 * 1_000L;
+            AutomaticOnOffKeepaliveTracker.METRICS_COLLECTION_DURATION_MS + 2 * 60 * 60 * 1_000L;
 
     @NonNull private final Handler mConnectivityServiceHandler;
     @NonNull private final Dependencies mDependencies;
diff --git a/staticlibs/Android.bp b/staticlibs/Android.bp
index 8f018c0..3cbabcc 100644
--- a/staticlibs/Android.bp
+++ b/staticlibs/Android.bp
@@ -28,34 +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",
-      "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: [
+    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__",
@@ -65,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",
@@ -95,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: [
@@ -464,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/netlink/InetDiagMessage.java b/staticlibs/device/com/android/net/module/util/netlink/InetDiagMessage.java
index b980c7d..fecaa09 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/InetDiagMessage.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/InetDiagMessage.java
@@ -31,6 +31,7 @@
 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;
@@ -281,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
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 81adcd6..541a375 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/NetlinkUtils.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/NetlinkUtils.java
@@ -86,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
@@ -160,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);
@@ -224,22 +225,41 @@
     }
 
     /**
-     * Create netlink socket with the given netlink protocol type.
+     * Create netlink socket with the given netlink protocol type and buffersize.
+     *
+     * @param nlProto the netlink protocol
+     * @param bufferSize the receive buffer size to set when the value is not 0
      *
      * @return fd the fileDescriptor of the socket.
      * @throws ErrnoException if the FileDescriptor not connect to be created successfully
      */
-    public static FileDescriptor netlinkSocketForProto(int nlProto) throws ErrnoException {
+    public static FileDescriptor netlinkSocketForProto(int nlProto, int bufferSize)
+            throws ErrnoException {
         final FileDescriptor fd = Os.socket(AF_NETLINK, SOCK_DGRAM | SOCK_CLOEXEC, nlProto);
-        Os.setsockoptInt(fd, SOL_SOCKET, SO_RCVBUF, SOCKET_RECV_BUFSIZE);
+        if (bufferSize > 0) {
+            Os.setsockoptInt(fd, SOL_SOCKET, SO_RCVBUF, bufferSize);
+        }
         return fd;
     }
 
     /**
+     * Create netlink socket with the given netlink protocol type. Receive buffer size is not set.
+     *
+     * @param nlProto the netlink protocol
+     *
+     * @return fd the fileDescriptor of the socket.
+     * @throws ErrnoException if the FileDescriptor not connect to be created successfully
+     */
+    public static FileDescriptor netlinkSocketForProto(int nlProto)
+            throws ErrnoException {
+        return netlinkSocketForProto(nlProto, 0);
+    }
+
+    /**
      * Construct a netlink inet_diag socket.
      */
     public static FileDescriptor createNetLinkInetDiagSocket() throws ErrnoException {
-        return Os.socket(AF_NETLINK, SOCK_DGRAM | SOCK_CLOEXEC, NETLINK_INET_DIAG);
+        return netlinkSocketForProto(NETLINK_INET_DIAG);
     }
 
     /**
@@ -374,7 +394,7 @@
             Consumer<T> func)
             throws SocketException, InterruptedIOException, ErrnoException {
         // Create socket
-        final FileDescriptor fd = netlinkSocketForProto(nlFamily);
+        final FileDescriptor fd = netlinkSocketForProto(nlFamily, SOCKET_DUMP_RECV_BUFSIZE);
         try {
             getAndProcessNetlinkDumpMessagesWithFd(fd, dumpRequestMessage, nlFamily,
                     msgClass, func);
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_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 2b7e620..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"],
 }
 
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/tests/unit/Android.bp b/staticlibs/tests/unit/Android.bp
index 0dfca57..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: [
@@ -35,7 +39,7 @@
     ],
     lint: {
         strict_updatability_linting: true,
-        test: true
+        test: true,
     },
 }
 
@@ -52,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/netlink/NetlinkUtilsTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/NetlinkUtilsTest.java
index 0958f11..f64adb8 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/NetlinkUtilsTest.java
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/NetlinkUtilsTest.java
@@ -21,6 +21,8 @@
 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;
@@ -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;
 
@@ -204,4 +209,23 @@
             assertNotNull("Route doesn't contain destination: " + route, route.getDestination());
         }
     }
+
+    @Test @IgnoreUpTo(Build.VERSION_CODES.R) // getsockoptInt requires > R
+    public void testNetlinkSocketForProto_defaultBufferSize() throws Exception {
+        final FileDescriptor fd = NetlinkUtils.netlinkSocketForProto(NETLINK_ROUTE);
+        final int bufferSize = Os.getsockoptInt(fd, SOL_SOCKET, SO_RCVBUF) / 2;
+
+        assertTrue("bufferSize: " + bufferSize, bufferSize > 0); // whatever the default value is
+        SocketUtils.closeSocket(fd);
+    }
+
+    @Test @IgnoreUpTo(Build.VERSION_CODES.R) // getsockoptInt requires > R
+    public void testNetlinkSocketForProto_setBufferSize() throws Exception {
+        final FileDescriptor fd = NetlinkUtils.netlinkSocketForProto(NETLINK_ROUTE,
+                8000);
+        final int bufferSize = Os.getsockoptInt(fd, SOL_SOCKET, SO_RCVBUF) / 2;
+
+        assertEquals(8000, bufferSize);
+        SocketUtils.closeSocket(fd);
+    }
 }
diff --git a/staticlibs/testutils/Android.bp b/staticlibs/testutils/Android.bp
index 43853ee..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 {
diff --git a/staticlibs/testutils/app/connectivitychecker/Android.bp b/staticlibs/testutils/app/connectivitychecker/Android.bp
index 049ec9e..5af8c14 100644
--- a/staticlibs/testutils/app/connectivitychecker/Android.bp
+++ b/staticlibs/testutils/app/connectivitychecker/Android.bp
@@ -13,6 +13,7 @@
 // limitations under the License.
 
 package {
+    default_team: "trendy_team_fwk_core_networking",
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
@@ -30,5 +31,7 @@
         "net-tests-utils",
     ],
     host_required: ["net-tests-utils-host-common"],
-    lint: { strict_updatability_linting: true },
+    lint: {
+        strict_updatability_linting: true,
+    },
 }
diff --git a/staticlibs/testutils/devicetests/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/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/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/cts/hostside/Android.bp b/tests/cts/hostside/Android.bp
index a0aafc6..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"],
 }
 
@@ -45,7 +42,7 @@
         "general-tests",
         "mcts-tethering",
         "mts-tethering",
-        "sts"
+        "sts",
     ],
     data: [
         ":CtsHostsideNetworkTestsApp",
diff --git a/tests/cts/hostside/aidl/Android.bp b/tests/cts/hostside/aidl/Android.bp
index 2751f6f..18a5897 100644
--- a/tests/cts/hostside/aidl/Android.bp
+++ b/tests/cts/hostside/aidl/Android.bp
@@ -13,6 +13,7 @@
 // limitations under the License.
 
 package {
+    default_team: "trendy_team_fwk_core_networking",
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
diff --git a/tests/cts/hostside/app/Android.bp b/tests/cts/hostside/app/Android.bp
index 470bb17..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/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/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 f0e0ae8..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"],
 }
 
@@ -69,7 +70,7 @@
     data: [
         ":ConnectivityTestPreparer",
         ":CtsCarrierServicePackage",
-    ]
+    ],
 }
 
 // Networking CTS tests for development and release. These tests always target the platform SDK
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/appForApi23/Android.bp b/tests/cts/net/appForApi23/Android.bp
index b39690f..d300743 100644
--- a/tests/cts/net/appForApi23/Android.bp
+++ b/tests/cts/net/appForApi23/Android.bp
@@ -13,6 +13,7 @@
 // limitations under the License.
 
 package {
+    default_team: "trendy_team_fwk_core_networking",
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
diff --git a/tests/cts/net/jni/Android.bp b/tests/cts/net/jni/Android.bp
index a421349..fbf4f29 100644
--- a/tests/cts/net/jni/Android.bp
+++ b/tests/cts/net/jni/Android.bp
@@ -13,6 +13,7 @@
 // limitations under the License.
 
 package {
+    default_team: "trendy_team_fwk_core_networking",
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
diff --git a/tests/cts/net/native/Android.bp b/tests/cts/net/native/Android.bp
index 153ff51..3f24592 100644
--- a/tests/cts/net/native/Android.bp
+++ b/tests/cts/net/native/Android.bp
@@ -15,6 +15,7 @@
 // Build the unit tests.
 
 package {
+    default_team: "trendy_team_fwk_core_networking",
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
diff --git a/tests/cts/net/native/dns/Android.bp b/tests/cts/net/native/dns/Android.bp
index a9e3715..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"],
 }
 
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/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/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/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/com/android/server/connectivity/KeepaliveStatsTrackerTest.java b/tests/unit/java/com/android/server/connectivity/KeepaliveStatsTrackerTest.java
index 1b964e2..294dacb 100644
--- a/tests/unit/java/com/android/server/connectivity/KeepaliveStatsTrackerTest.java
+++ b/tests/unit/java/com/android/server/connectivity/KeepaliveStatsTrackerTest.java
@@ -1297,8 +1297,8 @@
 
         assertTrue(mKeepaliveStatsTracker.allMetricsExpected(dailyKeepaliveInfoReported));
 
-        // Write time after 26 hours.
-        final int writeTime2 = 26 * 60 * 60 * 1000;
+        // Write time after 27 hours.
+        final int writeTime2 = 27 * 60 * 60 * 1000;
         setElapsedRealtime(writeTime2);
 
         visibleOnHandlerThread(mTestHandler, () -> mKeepaliveStatsTracker.writeAndResetMetrics());
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java
index a8cfe63..58124f3 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java
@@ -17,8 +17,12 @@
 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;
@@ -147,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) {
@@ -173,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])
@@ -191,7 +203,15 @@
                 .thenReturn(expectedIPv6Packets[12])
                 .thenReturn(expectedIPv6Packets[13])
                 .thenReturn(expectedIPv6Packets[14])
-                .thenReturn(expectedIPv6Packets[15]);
+                .thenReturn(expectedIPv6Packets[15])
+                .thenReturn(expectedIPv6Packets[16])
+                .thenReturn(expectedIPv6Packets[17])
+                .thenReturn(expectedIPv6Packets[18])
+                .thenReturn(expectedIPv6Packets[19])
+                .thenReturn(expectedIPv6Packets[20])
+                .thenReturn(expectedIPv6Packets[21])
+                .thenReturn(expectedIPv6Packets[22])
+                .thenReturn(expectedIPv6Packets[23]);
 
         thread = new HandlerThread("MdnsServiceTypeClientTests");
         thread.start();
@@ -221,15 +241,22 @@
             return true;
         }).when(mockDeps).sendMessage(any(Handler.class), any(Message.class));
 
-        client =
-                new MdnsServiceTypeClient(SERVICE_TYPE, mockSocketClient, currentThreadExecutor,
-                        mockDecoderClock, socketKey, mockSharedLog, thread.getLooper(), mockDeps,
-                        serviceCache) {
-                    @Override
-                    MdnsPacketWriter createMdnsPacketWriter() {
-                        return mockPacketWriter;
-                    }
-                };
+        client = makeMdnsServiceTypeClient(mockPacketWriter);
+    }
+
+    private MdnsServiceTypeClient makeMdnsServiceTypeClient(
+            @Nullable MdnsPacketWriter packetWriter) {
+        return new MdnsServiceTypeClient(SERVICE_TYPE, mockSocketClient, currentThreadExecutor,
+                mockDecoderClock, socketKey, mockSharedLog, thread.getLooper(), mockDeps,
+                serviceCache) {
+            @Override
+            MdnsPacketWriter createMdnsPacketWriter() {
+                if (packetWriter == null) {
+                    return super.createMdnsPacketWriter();
+                }
+                return packetWriter;
+            }
+        };
     }
 
     @After
@@ -542,13 +569,12 @@
         MdnsSearchOptions searchOptions = MdnsSearchOptions.newBuilder()
                 .addSubtype(SUBTYPE).setQueryMode(ACTIVE_QUERY_MODE).build();
         QueryTaskConfig config = new QueryTaskConfig(
-                searchOptions.getSubtypes(), searchOptions.getQueryMode(),
+                searchOptions.getQueryMode(),
                 false /* onlyUseIpv6OnIpv6OnlyNetworks */, 3 /* numOfQueriesBeforeBackoff */,
                 socketKey);
 
         // This is the first query. We will ask for unicast response.
         assertTrue(config.expectUnicastResponse);
-        assertEquals(config.subtypes, searchOptions.getSubtypes());
         assertEquals(config.transactionId, 1);
 
         // For the rest of queries in this burst, we will NOT ask for unicast response.
@@ -556,7 +582,6 @@
             int oldTransactionId = config.transactionId;
             config = config.getConfigForNextRun();
             assertFalse(config.expectUnicastResponse);
-            assertEquals(config.subtypes, searchOptions.getSubtypes());
             assertEquals(config.transactionId, oldTransactionId + 1);
         }
 
@@ -564,7 +589,6 @@
         int oldTransactionId = config.transactionId;
         config = config.getConfigForNextRun();
         assertTrue(config.expectUnicastResponse);
-        assertEquals(config.subtypes, searchOptions.getSubtypes());
         assertEquals(config.transactionId, oldTransactionId + 1);
     }
 
@@ -573,13 +597,12 @@
         MdnsSearchOptions searchOptions = MdnsSearchOptions.newBuilder()
                 .addSubtype(SUBTYPE).setQueryMode(ACTIVE_QUERY_MODE).build();
         QueryTaskConfig config = new QueryTaskConfig(
-                searchOptions.getSubtypes(), searchOptions.getQueryMode(),
+                searchOptions.getQueryMode(),
                 false /* onlyUseIpv6OnIpv6OnlyNetworks */, 3 /* numOfQueriesBeforeBackoff */,
                 socketKey);
 
         // This is the first query. We will ask for unicast response.
         assertTrue(config.expectUnicastResponse);
-        assertEquals(config.subtypes, searchOptions.getSubtypes());
         assertEquals(config.transactionId, 1);
 
         // For the rest of queries in this burst, we will NOT ask for unicast response.
@@ -587,7 +610,6 @@
             int oldTransactionId = config.transactionId;
             config = config.getConfigForNextRun();
             assertFalse(config.expectUnicastResponse);
-            assertEquals(config.subtypes, searchOptions.getSubtypes());
             assertEquals(config.transactionId, oldTransactionId + 1);
         }
 
@@ -595,7 +617,6 @@
         int oldTransactionId = config.transactionId;
         config = config.getConfigForNextRun();
         assertFalse(config.expectUnicastResponse);
-        assertEquals(config.subtypes, searchOptions.getSubtypes());
         assertEquals(config.transactionId, oldTransactionId + 1);
     }
 
@@ -673,6 +694,81 @@
                 any(), any(), eq(MdnsConfigs.timeBetweenQueriesInBurstMs()));
     }
 
+    @Test
+    public void testCombinedSubtypesQueriedWithMultipleListeners() throws Exception {
+        client = makeMdnsServiceTypeClient(/* packetWriter= */ null);
+        final MdnsSearchOptions searchOptions1 = MdnsSearchOptions.newBuilder()
+                .addSubtype("subtype1").build();
+        final MdnsSearchOptions searchOptions2 = MdnsSearchOptions.newBuilder()
+                .addSubtype("subtype2").build();
+        startSendAndReceive(mockListenerOne, searchOptions1);
+        currentThreadExecutor.getAndClearSubmittedRunnable().run();
+
+        InOrder inOrder = inOrder(mockListenerOne, mockSocketClient, mockDeps);
+
+        // Verify the query asks for subtype1
+        final ArgumentCaptor<DatagramPacket> subtype1QueryCaptor =
+                ArgumentCaptor.forClass(DatagramPacket.class);
+        currentThreadExecutor.getAndClearLastScheduledRunnable().run();
+        // Send twice for IPv4 and IPv6
+        inOrder.verify(mockSocketClient, times(2)).sendPacketRequestingUnicastResponse(
+                subtype1QueryCaptor.capture(),
+                eq(socketKey), eq(false));
+
+        final MdnsPacket subtype1Query = MdnsPacket.parse(
+                new MdnsPacketReader(subtype1QueryCaptor.getValue()));
+
+        assertEquals(2, subtype1Query.questions.size());
+        assertTrue(hasQuestion(subtype1Query, MdnsRecord.TYPE_PTR, SERVICE_TYPE_LABELS));
+        assertTrue(hasQuestion(subtype1Query, MdnsRecord.TYPE_PTR,
+                getServiceTypeWithSubtype("_subtype1")));
+
+        // Add subtype2
+        startSendAndReceive(mockListenerTwo, searchOptions2);
+        inOrder.verify(mockDeps).removeMessages(any(), eq(EVENT_START_QUERYTASK));
+        currentThreadExecutor.getAndClearLastScheduledRunnable().run();
+
+        final ArgumentCaptor<DatagramPacket> combinedSubtypesQueryCaptor =
+                ArgumentCaptor.forClass(DatagramPacket.class);
+        inOrder.verify(mockSocketClient, times(2)).sendPacketRequestingUnicastResponse(
+                combinedSubtypesQueryCaptor.capture(),
+                eq(socketKey), eq(false));
+        // The next query must have been scheduled
+        inOrder.verify(mockDeps).sendMessageDelayed(any(), any(), anyLong());
+
+        final MdnsPacket combinedSubtypesQuery = MdnsPacket.parse(
+                new MdnsPacketReader(combinedSubtypesQueryCaptor.getValue()));
+
+        assertEquals(3, combinedSubtypesQuery.questions.size());
+        assertTrue(hasQuestion(combinedSubtypesQuery, MdnsRecord.TYPE_PTR, SERVICE_TYPE_LABELS));
+        assertTrue(hasQuestion(combinedSubtypesQuery, MdnsRecord.TYPE_PTR,
+                getServiceTypeWithSubtype("_subtype1")));
+        assertTrue(hasQuestion(combinedSubtypesQuery, MdnsRecord.TYPE_PTR,
+                getServiceTypeWithSubtype("_subtype2")));
+
+        // Remove subtype1
+        stopSendAndReceive(mockListenerOne);
+
+        // Queries are not rescheduled, but the next query is affected
+        dispatchMessage();
+        currentThreadExecutor.getAndClearLastScheduledRunnable().run();
+
+        final ArgumentCaptor<DatagramPacket> subtype2QueryCaptor =
+                ArgumentCaptor.forClass(DatagramPacket.class);
+        // Send twice for IPv4 and IPv6
+        inOrder.verify(mockSocketClient, times(2)).sendPacketRequestingMulticastResponse(
+                subtype2QueryCaptor.capture(),
+                eq(socketKey), eq(false));
+
+        final MdnsPacket subtype2Query = MdnsPacket.parse(
+                new MdnsPacketReader(subtype2QueryCaptor.getValue()));
+
+        assertEquals(2, subtype2Query.questions.size());
+        assertTrue(hasQuestion(subtype2Query, MdnsRecord.TYPE_PTR, SERVICE_TYPE_LABELS));
+        assertTrue(hasQuestion(subtype2Query, MdnsRecord.TYPE_PTR,
+                getServiceTypeWithSubtype("_subtype2")));
+    }
+
     private static void verifyServiceInfo(MdnsServiceInfo serviceInfo, String serviceName,
             String[] serviceType, List<String> ipv4Addresses, List<String> ipv6Addresses, int port,
             List<String> subTypes, Map<String, String> attributes, SocketKey socketKey) {
@@ -925,15 +1021,7 @@
     public void processResponse_searchOptionsEnableServiceRemoval_shouldRemove()
             throws Exception {
         final String serviceInstanceName = "service-instance-1";
-        client =
-                new MdnsServiceTypeClient(SERVICE_TYPE, mockSocketClient, currentThreadExecutor,
-                        mockDecoderClock, socketKey, mockSharedLog, thread.getLooper(), mockDeps,
-                        serviceCache) {
-                    @Override
-                    MdnsPacketWriter createMdnsPacketWriter() {
-                        return mockPacketWriter;
-                    }
-                };
+        client = makeMdnsServiceTypeClient(mockPacketWriter);
         MdnsSearchOptions searchOptions = MdnsSearchOptions.newBuilder()
                 .setRemoveExpiredService(true)
                 .setNumOfQueriesBeforeBackoff(Integer.MAX_VALUE)
@@ -971,15 +1059,7 @@
     public void processResponse_searchOptionsNotEnableServiceRemoval_shouldNotRemove()
             throws Exception {
         final String serviceInstanceName = "service-instance-1";
-        client =
-                new MdnsServiceTypeClient(SERVICE_TYPE, mockSocketClient, currentThreadExecutor,
-                        mockDecoderClock, socketKey, mockSharedLog, thread.getLooper(), mockDeps,
-                        serviceCache) {
-                    @Override
-                    MdnsPacketWriter createMdnsPacketWriter() {
-                        return mockPacketWriter;
-                    }
-                };
+        client = makeMdnsServiceTypeClient(mockPacketWriter);
         startSendAndReceive(mockListenerOne, MdnsSearchOptions.getDefaultOptions());
         Runnable firstMdnsTask = currentThreadExecutor.getAndClearSubmittedRunnable();
 
@@ -1005,15 +1085,7 @@
             throws Exception {
         //MdnsConfigsFlagsImpl.removeServiceAfterTtlExpires.override(true);
         final String serviceInstanceName = "service-instance-1";
-        client =
-                new MdnsServiceTypeClient(SERVICE_TYPE, mockSocketClient, currentThreadExecutor,
-                        mockDecoderClock, socketKey, mockSharedLog, thread.getLooper(), mockDeps,
-                        serviceCache) {
-                    @Override
-                    MdnsPacketWriter createMdnsPacketWriter() {
-                        return mockPacketWriter;
-                    }
-                };
+        client = makeMdnsServiceTypeClient(mockPacketWriter);
         startSendAndReceive(mockListenerOne, MdnsSearchOptions.getDefaultOptions());
         Runnable firstMdnsTask = currentThreadExecutor.getAndClearSubmittedRunnable();
 
@@ -1128,9 +1200,7 @@
 
     @Test
     public void testProcessResponse_Resolve() throws Exception {
-        client = new MdnsServiceTypeClient(SERVICE_TYPE, mockSocketClient, currentThreadExecutor,
-                mockDecoderClock, socketKey, mockSharedLog, thread.getLooper(), mockDeps,
-                serviceCache);
+        client = makeMdnsServiceTypeClient(/* packetWriter= */ null);
 
         final String instanceName = "service-instance";
         final String[] hostname = new String[] { "testhost "};
@@ -1223,9 +1293,7 @@
 
     @Test
     public void testRenewTxtSrvInResolve() throws Exception {
-        client = new MdnsServiceTypeClient(SERVICE_TYPE, mockSocketClient, currentThreadExecutor,
-                mockDecoderClock, socketKey, mockSharedLog, thread.getLooper(), mockDeps,
-                serviceCache);
+        client = makeMdnsServiceTypeClient(/* packetWriter= */ null);
 
         final String instanceName = "service-instance";
         final String[] hostname = new String[] { "testhost "};
@@ -1339,9 +1407,7 @@
 
     @Test
     public void testProcessResponse_ResolveExcludesOtherServices() {
-        client = new MdnsServiceTypeClient(SERVICE_TYPE, mockSocketClient, currentThreadExecutor,
-                mockDecoderClock, socketKey, mockSharedLog, thread.getLooper(), mockDeps,
-                serviceCache);
+        client = makeMdnsServiceTypeClient(/* packetWriter= */ null);
 
         final String requestedInstance = "instance1";
         final String otherInstance = "instance2";
@@ -1409,9 +1475,7 @@
 
     @Test
     public void testProcessResponse_SubtypeDiscoveryLimitedToSubtype() {
-        client = new MdnsServiceTypeClient(SERVICE_TYPE, mockSocketClient, currentThreadExecutor,
-                mockDecoderClock, socketKey, mockSharedLog, thread.getLooper(), mockDeps,
-                serviceCache);
+        client = makeMdnsServiceTypeClient(/* packetWriter= */ null);
 
         final String matchingInstance = "instance1";
         final String subtype = "_subtype";
@@ -1498,10 +1562,91 @@
     }
 
     @Test
+    public void testProcessResponse_SubtypeChange() {
+        client = makeMdnsServiceTypeClient(/* packetWriter= */ null);
+
+        final String matchingInstance = "instance1";
+        final String subtype = "_subtype";
+        final String ipV4Address = "192.0.2.0";
+        final String ipV6Address = "2001:db8::";
+
+        final MdnsSearchOptions options = MdnsSearchOptions.newBuilder()
+                .addSubtype("othersub").build();
+
+        startSendAndReceive(mockListenerOne, options);
+
+        // Complete response from instanceName
+        final MdnsPacket packetWithoutSubtype = createResponse(
+                matchingInstance, ipV4Address, 5353, SERVICE_TYPE_LABELS,
+                Collections.emptyMap() /* textAttributes */, TEST_TTL);
+        final MdnsPointerRecord originalPtr = (MdnsPointerRecord) CollectionUtils.findFirst(
+                packetWithoutSubtype.answers, r -> r instanceof MdnsPointerRecord);
+
+        // Add a subtype PTR record
+        final ArrayList<MdnsRecord> newAnswers = new ArrayList<>(packetWithoutSubtype.answers);
+        newAnswers.add(new MdnsPointerRecord(
+                // PTR should be _subtype._sub._type._tcp.local -> instance1._type._tcp.local
+                Stream.concat(Stream.of(subtype, "_sub"), Arrays.stream(SERVICE_TYPE_LABELS))
+                        .toArray(String[]::new),
+                originalPtr.getReceiptTime(), originalPtr.getCacheFlush(), originalPtr.getTtl(),
+                originalPtr.getPointer()));
+        processResponse(new MdnsPacket(
+                packetWithoutSubtype.flags,
+                packetWithoutSubtype.questions,
+                newAnswers,
+                packetWithoutSubtype.authorityRecords,
+                packetWithoutSubtype.additionalRecords), socketKey);
+
+        // The subtype does not match
+        final InOrder inOrder = inOrder(mockListenerOne);
+        inOrder.verify(mockListenerOne, never()).onServiceNameDiscovered(any(), anyBoolean());
+
+        // Add another matching subtype
+        newAnswers.add(new MdnsPointerRecord(
+                // PTR should be _subtype._sub._type._tcp.local -> instance1._type._tcp.local
+                Stream.concat(Stream.of("_othersub", "_sub"), Arrays.stream(SERVICE_TYPE_LABELS))
+                        .toArray(String[]::new),
+                originalPtr.getReceiptTime(), originalPtr.getCacheFlush(), originalPtr.getTtl(),
+                originalPtr.getPointer()));
+        processResponse(new MdnsPacket(
+                packetWithoutSubtype.flags,
+                packetWithoutSubtype.questions,
+                newAnswers,
+                packetWithoutSubtype.authorityRecords,
+                packetWithoutSubtype.additionalRecords), socketKey);
+
+        final ArgumentMatcher<MdnsServiceInfo> subtypeInstanceMatcher = info ->
+                info.getServiceInstanceName().equals(matchingInstance)
+                        && info.getSubtypes().equals(List.of("_subtype", "_othersub"));
+
+        // Service found callbacks are sent now
+        inOrder.verify(mockListenerOne).onServiceNameDiscovered(
+                argThat(subtypeInstanceMatcher), eq(false) /* isServiceFromCache */);
+        inOrder.verify(mockListenerOne).onServiceFound(
+                argThat(subtypeInstanceMatcher), eq(false) /* isServiceFromCache */);
+
+        // Address update: update callbacks are sent
+        processResponse(createResponse(
+                matchingInstance, ipV6Address, 5353, SERVICE_TYPE_LABELS,
+                Collections.emptyMap(), TEST_TTL), socketKey);
+
+        inOrder.verify(mockListenerOne).onServiceUpdated(argThat(info ->
+                subtypeInstanceMatcher.matches(info)
+                        && info.getIpv4Addresses().equals(List.of(ipV4Address))
+                        && info.getIpv6Addresses().equals(List.of(ipV6Address))));
+
+        // Goodbye: service removed callbacks are sent
+        processResponse(createResponse(
+                matchingInstance, ipV6Address, 5353, SERVICE_TYPE_LABELS,
+                Collections.emptyMap(), 0L /* ttl */), socketKey);
+
+        inOrder.verify(mockListenerOne).onServiceRemoved(matchServiceName(matchingInstance));
+        inOrder.verify(mockListenerOne).onServiceNameRemoved(matchServiceName(matchingInstance));
+    }
+
+    @Test
     public void testNotifySocketDestroyed() throws Exception {
-        client = new MdnsServiceTypeClient(SERVICE_TYPE, mockSocketClient, currentThreadExecutor,
-                mockDecoderClock, socketKey, mockSharedLog, thread.getLooper(), mockDeps,
-                serviceCache);
+        client = makeMdnsServiceTypeClient(/* packetWriter= */ null);
 
         final String requestedInstance = "instance1";
         final String otherInstance = "instance2";
@@ -1672,6 +1817,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));
     }
@@ -1718,6 +1968,11 @@
                 Arrays.stream(SERVICE_TYPE_LABELS)).toArray(String[]::new);
     }
 
+    private static String[] getServiceTypeWithSubtype(String subtype) {
+        return Stream.concat(Stream.of(subtype, "_sub"),
+                Arrays.stream(SERVICE_TYPE_LABELS)).toArray(String[]::new);
+    }
+
     private static boolean hasQuestion(MdnsPacket packet, int type) {
         return hasQuestion(packet, type, null);
     }
diff --git a/tests/unit/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/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/demoapp/Android.bp b/thread/demoapp/Android.bp
index da7a5f8..fcfd469 100644
--- a/thread/demoapp/Android.bp
+++ b/thread/demoapp/Android.bp
@@ -13,6 +13,7 @@
 // limitations under the License.
 
 package {
+    default_team: "trendy_team_fwk_thread_network",
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
diff --git a/thread/framework/Android.bp b/thread/framework/Android.bp
index cc598d8..846253c 100644
--- a/thread/framework/Android.bp
+++ b/thread/framework/Android.bp
@@ -15,6 +15,7 @@
 //
 
 package {
+    default_team: "trendy_team_fwk_thread_network",
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
diff --git a/thread/service/Android.bp b/thread/service/Android.bp
index 69295cc..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"],
 }
 
@@ -47,9 +48,6 @@
         "ot-daemon-aidl-java",
     ],
     apex_available: ["com.android.tethering"],
-    optimize: {
-        proguard_flags_files: ["proguard.flags"],
-    },
 }
 
 cc_library_shared {
diff --git a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
index 7b9f290..b5f7230 100644
--- a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
+++ b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
@@ -66,7 +66,6 @@
 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;
@@ -79,7 +78,6 @@
 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;
@@ -151,7 +149,6 @@
     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
@@ -295,12 +292,16 @@
         return mOtDaemon;
     }
 
-    // TODO(b/309792480): restarts the OT daemon service
     private void onOtDaemonDied() {
-        Log.w(TAG, "OT daemon became dead, clean up...");
+        checkOnHandlerThread();
+        Log.w(TAG, "OT daemon is dead, clean up and restart it...");
+
         OperationReceiverWrapper.onOtDaemonDied();
         mOtDaemonCallbackProxy.onOtDaemonDied();
+        mTunIfController.onOtDaemonDied();
+
         mOtDaemon = null;
+        initializeOtDaemon();
     }
 
     public void initialize() {
@@ -313,8 +314,6 @@
                         throw new IllegalStateException(
                                 "Failed to create Thread tunnel interface", e);
                     }
-                    mLinkProperties.setInterfaceName(TUN_IF_NAME);
-                    mLinkProperties.setMtu(TunInterfaceController.MTU);
                     mConnectivityManager.registerNetworkProvider(mNetworkProvider);
                     requestUpstreamNetwork();
                     requestThreadNetwork();
@@ -365,25 +364,31 @@
         @Override
         public void onAvailable(@NonNull Network network) {
             checkOnHandlerThread();
-            Log.i(TAG, "onAvailable: " + network);
+            Log.i(TAG, "Upstream network available: " + network);
         }
 
         @Override
         public void onLost(@NonNull Network network) {
             checkOnHandlerThread();
-            Log.i(TAG, "onLost: " + network);
+            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();
-            Log.i(
-                    TAG,
-                    String.format(
-                            "onLinkPropertiesChanged: {network: %s, interface: %s}",
-                            network, linkProperties.getInterfaceName()));
-            mNetworkToInterface.put(network, linkProperties.getInterfaceName());
+
+            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));
             }
@@ -394,14 +399,20 @@
         @Override
         public void onAvailable(@NonNull Network network) {
             checkOnHandlerThread();
-            Log.i(TAG, "onAvailable: Thread network Available");
+            Log.i(TAG, "Thread network available: " + network);
         }
 
         @Override
         public void onLocalNetworkInfoChanged(
                 @NonNull Network network, @NonNull LocalNetworkInfo localNetworkInfo) {
             checkOnHandlerThread();
-            Log.i(TAG, "onLocalNetworkInfoChanged: " + localNetworkInfo);
+            Log.i(
+                    TAG,
+                    "LocalNetworkInfo of Thread network changed: {threadNetwork: "
+                            + network
+                            + ", localNetworkInfo: "
+                            + localNetworkInfo
+                            + "}");
             if (localNetworkInfo.getUpstreamNetwork() == null) {
                 mUpstreamNetwork = null;
                 return;
@@ -453,7 +464,7 @@
                 mHandler.getLooper(),
                 TAG,
                 netCaps,
-                mLinkProperties,
+                mTunIfController.getLinkProperties(),
                 newLocalNetworkConfig(),
                 score,
                 new NetworkAgentConfig.Builder().build(),
@@ -484,46 +495,6 @@
         mNetworkAgent = null;
     }
 
-    private void updateTunInterfaceAddress(LinkAddress linkAddress, boolean isAdded) {
-        try {
-            if (isAdded) {
-                mTunIfController.addAddress(linkAddress);
-            } else {
-                mTunIfController.removeAddress(linkAddress);
-            }
-        } catch (IOException e) {
-            Log.e(
-                    TAG,
-                    String.format(
-                            "Failed to %s Thread tun interface address %s",
-                            (isAdded ? "add" : "remove"), linkAddress),
-                    e);
-        }
-    }
-
-    private void updateNetworkLinkProperties(LinkAddress linkAddress, boolean isAdded) {
-        RouteInfo routeInfo =
-                new RouteInfo(
-                        new IpPrefix(linkAddress.getAddress(), 64),
-                        null,
-                        TUN_IF_NAME,
-                        RouteInfo.RTN_UNICAST,
-                        TunInterfaceController.MTU);
-        if (isAdded) {
-            mLinkProperties.addLinkAddress(linkAddress);
-            mLinkProperties.addRoute(routeInfo);
-        } else {
-            mLinkProperties.removeLinkAddress(linkAddress);
-            mLinkProperties.removeRoute(routeInfo);
-        }
-
-        // The Thread daemon can send link property updates before the networkAgent is
-        // registered
-        if (mNetworkAgent != null) {
-            mNetworkAgent.sendLinkProperties(mLinkProperties);
-        }
-    }
-
     @Override
     public int getThreadVersion() {
         return THREAD_VERSION_1_3;
@@ -829,7 +800,7 @@
                 && infraIfName.equals(mBorderRouterConfig.infraInterfaceName)) {
             return;
         }
-        Log.i(TAG, "enableBorderRouting on AIL: " + infraIfName);
+        Log.i(TAG, "Enable border routing on AIL: " + infraIfName);
         try {
             mBorderRouterConfig.infraInterfaceName = infraIfName;
             mBorderRouterConfig.infraInterfaceIcmp6Socket =
@@ -860,7 +831,7 @@
     private void handleThreadInterfaceStateChanged(boolean isUp) {
         try {
             mTunIfController.setInterfaceUp(isUp);
-            Log.d(TAG, "Thread network interface becomes " + (isUp ? "up" : "down"));
+            Log.i(TAG, "Thread TUN interface becomes " + (isUp ? "up" : "down"));
         } catch (IOException e) {
             Log.e(TAG, "Failed to handle Thread interface state changes", e);
         }
@@ -868,13 +839,13 @@
 
     private void handleDeviceRoleChanged(@DeviceRole int deviceRole) {
         if (ThreadNetworkController.isAttached(deviceRole)) {
-            Log.d(TAG, "Attached to the Thread network");
+            Log.i(TAG, "Attached to the Thread network");
 
             // This is an idempotent method which can be called for multiple times when the device
             // is already attached (e.g. going from Child to Router)
             registerThreadNetwork();
         } else {
-            Log.d(TAG, "Detached from the Thread network");
+            Log.i(TAG, "Detached from the Thread network");
 
             // This is an idempotent method which can be called for multiple times when the device
             // is already detached or stopped
@@ -891,10 +862,17 @@
         }
 
         LinkAddress linkAddress = newLinkAddress(addressInfo);
-        Log.d(TAG, (isAdded ? "Adding" : "Removing") + " address " + linkAddress);
+        if (isAdded) {
+            mTunIfController.addAddress(linkAddress);
+        } else {
+            mTunIfController.removeAddress(linkAddress);
+        }
 
-        updateTunInterfaceAddress(linkAddress, isAdded);
-        updateNetworkLinkProperties(linkAddress, isAdded);
+        // The OT daemon can send link property updates before the networkAgent is
+        // registered
+        if (mNetworkAgent != null) {
+            mNetworkAgent.sendLinkProperties(mTunIfController.getLinkProperties());
+        }
     }
 
     private boolean isMulticastForwardingEnabled() {
@@ -915,6 +893,9 @@
         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
@@ -930,10 +911,6 @@
             mDownstreamMulticastRoutingConfig = CONFIG_FORWARD_NONE;
         }
         sendLocalNetworkConfig();
-        Log.d(
-                TAG,
-                "Sent updated localNetworkConfig with multicast forwarding "
-                        + (isEnabled ? "enabled" : "disabled"));
     }
 
     private void handleMulticastForwardingAddressChanged(byte[] addressBytes, boolean isAdded) {
diff --git a/thread/service/java/com/android/server/thread/TunInterfaceController.java b/thread/service/java/com/android/server/thread/TunInterfaceController.java
index 7223b2a..b29a54f 100644
--- a/thread/service/java/com/android/server/thread/TunInterfaceController.java
+++ b/thread/service/java/com/android/server/thread/TunInterfaceController.java
@@ -17,7 +17,10 @@
 package com.android.server.thread;
 
 import android.annotation.Nullable;
+import android.net.IpPrefix;
 import android.net.LinkAddress;
+import android.net.LinkProperties;
+import android.net.RouteInfo;
 import android.net.util.SocketUtils;
 import android.os.ParcelFileDescriptor;
 import android.os.SystemClock;
@@ -31,6 +34,7 @@
 
 import java.io.FileDescriptor;
 import java.io.IOException;
+import java.io.InterruptedIOException;
 
 /** Controller for virtual/tunnel network interfaces. */
 public class TunInterfaceController {
@@ -43,13 +47,21 @@
     }
 
     private final String mIfName;
+    private final LinkProperties mLinkProperties = new LinkProperties();
     private ParcelFileDescriptor mParcelTunFd;
     private FileDescriptor mNetlinkSocket;
     private static int sNetlinkSeqNo = 0;
 
     /** Creates a new {@link TunInterfaceController} instance for given interface. */
     public TunInterfaceController(String interfaceName) {
-        this.mIfName = interfaceName;
+        mIfName = interfaceName;
+        mLinkProperties.setInterfaceName(mIfName);
+        mLinkProperties.setMtu(MTU);
+    }
+
+    /** Returns link properties of the Thread TUN interface. */
+    public LinkProperties getLinkProperties() {
+        return mLinkProperties;
     }
 
     /**
@@ -87,13 +99,18 @@
 
     /** Sets the interface up or down according to {@code isUp}. */
     public void setInterfaceUp(boolean isUp) throws IOException {
+        if (!isUp) {
+            for (LinkAddress address : mLinkProperties.getAllLinkAddresses()) {
+                removeAddress(address);
+            }
+        }
         nativeSetInterfaceUp(mIfName, isUp);
     }
 
     private native void nativeSetInterfaceUp(String interfaceName, boolean isUp) throws IOException;
 
     /** Adds a new address to the interface. */
-    public void addAddress(LinkAddress address) throws IOException {
+    public void addAddress(LinkAddress address) {
         Log.d(TAG, "Adding address " + address + " with flags: " + address.getFlags());
 
         long validLifetimeSeconds;
@@ -121,7 +138,7 @@
 
         byte[] message =
                 RtNetlinkAddressMessage.newRtmNewAddressMessage(
-                        sNetlinkSeqNo,
+                        sNetlinkSeqNo++,
                         address.getAddress(),
                         (short) address.getPrefixLength(),
                         address.getFlags(),
@@ -131,13 +148,51 @@
                         preferredLifetimeSeconds);
         try {
             Os.write(mNetlinkSocket, message, 0, message.length);
-        } catch (ErrnoException e) {
-            throw new IOException("Failed to send netlink message", e);
+        } catch (ErrnoException | InterruptedIOException e) {
+            Log.e(TAG, "Failed to add address " + address, e);
+            return;
         }
+        mLinkProperties.addLinkAddress(address);
+        mLinkProperties.addRoute(getRouteForAddress(address));
     }
 
     /** Removes an address from the interface. */
-    public void removeAddress(LinkAddress address) throws IOException {
-        // TODO(b/263222068): remove address with netlink
+    public void removeAddress(LinkAddress address) {
+        Log.d(TAG, "Removing address " + address);
+        byte[] message =
+                RtNetlinkAddressMessage.newRtmDelAddressMessage(
+                        sNetlinkSeqNo++,
+                        address.getAddress(),
+                        (short) address.getPrefixLength(),
+                        Os.if_nametoindex(mIfName));
+
+        // Intentionally update the mLinkProperties before send netlink message because the
+        // address is already removed from ot-daemon and apps can't reach to the address even
+        // when the netlink request below fails
+        mLinkProperties.removeLinkAddress(address);
+        mLinkProperties.removeRoute(getRouteForAddress(address));
+        try {
+            Os.write(mNetlinkSocket, message, 0, message.length);
+        } catch (ErrnoException | InterruptedIOException e) {
+            Log.e(TAG, "Failed to remove address " + address, e);
+        }
+    }
+
+    private RouteInfo getRouteForAddress(LinkAddress linkAddress) {
+        return new RouteInfo(
+                new IpPrefix(linkAddress.getAddress(), linkAddress.getPrefixLength()),
+                null,
+                mIfName,
+                RouteInfo.RTN_UNICAST,
+                MTU);
+    }
+
+    /** Called by {@link ThreadNetworkControllerService} to do clean up when ot-daemon is dead. */
+    public void onOtDaemonDied() {
+        try {
+            setInterfaceUp(false);
+        } catch (IOException e) {
+            Log.e(TAG, "Failed to set Thread TUN interface down");
+        }
     }
 }
diff --git a/thread/service/proguard.flags b/thread/service/proguard.flags
deleted file mode 100644
index 5028982..0000000
--- a/thread/service/proguard.flags
+++ /dev/null
@@ -1,4 +0,0 @@
-# Ensure the callback methods are not stripped
--keepclassmembers class **.ThreadNetworkControllerService$ThreadNetworkCallback {
-    *;
-}
diff --git a/thread/tests/cts/Android.bp b/thread/tests/cts/Android.bp
index 2f38bfd..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"],
 }
 
diff --git a/thread/tests/integration/Android.bp b/thread/tests/integration/Android.bp
index ebd6a4d..6ba192d 100644
--- a/thread/tests/integration/Android.bp
+++ b/thread/tests/integration/Android.bp
@@ -15,6 +15,7 @@
 //
 
 package {
+    default_team: "trendy_team_fwk_thread_network",
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
@@ -23,12 +24,14 @@
     min_sdk_version: "30",
     static_libs: [
         "androidx.test.rules",
+        "compatibility-device-util-axt",
         "guava",
         "mockito-target-minus-junit4",
         "net-tests-utils",
         "net-utils-device-common",
         "net-utils-device-common-bpf",
         "testables",
+        "truth",
     ],
     libs: [
         "android.test.runner",
diff --git a/thread/tests/integration/AndroidTest.xml b/thread/tests/integration/AndroidTest.xml
index ec9b5f3..152c1c3 100644
--- a/thread/tests/integration/AndroidTest.xml
+++ b/thread/tests/integration/AndroidTest.xml
@@ -31,6 +31,8 @@
         <option name="mainline-module-package-name" value="com.google.android.tethering" />
     </object>
 
+    <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer" />
+
     <!-- Install test -->
     <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
         <option name="test-file-name" value="ThreadNetworkIntegrationTests.apk" />
diff --git a/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java b/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java
index 5d9f084..29ada1b 100644
--- a/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java
+++ b/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java
@@ -18,14 +18,15 @@
 
 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 android.net.thread.utils.IntegrationTestUtils.JOIN_TIMEOUT;
+import static android.net.thread.utils.IntegrationTestUtils.isExpectedIcmpv6Packet;
+import static android.net.thread.utils.IntegrationTestUtils.isSimulatedThreadRadioSupported;
+import static android.net.thread.utils.IntegrationTestUtils.newPacketReader;
+import static android.net.thread.utils.IntegrationTestUtils.readPacketFrom;
+import static android.net.thread.utils.IntegrationTestUtils.waitFor;
+import static android.net.thread.utils.IntegrationTestUtils.waitForStateAnyOf;
 
 import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ECHO_REPLY_TYPE;
 import static com.android.testutils.TestNetworkTrackerKt.initTestNetwork;
@@ -41,6 +42,8 @@
 import android.content.Context;
 import android.net.LinkProperties;
 import android.net.MacAddress;
+import android.net.thread.utils.FullThreadDevice;
+import android.net.thread.utils.InfraNetworkDevice;
 import android.os.Handler;
 import android.os.HandlerThread;
 
@@ -57,6 +60,7 @@
 import org.junit.runner.RunWith;
 
 import java.net.Inet6Address;
+import java.time.Duration;
 import java.util.List;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
@@ -155,14 +159,14 @@
         runAsShell(
                 PERMISSION_THREAD_NETWORK_PRIVILEGED,
                 () -> mController.join(DEFAULT_DATASET, directExecutor(), result -> {}));
-        waitForStateAnyOf(mController, List.of(DEVICE_ROLE_LEADER), 30 /* timeoutSeconds */);
+        waitForStateAnyOf(mController, List.of(DEVICE_ROLE_LEADER), JOIN_TIMEOUT);
 
         // 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 */);
+        ftd.waitForStateAnyOf(List.of("router", "child"), JOIN_TIMEOUT);
+        waitFor(() -> ftd.getOmrAddress() != null, Duration.ofSeconds(60));
         Inet6Address ftdOmr = ftd.getOmrAddress();
         assertNotNull(ftdOmr);
 
@@ -171,7 +175,7 @@
                 newPacketReader(mInfraNetworkTracker.getTestIface(), mHandler);
         InfraNetworkDevice infraDevice =
                 new InfraNetworkDevice(MacAddress.fromString("1:2:3:4:5:6"), infraNetworkReader);
-        infraDevice.runSlaac(60 /* timeoutSeconds */);
+        infraDevice.runSlaac(Duration.ofSeconds(60));
         assertNotNull(infraDevice.ipv6Addr);
 
         // Infra device sends an echo request to FTD's OMR.
diff --git a/thread/tests/integration/src/android/net/thread/ThreadIntegrationTest.java b/thread/tests/integration/src/android/net/thread/ThreadIntegrationTest.java
new file mode 100644
index 0000000..70897f0
--- /dev/null
+++ b/thread/tests/integration/src/android/net/thread/ThreadIntegrationTest.java
@@ -0,0 +1,198 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.thread;
+
+import static android.Manifest.permission.NETWORK_SETTINGS;
+import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_DETACHED;
+import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_LEADER;
+import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_STOPPED;
+import static android.net.thread.ThreadNetworkManager.PERMISSION_THREAD_NETWORK_PRIVILEGED;
+import static android.net.thread.utils.IntegrationTestUtils.CALLBACK_TIMEOUT;
+import static android.net.thread.utils.IntegrationTestUtils.LEAVE_TIMEOUT;
+import static android.net.thread.utils.IntegrationTestUtils.RESTART_JOIN_TIMEOUT;
+import static android.net.thread.utils.IntegrationTestUtils.waitForStateAnyOf;
+
+import static com.android.compatibility.common.util.SystemUtil.runShellCommand;
+import static com.android.testutils.TestPermissionUtil.runAsShell;
+
+import static com.google.common.io.BaseEncoding.base16;
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
+
+import static org.junit.Assume.assumeNotNull;
+
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.net.thread.ThreadNetworkController.StateCallback;
+import android.net.thread.utils.OtDaemonController;
+import android.os.SystemClock;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.filters.LargeTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.net.Inet6Address;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+
+/** Tests for E2E Android Thread integration with ot-daemon, ConnectivityService, etc.. */
+@LargeTest
+@RunWith(AndroidJUnit4.class)
+public class ThreadIntegrationTest {
+    private final Context mContext = ApplicationProvider.getApplicationContext();
+    private ThreadNetworkController mController;
+    private OtDaemonController mOtCtl;
+
+    // A valid Thread Active Operational Dataset generated from OpenThread CLI "dataset init new".
+    private static final byte[] DEFAULT_DATASET_TLVS =
+            base16().decode(
+                            "0E080000000000010000000300001335060004001FFFE002"
+                                    + "08ACC214689BC40BDF0708FD64DB1225F47E0B0510F26B31"
+                                    + "53760F519A63BAFDDFFC80D2AF030F4F70656E5468726561"
+                                    + "642D643961300102D9A00410A245479C836D551B9CA557F7"
+                                    + "B9D351B40C0402A0FFF8");
+    private static final ActiveOperationalDataset DEFAULT_DATASET =
+            ActiveOperationalDataset.fromThreadTlvs(DEFAULT_DATASET_TLVS);
+
+    @Before
+    public void setUp() throws Exception {
+        final ThreadNetworkManager manager = mContext.getSystemService(ThreadNetworkManager.class);
+        if (manager != null) {
+            mController = manager.getAllThreadNetworkControllers().get(0);
+        }
+
+        // Run the tests on only devices where the Thread feature is available
+        assumeNotNull(mController);
+
+        mOtCtl = new OtDaemonController();
+        leaveAndWait(mController);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        if (mController == null) {
+            return;
+        }
+
+        setTestUpStreamNetworkAndWait(mController, null);
+        leaveAndWait(mController);
+    }
+
+    @Test
+    public void otDaemonRestart_notJoinedAndStopped_deviceRoleIsStopped() throws Exception {
+        leaveAndWait(mController);
+
+        runShellCommand("stop ot-daemon");
+        // TODO(b/323331973): the sleep is needed to workaround the race conditions
+        SystemClock.sleep(200);
+
+        waitForStateAnyOf(mController, List.of(DEVICE_ROLE_STOPPED), CALLBACK_TIMEOUT);
+    }
+
+    @Test
+    public void otDaemonRestart_JoinedNetworkAndStopped_autoRejoined() throws Exception {
+        joinAndWait(mController, DEFAULT_DATASET);
+
+        runShellCommand("stop ot-daemon");
+
+        waitForStateAnyOf(mController, List.of(DEVICE_ROLE_DETACHED), CALLBACK_TIMEOUT);
+        waitForStateAnyOf(mController, List.of(DEVICE_ROLE_LEADER), RESTART_JOIN_TIMEOUT);
+    }
+
+    @Test
+    public void otDaemonFactoryReset_deviceRoleIsStopped() throws Exception {
+        joinAndWait(mController, DEFAULT_DATASET);
+
+        mOtCtl.factoryReset();
+
+        assertThat(getDeviceRole(mController)).isEqualTo(DEVICE_ROLE_STOPPED);
+    }
+
+    @Test
+    public void otDaemonFactoryReset_addressesRemoved() throws Exception {
+        joinAndWait(mController, DEFAULT_DATASET);
+
+        mOtCtl.factoryReset();
+        String ifconfig = runShellCommand("ifconfig thread-wpan");
+
+        assertThat(ifconfig).doesNotContain("inet6 addr");
+    }
+
+    @Test
+    public void tunInterface_joinedNetwork_otAddressesAddedToTunInterface() throws Exception {
+        joinAndWait(mController, DEFAULT_DATASET);
+
+        String ifconfig = runShellCommand("ifconfig thread-wpan");
+        List<Inet6Address> otAddresses = mOtCtl.getAddresses();
+        assertThat(otAddresses).isNotEmpty();
+        for (Inet6Address otAddress : otAddresses) {
+            assertThat(ifconfig).contains(otAddress.getHostAddress());
+        }
+    }
+
+    // TODO (b/323300829): add more tests for integration with linux platform and
+    // ConnectivityService
+
+    private static int getDeviceRole(ThreadNetworkController controller) throws Exception {
+        CompletableFuture<Integer> future = new CompletableFuture<>();
+        StateCallback callback = future::complete;
+        controller.registerStateCallback(directExecutor(), callback);
+        try {
+            return future.get(CALLBACK_TIMEOUT.toMillis(), MILLISECONDS);
+        } finally {
+            controller.unregisterStateCallback(callback);
+        }
+    }
+
+    private static void joinAndWait(
+            ThreadNetworkController controller, ActiveOperationalDataset activeDataset)
+            throws Exception {
+        runAsShell(
+                PERMISSION_THREAD_NETWORK_PRIVILEGED,
+                () -> controller.join(activeDataset, directExecutor(), result -> {}));
+        waitForStateAnyOf(controller, List.of(DEVICE_ROLE_LEADER), RESTART_JOIN_TIMEOUT);
+    }
+
+    private static void leaveAndWait(ThreadNetworkController controller) throws Exception {
+        CompletableFuture<Void> future = new CompletableFuture<>();
+        runAsShell(
+                PERMISSION_THREAD_NETWORK_PRIVILEGED,
+                () -> controller.leave(directExecutor(), future::complete));
+        future.get(LEAVE_TIMEOUT.toMillis(), MILLISECONDS);
+    }
+
+    private static void setTestUpStreamNetworkAndWait(
+            ThreadNetworkController controller, @Nullable String networkInterfaceName)
+            throws Exception {
+        CompletableFuture<Void> future = new CompletableFuture<>();
+        runAsShell(
+                PERMISSION_THREAD_NETWORK_PRIVILEGED,
+                NETWORK_SETTINGS,
+                () -> {
+                    controller.setTestNetworkAsUpstream(
+                            networkInterfaceName, directExecutor(), future::complete);
+                });
+        future.get(CALLBACK_TIMEOUT.toMillis(), MILLISECONDS);
+    }
+}
diff --git a/thread/tests/integration/src/android/net/thread/FullThreadDevice.java b/thread/tests/integration/src/android/net/thread/utils/FullThreadDevice.java
similarity index 93%
rename from thread/tests/integration/src/android/net/thread/FullThreadDevice.java
rename to thread/tests/integration/src/android/net/thread/utils/FullThreadDevice.java
index 01638f3..031d205 100644
--- a/thread/tests/integration/src/android/net/thread/FullThreadDevice.java
+++ b/thread/tests/integration/src/android/net/thread/utils/FullThreadDevice.java
@@ -13,9 +13,9 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package android.net.thread;
+package android.net.thread.utils;
 
-import static android.net.thread.IntegrationTestUtils.waitFor;
+import static android.net.thread.utils.IntegrationTestUtils.waitFor;
 
 import static com.google.common.io.BaseEncoding.base16;
 
@@ -23,6 +23,7 @@
 
 import android.net.InetAddresses;
 import android.net.IpPrefix;
+import android.net.thread.ActiveOperationalDataset;
 
 import java.io.BufferedReader;
 import java.io.BufferedWriter;
@@ -30,6 +31,7 @@
 import java.io.InputStreamReader;
 import java.io.OutputStreamWriter;
 import java.net.Inet6Address;
+import java.time.Duration;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.concurrent.TimeoutException;
@@ -115,10 +117,10 @@
      *
      * @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.
+     * @param timeout the time to wait for the expected state before throwing
      */
-    public void waitForStateAnyOf(List<String> states, int timeoutSeconds) throws TimeoutException {
-        waitFor(() -> states.contains(getState()), timeoutSeconds);
+    public void waitForStateAnyOf(List<String> states, Duration timeout) throws TimeoutException {
+        waitFor(() -> states.contains(getState()), timeout);
     }
 
     /**
diff --git a/thread/tests/integration/src/android/net/thread/InfraNetworkDevice.java b/thread/tests/integration/src/android/net/thread/utils/InfraNetworkDevice.java
similarity index 91%
rename from thread/tests/integration/src/android/net/thread/InfraNetworkDevice.java
rename to thread/tests/integration/src/android/net/thread/utils/InfraNetworkDevice.java
index 43a800d..3081f9f 100644
--- a/thread/tests/integration/src/android/net/thread/InfraNetworkDevice.java
+++ b/thread/tests/integration/src/android/net/thread/utils/InfraNetworkDevice.java
@@ -13,11 +13,11 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package android.net.thread;
+package android.net.thread.utils;
 
-import static android.net.thread.IntegrationTestUtils.getRaPios;
-import static android.net.thread.IntegrationTestUtils.readPacketFrom;
-import static android.net.thread.IntegrationTestUtils.waitFor;
+import static android.net.thread.utils.IntegrationTestUtils.getRaPios;
+import static android.net.thread.utils.IntegrationTestUtils.readPacketFrom;
+import static android.net.thread.utils.IntegrationTestUtils.waitFor;
 
 import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ND_OPTION_SLLA;
 import static com.android.net.module.util.NetworkStackConstants.IPV6_ADDR_ALL_ROUTERS_MULTICAST;
@@ -34,6 +34,7 @@
 import java.net.Inet6Address;
 import java.net.InetAddress;
 import java.nio.ByteBuffer;
+import java.time.Duration;
 import java.util.List;
 import java.util.Random;
 import java.util.concurrent.TimeoutException;
@@ -100,8 +101,8 @@
      * @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 */);
+    public void runSlaac(Duration timeout) throws TimeoutException {
+        waitFor(() -> (ipv6Addr = runSlaac()) != null, timeout);
     }
 
     private Inet6Address runSlaac() {
diff --git a/thread/tests/integration/src/android/net/thread/IntegrationTestUtils.java b/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.java
similarity index 80%
rename from thread/tests/integration/src/android/net/thread/IntegrationTestUtils.java
rename to thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.java
index c465d57..f223367 100644
--- a/thread/tests/integration/src/android/net/thread/IntegrationTestUtils.java
+++ b/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.java
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package android.net.thread;
+package android.net.thread.utils;
 
 import static android.system.OsConstants.IPPROTO_ICMPV6;
 
@@ -23,6 +23,7 @@
 import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
 
 import android.net.TestNetworkInterface;
+import android.net.thread.ThreadNetworkController;
 import android.os.Handler;
 import android.os.SystemClock;
 import android.os.SystemProperties;
@@ -39,6 +40,7 @@
 
 import java.io.FileDescriptor;
 import java.nio.ByteBuffer;
+import java.time.Duration;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.concurrent.ExecutionException;
@@ -49,6 +51,14 @@
 
 /** Static utility methods relating to Thread integration tests. */
 public final class IntegrationTestUtils {
+    // The timeout of join() after restarting ot-daemon. The device needs to send 6 Link Request
+    // every 5 seconds, followed by 4 Parent Request every second. So this value needs to be 40
+    // seconds to be safe
+    public static final Duration RESTART_JOIN_TIMEOUT = Duration.ofSeconds(40);
+    public static final Duration JOIN_TIMEOUT = Duration.ofSeconds(30);
+    public static final Duration LEAVE_TIMEOUT = Duration.ofSeconds(2);
+    public static final Duration CALLBACK_TIMEOUT = Duration.ofSeconds(1);
+
     private IntegrationTestUtils() {}
 
     /** Returns whether the device supports simulated Thread radio. */
@@ -60,49 +70,33 @@
     /**
      * 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.
+     * @param condition the condition to check
+     * @param timeout the time to wait for the condition before throwing
+     * @throws TimeoutException if the condition is still not met when the timeout expires
      */
-    public static void waitFor(Supplier<Boolean> condition, int timeoutSeconds)
+    public static void waitFor(Supplier<Boolean> condition, Duration timeout)
             throws TimeoutException {
-        waitFor(condition, timeoutSeconds, 1);
-    }
+        final long intervalMills = 1000;
+        final long timeoutMills = timeout.toMillis();
 
-    /**
-     * 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) {
+        for (long i = 0; i < timeoutMills; i += intervalMills) {
             if (condition.get()) {
                 return;
             }
-            SystemClock.sleep(intervalSeconds * 1000L);
+            SystemClock.sleep(intervalMills);
         }
         if (condition.get()) {
             return;
         }
-        throw new TimeoutException(
-                String.format(
-                        "The condition failed to become true in %d seconds.", timeoutSeconds));
+        throw new TimeoutException("The condition failed to become true in " + timeout);
     }
 
     /**
      * Creates a {@link TapPacketReader} given the {@link TestNetworkInterface} and {@link Handler}.
      *
-     * @param testNetworkInterface the TUN interface of the test network.
-     * @param handler the handler to process the packets.
-     * @return the {@link TapPacketReader}.
+     * @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) {
@@ -117,16 +111,16 @@
     /**
      * Waits for the Thread module to enter any state of the given {@code deviceRoles}.
      *
-     * @param controller the {@link ThreadNetworkController}.
+     * @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.
+     *     ThreadNetworkController.DeviceRole}
+     * @param timeout the time to wait for the expected state before throwing
+     * @return the {@link ThreadNetworkController.DeviceRole} after waiting
      * @throws TimeoutException if the device hasn't become any of expected roles until the timeout
-     *     expires.
+     *     expires
      */
     public static int waitForStateAnyOf(
-            ThreadNetworkController controller, List<Integer> deviceRoles, int timeoutSeconds)
+            ThreadNetworkController controller, List<Integer> deviceRoles, Duration timeout)
             throws TimeoutException {
         SettableFuture<Integer> future = SettableFuture.create();
         ThreadNetworkController.StateCallback callback =
@@ -137,24 +131,24 @@
                 };
         controller.registerStateCallback(directExecutor(), callback);
         try {
-            int role = future.get(timeoutSeconds, TimeUnit.SECONDS);
-            controller.unregisterStateCallback(callback);
-            return role;
+            return future.get(timeout.toMillis(), TimeUnit.MILLISECONDS);
         } catch (InterruptedException | ExecutionException e) {
             throw new TimeoutException(
                     String.format(
-                            "The device didn't become an expected role in %d seconds.",
-                            timeoutSeconds));
+                            "The device didn't become an expected role in %s: %s",
+                            timeout, e.getMessage()));
+        } finally {
+            controller.unregisterStateCallback(callback);
         }
     }
 
     /**
      * 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.
+     * @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.
+     *     than 3000ms to read the next packet, the method will return null
      */
     public static byte[] readPacketFrom(TapPacketReader packetReader, Predicate<byte[]> filter) {
         byte[] packet;
diff --git a/thread/tests/integration/src/android/net/thread/utils/OtDaemonController.java b/thread/tests/integration/src/android/net/thread/utils/OtDaemonController.java
new file mode 100644
index 0000000..4a06fe8
--- /dev/null
+++ b/thread/tests/integration/src/android/net/thread/utils/OtDaemonController.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.thread.utils;
+
+import android.net.InetAddresses;
+import android.os.SystemClock;
+
+import com.android.compatibility.common.util.SystemUtil;
+
+import java.net.Inet6Address;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Wrapper of the "/system/bin/ot-ctl" which can be used to send CLI commands to ot-daemon to
+ * control its behavior.
+ *
+ * <p>Note that this class takes root privileged to run.
+ */
+public final class OtDaemonController {
+    private static final String OT_CTL = "/system/bin/ot-ctl";
+
+    /**
+     * Factory resets ot-daemon.
+     *
+     * <p>This will erase all persistent data written into apexdata/com.android.apex/ot-daemon and
+     * restart the ot-daemon service.
+     */
+    public void factoryReset() {
+        executeCommand("factoryreset");
+
+        // TODO(b/323164524): ot-ctl is a separate process so that the tests can't depend on the
+        // time sequence. Here needs to wait for system server to receive the ot-daemon death
+        // signal and take actions.
+        // A proper fix is to replace "ot-ctl" with "cmd thread_network ot-ctl" which is
+        // synchronized with the system server
+        SystemClock.sleep(500);
+    }
+
+    /** Returns the list of IPv6 addresses on ot-daemon. */
+    public List<Inet6Address> getAddresses() {
+        String output = executeCommand("ipaddr");
+        return Arrays.asList(output.split("\n")).stream()
+                .map(String::trim)
+                .filter(str -> !str.equals("Done"))
+                .map(addr -> InetAddresses.parseNumericAddress(addr))
+                .map(inetAddr -> (Inet6Address) inetAddr)
+                .toList();
+    }
+
+    public String executeCommand(String cmd) {
+        return SystemUtil.runShellCommand(OT_CTL + " " + cmd);
+    }
+}
diff --git a/thread/tests/unit/Android.bp b/thread/tests/unit/Android.bp
index 291475e..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,6 +30,7 @@
     ],
     test_suites: [
         "general-tests",
+        "mts-tethering",
     ],
     static_libs: [
         "frameworks-base-testutils",
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"],
 }