Merge "Revert "Use addEntries when creating NetworkStats"" into main
diff --git a/Tethering/src/com/android/networkstack/tethering/metrics/TetheringMetrics.java b/Tethering/src/com/android/networkstack/tethering/metrics/TetheringMetrics.java
index 136dfb1..ac4d8b1 100644
--- a/Tethering/src/com/android/networkstack/tethering/metrics/TetheringMetrics.java
+++ b/Tethering/src/com/android/networkstack/tethering/metrics/TetheringMetrics.java
@@ -22,6 +22,12 @@
 import static android.net.NetworkCapabilities.TRANSPORT_LOWPAN;
 import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
 import static android.net.NetworkCapabilities.TRANSPORT_WIFI_AWARE;
+import static android.net.NetworkStats.DEFAULT_NETWORK_YES;
+import static android.net.NetworkStats.METERED_YES;
+import static android.net.NetworkTemplate.MATCH_BLUETOOTH;
+import static android.net.NetworkTemplate.MATCH_ETHERNET;
+import static android.net.NetworkTemplate.MATCH_MOBILE;
+import static android.net.NetworkTemplate.MATCH_WIFI;
 import static android.net.TetheringManager.TETHERING_BLUETOOTH;
 import static android.net.TetheringManager.TETHERING_ETHERNET;
 import static android.net.TetheringManager.TETHERING_NCM;
@@ -48,6 +54,7 @@
 import android.annotation.Nullable;
 import android.content.Context;
 import android.net.NetworkCapabilities;
+import android.net.NetworkTemplate;
 import android.stats.connectivity.DownstreamType;
 import android.stats.connectivity.ErrorCode;
 import android.stats.connectivity.UpstreamType;
@@ -419,4 +426,46 @@
         }
         return false;
     }
+
+    /**
+     * Build NetworkTemplate for the given upstream type.
+     *
+     * <p> NetworkTemplate.Builder API was introduced in Android T.
+     *
+     * @param type the upstream type
+     * @return A NetworkTemplate object with a corresponding match rule or null if tethering
+     * metrics' data usage cannot be collected for a given upstream type.
+     */
+    @Nullable
+    public static NetworkTemplate buildNetworkTemplateForUpstreamType(@NonNull UpstreamType type) {
+        if (!isUsageSupportedForUpstreamType(type)) return null;
+
+        switch (type) {
+            case UT_CELLULAR:
+                // TODO: Handle the DUN connection, which is not a default network.
+                return new NetworkTemplate.Builder(MATCH_MOBILE)
+                        .setMeteredness(METERED_YES)
+                        .setDefaultNetworkStatus(DEFAULT_NETWORK_YES)
+                        .build();
+            case UT_WIFI:
+                return new NetworkTemplate.Builder(MATCH_WIFI)
+                        .setMeteredness(METERED_YES)
+                        .setDefaultNetworkStatus(DEFAULT_NETWORK_YES)
+                        .build();
+            case UT_BLUETOOTH:
+                return new NetworkTemplate.Builder(MATCH_BLUETOOTH)
+                        .setMeteredness(METERED_YES)
+                        .setDefaultNetworkStatus(DEFAULT_NETWORK_YES)
+                        .build();
+            case UT_ETHERNET:
+                return new NetworkTemplate.Builder(MATCH_ETHERNET)
+                        .setMeteredness(METERED_YES)
+                        .setDefaultNetworkStatus(DEFAULT_NETWORK_YES)
+                        .build();
+            default:
+                Log.e(TAG, "Unsupported UpstreamType: " + type.name());
+                break;
+        }
+        return null;
+    }
 }
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/metrics/TetheringMetricsTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/metrics/TetheringMetricsTest.java
index 7cef9cb..fbc2893 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/metrics/TetheringMetricsTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/metrics/TetheringMetricsTest.java
@@ -22,6 +22,10 @@
 import static android.net.NetworkCapabilities.TRANSPORT_LOWPAN;
 import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
 import static android.net.NetworkCapabilities.TRANSPORT_WIFI_AWARE;
+import static android.net.NetworkTemplate.MATCH_BLUETOOTH;
+import static android.net.NetworkTemplate.MATCH_ETHERNET;
+import static android.net.NetworkTemplate.MATCH_MOBILE;
+import static android.net.NetworkTemplate.MATCH_WIFI;
 import static android.net.TetheringManager.TETHERING_BLUETOOTH;
 import static android.net.TetheringManager.TETHERING_ETHERNET;
 import static android.net.TetheringManager.TETHERING_NCM;
@@ -47,11 +51,14 @@
 import static android.net.TetheringManager.TETHER_ERROR_UNTETHER_IFACE_ERROR;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.verify;
 
 import android.content.Context;
 import android.net.NetworkCapabilities;
+import android.net.NetworkTemplate;
+import android.os.Build;
 import android.stats.connectivity.DownstreamType;
 import android.stats.connectivity.ErrorCode;
 import android.stats.connectivity.UpstreamType;
@@ -62,8 +69,11 @@
 
 import com.android.networkstack.tethering.UpstreamNetworkState;
 import com.android.networkstack.tethering.metrics.TetheringMetrics.Dependencies;
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
 
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
@@ -72,12 +82,15 @@
 @RunWith(AndroidJUnit4.class)
 @SmallTest
 public final class TetheringMetricsTest {
+    @Rule public final DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule();
+
     private static final String TEST_CALLER_PKG = "com.test.caller.pkg";
     private static final String SETTINGS_PKG = "com.android.settings";
     private static final String SYSTEMUI_PKG = "com.android.systemui";
     private static final String GMS_PKG = "com.google.android.gms";
     private static final long TEST_START_TIME = 1670395936033L;
     private static final long SECOND_IN_MILLIS = 1_000L;
+    private static final int MATCH_NONE = -1;
 
     @Mock private Context mContext;
     @Mock private Dependencies mDeps;
@@ -392,4 +405,27 @@
         runUsageSupportedForUpstreamTypeTest(UpstreamType.UT_LOWPAN, false /* isSupported */);
         runUsageSupportedForUpstreamTypeTest(UpstreamType.UT_UNKNOWN, false /* isSupported */);
     }
+
+    private void runBuildNetworkTemplateForUpstreamType(final UpstreamType upstreamType,
+            final int matchRule)  {
+        final NetworkTemplate template =
+                TetheringMetrics.buildNetworkTemplateForUpstreamType(upstreamType);
+        if (matchRule == MATCH_NONE) {
+            assertNull(template);
+        } else {
+            assertEquals(matchRule, template.getMatchRule());
+        }
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+    public void testBuildNetworkTemplateForUpstreamType() {
+        runBuildNetworkTemplateForUpstreamType(UpstreamType.UT_CELLULAR, MATCH_MOBILE);
+        runBuildNetworkTemplateForUpstreamType(UpstreamType.UT_WIFI, MATCH_WIFI);
+        runBuildNetworkTemplateForUpstreamType(UpstreamType.UT_BLUETOOTH, MATCH_BLUETOOTH);
+        runBuildNetworkTemplateForUpstreamType(UpstreamType.UT_ETHERNET, MATCH_ETHERNET);
+        runBuildNetworkTemplateForUpstreamType(UpstreamType.UT_WIFI_AWARE, MATCH_NONE);
+        runBuildNetworkTemplateForUpstreamType(UpstreamType.UT_LOWPAN, MATCH_NONE);
+        runBuildNetworkTemplateForUpstreamType(UpstreamType.UT_UNKNOWN, MATCH_NONE);
+    }
 }
diff --git a/common/FlaggedApi.bp b/common/FlaggedApi.bp
index d5cfde3..fef9ac3 100644
--- a/common/FlaggedApi.bp
+++ b/common/FlaggedApi.bp
@@ -47,3 +47,20 @@
     srcs: ["nearby_flags.aconfig"],
     visibility: ["//packages/modules/Connectivity:__subpackages__"],
 }
+
+aconfig_declarations {
+    name: "com.android.networksecurity.flags-aconfig",
+    package: "com.android.net.ct.flags",
+    container: "com.android.tethering",
+    srcs: ["networksecurity_flags.aconfig"],
+    visibility: ["//packages/modules/Connectivity:__subpackages__"],
+}
+
+java_aconfig_library {
+    name: "networksecurity_flags_java_lib",
+    aconfig_declarations: "com.android.networksecurity.flags-aconfig",
+    min_sdk_version: "30",
+    defaults: ["framework-minus-apex-aconfig-java-defaults"],
+    apex_available: ["com.android.tethering"],
+    visibility: ["//packages/modules/Connectivity:__subpackages__"],
+}
diff --git a/common/networksecurity_flags.aconfig b/common/networksecurity_flags.aconfig
new file mode 100644
index 0000000..ef8ffcd
--- /dev/null
+++ b/common/networksecurity_flags.aconfig
@@ -0,0 +1,9 @@
+package: "com.android.net.ct.flags"
+container: "com.android.tethering"
+flag {
+    name: "certificate_transparency_service"
+    is_exported: true
+    namespace: "network_security"
+    description: "Enable service for certificate transparency log list data"
+    bug: "319829948"
+}
diff --git a/framework/Android.bp b/framework/Android.bp
index deea6b6..4c4f792 100644
--- a/framework/Android.bp
+++ b/framework/Android.bp
@@ -63,6 +63,7 @@
         ":framework-connectivity-sources",
         ":net-utils-framework-common-srcs",
         ":framework-connectivity-api-shared-srcs",
+        ":framework-networksecurity-sources",
     ],
     aidl: {
         generate_get_transaction_name: true,
@@ -93,6 +94,7 @@
         "modules-utils-backgroundthread",
         "modules-utils-build",
         "modules-utils-preconditions",
+        "networksecurity_flags_java_lib",
         "framework-connectivity-javastream-protos",
     ],
     impl_only_static_libs: [
@@ -207,6 +209,7 @@
     },
     aconfig_declarations: [
         "com.android.net.flags-aconfig",
+        "com.android.networksecurity.flags-aconfig",
     ],
 }
 
@@ -307,6 +310,7 @@
     srcs: [
         ":framework-connectivity-sources",
         ":framework-connectivity-tiramisu-updatable-sources",
+        ":framework-networksecurity-sources",
         ":framework-nearby-java-sources",
         ":framework-thread-sources",
     ],
diff --git a/netbpfload/NetBpfLoad.cpp b/netbpfload/NetBpfLoad.cpp
index 8b539aa..afb44cc 100644
--- a/netbpfload/NetBpfLoad.cpp
+++ b/netbpfload/NetBpfLoad.cpp
@@ -137,9 +137,6 @@
 // Size of the BPF log buffer for verifier logging
 #define BPF_LOAD_LOG_SZ 0xfffff
 
