Merge "Use helper function getIndexForValue in MulticastRoutingCoordinator." into main
diff --git a/Cronet/tests/common/AndroidTest.xml b/Cronet/tests/common/AndroidTest.xml
index ae6b65b..7646a04 100644
--- a/Cronet/tests/common/AndroidTest.xml
+++ b/Cronet/tests/common/AndroidTest.xml
@@ -43,6 +43,8 @@
         <option name="exclude-filter" value="org.chromium.net.NetworkChangesTest" />
         <!-- b/316550794 -->
         <option name="exclude-filter" value="org.chromium.net.impl.CronetLoggerTest#testEngineCreation" />
+        <!-- b/327182569 -->
+        <option name="exclude-filter" value="org.chromium.net.urlconnection.CronetURLStreamHandlerFactoryTest#testSetUrlStreamFactoryUsesCronetForNative" />
         <option name="hidden-api-checks" value="false"/>
         <option name="isolated-storage" value="false"/>
         <option name="orchestrator" value="true"/>
diff --git a/Cronet/tests/mts/AndroidTest.xml b/Cronet/tests/mts/AndroidTest.xml
index 5aed655..a438e2e 100644
--- a/Cronet/tests/mts/AndroidTest.xml
+++ b/Cronet/tests/mts/AndroidTest.xml
@@ -43,6 +43,8 @@
         <option name="exclude-filter" value="org.chromium.net.NetworkChangesTest" />
         <!-- b/316550794 -->
         <option name="exclude-filter" value="org.chromium.net.impl.CronetLoggerTest#testEngineCreation" />
+        <!-- b/327182569 -->
+        <option name="exclude-filter" value="org.chromium.net.urlconnection.CronetURLStreamHandlerFactoryTest#testSetUrlStreamFactoryUsesCronetForNative" />
         <option name="hidden-api-checks" value="false"/>
         <option name="isolated-storage" value="false"/>
         <option name="orchestrator" value="true"/>
@@ -53,4 +55,4 @@
             class="com.android.tradefed.testtype.suite.module.MainlineTestModuleController">
         <option name="mainline-module-package-name" value="com.google.android.tethering" />
     </object>
-</configuration>
\ No newline at end of file
+</configuration>
diff --git a/OWNERS_core_networking b/OWNERS_core_networking
index 83f798a..6d8ed4a 100644
--- a/OWNERS_core_networking
+++ b/OWNERS_core_networking
@@ -1,10 +1,6 @@
-chiachangwang@google.com
-cken@google.com
 jchalard@google.com
 junyulai@google.com
-lifr@google.com
 lorenzo@google.com
-markchien@google.com
 martinwu@google.com
 maze@google.com
 motomuman@google.com
@@ -12,7 +8,5 @@
 prohr@google.com
 reminv@google.com
 satk@google.com
-waynema@google.com
 xiaom@google.com
-yumike@google.com
 yuyanghuang@google.com
diff --git a/Tethering/apex/Android.bp b/Tethering/apex/Android.bp
index 4bae221..304a6ed 100644
--- a/Tethering/apex/Android.bp
+++ b/Tethering/apex/Android.bp
@@ -113,6 +113,7 @@
     prebuilts: [
         "current_sdkinfo",
         "netbpfload.mainline.rc",
+        "netbpfload.35rc",
         "ot-daemon.init.34rc",
     ],
     manifest: "manifest.json",
diff --git a/bpf_progs/offload.c b/bpf_progs/offload.c
index 4f152bf..8e72747 100644
--- a/bpf_progs/offload.c
+++ b/bpf_progs/offload.c
@@ -876,5 +876,5 @@
 }
 
 LICENSE("Apache 2.0");
-CRITICAL("Connectivity (Tethering)");
+//CRITICAL("Connectivity (Tethering)");
 DISABLE_BTF_ON_USER_BUILDS();
diff --git a/framework-t/src/android/net/INetworkStatsService.aidl b/framework-t/src/android/net/INetworkStatsService.aidl
index c86f7fd..7f0c1fe 100644
--- a/framework-t/src/android/net/INetworkStatsService.aidl
+++ b/framework-t/src/android/net/INetworkStatsService.aidl
@@ -101,4 +101,7 @@
      * Note that invocation of any interface will be sent to all providers.
      */
      void setStatsProviderWarningAndLimitAsync(String iface, long warning, long limit);
+
+     /** Clear TrafficStats rate-limit caches. */
+     void clearTrafficStatsRateLimitCaches();
 }
diff --git a/framework-t/src/android/net/TrafficStats.java b/framework-t/src/android/net/TrafficStats.java
index a69b38d..77c8001 100644
--- a/framework-t/src/android/net/TrafficStats.java
+++ b/framework-t/src/android/net/TrafficStats.java
@@ -19,6 +19,7 @@
 import static android.annotation.SystemApi.Client.MODULE_LIBRARIES;
 
 import android.annotation.NonNull;
+import android.annotation.RequiresPermission;
 import android.annotation.SuppressLint;
 import android.annotation.SystemApi;
 import android.annotation.TestApi;
@@ -692,6 +693,27 @@
         return UNSUPPORTED;
     }
 
+    /** Clear TrafficStats rate-limit caches.
+     *
+     * This is mainly for {@link com.android.server.net.NetworkStatsService} to
+     * clear rate-limit cache to avoid caching for TrafficStats API results.
+     * Tests might get stale values after generating network traffic, which
+     * generally need to wait for cache expiry to get updated values.
+     *
+     * @hide
+     */
+    @RequiresPermission(anyOf = {
+            NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK,
+            android.Manifest.permission.NETWORK_STACK,
+            android.Manifest.permission.NETWORK_SETTINGS})
+    public static void clearRateLimitCaches() {
+        try {
+            getStatsService().clearTrafficStatsRateLimitCaches();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
     /**
      * Return the number of packets transmitted on the specified interface since the interface
      * was created. Statistics are measured at the network layer, so both TCP and
diff --git a/framework/Android.bp b/framework/Android.bp
index 8787167..deb1c5a 100644
--- a/framework/Android.bp
+++ b/framework/Android.bp
@@ -117,7 +117,6 @@
     static_libs: [
         "httpclient_api",
         "httpclient_impl",
-        "http_client_logging",
         // Framework-connectivity-pre-jarjar is identical to framework-connectivity
         // implementation, but without the jarjar rules. However, framework-connectivity
         // is not based on framework-connectivity-pre-jarjar, it's rebuilt from source
@@ -147,7 +146,6 @@
     ],
     impl_only_static_libs: [
         "httpclient_impl",
-        "http_client_logging",
     ],
 }
 
diff --git a/netbpfload/Android.bp b/netbpfload/Android.bp
index c39b46c..f278695 100644
--- a/netbpfload/Android.bp
+++ b/netbpfload/Android.bp
@@ -75,3 +75,10 @@
     filename: "netbpfload.33rc",
     installable: false,
 }
+
+prebuilt_etc {
+    name: "netbpfload.35rc",
+    src: "netbpfload.35rc",
+    filename: "netbpfload.35rc",
+    installable: false,
+}
diff --git a/netbpfload/NetBpfLoad.cpp b/netbpfload/NetBpfLoad.cpp
index 83bb98c..710782d 100644
--- a/netbpfload/NetBpfLoad.cpp
+++ b/netbpfload/NetBpfLoad.cpp
@@ -313,7 +313,7 @@
         return 1;
     }
 
-    if (false && isAtLeastV) {
+    if (isAtLeastV) {
         // Linux 5.16-rc1 changed the default to 2 (disabled but changeable),
         // but we need 0 (enabled)
         // (this writeFile is known to fail on at least 4.19, but always defaults to 0 on
@@ -380,7 +380,7 @@
         return 1;
     }
 
-    if (false && isAtLeastV) {
+    if (isAtLeastV) {
         ALOGI("done, transferring control to platform bpfloader.");
 
         const char * args[] = { platformBpfLoader, NULL, };
diff --git a/netbpfload/loader.cpp b/netbpfload/loader.cpp
index 9dd0d2a..52428a3 100644
--- a/netbpfload/loader.cpp
+++ b/netbpfload/loader.cpp
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-#define LOG_TAG "NetBpfLoader"
+#define LOG_TAG "NetBpfLoad"
 
 #include <errno.h>
 #include <fcntl.h>
@@ -769,7 +769,7 @@
               .max_entries = max_entries,
               .map_flags = md[i].map_flags,
             };
-            if (isAtLeastKernelVersion(4, 14, 0))
+            if (isAtLeastKernelVersion(4, 15, 0))
                 strlcpy(req.map_name, mapNames[i].c_str(), sizeof(req.map_name));
             fd.reset(bpf(BPF_MAP_CREATE, req));
             saved_errno = errno;
@@ -1012,7 +1012,7 @@
               .log_size = static_cast<__u32>(log_buf.size()),
               .expected_attach_type = cs[i].expected_attach_type,
             };
-            if (isAtLeastKernelVersion(4, 14, 0))
+            if (isAtLeastKernelVersion(4, 15, 0))
                 strlcpy(req.prog_name, cs[i].name.c_str(), sizeof(req.prog_name));
             fd.reset(bpf(BPF_PROG_LOAD, req));
 
diff --git a/netbpfload/netbpfload.35rc b/netbpfload/netbpfload.35rc
new file mode 100644
index 0000000..0fbcb5a
--- /dev/null
+++ b/netbpfload/netbpfload.35rc
@@ -0,0 +1,9 @@
+service bpfloader /apex/com.android.tethering/bin/netbpfload
+    capabilities CHOWN SYS_ADMIN NET_ADMIN
+    group root graphics network_stack net_admin net_bw_acct net_bw_stats net_raw system
+    user root
+    file /dev/kmsg w
+    rlimit memlock 1073741824 1073741824
+    oneshot
+    reboot_on_failure reboot,bpfloader-failed
+    override
diff --git a/netd/BpfHandler.cpp b/netd/BpfHandler.cpp
index 0d75c05..b535ebf 100644
--- a/netd/BpfHandler.cpp
+++ b/netd/BpfHandler.cpp
@@ -183,7 +183,8 @@
         // Make sure BPF programs are loaded before doing anything
         ALOGI("Waiting for BPF programs");
 
-        if (true || !modules::sdklevel::IsAtLeastV()) {
+        // TODO: use !modules::sdklevel::IsAtLeastV() once api finalized
+        if (android_get_device_api_level() < __ANDROID_API_V__) {
             waitForNetProgsLoaded();
             ALOGI("Networking BPF programs are loaded");
 
diff --git a/service-t/src/com/android/metrics/NetworkNsdReportedMetrics.java b/service-t/src/com/android/metrics/NetworkNsdReportedMetrics.java
index 42a922d..385adc6 100644
--- a/service-t/src/com/android/metrics/NetworkNsdReportedMetrics.java
+++ b/service-t/src/com/android/metrics/NetworkNsdReportedMetrics.java
@@ -247,12 +247,15 @@
      * @param isLegacy Whether this call is using legacy backend.
      * @param transactionId The transaction id of service resolution.
      * @param durationMs The duration before stop resolving the service.
+     * @param sentQueryCount The count of sent queries during resolving.
      */
-    public void reportServiceResolutionStop(boolean isLegacy, int transactionId, long durationMs) {
+    public void reportServiceResolutionStop(boolean isLegacy, int transactionId, long durationMs,
+            int sentQueryCount) {
         final Builder builder = makeReportedBuilder(isLegacy, transactionId);
         builder.setType(NsdEventType.NET_RESOLVE);
         builder.setQueryResult(MdnsQueryResult.MQR_SERVICE_RESOLUTION_STOP);
         builder.setEventDurationMillisec(durationMs);
+        builder.setSentQueryCount(sentQueryCount);
         mDependencies.statsWrite(builder.build());
     }
 
diff --git a/service-t/src/com/android/server/NsdService.java b/service-t/src/com/android/server/NsdService.java
index 8552eec..7c1ca30 100644
--- a/service-t/src/com/android/server/NsdService.java
+++ b/service-t/src/com/android/server/NsdService.java
@@ -1825,12 +1825,20 @@
      * <p>For now NsdService only allows single-label hostnames conforming to RFC 1035. In other
      * words, the hostname should be at most 63 characters long and it only contains letters, digits
      * and hyphens.
+     *
+     * <p>Additionally, this allows hostname starting with a digit to support Matter devices. Per
+     * Matter spec 4.3.1.1:
+     *
+     * <p>The target host name SHALL be constructed using one of the available link-layer addresses,
+     * such as a 48-bit device MAC address (for Ethernet and Wi‑Fi) or a 64-bit MAC Extended Address
+     * (for Thread) expressed as a fixed-length twelve-character (or sixteen-character) hexadecimal
+     * string, encoded as ASCII (UTF-8) text using capital letters, e.g., B75AFB458ECD.<domain>.
      */
     public static boolean checkHostname(@Nullable String hostname) {
         if (hostname == null) {
             return true;
         }
-        String HOSTNAME_REGEX = "^[a-zA-Z]([a-zA-Z0-9-_]{0,61}[a-zA-Z0-9])?$";
+        String HOSTNAME_REGEX = "^[a-zA-Z0-9]([a-zA-Z0-9-_]{0,61}[a-zA-Z0-9])?$";
         return Pattern.compile(HOSTNAME_REGEX).matcher(hostname).matches();
     }
 
@@ -2851,7 +2859,8 @@
                                 request.getSentQueryCount());
                     } else if (listener instanceof ResolutionListener) {
                         mMetrics.reportServiceResolutionStop(false /* isLegacy */, transactionId,
-                                request.calculateRequestDurationMs(mClock.elapsedRealtime()));
+                                request.calculateRequestDurationMs(mClock.elapsedRealtime()),
+                                request.getSentQueryCount());
                     } else if (listener instanceof ServiceInfoListener) {
                         mMetrics.reportServiceInfoCallbackUnregistered(transactionId,
                                 request.calculateRequestDurationMs(mClock.elapsedRealtime()),
@@ -2892,7 +2901,8 @@
                     case NsdManager.RESOLVE_SERVICE:
                         stopResolveService(transactionId);
                         mMetrics.reportServiceResolutionStop(true /* isLegacy */, transactionId,
-                                request.calculateRequestDurationMs(mClock.elapsedRealtime()));
+                                request.calculateRequestDurationMs(mClock.elapsedRealtime()),
+                                NO_SENT_QUERY_COUNT);
                         break;
                     case NsdManager.REGISTER_SERVICE:
                         unregisterService(transactionId);
@@ -3108,7 +3118,8 @@
             mMetrics.reportServiceResolutionStop(
                     isLegacyClientRequest(request),
                     request.mTransactionId,
-                    request.calculateRequestDurationMs(mClock.elapsedRealtime()));
+                    request.calculateRequestDurationMs(mClock.elapsedRealtime()),
+                    request.getSentQueryCount());
             try {
                 mCb.onStopResolutionSucceeded(listenerKey);
             } catch (RemoteException e) {
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 073e465..eb85110 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java
@@ -97,6 +97,8 @@
     @NonNull
     private final Looper mLooper;
     @NonNull
+    private final Dependencies mDeps;
+    @NonNull
     private final String[] mDeviceHostname;
     @NonNull
     private final MdnsFeatureFlags mMdnsFeatureFlags;
@@ -111,6 +113,7 @@
             @NonNull String[] deviceHostname, @NonNull MdnsFeatureFlags mdnsFeatureFlags) {
         mDeviceHostname = deviceHostname;
         mLooper = looper;
+        mDeps = deps;
         mMdnsFeatureFlags = mdnsFeatureFlags;
     }
 
@@ -127,6 +130,10 @@
         public Enumeration<InetAddress> getInterfaceInetAddresses(@NonNull NetworkInterface iface) {
             return iface.getInetAddresses();
         }
+
+        public long elapsedRealTime() {
+            return SystemClock.elapsedRealtime();
+        }
     }
 
     private static class RecordInfo<T extends MdnsRecord> {
@@ -140,17 +147,25 @@
         public final boolean isSharedName;
 
         /**
-         * Last time (as per SystemClock.elapsedRealtime) when advertised via multicast, 0 if never
+         * Last time (as per SystemClock.elapsedRealtime) when advertised via multicast on IPv4, 0
+         * if never
          */
-        public long lastAdvertisedTimeMs;
+        public long lastAdvertisedOnIpv4TimeMs;
 
         /**
-         * Last time (as per SystemClock.elapsedRealtime) when sent via unicast or multicast,
-         * 0 if never
+         * Last time (as per SystemClock.elapsedRealtime) when advertised via multicast on IPv6, 0
+         * if never
          */
-        // FIXME: the `lastSentTimeMs` and `lastAdvertisedTimeMs` should be maintained separately
-        // for IPv4 and IPv6, because neither IPv4 nor and IPv6 clients can receive replies in
-        // different address space.
+        public long lastAdvertisedOnIpv6TimeMs;
+
+        /**
+         * Last time (as per SystemClock.elapsedRealtime) when sent via unicast or multicast, 0 if
+         * never.
+         *
+         * <p>Different from lastAdvertisedOnIpv(4|6)TimeMs, lastSentTimeMs is mainly used for
+         * tracking is a record is ever sent out, no matter unicast/multicast or IPv4/IPv6. It's
+         * unnecessary to maintain two versions (IPv4/IPv6) for it.
+         */
         public long lastSentTimeMs;
 
         RecordInfo(NsdServiceInfo serviceInfo, T record, boolean sharedName) {
@@ -577,7 +592,8 @@
      */
     @Nullable
     public MdnsReplyInfo getReply(MdnsPacket packet, InetSocketAddress src) {
-        final long now = SystemClock.elapsedRealtime();
+        final long now = mDeps.elapsedRealTime();
+        final boolean isQuestionOnIpv4 = src.getAddress() instanceof Inet4Address;
 
         // TODO: b/322142420 - Set<RecordInfo<?>> may contain duplicate records wrapped in different
         // RecordInfo<?>s when custom host is enabled.
@@ -595,7 +611,7 @@
                     null /* serviceSrvRecord */, null /* serviceTxtRecord */,
                     null /* hostname */,
                     replyUnicastEnabled, now, answerInfo, additionalAnswerInfo,
-                    Collections.emptyList())) {
+                    Collections.emptyList(), isQuestionOnIpv4)) {
                 replyUnicast &= question.isUnicastReplyRequested();
             }
 
@@ -607,7 +623,7 @@
                         registration.srvRecord, registration.txtRecord,
                         registration.serviceInfo.getHostname(),
                         replyUnicastEnabled, now,
