Merge changes I7fae1964,Ic7cc9e1d into main

* changes:
  Add carrier-privileged app bypass for start/stopTethering
  Fix CarrierConfigRule#setHoldCarrierPrivilege race
diff --git a/bpf/loader/NetBpfLoad.cpp b/bpf/loader/NetBpfLoad.cpp
index d70a2c8..63de1a6 100644
--- a/bpf/loader/NetBpfLoad.cpp
+++ b/bpf/loader/NetBpfLoad.cpp
@@ -556,9 +556,9 @@
         vector<string> csSymNames;
         ret = getSectionSymNames(elfFile, oldName, csSymNames, STT_FUNC);
         if (ret || !csSymNames.size()) return ret;
-        for (size_t i = 0; i < progDefNames.size(); ++i) {
-            if (!progDefNames[i].compare(csSymNames[0] + "_def")) {
-                cs_temp.prog_def = pd[i];
+        for (size_t j = 0; j < progDefNames.size(); ++j) {
+            if (!progDefNames[j].compare(csSymNames[0] + "_def")) {
+                cs_temp.prog_def = pd[j];
                 break;
             }
         }
@@ -769,7 +769,7 @@
     const size_t max_name = 256;
     char kvTypeName[max_name];
     int64_t keySize, valueSize;
-    uint32_t kvId;
+    int32_t kvId;
 
     if (snprintf(kvTypeName, max_name, "____btf_map_%s", mapName) == max_name) {
         ALOGE("____btf_map_%s is too long", mapName);
@@ -858,14 +858,16 @@
 
     struct btf *btf = NULL;
     auto scopeGuard = base::make_scope_guard([btf] { if (btf) btf__free(btf); });
-    if (isAtLeastKernelVersion(4, 18, 0)) {
+    if (isAtLeastKernelVersion(5, 10, 0)) {
+        // Untested on Linux Kernel 5.4, but likely compatible.
         // On Linux Kernels older than 4.18 BPF_BTF_LOAD command doesn't exist.
+        // On Linux Kernels older than 5.2 BTF_KIND_VAR and BTF_KIND_DATASEC don't exist.
         ret = readSectionByName(".BTF", elfFile, btfData);
         if (ret) {
             ALOGE("Failed to read .BTF section, ret:%d", ret);
             return ret;
         }
-        struct btf *btf = btf__new(btfData.data(), btfData.size());
+        btf = btf__new(btfData.data(), btfData.size());
         if (btf == NULL) {
             ALOGE("btf__new failed, errno: %d", errno);
             return -errno;
diff --git a/bpf/loader/netbpfload.rc b/bpf/loader/netbpfload.rc
index 10bfbb2..4cc6284 100644
--- a/bpf/loader/netbpfload.rc
+++ b/bpf/loader/netbpfload.rc
@@ -1,3 +1,5 @@
+# 2025 2 36 0 0 # 25q2 sdk/api level 36.0 - Android 16 Baklava QPR0
+
 # Note: This will actually execute /apex/com.android.tethering/bin/netbpfload
 # by virtue of 'service bpfloader' being overridden by the apex shipped .rc
 # Warning: most of the below settings are irrelevant unless the apex is missing.
diff --git a/bpf/netd/BpfHandler.cpp b/bpf/netd/BpfHandler.cpp
index d41aa81..680c05e 100644
--- a/bpf/netd/BpfHandler.cpp
+++ b/bpf/netd/BpfHandler.cpp
@@ -268,6 +268,16 @@
     RETURN_IF_NOT_OK(initMaps());
 
     if (isAtLeast25Q2) {
+        struct rlimit limit = {
+            .rlim_cur = 1u << 30,  // 1 GiB
+            .rlim_max = 1u << 30,  // 1 GiB
+        };
+        // 25Q2 netd.rc includes "rlimit memlock 1073741824 1073741824"
+        // so this should be a no-op, and thus just succeed.
+        // make sure it isn't lowered in platform netd.rc...
+        if (setrlimit(RLIMIT_MEMLOCK, &limit))
+            return statusFromErrno(errno, "Failed to set 1GiB RLIMIT_MEMLOCK");
+
         // Make sure netd can create & write maps.  sepolicy is V+, but enough to enforce on 25Q2+
         int key = 1;
         int value = 123;
diff --git a/common/src/com/android/net/module/util/bpf/LocalNetAccessKey.java b/common/src/com/android/net/module/util/bpf/LocalNetAccessKey.java
index 95265b9..48e8b06 100644
--- a/common/src/com/android/net/module/util/bpf/LocalNetAccessKey.java
+++ b/common/src/com/android/net/module/util/bpf/LocalNetAccessKey.java
@@ -61,12 +61,46 @@
 
     @Override
     public String toString() {
-        return "LocalNetAccessKey{"
-                + "lpmBitlen=" + lpmBitlen
-                + ", ifIndex=" + ifIndex
-                + ", remoteAddress=" + remoteAddress
-                + ", protocol=" + protocol
-                + ", remotePort=" + remotePort
-                + "}";
+        String s = "LocalNetAccessKey{lpmBitlen=" + lpmBitlen;
+
+        long bits = lpmBitlen;
+
+        // u32 ifIndex
+        if (bits <= 0 && ifIndex != 0) s += " ??";
+        if (bits > 0 || ifIndex != 0) s += " ifIndex=" + ifIndex;
+        if (bits > 0 && bits < 32) s += "/" + bits + "[LE]";
+        bits -= 32;
+
+        // u128 remoteAddress
+        if (bits <= 0 && !remoteAddress.isAnyLocalAddress()) s += " ??";
+        if (bits > 0 || !remoteAddress.isAnyLocalAddress()) {
+            s += " remoteAddress=";
+            String ip = remoteAddress.toString();
+            if (ip.startsWith("/::ffff:")) { // technically wrong IPv4-mapped IPv6 address detection
+              s += ip.substring(8);
+              if (bits >= 96 && bits < 128) s += "/" + (bits - 96);
+            } else if (ip.startsWith("/")) {
+              s += ip.substring(1);
+              if (bits >= 0 && bits < 128) s += "/" + bits;
+            } else { // WTF, includes a hostname or what?
+              s += ip;
+            }
+        }
+        bits -= 128;
+
+        // u16 protocol
+        if (bits <= 0 && protocol != 0) s += " ??";
+        if (bits > 0 || protocol != 0) s += " protocol=" + protocol;
+        if (bits > 0 && bits < 16) s += "/" + bits + "[LE16]";
+        bits -= 16;
+
+        // be16 remotePort
+        if (bits <= 0 && remotePort != 0) s += " ??";
+        if (bits > 0 || remotePort != 0) s += " remotePort=" + remotePort;
+        if (bits > 0 && bits < 16) s += "/" + bits + "[BE16]";
+        bits -= 16;
+
+        s += "}";
+        return s;
     }
 }
diff --git a/framework/src/android/net/NetworkCapabilities.java b/framework/src/android/net/NetworkCapabilities.java
index c6b62ee..8355d31 100644
--- a/framework/src/android/net/NetworkCapabilities.java
+++ b/framework/src/android/net/NetworkCapabilities.java
@@ -360,6 +360,8 @@
         mUnderlyingNetworks = null;
         mEnterpriseId = 0;
         mReservationId = RES_ID_UNSET;
+        // TODO: Change to default disabled when introduce this filtering.
+        mMatchNonThreadLocalNetworks = true;
     }
 
     /**
@@ -395,6 +397,7 @@
         mUnderlyingNetworks = nc.mUnderlyingNetworks;
         mEnterpriseId = nc.mEnterpriseId;
         mReservationId = nc.mReservationId;
+        mMatchNonThreadLocalNetworks = nc.mMatchNonThreadLocalNetworks;
     }
 
     /**
@@ -2236,7 +2239,8 @@
                 && (onlyImmutable || satisfiedBySSID(nc))
                 && (onlyImmutable || satisfiedByRequestor(nc))
                 && (onlyImmutable || satisfiedBySubscriptionIds(nc)))
-                && satisfiedByReservationId(nc);
+                && satisfiedByReservationId(nc)
+                && satisfiedByMatchNonThreadLocalNetworks(nc);
     }
 
     /**
@@ -2351,7 +2355,8 @@
                 && equalsSubscriptionIds(that)
                 && equalsUnderlyingNetworks(that)
                 && equalsEnterpriseCapabilitiesId(that)
-                && equalsReservationId(that);
+                && equalsReservationId(that)
+                && equalsMatchNonThreadLocalNetworks(that);
     }
 
     @Override
@@ -2371,15 +2376,15 @@
                 + Objects.hashCode(mAllowedUids) * 41
                 + Objects.hashCode(mSSID) * 43
                 + Objects.hashCode(mTransportInfo) * 47
-                + Objects.hashCode(mPrivateDnsBroken) * 53
+                + Boolean.hashCode(mPrivateDnsBroken) * 53
                 + Objects.hashCode(mRequestorUid) * 59
                 + Objects.hashCode(mRequestorPackageName) * 61
                 + Arrays.hashCode(mAdministratorUids) * 67
                 + Objects.hashCode(mSubIds) * 71
                 + Objects.hashCode(mUnderlyingNetworks) * 73
                 + mEnterpriseId * 79
-                + mReservationId * 83;
-
+                + mReservationId * 83
+                + Boolean.hashCode(mMatchNonThreadLocalNetworks) * 89;
     }
 
     @Override
@@ -2418,6 +2423,7 @@
         dest.writeTypedList(mUnderlyingNetworks);
         dest.writeInt(mEnterpriseId & ALL_VALID_ENTERPRISE_IDS);
         dest.writeInt(mReservationId);
+        dest.writeBoolean(mMatchNonThreadLocalNetworks);
     }
 
     public static final @android.annotation.NonNull Creator<NetworkCapabilities> CREATOR =
@@ -2454,8 +2460,10 @@
                 netCap.setUnderlyingNetworks(in.createTypedArrayList(Network.CREATOR));
                 netCap.mEnterpriseId = in.readInt() & ALL_VALID_ENTERPRISE_IDS;
                 netCap.mReservationId = in.readInt();
+                netCap.mMatchNonThreadLocalNetworks = in.readBoolean();
                 return netCap;
             }
+
             @Override
             public NetworkCapabilities[] newArray(int size) {
                 return new NetworkCapabilities[size];
@@ -2561,6 +2569,10 @@
             sb.append(" ReservationId: ").append(isReservationOffer ? "*" : mReservationId);
         }
 
+        if (mMatchNonThreadLocalNetworks) {
+            sb.append(" MatchNonThreadLocalNetworks");
+        }
+
         sb.append(" UnderlyingNetworks: ");
         if (mUnderlyingNetworks != null) {
             sb.append("[");
@@ -2945,7 +2957,45 @@
         return mReservationId == nc.mReservationId;
     }
 
+    /**
+     * Flag to control whether a NetworkRequest can match non-thread local networks.
+     * @hide
+     */
+    // TODO: Change to default disabled when introduce this filtering.
+    private boolean mMatchNonThreadLocalNetworks = true;
 
+    /**
+     * Returns the match non-thread local networks flag.
+     *
+     * @hide
+     */
+    public boolean getMatchNonThreadLocalNetworks() {
+        return mMatchNonThreadLocalNetworks;
+    }
+
+    /**
+     * Set the match non-thread local networks flag.
+     * @hide
+     */
+    public void setMatchNonThreadLocalNetworks(boolean enabled) {
+        mMatchNonThreadLocalNetworks = enabled;
+    }
+
+    private boolean equalsMatchNonThreadLocalNetworks(@NonNull NetworkCapabilities nc) {
+        return mMatchNonThreadLocalNetworks == nc.mMatchNonThreadLocalNetworks;
+    }
+
+    // If the flag was set, the NetworkRequest can match all local networks.
+    // Otherwise, it can only see local networks created by Thread.
+    @SuppressWarnings("FlaggedApi")
+    private boolean satisfiedByMatchNonThreadLocalNetworks(@NonNull NetworkCapabilities nc) {
+        // If the network is not a local network, out of scope.
+        if (!nc.hasCapability(NET_CAPABILITY_LOCAL_NETWORK)) return true;
+        // If there is no restriction on matching non-thread local networks, return.
+        if (mMatchNonThreadLocalNetworks) return true;
+
+        return nc.hasTransport(TRANSPORT_THREAD);
+    }
 
     /**
      * Returns a bitmask of all the applicable redactions (based on the permissions held by the
diff --git a/framework/src/android/net/connectivity/ConnectivityCompatChanges.java b/framework/src/android/net/connectivity/ConnectivityCompatChanges.java
index 2261c69..3b2520e 100644
--- a/framework/src/android/net/connectivity/ConnectivityCompatChanges.java
+++ b/framework/src/android/net/connectivity/ConnectivityCompatChanges.java
@@ -139,13 +139,13 @@
 
     /**
      * Restrict local network access.
-     *
      * Apps targeting a release after V will require permissions to access the local network.
      *
+     * ToDo: Update the target SDK version once it's finalized.
      * @hide
      */
     @ChangeId
-    @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.CUR_DEVELOPMENT)
+    @EnabledAfter(targetSdkVersion = 36)
     public static final long RESTRICT_LOCAL_NETWORK = 365139289L;
 
     private ConnectivityCompatChanges() {
diff --git a/service-t/src/com/android/server/ethernet/EthernetTracker.java b/service-t/src/com/android/server/ethernet/EthernetTracker.java
index 9b3c7ba..48467ed 100644
--- a/service-t/src/com/android/server/ethernet/EthernetTracker.java
+++ b/service-t/src/com/android/server/ethernet/EthernetTracker.java
@@ -755,7 +755,17 @@
     private void parseEthernetConfig(String configString) {
         final EthernetTrackerConfig config = createEthernetTrackerConfig(configString);
         NetworkCapabilities nc;
-        if (TextUtils.isEmpty(config.mCapabilities)) {
+        // Starting with Android B (API level 36), we provide default NetworkCapabilities
+        // for Ethernet interfaces when no explicit capabilities are specified in the
+        // configuration string. This change is made to ensure consistent and expected
+        // network behavior for Ethernet devices.
+        //
+        // It's possible that OEMs or device manufacturers may have relied on the previous
+        // behavior (where interfaces without specified capabilities would have minimal
+        // capabilities) to prevent certain Ethernet interfaces from becoming
+        // the default network. To avoid breaking existing device configurations, this
+        // change is gated by the SDK level.
+        if (SdkLevel.isAtLeastB() && TextUtils.isEmpty(config.mCapabilities)) {
             boolean isTestIface = config.mIface.matches(TEST_IFACE_REGEXP);
             nc = createDefaultNetworkCapabilities(isTestIface, config.mTransport);
         } else {
diff --git a/service-t/src/com/android/server/net/NetworkStatsFactory.java b/service-t/src/com/android/server/net/NetworkStatsFactory.java
index 5ff708d..c5a69c0 100644
--- a/service-t/src/com/android/server/net/NetworkStatsFactory.java
+++ b/service-t/src/com/android/server/net/NetworkStatsFactory.java
@@ -19,6 +19,7 @@
 import static android.net.NetworkStats.INTERFACES_ALL;
 import static android.net.NetworkStats.TAG_ALL;
 import static android.net.NetworkStats.UID_ALL;
+import static android.provider.DeviceConfig.NAMESPACE_TETHERING;
 
 import android.annotation.NonNull;
 import android.content.Context;
@@ -26,15 +27,26 @@
 import android.net.UnderlyingNetworkInfo;
 import android.os.ServiceSpecificException;
 import android.os.SystemClock;
+import android.util.ArraySet;
+import android.util.IndentingPrintWriter;
+import android.util.Log;
+import android.util.Pair;
+import android.util.SparseArray;
+import android.util.SparseBooleanArray;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.net.module.util.DeviceConfigUtils;
 import com.android.server.BpfNetMaps;
 import com.android.server.connectivity.InterfaceTracker;
 
 import java.io.IOException;
 import java.net.ProtocolException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 
 /**
@@ -65,6 +77,18 @@
     /** Set containing info about active VPNs and their underlying networks. */
     private volatile UnderlyingNetworkInfo[] mUnderlyingNetworkInfos = new UnderlyingNetworkInfo[0];
 
+    static final String CONFIG_PER_UID_TAG_THROTTLING = "per_uid_tag_throttling";
+    static final String CONFIG_PER_UID_TAG_THROTTLING_THRESHOLD =
+            "per_uid_tag_throttling_threshold";
+    private static final int DEFAULT_TAGS_PER_UID_THRESHOLD = 1000;
+    private static final int DUMP_TAGS_PER_UID_COUNT = 20;
+    private final boolean mSupportPerUidTagThrottling;
+    private final int mPerUidTagThrottlingThreshold;
+
+    // Map for set of distinct tags per uid. Used for tag count limiting.
+    @GuardedBy("mPersistentDataLock")
+    private final SparseArray<SparseBooleanArray> mUidTagSets = new SparseArray<>();
+
     // A persistent snapshot of cumulative stats since device start
     @GuardedBy("mPersistentDataLock")
     private NetworkStats mPersistSnapshot;
@@ -110,6 +134,26 @@
         public BpfNetMaps createBpfNetMaps(@NonNull Context ctx) {
             return new BpfNetMaps(ctx, new InterfaceTracker(ctx));
         }
+
+        /**
+         * Check whether one specific feature is not disabled.
+         * @param name Flag name of the experiment in the tethering namespace.
+         * @see DeviceConfigUtils#isTetheringFeatureNotChickenedOut(Context, String)
+         */
+        public boolean isFeatureNotChickenedOut(@NonNull Context context, @NonNull String name) {
+            return DeviceConfigUtils.isTetheringFeatureNotChickenedOut(context, name);
+        }
+
+        /**
+         * Wrapper method for DeviceConfigUtils#getDeviceConfigPropertyInt for test injections.
+         *
+         * See {@link DeviceConfigUtils#getDeviceConfigPropertyInt(String, String, int)}
+         * for more detailed information.
+         */
+        public int getDeviceConfigPropertyInt(@NonNull String name, int defaultValue) {
+            return DeviceConfigUtils.getDeviceConfigPropertyInt(
+                    NAMESPACE_TETHERING, name, defaultValue);
+        }
     }
 
     /**
@@ -162,6 +206,10 @@
         }
         mContext = ctx;
         mDeps = deps;
+        mSupportPerUidTagThrottling = mDeps.isFeatureNotChickenedOut(
+            ctx, CONFIG_PER_UID_TAG_THROTTLING);
+        mPerUidTagThrottlingThreshold = mDeps.getDeviceConfigPropertyInt(
+                CONFIG_PER_UID_TAG_THROTTLING_THRESHOLD, DEFAULT_TAGS_PER_UID_THRESHOLD);
     }
 
     /**
@@ -210,10 +258,13 @@
             requestSwapActiveStatsMapLocked();
             // Stats are always read from the inactive map, so they must be read after the
             // swap
-            final NetworkStats stats = mDeps.getNetworkStatsDetail();
+            final NetworkStats diff = mDeps.getNetworkStatsDetail();
+            // Filter based on UID tag set before merging.
+            final NetworkStats filteredDiff = mSupportPerUidTagThrottling
+                    ? filterStatsByUidTagSets(diff) : diff;
             // BPF stats are incremental; fold into mPersistSnapshot.
-            mPersistSnapshot.setElapsedRealtime(stats.getElapsedRealtime());
-            mPersistSnapshot.combineAllValues(stats);
+            mPersistSnapshot.setElapsedRealtime(diff.getElapsedRealtime());
+            mPersistSnapshot.combineAllValues(filteredDiff);
 
             NetworkStats adjustedStats = adjustForTunAnd464Xlat(mPersistSnapshot, prev, vpnArray);
 
@@ -224,6 +275,41 @@
     }
 
     @GuardedBy("mPersistentDataLock")
+    private NetworkStats filterStatsByUidTagSets(NetworkStats stats) {
+        final NetworkStats filteredStats =
+                new NetworkStats(stats.getElapsedRealtime(), stats.size());
+
+        final NetworkStats.Entry entry = new NetworkStats.Entry();
+        final Set<Integer> tooManyTagsUidSet = new ArraySet<>();
+        for (int i = 0; i < stats.size(); i++) {
+            stats.getValues(i, entry);
+            final int uid = entry.uid;
+            final int tag = entry.tag;
+
+            if (tag == NetworkStats.TAG_NONE) {
+                filteredStats.combineValues(entry);
+                continue;
+            }
+
+            SparseBooleanArray tagSet = mUidTagSets.get(uid);
+            if (tagSet == null) {
+                tagSet = new SparseBooleanArray();
+            }
+            if (tagSet.size() < mPerUidTagThrottlingThreshold || tagSet.get(tag)) {
+                filteredStats.combineValues(entry);
+                tagSet.put(tag, true);
+                mUidTagSets.put(uid, tagSet);
+            } else {
+                tooManyTagsUidSet.add(uid);
+            }
+        }
+        if (tooManyTagsUidSet.size() > 0) {
+            Log.wtf(TAG, "Too many tags detected for uids: " + tooManyTagsUidSet);
+        }
+        return filteredStats;
+    }
+
+    @GuardedBy("mPersistentDataLock")
     private NetworkStats adjustForTunAnd464Xlat(NetworkStats uidDetailStats,
             NetworkStats previousStats, UnderlyingNetworkInfo[] vpnArray) {
         // Calculate delta from last snapshot
@@ -307,4 +393,34 @@
         pe.initCause(cause);
         return pe;
     }
+
+    /**
+     * Dump the contents of NetworkStatsFactory.
+     */
+    public void dump(IndentingPrintWriter pw) {
+        dumpUidTagSets(pw);
+    }
+
+    private void dumpUidTagSets(IndentingPrintWriter pw) {
+        pw.println("Top distinct tag counts in UidTagSets:");
+        pw.increaseIndent();
+        final List<Pair<Integer, Integer>> countForUidList = new ArrayList<>();
+        synchronized (mPersistentDataLock) {
+            for (int i = 0; i < mUidTagSets.size(); i++) {
+                final Pair<Integer, Integer> countForUid =
+                        new Pair<>(mUidTagSets.keyAt(i), mUidTagSets.valueAt(i).size());
+                countForUidList.add(countForUid);
+            }
+        }
+        Collections.sort(countForUidList,
+                (entry1, entry2) -> Integer.compare(entry2.second, entry1.second));
+        final int dumpSize = Math.min(countForUidList.size(), DUMP_TAGS_PER_UID_COUNT);
+        for (int j = 0; j < dumpSize; j++) {
+            final Pair<Integer, Integer> entry = countForUidList.get(j);
+            pw.print(entry.first);
+            pw.print("=");
+            pw.println(entry.second);
+        }
+        pw.decreaseIndent();
+    }
 }
diff --git a/service-t/src/com/android/server/net/NetworkStatsService.java b/service-t/src/com/android/server/net/NetworkStatsService.java
index 5c5f4ca..75d30a9 100644
--- a/service-t/src/com/android/server/net/NetworkStatsService.java
+++ b/service-t/src/com/android/server/net/NetworkStatsService.java
@@ -3228,6 +3228,12 @@
             pw.increaseIndent();
             mSkDestroyListener.dump(pw);
             pw.decreaseIndent();
+
+            pw.println();
+            pw.println("NetworkStatsFactory logs:");
+            pw.increaseIndent();
+            mStatsFactory.dump(pw);
+            pw.decreaseIndent();
         }
     }
 
diff --git a/service/ServiceConnectivityResources/res/values/overlayable.xml b/service/ServiceConnectivityResources/res/values/overlayable.xml
index 5c0ba78..28b46c1 100644
--- a/service/ServiceConnectivityResources/res/values/overlayable.xml
+++ b/service/ServiceConnectivityResources/res/values/overlayable.xml
@@ -48,6 +48,8 @@
 
             <!-- Configuration values for ThreadNetworkService -->
             <item type="bool" name="config_thread_default_enabled" />
+            <item type="bool" name="config_thread_border_router_default_enabled" />
+            <item type="bool" name="config_thread_country_code_enabled" />
             <item type="bool" name="config_thread_location_use_for_country_code_enabled" />
             <item type="string" name="config_thread_vendor_name" />
             <item type="string" name="config_thread_vendor_oui" />
diff --git a/service/src/com/android/server/BpfNetMaps.java b/service/src/com/android/server/BpfNetMaps.java
index 9bd407d..523ffee 100644
--- a/service/src/com/android/server/BpfNetMaps.java
+++ b/service/src/com/android/server/BpfNetMaps.java
@@ -1327,7 +1327,7 @@
                             + value.iif1 + "(" + mDeps.getIfName(value.iif1) + "), "
                             + value.iif2 + "(" + mDeps.getIfName(value.iif2) + ")");
             if (sLocalNetBlockedUidMap != null) {
-                BpfDump.dumpMap(sLocalNetAccessMap, pw, "sLocalNetAccessMap",
+                BpfDump.dumpMap(sLocalNetAccessMap, pw, "sLocalNetAccessMap (default is true meaning global)",
                         (key, value) -> "" + key + ": " + value.val);
             }
             if (sLocalNetBlockedUidMap != null) {
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index e27b72d..b9b590b 100644
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -10059,10 +10059,12 @@
             // Adds dns allow rule to LocalNetAccessMap for both TCP and UDP protocol at port 53,
             // if it is a local dns (ie. it falls in the local prefix range).
             if (prefix.contains(dnsServer)) {
-                mBpfNetMaps.addLocalNetAccess(getIpv4MappedAddressBitLen(), iface, dnsServer,
+                mBpfNetMaps.addLocalNetAccess(32 + 128 + 16 + 16, iface, dnsServer,
                         IPPROTO_UDP, 53, true);
-                mBpfNetMaps.addLocalNetAccess(getIpv4MappedAddressBitLen(), iface, dnsServer,
+                mBpfNetMaps.addLocalNetAccess(32 + 128 + 16 + 16, iface, dnsServer,
                         IPPROTO_TCP, 53, true);
+                mBpfNetMaps.addLocalNetAccess(32 + 128 + 16 + 16, iface, dnsServer,
+                        IPPROTO_TCP, 853, true);  // DNS over TLS
             }
         }
     }
@@ -10081,25 +10083,17 @@
             // Removes dns allow rule from LocalNetAccessMap for both TCP and UDP protocol
             // at port 53, if it is a local dns (ie. it falls in the prefix range).
             if (prefix.contains(dnsServer)) {
-                mBpfNetMaps.removeLocalNetAccess(getIpv4MappedAddressBitLen(), iface, dnsServer,
+                mBpfNetMaps.removeLocalNetAccess(32 + 128 + 16 + 16, iface, dnsServer,
                         IPPROTO_UDP, 53);
-                mBpfNetMaps.removeLocalNetAccess(getIpv4MappedAddressBitLen(), iface, dnsServer,
+                mBpfNetMaps.removeLocalNetAccess(32 + 128 + 16 + 16, iface, dnsServer,
                         IPPROTO_TCP, 53);
+                mBpfNetMaps.removeLocalNetAccess(32 + 128 + 16 + 16, iface, dnsServer,
+                        IPPROTO_TCP, 853);  // DNS over TLS
             }
         }
     }
 
     /**
-     * Returns total bit length of an Ipv4 mapped address.
-     */
-    private int getIpv4MappedAddressBitLen() {
-        final int ifaceLen = 32; // bit length of interface
-        final int inetAddressLen = 32 + 96; // length of ipv4 mapped addresses
-        final int portProtocolLen = 32;  //16 for port + 16 for protocol;
-        return ifaceLen + inetAddressLen + portProtocolLen;
-    }
-
-    /**
      * Have netd update routes from oldLp to newLp.
      * @return true if routes changed between oldLp and newLp
      */
diff --git a/service/src/com/android/server/L2capNetworkProvider.java b/service/src/com/android/server/L2capNetworkProvider.java
index 0352ad5..149979f 100644
--- a/service/src/com/android/server/L2capNetworkProvider.java
+++ b/service/src/com/android/server/L2capNetworkProvider.java
@@ -597,10 +597,12 @@
             final ClientRequestInfo cri = mClientNetworkRequests.get(specifier);
             if (cri == null) return;
 
+            // Release ClientNetworkRequest before sending onUnavailable() to ensure the app
+            // first receives an onLost() callback if a network had been created.
+            releaseClientNetworkRequest(cri);
             for (NetworkRequest request : cri.requests) {
                 mProvider.declareNetworkRequestUnfulfillable(request);
             }
-            releaseClientNetworkRequest(cri);
         }
     }
 
diff --git a/staticlibs/tests/unit/host/python/apf_utils_test.py b/staticlibs/tests/unit/host/python/apf_utils_test.py
index d4753b7..55fbe58 100644
--- a/staticlibs/tests/unit/host/python/apf_utils_test.py
+++ b/staticlibs/tests/unit/host/python/apf_utils_test.py
@@ -27,6 +27,7 @@
     get_apf_counters_from_dumpsys,
     get_ipv4_addresses,
     get_non_tentative_ipv6_addresses,
+    get_exclude_all_host_ipv6_multicast_addresses,
     get_hardware_address,
     is_send_raw_packet_downstream_supported,
     is_packet_capture_supported,
@@ -170,6 +171,31 @@
     ip_addresses = get_non_tentative_ipv6_addresses(self.mock_ad, "wlan0")
     asserts.assert_equal(ip_addresses, [])
 
+
+  @patch("net_tests_utils.host.python.adb_utils.adb_shell")
+  def test_get_exclude_all_host_ipv6_multicast_addresses_success(
+      self, mock_adb_shell: MagicMock
+  ) -> None:
+    mock_adb_shell.return_value = """
+47:     wlan0
+        inet6 ff02::1:ff99:37b0
+        inet6 ff02::1:ffb7:cba2 users 2
+        inet6 ff02::1
+        inet6 ff01::1
+"""
+    ip_addresses = get_exclude_all_host_ipv6_multicast_addresses(self.mock_ad, "wlan0")
+    asserts.assert_equal(ip_addresses,
+                         ["ff02::1:ff99:37b0",
+                          "ff02::1:ffb7:cba2"])
+
+  @patch("net_tests_utils.host.python.adb_utils.adb_shell")
+  def test_get_exclude_all_host_ipv6_multicast_addresses_not_found(
+          self, mock_adb_shell: MagicMock
+  ) -> None:
+    mock_adb_shell.return_value = ""
+    ip_addresses = get_exclude_all_host_ipv6_multicast_addresses(self.mock_ad, "wlan0")
+    asserts.assert_equal(ip_addresses, [])
+
   @patch("net_tests_utils.host.python.adb_utils.adb_shell")
   def test_send_raw_packet_downstream_success(
       self, mock_adb_shell: MagicMock
diff --git a/staticlibs/tests/unit/host/python/packet_utils_test.py b/staticlibs/tests/unit/host/python/packet_utils_test.py
deleted file mode 100644
index 8ad9576..0000000
--- a/staticlibs/tests/unit/host/python/packet_utils_test.py
+++ /dev/null
@@ -1,72 +0,0 @@
-#  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.
-
-from mobly import asserts
-from mobly import base_test
-from net_tests_utils.host.python import packet_utils
-
-class TestPacketUtils(base_test.BaseTestClass):
-    def test_unicast_arp_request(self):
-        # Using scapy to generate unicast arp request packet:
-        #   eth = Ether(src="00:01:02:03:04:05", dst="01:02:03:04:05:06")
-        #   arp = ARP(op=1, pdst="192.168.1.1", hwsrc="00:01:02:03:04:05", psrc="192.168.1.2")
-        #   pkt = eth/arp
-        expect_arp_request = """
-            01020304050600010203040508060001080006040001000102030405c0a80102000000000000c0a80101
-        """.upper().replace(" ", "").replace("\n", "")
-        arp_request = packet_utils.construct_arp_packet(
-            src_mac="00:01:02:03:04:05",
-            dst_mac="01:02:03:04:05:06",
-            src_ip="192.168.1.2",
-            dst_ip="192.168.1.1",
-            op=packet_utils.ARP_REQUEST_OP
-        )
-        asserts.assert_equal(expect_arp_request, arp_request)
-
-    def test_broadcast_arp_request(self):
-        # Using scapy to generate unicast arp request packet:
-        #   eth = Ether(src="00:01:02:03:04:05", dst="FF:FF:FF:FF:FF:FF")
-        #   arp = ARP(op=1, pdst="192.168.1.1", hwsrc="00:01:02:03:04:05", psrc="192.168.1.2")
-        #   pkt = eth/arp
-        expect_arp_request = """
-            ffffffffffff00010203040508060001080006040001000102030405c0a80102000000000000c0a80101
-        """.upper().replace(" ", "").replace("\n", "")
-        arp_request = packet_utils.construct_arp_packet(
-            src_mac="00:01:02:03:04:05",
-            dst_mac=packet_utils.ETHER_BROADCAST_MAC_ADDRESS,
-            src_ip="192.168.1.2",
-            dst_ip="192.168.1.1",
-            op=packet_utils.ARP_REQUEST_OP
-        )
-        asserts.assert_equal(expect_arp_request, arp_request)
-
-    def test_arp_reply(self):
-        # Using scapy to generate unicast arp request packet:
-        #   eth = Ether(src="01:02:03:04:05:06", dst="00:01:02:03:04:05")
-        #   arp = ARP(op=2, pdst="192.168.1.2", \
-        #             hwsrc="01:02:03:04:05:06", \
-        #             psrc="192.168.1.1", \
-        #             hwdst="00:01:02:03:04:05")
-        #   pkt = eth/arp
-        expect_arp_reply = """
-            00010203040501020304050608060001080006040002010203040506c0a80101000102030405c0a80102
-        """.upper().replace(" ", "").replace("\n", "")
-        arp_reply = packet_utils.construct_arp_packet(
-            src_mac="01:02:03:04:05:06",
-            dst_mac="00:01:02:03:04:05",
-            src_ip="192.168.1.1",
-            dst_ip="192.168.1.2",
-            op=packet_utils.ARP_REPLY_OP
-        )
-        asserts.assert_equal(expect_arp_reply, arp_reply)
diff --git a/staticlibs/tests/unit/host/python/run_tests.py b/staticlibs/tests/unit/host/python/run_tests.py
index 498dbaf..fa6a310 100644
--- a/staticlibs/tests/unit/host/python/run_tests.py
+++ b/staticlibs/tests/unit/host/python/run_tests.py
@@ -18,7 +18,6 @@
 from host.python.adb_utils_test import TestAdbUtils
 from host.python.apf_utils_test import TestApfUtils
 from host.python.assert_utils_test import TestAssertUtils
-from host.python.packet_utils_test import TestPacketUtils
 from mobly import suite_runner
 
 
@@ -32,5 +31,5 @@
   sys.argv.pop(1)
   # TODO: make the tests can be executed without manually list classes.
   suite_runner.run_suite(
-      [TestAssertUtils, TestAdbUtils, TestApfUtils, TestPacketUtils], sys.argv
+      [TestAssertUtils, TestAdbUtils, TestApfUtils], sys.argv
   )
diff --git a/staticlibs/testutils/host/python/apf_test_base.py b/staticlibs/testutils/host/python/apf_test_base.py
index 33b3838..6a62e21 100644
--- a/staticlibs/testutils/host/python/apf_test_base.py
+++ b/staticlibs/testutils/host/python/apf_test_base.py
@@ -70,6 +70,9 @@
     # Enable doze mode to activate APF.
     adb_utils.set_doze_mode(self.clientDevice, True)
 
+    # wait for APF to become active.
+    time.sleep(3)
+
   def teardown_class(self):
     adb_utils.set_doze_mode(self.clientDevice, False)
     tether_utils.cleanup_tethering_for_upstream_type(
diff --git a/staticlibs/testutils/host/python/apf_utils.py b/staticlibs/testutils/host/python/apf_utils.py
index 1648d36..4835c23 100644
--- a/staticlibs/testutils/host/python/apf_utils.py
+++ b/staticlibs/testutils/host/python/apf_utils.py
@@ -148,6 +148,38 @@
   else:
     return []
 
+def get_exclude_all_host_ipv6_multicast_addresses(
+    ad: android_device.AndroidDevice, iface_name: str
+) -> list[str]:
+  """Retrieves the IPv6 multicast addresses of a given interface on an Android device.
+
+  This function executes an ADB shell command (`ip -6 maddr show`) to get the
+  network interface information and extracts the IPv6 multicast address from the output.
+  If devices have no IPv6 multicast address, raise PatternNotFoundException.
+
+  Args:
+      ad: The Android device object.
+      iface_name: The name of the network interface (e.g., "wlan0").
+
+  Returns:
+      The IPv6 multicast addresses of the interface as a list of string.
+      Return empty list if no IPv6 multicast address.
+  """
+  # output format
+  # 47:     wlan0
+  #         inet6 ff02::1:ff99:37b0
+  #         inet6 ff02::1:ffb7:cba2 users 2
+  #         inet6 ff02::1
+  #         inet6 ff01::1
+  output = adb_utils.adb_shell(ad, f"ip -6 maddr show {iface_name}")
+  pattern = r"inet6\s+([a-fA-F0-9:]+)(?:\s+users\s+\d+)?"
+  matches = re.findall(pattern, output)
+
+  if matches:
+    return [addr for addr in matches if addr not in ("ff02::1", "ff01::1")]
+  else:
+    return []
+
 def get_hardware_address(
     ad: android_device.AndroidDevice, iface_name: str
 ) -> str:
@@ -407,10 +439,28 @@
     def wrapper(self, *args, **kwargs):
       asserts.abort_class_if(
         (not hasattr(self, 'client')) or (not hasattr(self.client, 'isAtLeastB')),
-        "client device is not B+"
+        "no valid client attribute"
       )
 
-      asserts.abort_class_if(not self.client.isAtLeastB(), "not B+")
+      asserts.abort_class_if(not self.client.isAtLeastB(), "client device is not Android B+")
+      return test_function(self, *args, **kwargs)
+    return wrapper
+  return decorator
+
+def apf_ram_at_least(size):
+  def decorator(test_function):
+    @functools.wraps(test_function)
+    def wrapper(self, *args, **kwargs):
+      asserts.abort_class_if(
+        (not hasattr(self, 'clientDevice')) or (not hasattr(self, 'client_iface_name')),
+        "no valid client attribute"
+      )
+
+      caps = get_apf_capabilities(self.clientDevice, self.client_iface_name)
+      asserts.skip_if(
+        caps.apf_ram_size < size,
+        f'APF rame size {caps.apf_ram_size} < {size}'
+      )
       return test_function(self, *args, **kwargs)
     return wrapper
   return decorator
diff --git a/staticlibs/testutils/host/python/packet_utils.py b/staticlibs/testutils/host/python/packet_utils.py
deleted file mode 100644
index b613f03..0000000
--- a/staticlibs/testutils/host/python/packet_utils.py
+++ /dev/null
@@ -1,70 +0,0 @@
-#  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.
-from ipaddress import IPv4Address
-from socket import inet_aton
-
-ETHER_BROADCAST_MAC_ADDRESS = "FF:FF:FF:FF:FF:FF"
-ARP_REQUEST_OP = 1
-ARP_REPLY_OP = 2
-
-"""
-This variable defines a template for constructing ARP packets in hexadecimal format.
-It's used to provide the common fields for ARP packet, and replaced needed fields when constructing
-"""
-ARP_TEMPLATE = (
-    # Ether Header (14 bytes)
-    "{dst_mac}" + # DA
-    "{src_mac}" + # SA
-    "0806" + # ARP
-    # ARP Header (28 bytes)
-    "0001" + # Hardware type (Ethernet)
-    "0800" + # Protocol type (IPv4)
-    "06" + # hardware address length
-    "04" + # protocol address length
-    "{opcode}" + # opcode
-    "{sender_mac}" + # sender MAC
-    "{sender_ip}" + # sender IP
-    "{target_mac}" + # target MAC
-    "{target_ip}" # target IP
-)
-
-def construct_arp_packet(src_mac, dst_mac, src_ip, dst_ip, op) -> str:
-    """Constructs an ARP packet as a hexadecimal string.
-
-    This function creates an ARP packet by filling in the required fields
-    in a predefined ARP packet template.
-
-    Args:
-    src_mac: The MAC address of the sender. (e.g. "11:22:33:44:55:66")
-    dst_mac: The MAC address of the recipient. (e.g. "aa:bb:cc:dd:ee:ff")
-    src_ip: The IP address of the sender. (e.g. "1.1.1.1")
-    dst_ip: The IP address of the target machine. (e.g. "2.2.2.2")
-    op: The op code of the ARP packet, refer to ARP_*_OP
-
-    Returns:
-    A string representing the ARP packet in hexadecimal format.
-    """
-    # Replace the needed fields from packet template
-    arp_pkt = ARP_TEMPLATE.format(
-            dst_mac=dst_mac.replace(":",""),
-            src_mac=src_mac.replace(":",""),
-            opcode=str(op).rjust(4, "0"),
-            sender_mac=src_mac.replace(":",""),
-            sender_ip=inet_aton(src_ip).hex(),
-            target_mac=("000000000000" if op == ARP_REQUEST_OP else dst_mac.replace(":", "")),
-            target_ip=inet_aton(dst_ip).hex()
-    )
-
-    # always convert to upper case hex string
-    return arp_pkt.upper()
\ No newline at end of file
diff --git a/tests/common/java/android/net/NetworkCapabilitiesTest.java b/tests/common/java/android/net/NetworkCapabilitiesTest.java
index d694637..3fc2af0 100644
--- a/tests/common/java/android/net/NetworkCapabilitiesTest.java
+++ b/tests/common/java/android/net/NetworkCapabilitiesTest.java
@@ -56,6 +56,7 @@
 import static android.net.NetworkCapabilities.TRANSPORT_ETHERNET;
 import static android.net.NetworkCapabilities.TRANSPORT_SATELLITE;
 import static android.net.NetworkCapabilities.TRANSPORT_TEST;
+import static android.net.NetworkCapabilities.TRANSPORT_THREAD;
 import static android.net.NetworkCapabilities.TRANSPORT_USB;
 import static android.net.NetworkCapabilities.TRANSPORT_VPN;
 import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
@@ -1532,4 +1533,93 @@
         nc.setReservationId(43);
         assertNotEquals(nc, other);
     }
+
+    @Test
+    public void testMatchNonThreadLocalNetworks_equals() {
+        final NetworkCapabilities nc = new NetworkCapabilities();
+        nc.setMatchNonThreadLocalNetworks(true);
+        final NetworkCapabilities other = new NetworkCapabilities(nc);
+        assertEquals(nc, other);
+
+        nc.setMatchNonThreadLocalNetworks(false);
+        assertNotEquals(nc, other);
+    }
+
+    @Test
+    public void testMatchNonThreadLocalNetworks_enabled() {
+        doTestMatchNonThreadLocalNetworks(true);
+    }
+
+    @Test
+    public void testMatchNonThreadLocalNetworks_disabled() {
+        doTestMatchNonThreadLocalNetworks(false);
+    }
+
+    private void doTestMatchNonThreadLocalNetworks(boolean enabled) {
+        // Setup request NCs.
+        final NetworkCapabilities noTransportRequestNc = new NetworkCapabilities();
+        final NetworkCapabilities threadRequestNc =
+                new NetworkCapabilities.Builder().addTransportType(TRANSPORT_THREAD).build();
+        final NetworkCapabilities wifiRequestNc =
+                new NetworkCapabilities.Builder().addTransportType(TRANSPORT_WIFI).build();
+        final NetworkCapabilities multiTransportRequestNc =
+                new NetworkCapabilities.Builder().addTransportType(
+                        TRANSPORT_THREAD).addTransportType(TRANSPORT_WIFI).build();
+
+        // Setup network NCs.
+        final NetworkCapabilities localNoTransportNc = new NetworkCapabilities.Builder()
+                .addCapability(NET_CAPABILITY_LOCAL_NETWORK).build();
+        final NetworkCapabilities localThreadsNc = new NetworkCapabilities.Builder()
+                .addCapability(NET_CAPABILITY_LOCAL_NETWORK)
+                .addTransportType(TRANSPORT_THREAD).build();
+        final NetworkCapabilities localWifiNc = new NetworkCapabilities.Builder()
+                .addCapability(NET_CAPABILITY_LOCAL_NETWORK)
+                .addTransportType(TRANSPORT_WIFI).build();
+        final NetworkCapabilities wanWifiNc = new NetworkCapabilities.Builder()
+                .addTransportType(TRANSPORT_WIFI).build();
+
+        // Mark flags accordingly.
+        noTransportRequestNc.setMatchNonThreadLocalNetworks(enabled);
+        threadRequestNc.setMatchNonThreadLocalNetworks(enabled);
+        wifiRequestNc.setMatchNonThreadLocalNetworks(enabled);
+        multiTransportRequestNc.setMatchNonThreadLocalNetworks(enabled);
+
+        if (enabled) {
+            // A request with no specific transport matches all networks.
+            assertTrue(noTransportRequestNc.satisfiedByNetworkCapabilities(localNoTransportNc));
+            assertTrue(noTransportRequestNc.satisfiedByNetworkCapabilities(localWifiNc));
+        } else {
+            // A request with no specific transport only matches thread networks.
+            assertFalse(noTransportRequestNc.satisfiedByNetworkCapabilities(localNoTransportNc));
+            assertFalse(noTransportRequestNc.satisfiedByNetworkCapabilities(localWifiNc));
+        }
+        assertTrue(noTransportRequestNc.satisfiedByNetworkCapabilities(localThreadsNc));
+        assertTrue(noTransportRequestNc.satisfiedByNetworkCapabilities(wanWifiNc));
+
+        // A request with TRANSPORT_THREAD only matches thread networks.
+        assertFalse(threadRequestNc.satisfiedByNetworkCapabilities(localNoTransportNc));
+        assertTrue(threadRequestNc.satisfiedByNetworkCapabilities(localThreadsNc));
+        assertFalse(threadRequestNc.satisfiedByNetworkCapabilities(localWifiNc));
+        assertFalse(threadRequestNc.satisfiedByNetworkCapabilities(wanWifiNc));
+
+        assertFalse(multiTransportRequestNc.satisfiedByNetworkCapabilities(localNoTransportNc));
+        assertTrue(multiTransportRequestNc.satisfiedByNetworkCapabilities(localThreadsNc));
+        assertTrue(multiTransportRequestNc.satisfiedByNetworkCapabilities(wanWifiNc));
+        if (enabled) {
+            assertTrue(multiTransportRequestNc.satisfiedByNetworkCapabilities(localWifiNc));
+        } else {
+            // A request with multiple transports only matches thread networks.
+            assertFalse(multiTransportRequestNc.satisfiedByNetworkCapabilities(localWifiNc));
+        }
+
+        assertFalse(wifiRequestNc.satisfiedByNetworkCapabilities(localNoTransportNc));
+        assertFalse(wifiRequestNc.satisfiedByNetworkCapabilities(localThreadsNc));
+        assertTrue(wifiRequestNc.satisfiedByNetworkCapabilities(wanWifiNc));
+        if (enabled) {
+            assertTrue(wifiRequestNc.satisfiedByNetworkCapabilities(localWifiNc));
+        } else {
+            // A request without TRANSPORT_THREAD matches nothing.
+            assertFalse(wifiRequestNc.satisfiedByNetworkCapabilities(localWifiNc));
+        }
+    }
 }
diff --git a/tests/cts/multidevices/apfv6_test.py b/tests/cts/multidevices/apfv6_test.py
index fb45f4a..2404966 100644
--- a/tests/cts/multidevices/apfv6_test.py
+++ b/tests/cts/multidevices/apfv6_test.py
@@ -14,10 +14,23 @@
 
 from mobly import asserts
 from scapy.layers.inet import IP, ICMP, IPOption_Router_Alert
-from scapy.layers.inet6 import IPv6, ICMPv6EchoRequest, ICMPv6EchoReply
-from scapy.layers.l2 import Ether
+from scapy.layers.inet6 import (
+    IPv6,
+    IPv6ExtHdrHopByHop,
+    ICMPv6EchoRequest,
+    ICMPv6EchoReply,
+    ICMPv6MLQuery2,
+    ICMPv6MLReport2,
+    ICMPv6MLDMultAddrRec,
+    ICMPv6NDOptSrcLLAddr,
+    ICMPv6NDOptDstLLAddr,
+    ICMPv6ND_NS,
+    ICMPv6ND_NA,
+    RouterAlert
+)
+from scapy.layers.l2 import ARP, Ether
 from scapy.contrib.igmpv3 import IGMPv3, IGMPv3mq, IGMPv3mr, IGMPv3gr
-from net_tests_utils.host.python import apf_test_base, apf_utils, adb_utils, assert_utils, packet_utils
+from net_tests_utils.host.python import apf_test_base, apf_utils, adb_utils, assert_utils
 
 APFV6_VERSION = 6000
 ARP_OFFLOAD_REPLY_LEN = 60
@@ -40,51 +53,75 @@
         super().teardown_class()
 
     def test_unicast_arp_request_offload(self):
-        arp_request = packet_utils.construct_arp_packet(
-            src_mac=self.server_mac_address,
-            dst_mac=self.client_mac_address,
-            src_ip=self.server_ipv4_addresses[0],
-            dst_ip=self.client_ipv4_addresses[0],
-            op=packet_utils.ARP_REQUEST_OP
+        eth = Ether(src=self.server_mac_address, dst=self.client_mac_address)
+        arp = ARP(
+            op=1,
+            psrc=self.server_ipv4_addresses[0],
+            pdst=self.client_ipv4_addresses[0],
+            hwsrc=self.server_mac_address
         )
+        arp_request = bytes(eth/arp).hex()
 
-        arp_reply = packet_utils.construct_arp_packet(
-            src_mac=self.client_mac_address,
-            dst_mac=self.server_mac_address,
-            src_ip=self.client_ipv4_addresses[0],
-            dst_ip=self.server_ipv4_addresses[0],
-            op=packet_utils.ARP_REPLY_OP
+        eth = Ether(src=self.client_mac_address, dst=self.server_mac_address)
+        arp = ARP(
+            op=2,
+            psrc=self.client_ipv4_addresses[0],
+            pdst=self.server_ipv4_addresses[0],
+            hwsrc=self.client_mac_address,
+            hwdst=self.server_mac_address
         )
+        expected_arp_reply = bytes(eth/arp).hex()
 
         # Add zero padding up to 60 bytes, since APFv6 ARP offload always sent out 60 bytes reply
-        arp_reply = arp_reply.ljust(ARP_OFFLOAD_REPLY_LEN * 2, "0")
+        expected_arp_reply = expected_arp_reply.ljust(ARP_OFFLOAD_REPLY_LEN * 2, "0")
 
         self.send_packet_and_expect_reply_received(
-            arp_request, "DROPPED_ARP_REQUEST_REPLIED", arp_reply
+            arp_request, "DROPPED_ARP_REQUEST_REPLIED", expected_arp_reply
         )
 
     def test_broadcast_arp_request_offload(self):
-        arp_request = packet_utils.construct_arp_packet(
-            src_mac=self.server_mac_address,
-            dst_mac=packet_utils.ETHER_BROADCAST_MAC_ADDRESS,
-            src_ip=self.server_ipv4_addresses[0],
-            dst_ip=self.client_ipv4_addresses[0],
-            op=packet_utils.ARP_REQUEST_OP
+        eth = Ether(src=self.server_mac_address, dst='ff:ff:ff:ff:ff:ff')
+        arp = ARP(
+            op=1,
+            psrc=self.server_ipv4_addresses[0],
+            pdst=self.client_ipv4_addresses[0],
+            hwsrc=self.server_mac_address
         )
+        arp_request = bytes(eth/arp).hex()
 
-        arp_reply = packet_utils.construct_arp_packet(
-            src_mac=self.client_mac_address,
-            dst_mac=self.server_mac_address,
-            src_ip=self.client_ipv4_addresses[0],
-            dst_ip=self.server_ipv4_addresses[0],
-            op=packet_utils.ARP_REPLY_OP
+        eth = Ether(src=self.client_mac_address, dst=self.server_mac_address)
+        arp = ARP(
+            op=2,
+            psrc=self.client_ipv4_addresses[0],
+            pdst=self.server_ipv4_addresses[0],
+            hwsrc=self.client_mac_address,
+            hwdst=self.server_mac_address
         )
+        expected_arp_reply = bytes(eth/arp).hex()
 
         # Add zero padding up to 60 bytes, since APFv6 ARP offload always sent out 60 bytes reply
-        arp_reply = arp_reply.ljust(ARP_OFFLOAD_REPLY_LEN * 2, "0")
+        expected_arp_reply = expected_arp_reply.ljust(ARP_OFFLOAD_REPLY_LEN * 2, "0")
 
         self.send_packet_and_expect_reply_received(
-            arp_request, "DROPPED_ARP_REQUEST_REPLIED", arp_reply
+            arp_request, "DROPPED_ARP_REQUEST_REPLIED", expected_arp_reply
+        )
+
+    def test_non_dad_ipv6_neighbor_solicitation_offload(self):
+        eth = Ether(src=self.server_mac_address, dst=self.client_mac_address)
+        ip = IPv6(src=self.server_ipv6_addresses[0], dst=self.client_ipv6_addresses[0])
+        icmpv6 = ICMPv6ND_NS(tgt=self.client_ipv6_addresses[0])
+        opt = ICMPv6NDOptSrcLLAddr(lladdr=self.server_mac_address)
+        neighbor_solicitation = bytes(eth/ip/icmpv6/opt).hex()
+
+        eth = Ether(src=self.client_mac_address, dst=self.server_mac_address)
+        ip = IPv6(src=self.client_ipv6_addresses[0], dst=self.server_ipv6_addresses[0])
+        icmpv6 = ICMPv6ND_NA(tgt=self.client_ipv6_addresses[0], R=1, S=1, O=1)
+        opt = ICMPv6NDOptDstLLAddr(lladdr=self.client_mac_address)
+        expected_neighbor_advertisement = bytes(eth/ip/icmpv6/opt).hex()
+        self.send_packet_and_expect_reply_received(
+            neighbor_solicitation,
+            "DROPPED_IPV6_NS_REPLIED_NON_DAD",
+            expected_neighbor_advertisement
         )
 
     @apf_utils.at_least_B()
@@ -103,6 +140,7 @@
         )
 
     @apf_utils.at_least_B()
+    @apf_utils.apf_ram_at_least(3000)
     def test_ipv6_icmp_echo_request_offload(self):
         eth = Ether(src=self.server_mac_address, dst=self.client_mac_address)
         ip = IPv6(src=self.server_ipv6_addresses[0], dst=self.client_ipv6_addresses[0])
@@ -161,3 +199,28 @@
                 self.clientDevice,
                 f'ip addr del {addr}/32 dev {self.client_iface_name}'
             )
+
+    @apf_utils.at_least_B()
+    @apf_utils.apf_ram_at_least(3000)
+    def test_mldv2_general_query_offload(self):
+        ether = Ether(src=self.server_mac_address, dst='33:33:00:00:00:01')
+        ip = IPv6(src=self.server_ipv6_addresses[0], dst='ff02::1', hlim=1)
+        hopOpts = IPv6ExtHdrHopByHop(options=[RouterAlert(otype=5)])
+        mld = ICMPv6MLQuery2()
+        mldv2_general_query = bytes(ether/ip/hopOpts/mld).hex()
+
+        ether = Ether(src=self.client_mac_address, dst='33:33:00:00:00:16')
+        ip = IPv6(src=self.client_ipv6_addresses[0], dst='ff02::16', hlim=1)
+
+        mcast_addrs = apf_utils.get_exclude_all_host_ipv6_multicast_addresses(
+            self.clientDevice, self.client_iface_name
+        )
+
+        mld_records = []
+        for addr in mcast_addrs:
+            mld_records.append(ICMPv6MLDMultAddrRec(dst=addr, rtype=2))
+        mld = ICMPv6MLReport2(records=mld_records)
+        expected_mldv2_report = bytes(ether/ip/hopOpts/mld).hex()
+        self.send_packet_and_expect_reply_received(
+            mldv2_general_query, "DROPPED_IPV6_MLD_V2_GENERAL_QUERY_REPLIED", expected_mldv2_report
+        )
diff --git a/tests/cts/net/AndroidManifest.xml b/tests/cts/net/AndroidManifest.xml
index 098cc0a..acf89be 100644
--- a/tests/cts/net/AndroidManifest.xml
+++ b/tests/cts/net/AndroidManifest.xml
@@ -26,6 +26,7 @@
     <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
     <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
     <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
+    <uses-permission android:name="android.permission.NEARBY_WIFI_DEVICES" />
     <uses-permission android:name="android.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS" />
     <uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
     <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS" />
diff --git a/tests/cts/net/src/android/net/cts/DscpPolicyTest.kt b/tests/cts/net/src/android/net/cts/DscpPolicyTest.kt
index df4dab5..d531e7a 100644
--- a/tests/cts/net/src/android/net/cts/DscpPolicyTest.kt
+++ b/tests/cts/net/src/android/net/cts/DscpPolicyTest.kt
@@ -774,7 +774,9 @@
         runAsShell(MANAGE_TEST_NETWORKS) { agent.register() }
         // Without the fix, this will crash the system with SIGSEGV.
         agent.sendAddDscpPolicy(DscpPolicy.Builder(1, 1).build())
-        agent.expectCallback<OnDscpPolicyStatusUpdated>()
+        // Will receive OnNetworkCreated first if the agent is created early. To avoid reading
+        // the flag here, use eventuallyExpect.
+        agent.eventuallyExpect<OnDscpPolicyStatusUpdated>()
     }
 }
 
diff --git a/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt b/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
index 4c3bce0..4703ac7 100644
--- a/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
+++ b/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
@@ -15,6 +15,7 @@
  */
 package android.net.cts
 
+import android.Manifest.permission.NEARBY_WIFI_DEVICES
 import android.Manifest.permission.NETWORK_SETTINGS
 import android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE
 import android.app.Instrumentation
@@ -247,6 +248,12 @@
     @Before
     fun setUp() {
         instrumentation.getUiAutomation().adoptShellPermissionIdentity()
+        if (SdkLevel.isAtLeastT()) {
+            instrumentation.getUiAutomation().grantRuntimePermission(
+                "android.net.cts",
+                NEARBY_WIFI_DEVICES
+            )
+        }
         mHandlerThread.start()
     }
 
@@ -751,12 +758,24 @@
         tryTest {
             // This process is not the carrier service UID, so allowedUids should be ignored in all
             // the following cases.
-            doTestAllowedUidsWithSubId(defaultSubId, TRANSPORT_CELLULAR, uid,
-                    expectUidsPresent = false)
-            doTestAllowedUidsWithSubId(defaultSubId, TRANSPORT_WIFI, uid,
-                    expectUidsPresent = false)
-            doTestAllowedUidsWithSubId(defaultSubId, TRANSPORT_BLUETOOTH, uid,
-                    expectUidsPresent = false)
+            doTestAllowedUidsWithSubId(
+                defaultSubId,
+                TRANSPORT_CELLULAR,
+                uid,
+                    expectUidsPresent = false
+            )
+            doTestAllowedUidsWithSubId(
+                defaultSubId,
+                TRANSPORT_WIFI,
+                uid,
+                    expectUidsPresent = false
+            )
+            doTestAllowedUidsWithSubId(
+                defaultSubId,
+                TRANSPORT_BLUETOOTH,
+                uid,
+                    expectUidsPresent = false
+            )
 
             // The tools to set the carrier service package override do not exist before U,
             // so there is no way to test the rest of this test on < U.
@@ -774,9 +793,11 @@
             val timeout = SystemClock.elapsedRealtime() + DEFAULT_TIMEOUT_MS
             while (true) {
                 if (SystemClock.elapsedRealtime() > timeout) {
-                    fail("Couldn't make $servicePackage the service package for $defaultSubId: " +
+                    fail(
+                        "Couldn't make $servicePackage the service package for $defaultSubId: " +
                             "dumpsys connectivity".execute().split("\n")
-                                    .filter { it.contains("Logical slot = $defaultSlotIndex.*") })
+                                    .filter { it.contains("Logical slot = $defaultSlotIndex.*") }
+                    )
                 }
                 if ("dumpsys connectivity"
                         .execute()
@@ -799,10 +820,18 @@
                 // TODO(b/315136340): Allow ownerUid to see allowedUids and enable below test case
                 // doTestAllowedUids(defaultSubId, TRANSPORT_WIFI, uid, expectUidsPresent = true)
             }
-            doTestAllowedUidsWithSubId(defaultSubId, TRANSPORT_BLUETOOTH, uid,
-                    expectUidsPresent = false)
-            doTestAllowedUidsWithSubId(defaultSubId, intArrayOf(TRANSPORT_CELLULAR, TRANSPORT_WIFI),
-                    uid, expectUidsPresent = false)
+            doTestAllowedUidsWithSubId(
+                defaultSubId,
+                TRANSPORT_BLUETOOTH,
+                uid,
+                    expectUidsPresent = false
+            )
+            doTestAllowedUidsWithSubId(
+                defaultSubId,
+                intArrayOf(TRANSPORT_CELLULAR, TRANSPORT_WIFI),
+                    uid,
+                expectUidsPresent = false
+            )
         }
     }
 
@@ -1860,8 +1889,10 @@
                 it.setTransportInfo(VpnTransportInfo(
                     VpnManager.TYPE_VPN_PLATFORM,
                     sessionId,
-                    /*bypassable=*/ false,
-                    /*longLivedTcpConnectionsExpensive=*/ false
+                    /*bypassable=*/
+                    false,
+                    /*longLivedTcpConnectionsExpensive=*/
+                    false
                 ))
                 it.underlyingNetworks = listOf()
             }
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSL2capProviderTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSL2capProviderTest.kt
index babcba9..ee5b4ee 100644
--- a/tests/unit/java/com/android/server/connectivityservice/CSL2capProviderTest.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/CSL2capProviderTest.kt
@@ -38,12 +38,14 @@
 import android.net.NetworkSpecifier
 import android.net.RouteInfo
 import android.os.Build
+import android.os.Handler
 import android.os.HandlerThread
 import android.os.ParcelFileDescriptor
 import com.android.server.net.L2capNetwork.L2capIpClient
 import com.android.server.net.L2capPacketForwarder
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
 import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.RecorderCallback.CallbackEntry.Lost
 import com.android.testutils.RecorderCallback.CallbackEntry.Reserved
 import com.android.testutils.RecorderCallback.CallbackEntry.Unavailable
 import com.android.testutils.TestableNetworkCallback
@@ -59,6 +61,7 @@
 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.ArgumentMatchers.isNull
 import org.mockito.Mockito.doAnswer
@@ -394,4 +397,34 @@
         val cb2 = requestNetwork(nr)
         cb2.expectAvailableCallbacks(anyNetwork(), validated = false)
     }
+
+    /** Test to ensure onLost() is sent before onUnavailable() when the network is torn down. */
+    @Test
+    fun testClientNetwork_gracefulTearDown() {
+        val specifier = L2capNetworkSpecifier.Builder()
+            .setRole(ROLE_CLIENT)
+            .setHeaderCompression(HEADER_COMPRESSION_NONE)
+            .setRemoteAddress(MacAddress.fromBytes(REMOTE_MAC))
+            .setPsm(PSM)
+            .build()
+
+        val nr = REQUEST.copyWithSpecifier(specifier)
+        val cb = requestNetwork(nr)
+        cb.expectAvailableCallbacks(anyNetwork(), validated = false)
+
+        // Capture the L2capPacketForwarder callback object to tear down the network.
+        val handlerCaptor = ArgumentCaptor.forClass(Handler::class.java)
+        val forwarderCbCaptor = ArgumentCaptor.forClass(L2capPacketForwarder.ICallback::class.java)
+        verify(providerDeps).createL2capPacketForwarder(
+                handlerCaptor.capture(), any(), any(), any(), forwarderCbCaptor.capture())
+        val handler = handlerCaptor.value
+        val forwarderCb = forwarderCbCaptor.value
+
+        // Trigger a forwarding error
+        handler.post { forwarderCb.onError() }
+        handler.waitForIdle(HANDLER_TIMEOUT_MS)
+
+        cb.expect<Lost>()
+        cb.expect<Unavailable>()
+    }
 }