-// Unspecified attach type is 0 which is BPF_CGROUP_INET_INGRESS.
-#define BPF_ATTACH_TYPE_UNSPEC BPF_CGROUP_INET_INGRESS
-
 static unsigned int page_size = static_cast<unsigned int>(getpagesize());
 
 constexpr const char* lookupSelinuxContext(const domain d) {
@@ -201,7 +198,7 @@
 typedef struct {
     const char* name;
     enum bpf_prog_type type;
-    enum bpf_attach_type expected_attach_type;
+    enum bpf_attach_type attach_type;
 } sectionType;
 
 /*
@@ -221,8 +218,8 @@
 sectionType sectionNameTypes[] = {
         {"bind4/",             BPF_PROG_TYPE_CGROUP_SOCK_ADDR, BPF_CGROUP_INET4_BIND},
         {"bind6/",             BPF_PROG_TYPE_CGROUP_SOCK_ADDR, BPF_CGROUP_INET6_BIND},
-        {"cgroupskb/",         BPF_PROG_TYPE_CGROUP_SKB,       BPF_ATTACH_TYPE_UNSPEC},
-        {"cgroupsock/",        BPF_PROG_TYPE_CGROUP_SOCK,      BPF_ATTACH_TYPE_UNSPEC},
+        {"cgroupskb/",         BPF_PROG_TYPE_CGROUP_SKB},
+        {"cgroupsock/",        BPF_PROG_TYPE_CGROUP_SOCK},
         {"cgroupsockcreate/",  BPF_PROG_TYPE_CGROUP_SOCK,      BPF_CGROUP_INET_SOCK_CREATE},
         {"cgroupsockrelease/", BPF_PROG_TYPE_CGROUP_SOCK,      BPF_CGROUP_INET_SOCK_RELEASE},
         {"connect4/",          BPF_PROG_TYPE_CGROUP_SOCK_ADDR, BPF_CGROUP_INET4_CONNECT},
@@ -230,28 +227,24 @@
         {"egress/",            BPF_PROG_TYPE_CGROUP_SKB,       BPF_CGROUP_INET_EGRESS},
         {"getsockopt/",        BPF_PROG_TYPE_CGROUP_SOCKOPT,   BPF_CGROUP_GETSOCKOPT},
         {"ingress/",           BPF_PROG_TYPE_CGROUP_SKB,       BPF_CGROUP_INET_INGRESS},
-        {"lwt_in/",            BPF_PROG_TYPE_LWT_IN,           BPF_ATTACH_TYPE_UNSPEC},
-        {"lwt_out/",           BPF_PROG_TYPE_LWT_OUT,          BPF_ATTACH_TYPE_UNSPEC},
-        {"lwt_seg6local/",     BPF_PROG_TYPE_LWT_SEG6LOCAL,    BPF_ATTACH_TYPE_UNSPEC},
-        {"lwt_xmit/",          BPF_PROG_TYPE_LWT_XMIT,         BPF_ATTACH_TYPE_UNSPEC},
         {"postbind4/",         BPF_PROG_TYPE_CGROUP_SOCK,      BPF_CGROUP_INET4_POST_BIND},
         {"postbind6/",         BPF_PROG_TYPE_CGROUP_SOCK,      BPF_CGROUP_INET6_POST_BIND},
         {"recvmsg4/",          BPF_PROG_TYPE_CGROUP_SOCK_ADDR, BPF_CGROUP_UDP4_RECVMSG},
         {"recvmsg6/",          BPF_PROG_TYPE_CGROUP_SOCK_ADDR, BPF_CGROUP_UDP6_RECVMSG},
-        {"schedact/",          BPF_PROG_TYPE_SCHED_ACT,        BPF_ATTACH_TYPE_UNSPEC},
-        {"schedcls/",          BPF_PROG_TYPE_SCHED_CLS,        BPF_ATTACH_TYPE_UNSPEC},
+        {"schedact/",          BPF_PROG_TYPE_SCHED_ACT},
+        {"schedcls/",          BPF_PROG_TYPE_SCHED_CLS},
         {"sendmsg4/",          BPF_PROG_TYPE_CGROUP_SOCK_ADDR, BPF_CGROUP_UDP4_SENDMSG},
         {"sendmsg6/",          BPF_PROG_TYPE_CGROUP_SOCK_ADDR, BPF_CGROUP_UDP6_SENDMSG},
         {"setsockopt/",        BPF_PROG_TYPE_CGROUP_SOCKOPT,   BPF_CGROUP_SETSOCKOPT},
-        {"skfilter/",          BPF_PROG_TYPE_SOCKET_FILTER,    BPF_ATTACH_TYPE_UNSPEC},
+        {"skfilter/",          BPF_PROG_TYPE_SOCKET_FILTER},
         {"sockops/",           BPF_PROG_TYPE_SOCK_OPS,         BPF_CGROUP_SOCK_OPS},
         {"sysctl",             BPF_PROG_TYPE_CGROUP_SYSCTL,    BPF_CGROUP_SYSCTL},
-        {"xdp/",               BPF_PROG_TYPE_XDP,              BPF_ATTACH_TYPE_UNSPEC},
+        {"xdp/",               BPF_PROG_TYPE_XDP},
 };
 
 typedef struct {
     enum bpf_prog_type type;
-    enum bpf_attach_type expected_attach_type;
+    enum bpf_attach_type attach_type;
     string name;
     vector<char> data;
     vector<char> rel_data;
@@ -435,12 +428,6 @@
     return BPF_PROG_TYPE_UNSPEC;
 }
 
-static enum bpf_attach_type getExpectedAttachType(string& name) {
-    for (auto& snt : sectionNameTypes)
-        if (StartsWith(name, snt.name)) return snt.expected_attach_type;
-    return BPF_ATTACH_TYPE_UNSPEC;
-}
-
 /*
 static string getSectionName(enum bpf_prog_type type)
 {
@@ -556,7 +543,8 @@
         if (ptype == BPF_PROG_TYPE_UNSPEC) continue;
 
         // This must be done before '/' is replaced with '_'.
-        cs_temp.expected_attach_type = getExpectedAttachType(name);
+        for (auto& snt : sectionNameTypes)
+            if (StartsWith(name, snt.name)) cs_temp.attach_type = snt.attach_type;
 
         string oldName = name;
 
@@ -1062,7 +1050,7 @@
               .log_level = 1,
               .log_buf = ptr_to_u64(log_buf.data()),
               .log_size = static_cast<__u32>(log_buf.size()),
-              .expected_attach_type = cs[i].expected_attach_type,
+              .expected_attach_type = cs[i].attach_type,
             };
             if (isAtLeastKernelVersion(4, 15, 0))
                 strlcpy(req.prog_name, cs[i].name.c_str(), sizeof(req.prog_name));
diff --git a/netd/BpfBaseTest.cpp b/netd/BpfBaseTest.cpp
index c979a7b..34dfbb4 100644
--- a/netd/BpfBaseTest.cpp
+++ b/netd/BpfBaseTest.cpp
@@ -56,7 +56,7 @@
 
 TEST_F(BpfBasicTest, TestCgroupMounted) {
     std::string cg2_path;
-    ASSERT_EQ(true, CgroupGetControllerPath(CGROUPV2_CONTROLLER_NAME, &cg2_path));
+    ASSERT_EQ(true, CgroupGetControllerPath(CGROUPV2_HIERARCHY_NAME, &cg2_path));
     ASSERT_EQ(0, access(cg2_path.c_str(), R_OK));
     ASSERT_EQ(0, access((cg2_path + "/cgroup.controllers").c_str(), R_OK));
 }
diff --git a/networksecurity/OWNERS b/networksecurity/OWNERS
new file mode 100644
index 0000000..1a4130a
--- /dev/null
+++ b/networksecurity/OWNERS
@@ -0,0 +1,4 @@
+# Bug component: 1479456
+
+sandrom@google.com
+tweek@google.com
diff --git a/networksecurity/framework/Android.bp b/networksecurity/framework/Android.bp
new file mode 100644
index 0000000..2b77926
--- /dev/null
+++ b/networksecurity/framework/Android.bp
@@ -0,0 +1,31 @@
+//
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+package {
+    default_team: "trendy_team_platform_security",
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+filegroup {
+    name: "framework-networksecurity-sources",
+    srcs: [
+        "src/**/*.java",
+        "src/**/*.aidl",
+    ],
+    path: "src",
+    visibility: [
+        "//packages/modules/Connectivity:__subpackages__",
+    ],
+}
diff --git a/networksecurity/framework/src/android/net/ct/CertificateTransparencyManager.java b/networksecurity/framework/src/android/net/ct/CertificateTransparencyManager.java
new file mode 100644
index 0000000..94521ae
--- /dev/null
+++ b/networksecurity/framework/src/android/net/ct/CertificateTransparencyManager.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.net.ct;
+
+import android.annotation.FlaggedApi;
+import android.annotation.SystemService;
+
+import com.android.net.ct.flags.Flags;
+
+/**
+ * Provides the primary API for the Certificate Transparency Manager.
+ *
+ * @hide
+ */
+@FlaggedApi(Flags.FLAG_CERTIFICATE_TRANSPARENCY_SERVICE)
+@SystemService(CertificateTransparencyManager.SERVICE_NAME)
+public final class CertificateTransparencyManager {
+
+    public static final String SERVICE_NAME = "certificate_transparency";
+
+    /**
+     * Creates a new CertificateTransparencyManager instance.
+     *
+     * @hide
+     */
+    public CertificateTransparencyManager() {}
+}
diff --git a/networksecurity/framework/src/android/net/ct/ICertificateTransparencyManager.aidl b/networksecurity/framework/src/android/net/ct/ICertificateTransparencyManager.aidl
new file mode 100644
index 0000000..b5bce7f
--- /dev/null
+++ b/networksecurity/framework/src/android/net/ct/ICertificateTransparencyManager.aidl
@@ -0,0 +1,22 @@
+/**
+ * Copyright (c) 2024, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.net.ct;
+
+/**
+* Interface for communicating with CertificateTransparencyService.
+* @hide
+*/
+interface ICertificateTransparencyManager {}
diff --git a/networksecurity/service/Android.bp b/networksecurity/service/Android.bp
new file mode 100644
index 0000000..e33abd5
--- /dev/null
+++ b/networksecurity/service/Android.bp
@@ -0,0 +1,41 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package {
+    default_team: "trendy_team_platform_security",
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+// Main lib for Certificate Transparency services.
+java_library {
+    name: "service-networksecurity-pre-jarjar",
+    defaults: ["framework-system-server-module-defaults"],
+    visibility: ["//packages/modules/Connectivity:__subpackages__"],
+
+    srcs: [
+        "src/**/*.java",
+    ],
+
+    libs: [
+        "framework-configinfrastructure",
+        "framework-connectivity-pre-jarjar",
+        "service-connectivity-pre-jarjar",
+    ],
+
+    // This is included in service-connectivity which is 30+
+    // TODO (b/293613362): allow APEXes to have service jars with higher min_sdk than the APEX
+    // (service-connectivity is only used on 31+) and use 31 here
+    min_sdk_version: "30",
+    sdk_version: "system_server_current",
+    apex_available: ["com.android.tethering"],
+}
diff --git a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyFlagsListener.java b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyFlagsListener.java
new file mode 100644
index 0000000..8dd5951
--- /dev/null
+++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyFlagsListener.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.server.net.ct;
+
+import static android.provider.DeviceConfig.NAMESPACE_TETHERING;
+
+import android.content.Context;
+import android.provider.DeviceConfig;
+import android.provider.DeviceConfig.Properties;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.modules.utils.build.SdkLevel;
+
+import java.util.concurrent.Executors;
+
+/** Listener class for the Certificate Transparency Phenotype flags. */
+class CertificateTransparencyFlagsListener implements DeviceConfig.OnPropertiesChangedListener {
+
+    private static final String TAG = "CertificateTransparency";
+
+    private static final String VERSION = "version";
+    private static final String CONTENT_URL = "content_url";
+    private static final String METADATA_URL = "metadata_url";
+
+    CertificateTransparencyFlagsListener(Context context) {}
+
+    void initialize() {
+        DeviceConfig.addOnPropertiesChangedListener(
+                NAMESPACE_TETHERING, Executors.newSingleThreadExecutor(), this);
+        // TODO: handle property changes triggering on boot before registering this listener.
+    }
+
+    @Override
+    public void onPropertiesChanged(Properties properties) {
+        if (!SdkLevel.isAtLeastV() || !NAMESPACE_TETHERING.equals(properties.getNamespace())) {
+            return;
+        }
+
+        String newVersion = DeviceConfig.getString(NAMESPACE_TETHERING, VERSION, "");
+        String newContentUrl = DeviceConfig.getString(NAMESPACE_TETHERING, CONTENT_URL, "");
+        String newMetadataUrl = DeviceConfig.getString(NAMESPACE_TETHERING, METADATA_URL, "");
+        if (TextUtils.isEmpty(newVersion)
+                || TextUtils.isEmpty(newContentUrl)
+                || TextUtils.isEmpty(newMetadataUrl)) {
+            return;
+        }
+
+        Log.d(TAG, "newVersion=" + newVersion);
+        Log.d(TAG, "newContentUrl=" + newContentUrl);
+        Log.d(TAG, "newMetadataUrl=" + newMetadataUrl);
+        // TODO: start download of URLs.
+    }
+}
diff --git a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyService.java b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyService.java
new file mode 100644
index 0000000..406a57f
--- /dev/null
+++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyService.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 com.android.server.net.ct;
+
+import android.annotation.RequiresApi;
+import android.content.Context;
+import android.net.ct.ICertificateTransparencyManager;
+import android.os.Build;
+import android.util.Log;
+
+import com.android.net.ct.flags.Flags;
+import com.android.net.module.util.DeviceConfigUtils;
+import com.android.server.SystemService;
+
+/** Implementation of the Certificate Transparency service. */
+@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+public class CertificateTransparencyService extends ICertificateTransparencyManager.Stub {
+
+    private static final String TAG = "CertificateTransparency";
+    private static final String CERTIFICATE_TRANSPARENCY_ENABLED =
+            "certificate_transparency_service_enabled";
+
+    private final CertificateTransparencyFlagsListener mFlagsListener;
+
+    /**
+     * @return true if the CertificateTransparency service is enabled.
+     */
+    public static boolean enabled(Context context) {
+        // TODO: replace isTetheringFeatureEnabled with CT namespace flag.
+        return DeviceConfigUtils.isTetheringFeatureEnabled(
+                        context, CERTIFICATE_TRANSPARENCY_ENABLED)
+                && Flags.certificateTransparencyService();
+    }
+
+    /** Creates a new {@link CertificateTransparencyService} object. */
+    public CertificateTransparencyService(Context context) {
+        mFlagsListener = new CertificateTransparencyFlagsListener(context);
+    }
+
+    /**
+     * Called by {@link com.android.server.ConnectivityServiceInitializer}.
+     *
+     * @see com.android.server.SystemService#onBootPhase
+     */
+    public void onBootPhase(int phase) {
+
+        switch (phase) {
+            case SystemService.PHASE_BOOT_COMPLETED:
+                Log.d(TAG, "setting up flags listeners");
+                mFlagsListener.initialize();
+                break;
+            default:
+        }
+    }
+}
diff --git a/service-t/Android.bp b/service-t/Android.bp
index 779f354..e00b7cf 100644
--- a/service-t/Android.bp
+++ b/service-t/Android.bp
@@ -59,6 +59,7 @@
         "framework-wifi",
         "service-connectivity-pre-jarjar",
         "service-nearby-pre-jarjar",
+        "service-networksecurity-pre-jarjar",
         "service-thread-pre-jarjar",
         service_remoteauth_pre_jarjar_lib,
         "ServiceConnectivityResources",
diff --git a/service-t/src/com/android/server/ConnectivityServiceInitializer.java b/service-t/src/com/android/server/ConnectivityServiceInitializer.java
index 1ac2f6e..5d23fdc 100644
--- a/service-t/src/com/android/server/ConnectivityServiceInitializer.java
+++ b/service-t/src/com/android/server/ConnectivityServiceInitializer.java
@@ -28,6 +28,7 @@
 import com.android.server.ethernet.EthernetService;
 import com.android.server.ethernet.EthernetServiceImpl;
 import com.android.server.nearby.NearbyService;
+import com.android.server.net.ct.CertificateTransparencyService;
 import com.android.server.thread.ThreadNetworkService;
 
 /**
@@ -43,6 +44,7 @@
     private final NearbyService mNearbyService;
     private final EthernetServiceImpl mEthernetServiceImpl;
     private final ThreadNetworkService mThreadNetworkService;
+    private final CertificateTransparencyService mCertificateTransparencyService;
 
     public ConnectivityServiceInitializer(Context context) {
         super(context);
@@ -55,6 +57,7 @@
         mNsdService = createNsdService(context);
         mNearbyService = createNearbyService(context);
         mThreadNetworkService = createThreadNetworkService(context);
+        mCertificateTransparencyService = createCertificateTransparencyService(context);
     }
 
     @Override
@@ -111,6 +114,10 @@
         if (mThreadNetworkService != null) {
             mThreadNetworkService.onBootPhase(phase);
         }
+
+        if (SdkLevel.isAtLeastV() && mCertificateTransparencyService != null) {
+            mCertificateTransparencyService.onBootPhase(phase);
+        }
     }
 
     /**
@@ -186,4 +193,13 @@
         }
         return new ThreadNetworkService(context);
     }
+
+    /** Return CertificateTransparencyService instance if enable, otherwise null. */
+    @Nullable
+    private CertificateTransparencyService createCertificateTransparencyService(
+            final Context context) {
+        return SdkLevel.isAtLeastV() && CertificateTransparencyService.enabled(context)
+                ? new CertificateTransparencyService(context)
+                : null;
+    }
 }
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsDiscoveryManager.java b/service-t/src/com/android/server/connectivity/mdns/MdnsDiscoveryManager.java
index 8123d27..7fa605a 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsDiscoveryManager.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsDiscoveryManager.java
@@ -390,12 +390,18 @@
      */
     public void dump(PrintWriter pw) {
         discoveryExecutor.checkAndRunOnHandlerThread(() -> {
-            pw.println();
+            pw.println("Clients:");
             // Dump ServiceTypeClients
             for (MdnsServiceTypeClient serviceTypeClient
                     : perSocketServiceTypeClients.getAllMdnsServiceTypeClient()) {
                 serviceTypeClient.dump(pw);
             }
+            pw.println();
+            // Dump ServiceCache
+            pw.println("Cached services:");
+            if (serviceCache != null) {
+                serviceCache.dump(pw, "  ");
+            }
         });
     }
 }