-                        answerInfo, additionalAnswerInfo, packet.answers)) {
+                        answerInfo, additionalAnswerInfo, packet.answers, isQuestionOnIpv4)) {
                     replyUnicast &= question.isUnicastReplyRequested();
                     registration.repliedServiceCount++;
                     registration.sentPacketCount++;
@@ -685,7 +701,7 @@
             // multicast responses. Unicast replies are faster as they do not need to wait for the
             // beacon interval on Wi-Fi.
             dest = src;
-        } else if (src.getAddress() instanceof Inet4Address) {
+        } else if (isQuestionOnIpv4) {
             dest = IPV4_SOCKET_ADDR;
         } else {
             dest = IPV6_SOCKET_ADDR;
@@ -697,7 +713,11 @@
             // TODO: consider actual packet send delay after response aggregation
             info.lastSentTimeMs = now + delayMs;
             if (!replyUnicast) {
-                info.lastAdvertisedTimeMs = info.lastSentTimeMs;
+                if (isQuestionOnIpv4) {
+                    info.lastAdvertisedOnIpv4TimeMs = info.lastSentTimeMs;
+                } else {
+                    info.lastAdvertisedOnIpv6TimeMs = info.lastSentTimeMs;
+                }
             }
             // Different RecordInfos may the contain the same record
             if (!answerRecords.contains(info.record)) {
@@ -729,7 +749,8 @@
             @Nullable String hostname,
             boolean replyUnicastEnabled, long now, @NonNull Set<RecordInfo<?>> answerInfo,
             @NonNull Set<RecordInfo<?>> additionalAnswerInfo,
-            @NonNull List<MdnsRecord> knownAnswerRecords) {
+            @NonNull List<MdnsRecord> knownAnswerRecords,
+            boolean isQuestionOnIpv4) {
         boolean hasDnsSdPtrRecordAnswer = false;
         boolean hasDnsSdSrvRecordAnswer = false;
         boolean hasFullyOwnedNameMatch = false;
@@ -778,10 +799,20 @@
 
             // TODO: responses to probe queries should bypass this check and only ensure the
             // reply is sent 250ms after the last sent time (RFC 6762 p.15)
-            if (!(replyUnicastEnabled && question.isUnicastReplyRequested())
-                    && info.lastAdvertisedTimeMs > 0L
-                    && now - info.lastAdvertisedTimeMs < MIN_MULTICAST_REPLY_INTERVAL_MS) {
-                continue;
+            if (!(replyUnicastEnabled && question.isUnicastReplyRequested())) {
+                if (isQuestionOnIpv4) { // IPv4
+                    if (info.lastAdvertisedOnIpv4TimeMs > 0L
+                            && now - info.lastAdvertisedOnIpv4TimeMs
+                                    < MIN_MULTICAST_REPLY_INTERVAL_MS) {
+                        continue;
+                    }
+                } else { // IPv6
+                    if (info.lastAdvertisedOnIpv6TimeMs > 0L
+                            && now - info.lastAdvertisedOnIpv6TimeMs
+                                    < MIN_MULTICAST_REPLY_INTERVAL_MS) {
+                        continue;
+                    }
+                }
             }
 
             answerInfo.add(info);
@@ -1302,10 +1333,11 @@
         final ServiceRegistration registration = mServices.get(serviceId);
         if (registration == null) return;
 
-        final long now = SystemClock.elapsedRealtime();
+        final long now = mDeps.elapsedRealTime();
         for (RecordInfo<?> record : registration.allRecords) {
             record.lastSentTimeMs = now;
-            record.lastAdvertisedTimeMs = now;
+            record.lastAdvertisedOnIpv4TimeMs = now;
+            record.lastAdvertisedOnIpv6TimeMs = now;
         }
         registration.sentPacketCount += sentPacketCount;
     }
diff --git a/service-t/src/com/android/server/ethernet/EthernetServiceImpl.java b/service-t/src/com/android/server/ethernet/EthernetServiceImpl.java
index b8689d6..92f1953 100644
--- a/service-t/src/com/android/server/ethernet/EthernetServiceImpl.java
+++ b/service-t/src/com/android/server/ethernet/EthernetServiceImpl.java
@@ -108,7 +108,11 @@
             PermissionUtils.enforceRestrictedNetworkPermission(mContext, TAG);
         }
 
-        return new IpConfiguration(mTracker.getIpConfiguration(iface));
+        // This causes thread-unsafe access on mIpConfigurations which might
+        // race with calls to EthernetManager#updateConfiguration().
+        // EthernetManager#getConfiguration() has been marked as
+        // @UnsupportedAppUsage since Android R.
+        return mTracker.getIpConfiguration(iface);
     }
 
     /**
diff --git a/service-t/src/com/android/server/ethernet/EthernetTracker.java b/service-t/src/com/android/server/ethernet/EthernetTracker.java
index 71f289e..a60592f 100644
--- a/service-t/src/com/android/server/ethernet/EthernetTracker.java
+++ b/service-t/src/com/android/server/ethernet/EthernetTracker.java
@@ -31,8 +31,6 @@
 import android.net.ITetheredInterfaceCallback;
 import android.net.InterfaceConfigurationParcel;
 import android.net.IpConfiguration;
-import android.net.IpConfiguration.IpAssignment;
-import android.net.IpConfiguration.ProxySettings;
 import android.net.LinkAddress;
 import android.net.NetworkCapabilities;
 import android.net.StaticIpConfiguration;
@@ -111,6 +109,7 @@
     /** Mapping between {iface name | mac address} -> {NetworkCapabilities} */
     private final ConcurrentHashMap<String, NetworkCapabilities> mNetworkCapabilities =
             new ConcurrentHashMap<>();
+    /** Mapping between {iface name | mac address} -> {IpConfiguration} */
     private final ConcurrentHashMap<String, IpConfiguration> mIpConfigurations =
             new ConcurrentHashMap<>();
 
@@ -298,7 +297,7 @@
     }
 
     private IpConfiguration getIpConfigurationForCallback(String iface, int state) {
-        return (state == EthernetManager.STATE_ABSENT) ? null : getOrCreateIpConfiguration(iface);
+        return (state == EthernetManager.STATE_ABSENT) ? null : getIpConfiguration(iface);
     }
 
     private void ensureRunningOnEthernetServiceThread() {
@@ -391,8 +390,83 @@
         mHandler.post(() -> setInterfaceAdministrativeState(iface, enabled, cb));
     }
 
-    IpConfiguration getIpConfiguration(String iface) {
-        return mIpConfigurations.get(iface);
+    private @Nullable String getHwAddress(String iface) {
+        if (getInterfaceRole(iface) == EthernetManager.ROLE_SERVER) {
+            return mTetheringInterfaceHwAddr;
+        }
+
+        return mFactory.getHwAddress(iface);
+    }
+
+    /**
+     * Get the IP configuration of the interface, or the default if the interface doesn't exist.
+     * @param iface the name of the interface to retrieve.
+     *
+     * @return The IP configuration
+     */
+    public IpConfiguration getIpConfiguration(String iface) {
+        return getIpConfiguration(iface, getHwAddress(iface));
+    }
+
+    private IpConfiguration getIpConfiguration(String iface, @Nullable String hwAddress) {
+        // Look up Ip configuration first by ifname, then by MAC address.
+        IpConfiguration ipConfig = mIpConfigurations.get(iface);
+        if (ipConfig != null) {
+            return ipConfig;
+        }
+
+        if (hwAddress == null) {
+            // should never happen.
+            Log.wtf(TAG, "No hardware address for interface " + iface);
+        } else {
+            ipConfig = mIpConfigurations.get(hwAddress);
+        }
+
+        if (ipConfig == null) {
+            ipConfig = new IpConfiguration.Builder().build();
+        }
+
+        return ipConfig;
+    }
+
+    private NetworkCapabilities getNetworkCapabilities(String iface) {
+        return getNetworkCapabilities(iface, getHwAddress(iface));
+    }
+
+    private NetworkCapabilities getNetworkCapabilities(String iface, @Nullable String hwAddress) {
+        // Look up network capabilities first by ifname, then by MAC address.
+        NetworkCapabilities networkCapabilities = mNetworkCapabilities.get(iface);
+        if (networkCapabilities != null) {
+            return networkCapabilities;
+        }
+
+        if (hwAddress == null) {
+            // should never happen.
+            Log.wtf(TAG, "No hardware address for interface " + iface);
+        } else {
+            networkCapabilities = mNetworkCapabilities.get(hwAddress);
+        }
+
+        if (networkCapabilities != null) {
+            return networkCapabilities;
+        }
+
+        final NetworkCapabilities.Builder builder = createNetworkCapabilities(
+                false /* clear default capabilities */, null, null)
+                .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
+                .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)
+                .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING)
+                .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED)
+                .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED)
+                .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED);
+
+        if (isValidTestInterface(iface)) {
+            builder.addTransportType(NetworkCapabilities.TRANSPORT_TEST);
+        } else {
+            builder.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
+        }
+
+        return builder.build();
     }
 
     @VisibleForTesting(visibility = PACKAGE)
@@ -433,8 +507,8 @@
      * NET_CAPABILITY_NOT_RESTRICTED) capability. Otherwise, returns false.
      */
     boolean isRestrictedInterface(String iface) {
-        final NetworkCapabilities nc = mNetworkCapabilities.get(iface);
-        return nc != null && !nc.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED);
+        final NetworkCapabilities nc = getNetworkCapabilities(iface);
+        return !nc.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED);
     }
 
     void addListener(IEthernetServiceListener listener, boolean canUseRestrictedNetworks) {
@@ -623,17 +697,9 @@
             return;
         }
 
-        NetworkCapabilities nc = mNetworkCapabilities.get(iface);
-        if (nc == null) {
-            // Try to resolve using mac address
-            nc = mNetworkCapabilities.get(hwAddress);
-            if (nc == null) {
-                final boolean isTestIface = iface.matches(TEST_IFACE_REGEXP);
-                nc = createDefaultNetworkCapabilities(isTestIface);
-            }
-        }
+        final NetworkCapabilities nc = getNetworkCapabilities(iface, hwAddress);
+        final IpConfiguration ipConfiguration = getIpConfiguration(iface, hwAddress);
 
-        IpConfiguration ipConfiguration = getOrCreateIpConfiguration(iface);
         Log.d(TAG, "Tracking interface in client mode: " + iface);
         mFactory.addInterface(iface, hwAddress, ipConfiguration, nc);
 
@@ -773,25 +839,6 @@
         return new EthernetTrackerConfig(configString.split(";", /* limit of tokens */ 4));
     }
 