diff --git a/tests/unit/java/com/android/server/net/NetworkStatsFactoryTest.java b/tests/unit/java/com/android/server/net/NetworkStatsFactoryTest.java
index 63daebc..89acf69 100644
--- a/tests/unit/java/com/android/server/net/NetworkStatsFactoryTest.java
+++ b/tests/unit/java/com/android/server/net/NetworkStatsFactoryTest.java
@@ -20,6 +20,7 @@
 import static android.net.NetworkStats.DEFAULT_NETWORK_NO;
 import static android.net.NetworkStats.METERED_ALL;
 import static android.net.NetworkStats.METERED_NO;
+import static android.net.NetworkStats.METERED_YES;
 import static android.net.NetworkStats.ROAMING_ALL;
 import static android.net.NetworkStats.ROAMING_NO;
 import static android.net.NetworkStats.SET_ALL;
@@ -29,6 +30,8 @@
 import static android.net.NetworkStats.TAG_NONE;
 import static android.net.NetworkStats.UID_ALL;
 
+import static com.android.server.net.NetworkStatsFactory.CONFIG_PER_UID_TAG_THROTTLING;
+import static com.android.server.net.NetworkStatsFactory.CONFIG_PER_UID_TAG_THROTTLING_THRESHOLD;
 import static com.android.server.net.NetworkStatsFactory.kernelToTag;
 import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
 