\ No newline at end of file
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java b/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java
index ebd95c9..c3cb776 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java
@@ -228,12 +228,12 @@
          * Create a ServiceRegistration with only update the subType.
          */
         ServiceRegistration withSubtypes(@NonNull Set<String> newSubtypes,
-                boolean avoidEmptyTxtRecords) {
+                @NonNull MdnsFeatureFlags featureFlags) {
             NsdServiceInfo newServiceInfo = new NsdServiceInfo(serviceInfo);
             newServiceInfo.setSubtypes(newSubtypes);
             return new ServiceRegistration(srvRecord.record.getServiceHost(), newServiceInfo,
                     repliedServiceCount, sentPacketCount, exiting, isProbing, ttl,
-                    avoidEmptyTxtRecords);
+                    featureFlags);
         }
 
         /**
@@ -241,7 +241,7 @@
          */
         ServiceRegistration(@NonNull String[] deviceHostname, @NonNull NsdServiceInfo serviceInfo,
                 int repliedServiceCount, int sentPacketCount, boolean exiting, boolean isProbing,
-                @Nullable Duration ttl, boolean avoidEmptyTxtRecords) {
+                @Nullable Duration ttl, @NonNull MdnsFeatureFlags featureFlags) {
             this.serviceInfo = serviceInfo;
 
             final long nonNameRecordsTtlMillis;
@@ -313,7 +313,7 @@
                                 true /* cacheFlush */,
                                 nonNameRecordsTtlMillis,
                                 attrsToTextEntries(
-                                        serviceInfo.getAttributes(), avoidEmptyTxtRecords)),
+                                        serviceInfo.getAttributes(), featureFlags)),
                         false /* sharedName */);
 
                 allRecords.addAll(ptrRecords);
@@ -397,9 +397,9 @@
          */
         ServiceRegistration(@NonNull String[] deviceHostname, @NonNull NsdServiceInfo serviceInfo,
                 int repliedServiceCount, int sentPacketCount, @Nullable Duration ttl,
-                boolean avoidEmptyTxtRecords) {
+                @NonNull MdnsFeatureFlags featureFlags) {
             this(deviceHostname, serviceInfo,repliedServiceCount, sentPacketCount,
-                    false /* exiting */, true /* isProbing */, ttl, avoidEmptyTxtRecords);
+                    false /* exiting */, true /* isProbing */, ttl, featureFlags);
         }
 
         void setProbing(boolean probing) {
@@ -450,7 +450,7 @@
                     "Service ID must already exist for an update request: " + serviceId);
         }
         final ServiceRegistration updatedRegistration = existingRegistration.withSubtypes(
-                subtypes, mMdnsFeatureFlags.avoidAdvertisingEmptyTxtRecords());
+                subtypes, mMdnsFeatureFlags);
         mServices.put(serviceId, updatedRegistration);
     }
 
@@ -482,7 +482,7 @@
         final ServiceRegistration registration = new ServiceRegistration(
                 mDeviceHostname, serviceInfo, NO_PACKET /* repliedServiceCount */,
                 NO_PACKET /* sentPacketCount */, ttl,
-                mMdnsFeatureFlags.avoidAdvertisingEmptyTxtRecords());
+                mMdnsFeatureFlags);
         mServices.put(serviceId, registration);
 
         // Remove existing exiting service
@@ -553,14 +553,14 @@
         return new MdnsProber.ProbingInfo(serviceId, probingRecords);
     }
 