-    private static NetworkCapabilities createDefaultNetworkCapabilities(boolean isTestIface) {
-        NetworkCapabilities.Builder builder = createNetworkCapabilities(
-                false /* clear default capabilities */, null, null)
-                .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
-                .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)
-                .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING)
-                .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED)
-                .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED)
-                .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED);
-
-        if (isTestIface) {
-            builder.addTransportType(NetworkCapabilities.TRANSPORT_TEST);
-        } else {
-            builder.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
-        }
-
-        return builder.build();
-    }
-
     /**
      * Parses a static list of network capabilities
      *
@@ -926,15 +973,6 @@
         return new IpConfiguration.Builder().setStaticIpConfiguration(staticIpConfig).build();
     }
 
-    private IpConfiguration getOrCreateIpConfiguration(String iface) {
-        IpConfiguration ret = mIpConfigurations.get(iface);
-        if (ret != null) return ret;
-        ret = new IpConfiguration();
-        ret.setIpAssignment(IpAssignment.DHCP);
-        ret.setProxySettings(ProxySettings.NONE);
-        return ret;
-    }
-
     private boolean isValidEthernetInterface(String iface) {
         return iface.matches(mIfaceMatch) || isValidTestInterface(iface);
     }
@@ -1021,7 +1059,7 @@
             pw.println("IP Configurations:");
             pw.increaseIndent();
             for (String iface : mIpConfigurations.keySet()) {
-                pw.println(iface + ": " + mIpConfigurations.get(iface));
+                pw.println(iface + ": " + getIpConfiguration(iface));
             }
             pw.decreaseIndent();
             pw.println();
@@ -1029,7 +1067,7 @@
             pw.println("Network Capabilities:");
             pw.increaseIndent();
             for (String iface : mNetworkCapabilities.keySet()) {
-                pw.println(iface + ": " + mNetworkCapabilities.get(iface));
+                pw.println(iface + ": " + getNetworkCapabilities(iface));
             }
             pw.decreaseIndent();
             pw.println();
diff --git a/service-t/src/com/android/server/net/NetworkStatsService.java b/service-t/src/com/android/server/net/NetworkStatsService.java
index 5e98ee1..8305c1e 100644
--- a/service-t/src/com/android/server/net/NetworkStatsService.java
+++ b/service-t/src/com/android/server/net/NetworkStatsService.java
@@ -16,6 +16,7 @@
 
 package com.android.server.net;
 
+import static android.Manifest.permission.NETWORK_SETTINGS;
 import static android.Manifest.permission.NETWORK_STATS_PROVIDER;
 import static android.Manifest.permission.READ_NETWORK_USAGE_HISTORY;
 import static android.Manifest.permission.UPDATE_DEVICE_STATS;
@@ -50,12 +51,17 @@
 import static android.net.NetworkTemplate.MATCH_WIFI;
 import static android.net.TrafficStats.KB_IN_BYTES;
 import static android.net.TrafficStats.MB_IN_BYTES;
+import static android.net.TrafficStats.TYPE_RX_BYTES;
+import static android.net.TrafficStats.TYPE_RX_PACKETS;
+import static android.net.TrafficStats.TYPE_TX_BYTES;
+import static android.net.TrafficStats.TYPE_TX_PACKETS;
 import static android.net.TrafficStats.UID_TETHERING;
 import static android.net.TrafficStats.UNSUPPORTED;
 import static android.net.netstats.NetworkStatsDataMigrationUtils.PREFIX_UID;
 import static android.net.netstats.NetworkStatsDataMigrationUtils.PREFIX_UID_TAG;
 import static android.net.netstats.NetworkStatsDataMigrationUtils.PREFIX_XT;
 import static android.os.Trace.TRACE_TAG_NETWORK;
+import static android.provider.DeviceConfig.NAMESPACE_TETHERING;
 import static android.system.OsConstants.ENOENT;
 import static android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID;
 import static android.text.format.DateUtils.DAY_IN_MILLIS;
@@ -64,6 +70,7 @@
 import static android.text.format.DateUtils.SECOND_IN_MILLIS;
 
 import static com.android.internal.annotations.VisibleForTesting.Visibility.PRIVATE;
+import static com.android.net.module.util.DeviceConfigUtils.getDeviceConfigPropertyInt;
 import static com.android.net.module.util.NetworkCapabilitiesUtils.getDisplayTransport;
 import static com.android.net.module.util.NetworkStatsUtils.LIMIT_GLOBAL_ALERT;
 import static com.android.server.net.NetworkStatsEventLogger.POLL_REASON_PERIODIC;
@@ -299,6 +306,12 @@
     static final String NETSTATS_FASTDATAINPUT_SUCCESSES_COUNTER_NAME = "fastdatainput.successes";
     static final String NETSTATS_FASTDATAINPUT_FALLBACKS_COUNTER_NAME = "fastdatainput.fallbacks";
 
+    static final String TRAFFIC_STATS_CACHE_EXPIRY_DURATION_NAME =
+            "trafficstats_cache_expiry_duration_ms";
+    static final String TRAFFIC_STATS_CACHE_MAX_ENTRIES_NAME = "trafficstats_cache_max_entries";
+    static final int DEFAULT_TRAFFIC_STATS_CACHE_EXPIRY_DURATION_MS = 1000;
+    static final int DEFAULT_TRAFFIC_STATS_CACHE_MAX_ENTRIES = 400;
+
     private final Context mContext;
     private final NetworkStatsFactory mStatsFactory;
     private final AlarmManager mAlarmManager;
@@ -454,6 +467,13 @@
 
     private long mLastStatsSessionPoll;
 
+    private final TrafficStatsRateLimitCache mTrafficStatsTotalCache;
+    private final TrafficStatsRateLimitCache mTrafficStatsIfaceCache;
+    private final TrafficStatsRateLimitCache mTrafficStatsUidCache;
+    static final String TRAFFICSTATS_RATE_LIMIT_CACHE_ENABLED_FLAG =
+            "trafficstats_rate_limit_cache_enabled_flag";
+    private final boolean mSupportTrafficStatsRateLimitCache;
+
     private final Object mOpenSessionCallsLock = new Object();
 
     /**
@@ -643,6 +663,16 @@
             mEventLogger = null;
         }
 
+        final long cacheExpiryDurationMs = mDeps.getTrafficStatsRateLimitCacheExpiryDuration();
+        final int cacheMaxEntries = mDeps.getTrafficStatsRateLimitCacheMaxEntries();
+        mSupportTrafficStatsRateLimitCache = mDeps.supportTrafficStatsRateLimitCache(mContext);
+        mTrafficStatsTotalCache = new TrafficStatsRateLimitCache(mClock,
+                cacheExpiryDurationMs, cacheMaxEntries);
+        mTrafficStatsIfaceCache = new TrafficStatsRateLimitCache(mClock,
+                cacheExpiryDurationMs, cacheMaxEntries);
+        mTrafficStatsUidCache = new TrafficStatsRateLimitCache(mClock,
+                cacheExpiryDurationMs, cacheMaxEntries);
+
         // TODO: Remove bpfNetMaps creation and always start SkDestroyListener
         // Following code is for the experiment to verify the SkDestroyListener refactoring. Based
         // on the experiment flag, BpfNetMaps starts C SkDestroyListener (existing code) or
@@ -696,7 +726,7 @@
          * Get the count of import legacy target attempts.
          */
         public int getImportLegacyTargetAttempts() {
-            return DeviceConfigUtils.getDeviceConfigPropertyInt(
+            return getDeviceConfigPropertyInt(
                     DeviceConfig.NAMESPACE_TETHERING,
                     NETSTATS_IMPORT_LEGACY_TARGET_ATTEMPTS,
                     DEFAULT_NETSTATS_IMPORT_LEGACY_TARGET_ATTEMPTS);
@@ -706,7 +736,7 @@
          * Get the count of using FastDataInput target attempts.
          */
         public int getUseFastDataInputTargetAttempts() {
-            return DeviceConfigUtils.getDeviceConfigPropertyInt(
+            return getDeviceConfigPropertyInt(
                     DeviceConfig.NAMESPACE_TETHERING,
                     NETSTATS_FASTDATAINPUT_TARGET_ATTEMPTS, 0);
         }
@@ -888,6 +918,75 @@
             return DeviceConfigUtils.isTetheringFeatureNotChickenedOut(
                     ctx, CONFIG_ENABLE_NETWORK_STATS_EVENT_LOGGER);
         }
+
+        /**
+         * Get whether TrafficStats rate-limit cache is supported.
+         *
+         * 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 supportTrafficStatsRateLimitCache(@NonNull Context ctx) {
+            return false;
+        }
+
+        /**
+         * Get TrafficStats rate-limit cache expiry.
+         *
+         * This method should only be called once in the constructor,
+         * to ensure that the code does not need to deal with flag values changing at runtime.
+         */
+        public int getTrafficStatsRateLimitCacheExpiryDuration() {
+            return getDeviceConfigPropertyInt(
+                    NAMESPACE_TETHERING, TRAFFIC_STATS_CACHE_EXPIRY_DURATION_NAME,
+                    DEFAULT_TRAFFIC_STATS_CACHE_EXPIRY_DURATION_MS);
+        }
+
+        /**
+         * Get TrafficStats rate-limit cache max entries.
+         *
+         * This method should only be called once in the constructor,
+         * to ensure that the code does not need to deal with flag values changing at runtime.
+         */
+        public int getTrafficStatsRateLimitCacheMaxEntries() {
+            return getDeviceConfigPropertyInt(
+                    NAMESPACE_TETHERING, TRAFFIC_STATS_CACHE_MAX_ENTRIES_NAME,
+                    DEFAULT_TRAFFIC_STATS_CACHE_MAX_ENTRIES);
+        }
+
+        /**
+         * Retrieves native network total statistics.
+         *
+         * @return A NetworkStats.Entry containing the native statistics, or
+         *         null if an error occurs.
+         */
+        @Nullable
+        public NetworkStats.Entry nativeGetTotalStat() {
+            return NetworkStatsService.nativeGetTotalStat();
+        }
+
+        /**
+         * Retrieves native network interface statistics for the specified interface.
+         *
+         * @param iface The name of the network interface to query.
+         * @return A NetworkStats.Entry containing the native statistics for the interface, or
+         *         null if an error occurs.
+         */
+        @Nullable
+        public NetworkStats.Entry nativeGetIfaceStat(String iface) {
+            return NetworkStatsService.nativeGetIfaceStat(iface);
+        }
+
+        /**
+         * Retrieves native network uid statistics for the specified uid.
+         *
+         * @param uid The uid of the application to query.
+         * @return A NetworkStats.Entry containing the native statistics for the uid, or
+         *         null if an error occurs.
+         */
+        @Nullable
+        public NetworkStats.Entry nativeGetUidStat(int uid) {
+            return NetworkStatsService.nativeGetUidStat(uid);
+        }
     }
 
     /**
@@ -1983,53 +2082,106 @@
         if (callingUid != android.os.Process.SYSTEM_UID && callingUid != uid) {
             return UNSUPPORTED;
         }
-        return getEntryValueForType(nativeGetUidStat(uid), type);
+        if (!isEntryValueTypeValid(type)) return UNSUPPORTED;
+
+        if (!mSupportTrafficStatsRateLimitCache) {
+            return getEntryValueForType(mDeps.nativeGetUidStat(uid), type);
+        }
+
+        final NetworkStats.Entry entry = mTrafficStatsUidCache.getOrCompute(IFACE_ALL, uid,
+                () -> mDeps.nativeGetUidStat(uid));
+
+        return getEntryValueForType(entry, type);
+    }
+
+    @Nullable
+    private NetworkStats.Entry getIfaceStatsInternal(@NonNull String iface) {
+        final NetworkStats.Entry entry = mDeps.nativeGetIfaceStat(iface);
+        if (entry == null) {
+            return null;
+        }
+        // When tethering offload is in use, nativeIfaceStats does not contain usage from
+        // offload, add it back here. Note that the included statistics might be stale
+        // since polling newest stats from hardware might impact system health and not
+        // suitable for TrafficStats API use cases.
+        entry.add(getProviderIfaceStats(iface));
+        return entry;
     }
 
     @Override
     public long getIfaceStats(@NonNull String iface, int type) {
         Objects.requireNonNull(iface);
-        final NetworkStats.Entry entry = nativeGetIfaceStat(iface);
-        final long value = getEntryValueForType(entry, type);
-        if (value == UNSUPPORTED) {
-            return UNSUPPORTED;
-        } else {
-            // When tethering offload is in use, nativeIfaceStats does not contain usage from
-            // offload, add it back here. Note that the included statistics might be stale
-            // since polling newest stats from hardware might impact system health and not
-            // suitable for TrafficStats API use cases.
-            entry.add(getProviderIfaceStats(iface));
-            return getEntryValueForType(entry, type);
+        if (!isEntryValueTypeValid(type)) return UNSUPPORTED;
+
+        if (!mSupportTrafficStatsRateLimitCache) {
+            return getEntryValueForType(getIfaceStatsInternal(iface), type);
         }
+
+        final NetworkStats.Entry entry = mTrafficStatsIfaceCache.getOrCompute(iface, UID_ALL,
+                () -> getIfaceStatsInternal(iface));
+
+        return getEntryValueForType(entry, type);
     }
 
     private long getEntryValueForType(@Nullable NetworkStats.Entry entry, int type) {
         if (entry == null) return UNSUPPORTED;
+        if (!isEntryValueTypeValid(type)) return UNSUPPORTED;
         switch (type) {
-            case TrafficStats.TYPE_RX_BYTES:
+            case TYPE_RX_BYTES:
                 return entry.rxBytes;
-            case TrafficStats.TYPE_TX_BYTES:
-                return entry.txBytes;
-            case TrafficStats.TYPE_RX_PACKETS:
+            case TYPE_RX_PACKETS:
                 return entry.rxPackets;
-            case TrafficStats.TYPE_TX_PACKETS:
+            case TYPE_TX_BYTES:
+                return entry.txBytes;
+            case TYPE_TX_PACKETS:
                 return entry.txPackets;
             default:
-                return UNSUPPORTED;
+                throw new IllegalStateException("Bug: Invalid type: "
+                        + type + " should not reach here.");
         }
     }
 
+    private boolean isEntryValueTypeValid(int type) {
+        switch (type) {
+            case TYPE_RX_BYTES:
+            case TYPE_RX_PACKETS:
+            case TYPE_TX_BYTES:
+            case TYPE_TX_PACKETS:
+                return true;
+            default :
+                return false;
+        }
+    }
+
+    @Nullable
+    private NetworkStats.Entry getTotalStatsInternal() {
+        final NetworkStats.Entry entry = mDeps.nativeGetTotalStat();
+        if (entry == null) {
+            return null;
+        }
+        entry.add(getProviderIfaceStats(IFACE_ALL));
+        return entry;
+    }
+
     @Override
     public long getTotalStats(int type) {
-        final NetworkStats.Entry entry = nativeGetTotalStat();
-        final long value = getEntryValueForType(entry, type);
-        if (value == UNSUPPORTED) {
-            return UNSUPPORTED;
-        } else {
-            // Refer to comment in getIfaceStats
-            entry.add(getProviderIfaceStats(IFACE_ALL));
-            return getEntryValueForType(entry, type);
+        if (!isEntryValueTypeValid(type)) return UNSUPPORTED;
+        if (!mSupportTrafficStatsRateLimitCache) {
+            return getEntryValueForType(getTotalStatsInternal(), type);
         }
+
+        final NetworkStats.Entry entry = mTrafficStatsTotalCache.getOrCompute(IFACE_ALL, UID_ALL,
+                () -> getTotalStatsInternal());
+
+        return getEntryValueForType(entry, type);
+    }
+
+    @Override
+    public void clearTrafficStatsRateLimitCaches() {
+        PermissionUtils.enforceNetworkStackPermissionOr(mContext, NETWORK_SETTINGS);
+        mTrafficStatsUidCache.clear();
+        mTrafficStatsIfaceCache.clear();
+        mTrafficStatsTotalCache.clear();
     }
 
     private NetworkStats.Entry getProviderIfaceStats(@Nullable String iface) {
@@ -2785,6 +2937,14 @@
             } catch (IOException e) {
                 pw.println("(failed to dump FastDataInput counters)");
             }
+            pw.print("trafficstats.cache.supported", mSupportTrafficStatsRateLimitCache);
+            pw.println();
+            pw.print(TRAFFIC_STATS_CACHE_EXPIRY_DURATION_NAME,
+                    mDeps.getTrafficStatsRateLimitCacheExpiryDuration());
+            pw.println();
+            pw.print(TRAFFIC_STATS_CACHE_MAX_ENTRIES_NAME,
+                    mDeps.getTrafficStatsRateLimitCacheMaxEntries());
+            pw.println();
 
             pw.decreaseIndent();
 
diff --git a/service/jni/com_android_server_connectivity_ClatCoordinator.cpp b/service/jni/com_android_server_connectivity_ClatCoordinator.cpp
index 4214bc9..c07d050 100644
--- a/service/jni/com_android_server_connectivity_ClatCoordinator.cpp
+++ b/service/jni/com_android_server_connectivity_ClatCoordinator.cpp
@@ -114,7 +114,8 @@
 
     V("/sys/fs/bpf", S_IFDIR|S_ISVTX|0777, ROOT, ROOT, "fs_bpf", DIR);
 
-    if (false && modules::sdklevel::IsAtLeastV()) {
+    // TODO: use modules::sdklevel::IsAtLeastV() once api finalized
+    if (android_get_device_api_level() >= __ANDROID_API_V__) {
         V("/sys/fs/bpf/net_shared", S_IFDIR|01777, ROOT, ROOT, "fs_bpf_net_shared", DIR);
     } else {
         V("/sys/fs/bpf/net_shared", S_IFDIR|01777, SYSTEM, SYSTEM, "fs_bpf_net_shared", DIR);
diff --git a/service/src/com/android/server/BpfNetMaps.java b/service/src/com/android/server/BpfNetMaps.java
index 42c1628..fc6d8c4 100644
--- a/service/src/com/android/server/BpfNetMaps.java
+++ b/service/src/com/android/server/BpfNetMaps.java
@@ -918,25 +918,6 @@
         }
     }
 
-    /**
-     * Return whether the network is blocked by firewall chains for the given uid.
-     *
-     * Note that {@link #getDataSaverEnabled()} has a latency before V.
-     *
-     * @param uid The target uid.
-     * @param isNetworkMetered Whether the target network is metered.
-     *
-     * @return True if the network is blocked. Otherwise, false.
-     * @throws ServiceSpecificException if the read fails.
-     *
-     * @hide
-     */
-    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
-    public boolean isUidNetworkingBlocked(final int uid, boolean isNetworkMetered) {
-        return BpfNetMapsUtils.isUidNetworkingBlocked(uid, isNetworkMetered,
-                sConfigurationMap, sUidOwnerMap, sDataSaverEnabledMap);
-    }
-
     /** Register callback for statsd to pull atom. */
     @RequiresApi(Build.VERSION_CODES.TIRAMISU)
     public void setPullAtomCallback(final Context context) {
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index b1ae019..a15a2bf 100755
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -2238,11 +2238,7 @@
         final long ident = Binder.clearCallingIdentity();
         try {
             final boolean metered = nc == null ? true : nc.isMetered();
-            if (mDeps.isAtLeastV()) {
-                return mBpfNetMaps.isUidNetworkingBlocked(uid, metered);
-            } else {
-                return mPolicyManager.isUidNetworkingBlocked(uid, metered);
-            }
+            return mPolicyManager.isUidNetworkingBlocked(uid, metered);
         } finally {
             Binder.restoreCallingIdentity(ident);
         }
diff --git a/staticlibs/device/com/android/net/module/util/netlink/NetlinkConstants.java b/staticlibs/device/com/android/net/module/util/netlink/NetlinkConstants.java
index ad7a4d7..1896de6 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/NetlinkConstants.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/NetlinkConstants.java
@@ -123,6 +123,7 @@
     public static final short RTM_NEWRULE                   = 32;
     public static final short RTM_DELRULE                   = 33;
     public static final short RTM_GETRULE                   = 34;
+    public static final short RTM_NEWPREFIX                 = 52;
     public static final short RTM_NEWNDUSEROPT              = 68;
 
     // Netfilter netlink message types are presented by two bytes: high byte subsystem and
@@ -148,6 +149,8 @@
     public static final int RTMGRP_IPV4_IFADDR = 0x10;
     public static final int RTMGRP_IPV6_IFADDR = 0x100;
     public static final int RTMGRP_IPV6_ROUTE  = 0x400;
+    public static final int RTNLGRP_IPV6_PREFIX = 18;
+    public static final int RTMGRP_IPV6_PREFIX = 1 << (RTNLGRP_IPV6_PREFIX - 1);
     public static final int RTNLGRP_ND_USEROPT = 20;
     public static final int RTMGRP_ND_USEROPT = 1 << (RTNLGRP_ND_USEROPT - 1);
 
@@ -207,6 +210,7 @@
             case RTM_NEWRULE: return "RTM_NEWRULE";
             case RTM_DELRULE: return "RTM_DELRULE";
             case RTM_GETRULE: return "RTM_GETRULE";
+            case RTM_NEWPREFIX: return "RTM_NEWPREFIX";
             case RTM_NEWNDUSEROPT: return "RTM_NEWNDUSEROPT";
             default: return "unknown RTM type: " + String.valueOf(nlmType);
         }
diff --git a/staticlibs/device/com/android/net/module/util/netlink/StructPrefixCacheInfo.java b/staticlibs/device/com/android/net/module/util/netlink/StructPrefixCacheInfo.java
new file mode 100644
index 0000000..cfaa6e1
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/netlink/StructPrefixCacheInfo.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util.netlink;
+
+import androidx.annotation.NonNull;
+
+import com.android.net.module.util.Struct;
+import com.android.net.module.util.Struct.Field;
+import com.android.net.module.util.Struct.Type;
+
+import java.nio.ByteBuffer;
+
+/**
+ * struct prefix_cacheinfo {
+ *     __u32 preferred_time;
+ *     __u32 valid_time;
+ * }
+ *
+ * see also:
+ *
+ *     include/uapi/linux/if_addr.h
+ *
+ * @hide
+ */
+public class StructPrefixCacheInfo extends Struct {
+    public static final int STRUCT_SIZE = 8;
+
+    @Field(order = 0, type = Type.U32)
+    public final long preferred_time;
+    @Field(order = 1, type = Type.U32)
+    public final long valid_time;
+
+    StructPrefixCacheInfo(long preferred, long valid) {
+        this.preferred_time = preferred;
+        this.valid_time = valid;
+    }
+
+    /**
+     * Parse a prefix_cacheinfo struct from a {@link ByteBuffer}.
+     *
+     * @param byteBuffer The buffer from which to parse the prefix_cacheinfo.
+     * @return the parsed prefix_cacheinfo struct, or throw IllegalArgumentException if the
+     *         prefix_cacheinfo struct could not be parsed successfully(for example, if it was
+     *         truncated).
+     */
+    public static StructPrefixCacheInfo parse(@NonNull final ByteBuffer byteBuffer) {
+        if (byteBuffer.remaining() < STRUCT_SIZE) {
+            throw new IllegalArgumentException("Invalid bytebuffer remaining size "
+                    + byteBuffer.remaining() + " for prefix_cacheinfo attribute");
+        }
+
+        // The ByteOrder must already have been set to native order.
+        return Struct.parse(StructPrefixCacheInfo.class, byteBuffer);
+    }
+
+    /**
+     * Write a prefix_cacheinfo struct to {@link ByteBuffer}.
+     */
+    public void pack(@NonNull final ByteBuffer byteBuffer) {
+        // The ByteOrder must already have been set to native order.
+        writeToByteBuffer(byteBuffer);
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/netlink/StructPrefixMsg.java b/staticlibs/device/com/android/net/module/util/netlink/StructPrefixMsg.java
new file mode 100644
index 0000000..504d6c7
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/netlink/StructPrefixMsg.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util.netlink;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
+
+import com.android.net.module.util.Struct;
+import com.android.net.module.util.Struct.Field;
+import com.android.net.module.util.Struct.Type;
+
+import java.nio.ByteBuffer;
+
+/**
+ * struct prefixmsg {
+ *     unsigned char  prefix_family;
+ *     unsigned char  prefix_pad1;
+ *     unsigned short prefix_pad2;
+ *     int            prefix_ifindex;
+ *     unsigned char  prefix_type;
+ *     unsigned char  prefix_len;
+ *     unsigned char  prefix_flags;
+ *     unsigned char  prefix_pad3;
+ * }
+ *
+ * see also:
+ *
+ *     include/uapi/linux/rtnetlink.h
+ *
+ * @hide
+ */
+public class StructPrefixMsg extends Struct {
+    // Already aligned.
+    public static final int STRUCT_SIZE = 12;
+
+    @Field(order = 0, type = Type.U8, padding = 3)
+    public final short prefix_family;
+    @Field(order = 1, type = Type.S32)
+    public final int prefix_ifindex;
+    @Field(order = 2, type = Type.U8)
+    public final short prefix_type;
+    @Field(order = 3, type = Type.U8)
+    public final short prefix_len;
+    @Field(order = 4, type = Type.U8, padding = 1)
+    public final short prefix_flags;
+
+    @VisibleForTesting
+    public StructPrefixMsg(short family, int ifindex, short type, short len, short flags) {
+        this.prefix_family = family;
+        this.prefix_ifindex = ifindex;
+        this.prefix_type = type;
+        this.prefix_len = len;
+        this.prefix_flags = flags;
+    }
+
+    /**
+     * Parse a prefixmsg struct from a {@link ByteBuffer}.
+     *
+     * @param byteBuffer The buffer from which to parse the prefixmsg.
+     * @return the parsed prefixmsg struct, or throw IllegalArgumentException if the prefixmsg
+     *         struct could not be parsed successfully (for example, if it was truncated).
+     */
+    public static StructPrefixMsg parse(@NonNull final ByteBuffer byteBuffer) {
+        if (byteBuffer.remaining() < STRUCT_SIZE) {
+            throw new IllegalArgumentException("Invalid bytebuffer remaining size "
+                    + byteBuffer.remaining() + "for prefix_msg struct.");
+        }
+
+        // The ByteOrder must already have been set to native order.
+        return Struct.parse(StructPrefixMsg.class, byteBuffer);
+    }
+
+    /**
+     * Write a prefixmsg struct to {@link ByteBuffer}.
+     */
+    public void pack(@NonNull final ByteBuffer byteBuffer) {
+        // The ByteOrder must already have been set to native order.
+        writeToByteBuffer(byteBuffer);
+    }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/NetlinkConstantsTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/NetlinkConstantsTest.java
index 143e4d4..e42c552 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/NetlinkConstantsTest.java
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/NetlinkConstantsTest.java
@@ -46,6 +46,7 @@
 import static com.android.net.module.util.netlink.NetlinkConstants.RTM_NEWADDR;
 import static com.android.net.module.util.netlink.NetlinkConstants.RTM_NEWLINK;
 import static com.android.net.module.util.netlink.NetlinkConstants.RTM_NEWNDUSEROPT;
+import static com.android.net.module.util.netlink.NetlinkConstants.RTM_NEWPREFIX;
 import static com.android.net.module.util.netlink.NetlinkConstants.RTM_NEWNEIGH;
 import static com.android.net.module.util.netlink.NetlinkConstants.RTM_NEWROUTE;
 import static com.android.net.module.util.netlink.NetlinkConstants.RTM_NEWRULE;
@@ -89,6 +90,7 @@
         assertEquals("RTM_NEWRULE", stringForNlMsgType(RTM_NEWRULE, NETLINK_ROUTE));
         assertEquals("RTM_DELRULE", stringForNlMsgType(RTM_DELRULE, NETLINK_ROUTE));
         assertEquals("RTM_GETRULE", stringForNlMsgType(RTM_GETRULE, NETLINK_ROUTE));
+        assertEquals("RTM_NEWPREFIX", stringForNlMsgType(RTM_NEWPREFIX, NETLINK_ROUTE));
         assertEquals("RTM_NEWNDUSEROPT", stringForNlMsgType(RTM_NEWNDUSEROPT, NETLINK_ROUTE));
 
         assertEquals("SOCK_DIAG_BY_FAMILY",
diff --git a/staticlibs/testutils/Android.bp b/staticlibs/testutils/Android.bp
index 9124ac0..3843b90 100644
--- a/staticlibs/testutils/Android.bp
+++ b/staticlibs/testutils/Android.bp
@@ -99,6 +99,8 @@
         "mcts-networking",
         "mts-tethering",
         "mcts-tethering",
+        "mcts-wifi",
+        "mcts-dnsresolver",
     ],
     data: [":ConnectivityTestPreparer"],
 }
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/AutoReleaseNetworkCallbackRule.kt b/staticlibs/testutils/devicetests/com/android/testutils/AutoReleaseNetworkCallbackRule.kt
index 28ae609..93422ad 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/AutoReleaseNetworkCallbackRule.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/AutoReleaseNetworkCallbackRule.kt
@@ -21,6 +21,7 @@
 import android.net.Network
 import android.net.NetworkCapabilities
 import android.net.NetworkRequest
+import android.os.Handler
 import androidx.test.platform.app.InstrumentationRegistry
 import com.android.testutils.RecorderCallback.CallbackEntry
 import java.util.Collections
@@ -97,6 +98,15 @@
         cellRequestCb = null
     }
 
+    private fun addCallback(
+        cb: TestableNetworkCallback,
+        registrar: (TestableNetworkCallback) -> Unit
+    ): TestableNetworkCallback {
+        registrar(cb)
+        cbToCleanup.add(cb)
+        return cb
+    }
+
     /**
      * File a request for a Network.
      *
@@ -109,14 +119,27 @@
     @JvmOverloads
     fun requestNetwork(
         request: NetworkRequest,
-        cb: TestableNetworkCallback = TestableNetworkCallback()
-    ): TestableNetworkCallback {
-        cm.requestNetwork(request, cb)
-        cbToCleanup.add(cb)
-        return cb
+        cb: TestableNetworkCallback = TestableNetworkCallback(),
+        handler: Handler? = null
+    ) = addCallback(cb) {
+        if (handler == null) {
+            cm.requestNetwork(request, it)
+        } else {
+            cm.requestNetwork(request, it, handler)
+        }
     }
 
     /**
+     * Overload of [requestNetwork] that allows specifying a timeout.
+     */
+    @JvmOverloads
+    fun requestNetwork(
+        request: NetworkRequest,
+        cb: TestableNetworkCallback = TestableNetworkCallback(),
+        timeoutMs: Int,
+    ) = addCallback(cb) { cm.requestNetwork(request, it, timeoutMs) }
+
+    /**
      * File a callback for a NetworkRequest.
      *
      * This will fail tests (throw) if the cell network cannot be obtained, or if it was already
@@ -129,13 +152,63 @@
     fun registerNetworkCallback(
         request: NetworkRequest,
         cb: TestableNetworkCallback = TestableNetworkCallback()
-    ): TestableNetworkCallback {
-        cm.registerNetworkCallback(request, cb)
-        cbToCleanup.add(cb)
-        return cb
+    ) = addCallback(cb) { cm.registerNetworkCallback(request, it) }
+
+    /**
+     * @see ConnectivityManager.registerDefaultNetworkCallback
+     */
+    @JvmOverloads
+    fun registerDefaultNetworkCallback(
+        cb: TestableNetworkCallback = TestableNetworkCallback(),
+        handler: Handler? = null
+    ) = addCallback(cb) {
+        if (handler == null) {
+            cm.registerDefaultNetworkCallback(it)
+        } else {
+            cm.registerDefaultNetworkCallback(it, handler)
+        }
     }
 
     /**
+     * @see ConnectivityManager.registerSystemDefaultNetworkCallback
+     */
+    @JvmOverloads
+    fun registerSystemDefaultNetworkCallback(
+        cb: TestableNetworkCallback = TestableNetworkCallback(),
+        handler: Handler
+    ) = addCallback(cb) { cm.registerSystemDefaultNetworkCallback(it, handler) }
+
+    /**
+     * @see ConnectivityManager.registerDefaultNetworkCallbackForUid
+     */
+    @JvmOverloads
+    fun registerDefaultNetworkCallbackForUid(
+        uid: Int,
+        cb: TestableNetworkCallback = TestableNetworkCallback(),
+        handler: Handler
+    ) = addCallback(cb) { cm.registerDefaultNetworkCallbackForUid(uid, it, handler) }
+
+    /**
+     * @see ConnectivityManager.registerBestMatchingNetworkCallback
+     */
+    @JvmOverloads
+    fun registerBestMatchingNetworkCallback(
+        request: NetworkRequest,
+        cb: TestableNetworkCallback = TestableNetworkCallback(),
+        handler: Handler
+    ) = addCallback(cb) { cm.registerBestMatchingNetworkCallback(request, it, handler) }
+
+    /**
+     * @see ConnectivityManager.requestBackgroundNetwork
+     */
+    @JvmOverloads
+    fun requestBackgroundNetwork(
+        request: NetworkRequest,
+        cb: TestableNetworkCallback = TestableNetworkCallback(),
+        handler: Handler
+    ) = addCallback(cb) { cm.requestBackgroundNetwork(request, it, handler) }
+
+    /**
      * Unregister a callback filed using registration methods in this class.
      */
     fun unregisterNetworkCallback(cb: NetworkCallback) {
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/ConnectUtil.kt b/staticlibs/testutils/devicetests/com/android/testutils/ConnectUtil.kt
index 8090d5b..3857810 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/ConnectUtil.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/ConnectUtil.kt
@@ -86,6 +86,7 @@
         val callback = TestableNetworkCallback(timeoutMs = WIFI_CONNECT_TIMEOUT_MS)
         cm.registerNetworkCallback(NetworkRequest.Builder()
                 .addTransportType(TRANSPORT_WIFI)
+                .addCapability(NET_CAPABILITY_INTERNET)
                 .build(), callback)
 
         return tryTest {
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/SetFeatureFlagsRule.kt b/staticlibs/testutils/devicetests/com/android/testutils/SetFeatureFlagsRule.kt
new file mode 100644
index 0000000..4185b05
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/SetFeatureFlagsRule.kt
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.testutils.com.android.testutils
+
+import org.junit.rules.TestRule
+import org.junit.runner.Description
+import org.junit.runners.model.Statement
+
+/**
+ * A JUnit Rule that sets feature flags based on `@FeatureFlag` annotations.
+ *
+ * This rule enables dynamic control of feature flag states during testing.
+ *
+ * **Usage:**
+ * ```kotlin
+ * class MyTestClass {
+ *   @get:Rule
+ *   val setFeatureFlagsRule = SetFeatureFlagsRule(setFlagsMethod = (name, enabled) -> {
+ *     // Custom handling code.
+ *   })
+ *
+ *   // ... test methods with @FeatureFlag annotations
+ *   @FeatureFlag("FooBar1", true)
+ *   @FeatureFlag("FooBar2", false)
+ *   @Test
+ *   fun testFooBar() {}
+ * }
+ * ```
+ */
+class SetFeatureFlagsRule(val setFlagsMethod: (name: String, enabled: Boolean) -> Unit) : TestRule {
+    /**
+     * This annotation marks a test method as requiring a specific feature flag to be configured.
+     *
+     * Use this on test methods to dynamically control feature flag states during testing.
+     *
+     * @param name The name of the feature flag.
+     * @param enabled The desired state (true for enabled, false for disabled) of the feature flag.
+     */
+    @Target(AnnotationTarget.FUNCTION)
+    @Retention(AnnotationRetention.RUNTIME)
+    annotation class FeatureFlag(val name: String, val enabled: Boolean = true)
+
+    /**
+     * This method is the core of the rule, executed by the JUnit framework before each test method.
+     *
+     * It retrieves the test method's metadata.
+     * If any `@FeatureFlag` annotation is found, it passes every feature flag's name
+     * and enabled state into the user-specified lambda to apply custom actions.
+     */
+    override fun apply(base: Statement, description: Description): Statement {
+        return object : Statement() {
+            override fun evaluate() {
+                val testMethod = description.testClass.getMethod(description.methodName)
+                val featureFlagAnnotations = testMethod.getAnnotationsByType(
+                    FeatureFlag::class.java
+                )
+
+                for (featureFlagAnnotation in featureFlagAnnotations) {
+                    setFlagsMethod(featureFlagAnnotation.name, featureFlagAnnotation.enabled)
+                }
+
+                // Execute the test method, which includes methods annotated with
+                // @Before, @Test and @After.
+                base.evaluate()
+            }
+        }
+    }
+}
diff --git a/tests/cts/net/src/android/net/cts/ApfIntegrationTest.kt b/tests/cts/net/src/android/net/cts/ApfIntegrationTest.kt
index 3be44f7..0e57019 100644
--- a/tests/cts/net/src/android/net/cts/ApfIntegrationTest.kt
+++ b/tests/cts/net/src/android/net/cts/ApfIntegrationTest.kt
@@ -22,33 +22,51 @@
 import android.Manifest.permission.WRITE_DEVICE_CONFIG
 import android.content.pm.PackageManager.FEATURE_WIFI
 import android.net.ConnectivityManager
+import android.net.Network
 import android.net.NetworkCapabilities
 import android.net.NetworkRequest
 import android.net.apf.ApfCapabilities
 import android.os.Build
+import android.os.Handler
+import android.os.HandlerThread
 import android.os.PowerManager
 import android.platform.test.annotations.AppModeFull
 import android.provider.DeviceConfig
 import android.provider.DeviceConfig.NAMESPACE_CONNECTIVITY
+import android.system.Os
 import android.system.OsConstants
+import android.system.OsConstants.AF_INET
+import android.system.OsConstants.IPPROTO_ICMP
+import android.system.OsConstants.SOCK_DGRAM
+import android.system.OsConstants.SOCK_NONBLOCK
+import android.util.Log
 import androidx.test.platform.app.InstrumentationRegistry
 import com.android.compatibility.common.util.PropertyUtil.getVsrApiLevel
 import com.android.compatibility.common.util.SystemUtil.runShellCommand
 import com.android.compatibility.common.util.SystemUtil.runShellCommandOrThrow
 import com.android.internal.util.HexDump
+import com.android.net.module.util.PacketReader
 import com.android.testutils.DevSdkIgnoreRule
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
 import com.android.testutils.DevSdkIgnoreRunner
 import com.android.testutils.NetworkStackModuleTest
+import com.android.testutils.RecorderCallback.CallbackEntry.Available
 import com.android.testutils.RecorderCallback.CallbackEntry.LinkPropertiesChanged
 import com.android.testutils.SkipPresubmit
 import com.android.testutils.TestableNetworkCallback
 import com.android.testutils.runAsShell
+import com.android.testutils.waitForIdle
 import com.google.common.truth.Truth.assertThat
 import com.google.common.truth.Truth.assertWithMessage
 import com.google.common.truth.TruthJUnit.assume
+import java.io.FileDescriptor
 import java.lang.Thread
+import java.net.InetSocketAddress
+import java.util.concurrent.CompletableFuture
+import java.util.concurrent.TimeUnit
+import java.util.concurrent.TimeoutException
 import kotlin.random.Random
+import kotlin.test.assertFailsWith
 import kotlin.test.assertNotNull
 import org.junit.After
 import org.junit.Before
@@ -61,12 +79,17 @@
 private const val TIMEOUT_MS = 2000L
 private const val APF_NEW_RA_FILTER_VERSION = "apf_new_ra_filter_version"
 private const val POLLING_INTERVAL_MS: Int = 100
+private const val RCV_BUFFER_SIZE = 1480
 
 @AppModeFull(reason = "CHANGE_NETWORK_STATE permission can't be granted to instant apps")
 @RunWith(DevSdkIgnoreRunner::class)
 @NetworkStackModuleTest
+// ByteArray.toHexString is experimental API
+@kotlin.ExperimentalStdlibApi
 class ApfIntegrationTest {
     companion object {
+        private val PING_DESTINATION = InetSocketAddress("8.8.8.8", 0)
+
         @BeforeClass
         @JvmStatic
         @Suppress("ktlint:standard:no-multi-spaces")
@@ -86,6 +109,72 @@
         }
     }
 
+    class IcmpPacketReader(
+            handler: Handler,
+            private val network: Network
+    ) : PacketReader(handler, RCV_BUFFER_SIZE) {
+        private var sockFd: FileDescriptor? = null
+        private var futureReply: CompletableFuture<ByteArray>? = null
+
+        override fun createFd(): FileDescriptor {
+            // sockFd is closed by calling super.stop()
+            val sock = Os.socket(AF_INET, SOCK_DGRAM or SOCK_NONBLOCK, IPPROTO_ICMP)
+            // APF runs only on WiFi, so make sure the socket is bound to the right network.
+            network.bindSocket(sock)
+            sockFd = sock
+            return sock
+        }
+
+        override fun handlePacket(recvbuf: ByteArray, length: Int) {
+            // Only copy the ping data and complete the future.
+            val result = recvbuf.sliceArray(8..<length)
+            Log.i(TAG, "Received ping reply: ${result.toHexString()}")
+            futureReply!!.complete(recvbuf.sliceArray(8..<length))
+        }
+
+        fun sendPing(data: ByteArray) {
+            require(data.size == 56)
+
+            // rfc792: Echo (type 0x08) or Echo Reply (type 0x00) Message:
+            //  0                   1                   2                   3
+            //  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+            // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+            // |     Type      |     Code      |          Checksum             |
+            // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+            // |           Identifier          |        Sequence Number        |
+            // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+            // |     Data ...
+            // +-+-+-+-+-
+            val icmpHeader = byteArrayOf(0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00)
+            val packet = icmpHeader + data
+            Log.i(TAG, "Sent ping: ${packet.toHexString()}")
+            futureReply = CompletableFuture<ByteArray>()
+            Os.sendto(sockFd!!, packet, 0, packet.size, 0, PING_DESTINATION)
+        }
+
+        fun expectPingReply(): ByteArray {
+            return futureReply!!.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
+        }
+
+        fun expectPingDropped() {
+            assertFailsWith(TimeoutException::class) {
+                futureReply!!.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
+            }
+        }
+
+        override fun start(): Boolean {
+            // Ignore the fact start() could return false or throw an exception.
+            handler.post({ super.start() })
+            handler.waitForIdle(TIMEOUT_MS)
+            return true
+        }
+
+        override fun stop() {
+            handler.post({ super.stop() })
+            handler.waitForIdle(TIMEOUT_MS)
+        }
+    }
+
     @get:Rule
     val ignoreRule = DevSdkIgnoreRule()
 
@@ -94,9 +183,13 @@
     private val pm by lazy { context.packageManager }
     private val powerManager by lazy { context.getSystemService(PowerManager::class.java)!! }
     private val wakeLock by lazy { powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG) }
+    private lateinit var network: Network
     private lateinit var ifname: String
     private lateinit var networkCallback: TestableNetworkCallback
     private lateinit var caps: ApfCapabilities
+    private val handlerThread = HandlerThread("$TAG handler thread").apply { start() }
+    private val handler = Handler(handlerThread.looper)
+    private lateinit var packetReader: IcmpPacketReader
 
     fun getApfCapabilities(): ApfCapabilities {
         val caps = runShellCommand("cmd network_stack apf $ifname capabilities").trim()
@@ -146,6 +239,7 @@
                         .build(),
                 networkCallback
         )
+        network = networkCallback.expect<Available>().network
         networkCallback.eventuallyExpect<LinkPropertiesChanged>(TIMEOUT_MS) {
             ifname = assertNotNull(it.lp.interfaceName)
             true
@@ -155,10 +249,19 @@
         // respective VSR releases and all other tests are based on the capabilities indicated.
         runShellCommand("cmd network_stack apf $ifname pause")
         caps = getApfCapabilities()
+
+        packetReader = IcmpPacketReader(handler, network)
+        packetReader.start()
     }
 
     @After
     fun tearDown() {
+        if (::packetReader.isInitialized) {
+            packetReader.stop()
+        }
+        handlerThread.quitSafely()
+        handlerThread.join()
+
         if (::ifname.isInitialized) {
             runShellCommand("cmd network_stack apf $ifname resume")
         }
@@ -203,7 +306,7 @@
     }
 
     fun installProgram(bytes: ByteArray) {
-        val prog = HexDump.toHexString(bytes, 0 /* offset */, bytes.size, false /* upperCase */)
+        val prog = bytes.toHexString()
         val result = runShellCommandOrThrow("cmd network_stack apf $ifname install $prog").trim()
         // runShellCommandOrThrow only throws on S+.
         assertThat(result).isEqualTo("success")
@@ -236,4 +339,14 @@
             assertWithMessage("read/write $i byte prog failed").that(readResult).isEqualTo(program)
         }
     }
+
+    // TODO: this is a placeholder test to test the IcmpPacketReader functionality and will soon be
+    // replaced by a real test.
+    @Test
+    fun testPing() {
+        val data = ByteArray(56)
+        Random.nextBytes(data)
+        packetReader.sendPing(data)
+        assertThat(packetReader.expectPingReply()).isEqualTo(data)
+    }
 }
diff --git a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
index 4d465ba..c0f1080 100644
--- a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
@@ -71,7 +71,9 @@
 import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VPN;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_PARTIAL_CONNECTIVITY;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED;
 import static android.net.NetworkCapabilities.TRANSPORT_TEST;
 import static android.net.NetworkCapabilities.TRANSPORT_VPN;
@@ -81,7 +83,6 @@
 import static android.net.cts.util.CtsNetUtils.HTTP_PORT;
 import static android.net.cts.util.CtsNetUtils.NETWORK_CALLBACK_ACTION;
 import static android.net.cts.util.CtsNetUtils.TEST_HOST;
-import static android.net.cts.util.CtsNetUtils.TestNetworkCallback;
 import static android.net.cts.util.CtsTetheringUtils.TestTetheringEventCallback;
 import static android.os.MessageQueue.OnFileDescriptorEventListener.EVENT_INPUT;
 import static android.os.Process.INVALID_UID;
@@ -333,8 +334,6 @@
     private final ArraySet<Integer> mNetworkTypes = new ArraySet<>();
     private UiAutomation mUiAutomation;
     private CtsNetUtils mCtsNetUtils;
-    // The registered callbacks.
-    private List<NetworkCallback> mRegisteredCallbacks = new ArrayList<>();
     // Used for cleanup purposes.
     private final List<Range<Integer>> mVpnRequiredUidRanges = new ArrayList<>();
 
@@ -425,15 +424,12 @@
         // All tests in this class require a working Internet connection as they start. Make
         // sure there is still one as they end that's ready to use for the next test to use.
         mTestValidationConfigRule.runAfterNextCleanup(() -> {
-            final TestNetworkCallback callback = new TestNetworkCallback();
-            registerDefaultNetworkCallback(callback);
-            try {
-                assertNotNull("Couldn't restore Internet connectivity",
-                        callback.waitForAvailable());
-            } finally {
-                // Unregister all registered callbacks.
-                unregisterRegisteredCallbacks();
-            }
+            // mTestValidationConfigRule has higher order than networkCallbackRule, so
+            // networkCallbackRule is the outer rule and will be cleaned up after this method.
+            final TestableNetworkCallback callback =
+                    networkCallbackRule.registerDefaultNetworkCallback();
+            assertNotNull("Couldn't restore Internet connectivity",
+                    callback.eventuallyExpect(CallbackEntry.AVAILABLE));
         });
     }
 