@@ -36,6 +39,9 @@
 import static org.junit.Assert.fail;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.doReturn;
 
 import android.content.Context;
@@ -52,12 +58,15 @@
 import com.android.server.BpfNetMaps;
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRunner;
+import com.android.testutils.com.android.testutils.SetFeatureFlagsRule;
+import com.android.testutils.com.android.testutils.SetFeatureFlagsRule.FeatureFlag;
 
 import libcore.io.IoUtils;
 import libcore.testing.io.TestIoUtils;
 
 import org.junit.After;
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
@@ -66,6 +75,7 @@
 import java.io.File;
 import java.io.IOException;
 import java.net.ProtocolException;
+import java.util.HashMap;
 
 /** Tests for {@link NetworkStatsFactory}. */
 @RunWith(DevSdkIgnoreRunner.class)
@@ -73,6 +83,7 @@
 @DevSdkIgnoreRule.IgnoreUpTo(SC_V2)
 public class NetworkStatsFactoryTest extends NetworkStatsBaseTest {
     private static final String CLAT_PREFIX = "v4-";
+    private static final int TEST_TAGS_PER_UID_THRESHOLD = 10;
 
     private File mTestProc;
     private NetworkStatsFactory mFactory;
@@ -80,6 +91,16 @@
     @Mock private NetworkStatsFactory.Dependencies mDeps;
     @Mock private BpfNetMaps mBpfNetMaps;
 
+    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;
+            }, (name) -> mFeatureFlags.getOrDefault(name, false));
+
     @Before
     public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