-    private static List<MdnsServiceInfo.TextEntry> attrsToTextEntries(Map<String, byte[]> attrs,
-            boolean avoidEmptyTxtRecords) {
+    private static List<MdnsServiceInfo.TextEntry> attrsToTextEntries(
+            @NonNull Map<String, byte[]> attrs, @NonNull MdnsFeatureFlags featureFlags) {
         final List<MdnsServiceInfo.TextEntry> out = new ArrayList<>(
                 attrs.size() == 0 ? 1 : attrs.size());
-        if (avoidEmptyTxtRecords && attrs.size() == 0) {
+        if (featureFlags.avoidAdvertisingEmptyTxtRecords() && attrs.size() == 0) {
             // As per RFC6763 6.1, empty TXT records are not allowed, but records containing a
             // single empty String must be treated as equivalent.
-            out.add(new MdnsServiceInfo.TextEntry("", (byte[]) null));
+            out.add(new MdnsServiceInfo.TextEntry("", MdnsServiceInfo.TextEntry.VALUE_NONE));
             return out;
         }
 
@@ -1418,7 +1418,7 @@
 
         final ServiceRegistration newService = new ServiceRegistration(mDeviceHostname, newInfo,
                 existing.repliedServiceCount, existing.sentPacketCount, existing.ttl,
-                mMdnsFeatureFlags.avoidAdvertisingEmptyTxtRecords());
+                mMdnsFeatureFlags);
         mServices.put(serviceId, newService);
         return makeProbingInfo(serviceId, newService);
     }
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsResponse.java b/service-t/src/com/android/server/connectivity/mdns/MdnsResponse.java
index 3636644..2957da5 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsResponse.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsResponse.java
@@ -19,11 +19,14 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.net.Network;
+import android.os.SystemClock;
+import android.text.TextUtils;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.net.module.util.DnsUtils;
 
 import java.io.IOException;
+import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Iterator;
@@ -426,4 +429,18 @@
 
         return count;
     }
+
+    @Override
+    public String toString() {
+        return "Name: " + TextUtils.join(".", serviceName)
+                + ", pointerRecords: " + pointerRecords
+                + ", serviceRecord: " + serviceRecord
+                + ", textRecord: " + textRecord
+                + ", inet4AddressRecords: " + inet4AddressRecords
+                + ", inet6AddressRecords: " + inet6AddressRecords
+                + ", interfaceIndex: " + interfaceIndex
+                + ", network: " + network
+                + ", lastUpdateTime: " + Instant.now().minusMillis(
+                        SystemClock.elapsedRealtime() - lastUpdateTime);
+    }
 }
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceCache.java b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceCache.java
index a8a4ef1..591ed8b 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceCache.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceCache.java
@@ -32,6 +32,7 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.connectivity.mdns.util.MdnsUtils;
 
+import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Iterator;
@@ -71,6 +72,11 @@
             return Objects.equals(mUpperCaseServiceType, ((CacheKey) other).mUpperCaseServiceType)
                     && Objects.equals(mSocketKey, ((CacheKey) other).mSocketKey);
         }
+
+        @Override
+        public String toString() {
+            return "CacheKey{ ServiceType=" + mUpperCaseServiceType + ", " + mSocketKey + " }";
+        }
     }
     /**
      * A map of cached services. Key is composed of service type and socket. Value is the list of
@@ -353,6 +359,22 @@
         mNextExpirationTime = getNextExpirationTime(now);
     }
 
+    /**
+     * Dump ServiceCache state.
+     */
+    public void dump(PrintWriter pw, String indent) {
+        ensureRunningOnHandlerThread(mHandler);
+        // IndentingPrintWriter cannot be used on the mDNS stack build. So, manually add an indent.
+        for (int i = 0; i < mCachedServices.size(); i++) {
+            final CacheKey key = mCachedServices.keyAt(i);
+            pw.println(indent + key);
+            for (MdnsResponse response : mCachedServices.valueAt(i)) {
+                pw.println(indent + "  Response{ " + response + " }");
+            }
+            pw.println();
+        }
+    }
+
     /*** Callbacks for listening service expiration */
     public interface ServiceExpiredCallback {
         /*** Notify the service is expired */
diff --git a/service-t/src/com/android/server/net/NetworkStatsService.java b/service-t/src/com/android/server/net/NetworkStatsService.java
index 114cf2e..11343d2 100644
--- a/service-t/src/com/android/server/net/NetworkStatsService.java
+++ b/service-t/src/com/android/server/net/NetworkStatsService.java
@@ -74,12 +74,12 @@
 import static com.android.net.module.util.DeviceConfigUtils.getDeviceConfigPropertyInt;
 import static com.android.net.module.util.NetworkCapabilitiesUtils.getDisplayTransport;
 import static com.android.net.module.util.NetworkStatsUtils.LIMIT_GLOBAL_ALERT;
-import static com.android.server.net.NetworkStatsEventLogger.POLL_REASON_PERIODIC;
 import static com.android.server.net.NetworkStatsEventLogger.POLL_REASON_DUMPSYS;
 import static com.android.server.net.NetworkStatsEventLogger.POLL_REASON_FORCE_UPDATE;
 import static com.android.server.net.NetworkStatsEventLogger.POLL_REASON_GLOBAL_ALERT;
 import static com.android.server.net.NetworkStatsEventLogger.POLL_REASON_NETWORK_STATUS_CHANGED;
 import static com.android.server.net.NetworkStatsEventLogger.POLL_REASON_OPEN_SESSION;
+import static com.android.server.net.NetworkStatsEventLogger.POLL_REASON_PERIODIC;
 import static com.android.server.net.NetworkStatsEventLogger.POLL_REASON_RAT_CHANGED;
 import static com.android.server.net.NetworkStatsEventLogger.POLL_REASON_REG_CALLBACK;
 import static com.android.server.net.NetworkStatsEventLogger.POLL_REASON_REMOVE_UIDS;
@@ -242,13 +242,11 @@
     // A message for broadcasting ACTION_NETWORK_STATS_UPDATED in handler thread to prevent
     // deadlock.
     private static final int MSG_BROADCAST_NETWORK_STATS_UPDATED = 4;
-
     /** Flags to control detail level of poll event. */
     private static final int FLAG_PERSIST_NETWORK = 0x1;
     private static final int FLAG_PERSIST_UID = 0x2;
     private static final int FLAG_PERSIST_ALL = FLAG_PERSIST_NETWORK | FLAG_PERSIST_UID;
     private static final int FLAG_PERSIST_FORCE = 0x100;
-
     /**
      * When global alert quota is high, wait for this delay before processing each polling,
      * and do not schedule further polls once there is already one queued.
@@ -313,6 +311,12 @@
     static final String TRAFFIC_STATS_CACHE_MAX_ENTRIES_NAME = "trafficstats_cache_max_entries";
     static final int DEFAULT_TRAFFIC_STATS_CACHE_EXPIRY_DURATION_MS = 1000;
     static final int DEFAULT_TRAFFIC_STATS_CACHE_MAX_ENTRIES = 400;
+    /**
+     * The delay time between to network stats update intents.
+     * Added to fix intent spams (b/3115462)
+     */
+    @VisibleForTesting(visibility = PRIVATE)
+    static final int BROADCAST_NETWORK_STATS_UPDATED_DELAY_MS = 1000;
 
     private final Context mContext;
     private final NetworkStatsFactory mStatsFactory;
@@ -385,6 +389,7 @@
         long getXtPersistBytes(long def);
         long getUidPersistBytes(long def);
         long getUidTagPersistBytes(long def);
+        long getBroadcastNetworkStatsUpdateDelayMs();
     }
 
     private final Object mStatsLock = new Object();
@@ -469,15 +474,36 @@
 
     private long mLastStatsSessionPoll;
 
+    /**
+     * The timestamp of the most recent network stats broadcast.
+     *
+     * Note that this time could be in the past for completed broadcasts,
+     * or in the future for scheduled broadcasts.
+     *
+     * It is initialized to {@code Long.MIN_VALUE} to ensure that the first broadcast request
+     * is fulfilled immediately, regardless of the delay time.
+     *
+     * This value is used to enforce rate limiting on intents, preventing intent spam.
+     */
+    @GuardedBy("mStatsLock")
+    private long mLatestNetworkStatsUpdatedBroadcastScheduledTime = Long.MIN_VALUE;
+
+
     private final TrafficStatsRateLimitCache mTrafficStatsTotalCache;
     private final TrafficStatsRateLimitCache mTrafficStatsIfaceCache;
     private final TrafficStatsRateLimitCache mTrafficStatsUidCache;
     static final String TRAFFICSTATS_RATE_LIMIT_CACHE_ENABLED_FLAG =
             "trafficstats_rate_limit_cache_enabled_flag";
+    static final String BROADCAST_NETWORK_STATS_UPDATED_RATE_LIMIT_ENABLED_FLAG =
+            "broadcast_network_stats_updated_rate_limit_enabled_flag";
     private final boolean mAlwaysUseTrafficStatsRateLimitCache;
     private final int mTrafficStatsRateLimitCacheExpiryDuration;
     private final int mTrafficStatsRateLimitCacheMaxEntries;
 
+    private final boolean mBroadcastNetworkStatsUpdatedRateLimitEnabled;
+
+
+
     private final Object mOpenSessionCallsLock = new Object();
 
     /**
@@ -669,6 +695,8 @@
 
         mAlwaysUseTrafficStatsRateLimitCache =
                 mDeps.alwaysUseTrafficStatsRateLimitCache(mContext);
+        mBroadcastNetworkStatsUpdatedRateLimitEnabled =
+                mDeps.enabledBroadcastNetworkStatsUpdatedRateLimiting(mContext);
         mTrafficStatsRateLimitCacheExpiryDuration =
                 mDeps.getTrafficStatsRateLimitCacheExpiryDuration();
         mTrafficStatsRateLimitCacheMaxEntries =
@@ -696,6 +724,15 @@
     @VisibleForTesting
     public static class Dependencies {
         /**
+         * Get broadcast network stats updated delay time in ms
+         * @return
+         */
+        @NonNull
+        public long getBroadcastNetworkStatsUpdateDelayMs() {
+            return BROADCAST_NETWORK_STATS_UPDATED_DELAY_MS;
+        }
+
+        /**
          * Get legacy platform stats directory.
          */
         @NonNull