@@ -993,10 +989,10 @@
         // default network.
         return new NetworkRequest.Builder()
                 .clearCapabilities()
-                .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
-                .addCapability(NetworkCapabilities.NET_CAPABILITY_TRUSTED)
-                .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
-                .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
+                .addCapability(NET_CAPABILITY_NOT_RESTRICTED)
+                .addCapability(NET_CAPABILITY_TRUSTED)
+                .addCapability(NET_CAPABILITY_NOT_VPN)
+                .addCapability(NET_CAPABILITY_INTERNET)
                 .build();
     }
 
@@ -1027,10 +1023,10 @@
         final String invalidPrivateDnsServer = "invalidhostname.example.com";
         final String goodPrivateDnsServer = "dns.google";
         mCtsNetUtils.storePrivateDnsSetting();
-        final TestableNetworkCallback cb = new TestableNetworkCallback();
         final NetworkRequest networkRequest = new NetworkRequest.Builder()
                 .addCapability(NET_CAPABILITY_INTERNET).build();
-        registerNetworkCallback(networkRequest, cb);
+        final TestableNetworkCallback cb =
+                networkCallbackRule.registerNetworkCallback(networkRequest);
         final Network networkForPrivateDns = mCm.getActiveNetwork();
         try {
             // Verifying the good private DNS sever
@@ -1068,24 +1064,27 @@
         assumeTrue(mPackageManager.hasSystemFeature(FEATURE_WIFI));
 
         // We will register for a WIFI network being available or lost.
-        final TestNetworkCallback callback = new TestNetworkCallback();
-        registerNetworkCallback(makeWifiNetworkRequest(), callback);
+        final TestableNetworkCallback callback = networkCallbackRule.registerNetworkCallback(
+                makeWifiNetworkRequest());
 
-        final TestNetworkCallback defaultTrackingCallback = new TestNetworkCallback();
-        registerDefaultNetworkCallback(defaultTrackingCallback);
+        final TestableNetworkCallback defaultTrackingCallback =
+                networkCallbackRule.registerDefaultNetworkCallback();
 
-        final TestNetworkCallback systemDefaultCallback = new TestNetworkCallback();
-        final TestNetworkCallback perUidCallback = new TestNetworkCallback();
-        final TestNetworkCallback bestMatchingCallback = new TestNetworkCallback();
+        final TestableNetworkCallback systemDefaultCallback = new TestableNetworkCallback();
+        final TestableNetworkCallback perUidCallback = new TestableNetworkCallback();
+        final TestableNetworkCallback bestMatchingCallback = new TestableNetworkCallback();
         final Handler h = new Handler(Looper.getMainLooper());
         if (TestUtils.shouldTestSApis()) {
             assertThrows(SecurityException.class, () ->
-                    registerSystemDefaultNetworkCallback(systemDefaultCallback, h));
+                    networkCallbackRule.registerSystemDefaultNetworkCallback(
+                            systemDefaultCallback, h));
             runWithShellPermissionIdentity(() -> {
-                registerSystemDefaultNetworkCallback(systemDefaultCallback, h);
-                registerDefaultNetworkCallbackForUid(Process.myUid(), perUidCallback, h);
+                networkCallbackRule.registerSystemDefaultNetworkCallback(systemDefaultCallback, h);
+                networkCallbackRule.registerDefaultNetworkCallbackForUid(Process.myUid(),
+                        perUidCallback, h);
             }, NETWORK_SETTINGS);
-            registerBestMatchingNetworkCallback(makeDefaultRequest(), bestMatchingCallback, h);
+            networkCallbackRule.registerBestMatchingNetworkCallback(
+                    makeDefaultRequest(), bestMatchingCallback, h);
         }
 
         Network wifiNetwork = null;