@@ -90,6 +111,10 @@
         // related to networkStatsFactory is compiled to a minimal native library and loaded here.
         System.loadLibrary("networkstatsfactorytestjni");
         doReturn(mBpfNetMaps).when(mDeps).createBpfNetMaps(any());
+        doAnswer(invocation -> mFeatureFlags.getOrDefault((String) invocation.getArgument(1), true))
+            .when(mDeps).isFeatureNotChickenedOut(any(), anyString());
+        doReturn(TEST_TAGS_PER_UID_THRESHOLD).when(mDeps)
+                .getDeviceConfigPropertyInt(eq(CONFIG_PER_UID_TAG_THROTTLING_THRESHOLD), anyInt());
 
         mFactory = new NetworkStatsFactory(mContext, mDeps);
         mFactory.updateUnderlyingNetworkInfos(new UnderlyingNetworkInfo[0]);
@@ -498,6 +523,71 @@
         assertValues(removedUidsStats, TEST_IFACE, UID_GREEN, 64L, 3L, 1024L, 8L);
     }
 
+    @FeatureFlag(name = CONFIG_PER_UID_TAG_THROTTLING)
+    @Test
+    public void testFilterTooManyTags_featureEnabled() throws Exception {
+        doTestFilterTooManyTags(true);
+    }
+
+    @FeatureFlag(name = CONFIG_PER_UID_TAG_THROTTLING, enabled = false)
+    @Test
+    public void testFilterTooManyTags_featureDisabled() throws Exception {
+        doTestFilterTooManyTags(false);
+    }
+
+    private void doTestFilterTooManyTags(boolean supportPerUidTagThrottling) throws Exception {
+        // Add entries for UID_RED which reaches the threshold.
+        final NetworkStats statsWithManyTags = new NetworkStats(0L, TEST_TAGS_PER_UID_THRESHOLD);
+        for (int tag = 1; tag <= TEST_TAGS_PER_UID_THRESHOLD; tag++) {
+            statsWithManyTags.combineValues(
+                    new NetworkStats.Entry(TEST_IFACE, UID_RED, SET_DEFAULT, tag,
+                            METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 12L, 18L, 14L, 1L, 0L));
+        }
+        doReturn(statsWithManyTags).when(mDeps).getNetworkStatsDetail();
+        final NetworkStats stats1 = mFactory.readNetworkStatsDetail();
+        assertEquals(stats1.size(), TEST_TAGS_PER_UID_THRESHOLD);
+
+        // Add 2 new entries with pre-existing tag, verify they can be added no matter what.
+        final NetworkStats newDiffWithExistingTag = new NetworkStats(0L, 2);
+        // This one should be added as a new entry, as the metered data doesn't exist yet.
+        newDiffWithExistingTag.combineValues(
+                new NetworkStats.Entry(TEST_IFACE, UID_RED, SET_DEFAULT,
+                        TEST_TAGS_PER_UID_THRESHOLD,
+                        METERED_YES, ROAMING_NO, DEFAULT_NETWORK_NO, 3L, 5L, 8L, 1L, 1L));
+        // This one should be combined into existing entry.
+        newDiffWithExistingTag.combineValues(
+                new NetworkStats.Entry(TEST_IFACE, UID_RED, SET_DEFAULT,
+                        TEST_TAGS_PER_UID_THRESHOLD,
+                        METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 1L, 2L, 3L, 4L, 5L));
+
+        doReturn(newDiffWithExistingTag).when(mDeps).getNetworkStatsDetail();
+        final NetworkStats stats2 = mFactory.readNetworkStatsDetail();
+        assertEquals(stats2.size(), TEST_TAGS_PER_UID_THRESHOLD + 1);
+        assertValues(stats2, TEST_IFACE, UID_RED, SET_DEFAULT, TEST_TAGS_PER_UID_THRESHOLD,
+                METERED_YES, ROAMING_NO, DEFAULT_NETWORK_NO, 3L, 5L, 8L, 1L, 1L);
+        assertValues(stats2, TEST_IFACE, UID_RED, SET_DEFAULT, TEST_TAGS_PER_UID_THRESHOLD,
+                METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 13L, 20L, 17L, 5L, 5L);
+
+        // Add an entry which exceeds the threshold, verify the entry is filtered out.
+        final NetworkStats newDiffWithNonExistingTag = new NetworkStats(0L, 1);
+        newDiffWithNonExistingTag.combineValues(
+                new NetworkStats.Entry(TEST_IFACE, UID_RED, SET_DEFAULT,
+                        TEST_TAGS_PER_UID_THRESHOLD + 1,
+                        METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 12L, 18L, 14L, 1L, 0L));
+        doReturn(newDiffWithNonExistingTag).when(mDeps).getNetworkStatsDetail();
+        final NetworkStats stats3 = mFactory.readNetworkStatsDetail();
+        if (supportPerUidTagThrottling) {
+            assertEquals(stats3.size(), TEST_TAGS_PER_UID_THRESHOLD + 1);
+            assertNoStatsEntry(stats3, TEST_IFACE, UID_RED, SET_DEFAULT,
+                    TEST_TAGS_PER_UID_THRESHOLD + 1);
+        } else {
+            assertEquals(stats3.size(), TEST_TAGS_PER_UID_THRESHOLD + 2);
+            assertValues(stats3, TEST_IFACE, UID_RED, SET_DEFAULT,
+                    TEST_TAGS_PER_UID_THRESHOLD + 1,
+                    METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 12L, 18L, 14L, 1L, 0L);
+        }
+    }
+
     private NetworkStats buildEmptyStats() {
         return new NetworkStats(SystemClock.elapsedRealtime(), 0);
     }