@@ -927,6 +964,17 @@
         }
 
         /**
+         * Get whether broadcast network stats update rate limiting is enabled.
+         *
+         * This method should only be called once in the constructor,
+         * to ensure that the code does not need to deal with flag values changing at runtime.
+         */
+        public boolean enabledBroadcastNetworkStatsUpdatedRateLimiting(Context ctx) {
+            return DeviceConfigUtils.isTetheringFeatureNotChickenedOut(
+                    ctx, BROADCAST_NETWORK_STATS_UPDATED_RATE_LIMIT_ENABLED_FLAG);
+        }
+
+        /**
          * Get whether TrafficStats rate-limit cache is always applied.
          *
          * This method should only be called once in the constructor,
@@ -2645,8 +2693,22 @@
             performSampleLocked();
         }
 
-        // finally, dispatch updated event to any listeners
-        mHandler.sendMessage(mHandler.obtainMessage(MSG_BROADCAST_NETWORK_STATS_UPDATED));
+        // Dispatch updated event to listeners, preventing intent spamming
+        // (b/343844995) possibly from abnormal modem RAT changes or misbehaving
+        // app calls (see NetworkStatsEventLogger#POLL_REASON_* for possible reasons).
+        // If no broadcasts are scheduled, use the time of the last broadcast
+        // to schedule the next one ASAP.
+        if (!mBroadcastNetworkStatsUpdatedRateLimitEnabled) {
+            mHandler.sendMessage(mHandler.obtainMessage(MSG_BROADCAST_NETWORK_STATS_UPDATED));
+        } else if (mLatestNetworkStatsUpdatedBroadcastScheduledTime < SystemClock.uptimeMillis()) {
+            mLatestNetworkStatsUpdatedBroadcastScheduledTime = Math.max(
+                    mLatestNetworkStatsUpdatedBroadcastScheduledTime
+                            + mSettings.getBroadcastNetworkStatsUpdateDelayMs(),
+                    SystemClock.uptimeMillis()
+            );
+            mHandler.sendMessageAtTime(mHandler.obtainMessage(MSG_BROADCAST_NETWORK_STATS_UPDATED),
+                    mLatestNetworkStatsUpdatedBroadcastScheduledTime);
+        }
 
         Trace.traceEnd(TRACE_TAG_NETWORK);
     }
@@ -3605,6 +3667,11 @@
         public long getUidTagPersistBytes(long def) {
             return def;
         }
+
+        @Override
+        public long getBroadcastNetworkStatsUpdateDelayMs() {
+            return BROADCAST_NETWORK_STATS_UPDATED_DELAY_MS;
+        }
     }
 
     // TODO: Read stats by using BpfNetMapsReader.
diff --git a/service/Android.bp b/service/Android.bp
index 1a0e045..c68f0b8 100644
--- a/service/Android.bp
+++ b/service/Android.bp
@@ -206,6 +206,7 @@
     },
     visibility: [
         "//packages/modules/Connectivity/service-t",
+        "//packages/modules/Connectivity/networksecurity:__subpackages__",
         "//packages/modules/Connectivity/tests:__subpackages__",
         "//packages/modules/Connectivity/thread/service:__subpackages__",
         "//packages/modules/Connectivity/thread/tests:__subpackages__",
@@ -247,6 +248,7 @@
         "service-connectivity-pre-jarjar",
         "service-connectivity-tiramisu-pre-jarjar",
         "service-nearby-pre-jarjar",
+        "service-networksecurity-pre-jarjar",
         service_remoteauth_pre_jarjar_lib,
         "service-thread-pre-jarjar",
     ],
@@ -315,6 +317,7 @@
         ":framework-connectivity-jarjar-rules",
         ":service-connectivity-jarjar-gen",
         ":service-nearby-jarjar-gen",
+        ":service-networksecurity-jarjar-gen",
         ":service-remoteauth-jarjar-gen",
         ":service-thread-jarjar-gen",
     ],
@@ -404,6 +407,24 @@
     visibility: ["//visibility:private"],
 }
 
+java_genrule {
+    name: "service-networksecurity-jarjar-gen",
+    tool_files: [
+        ":service-networksecurity-pre-jarjar{.jar}",
+        "jarjar-excludes.txt",
+    ],
+    tools: [
+        "jarjar-rules-generator",
+    ],
+    out: ["service_ct_jarjar_rules.txt"],
+    cmd: "$(location jarjar-rules-generator) " +
+        "$(location :service-networksecurity-pre-jarjar{.jar}) " +
+        "--prefix com.android.server.net.ct " +
+        "--excludes $(location jarjar-excludes.txt) " +
+        "--output $(out)",
+    visibility: ["//visibility:private"],
+}
+
 genrule {
     name: "statslog-connectivity-java-gen",
     tools: ["stats-log-api-gen"],
diff --git a/service/src/com/android/server/connectivity/DnsManager.java b/service/src/com/android/server/connectivity/DnsManager.java
index ac02229..b95e3b1 100644
--- a/service/src/com/android/server/connectivity/DnsManager.java
+++ b/service/src/com/android/server/connectivity/DnsManager.java
@@ -41,6 +41,7 @@
 import android.net.NetworkCapabilities;
 import android.net.ResolverParamsParcel;
 import android.net.Uri;
+import android.net.resolv.aidl.DohParamsParcel;
 import android.net.shared.PrivateDnsConfig;
 import android.os.Binder;
 import android.os.RemoteException;
@@ -52,16 +53,17 @@
 import android.util.Pair;
 
 import java.net.InetAddress;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Iterator;
+import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
-import java.util.stream.Collectors;
 
 /**
  * Encapsulate the management of DNS settings for networks.
@@ -382,15 +384,14 @@
         paramsParcel.domains = getDomainStrings(lp.getDomains());
         paramsParcel.tlsName = strictMode ? privateDnsCfg.hostname : "";
         paramsParcel.tlsServers =
-                strictMode ? makeStrings(
-                        Arrays.stream(privateDnsCfg.ips)
-                              .filter((ip) -> lp.isReachable(ip))
-                              .collect(Collectors.toList()))
+                strictMode ? makeStrings(getReachableAddressList(privateDnsCfg.ips, lp))
                 : useTls ? paramsParcel.servers  // Opportunistic
                 : new String[0];            // Off
         paramsParcel.transportTypes = nc.getTransportTypes();
         paramsParcel.meteredNetwork = nc.isMetered();
         paramsParcel.interfaceNames = lp.getAllInterfaceNames().toArray(new String[0]);
+        paramsParcel.dohParams = makeDohParamsParcel(privateDnsCfg, lp);
+
         // Prepare to track the validation status of the DNS servers in the
         // resolver config when private DNS is in opportunistic or strict mode.
         if (useTls) {
@@ -404,15 +405,16 @@
         }
 
         Log.d(TAG, String.format("sendDnsConfigurationForNetwork(%d, %s, %s, %d, %d, %d, %d, "
-                + "%d, %d, %s, %s, %s, %b, %s)", paramsParcel.netId,
+                + "%d, %d, %s, %s, %s, %b, %s, %s, %s, %s, %d)", paramsParcel.netId,
                 Arrays.toString(paramsParcel.servers), Arrays.toString(paramsParcel.domains),
                 paramsParcel.sampleValiditySeconds, paramsParcel.successThreshold,
                 paramsParcel.minSamples, paramsParcel.maxSamples, paramsParcel.baseTimeoutMsec,
                 paramsParcel.retryCount, paramsParcel.tlsName,
                 Arrays.toString(paramsParcel.tlsServers),
                 Arrays.toString(paramsParcel.transportTypes), paramsParcel.meteredNetwork,
-                Arrays.toString(paramsParcel.interfaceNames)));
-
+                Arrays.toString(paramsParcel.interfaceNames),
+                paramsParcel.dohParams.name, Arrays.toString(paramsParcel.dohParams.ips),
+                paramsParcel.dohParams.dohpath, paramsParcel.dohParams.port));
         try {
             mDnsResolver.setResolverConfiguration(paramsParcel);
         } catch (RemoteException | ServiceSpecificException e) {
@@ -498,4 +500,26 @@
     private static String[] getDomainStrings(String domains) {
         return (TextUtils.isEmpty(domains)) ? new String[0] : domains.split(" ");
     }
+
+    @NonNull
+    private List<InetAddress> getReachableAddressList(@NonNull InetAddress[] ips,
+            @NonNull LinkProperties lp) {
+        final ArrayList<InetAddress> out = new ArrayList<InetAddress>(Arrays.asList(ips));
+        out.removeIf(ip -> !lp.isReachable(ip));
+        return out;
+    }
+
+    @NonNull
+    private DohParamsParcel makeDohParamsParcel(@NonNull PrivateDnsConfig cfg,
+            @NonNull LinkProperties lp) {
+        if (cfg.mode == PRIVATE_DNS_MODE_OFF) {
+            return new DohParamsParcel.Builder().build();
+        }
+        return new DohParamsParcel.Builder()
+                .setName(cfg.dohName)
+                .setIps(makeStrings(getReachableAddressList(cfg.dohIps, lp)))
+                .setDohpath(cfg.dohPath)
+                .setPort(cfg.dohPort)
+                .build();
+    }
 }
diff --git a/staticlibs/testutils/host/python/wifip2p_utils.py b/staticlibs/testutils/host/python/wifip2p_utils.py
index 8b4ffa5..ef6af75 100644
--- a/staticlibs/testutils/host/python/wifip2p_utils.py
+++ b/staticlibs/testutils/host/python/wifip2p_utils.py
@@ -14,11 +14,13 @@
 
 from mobly import asserts
 from mobly.controllers import android_device
+from net_tests_utils.host.python import tether_utils
 
 
 def assume_wifi_p2p_test_preconditions(
     server_device: android_device, client_device: android_device
 ) -> None:
+  """Preconditions check for running Wi-Fi P2P test."""
   server = server_device.connectivity_multi_devices_snippet
   client = client_device.connectivity_multi_devices_snippet
 
@@ -36,10 +38,51 @@
 def setup_wifi_p2p_server_and_client(
     server_device: android_device, client_device: android_device
 ) -> None:
-  """Set up the Wi-Fi P2P server and client."""
+  """Set up the Wi-Fi P2P server and client, then connect them to establish a Wi-Fi P2P connection."""
+  server = server_device.connectivity_multi_devices_snippet
+  client = client_device.connectivity_multi_devices_snippet
+
   # Start Wi-Fi P2P on both server and client.
-  server_device.connectivity_multi_devices_snippet.startWifiP2p()
-  client_device.connectivity_multi_devices_snippet.startWifiP2p()
+  server.startWifiP2p()
+  client.startWifiP2p()
+
+  # Get the current device name
+  server_name = server.getDeviceName()
+  client_name = client.getDeviceName()
+
+  # Generate Wi-Fi P2P group passphrase with random characters.
+  group_name = "DIRECT-" + tether_utils.generate_uuid32_base64()
+  group_passphrase = tether_utils.generate_uuid32_base64()
+
+  # Server creates a Wi-Fi P2P group
+  server.createGroup(group_name, group_passphrase)
+
+  # Start Wi-Fi P2p peers discovery on both devices
+  server.startPeersDiscovery()
+  client.startPeersDiscovery()
+
+  # Ensure the target device has been discovered
+  server_address = client.ensureDeviceDiscovered(server_name)
+  client_address = server.ensureDeviceDiscovered(client_name)
+
+  # Server invites the device to the group
+  server.inviteDeviceToGroup(group_name, group_passphrase, client_address)
+
+  # Wait for a p2p connection changed intent to ensure the invitation has been
+  # received.
+  client.waitForP2pConnectionChanged(True, group_name)
+  # Accept the group invitation
+  client.acceptGroupInvitation(server_address)
+
+  # Server waits for connection request from client and accept joining
+  server.waitForPeerConnectionRequestAndAcceptJoining(client_address)
+
+  # Wait for a p2p connection changed intent to ensure joining the group
+  client.waitForP2pConnectionChanged(False, group_name)
+
+  # Ensure Wi-Fi P2P connected on both devices
+  client.ensureDeviceConnected(server_name)
+  server.ensureDeviceConnected(client_name)
 
 
 def cleanup_wifi_p2p(
diff --git a/tests/cts/multidevices/connectivity_multi_devices_test.py b/tests/cts/multidevices/connectivity_multi_devices_test.py
index eceb535..416a2e8 100644
--- a/tests/cts/multidevices/connectivity_multi_devices_test.py
+++ b/tests/cts/multidevices/connectivity_multi_devices_test.py
@@ -13,6 +13,7 @@
 #  limitations under the License.
 
 from net_tests_utils.host.python import mdns_utils, multi_devices_test_base, tether_utils
+from net_tests_utils.host.python import wifip2p_utils
 from net_tests_utils.host.python.tether_utils import UpstreamType
 
 
@@ -68,3 +69,21 @@
       tether_utils.cleanup_tethering_for_upstream_type(
           self.serverDevice, UpstreamType.NONE
       )
+
+  def test_mdns_via_wifip2p(self):
+    wifip2p_utils.assume_wifi_p2p_test_preconditions(
+        self.serverDevice, self.clientDevice
+    )
+    mdns_utils.assume_mdns_test_preconditions(
+        self.clientDevice, self.serverDevice
+    )
+    try:
+      wifip2p_utils.setup_wifi_p2p_server_and_client(
+          self.serverDevice, self.clientDevice
+      )
+      mdns_utils.register_mdns_service_and_discover_resolve(
+          self.clientDevice, self.serverDevice
+      )
+    finally:
+      mdns_utils.cleanup_mdns_service(self.clientDevice, self.serverDevice)
+      wifip2p_utils.cleanup_wifi_p2p(self.serverDevice, self.clientDevice)
diff --git a/tests/cts/multidevices/snippet/Wifip2pMultiDevicesSnippet.kt b/tests/cts/multidevices/snippet/Wifip2pMultiDevicesSnippet.kt
index e0929bb..f8c9351 100644
--- a/tests/cts/multidevices/snippet/Wifip2pMultiDevicesSnippet.kt
+++ b/tests/cts/multidevices/snippet/Wifip2pMultiDevicesSnippet.kt
@@ -16,13 +16,28 @@
 
 package com.google.snippet.connectivity
 
+import android.Manifest.permission.MANAGE_WIFI_NETWORK_SELECTION
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.net.MacAddress
 import android.net.wifi.WifiManager
+import android.net.wifi.p2p.WifiP2pConfig
+import android.net.wifi.p2p.WifiP2pDevice
+import android.net.wifi.p2p.WifiP2pDeviceList
+import android.net.wifi.p2p.WifiP2pGroup
 import android.net.wifi.p2p.WifiP2pManager
 import androidx.test.platform.app.InstrumentationRegistry
+import com.android.net.module.util.ArrayTrackRecord
+import com.android.testutils.runAsShell
 import com.google.android.mobly.snippet.Snippet
 import com.google.android.mobly.snippet.rpc.Rpc
+import com.google.snippet.connectivity.Wifip2pMultiDevicesSnippet.Wifip2pIntentReceiver.IntentReceivedEvent.ConnectionChanged
+import com.google.snippet.connectivity.Wifip2pMultiDevicesSnippet.Wifip2pIntentReceiver.IntentReceivedEvent.PeersChanged
 import java.util.concurrent.CompletableFuture
 import java.util.concurrent.TimeUnit
+import kotlin.test.assertNotNull
 import kotlin.test.fail
 
 private const val TIMEOUT_MS = 60000L
@@ -38,6 +53,35 @@
                 ?: fail("Could not get WifiP2pManager service")
     }
     private lateinit var wifip2pChannel: WifiP2pManager.Channel
+    private val wifip2pIntentReceiver = Wifip2pIntentReceiver()
+
+    private class Wifip2pIntentReceiver : BroadcastReceiver() {
+        val history = ArrayTrackRecord<IntentReceivedEvent>().newReadHead()
+
+        sealed class IntentReceivedEvent {
+            abstract val intent: Intent
+            data class ConnectionChanged(override val intent: Intent) : IntentReceivedEvent()
+            data class PeersChanged(override val intent: Intent) : IntentReceivedEvent()
+        }
+
+        override fun onReceive(context: Context, intent: Intent) {
+            when (intent.action) {
+                WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION -> {
+                    history.add(ConnectionChanged(intent))
+                }
+                WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION -> {
+                    history.add(PeersChanged(intent))
+                }
+            }
+        }
+
+        inline fun <reified T : IntentReceivedEvent> eventuallyExpectedIntent(
+                timeoutMs: Long = TIMEOUT_MS,
+                crossinline predicate: (T) -> Boolean = { true }
+        ): T = history.poll(timeoutMs) { it is T && predicate(it) }.also {
+            assertNotNull(it, "Intent ${T::class} not received within ${timeoutMs}ms.")
+        } as T
+    }
 
     @Rpc(description = "Check whether the device supports Wi-Fi P2P.")
     fun isP2pSupported() = wifiManager.isP2pSupported()
@@ -55,6 +99,10 @@
             }
         }
         p2pStateEnabledFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
+        // Register an intent filter to receive Wi-Fi P2P intents
+        val filter = IntentFilter(WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION)
+        filter.addAction(WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION)
+        context.registerReceiver(wifip2pIntentReceiver, filter)
     }
 
     @Rpc(description = "Stop Wi-Fi P2P")
@@ -63,5 +111,202 @@
             wifip2pManager.cancelConnect(wifip2pChannel, null)
             wifip2pManager.removeGroup(wifip2pChannel, null)
         }
+        // Unregister the intent filter
+        context.unregisterReceiver(wifip2pIntentReceiver)
+    }
+
+    @Rpc(description = "Get the current device name")
+    fun getDeviceName(): String {
+        // Retrieve current device info
+        val deviceFuture = CompletableFuture<String>()
+        wifip2pManager.requestDeviceInfo(wifip2pChannel) { wifiP2pDevice ->
+            if (wifiP2pDevice != null) {
+                deviceFuture.complete(wifiP2pDevice.deviceName)
+            }
+        }
+        // Return current device name
+        return deviceFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
+    }
+
+    @Rpc(description = "Wait for a p2p connection changed intent and check the group")
+    @Suppress("DEPRECATION")
+    fun waitForP2pConnectionChanged(ignoreGroupCheck: Boolean, groupName: String) {
+        wifip2pIntentReceiver.eventuallyExpectedIntent<ConnectionChanged>() {
+            val p2pGroup: WifiP2pGroup? =
+                    it.intent.getParcelableExtra(WifiP2pManager.EXTRA_WIFI_P2P_GROUP)
+            val groupMatched = p2pGroup?.networkName == groupName
+            return@eventuallyExpectedIntent ignoreGroupCheck || groupMatched
+        }
+    }
+
+    @Rpc(description = "Create a Wi-Fi P2P group")
+    fun createGroup(groupName: String, groupPassphrase: String) {
+        // Create a Wi-Fi P2P group
+        val wifip2pConfig = WifiP2pConfig.Builder()
+                .setNetworkName(groupName)
+                .setPassphrase(groupPassphrase)
+                .build()
+        val createGroupFuture = CompletableFuture<Boolean>()
+        wifip2pManager.createGroup(
+                wifip2pChannel,
+                wifip2pConfig,
+                object : WifiP2pManager.ActionListener {
+                    override fun onFailure(reason: Int) = Unit
+                    override fun onSuccess() { createGroupFuture.complete(true) }
+                }
+        )
+        createGroupFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
+
+        // Ensure the Wi-Fi P2P group is created.
+        waitForP2pConnectionChanged(false, groupName)
+    }
+
+    @Rpc(description = "Start Wi-Fi P2P peers discovery")
+    fun startPeersDiscovery() {
+        // Start discovery Wi-Fi P2P peers
+        wifip2pManager.discoverPeers(wifip2pChannel, null)
+
+        // Ensure the discovery is started
+        val p2pDiscoveryStartedFuture = CompletableFuture<Boolean>()
+        wifip2pManager.requestDiscoveryState(wifip2pChannel) { state ->
+            if (state == WifiP2pManager.WIFI_P2P_DISCOVERY_STARTED) {
+                p2pDiscoveryStartedFuture.complete(true)
+            }
+        }
+        p2pDiscoveryStartedFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
+    }
+
+    /**
+     * Get the device address from the given intent that matches the given device name.
+     *
+     * @param peersChangedIntent the intent to get the device address from
+     * @param deviceName the target device name
+     * @return the address of the target device or null if no devices match.
+     */
+    @Suppress("DEPRECATION")
+    private fun getDeviceAddress(peersChangedIntent: Intent, deviceName: String): String? {
+        val peers: WifiP2pDeviceList? =
+                peersChangedIntent.getParcelableExtra(WifiP2pManager.EXTRA_P2P_DEVICE_LIST)
+        return peers?.deviceList?.firstOrNull { it.deviceName == deviceName }?.deviceAddress
+    }
+
+    /**
+     * Ensure the given device has been discovered and returns the associated device address for
+     * connection.
+     *
+     * @param deviceName the target device name
+     * @return the address of the target device.
+     */
+    @Rpc(description = "Ensure the target Wi-Fi P2P device is discovered")
+    fun ensureDeviceDiscovered(deviceName: String): String {
+        val changedEvent = wifip2pIntentReceiver.eventuallyExpectedIntent<PeersChanged>() {
+            return@eventuallyExpectedIntent getDeviceAddress(it.intent, deviceName) != null
+        }
+        return getDeviceAddress(changedEvent.intent, deviceName)
+                ?: fail("Missing device in filtered intent")
+    }
+
+    @Rpc(description = "Invite a Wi-Fi P2P device to the group")
+    fun inviteDeviceToGroup(groupName: String, groupPassphrase: String, deviceAddress: String) {
+        // Connect to the device to send invitation
+        val wifip2pConfig = WifiP2pConfig.Builder()
+                .setNetworkName(groupName)
+                .setPassphrase(groupPassphrase)
+                .setDeviceAddress(MacAddress.fromString(deviceAddress))
+                .build()
+        val connectedFuture = CompletableFuture<Boolean>()
+        wifip2pManager.connect(
+                wifip2pChannel,
+                wifip2pConfig,
+                object : WifiP2pManager.ActionListener {
+                    override fun onFailure(reason: Int) = Unit
+                    override fun onSuccess() {
+                        connectedFuture.complete(true)
+                    }
+                }
+        )
+        connectedFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
+    }
+
+    private fun runExternalApproverForGroupProcess(
+            deviceAddress: String,
+            isGroupInvitation: Boolean
+    ) {
+        val peer = MacAddress.fromString(deviceAddress)
+        runAsShell(MANAGE_WIFI_NETWORK_SELECTION) {
+            val connectionRequestFuture = CompletableFuture<Boolean>()
+            val attachedFuture = CompletableFuture<Boolean>()
+            wifip2pManager.addExternalApprover(
+                    wifip2pChannel,
+                    peer,
+                    object : WifiP2pManager.ExternalApproverRequestListener {
+                        override fun onAttached(deviceAddress: MacAddress) {
+                            attachedFuture.complete(true)
+                        }
+                        override fun onDetached(deviceAddress: MacAddress, reason: Int) = Unit
+                        override fun onConnectionRequested(
+                                requestType: Int,
+                                config: WifiP2pConfig,
+                                device: WifiP2pDevice
+                        ) {
+                            connectionRequestFuture.complete(true)
+                        }
+                        override fun onPinGenerated(deviceAddress: MacAddress, pin: String) = Unit
+                    }
+            )
+            if (isGroupInvitation) attachedFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS) else
+                connectionRequestFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
+
+            val resultFuture = CompletableFuture<Boolean>()
+            wifip2pManager.setConnectionRequestResult(
+                    wifip2pChannel,
+                    peer,
+                    WifiP2pManager.CONNECTION_REQUEST_ACCEPT,
+                    object : WifiP2pManager.ActionListener {
+                        override fun onFailure(reason: Int) = Unit
+                        override fun onSuccess() {
+                            resultFuture.complete(true)
+                        }
+                    }
+            )
+            resultFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
+
+            val removeFuture = CompletableFuture<Boolean>()
+            wifip2pManager.removeExternalApprover(
+                    wifip2pChannel,
+                    peer,
+                    object : WifiP2pManager.ActionListener {
+                        override fun onFailure(reason: Int) = Unit
+                        override fun onSuccess() {
+                            removeFuture.complete(true)
+                        }
+                    }
+            )
+            removeFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
+        }
+    }
+
+    @Rpc(description = "Accept P2P group invitation from device")
+    fun acceptGroupInvitation(deviceAddress: String) {
+        // Accept the Wi-Fi P2P group invitation
+        runExternalApproverForGroupProcess(deviceAddress, true /* isGroupInvitation */)
+    }
+
+    @Rpc(description = "Wait for connection request from the peer and accept joining")
+    fun waitForPeerConnectionRequestAndAcceptJoining(deviceAddress: String) {
+        // Wait for connection request from the peer and accept joining
+        runExternalApproverForGroupProcess(deviceAddress, false /* isGroupInvitation */)
+    }
+
+    @Rpc(description = "Ensure the target device is connected")
+    fun ensureDeviceConnected(deviceName: String) {
+        // Retrieve peers and ensure the target device is connected
+        val connectedFuture = CompletableFuture<Boolean>()
+        wifip2pManager.requestPeers(wifip2pChannel) { peers -> peers?.deviceList?.any {
+            it.deviceName == deviceName && it.status == WifiP2pDevice.CONNECTED }.let {
+                connectedFuture.complete(true)
+            }
+        }
+        connectedFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
     }
 }