@@ -1094,24 +1093,22 @@
         // Now we should expect to get a network callback about availability of the wifi
         // network even if it was already connected as a state-based action when the callback
         // is registered.
-        wifiNetwork = callback.waitForAvailable();
+        wifiNetwork = callback.eventuallyExpect(CallbackEntry.AVAILABLE).getNetwork();
         assertNotNull("Did not receive onAvailable for TRANSPORT_WIFI request",
                 wifiNetwork);
 
-        final Network defaultNetwork = defaultTrackingCallback.waitForAvailable();
+        final Network defaultNetwork = defaultTrackingCallback.eventuallyExpect(
+                CallbackEntry.AVAILABLE).getNetwork();
         assertNotNull("Did not receive onAvailable on default network callback",
                 defaultNetwork);
 
         if (TestUtils.shouldTestSApis()) {
-            assertNotNull("Did not receive onAvailable on system default network callback",
-                    systemDefaultCallback.waitForAvailable());
-            final Network perUidNetwork = perUidCallback.waitForAvailable();
-            assertNotNull("Did not receive onAvailable on per-UID default network callback",
-                    perUidNetwork);
+            systemDefaultCallback.eventuallyExpect(CallbackEntry.AVAILABLE);
+            final Network perUidNetwork = perUidCallback.eventuallyExpect(CallbackEntry.AVAILABLE)
+                    .getNetwork();
             assertEquals(defaultNetwork, perUidNetwork);
-            final Network bestMatchingNetwork = bestMatchingCallback.waitForAvailable();
-            assertNotNull("Did not receive onAvailable on best matching network callback",
-                    bestMatchingNetwork);
+            final Network bestMatchingNetwork = bestMatchingCallback.eventuallyExpect(
+                    CallbackEntry.AVAILABLE).getNetwork();
             assertEquals(defaultNetwork, bestMatchingNetwork);
         }
     }
@@ -1123,8 +1120,8 @@
         final Handler h = new Handler(Looper.getMainLooper());
         // Verify registerSystemDefaultNetworkCallback can be accessed via
         // CONNECTIVITY_USE_RESTRICTED_NETWORKS permission.
-        runWithShellPermissionIdentity(() ->
-                        registerSystemDefaultNetworkCallback(new TestNetworkCallback(), h),
+        runWithShellPermissionIdentity(
+                () -> networkCallbackRule.registerSystemDefaultNetworkCallback(h),
                 CONNECTIVITY_USE_RESTRICTED_NETWORKS);
     }
 
@@ -1294,15 +1291,14 @@
      */
     @AppModeFull(reason = "CHANGE_NETWORK_STATE permission can't be granted to instant apps")
     @Test
-    public void testRequestNetworkCallback() throws Exception {
-        final TestNetworkCallback callback = new TestNetworkCallback();
-        requestNetwork(new NetworkRequest.Builder()
-                .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
-                .build(), callback);
+    public void testRequestNetworkCallback() {
+        final TestableNetworkCallback callback = networkCallbackRule.requestNetwork(
+                new NetworkRequest.Builder().addCapability(
+                                NET_CAPABILITY_INTERNET)
+                        .build());
 
         // Wait to get callback for availability of internet
-        Network internetNetwork = callback.waitForAvailable();
-        assertNotNull("Did not receive NetworkCallback#onAvailable for INTERNET", internetNetwork);
+        callback.eventuallyExpect(CallbackEntry.AVAILABLE).getNetwork();
     }
 
     /**
@@ -1320,16 +1316,13 @@
             }
         }
 
-        final TestNetworkCallback callback = new TestNetworkCallback();
-        requestNetwork(new NetworkRequest.Builder().addTransportType(TRANSPORT_WIFI).build(),
-                callback, 100);
 
+        final TestableNetworkCallback callback = networkCallbackRule.requestNetwork(
+                new NetworkRequest.Builder().addTransportType(TRANSPORT_WIFI).build(),
+                100 /* timeoutMs */);
         try {
             // Wait to get callback for unavailability of requested network
-            assertTrue("Did not receive NetworkCallback#onUnavailable",
-                    callback.waitForUnavailable());
-        } catch (InterruptedException e) {
-            fail("NetworkCallback wait was interrupted.");
+            callback.eventuallyExpect(CallbackEntry.UNAVAILABLE, 2_000 /* timeoutMs */);
         } finally {
             if (previousWifiEnabledState) {
                 mCtsNetUtils.connectToWifi();
@@ -1416,40 +1409,48 @@
             final boolean useSystemDefault)
             throws Exception {
         final CompletableFuture<Network> networkFuture = new CompletableFuture<>();
-        final NetworkCallback networkCallback = new NetworkCallback() {
-            @Override
-            public void onCapabilitiesChanged(Network network, NetworkCapabilities nc) {
-                if (!nc.hasTransport(targetTransportType)) return;
 
-                final boolean metered = !nc.hasCapability(NET_CAPABILITY_NOT_METERED);
-                final boolean validated = nc.hasCapability(NET_CAPABILITY_VALIDATED);
-                if (metered == requestedMeteredness && (!waitForValidation || validated)) {
-                    networkFuture.complete(network);
+        // Registering a callback here guarantees onCapabilitiesChanged is called immediately
+        // with the current setting. Therefore, if the setting has already been changed,
+        // this method will return right away, and if not, it'll wait for the setting to change.
+        final TestableNetworkCallback networkCallback;
+        if (useSystemDefault) {
+            networkCallback = runWithShellPermissionIdentity(() -> {
+                if (isAtLeastS()) {
+                    return networkCallbackRule.registerSystemDefaultNetworkCallback(
+                            new Handler(Looper.getMainLooper()));
+                } else {
+                    // registerSystemDefaultNetworkCallback is only supported on S+.
+                    return networkCallbackRule.requestNetwork(
+                            new NetworkRequest.Builder()
+                                    .clearCapabilities()
+                                    .addCapability(NET_CAPABILITY_NOT_RESTRICTED)
+                                    .addCapability(NET_CAPABILITY_TRUSTED)
+                                    .addCapability(NET_CAPABILITY_NOT_VPN)
+                                    .addCapability(NET_CAPABILITY_INTERNET)
+                                    .build(),
+                            new TestableNetworkCallback(),
+                            new Handler(Looper.getMainLooper()));
                 }
-            }
-        };
-
-        try {
-            // Registering a callback here guarantees onCapabilitiesChanged is called immediately
-            // with the current setting. Therefore, if the setting has already been changed,
-            // this method will return right away, and if not, it'll wait for the setting to change.
-            if (useSystemDefault) {
-                runWithShellPermissionIdentity(() ->
-                                registerSystemDefaultNetworkCallback(networkCallback,
-                                        new Handler(Looper.getMainLooper())),
-                        NETWORK_SETTINGS);
-            } else {
-                registerDefaultNetworkCallback(networkCallback);
-            }
-
-            // Changing meteredness on wifi involves reconnecting, which can take several seconds
-            // (involves re-associating, DHCP...).
-            return networkFuture.get(NETWORK_CALLBACK_TIMEOUT_MS, TimeUnit.MILLISECONDS);
-        } catch (TimeoutException e) {
-            throw new AssertionError("Timed out waiting for active network metered status to "
-                    + "change to " + requestedMeteredness + " ; network = "
-                    + mCm.getActiveNetwork(), e);
+            },
+            NETWORK_SETTINGS);
+        } else {
+            networkCallback = networkCallbackRule.registerDefaultNetworkCallback();
         }
+
+        return networkCallback.eventuallyExpect(
+                CallbackEntry.NETWORK_CAPS_UPDATED,
+                // Changing meteredness on wifi involves reconnecting, which can take several
+                // seconds (involves re-associating, DHCP...).
+                NETWORK_CALLBACK_TIMEOUT_MS,
+                cb -> {
+                    final NetworkCapabilities nc = cb.getCaps();
+                    if (!nc.hasTransport(targetTransportType)) return false;
+
+                    final boolean metered = !nc.hasCapability(NET_CAPABILITY_NOT_METERED);
+                    final boolean validated = nc.hasCapability(NET_CAPABILITY_VALIDATED);
+                    return metered == requestedMeteredness && (!waitForValidation || validated);
+                }).getNetwork();
     }
 
     private Network setWifiMeteredStatusAndWait(String ssid, boolean isMetered,
@@ -2091,15 +2092,15 @@
     }
 
     private void verifyBindSocketToRestrictedNetworkDisallowed() throws Exception {
-        final TestableNetworkCallback testNetworkCb = new TestableNetworkCallback();
         final NetworkRequest testRequest = new NetworkRequest.Builder()
                 .addTransportType(NetworkCapabilities.TRANSPORT_TEST)
-                .removeCapability(NetworkCapabilities.NET_CAPABILITY_TRUSTED)
-                .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
+                .removeCapability(NET_CAPABILITY_TRUSTED)
+                .removeCapability(NET_CAPABILITY_NOT_RESTRICTED)
                 .setNetworkSpecifier(CompatUtil.makeTestNetworkSpecifier(
                         TEST_RESTRICTED_NW_IFACE_NAME))
                 .build();
-        runWithShellPermissionIdentity(() -> requestNetwork(testRequest, testNetworkCb),
+        final TestableNetworkCallback testNetworkCb = runWithShellPermissionIdentity(
+                () -> networkCallbackRule.requestNetwork(testRequest),
                 CONNECTIVITY_USE_RESTRICTED_NETWORKS,
                 // CONNECTIVITY_INTERNAL is for requesting restricted network because shell does not
                 // have CONNECTIVITY_USE_RESTRICTED_NETWORKS on R.
@@ -2115,7 +2116,7 @@
                     NETWORK_CALLBACK_TIMEOUT_MS,
                     entry -> network.equals(entry.getNetwork())
                             && (!((CallbackEntry.CapabilitiesChanged) entry).getCaps()
-                            .hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)));
+                            .hasCapability(NET_CAPABILITY_NOT_RESTRICTED)));
             // CtsNetTestCases package doesn't hold CONNECTIVITY_USE_RESTRICTED_NETWORKS, so it
             // does not allow to bind socket to restricted network.
             assertThrows(IOException.class, () -> network.bindSocket(socket));
@@ -2238,7 +2239,7 @@
 
     private void registerCallbackAndWaitForAvailable(@NonNull final NetworkRequest request,
             @NonNull final TestableNetworkCallback cb) {
-        registerNetworkCallback(request, cb);
+        networkCallbackRule.registerNetworkCallback(request, cb);
         waitForAvailable(cb);
     }
 
@@ -2346,21 +2347,13 @@
 
     private void verifySsidFromCallbackNetworkCapabilities(@NonNull String ssid, boolean hasSsid)
             throws Exception {
-        final CompletableFuture<NetworkCapabilities> foundNc = new CompletableFuture();
-        final NetworkCallback callback = new NetworkCallback() {
-            @Override
-            public void onCapabilitiesChanged(Network network, NetworkCapabilities nc) {
-                foundNc.complete(nc);
-            }
-        };
-
-        registerNetworkCallback(makeWifiNetworkRequest(), callback);
+        final TestableNetworkCallback callback =
+                networkCallbackRule.registerNetworkCallback(makeWifiNetworkRequest());
         // Registering a callback here guarantees onCapabilitiesChanged is called immediately
         // because WiFi network should be connected.
-        final NetworkCapabilities nc =
-                foundNc.get(NETWORK_CALLBACK_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+        final NetworkCapabilities nc = callback.eventuallyExpect(
+                CallbackEntry.NETWORK_CAPS_UPDATED, NETWORK_CALLBACK_TIMEOUT_MS).getCaps();
         // Verify if ssid is contained in the NetworkCapabilities received from callback.
-        assertNotNull("NetworkCapabilities of the network is null", nc);
         assertEquals(hasSsid, Pattern.compile(ssid).matcher(nc.toString()).find());
     }
 
@@ -2389,8 +2382,8 @@
         final NetworkRequest testRequest = new NetworkRequest.Builder()
                 .addTransportType(TRANSPORT_TEST)
                 // Test networks do not have NOT_VPN or TRUSTED capabilities by default
-                .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
-                .removeCapability(NetworkCapabilities.NET_CAPABILITY_TRUSTED)
+                .removeCapability(NET_CAPABILITY_NOT_VPN)
+                .removeCapability(NET_CAPABILITY_TRUSTED)
                 .setNetworkSpecifier(CompatUtil.makeTestNetworkSpecifier(
                         testNetworkInterface.getInterfaceName()))
                 .build();
@@ -2399,14 +2392,15 @@
         final TestableNetworkCallback callback = new TestableNetworkCallback();
         final Handler handler = new Handler(Looper.getMainLooper());
         assertThrows(SecurityException.class,
-                () -> requestBackgroundNetwork(testRequest, callback, handler));
+                () -> networkCallbackRule.requestBackgroundNetwork(testRequest, callback, handler));
 
         Network testNetwork = null;
         try {
             // Request background test network via Shell identity which has NETWORK_SETTINGS
             // permission granted.
             runWithShellPermissionIdentity(
-                    () -> requestBackgroundNetwork(testRequest, callback, handler),
+                    () -> networkCallbackRule.requestBackgroundNetwork(
+                            testRequest, callback, handler),
                     new String[] { android.Manifest.permission.NETWORK_SETTINGS });
 
             // Register the test network agent which has no foreground request associated to it.
@@ -2499,9 +2493,10 @@
         final int otherUid = UserHandle.getUid(5, Process.FIRST_APPLICATION_UID);
         final Handler handler = new Handler(Looper.getMainLooper());
 
-        registerDefaultNetworkCallback(myUidCallback, handler);
-        runWithShellPermissionIdentity(() -> registerDefaultNetworkCallbackForUid(
-                otherUid, otherUidCallback, handler), NETWORK_SETTINGS);
+        networkCallbackRule.registerDefaultNetworkCallback(myUidCallback, handler);
+        runWithShellPermissionIdentity(
+                () -> networkCallbackRule.registerDefaultNetworkCallbackForUid(
+                        otherUid, otherUidCallback, handler), NETWORK_SETTINGS);
 
         final Network defaultNetwork = myUidCallback.expect(CallbackEntry.AVAILABLE).getNetwork();
         final List<DetailedBlockedStatusCallback> allCallbacks =
@@ -2557,14 +2552,14 @@
         assertNotNull(info);
         assertEquals(DetailedState.CONNECTED, info.getDetailedState());
 
-        final TestableNetworkCallback callback = new TestableNetworkCallback();
+        final TestableNetworkCallback callback;
         try {
             mCmShim.setLegacyLockdownVpnEnabled(true);
 
             // setLegacyLockdownVpnEnabled is asynchronous and only takes effect when the
             // ConnectivityService handler thread processes it. Ensure it has taken effect by doing
             // something that blocks until the handler thread is idle.
-            registerDefaultNetworkCallback(callback);
+            callback = networkCallbackRule.registerDefaultNetworkCallback();
             waitForAvailable(callback);
 
             // Test one of the effects of setLegacyLockdownVpnEnabled: the fact that any NetworkInfo
@@ -2831,9 +2826,9 @@
     private void registerTestOemNetworkPreferenceCallbacks(
             @NonNull final TestableNetworkCallback defaultCallback,
             @NonNull final TestableNetworkCallback systemDefaultCallback) {
-        registerDefaultNetworkCallback(defaultCallback);
+        networkCallbackRule.registerDefaultNetworkCallback(defaultCallback);
         runWithShellPermissionIdentity(() ->
-                registerSystemDefaultNetworkCallback(systemDefaultCallback,
+                networkCallbackRule.registerSystemDefaultNetworkCallback(systemDefaultCallback,
                         new Handler(Looper.getMainLooper())), NETWORK_SETTINGS);
     }
 
@@ -2949,18 +2944,18 @@
                         + " unless device supports WiFi",
                 mPackageManager.hasSystemFeature(FEATURE_WIFI));
 
-        final TestNetworkCallback cb = new TestNetworkCallback();
         try {
             // Wait for partial connectivity to be detected on the network
             final Network network = preparePartialConnectivity();
 
-            requestNetwork(makeWifiNetworkRequest(), cb);
+            final TestableNetworkCallback cb = networkCallbackRule.requestNetwork(
+                    makeWifiNetworkRequest());
             runAsShell(NETWORK_SETTINGS, () -> {
                 // The always bit is verified in NetworkAgentTest
                 mCm.setAcceptPartialConnectivity(network, false /* accept */, false /* always */);
             });
             // Reject partial connectivity network should cause the network being torn down
-            assertEquals(network, cb.waitForLost());
+            assertEquals(network, cb.eventuallyExpect(CallbackEntry.LOST).getNetwork());
         } finally {
             mHttpServer.stop();
             // Wifi will not automatically reconnect to the network. ensureWifiDisconnected cannot
@@ -2988,7 +2983,6 @@
         assumeTrue("testAcceptPartialConnectivity_validatedNetwork cannot execute"
                         + " unless device supports WiFi and telephony", canRunTest);
 
-        final TestableNetworkCallback wifiCb = new TestableNetworkCallback();
         try {
             // Ensure at least one default network candidate connected.
             networkCallbackRule.requestCell();
@@ -2998,7 +2992,8 @@
             // guarantee that it won't become the default in the future.
             assertNotEquals(wifiNetwork, mCm.getActiveNetwork());
 
-            registerNetworkCallback(makeWifiNetworkRequest(), wifiCb);
+            final TestableNetworkCallback wifiCb = networkCallbackRule.registerNetworkCallback(
+                    makeWifiNetworkRequest());
             runAsShell(NETWORK_SETTINGS, () -> {
                 mCm.setAcceptUnvalidated(wifiNetwork, false /* accept */, false /* always */);
             });
@@ -3025,8 +3020,6 @@
         assumeTrue("testSetAvoidUnvalidated cannot execute"
                 + " unless device supports WiFi and telephony", canRunTest);
 
-        final TestableNetworkCallback wifiCb = new TestableNetworkCallback();
-        final TestableNetworkCallback defaultCb = new TestableNetworkCallback();
         final int previousAvoidBadWifi =
                 ConnectivitySettingsManager.getNetworkAvoidBadWifi(mContext);
 
@@ -3036,8 +3029,10 @@
             final Network cellNetwork = networkCallbackRule.requestCell();
             final Network wifiNetwork = prepareValidatedNetwork();
 
-            registerDefaultNetworkCallback(defaultCb);
-            registerNetworkCallback(makeWifiNetworkRequest(), wifiCb);
+            final TestableNetworkCallback defaultCb =
+                    networkCallbackRule.registerDefaultNetworkCallback();
+            final TestableNetworkCallback wifiCb = networkCallbackRule.registerNetworkCallback(
+                    makeWifiNetworkRequest());
 
             // Verify wifi is the default network.
             defaultCb.eventuallyExpect(CallbackEntry.AVAILABLE, NETWORK_CALLBACK_TIMEOUT_MS,
@@ -3100,20 +3095,11 @@
         });
     }
 
-    private Network expectNetworkHasCapability(Network network, int expectedNetCap, long timeout)
-            throws Exception {
-        final CompletableFuture<Network> future = new CompletableFuture();
-        final NetworkCallback cb = new NetworkCallback() {
-            @Override
-            public void onCapabilitiesChanged(Network n, NetworkCapabilities nc) {
-                if (n.equals(network) && nc.hasCapability(expectedNetCap)) {
-                    future.complete(network);
-                }
-            }
-        };
-
-        registerNetworkCallback(new NetworkRequest.Builder().build(), cb);
-        return future.get(timeout, TimeUnit.MILLISECONDS);
+    private Network expectNetworkHasCapability(Network network, int expectedNetCap, long timeout) {
+        return networkCallbackRule.registerNetworkCallback(new NetworkRequest.Builder().build())
+                .eventuallyExpect(CallbackEntry.NETWORK_CAPS_UPDATED, timeout,
+                        cb -> cb.getNetwork().equals(network)
+                                && cb.getCaps().hasCapability(expectedNetCap)).getNetwork();
     }
 
     private void prepareHttpServer() throws Exception {
@@ -3265,12 +3251,13 @@
         // For testing mobile data preferred uids feature, it needs both wifi and cell network.
         final Network wifiNetwork = mCtsNetUtils.ensureWifiConnected();
         final Network cellNetwork = networkCallbackRule.requestCell();
-        final TestableNetworkCallback defaultTrackingCb = new TestableNetworkCallback();
-        final TestableNetworkCallback systemDefaultCb = new TestableNetworkCallback();
         final Handler h = new Handler(Looper.getMainLooper());
-        runWithShellPermissionIdentity(() -> registerSystemDefaultNetworkCallback(
-                systemDefaultCb, h), NETWORK_SETTINGS);
-        registerDefaultNetworkCallback(defaultTrackingCb);
+        final TestableNetworkCallback systemDefaultCb = runWithShellPermissionIdentity(
+                () -> networkCallbackRule.registerSystemDefaultNetworkCallback(h),
+                NETWORK_SETTINGS);
+
+        final TestableNetworkCallback defaultTrackingCb =
+                networkCallbackRule.registerDefaultNetworkCallback();
 
         try {
             // CtsNetTestCases uid is not listed in MOBILE_DATA_PREFERRED_UIDS setting, so the
@@ -3339,7 +3326,7 @@
         // Create test network agent with restricted network.
         final NetworkCapabilities nc = new NetworkCapabilities.Builder()
                 .addTransportType(NetworkCapabilities.TRANSPORT_TEST)
-                .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
+                .removeCapability(NET_CAPABILITY_NOT_RESTRICTED)
                 .setNetworkSpecifier(CompatUtil.makeTestNetworkSpecifier(
                         TEST_RESTRICTED_NW_IFACE_NAME))
                 .build();
@@ -3373,23 +3360,23 @@
                 mContext, originalUidsAllowedOnRestrictedNetworks), NETWORK_SETTINGS);
 
         // File a restricted network request with permission first to hold the connection.
-        final TestableNetworkCallback testNetworkCb = new TestableNetworkCallback();
         final NetworkRequest testRequest = new NetworkRequest.Builder()
                 .addTransportType(NetworkCapabilities.TRANSPORT_TEST)
-                .removeCapability(NetworkCapabilities.NET_CAPABILITY_TRUSTED)
-                .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
+                .removeCapability(NET_CAPABILITY_TRUSTED)
+                .removeCapability(NET_CAPABILITY_NOT_RESTRICTED)
                 .setNetworkSpecifier(CompatUtil.makeTestNetworkSpecifier(
                         TEST_RESTRICTED_NW_IFACE_NAME))
                 .build();
-        runWithShellPermissionIdentity(() -> requestNetwork(testRequest, testNetworkCb),
+        final TestableNetworkCallback testNetworkCb = runWithShellPermissionIdentity(
+                () -> networkCallbackRule.requestNetwork(testRequest),
                 CONNECTIVITY_USE_RESTRICTED_NETWORKS);
 
         // File another restricted network request without permission.
         final TestableNetworkCallback restrictedNetworkCb = new TestableNetworkCallback();
         final NetworkRequest restrictedRequest = new NetworkRequest.Builder()
                 .addTransportType(NetworkCapabilities.TRANSPORT_TEST)
-                .removeCapability(NetworkCapabilities.NET_CAPABILITY_TRUSTED)
-                .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
+                .removeCapability(NET_CAPABILITY_TRUSTED)
+                .removeCapability(NET_CAPABILITY_NOT_RESTRICTED)
                 .setNetworkSpecifier(CompatUtil.makeTestNetworkSpecifier(
                         TEST_RESTRICTED_NW_IFACE_NAME))
                 .build();
@@ -3406,7 +3393,7 @@
                     NETWORK_CALLBACK_TIMEOUT_MS,
                     entry -> network.equals(entry.getNetwork())
                             && (!((CallbackEntry.CapabilitiesChanged) entry).getCaps()
-                            .hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)));
+                            .hasCapability(NET_CAPABILITY_NOT_RESTRICTED)));
             // CtsNetTestCases package doesn't hold CONNECTIVITY_USE_RESTRICTED_NETWORKS, so it
             // does not allow to bind socket to restricted network.
             assertThrows(IOException.class, () -> network.bindSocket(socket));
@@ -3424,13 +3411,13 @@
 
             if (TestUtils.shouldTestTApis()) {
                 // Uid is in allowed list. Try file network request again.
-                requestNetwork(restrictedRequest, restrictedNetworkCb);
+                networkCallbackRule.requestNetwork(restrictedRequest, restrictedNetworkCb);
                 // Verify that the network is restricted.
                 restrictedNetworkCb.eventuallyExpect(CallbackEntry.NETWORK_CAPS_UPDATED,
                         NETWORK_CALLBACK_TIMEOUT_MS,
                         entry -> network.equals(entry.getNetwork())
                                 && (!((CallbackEntry.CapabilitiesChanged) entry).getCaps()
-                                .hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)));
+                                .hasCapability(NET_CAPABILITY_NOT_RESTRICTED)));
             }
         } finally {
             agent.unregister();
@@ -3728,58 +3715,4 @@
         // shims, and @IgnoreUpTo does not check that.
         assumeTrue(TestUtils.shouldTestSApis());
     }