diff --git a/thread/docs/build-an-android-border-router.md b/thread/docs/build-an-android-border-router.md
index f90a23b..2687e26 100644
--- a/thread/docs/build-an-android-border-router.md
+++ b/thread/docs/build-an-android-border-router.md
@@ -380,10 +380,12 @@
 [config_thread.xml](https://cs.android.com/android/platform/superproject/main/+/main:packages/modules/Connectivity/service/ServiceConnectivityResources/res/values/config_thread.xml)
 for the full list.
 
-Typically, you must change the `config_thread_vendor_name`,
-`config_thread_vendor_oui` and `config_thread_model_name` to your vendor or
-product values. Those values will be included in the `_meshcop._udp` mDNS
-service which is always advertised by a Thread Border Router.
+Typically, you must set `config_thread_border_router_default_enabled` to `true`
+to enable your device as a Thread Border Router, and change the
+`config_thread_vendor_name`, `config_thread_vendor_oui` and
+`config_thread_model_name` to your vendor or product values. Those values will
+be included in the `_meshcop._udp` mDNS service which is always advertised by a
+Thread Border Router.
 
 To add the overlay, you need to create a new `ConnectivityOverlayOrange`
 runtime_resource_overlay target for your Orange device. Create a new
@@ -436,6 +438,7 @@
   ```
 - `config_thread.xml`:
   ```
+  <bool name="config_thread_border_router_default_enabled">true</bool>
   <string translatable="false" name="config_thread_vendor_name">Banana Inc.</string>
   <string translatable="false" name="config_thread_vendor_oui">AC:DE:48</string>
   <string translatable="false" name="config_thread_model_name">Orange</string>
diff --git a/thread/tests/integration/AndroidManifest.xml b/thread/tests/integration/AndroidManifest.xml
index a049184..8bee1e1 100644
--- a/thread/tests/integration/AndroidManifest.xml
+++ b/thread/tests/integration/AndroidManifest.xml
@@ -24,6 +24,7 @@
     <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>
     <uses-permission android:name="android.permission.THREAD_NETWORK_PRIVILEGED"/>
     <uses-permission android:name="android.permission.NETWORK_SETTINGS"/>
+    <uses-permission android:name="android.permission.NEARBY_WIFI_DEVICES" />
     <uses-permission android:name="android.permission.INTERNET"/>
 
     <application android:debuggable="true">
diff --git a/thread/tests/integration/src/android/net/thread/ThreadIntegrationTest.java b/thread/tests/integration/src/android/net/thread/ThreadIntegrationTest.java
index b608c5d..70f17ff 100644
--- a/thread/tests/integration/src/android/net/thread/ThreadIntegrationTest.java
+++ b/thread/tests/integration/src/android/net/thread/ThreadIntegrationTest.java
@@ -27,6 +27,8 @@
 import static android.net.thread.utils.ThreadNetworkControllerWrapper.JOIN_TIMEOUT;
 import static android.os.SystemClock.elapsedRealtime;
 
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
 import static com.android.compatibility.common.util.SystemUtil.runShellCommand;
 import static com.android.compatibility.common.util.SystemUtil.runShellCommandOrThrow;
 import static com.android.testutils.TestPermissionUtil.runAsShell;
@@ -121,6 +123,12 @@
 
     @Before
     public void setUp() throws Exception {
+        getInstrumentation()
+                .getUiAutomation()
+                .grantRuntimePermission(
+                        "com.android.thread.tests.integration",
+                        "android.permission.NEARBY_WIFI_DEVICES");
+
         mExecutor = Executors.newSingleThreadExecutor();
         mFtd = new FullThreadDevice(10 /* nodeId */);
         mOtCtl = new OtDaemonController();