diff --git a/tests/cts/net/src/android/net/cts/DnsTest.java b/tests/cts/net/src/android/net/cts/DnsTest.java
index fb63a19..b1e5680 100644
--- a/tests/cts/net/src/android/net/cts/DnsTest.java
+++ b/tests/cts/net/src/android/net/cts/DnsTest.java
@@ -16,6 +16,10 @@
 
 package android.net.cts;
 
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import android.content.Context;
 import android.content.pm.PackageManager;
 import android.net.ConnectivityManager;
 import android.net.ConnectivityManager.NetworkCallback;
@@ -23,10 +27,16 @@
 import android.net.Network;
 import android.net.NetworkInfo;
 import android.os.SystemClock;
-import android.test.AndroidTestCase;
 import android.util.Log;
 
 import androidx.test.filters.RequiresDevice;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.testutils.DevSdkIgnoreRunner;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
 
 import java.net.Inet4Address;
 import java.net.Inet6Address;
@@ -36,7 +46,9 @@
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
 
-public class DnsTest extends AndroidTestCase {
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRunner.RestoreDefaultNetwork
+public class DnsTest {
 
     static {
         System.loadLibrary("nativedns_jni");
@@ -46,10 +58,13 @@
     private static final String TAG = "DnsTest";
     private static final String PROXY_NETWORK_TYPE = "PROXY";
 
+    private Context mContext;
     private ConnectivityManager mCm;
 
+    @Before
     public void setUp() {
-        mCm = getContext().getSystemService(ConnectivityManager.class);
+        mContext = InstrumentationRegistry.getInstrumentation().getContext();
+        mCm = mContext.getSystemService(ConnectivityManager.class);
     }
 
     /**
@@ -69,6 +84,7 @@
      * Perf - measure size of first and second tier caches and their effect
      * Assert requires network permission
      */
+    @Test
     @RequiresDevice // IPv6 support may be missing on presubmit virtual hardware
     public void testDnsWorks() throws Exception {
         ensureIpv6Connectivity();
@@ -91,7 +107,7 @@
 
         // Skip the rest of the test if the active network for watch is PROXY.
         // TODO: Check NetworkInfo type in addition to type name once ag/601257 is merged.
-        if (getContext().getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH)
+        if (mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH)
                 && activeNetworkInfoIsProxy()) {
             Log.i(TAG, "Skipping test because the active network type name is PROXY.");
             return;
diff --git a/tests/unit/java/com/android/server/connectivity/DnsManagerTest.java b/tests/unit/java/com/android/server/connectivity/DnsManagerTest.java
index ea3d2dd..b47b97d 100644
--- a/tests/unit/java/com/android/server/connectivity/DnsManagerTest.java
+++ b/tests/unit/java/com/android/server/connectivity/DnsManagerTest.java
@@ -54,6 +54,7 @@
 import android.net.ResolverOptionsParcel;
 import android.net.ResolverParamsParcel;
 import android.net.RouteInfo;
+import android.net.resolv.aidl.DohParamsParcel;
 import android.net.shared.PrivateDnsConfig;
 import android.os.Build;
 import android.provider.Settings;
@@ -327,8 +328,16 @@
     @Test
     public void testSendDnsConfiguration() throws Exception {
         reset(mMockDnsResolver);
-        mDnsManager.updatePrivateDns(new Network(TEST_NETID),
-                mDnsManager.getPrivateDnsConfig());
+        final PrivateDnsConfig cfg = new PrivateDnsConfig(
+                PRIVATE_DNS_MODE_OPPORTUNISTIC /* mode */,
+                null /* hostname */,
+                null /* ips */,
+                "doh.com" /* dohName */,
+                null /* dohIps */,
+                "/some-path{?dns}" /* dohPath */,
+                5353 /* dohPort */);
+
+        mDnsManager.updatePrivateDns(new Network(TEST_NETID), cfg);
         final LinkProperties lp = new LinkProperties();
         lp.setInterfaceName(TEST_IFACENAME);
         lp.addDnsServer(InetAddress.getByName("3.3.3.3"));
@@ -352,7 +361,11 @@
         expectedParams.transportTypes = TEST_TRANSPORT_TYPES;
         expectedParams.resolverOptions = null;
         expectedParams.meteredNetwork = true;
-        expectedParams.dohParams = null;
+        expectedParams.dohParams = new DohParamsParcel.Builder()
+                .setName("doh.com")
+                .setDohpath("/some-path{?dns}")
+                .setPort(5353)
+                .build();
         expectedParams.interfaceNames = new String[]{TEST_IFACENAME};
         verify(mMockDnsResolver, times(1)).setResolverConfiguration(eq(expectedParams));
     }
diff --git a/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java b/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
index 7e0a225..3d2f389 100644
--- a/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
+++ b/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
@@ -69,6 +69,8 @@
 import static com.android.server.net.NetworkStatsEventLogger.POLL_REASON_RAT_CHANGED;
 import static com.android.server.net.NetworkStatsEventLogger.PollEvent.pollReasonNameOf;
 import static com.android.server.net.NetworkStatsService.ACTION_NETWORK_STATS_POLL;
+import static com.android.server.net.NetworkStatsService.ACTION_NETWORK_STATS_UPDATED;
+import static com.android.server.net.NetworkStatsService.BROADCAST_NETWORK_STATS_UPDATED_RATE_LIMIT_ENABLED_FLAG;
 import static com.android.server.net.NetworkStatsService.DEFAULT_TRAFFIC_STATS_CACHE_EXPIRY_DURATION_MS;
 import static com.android.server.net.NetworkStatsService.DEFAULT_TRAFFIC_STATS_CACHE_MAX_ENTRIES;
 import static com.android.server.net.NetworkStatsService.NETSTATS_FASTDATAINPUT_FALLBACKS_COUNTER_NAME;
@@ -82,6 +84,7 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.AdditionalMatchers.aryEq;
@@ -101,8 +104,10 @@
 
 import android.annotation.NonNull;
 import android.app.AlarmManager;
+import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
+import android.content.IntentFilter;
 import android.content.pm.PackageManager;
 import android.content.res.Resources;
 import android.database.ContentObserver;
@@ -138,6 +143,7 @@
 import android.provider.Settings;
 import android.system.ErrnoException;
 import android.telephony.TelephonyManager;
+import android.testing.TestableLooper;
 import android.text.TextUtils;
 import android.util.ArrayMap;
 import android.util.IndentingPrintWriter;
@@ -150,6 +156,7 @@
 import com.android.connectivity.resources.R;
 import com.android.internal.util.FileRotator;
 import com.android.internal.util.test.BroadcastInterceptingContext;
+import com.android.net.module.util.ArrayTrackRecord;
 import com.android.net.module.util.BpfDump;
 import com.android.net.module.util.IBpfMap;
 import com.android.net.module.util.LocationPermissionChecker;
@@ -618,6 +625,12 @@
         }
 
         @Override
+        public boolean enabledBroadcastNetworkStatsUpdatedRateLimiting(Context ctx) {
+            return mFeatureFlags.getOrDefault(
+                    BROADCAST_NETWORK_STATS_UPDATED_RATE_LIMIT_ENABLED_FLAG, true);
+        }
+
+        @Override
         public int getTrafficStatsRateLimitCacheExpiryDuration() {
             return DEFAULT_TRAFFIC_STATS_CACHE_EXPIRY_DURATION_MS;
         }
@@ -2617,6 +2630,8 @@
 
     private void mockDefaultSettings() throws Exception {
         mockSettings(HOUR_IN_MILLIS, WEEK_IN_MILLIS);
+        mSettings.setBroadcastNetworkStatsUpdateDelayMs(
+                NetworkStatsService.BROADCAST_NETWORK_STATS_UPDATED_DELAY_MS);
     }
 
     private void mockSettings(long bucketDuration, long deleteAge) {
@@ -2631,6 +2646,8 @@
         @NonNull
         private volatile Config mConfig;
         private final AtomicBoolean mCombineSubtypeEnabled = new AtomicBoolean();
+        private long mBroadcastNetworkStatsUpdateDelayMs =
+                NetworkStatsService.BROADCAST_NETWORK_STATS_UPDATED_DELAY_MS;
 
         TestNetworkStatsSettings(long bucketDuration, long deleteAge) {
             mConfig = new Config(bucketDuration, deleteAge, deleteAge);
@@ -2693,6 +2710,15 @@
         public boolean getAugmentEnabled() {
             return false;
         }
+
+        @Override
+        public long getBroadcastNetworkStatsUpdateDelayMs() {
+            return mBroadcastNetworkStatsUpdateDelayMs;
+        }
+
+        public void setBroadcastNetworkStatsUpdateDelayMs(long broadcastDelay) {
+            mBroadcastNetworkStatsUpdateDelayMs = broadcastDelay;
+        }
     }
 
     private void assertStatsFilesExist(boolean exist) {
@@ -3064,4 +3090,91 @@
         final String dump = getDump();
         assertDumpContains(dump, "Log for testing");
     }
+
+    private static class TestNetworkStatsUpdatedReceiver extends BroadcastReceiver {
+        private final ArrayTrackRecord<Intent>.ReadHead mHistory;
+
+        TestNetworkStatsUpdatedReceiver() {
+            mHistory = (new ArrayTrackRecord<Intent>()).newReadHead();
+        }
+
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            mHistory.add(intent);
+        }
+
+        /**
+         * Assert no broadcast intent is received in blocking manner
+         */
+        public void assertNoBroadcastIntentReceived()  {
+            assertNull(mHistory.peek());
+        }
+
+        /**
+         * Assert an intent is received and remove it from queue
+         */
+        public void assertBroadcastIntentReceived() {
+            assertNotNull(mHistory.poll(WAIT_TIMEOUT, number -> true));
+        }
+    }
+
+    @FeatureFlag(name = BROADCAST_NETWORK_STATS_UPDATED_RATE_LIMIT_ENABLED_FLAG)
+    @Test
+    public void testNetworkStatsUpdatedIntentSpam_rateLimitOn() throws Exception {
+        // Set the update delay long enough that messages won't be processed before unblocked
+        // Set a short time to test the behavior before reaching delay.
+        // Constraint: test running time < toleranceMs < update delay time
+        mSettings.setBroadcastNetworkStatsUpdateDelayMs(100_000L);
+        final long toleranceMs = 5000;
+
+        final TestableLooper mTestableLooper = new TestableLooper(mHandlerThread.getLooper());
+        final TestNetworkStatsUpdatedReceiver receiver = new TestNetworkStatsUpdatedReceiver();
+        mServiceContext.registerReceiver(receiver, new IntentFilter(ACTION_NETWORK_STATS_UPDATED));
+
+        try {
+            // Test that before anything, the intent is delivered immediately
+            mService.forceUpdate();
+            mTestableLooper.processAllMessages();
+            receiver.assertBroadcastIntentReceived();
+            receiver.assertNoBroadcastIntentReceived();
+
+            // Test that the next two intents results in exactly one intent delivered
+            for (int i = 0; i < 2; i++) {
+                mService.forceUpdate();
+            }
+            // Test that the delay depends on our set value
+            mTestableLooper.moveTimeForward(mSettings.getBroadcastNetworkStatsUpdateDelayMs()
+                    - toleranceMs);
+            mTestableLooper.processAllMessages();
+            receiver.assertNoBroadcastIntentReceived();
+
+            // Unblock messages and test that the second and third update
+            // is broadcasted right after the delay
+            mTestableLooper.moveTimeForward(toleranceMs);
+            mTestableLooper.processAllMessages();
+            receiver.assertBroadcastIntentReceived();
+            receiver.assertNoBroadcastIntentReceived();
+
+        } finally {
+            mTestableLooper.destroy();
+        }
+    }
+
+    @FeatureFlag(name = BROADCAST_NETWORK_STATS_UPDATED_RATE_LIMIT_ENABLED_FLAG, enabled = false)
+    @Test
+    public void testNetworkStatsUpdatedIntentSpam_rateLimitOff() throws Exception {
+        // Set the update delay long enough to ensure that messages are processed
+        // despite the rate limit.
+        mSettings.setBroadcastNetworkStatsUpdateDelayMs(100_000L);
+
+        final TestNetworkStatsUpdatedReceiver receiver = new TestNetworkStatsUpdatedReceiver();
+        mServiceContext.registerReceiver(receiver, new IntentFilter(ACTION_NETWORK_STATS_UPDATED));
+
+        for (int i = 0; i < 2; i++) {
+            mService.forceUpdate();
+            waitForIdle();
+            receiver.assertBroadcastIntentReceived();
+        }
+        receiver.assertNoBroadcastIntentReceived();
+    }
 }
diff --git a/thread/framework/java/android/net/thread/ChannelMaxPower.aidl b/thread/framework/java/android/net/thread/ChannelMaxPower.aidl
index bcda8a8..abc00b9 100644
--- a/thread/framework/java/android/net/thread/ChannelMaxPower.aidl
+++ b/thread/framework/java/android/net/thread/ChannelMaxPower.aidl
@@ -16,11 +16,11 @@
 
 package android.net.thread;
 
- /**
-  * Mapping from a channel to its max power.
-  *
-  * {@hide}
-  */
+/**
+ * Mapping from a channel to its max power.
+ *
+ * {@hide}
+ */
 parcelable ChannelMaxPower {
     int channel; // The Thread radio channel.
     int maxPower; // The max power in the unit of 0.01dBm. Passing INT16_MAX(32767) will
diff --git a/thread/framework/java/android/net/thread/IOperationalDatasetCallback.aidl b/thread/framework/java/android/net/thread/IOperationalDatasetCallback.aidl
index b576b33..3fece65 100644
--- a/thread/framework/java/android/net/thread/IOperationalDatasetCallback.aidl
+++ b/thread/framework/java/android/net/thread/IOperationalDatasetCallback.aidl
@@ -23,6 +23,8 @@
  * @hide
  */
 oneway interface IOperationalDatasetCallback {
-    void onActiveOperationalDatasetChanged(in @nullable ActiveOperationalDataset activeOpDataset);
-    void onPendingOperationalDatasetChanged(in @nullable PendingOperationalDataset pendingOpDataset);
+    void onActiveOperationalDatasetChanged(
+            in @nullable ActiveOperationalDataset activeOpDataset);
+    void onPendingOperationalDatasetChanged(
+            in @nullable PendingOperationalDataset pendingOpDataset);
 }
diff --git a/thread/framework/java/android/net/thread/IThreadNetworkController.aidl b/thread/framework/java/android/net/thread/IThreadNetworkController.aidl
index f50de74..b7f68c9 100644
--- a/thread/framework/java/android/net/thread/IThreadNetworkController.aidl
+++ b/thread/framework/java/android/net/thread/IThreadNetworkController.aidl
@@ -28,9 +28,9 @@
 import android.net.thread.ThreadConfiguration;
 
 /**
-* Interface for communicating with ThreadNetworkControllerService.
-* @hide
-*/
+ * Interface for communicating with ThreadNetworkControllerService.
+ * @hide
+ */
 interface IThreadNetworkController {
     void registerStateCallback(in IStateCallback callback);
     void unregisterStateCallback(in IStateCallback callback);
@@ -38,10 +38,12 @@
     void unregisterOperationalDatasetCallback(in IOperationalDatasetCallback callback);
 
     void join(in ActiveOperationalDataset activeOpDataset, in IOperationReceiver receiver);
-    void scheduleMigration(in PendingOperationalDataset pendingOpDataset, in IOperationReceiver receiver);
+    void scheduleMigration(
+            in PendingOperationalDataset pendingOpDataset, in IOperationReceiver receiver);
     void leave(in IOperationReceiver receiver);
 
-    void setTestNetworkAsUpstream(in String testNetworkInterfaceName, in IOperationReceiver receiver);
+    void setTestNetworkAsUpstream(
+            in String testNetworkInterfaceName, in IOperationReceiver receiver);
     void setChannelMaxPowers(in ChannelMaxPower[] channelMaxPowers, in IOperationReceiver receiver);
 
     int getThreadVersion();
diff --git a/thread/framework/java/android/net/thread/IThreadNetworkManager.aidl b/thread/framework/java/android/net/thread/IThreadNetworkManager.aidl
index 0e394b1..b63cd72 100644
--- a/thread/framework/java/android/net/thread/IThreadNetworkManager.aidl
+++ b/thread/framework/java/android/net/thread/IThreadNetworkManager.aidl
@@ -19,9 +19,9 @@
 import android.net.thread.IThreadNetworkController;
 
 /**
-* Interface for communicating with ThreadNetworkService.
-* @hide
-*/
+ * Interface for communicating with ThreadNetworkService.
+ * @hide
+ */
 interface IThreadNetworkManager {
     List<IThreadNetworkController> getAllThreadNetworkControllers();
 }
diff --git a/thread/scripts/make-pretty.sh b/thread/scripts/make-pretty.sh
index c176bfa..e012d41 100755
--- a/thread/scripts/make-pretty.sh
+++ b/thread/scripts/make-pretty.sh
@@ -1,9 +1,35 @@
 #!/usr/bin/env bash
 
-SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
+ANDROID_ROOT_DIR=$(
+    while [ ! -d ".repo" ] && [ "$PWD" != "/" ]; do cd ..; done
+    pwd
+)
 
-GOOGLE_JAVA_FORMAT=$SCRIPT_DIR/../../../../../prebuilts/tools/common/google-java-format/google-java-format
-ANDROID_BP_FORMAT=$SCRIPT_DIR/../../../../../prebuilts/build-tools/linux-x86/bin/bpfmt
+if [ ! -d "$ANDROID_ROOT_DIR/.repo" ]; then
+    echo "Error: The script has to run in an Android repo checkout"
+    exit 1
+fi
 
-$GOOGLE_JAVA_FORMAT --aosp -i $(find $SCRIPT_DIR/../ -name "*.java")
-$ANDROID_BP_FORMAT -w $(find $SCRIPT_DIR/../ -name "*.bp")
+GOOGLE_JAVA_FORMAT=$ANDROID_ROOT_DIR/prebuilts/tools/common/google-java-format/google-java-format
+ANDROID_BP_FORMAT=$ANDROID_ROOT_DIR/prebuilts/build-tools/linux-x86/bin/bpfmt
+AIDL_FORMAT=$ANDROID_ROOT_DIR/system/tools/aidl/aidl-format.sh
+
+CONNECTIVITY_DIR=$ANDROID_ROOT_DIR/packages/modules/Connectivity
+OPENTHREAD_DIR=$ANDROID_ROOT_DIR/external/openthread
+OTBR_POSIX_DIR=$ANDROID_ROOT_DIR/external/ot-br-posix
+
+ALLOWED_CODE_DIRS=($CONNECTIVITY_DIR $OPENTHREAD_DIR $OTBR_POSIX_DIR)
+CODE_DIR=$(git rev-parse --show-toplevel)
+
+if [[ ! " ${ALLOWED_CODE_DIRS[@]} " =~ " ${CODE_DIR} " ]]; then
+    echo "Error: The script has to run in the Git project Connectivity, openthread or ot-br-posix"
+    exit 1
+fi
+
+if [[ $CODE_DIR == $CONNECTIVITY_DIR ]]; then
+    CODE_DIR=$CODE_DIR"/thread"
+fi
+
+$GOOGLE_JAVA_FORMAT --aosp -i $(find $CODE_DIR -name "*.java")
+$ANDROID_BP_FORMAT -w $(find $CODE_DIR -name "*.bp")
+$AIDL_FORMAT -w $(find $CODE_DIR -name "*.aidl")