-
-    private void unregisterRegisteredCallbacks() {
-        for (NetworkCallback callback: mRegisteredCallbacks) {
-            mCm.unregisterNetworkCallback(callback);
-        }
-    }
-
-    private void registerDefaultNetworkCallback(NetworkCallback callback) {
-        mCm.registerDefaultNetworkCallback(callback);
-        mRegisteredCallbacks.add(callback);
-    }
-
-    private void registerDefaultNetworkCallback(NetworkCallback callback, Handler handler) {
-        mCm.registerDefaultNetworkCallback(callback, handler);
-        mRegisteredCallbacks.add(callback);
-    }
-
-    private void registerNetworkCallback(NetworkRequest request, NetworkCallback callback) {
-        mCm.registerNetworkCallback(request, callback);
-        mRegisteredCallbacks.add(callback);
-    }
-
-    private void registerSystemDefaultNetworkCallback(NetworkCallback callback, Handler handler) {
-        mCmShim.registerSystemDefaultNetworkCallback(callback, handler);
-        mRegisteredCallbacks.add(callback);
-    }
-
-    private void registerDefaultNetworkCallbackForUid(int uid, NetworkCallback callback,
-            Handler handler) throws Exception {
-        mCmShim.registerDefaultNetworkCallbackForUid(uid, callback, handler);
-        mRegisteredCallbacks.add(callback);
-    }
-
-    private void requestNetwork(NetworkRequest request, NetworkCallback callback) {
-        mCm.requestNetwork(request, callback);
-        mRegisteredCallbacks.add(callback);
-    }
-
-    private void requestNetwork(NetworkRequest request, NetworkCallback callback, int timeoutSec) {
-        mCm.requestNetwork(request, callback, timeoutSec);
-        mRegisteredCallbacks.add(callback);
-    }
-
-    private void registerBestMatchingNetworkCallback(NetworkRequest request,
-            NetworkCallback callback, Handler handler) {
-        mCm.registerBestMatchingNetworkCallback(request, callback, handler);
-        mRegisteredCallbacks.add(callback);
-    }
-
-    private void requestBackgroundNetwork(NetworkRequest request, NetworkCallback callback,
-            Handler handler) throws Exception {
-        mCmShim.requestBackgroundNetwork(request, callback, handler);
-        mRegisteredCallbacks.add(callback);
-    }
 }
diff --git a/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt b/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt
index d052551..6fa2812 100644
--- a/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt
+++ b/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt
@@ -905,13 +905,17 @@
         val iface = createInterface()
         val listener = EthernetStateListener()
         addInterfaceStateListener(listener)
+        // Uses eventuallyExpect to account for interfaces that could already exist on device
+        listener.eventuallyExpect(iface, STATE_LINK_UP, ROLE_CLIENT)
+
+        disableInterface(iface).expectResult(iface.name)
+        listener.eventuallyExpect(iface, STATE_LINK_DOWN, ROLE_CLIENT)
+
+        enableInterface(iface).expectResult(iface.name)
         listener.expectCallback(iface, STATE_LINK_UP, ROLE_CLIENT)
 
         disableInterface(iface).expectResult(iface.name)
         listener.expectCallback(iface, STATE_LINK_DOWN, ROLE_CLIENT)
-
-        enableInterface(iface).expectResult(iface.name)
-        listener.expectCallback(iface, STATE_LINK_UP, ROLE_CLIENT)
     }
 
     @Test
diff --git a/tests/cts/net/src/android/net/cts/IpSecManagerTest.java b/tests/cts/net/src/android/net/cts/IpSecManagerTest.java
index a40ed0f..b703f77 100644
--- a/tests/cts/net/src/android/net/cts/IpSecManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/IpSecManagerTest.java
@@ -81,6 +81,7 @@
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.modules.utils.build.SdkLevel;
+import com.android.testutils.ConnectivityModuleTest;
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
 import com.android.testutils.SkipMainlinePresubmit;
@@ -101,6 +102,7 @@
 import java.util.Map.Entry;
 import java.util.Set;
 
+@ConnectivityModuleTest
 @RunWith(AndroidJUnit4.class)
 @AppModeFull(reason = "Socket cannot bind in instant app mode")
 public class IpSecManagerTest extends IpSecBaseTest {
@@ -444,6 +446,11 @@
             long uidTxDelta = 0;
             long uidRxDelta = 0;
             for (int i = 0; i < 100; i++) {
+                // Clear TrafficStats cache is needed to avoid rate-limit caching for
+                // TrafficStats API results on V+ devices.
+                if (SdkLevel.isAtLeastV()) {
+                    runAsShell(NETWORK_SETTINGS, () -> TrafficStats.clearRateLimitCaches());
+                }
                 uidTxDelta = TrafficStats.getUidTxPackets(Os.getuid()) - uidTxPackets;
                 uidRxDelta = TrafficStats.getUidRxPackets(Os.getuid()) - uidRxPackets;
 
@@ -518,6 +525,11 @@
         }
 
         private static void initStatsChecker() throws Exception {
+            // Clear TrafficStats cache is needed to avoid rate-limit caching for
+            // TrafficStats API results on V+ devices.
+            if (SdkLevel.isAtLeastV()) {
+                runAsShell(NETWORK_SETTINGS, () -> TrafficStats.clearRateLimitCaches());
+            }
             uidTxBytes = TrafficStats.getUidTxBytes(Os.getuid());
             uidRxBytes = TrafficStats.getUidRxBytes(Os.getuid());
             uidTxPackets = TrafficStats.getUidTxPackets(Os.getuid());
diff --git a/tests/cts/net/src/android/net/cts/MultinetworkApiTest.java b/tests/cts/net/src/android/net/cts/MultinetworkApiTest.java
index 73f65e0..06a827b 100644
--- a/tests/cts/net/src/android/net/cts/MultinetworkApiTest.java
+++ b/tests/cts/net/src/android/net/cts/MultinetworkApiTest.java
@@ -265,6 +265,14 @@
      * Get all testable Networks with internet capability.
      */
     private Set<Network> getTestableNetworks() throws InterruptedException {
+        // Calling requestNetwork() to request a cell or Wi-Fi network via CtsNetUtils or
+        // NetworkCallbackRule requires the CHANGE_NETWORK_STATE permission. This permission cannot
+        // be granted to instant apps. Therefore, return currently available testable networks
+        // directly in instant mode.
+        if (mContext.getApplicationInfo().isInstantApp()) {
+            return new ArraySet<>(mCtsNetUtils.getTestableNetworks());
+        }
+
         // Obtain cell and Wi-Fi through CtsNetUtils (which uses NetworkCallbacks), as they may have
         // just been reconnected by the test using NetworkCallbacks, so synchronous calls may not
         // yet return them (synchronous calls and callbacks should not be mixed for a given
diff --git a/tests/integration/src/com/android/server/net/integrationtests/NetworkStatsIntegrationTest.kt b/tests/integration/src/com/android/server/net/integrationtests/NetworkStatsIntegrationTest.kt
index 52e502d..4780c5d 100644
--- a/tests/integration/src/com/android/server/net/integrationtests/NetworkStatsIntegrationTest.kt
+++ b/tests/integration/src/com/android/server/net/integrationtests/NetworkStatsIntegrationTest.kt
@@ -38,6 +38,7 @@
 import android.os.Build
 import android.os.Process
 import androidx.test.platform.app.InstrumentationRegistry
+import com.android.modules.utils.build.SdkLevel
 import com.android.server.net.integrationtests.NetworkStatsIntegrationTest.Direction.DOWNLOAD
 import com.android.server.net.integrationtests.NetworkStatsIntegrationTest.Direction.UPLOAD
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
@@ -214,6 +215,11 @@
         // In practice, for one way 10k download payload, the download usage is about
         // 11222~12880 bytes, with 14~17 packets. And the upload usage is about 1279~1626 bytes
         // with 14~17 packets, which is majorly contributed by TCP ACK packets.
+        // Clear TrafficStats cache is needed to avoid rate-limit caching for
+        // TrafficStats API results on V+ devices.
+        if (SdkLevel.isAtLeastV()) {
+            TrafficStats.clearRateLimitCaches()
+        }
         val snapshotAfterDownload = StatsSnapshot(context, internalInterfaceName)
         val (expectedDownloadLower, expectedDownloadUpper) = getExpectedStatsBounds(
             TEST_DOWNLOAD_SIZE,
@@ -236,6 +242,9 @@
         )
 
         // Verify upload data usage accounting.
+        if (SdkLevel.isAtLeastV()) {
+            TrafficStats.clearRateLimitCaches()
+        }
         val snapshotAfterUpload = StatsSnapshot(context, internalInterfaceName)
         val (expectedUploadLower, expectedUploadUpper) = getExpectedStatsBounds(
             TEST_UPLOAD_SIZE,
diff --git a/tests/unit/java/com/android/metrics/NetworkNsdReportedMetricsTest.kt b/tests/unit/java/com/android/metrics/NetworkNsdReportedMetricsTest.kt
index 3f6e88d..aa28e5a 100644
--- a/tests/unit/java/com/android/metrics/NetworkNsdReportedMetricsTest.kt
+++ b/tests/unit/java/com/android/metrics/NetworkNsdReportedMetricsTest.kt
@@ -231,8 +231,10 @@
         val clientId = 99
         val transactionId = 100
         val durationMs = 10L
+        val sentQueryCount = 10
         val metrics = NetworkNsdReportedMetrics(clientId, deps)
-        metrics.reportServiceResolutionStop(true /* isLegacy */, transactionId, durationMs)
+        metrics.reportServiceResolutionStop(
+                true /* isLegacy */, transactionId, durationMs, sentQueryCount)
 
         val eventCaptor = ArgumentCaptor.forClass(NetworkNsdReported::class.java)
         verify(deps).statsWrite(eventCaptor.capture())
@@ -243,6 +245,7 @@
             assertEquals(NsdEventType.NET_RESOLVE, it.type)
             assertEquals(MdnsQueryResult.MQR_SERVICE_RESOLUTION_STOP, it.queryResult)
             assertEquals(durationMs, it.eventDurationMillisec)
+            assertEquals(sentQueryCount, it.sentQueryCount)
         }
     }
 
diff --git a/tests/unit/java/com/android/server/ConnectivityServiceTest.java b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
index 7822fe0..f9a35fe 100755
--- a/tests/unit/java/com/android/server/ConnectivityServiceTest.java
+++ b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
@@ -457,6 +457,7 @@
 import java.io.IOException;
 import java.io.PrintWriter;
 import java.io.StringWriter;
+import java.lang.reflect.Method;
 import java.net.DatagramSocket;
 import java.net.Inet4Address;
 import java.net.Inet6Address;
@@ -1721,8 +1722,6 @@
     private void mockUidNetworkingBlocked() {
         doAnswer(i -> isUidBlocked(mBlockedReasons, i.getArgument(1))
         ).when(mNetworkPolicyManager).isUidNetworkingBlocked(anyInt(), anyBoolean());
-        doAnswer(i -> isUidBlocked(mBlockedReasons, i.getArgument(1))
-        ).when(mBpfNetMaps).isUidNetworkingBlocked(anyInt(), anyBoolean());
     }
 
     private boolean isUidBlocked(int blockedReasons, boolean meteredNetwork) {
@@ -19190,6 +19189,25 @@
         verifyClatdStop(null /* inOrder */, MOBILE_IFNAME);
     }
 
-    // Note : adding tests is ConnectivityServiceTest is deprecated, as it is too big for
+    private static final int EXPECTED_TEST_METHOD_COUNT = 332;
+
+    @Test
+    public void testTestMethodCount() {
+        final Class<?> testClass = this.getClass();
+
+        int actualTestMethodCount = 0;
+        for (final Method method : testClass.getDeclaredMethods()) {
+            if (method.isAnnotationPresent(Test.class)) {
+                actualTestMethodCount++;
+            }
+        }
+
+        assertEquals("Adding tests in ConnectivityServiceTest is deprecated, "
+                + "as it is too big for maintenance. Please consider adding new tests "
+                + "in subclasses of CSTest instead.",
+                EXPECTED_TEST_METHOD_COUNT, actualTestMethodCount);
+    }
+
+    // Note : adding tests in ConnectivityServiceTest is deprecated, as it is too big for
     // maintenance. Please consider adding new tests in subclasses of CSTest instead.
 }
diff --git a/tests/unit/java/com/android/server/NsdServiceTest.java b/tests/unit/java/com/android/server/NsdServiceTest.java
index d91e29c..aece3f7 100644
--- a/tests/unit/java/com/android/server/NsdServiceTest.java
+++ b/tests/unit/java/com/android/server/NsdServiceTest.java
@@ -41,6 +41,7 @@
 import static com.android.server.NsdService.DEFAULT_RUNNING_APP_ACTIVE_IMPORTANCE_CUTOFF;
 import static com.android.server.NsdService.MdnsListener;
 import static com.android.server.NsdService.NO_TRANSACTION;
+import static com.android.server.NsdService.checkHostname;
 import static com.android.server.NsdService.parseTypeAndSubtype;
 import static com.android.testutils.ContextUtils.mockService;
 
@@ -53,6 +54,7 @@
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertFalse;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.argThat;
@@ -904,7 +906,7 @@
                 request.getServiceName().equals(ns.getServiceName())
                         && request.getServiceType().equals(ns.getServiceType())));
         verify(mMetrics).reportServiceResolutionStop(
-                true /* isLegacy */, resolveId, 10L /* durationMs */);
+                true /* isLegacy */, resolveId, 10L /* durationMs */, 0 /* sentQueryCount */);
     }
 
     @Test
@@ -978,7 +980,7 @@
                 request.getServiceName().equals(ns.getServiceName())
                         && request.getServiceType().equals(ns.getServiceType())));
         verify(mMetrics).reportServiceResolutionStop(
-                true /* isLegacy */, getAddrId, 10L /* durationMs */);
+                true /* isLegacy */, getAddrId, 10L /* durationMs */,  0 /* sentQueryCount */);
     }
 
     private void verifyUpdatedServiceInfo(NsdServiceInfo info, String serviceName,
@@ -1679,20 +1681,23 @@
         // Subtypes are not used for resolution, only for discovery
         assertEquals(Collections.emptyList(), optionsCaptor.getValue().getSubtypes());
 
+        final MdnsListener listener = listenerCaptor.getValue();
+        // Callbacks for query sent.
+        listener.onDiscoveryQuerySent(Collections.emptyList(), 1 /* transactionId */);
+
         doReturn(TEST_TIME_MS + 10L).when(mClock).elapsedRealtime();
         client.stopServiceResolution(resolveListener);
         waitForIdle();
 
         // Verify the listener has been unregistered.
-        final MdnsListener listener = listenerCaptor.getValue();
         verify(mDiscoveryManager, timeout(TIMEOUT_MS))
                 .unregisterListener(eq(constructedServiceType), eq(listener));
         verify(resolveListener, timeout(TIMEOUT_MS)).onResolutionStopped(argThat(ns ->
                 request.getServiceName().equals(ns.getServiceName())
                         && request.getServiceType().equals(ns.getServiceType())));
         verify(mSocketProvider, timeout(CLEANUP_DELAY_MS + TIMEOUT_MS)).requestStopWhenInactive();
-        verify(mMetrics).reportServiceResolutionStop(
-                false /* isLegacy */, listener.mTransactionId, 10L /* durationMs */);
+        verify(mMetrics).reportServiceResolutionStop(false /* isLegacy */, listener.mTransactionId,
+                10L /* durationMs */, 1 /* sentQueryCount */);
     }
 
     @Test
@@ -1723,6 +1728,36 @@
     }
 
     @Test
+    public void TestCheckHostname() {
+        // Valid cases
+        assertTrue(checkHostname(null));
+        assertTrue(checkHostname("a"));
+        assertTrue(checkHostname("1"));
+        assertTrue(checkHostname("a-1234-bbbb-cccc000"));
+        assertTrue(checkHostname("A-1234-BBbb-CCCC000"));
+        assertTrue(checkHostname("1234-bbbb-cccc000"));
+        assertTrue(checkHostname("0123456789abcdef"
+                                + "0123456789abcdef"
+                                + "0123456789abcdef"
+                                + "0123456789abcde" // 63 characters
+                        ));
+
+        // Invalid cases
+        assertFalse(checkHostname("?"));
+        assertFalse(checkHostname("/"));
+        assertFalse(checkHostname("a-"));
+        assertFalse(checkHostname("B-"));
+        assertFalse(checkHostname("-A"));
+        assertFalse(checkHostname("-b"));
+        assertFalse(checkHostname("-1-"));
+        assertFalse(checkHostname("0123456789abcdef"
+                                + "0123456789abcdef"
+                                + "0123456789abcdef"
+                                + "0123456789abcdef" // 64 characters
+                        ));
+    }
+
+    @Test
     @EnableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
     public void testEnablePlatformMdnsBackend() {
         final NsdManager client = connectClient(mService);
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt
index 271cc65..f7e0b0e 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt
@@ -24,7 +24,6 @@
 import com.android.server.connectivity.mdns.MdnsAnnouncer.AnnouncementInfo
 import com.android.server.connectivity.mdns.MdnsInterfaceAdvertiser.CONFLICT_HOST
 import com.android.server.connectivity.mdns.MdnsInterfaceAdvertiser.CONFLICT_SERVICE
-import com.android.server.connectivity.mdns.MdnsProber.ProbingInfo
 import com.android.server.connectivity.mdns.MdnsRecord.TYPE_A
 import com.android.server.connectivity.mdns.MdnsRecord.TYPE_AAAA
 import com.android.server.connectivity.mdns.MdnsRecord.TYPE_PTR
@@ -52,10 +51,6 @@
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.mockito.ArgumentCaptor
-import org.mockito.ArgumentMatchers.eq
-import org.mockito.Mockito.mock
-import org.mockito.Mockito.verify
 
 private const val TEST_SERVICE_ID_1 = 42
 private const val TEST_SERVICE_ID_2 = 43
@@ -132,10 +127,23 @@
     private val deps = object : Dependencies() {
         override fun getInterfaceInetAddresses(iface: NetworkInterface) =
                 Collections.enumeration(TEST_ADDRESSES.map { it.address })
+
+        override fun elapsedRealTime() = now
+
+        fun elapse(duration: Long) {
+            now += duration
+        }
+
+        fun resetElapsedRealTime() {
+            now = 100
+        }
+
+        var now: Long = 100
     }
 
     @Before
     fun setUp() {
+        deps.resetElapsedRealTime();
         thread.start()
     }
 
@@ -1003,6 +1011,102 @@
     }
 
     @Test
+    fun testGetReply_ipv4AndIpv6Queries_ipv4AndIpv6Replies() {
+        val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+        repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1, setOf(TEST_SUBTYPE))
+        val query = makeQuery(TYPE_PTR to arrayOf("_testservice", "_tcp", "local"))
+
+        val srcIpv4 = InetSocketAddress(parseNumericAddress("192.0.2.123"), 5353)
+        val replyIpv4 = repository.getReply(query, srcIpv4)
+        val srcIpv6 = InetSocketAddress(parseNumericAddress("2001:db8::123"), 5353)
+        val replyIpv6 = repository.getReply(query, srcIpv6)
+
+        assertNotNull(replyIpv4)
+        assertEquals(MdnsConstants.getMdnsIPv4Address(), replyIpv4.destination.address)
+        assertEquals(MdnsConstants.MDNS_PORT, replyIpv4.destination.port)
+        assertNotNull(replyIpv6)
+        assertEquals(MdnsConstants.getMdnsIPv6Address(), replyIpv6.destination.address)
+        assertEquals(MdnsConstants.MDNS_PORT, replyIpv6.destination.port)
+    }
+
+    @Test
+    fun testGetReply_twoIpv4QueriesInOneSecond_theSecondReplyIsThrottled() {
+        val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+        repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1, setOf(TEST_SUBTYPE))
+        val query = makeQuery(TYPE_PTR to arrayOf("_testservice", "_tcp", "local"))
+
+        val srcIpv4 = InetSocketAddress(parseNumericAddress("192.0.2.123"), 5353)
+        val firstReplyIpv4 = repository.getReply(query, srcIpv4)
+        deps.elapse(500L)
+        val secondReply = repository.getReply(query, srcIpv4)
+
+        assertNotNull(firstReplyIpv4)
+        assertEquals(MdnsConstants.getMdnsIPv4Address(), firstReplyIpv4.destination.address)
+        assertEquals(MdnsConstants.MDNS_PORT, firstReplyIpv4.destination.port)
+        assertNull(secondReply)
+    }
+
+
+    @Test
+    fun testGetReply_twoIpv6QueriesInOneSecond_theSecondReplyIsThrottled() {
+        val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+        repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1, setOf(TEST_SUBTYPE))
+        val query = makeQuery(TYPE_PTR to arrayOf("_testservice", "_tcp", "local"))
+
+        val srcIpv6 = InetSocketAddress(parseNumericAddress("2001:db8::123"), 5353)
+        val firstReplyIpv6 = repository.getReply(query, srcIpv6)
+        deps.elapse(500L)
+        val secondReply = repository.getReply(query, srcIpv6)
+
+        assertNotNull(firstReplyIpv6)
+        assertEquals(MdnsConstants.getMdnsIPv6Address(), firstReplyIpv6.destination.address)
+        assertEquals(MdnsConstants.MDNS_PORT, firstReplyIpv6.destination.port)
+        assertNull(secondReply)
+    }
+
+    @Test
+    fun testGetReply_twoIpv4QueriesInMoreThanOneSecond_repliesAreNotThrottled() {
+        val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+        repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1, setOf(TEST_SUBTYPE))
+        val query = makeQuery(TYPE_PTR to arrayOf("_testservice", "_tcp", "local"))
+
+        val srcIpv4 = InetSocketAddress(parseNumericAddress("192.0.2.123"), 5353)
+        val firstReplyIpv4 = repository.getReply(query, srcIpv4)
+        // The longest possible interval that may make the reply throttled is
+        // 1000 (MIN_MULTICAST_REPLY_INTERVAL_MS) + 120 (delay for shared name) = 1120
+        deps.elapse(1121L)
+        val secondReplyIpv4 = repository.getReply(query, srcIpv4)
+
+        assertNotNull(firstReplyIpv4)
+        assertEquals(MdnsConstants.getMdnsIPv4Address(), firstReplyIpv4.destination.address)
+        assertEquals(MdnsConstants.MDNS_PORT, firstReplyIpv4.destination.port)
+        assertNotNull(secondReplyIpv4)
+        assertEquals(MdnsConstants.getMdnsIPv4Address(), secondReplyIpv4.destination.address)
+        assertEquals(MdnsConstants.MDNS_PORT, secondReplyIpv4.destination.port)
+    }
+
+    @Test
+    fun testGetReply_twoIpv6QueriesInMoreThanOneSecond_repliesAreNotThrottled() {
+        val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+        repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1, setOf(TEST_SUBTYPE))
+        val query = makeQuery(TYPE_PTR to arrayOf("_testservice", "_tcp", "local"))
+
+        val srcIpv6 = InetSocketAddress(parseNumericAddress("2001:db8::123"), 5353)
+        val firstReplyIpv6 = repository.getReply(query, srcIpv6)
+        // The longest possible interval that may make the reply throttled is
+        // 1000 (MIN_MULTICAST_REPLY_INTERVAL_MS) + 120 (delay for shared name) = 1120
+        deps.elapse(1121L)
+        val secondReplyIpv6 = repository.getReply(query, srcIpv6)
+
+        assertNotNull(firstReplyIpv6)
+        assertEquals(MdnsConstants.getMdnsIPv6Address(), firstReplyIpv6.destination.address)
+        assertEquals(MdnsConstants.MDNS_PORT, firstReplyIpv6.destination.port)
+        assertNotNull(secondReplyIpv6)
+        assertEquals(MdnsConstants.getMdnsIPv6Address(), secondReplyIpv6.destination.address)
+        assertEquals(MdnsConstants.MDNS_PORT, secondReplyIpv6.destination.port)
+    }
+
+    @Test
     fun testGetConflictingServices() {
         val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
         repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* ttl */)
diff --git a/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java b/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
index d4f5619..6425daa 100644
--- a/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
+++ b/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
@@ -67,11 +67,14 @@
 import static com.android.server.net.NetworkStatsEventLogger.POLL_REASON_RAT_CHANGED;
 import static com.android.server.net.NetworkStatsEventLogger.PollEvent.pollReasonNameOf;
 import static com.android.server.net.NetworkStatsService.ACTION_NETWORK_STATS_POLL;
+import static com.android.server.net.NetworkStatsService.DEFAULT_TRAFFIC_STATS_CACHE_EXPIRY_DURATION_MS;
+import static com.android.server.net.NetworkStatsService.DEFAULT_TRAFFIC_STATS_CACHE_MAX_ENTRIES;
 import static com.android.server.net.NetworkStatsService.NETSTATS_FASTDATAINPUT_FALLBACKS_COUNTER_NAME;
 import static com.android.server.net.NetworkStatsService.NETSTATS_FASTDATAINPUT_SUCCESSES_COUNTER_NAME;
 import static com.android.server.net.NetworkStatsService.NETSTATS_IMPORT_ATTEMPTS_COUNTER_NAME;
 import static com.android.server.net.NetworkStatsService.NETSTATS_IMPORT_FALLBACKS_COUNTER_NAME;
 import static com.android.server.net.NetworkStatsService.NETSTATS_IMPORT_SUCCESSES_COUNTER_NAME;
+import static com.android.server.net.NetworkStatsService.TRAFFICSTATS_RATE_LIMIT_CACHE_ENABLED_FLAG;
 import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
 
 import static org.junit.Assert.assertEquals;
@@ -116,6 +119,7 @@
 import android.net.TestNetworkSpecifier;
 import android.net.TetherStatsParcel;
 import android.net.TetheringManager;
+import android.net.TrafficStats;
 import android.net.UnderlyingNetworkInfo;
 import android.net.netstats.provider.INetworkStatsProviderCallback;
 import android.net.wifi.WifiInfo;
@@ -125,6 +129,7 @@
 import android.os.IBinder;
 import android.os.Looper;
 import android.os.PowerManager;
+import android.os.Process;
 import android.os.SimpleClock;
 import android.provider.Settings;
 import android.system.ErrnoException;
@@ -159,12 +164,15 @@
 import com.android.testutils.HandlerUtils;
 import com.android.testutils.TestBpfMap;
 import com.android.testutils.TestableNetworkStatsProviderBinder;
+import com.android.testutils.com.android.testutils.SetFeatureFlagsRule;
+import com.android.testutils.com.android.testutils.SetFeatureFlagsRule.FeatureFlag;
 
 import libcore.testing.io.TestIoUtils;
 
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Ignore;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
@@ -189,6 +197,7 @@
 import java.util.Set;
 import java.util.concurrent.Executor;
 import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.Function;
 
 /**
  * Tests for {@link NetworkStatsService}.
@@ -202,6 +211,7 @@
 // NetworkStatsService is not updatable before T, so tests do not need to be backwards compatible
 @DevSdkIgnoreRule.IgnoreUpTo(SC_V2)
 public class NetworkStatsServiceTest extends NetworkStatsBaseTest {
+
     private static final String TAG = "NetworkStatsServiceTest";
 
     private static final long TEST_START = 1194220800000L;
@@ -295,6 +305,16 @@
     private Boolean mIsDebuggable;
     private HandlerThread mObserverHandlerThread;
     final TestDependencies mDeps = new TestDependencies();
+    final HashMap<String, Boolean> mFeatureFlags = new HashMap<>();
+
+    // This will set feature flags from @FeatureFlag annotations
+    // into the map before setUp() runs.
+    @Rule
+    public final SetFeatureFlagsRule mSetFeatureFlagsRule =
+            new SetFeatureFlagsRule((name, enabled) -> {
+                mFeatureFlags.put(name, enabled);
+                return null;
+            });
 
     private class MockContext extends BroadcastInterceptingContext {
         private final Context mBaseContext;
@@ -395,8 +415,6 @@
 
         mElapsedRealtime = 0L;
 
-        mockDefaultSettings();
-        mockNetworkStatsUidDetail(buildEmptyStats());
         prepareForSystemReady();
         mService.systemReady();
         // Verify that system ready fetches realtime stats
@@ -432,6 +450,7 @@
 
     class TestDependencies extends NetworkStatsService.Dependencies {
         private int mCompareStatsInvocation = 0;
+        private NetworkStats.Entry mMockedTrafficStatsNativeStat = null;
 
         @Override
         public File getLegacyStatsDir() {
@@ -573,6 +592,43 @@
         public boolean supportEventLogger(@NonNull Context cts) {
             return true;
         }
+
+        @Override
+        public boolean supportTrafficStatsRateLimitCache(Context ctx) {
+            return mFeatureFlags.getOrDefault(TRAFFICSTATS_RATE_LIMIT_CACHE_ENABLED_FLAG, false);
+        }
+
+        @Override
+        public int getTrafficStatsRateLimitCacheExpiryDuration() {
+            return DEFAULT_TRAFFIC_STATS_CACHE_EXPIRY_DURATION_MS;
+        }
+
+        @Override
+        public int getTrafficStatsRateLimitCacheMaxEntries() {
+            return DEFAULT_TRAFFIC_STATS_CACHE_MAX_ENTRIES;
+        }
+
+        @Nullable
+        @Override
+        public NetworkStats.Entry nativeGetTotalStat() {
+            return mMockedTrafficStatsNativeStat;
+        }
+
+        @Nullable
+        @Override
+        public NetworkStats.Entry nativeGetIfaceStat(String iface) {
+            return mMockedTrafficStatsNativeStat;
+        }
+
+        @Nullable
+        @Override
+        public NetworkStats.Entry nativeGetUidStat(int uid) {
+            return mMockedTrafficStatsNativeStat;
+        }
+
+        public void setNativeStat(NetworkStats.Entry entry) {
+            mMockedTrafficStatsNativeStat = entry;
+        }
     }
 
     @After
@@ -729,8 +785,6 @@
         assertStatsFilesExist(true);
 
         // boot through serviceReady() again
-        mockDefaultSettings();
-        mockNetworkStatsUidDetail(buildEmptyStats());
         prepareForSystemReady();
 
         mService.systemReady();
@@ -2111,8 +2165,6 @@
                 getLegacyCollection(PREFIX_UID_TAG, true /* includeTags */));
 
         // Mock zero usage and boot through serviceReady(), verify there is no imported data.
-        mockDefaultSettings();
-        mockNetworkStatsUidDetail(buildEmptyStats());
         prepareForSystemReady();
         mService.systemReady();
         assertStatsFilesExist(false);
@@ -2124,8 +2176,6 @@
         assertStatsFilesExist(false);
 
         // Boot through systemReady() again.
-        mockDefaultSettings();
-        mockNetworkStatsUidDetail(buildEmptyStats());
         prepareForSystemReady();
         mService.systemReady();
 
@@ -2199,8 +2249,6 @@
                 getLegacyCollection(PREFIX_UID_TAG, true /* includeTags */));
 
         // Mock zero usage and boot through serviceReady(), verify there is no imported data.
-        mockDefaultSettings();
-        mockNetworkStatsUidDetail(buildEmptyStats());
         prepareForSystemReady();
         mService.systemReady();
         assertStatsFilesExist(false);
@@ -2212,8 +2260,6 @@
         assertStatsFilesExist(false);
 
         // Boot through systemReady() again.
-        mockDefaultSettings();
-        mockNetworkStatsUidDetail(buildEmptyStats());
         prepareForSystemReady();
         mService.systemReady();
 
@@ -2365,6 +2411,68 @@
         assertUidTotal(sTemplateWifi, UID_GREEN, 64L, 3L, 1024L, 8L, 0);
     }
 
+    @FeatureFlag(name = TRAFFICSTATS_RATE_LIMIT_CACHE_ENABLED_FLAG, enabled = false)
+    @Test
+    public void testTrafficStatsRateLimitCache_disabled() throws Exception {
+        doTestTrafficStatsRateLimitCache(false /* cacheEnabled */);
+    }
+
+    @FeatureFlag(name = TRAFFICSTATS_RATE_LIMIT_CACHE_ENABLED_FLAG)
+    @Test
+    public void testTrafficStatsRateLimitCache_enabled() throws Exception {
+        doTestTrafficStatsRateLimitCache(true /* cacheEnabled */);
+    }
+
+    private void doTestTrafficStatsRateLimitCache(boolean cacheEnabled) throws Exception {
+        mockDefaultSettings();
+        // Calling uid is not injected into the service, use the real uid to pass the caller check.
+        final int myUid = Process.myUid();
+        mockTrafficStatsValues(64L, 3L, 1024L, 8L);
+        assertTrafficStatsValues(TEST_IFACE, myUid, 64L, 3L, 1024L, 8L);
+
+        // Verify the values are cached.
+        incrementCurrentTime(DEFAULT_TRAFFIC_STATS_CACHE_EXPIRY_DURATION_MS / 2);
+        mockTrafficStatsValues(65L, 8L, 1055L, 9L);
+        if (cacheEnabled) {
+            assertTrafficStatsValues(TEST_IFACE, myUid, 64L, 3L, 1024L, 8L);
+        } else {
+            assertTrafficStatsValues(TEST_IFACE, myUid, 65L, 8L, 1055L, 9L);
+        }
+
+        // Verify the values are updated after cache expiry.
+        incrementCurrentTime(DEFAULT_TRAFFIC_STATS_CACHE_EXPIRY_DURATION_MS);
+        assertTrafficStatsValues(TEST_IFACE, myUid, 65L, 8L, 1055L, 9L);
+    }
+
+    private void mockTrafficStatsValues(long rxBytes, long rxPackets,
+            long txBytes, long txPackets) {
+        // In practice, keys and operations are not used and filled with default values when
+        // returned by JNI layer.
+        final NetworkStats.Entry entry = new NetworkStats.Entry(IFACE_ALL, UID_ALL, SET_DEFAULT,
+                TAG_NONE, METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO,
+                rxBytes, rxPackets, txBytes, txPackets, 0L);
+        mDeps.setNativeStat(entry);
+    }
+
+    // Assert for 3 different API return values respectively.
+    private void assertTrafficStatsValues(String iface, int uid, long rxBytes, long rxPackets,
+            long txBytes, long txPackets) {
+        assertTrafficStatsValuesThat(rxBytes, rxPackets, txBytes, txPackets,
+                (type) -> mService.getTotalStats(type));
+        assertTrafficStatsValuesThat(rxBytes, rxPackets, txBytes, txPackets,
+                (type) -> mService.getIfaceStats(iface, type));
+        assertTrafficStatsValuesThat(rxBytes, rxPackets, txBytes, txPackets,
+                (type) -> mService.getUidStats(uid, type));
+    }
+
+    private void assertTrafficStatsValuesThat(long rxBytes, long rxPackets, long txBytes,
+            long txPackets, Function<Integer, Long> fetcher) {
+        assertEquals(rxBytes, (long) fetcher.apply(TrafficStats.TYPE_RX_BYTES));
+        assertEquals(rxPackets, (long) fetcher.apply(TrafficStats.TYPE_RX_PACKETS));
+        assertEquals(txBytes, (long) fetcher.apply(TrafficStats.TYPE_TX_BYTES));
+        assertEquals(txPackets, (long) fetcher.apply(TrafficStats.TYPE_TX_PACKETS));
+    }
+
     private void assertShouldRunComparison(boolean expected, boolean isDebuggable) {
         assertEquals("shouldRunComparison (debuggable=" + isDebuggable + "): ",
                 expected, mService.shouldRunComparison());
@@ -2429,6 +2537,8 @@
     }
 
     private void prepareForSystemReady() throws Exception {
+        mockDefaultSettings();
+        mockNetworkStatsUidDetail(buildEmptyStats());
         mockNetworkStatsSummary(buildEmptyStats());
     }
 
diff --git a/thread/service/java/com/android/server/thread/TunInterfaceController.java b/thread/service/java/com/android/server/thread/TunInterfaceController.java
index 97cdd55..dec72b2 100644
--- a/thread/service/java/com/android/server/thread/TunInterfaceController.java
+++ b/thread/service/java/com/android/server/thread/TunInterfaceController.java
@@ -16,6 +16,8 @@
 
 package com.android.server.thread;
 
+import static android.system.OsConstants.EADDRINUSE;
+
 import android.annotation.Nullable;
 import android.net.IpPrefix;
 import android.net.LinkAddress;
@@ -39,6 +41,10 @@
 import java.io.InterruptedIOException;
 import java.net.Inet6Address;
 import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.MulticastSocket;
+import java.net.NetworkInterface;
+import java.net.SocketException;
 import java.net.UnknownHostException;
 import java.util.ArrayList;
 import java.util.List;
@@ -58,12 +64,16 @@
     private ParcelFileDescriptor mParcelTunFd;
     private FileDescriptor mNetlinkSocket;
     private static int sNetlinkSeqNo = 0;
+    private final MulticastSocket mMulticastSocket; // For join group and leave group
+    private NetworkInterface mNetworkInterface;
+    private List<InetAddress> mMulticastAddresses = new ArrayList<>();
 
     /** Creates a new {@link TunInterfaceController} instance for given interface. */
     public TunInterfaceController(String interfaceName) {
         mIfName = interfaceName;
         mLinkProperties.setInterfaceName(mIfName);
         mLinkProperties.setMtu(MTU);
+        mMulticastSocket = createMulticastSocket();
     }
 
     /** Returns link properties of the Thread TUN interface. */
@@ -83,6 +93,11 @@
         } catch (ErrnoException e) {
             throw new IOException("Failed to create netlink socket", e);
         }
+        try {
+            mNetworkInterface = NetworkInterface.getByName(mIfName);
+        } catch (SocketException e) {
+            throw new IOException("Failed to get NetworkInterface", e);
+        }
     }
 
     public void destroyTunInterface() {
@@ -94,6 +109,7 @@
         }
         mParcelTunFd = null;
         mNetlinkSocket = null;
+        mNetworkInterface = null;
     }
 
     /** Returns the FD of the tunnel interface. */
@@ -187,6 +203,7 @@
 
     public void updateAddresses(List<Ipv6AddressInfo> addressInfoList) {
         final List<LinkAddress> newLinkAddresses = new ArrayList<>();
+        final List<InetAddress> newMulticastAddresses = new ArrayList<>();
         boolean hasActiveOmrAddress = false;
 
         for (Ipv6AddressInfo addressInfo : addressInfoList) {
@@ -199,12 +216,10 @@
         for (Ipv6AddressInfo addressInfo : addressInfoList) {
             InetAddress address = addressInfoToInetAddress(addressInfo);
             if (address.isMulticastAddress()) {
-                // TODO: Logging here will create repeated logs for a single multicast address, and
-                // it currently is not mandatory for debugging. Add log for ignored multicast
-                // address when necessary.
-                continue;
+                newMulticastAddresses.add(address);
+            } else {
+                newLinkAddresses.add(newLinkAddress(addressInfo, hasActiveOmrAddress));
             }
-            newLinkAddresses.add(newLinkAddress(addressInfo, hasActiveOmrAddress));
         }
 
         final CompareResult<LinkAddress> addressDiff =
@@ -215,6 +230,17 @@
         for (LinkAddress linkAddress : addressDiff.added) {
             addAddress(linkAddress);
         }
+
+        final CompareResult<InetAddress> multicastAddressDiff =
+                new CompareResult<>(mMulticastAddresses, newMulticastAddresses);
+        for (InetAddress address : multicastAddressDiff.removed) {
+            leaveGroup(address);
+        }
+        for (InetAddress address : multicastAddressDiff.added) {
+            joinGroup(address);
+        }
+        mMulticastAddresses.clear();
+        mMulticastAddresses.addAll(newMulticastAddresses);
     }
 
     private RouteInfo getRouteForAddress(LinkAddress linkAddress) {
@@ -274,4 +300,37 @@
                 deprecationTimeMillis,
                 LinkAddress.LIFETIME_PERMANENT /* expirationTime */);
     }
+
+    private MulticastSocket createMulticastSocket() {
+        try {
+            return new MulticastSocket();
+        } catch (IOException e) {
+            throw new IllegalStateException("Failed to create multicast socket ", e);
+        }
+    }
+
+    private void joinGroup(InetAddress address) {
+        InetSocketAddress socketAddress = new InetSocketAddress(address, 0);
+        try {
+            mMulticastSocket.joinGroup(socketAddress, mNetworkInterface);
+        } catch (IOException e) {
+            if (e.getCause() instanceof ErrnoException) {
+                ErrnoException ee = (ErrnoException) e.getCause();
+                if (ee.errno == EADDRINUSE) {
+                    Log.w(TAG, "Already joined group" + address.getHostAddress(), e);
+                    return;
+                }
+            }
+            Log.e(TAG, "failed to join group " + address.getHostAddress(), e);
+        }
+    }
+
+    private void leaveGroup(InetAddress address) {
+        InetSocketAddress socketAddress = new InetSocketAddress(address, 0);
+        try {
+            mMulticastSocket.leaveGroup(socketAddress, mNetworkInterface);
+        } catch (IOException e) {
+            Log.e(TAG, "failed to leave group " + address.getHostAddress(), e);
+        }
+    }
 }
diff --git a/thread/tests/integration/Android.bp b/thread/tests/integration/Android.bp
index 94985b1..71693af 100644
--- a/thread/tests/integration/Android.bp
+++ b/thread/tests/integration/Android.bp
@@ -34,6 +34,7 @@
         "testables",
         "ThreadNetworkTestUtils",
         "truth",
+        "ot-daemon-aidl-java",
     ],
     libs: [
         "android.test.runner",
diff --git a/thread/tests/integration/src/android/net/thread/ServiceDiscoveryTest.java b/thread/tests/integration/src/android/net/thread/ServiceDiscoveryTest.java
index 5a8d21f..e10f134 100644
--- a/thread/tests/integration/src/android/net/thread/ServiceDiscoveryTest.java
+++ b/thread/tests/integration/src/android/net/thread/ServiceDiscoveryTest.java
@@ -265,6 +265,7 @@
     }
 
     @Test
+    @Ignore("TODO: b/333806992 - Enable when it's not flaky at all")
     public void advertisingProxy_srpClientUnregistersService_serviceIsNotDiscoverableByMdns()
             throws Exception {
         /*
diff --git a/thread/tests/integration/src/android/net/thread/ThreadIntegrationTest.java b/thread/tests/integration/src/android/net/thread/ThreadIntegrationTest.java
index 1410d41..e211e22 100644
--- a/thread/tests/integration/src/android/net/thread/ThreadIntegrationTest.java
+++ b/thread/tests/integration/src/android/net/thread/ThreadIntegrationTest.java
@@ -22,15 +22,18 @@
 import static android.net.thread.utils.IntegrationTestUtils.CALLBACK_TIMEOUT;
 import static android.net.thread.utils.IntegrationTestUtils.RESTART_JOIN_TIMEOUT;
 import static android.net.thread.utils.IntegrationTestUtils.getIpv6LinkAddresses;
+import static android.net.thread.utils.IntegrationTestUtils.isInMulticastGroup;
 import static android.net.thread.utils.IntegrationTestUtils.waitFor;
 
 import static com.android.compatibility.common.util.SystemUtil.runShellCommand;
 import static com.android.compatibility.common.util.SystemUtil.runShellCommandOrThrow;
+import static com.android.server.thread.openthread.IOtDaemon.TUN_IF_NAME;
 
 import static com.google.common.io.BaseEncoding.base16;
 import static com.google.common.truth.Truth.assertThat;
 
 import android.content.Context;
+import android.net.InetAddresses;
 import android.net.IpPrefix;
 import android.net.LinkAddress;
 import android.net.thread.utils.FullThreadDevice;
@@ -83,6 +86,9 @@
     private static final ActiveOperationalDataset DEFAULT_DATASET =
             ActiveOperationalDataset.fromThreadTlvs(DEFAULT_DATASET_TLVS);
 
+    private static final Inet6Address GROUP_ADDR_ALL_ROUTERS =
+            (Inet6Address) InetAddresses.parseNumericAddress("ff02::2");
+
     @Rule public final ThreadFeatureCheckerRule mThreadRule = new ThreadFeatureCheckerRule();
 
     private ExecutorService mExecutor;
@@ -224,6 +230,13 @@
         mOtCtl.executeCommand("br enable");
     }
 
+    @Test
+    public void joinNetwork_tunInterfaceJoinsAllRouterMulticastGroup() throws Exception {
+        mController.joinAndWait(DEFAULT_DATASET);
+
+        assertTunInterfaceMemberOfGroup(GROUP_ADDR_ALL_ROUTERS);
+    }
+
     // TODO (b/323300829): add more tests for integration with linux platform and
     // ConnectivityService
 
@@ -259,4 +272,8 @@
             throw new IllegalStateException(e);
         }
     }
+
+    private void assertTunInterfaceMemberOfGroup(Inet6Address address) throws Exception {
+        waitFor(() -> isInMulticastGroup(TUN_IF_NAME, address), TUN_ADDR_UPDATE_TIMEOUT);
+    }
 }