Merge "Remove unused function" into main
diff --git a/TEST_MAPPING b/TEST_MAPPING
index 375397b..4cf93a8 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -77,18 +77,6 @@
       "name": "libnetworkstats_test"
     },
     {
-      "name": "NetHttpCoverageTests",
-      "options": [
-        {
-          "exclude-annotation": "com.android.testutils.SkipPresubmit"
-        },
-        {
-          // These sometimes take longer than 1 min which is the presubmit timeout
-          "exclude-annotation": "androidx.test.filters.LargeTest"
-        }
-      ]
-    },
-    {
       "name": "CtsTetheringTestLatestSdk",
       "options": [
         {
diff --git a/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java b/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
index 81e18ab..00d9152 100644
--- a/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
+++ b/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
@@ -69,7 +69,6 @@
 import com.android.net.module.util.InterfaceParams;
 import com.android.net.module.util.NetworkStackConstants;
 import com.android.net.module.util.SharedLog;
-import com.android.net.module.util.Struct;
 import com.android.net.module.util.Struct.S32;
 import com.android.net.module.util.bpf.Tether4Key;
 import com.android.net.module.util.bpf.Tether4Value;
@@ -1343,19 +1342,6 @@
         pw.decreaseIndent();
     }
 
-    private <K extends Struct, V extends Struct> void dumpRawMap(IBpfMap<K, V> map,
-            IndentingPrintWriter pw) throws ErrnoException {
-        if (map == null) {
-            pw.println("No BPF support");
-            return;
-        }
-        if (map.isEmpty()) {
-            pw.println("No entries");
-            return;
-        }
-        map.forEach((k, v) -> pw.println(BpfDump.toBase64EncodedString(k, v)));
-    }
-
     /**
      * Dump raw BPF map into the base64 encoded strings "<base64 key>,<base64 value>".
      * Allow to dump only one map path once. For test only.
@@ -1375,16 +1361,16 @@
         // TODO: dump downstream4 map.
         if (CollectionUtils.contains(args, DUMPSYS_RAWMAP_ARG_STATS)) {
             try (IBpfMap<TetherStatsKey, TetherStatsValue> statsMap = mDeps.getBpfStatsMap()) {
-                dumpRawMap(statsMap, pw);
-            } catch (ErrnoException | IOException e) {
+                BpfDump.dumpRawMap(statsMap, pw);
+            } catch (IOException e) {
                 pw.println("Error dumping stats map: " + e);
             }
             return;
         }
         if (CollectionUtils.contains(args, DUMPSYS_RAWMAP_ARG_UPSTREAM4)) {
             try (IBpfMap<Tether4Key, Tether4Value> upstreamMap = mDeps.getBpfUpstream4Map()) {
-                dumpRawMap(upstreamMap, pw);
-            } catch (ErrnoException | IOException e) {
+                BpfDump.dumpRawMap(upstreamMap, pw);
+            } catch (IOException e) {
                 pw.println("Error dumping IPv4 map: " + e);
             }
             return;
diff --git a/Tethering/tests/privileged/src/com/android/networkstack/tethering/BpfMapTest.java b/Tethering/tests/privileged/src/com/android/networkstack/tethering/BpfMapTest.java
index d5d71bc..f8aa69f 100644
--- a/Tethering/tests/privileged/src/com/android/networkstack/tethering/BpfMapTest.java
+++ b/Tethering/tests/privileged/src/com/android/networkstack/tethering/BpfMapTest.java
@@ -22,19 +22,24 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeTrue;
 
 import android.net.MacAddress;
 import android.os.Build;
+import android.os.SystemClock;
 import android.system.ErrnoException;
 import android.system.Os;
 import android.system.OsConstants;
 import android.util.ArrayMap;
 
 import com.android.net.module.util.BpfMap;
+import com.android.net.module.util.SingleWriterBpfMap;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
 import com.android.testutils.DevSdkIgnoreRunner;
 
@@ -42,10 +47,18 @@
 import org.junit.BeforeClass;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
 
 import java.io.File;
 import java.net.InetAddress;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.Collection;
 import java.util.NoSuchElementException;
+import java.util.Random;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicInteger;
 
 
@@ -61,6 +74,17 @@
 
     private BpfMap<TetherDownstream6Key, Tether6Value> mTestMap;
 
+    private final boolean mShouldTestSingleWriterMap;
+
+    @Parameterized.Parameters
+    public static Collection<Boolean> shouldTestSingleWriterMap() {
+        return Arrays.asList(true, false);
+    }
+
+    public BpfMapTest(boolean shouldTestSingleWriterMap) {
+        mShouldTestSingleWriterMap = shouldTestSingleWriterMap;
+    }
+
     @BeforeClass
     public static void setupOnce() {
         System.loadLibrary(getTetheringJniLibraryName());
@@ -82,11 +106,16 @@
         initTestMap();
     }
 
-    private void initTestMap() throws Exception {
-        mTestMap = new BpfMap<>(
-                TETHER_DOWNSTREAM6_FS_PATH,
-                TetherDownstream6Key.class, Tether6Value.class);
+    private BpfMap<TetherDownstream6Key, Tether6Value> openTestMap() throws Exception {
+        return mShouldTestSingleWriterMap
+                ? new SingleWriterBpfMap<>(TETHER_DOWNSTREAM6_FS_PATH, TetherDownstream6Key.class,
+                Tether6Value.class)
+                : new BpfMap<>(TETHER_DOWNSTREAM6_FS_PATH, TetherDownstream6Key.class,
+                        Tether6Value.class);
+    }
 
+    private void initTestMap() throws Exception {
+        mTestMap = openTestMap();
         mTestMap.forEach((key, value) -> {
             try {
                 assertTrue(mTestMap.deleteEntry(key));
@@ -357,6 +386,25 @@
     }
 
     @Test
+    public void testMapContentsCorrectOnOpen() throws Exception {
+        final BpfMap<TetherDownstream6Key, Tether6Value> map1, map2;
+
+        map1 = openTestMap();
+        map1.clear();
+        for (int i = 0; i < mTestData.size(); i++) {
+            map1.insertEntry(mTestData.keyAt(i), mTestData.valueAt(i));
+        }
+
+        // We can't close and reopen map1, because close does nothing. Open another map instead.
+        map2 = openTestMap();
+        for (int i = 0; i < mTestData.size(); i++) {
+            assertEquals(mTestData.valueAt(i), map2.getValue(mTestData.keyAt(i)));
+        }
+
+        map1.clear();
+    }
+
+    @Test
     public void testInsertOverflow() throws Exception {
         final ArrayMap<TetherDownstream6Key, Tether6Value> testData =
                 new ArrayMap<>();
@@ -396,8 +444,18 @@
         }
     }
 
-    private static int getNumOpenFds() {
-        return new File("/proc/" + Os.getpid() + "/fd").listFiles().length;
+    private static int getNumOpenBpfMapFds() throws Exception {
+        int numFds = 0;
+        File[] openFiles = new File("/proc/self/fd").listFiles();
+        for (int i = 0; i < openFiles.length; i++) {
+            final Path path = openFiles[i].toPath();
+            if (!Files.isSymbolicLink(path)) continue;
+            if ("anon_inode:bpf-map".equals(Files.readSymbolicLink(path).toString())) {
+                numFds++;
+            }
+        }
+        assertNotEquals("Couldn't find any BPF map fds opened by this process", 0, numFds);
+        return numFds;
     }
 
     @Test
@@ -406,7 +464,7 @@
         // cache, expect that the fd amount is not increased in the iterations.
         // See the comment of BpfMap#close.
         final int iterations = 1000;
-        final int before = getNumOpenFds();
+        final int before = getNumOpenBpfMapFds();
         for (int i = 0; i < iterations; i++) {
             try (BpfMap<TetherDownstream6Key, Tether6Value> map = new BpfMap<>(
                     TETHER_DOWNSTREAM6_FS_PATH,
@@ -414,15 +472,74 @@
                 // do nothing
             }
         }
-        final int after = getNumOpenFds();
+        final int after = getNumOpenBpfMapFds();
 
         // Check that the number of open fds is the same as before.
-        // If this exact match becomes flaky, we probably need to distinguish that fd is belong
-        // to "bpf-map".
-        // ex:
-        // $ adb shell ls -all /proc/16196/fd
-        // [..] network_stack 64 2022-07-26 22:01:02.300002956 +0800 749 -> anon_inode:bpf-map
-        // [..] network_stack 64 2022-07-26 22:01:02.188002956 +0800 75 -> anon_inode:[eventfd]
         assertEquals("Fd leak after " + iterations + " iterations: ", before, after);
     }
+
+    @Test
+    public void testNullKey() {
+        assertThrows(NullPointerException.class, () ->
+                mTestMap.insertOrReplaceEntry(null, mTestData.valueAt(0)));
+    }
+
+    private void runBenchmarkThread(BpfMap<TetherDownstream6Key, Tether6Value> map,
+            CompletableFuture<Integer> future, int runtimeMs) {
+        int numReads = 0;
+        final Random r = new Random();
+        final long start = SystemClock.elapsedRealtime();
+        final long stop = start + runtimeMs;
+        while (SystemClock.elapsedRealtime() < stop) {
+            try {
+                final Tether6Value v = map.getValue(mTestData.keyAt(r.nextInt(mTestData.size())));
+                assertNotNull(v);
+                numReads++;
+            } catch (Exception e) {
+                future.completeExceptionally(e);
+                return;
+            }
+        }
+        future.complete(numReads);
+    }
+
+    @Test
+    public void testSingleWriterCacheEffectiveness() throws Exception {
+        assumeTrue(mShouldTestSingleWriterMap);
+
+        // Ensure the map is not empty.
+        for (int i = 0; i < mTestData.size(); i++) {
+            mTestMap.insertEntry(mTestData.keyAt(i), mTestData.valueAt(i));
+        }
+
+        // Benchmark parameters.
+        final int timeoutMs = 5_000;  // Only hit if threads don't complete.
+        final int benchmarkTimeMs = 2_000;
+        final int minReads = 50;
+        // Local testing on cuttlefish suggests that caching is ~10x faster.
+        // Only require 3x to reduce test flakiness.
+        final int expectedSpeedup = 3;
+
+        final BpfMap cachedMap = new SingleWriterBpfMap(TETHER_DOWNSTREAM6_FS_PATH,
+                TetherDownstream6Key.class, Tether6Value.class);
+        final BpfMap uncachedMap = new BpfMap(TETHER_DOWNSTREAM6_FS_PATH,
+                TetherDownstream6Key.class, Tether6Value.class);
+
+        final CompletableFuture<Integer> cachedResult = new CompletableFuture<>();
+        final CompletableFuture<Integer> uncachedResult = new CompletableFuture<>();
+
+        new Thread(() -> runBenchmarkThread(uncachedMap, uncachedResult, benchmarkTimeMs)).start();
+        new Thread(() -> runBenchmarkThread(cachedMap, cachedResult, benchmarkTimeMs)).start();
+
+        final int cached = cachedResult.get(timeoutMs, TimeUnit.MILLISECONDS);
+        final int uncached = uncachedResult.get(timeoutMs, TimeUnit.MILLISECONDS);
+
+        // Uncomment to see benchmark results.
+        // fail("Cached " + cached + ", uncached " + uncached + ": " + cached / uncached  +"x");
+
+        assertTrue("Less than " + minReads + "cached reads observed", cached > minReads);
+        assertTrue("Less than " + minReads + "uncached reads observed", uncached > minReads);
+        assertTrue("Cached map not at least " + expectedSpeedup + "x faster",
+                cached > expectedSpeedup * uncached);
+    }
 }
diff --git a/common/flags.aconfig b/common/flags.aconfig
index 6c3e89d..bc4168b 100644
--- a/common/flags.aconfig
+++ b/common/flags.aconfig
@@ -107,3 +107,11 @@
   description: "Flag for BLOCKED_REASON_NETWORK_RESTRICTED API"
   bug: "339559837"
 }
+
+flag {
+  name: "net_capability_not_bandwidth_constrained"
+  is_exported: true
+  namespace: "android_core_networking"
+  description: "Flag for NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED API"
+  bug: "343823469"
+}
diff --git a/framework-t/src/android/net/nsd/NsdManager.java b/framework-t/src/android/net/nsd/NsdManager.java
index 48d40e6..b21e22a 100644
--- a/framework-t/src/android/net/nsd/NsdManager.java
+++ b/framework-t/src/android/net/nsd/NsdManager.java
@@ -1782,16 +1782,16 @@
         if (!hasServiceName && !hasServiceType && serviceInfo.getPort() == 0) {
             return ServiceValidationType.NO_SERVICE;
         }
-        if (hasServiceName && hasServiceType) {
-            if (serviceInfo.getPort() < 0) {
-                throw new IllegalArgumentException("Invalid port");
-            }
-            if (serviceInfo.getPort() == 0) {
-                return ServiceValidationType.HAS_SERVICE_ZERO_PORT;
-            }
-            return ServiceValidationType.HAS_SERVICE;
+        if (!hasServiceName || !hasServiceType) {
+            throw new IllegalArgumentException("The service name or the service type is missing");
         }
-        throw new IllegalArgumentException("The service name or the service type is missing");
+        if (serviceInfo.getPort() < 0) {
+            throw new IllegalArgumentException("Invalid port");
+        }
+        if (serviceInfo.getPort() == 0) {
+            return ServiceValidationType.HAS_SERVICE_ZERO_PORT;
+        }
+        return ServiceValidationType.HAS_SERVICE;
     }
 
     /**
diff --git a/framework/api/current.txt b/framework/api/current.txt
index ef8415c..7bc0cf3 100644
--- a/framework/api/current.txt
+++ b/framework/api/current.txt
@@ -337,6 +337,7 @@
     field public static final int NET_CAPABILITY_MCX = 23; // 0x17
     field public static final int NET_CAPABILITY_MMS = 0; // 0x0
     field public static final int NET_CAPABILITY_MMTEL = 33; // 0x21
+    field @FlaggedApi("com.android.net.flags.net_capability_not_bandwidth_constrained") public static final int NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED = 37; // 0x25
     field public static final int NET_CAPABILITY_NOT_CONGESTED = 20; // 0x14
     field public static final int NET_CAPABILITY_NOT_METERED = 11; // 0xb
     field public static final int NET_CAPABILITY_NOT_RESTRICTED = 13; // 0xd
diff --git a/framework/src/android/net/ConnectivityManager.java b/framework/src/android/net/ConnectivityManager.java
index 0b37fa5..4eaf973 100644
--- a/framework/src/android/net/ConnectivityManager.java
+++ b/framework/src/android/net/ConnectivityManager.java
@@ -28,6 +28,7 @@
 import android.annotation.CallbackExecutor;
 import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
+import android.annotation.LongDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.RequiresApi;
@@ -1207,6 +1208,16 @@
     })
     public @interface FirewallRule {}
 
+    /** @hide */
+    public static final long FEATURE_USE_DECLARED_METHODS_FOR_CALLBACKS = 1L;
+
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @LongDef(flag = true, prefix = "FEATURE_", value = {
+            FEATURE_USE_DECLARED_METHODS_FOR_CALLBACKS
+    })
+    public @interface ConnectivityManagerFeature {}
+
     /**
      * A kludge to facilitate static access where a Context pointer isn't available, like in the
      * case of the static set/getProcessDefaultNetwork methods and from the Network class.
@@ -1220,6 +1231,14 @@
     @GuardedBy("mTetheringEventCallbacks")
     private TetheringManager mTetheringManager;
 
+    private final Object mEnabledConnectivityManagerFeaturesLock = new Object();
+    // mEnabledConnectivityManagerFeatures is lazy-loaded in this ConnectivityManager instance, but
+    // fetched from ConnectivityService, where it is loaded in ConnectivityService startup, so it
+    // should have consistent values.
+    @GuardedBy("sEnabledConnectivityManagerFeaturesLock")
+    @ConnectivityManagerFeature
+    private Long mEnabledConnectivityManagerFeatures = null;
+
     private TetheringManager getTetheringManager() {
         synchronized (mTetheringEventCallbacks) {
             if (mTetheringManager == null) {
@@ -4529,6 +4548,20 @@
         return request;
     }
 
+    private boolean isFeatureEnabled(@ConnectivityManagerFeature long connectivityManagerFeature) {
+        synchronized (mEnabledConnectivityManagerFeaturesLock) {
+            if (mEnabledConnectivityManagerFeatures == null) {
+                try {
+                    mEnabledConnectivityManagerFeatures =
+                            mService.getEnabledConnectivityManagerFeatures();
+                } catch (RemoteException e) {
+                    e.rethrowFromSystemServer();
+                }
+            }
+            return (mEnabledConnectivityManagerFeatures & connectivityManagerFeature) != 0;
+        }
+    }
+
     private NetworkRequest sendRequestForNetwork(NetworkCapabilities need, NetworkCallback callback,
             int timeoutMs, NetworkRequest.Type reqType, int legacyType, CallbackHandler handler) {
         return sendRequestForNetwork(Process.INVALID_UID, need, callback, timeoutMs, reqType,
diff --git a/framework/src/android/net/IConnectivityManager.aidl b/framework/src/android/net/IConnectivityManager.aidl
index 55c7085..f9de8ed 100644
--- a/framework/src/android/net/IConnectivityManager.aidl
+++ b/framework/src/android/net/IConnectivityManager.aidl
@@ -259,4 +259,6 @@
     void setTestLowTcpPollingTimerForKeepalive(long timeMs);
 
     IBinder getRoutingCoordinatorService();
+
+    long getEnabledConnectivityManagerFeatures();
 }
diff --git a/framework/src/android/net/NetworkCapabilities.java b/framework/src/android/net/NetworkCapabilities.java
index 45efbfe..6a14bde 100644
--- a/framework/src/android/net/NetworkCapabilities.java
+++ b/framework/src/android/net/NetworkCapabilities.java
@@ -36,9 +36,11 @@
 import android.os.Process;
 import android.text.TextUtils;
 import android.util.ArraySet;
+import android.util.Log;
 import android.util.Range;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.modules.utils.build.SdkLevel;
 import com.android.net.module.util.BitUtils;
 import com.android.net.module.util.CollectionUtils;
 import com.android.net.module.util.NetworkCapabilitiesUtils;
@@ -134,6 +136,8 @@
                 "com.android.net.flags.request_restricted_wifi";
         static final String SUPPORT_TRANSPORT_SATELLITE =
                 "com.android.net.flags.support_transport_satellite";
+        static final String NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED =
+                "com.android.net.flags.net_capability_not_bandwidth_constrained";
     }
 
     /**
@@ -458,6 +462,7 @@
             NET_CAPABILITY_PRIORITIZE_LATENCY,
             NET_CAPABILITY_PRIORITIZE_BANDWIDTH,
             NET_CAPABILITY_LOCAL_NETWORK,
+            NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED,
     })
     public @interface NetCapability { }
 
@@ -740,7 +745,26 @@
     @FlaggedApi(Flags.FLAG_NET_CAPABILITY_LOCAL_NETWORK)
     public static final int NET_CAPABILITY_LOCAL_NETWORK = 36;
 
-    private static final int MAX_NET_CAPABILITY = NET_CAPABILITY_LOCAL_NETWORK;
+    /**
+     * Indicates that this is not a bandwidth-constrained network.
+     *
+     * Starting from {@link Build.VERSION_CODES.VANILLA_ICE_CREAM}, this capability is by default
+     * set in {@link NetworkRequest}s and true for most networks.
+     *
+     * If a network lacks this capability, it is bandwidth-constrained. Bandwidth constrained
+     * networks cannot support high-bandwidth data transfers and applications that request and use
+     * them must ensure that they limit bandwidth usage to below the values returned by
+     * {@link #getLinkDownstreamBandwidthKbps()} and {@link #getLinkUpstreamBandwidthKbps()} and
+     * limit the frequency of their network usage. If applications perform high-bandwidth data
+     * transfers on constrained networks or perform network access too frequently, the system may
+     * block the app's access to the network. The system may take other measures to reduce network
+     * usage on constrained networks, such as disabling network access to apps that are not in the
+     * foreground.
+     */
+    @FlaggedApi(Flags.NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED)
+    public static final int NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED = 37;
+
+    private static final int MAX_NET_CAPABILITY = NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED;
 
     // Set all bits up to the MAX_NET_CAPABILITY-th bit
     private static final long ALL_VALID_CAPABILITIES = (2L << MAX_NET_CAPABILITY) - 1;
@@ -784,10 +808,17 @@
     /**
      * Capabilities that are set by default when the object is constructed.
      */
-    private static final long DEFAULT_CAPABILITIES =
-            (1L << NET_CAPABILITY_NOT_RESTRICTED) |
-            (1L << NET_CAPABILITY_TRUSTED) |
-            (1L << NET_CAPABILITY_NOT_VPN);
+    private static final long DEFAULT_CAPABILITIES;
+    static {
+        long defaultCapabilities =
+                (1L << NET_CAPABILITY_NOT_RESTRICTED)
+                | (1L << NET_CAPABILITY_TRUSTED)
+                | (1L << NET_CAPABILITY_NOT_VPN);
+        if (SdkLevel.isAtLeastV()) {
+            defaultCapabilities |= (1L << NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED);
+        }
+        DEFAULT_CAPABILITIES = defaultCapabilities;
+    }
 
     /**
      * Capabilities that are managed by ConnectivityService.
@@ -814,7 +845,9 @@
             (1L << NET_CAPABILITY_NOT_ROAMING) |
             (1L << NET_CAPABILITY_NOT_CONGESTED) |
             (1L << NET_CAPABILITY_NOT_SUSPENDED) |
-            (1L << NET_CAPABILITY_NOT_VCN_MANAGED);
+            (1L << NET_CAPABILITY_NOT_VCN_MANAGED) |
+            (1L << NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED);
+
 
     /**
      * Extra allowed capabilities for test networks that do not have TRANSPORT_CELLULAR. Test
@@ -843,7 +876,10 @@
         // If the given capability was previously added to the list of forbidden capabilities
         // then the capability will also be removed from the list of forbidden capabilities.
         // TODO: Add forbidden capabilities to the public API
-        checkValidCapability(capability);
+        if (!isValidCapability(capability)) {
+            Log.e(TAG, "addCapability is called with invalid capability: " + capability);
+            return this;
+        }
         mNetworkCapabilities |= 1L << capability;
         // remove from forbidden capability list
         mForbiddenNetworkCapabilities &= ~(1L << capability);
@@ -864,7 +900,10 @@
      * @hide
      */
     public void addForbiddenCapability(@NetCapability int capability) {
-        checkValidCapability(capability);
+        if (!isValidCapability(capability)) {
+            Log.e(TAG, "addForbiddenCapability is called with invalid capability: " + capability);
+            return;
+        }
         mForbiddenNetworkCapabilities |= 1L << capability;
         mNetworkCapabilities &= ~(1L << capability);  // remove from requested capabilities
     }
@@ -878,7 +917,10 @@
      * @hide
      */
     public @NonNull NetworkCapabilities removeCapability(@NetCapability int capability) {
-        checkValidCapability(capability);
+        if (!isValidCapability(capability)) {
+            Log.e(TAG, "removeCapability is called with invalid capability: " + capability);
+            return this;
+        }
         final long mask = ~(1L << capability);
         mNetworkCapabilities &= mask;
         return this;
@@ -893,7 +935,11 @@
      * @hide
      */
     public @NonNull NetworkCapabilities removeForbiddenCapability(@NetCapability int capability) {
-        checkValidCapability(capability);
+        if (!isValidCapability(capability)) {
+            Log.e(TAG,
+                    "removeForbiddenCapability is called with invalid capability: " + capability);
+            return this;
+        }
         mForbiddenNetworkCapabilities &= ~(1L << capability);
         return this;
     }
@@ -2589,6 +2635,7 @@
             case NET_CAPABILITY_PRIORITIZE_LATENCY:          return "PRIORITIZE_LATENCY";
             case NET_CAPABILITY_PRIORITIZE_BANDWIDTH:        return "PRIORITIZE_BANDWIDTH";
             case NET_CAPABILITY_LOCAL_NETWORK:        return "LOCAL_NETWORK";
+            case NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED:    return "NOT_BANDWIDTH_CONSTRAINED";
             default:                                  return Integer.toString(capability);
         }
     }
@@ -2632,12 +2679,6 @@
         return capability >= 0 && capability <= MAX_NET_CAPABILITY;
     }
 
-    private static void checkValidCapability(@NetworkCapabilities.NetCapability int capability) {
-        if (!isValidCapability(capability)) {
-            throw new IllegalArgumentException("NetworkCapability " + capability + " out of range");
-        }
-    }
-
     private static boolean isValidEnterpriseId(
             @NetworkCapabilities.EnterpriseId int enterpriseId) {
         return enterpriseId >= NET_ENTERPRISE_ID_1
diff --git a/framework/src/android/net/NetworkRequest.java b/framework/src/android/net/NetworkRequest.java
index f7600b2..502ac6f 100644
--- a/framework/src/android/net/NetworkRequest.java
+++ b/framework/src/android/net/NetworkRequest.java
@@ -20,6 +20,7 @@
 import static android.net.NetworkCapabilities.NET_CAPABILITY_DUN;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_FOREGROUND;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED;
@@ -286,7 +287,8 @@
                 NET_CAPABILITY_PARTIAL_CONNECTIVITY,
                 NET_CAPABILITY_TEMPORARILY_NOT_METERED,
                 NET_CAPABILITY_TRUSTED,
-                NET_CAPABILITY_VALIDATED);
+                NET_CAPABILITY_VALIDATED,
+                NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED);
 
         private final NetworkCapabilities mNetworkCapabilities;
 
diff --git a/framework/src/android/net/apf/ApfCapabilities.java b/framework/src/android/net/apf/ApfCapabilities.java
index 8efb182..6b18629 100644
--- a/framework/src/android/net/apf/ApfCapabilities.java
+++ b/framework/src/android/net/apf/ApfCapabilities.java
@@ -112,12 +112,12 @@
     /**
      * Determines whether the APF interpreter advertises support for the data buffer access opcodes
      * LDDW (LoaD Data Word) and STDW (STore Data Word). Full LDDW (LoaD Data Word) and
-     * STDW (STore Data Word) support is present from APFv4 on.
+     * STDW (STore Data Word) support is present from APFv3 on.
      *
      * @return {@code true} if the IWifiStaIface#readApfPacketFilterData is supported.
      */
     public boolean hasDataAccess() {
-        return apfVersionSupported >= 4;
+        return apfVersionSupported > 2;
     }
 
     /**
diff --git a/framework/src/android/net/connectivity/ConnectivityCompatChanges.java b/framework/src/android/net/connectivity/ConnectivityCompatChanges.java
index c726dab..51df8ab 100644
--- a/framework/src/android/net/connectivity/ConnectivityCompatChanges.java
+++ b/framework/src/android/net/connectivity/ConnectivityCompatChanges.java
@@ -121,6 +121,20 @@
     @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM)
     public static final long NETWORK_BLOCKED_WITHOUT_INTERNET_PERMISSION = 333340911L;
 
+    /**
+     * Enable caching for TrafficStats#get* APIs.
+     *
+     * Apps targeting Android V or later or running on Android V or later may take up to several
+     * seconds to see the updated results.
+     * Apps targeting lower android SDKs do not see cached result for backward compatibility,
+     * results of TrafficStats#get* APIs are reflecting network statistics immediately.
+     *
+     * @hide
+     */
+    @ChangeId
+    @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    public static final long ENABLE_TRAFFICSTATS_RATE_LIMIT_CACHE = 74210811L;
+
     private ConnectivityCompatChanges() {
     }
 }
diff --git a/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyManagerTest.java b/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyManagerTest.java
index 832ac03..1e36676 100644
--- a/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyManagerTest.java
+++ b/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyManagerTest.java
@@ -28,9 +28,7 @@
 import static org.junit.Assume.assumeTrue;
 
 import android.app.UiAutomation;
-import android.bluetooth.BluetoothAdapter;
-import android.bluetooth.BluetoothManager;
-import android.bluetooth.cts.BTAdapterUtils;
+import android.bluetooth.test_utils.EnableBluetoothRule;
 import android.content.Context;
 import android.location.LocationManager;
 import android.nearby.BroadcastCallback;
@@ -58,6 +56,7 @@
 import com.android.modules.utils.build.SdkLevel;
 
 import org.junit.Before;
+import org.junit.ClassRule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -76,6 +75,9 @@
 @RunWith(AndroidJUnit4.class)
 @RequiresApi(Build.VERSION_CODES.TIRAMISU)
 public class NearbyManagerTest {
+
+    @ClassRule public static final EnableBluetoothRule sEnableBluetooth = new EnableBluetoothRule();
+
     private static final byte[] SALT = new byte[]{1, 2};
     private static final byte[] SECRET_ID = new byte[]{1, 2, 3, 4};
     private static final byte[] META_DATA_ENCRYPTION_KEY = new byte[14];
@@ -128,8 +130,6 @@
 
         mContext = InstrumentationRegistry.getContext();
         mNearbyManager = mContext.getSystemService(NearbyManager.class);
-
-        enableBluetooth();
     }
 
     @Test
@@ -281,14 +281,6 @@
         assertThrows(SecurityException.class, () -> mNearbyManager.getPoweredOffFindingMode());
     }
 
-    private void enableBluetooth() {
-        BluetoothManager manager = mContext.getSystemService(BluetoothManager.class);
-        BluetoothAdapter bluetoothAdapter = manager.getAdapter();
-        if (!bluetoothAdapter.isEnabled()) {
-            assertThat(BTAdapterUtils.enableAdapter(bluetoothAdapter, mContext)).isTrue();
-        }
-    }
-
     private void enableLocation() {
         LocationManager locationManager = mContext.getSystemService(LocationManager.class);
         UserHandle user = Process.myUserHandle();
diff --git a/netbpfload/NetBpfLoad.cpp b/netbpfload/NetBpfLoad.cpp
index dc67f83..80df552 100644
--- a/netbpfload/NetBpfLoad.cpp
+++ b/netbpfload/NetBpfLoad.cpp
@@ -54,6 +54,7 @@
 namespace android {
 namespace bpf {
 
+using base::StartsWith;
 using base::EndsWith;
 using std::string;
 
@@ -228,6 +229,30 @@
     return !access("/metadata/gsi/dsu/booted", F_OK);
 }
 
+static bool hasGSM() {
+    static string ph = base::GetProperty("gsm.current.phone-type", "");
+    static bool gsm = (ph != "");
+    static bool logged = false;
+    if (!logged) {
+        logged = true;
+        ALOGI("hasGSM(gsm.current.phone-type='%s'): %s", ph.c_str(), gsm ? "true" : "false");
+    }
+    return gsm;
+}
+
+static bool isTV() {
+    if (hasGSM()) return false;  // TVs don't do GSM
+
+    static string key = base::GetProperty("ro.oem.key1", "");
+    static bool tv = StartsWith(key, "ATV00");
+    static bool logged = false;
+    if (!logged) {
+        logged = true;
+        ALOGI("isTV(ro.oem.key1='%s'): %s.", key.c_str(), tv ? "true" : "false");
+    }
+    return tv;
+}
+
 static int doLoad(char** argv, char * const envp[]) {
     const int device_api_level = android_get_device_api_level();
     const bool isAtLeastT = (device_api_level >= __ANDROID_API_T__);
@@ -261,24 +286,36 @@
         return 1;
     }
 
+    // both S and T require kernel 4.9 (and eBpf support)
     if (isAtLeastT && !isAtLeastKernelVersion(4, 9, 0)) {
         ALOGE("Android T requires kernel 4.9.");
         return 1;
     }
 
+    // U bumps the kernel requirement up to 4.14
     if (isAtLeastU && !isAtLeastKernelVersion(4, 14, 0)) {
         ALOGE("Android U requires kernel 4.14.");
         return 1;
     }
 
+    // V bumps the kernel requirement up to 4.19
+    // see also: //system/netd/tests/kernel_test.cpp TestKernel419
     if (isAtLeastV && !isAtLeastKernelVersion(4, 19, 0)) {
         ALOGE("Android V requires kernel 4.19.");
         return 1;
     }
 
-    if (isAtLeastV && isX86() && !isKernel64Bit()) {
+    // Technically already required by U, but only enforce on V+
+    // see also: //system/netd/tests/kernel_test.cpp TestKernel64Bit
+    if (isAtLeastV && isKernel32Bit() && isAtLeastKernelVersion(5, 16, 0)) {
+        ALOGE("Android V+ platform with 32 bit kernel version >= 5.16.0 is unsupported");
+        if (!isTV()) return 1;
+    }
+
+    // Various known ABI layout issues, particularly wrt. bpf and ipsec/xfrm.
+    if (isAtLeastV && isKernel32Bit() && isX86()) {
         ALOGE("Android V requires X86 kernel to be 64-bit.");
-        return 1;
+        if (!isTV()) return 1;
     }
 
     if (isAtLeastV) {
@@ -306,7 +343,6 @@
 
         if (bad && !isGSI()) {
             ALOGE("Unsupported kernel version (%07x).", kernelVersion());
-            sleep(60);
         }
     }
 
@@ -326,12 +362,16 @@
          * Some of these have userspace or kernel workarounds/hacks.
          * Some of them don't...
          * We're going to be removing the hacks.
+         * (for example "ANDROID: xfrm: remove in_compat_syscall() checks").
+         * Note: this check/enforcement only applies to *system* userspace code,
+         * it does not affect unprivileged apps, the 32-on-64 compatibility
+         * problems are AFAIK limited to various CAP_NET_ADMIN protected interfaces.
          *
          * Additionally the 32-bit kernel jit support is poor,
          * and 32-bit userspace on 64-bit kernel bpf ringbuffer compatibility is broken.
          */
         ALOGE("64-bit userspace required on 6.2+ kernels.");
-        return 1;
+        if (!isTV()) return 1;
     }
 
     // Ensure we can determine the Android build type.
diff --git a/netd/BpfHandler.cpp b/netd/BpfHandler.cpp
index 91fec90..925ee50 100644
--- a/netd/BpfHandler.cpp
+++ b/netd/BpfHandler.cpp
@@ -85,31 +85,6 @@
         return Status("U+ platform with kernel version < 4.14.0 is unsupported");
     }
 
-    if (modules::sdklevel::IsAtLeastV()) {
-        // V bumps the kernel requirement up to 4.19
-        // see also: //system/netd/tests/kernel_test.cpp TestKernel419
-        if (!bpf::isAtLeastKernelVersion(4, 19, 0)) {
-            return Status("V+ platform with kernel version < 4.19.0 is unsupported");
-        }
-
-        // Technically already required by U, but only enforce on V+
-        // see also: //system/netd/tests/kernel_test.cpp TestKernel64Bit
-        if (bpf::isKernel32Bit() && bpf::isAtLeastKernelVersion(5, 16, 0)) {
-            return Status("V+ platform with 32 bit kernel, version >= 5.16.0 is unsupported");
-        }
-    }
-
-    // Linux 6.1 is highest version supported by U, starting with V new kernels,
-    // ie. 6.2+ we are dropping various kernel/system userspace 32-on-64 hacks
-    // (for example "ANDROID: xfrm: remove in_compat_syscall() checks").
-    // Note: this check/enforcement only applies to *system* userspace code,
-    // it does not affect unprivileged apps, the 32-on-64 compatibility
-    // problems are AFAIK limited to various CAP_NET_ADMIN protected interfaces.
-    // see also: //system/bpf/bpfloader/BpfLoader.cpp main()
-    if (bpf::isUserspace32bit() && bpf::isAtLeastKernelVersion(6, 2, 0)) {
-        return Status("32 bit userspace with Kernel version >= 6.2.0 is unsupported");
-    }
-
     // U mandates this mount point (though it should also be the case on T)
     if (modules::sdklevel::IsAtLeastU() && !!strcmp(cg2_path, "/sys/fs/cgroup")) {
         return Status("U+ platform with cg2_path != /sys/fs/cgroup is unsupported");
diff --git a/service-t/src/com/android/metrics/NetworkNsdReportedMetrics.java b/service-t/src/com/android/metrics/NetworkNsdReportedMetrics.java
index 385adc6..a92dfaf 100644
--- a/service-t/src/com/android/metrics/NetworkNsdReportedMetrics.java
+++ b/service-t/src/com/android/metrics/NetworkNsdReportedMetrics.java
@@ -193,7 +193,8 @@
      * @param sentQueryCount The count of sent queries before stop discovery.
      */
     public void reportServiceDiscoveryStop(boolean isLegacy, int transactionId, long durationMs,
-            int foundCallbackCount, int lostCallbackCount, int servicesCount, int sentQueryCount) {
+            int foundCallbackCount, int lostCallbackCount, int servicesCount, int sentQueryCount,
+            boolean isServiceFromCache) {
         final Builder builder = makeReportedBuilder(isLegacy, transactionId);
         builder.setType(NsdEventType.NET_DISCOVER);
         builder.setQueryResult(MdnsQueryResult.MQR_SERVICE_DISCOVERY_STOP);
@@ -202,6 +203,7 @@
         builder.setLostCallbackCount(lostCallbackCount);
         builder.setFoundServiceCount(servicesCount);
         builder.setSentQueryCount(sentQueryCount);
+        builder.setIsKnownService(isServiceFromCache);
         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 0a8adf0..f8b0d53 100644
--- a/service-t/src/com/android/server/NsdService.java
+++ b/service-t/src/com/android/server/NsdService.java
@@ -1634,6 +1634,12 @@
                         NsdManager.nameOf(code), transactionId));
                 switch (code) {
                     case NsdManager.SERVICE_FOUND:
+                        // Set the ServiceFromCache flag only if the service is actually being
+                        // retrieved from the cache. This flag should not be overridden by later
+                        // service found event, which may not be cached.
+                        if (event.mIsServiceFromCache) {
+                            request.setServiceFromCache(true);
+                        }
                         clientInfo.onServiceFound(clientRequestId, info, request);
                         break;
                     case NsdManager.SERVICE_LOST:
@@ -2887,7 +2893,8 @@
                                 request.getFoundServiceCount(),
                                 request.getLostServiceCount(),
                                 request.getServicesCount(),
-                                request.getSentQueryCount());
+                                request.getSentQueryCount(),
+                                request.isServiceFromCache());
                     } else if (listener instanceof ResolutionListener) {
                         mMetrics.reportServiceResolutionStop(false /* isLegacy */, transactionId,
                                 request.calculateRequestDurationMs(mClock.elapsedRealtime()),
@@ -2927,7 +2934,8 @@
                                 request.getFoundServiceCount(),
                                 request.getLostServiceCount(),
                                 request.getServicesCount(),
-                                NO_SENT_QUERY_COUNT);
+                                NO_SENT_QUERY_COUNT,
+                                request.isServiceFromCache());
                         break;
                     case NsdManager.RESOLVE_SERVICE:
                         stopResolveService(transactionId);
@@ -3050,7 +3058,8 @@
                     request.getFoundServiceCount(),
                     request.getLostServiceCount(),
                     request.getServicesCount(),
-                    request.getSentQueryCount());
+                    request.getSentQueryCount(),
+                    request.isServiceFromCache());
             try {
                 mCb.onStopDiscoverySucceeded(listenerKey);
             } catch (RemoteException e) {
diff --git a/service-t/src/com/android/server/net/NetworkStatsService.java b/service-t/src/com/android/server/net/NetworkStatsService.java
index 8305c1e..5323392 100644
--- a/service-t/src/com/android/server/net/NetworkStatsService.java
+++ b/service-t/src/com/android/server/net/NetworkStatsService.java
@@ -57,6 +57,7 @@
 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.connectivity.ConnectivityCompatChanges.ENABLE_TRAFFICSTATS_RATE_LIMIT_CACHE;
 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;
@@ -91,6 +92,7 @@
 import android.app.AlarmManager;
 import android.app.BroadcastOptions;
 import android.app.PendingIntent;
+import android.app.compat.CompatChanges;
 import android.app.usage.NetworkStatsManager;
 import android.content.ApexEnvironment;
 import android.content.BroadcastReceiver;
@@ -472,7 +474,9 @@
     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 boolean mAlwaysUseTrafficStatsRateLimitCache;
+    private final int mTrafficStatsRateLimitCacheExpiryDuration;
+    private final int mTrafficStatsRateLimitCacheMaxEntries;
 
     private final Object mOpenSessionCallsLock = new Object();
 
@@ -663,15 +667,18 @@
             mEventLogger = null;
         }
 
-        final long cacheExpiryDurationMs = mDeps.getTrafficStatsRateLimitCacheExpiryDuration();
-        final int cacheMaxEntries = mDeps.getTrafficStatsRateLimitCacheMaxEntries();
-        mSupportTrafficStatsRateLimitCache = mDeps.supportTrafficStatsRateLimitCache(mContext);
+        mAlwaysUseTrafficStatsRateLimitCache =
+                mDeps.alwaysUseTrafficStatsRateLimitCache(mContext);
+        mTrafficStatsRateLimitCacheExpiryDuration =
+                mDeps.getTrafficStatsRateLimitCacheExpiryDuration();
+        mTrafficStatsRateLimitCacheMaxEntries =
+                mDeps.getTrafficStatsRateLimitCacheMaxEntries();
         mTrafficStatsTotalCache = new TrafficStatsRateLimitCache(mClock,
-                cacheExpiryDurationMs, cacheMaxEntries);
+                mTrafficStatsRateLimitCacheExpiryDuration, mTrafficStatsRateLimitCacheMaxEntries);
         mTrafficStatsIfaceCache = new TrafficStatsRateLimitCache(mClock,
-                cacheExpiryDurationMs, cacheMaxEntries);
+                mTrafficStatsRateLimitCacheExpiryDuration, mTrafficStatsRateLimitCacheMaxEntries);
         mTrafficStatsUidCache = new TrafficStatsRateLimitCache(mClock,
-                cacheExpiryDurationMs, cacheMaxEntries);
+                mTrafficStatsRateLimitCacheExpiryDuration, mTrafficStatsRateLimitCacheMaxEntries);
 
         // TODO: Remove bpfNetMaps creation and always start SkDestroyListener
         // Following code is for the experiment to verify the SkDestroyListener refactoring. Based
@@ -920,13 +927,14 @@
         }
 
         /**
-         * Get whether TrafficStats rate-limit cache is supported.
+         * Get whether TrafficStats rate-limit cache is always applied.
          *
          * 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;
+        public boolean alwaysUseTrafficStatsRateLimitCache(@NonNull Context ctx) {
+            return SdkLevel.isAtLeastV() && DeviceConfigUtils.isTetheringFeatureNotChickenedOut(
+                    ctx, TRAFFICSTATS_RATE_LIMIT_CACHE_ENABLED_FLAG);
         }
 
         /**
@@ -954,6 +962,13 @@
         }
 
         /**
+         * Wrapper method for {@link CompatChanges#isChangeEnabled(long, int)}
+         */
+        public boolean isChangeEnabled(final long changeId, final int uid) {
+            return CompatChanges.isChangeEnabled(changeId, uid);
+        }
+
+        /**
          * Retrieves native network total statistics.
          *
          * @return A NetworkStats.Entry containing the native statistics, or
@@ -2084,14 +2099,14 @@
         }
         if (!isEntryValueTypeValid(type)) return UNSUPPORTED;
 
-        if (!mSupportTrafficStatsRateLimitCache) {
-            return getEntryValueForType(mDeps.nativeGetUidStat(uid), type);
+        if (mAlwaysUseTrafficStatsRateLimitCache
+                || mDeps.isChangeEnabled(ENABLE_TRAFFICSTATS_RATE_LIMIT_CACHE, callingUid)) {
+            final NetworkStats.Entry entry = mTrafficStatsUidCache.getOrCompute(IFACE_ALL, uid,
+                    () -> mDeps.nativeGetUidStat(uid));
+            return getEntryValueForType(entry, type);
         }
 
-        final NetworkStats.Entry entry = mTrafficStatsUidCache.getOrCompute(IFACE_ALL, uid,
-                () -> mDeps.nativeGetUidStat(uid));
-
-        return getEntryValueForType(entry, type);
+        return getEntryValueForType(mDeps.nativeGetUidStat(uid), type);
     }
 
     @Nullable
@@ -2113,14 +2128,15 @@
         Objects.requireNonNull(iface);
         if (!isEntryValueTypeValid(type)) return UNSUPPORTED;
 
-        if (!mSupportTrafficStatsRateLimitCache) {
-            return getEntryValueForType(getIfaceStatsInternal(iface), type);
+        if (mAlwaysUseTrafficStatsRateLimitCache
+                || mDeps.isChangeEnabled(
+                        ENABLE_TRAFFICSTATS_RATE_LIMIT_CACHE, Binder.getCallingUid())) {
+            final NetworkStats.Entry entry = mTrafficStatsIfaceCache.getOrCompute(iface, UID_ALL,
+                    () -> getIfaceStatsInternal(iface));
+            return getEntryValueForType(entry, type);
         }
 
-        final NetworkStats.Entry entry = mTrafficStatsIfaceCache.getOrCompute(iface, UID_ALL,
-                () -> getIfaceStatsInternal(iface));
-
-        return getEntryValueForType(entry, type);
+        return getEntryValueForType(getIfaceStatsInternal(iface), type);
     }
 
     private long getEntryValueForType(@Nullable NetworkStats.Entry entry, int type) {
@@ -2166,14 +2182,15 @@
     @Override
     public long getTotalStats(int type) {
         if (!isEntryValueTypeValid(type)) return UNSUPPORTED;
-        if (!mSupportTrafficStatsRateLimitCache) {
-            return getEntryValueForType(getTotalStatsInternal(), type);
+        if (mAlwaysUseTrafficStatsRateLimitCache
+                || mDeps.isChangeEnabled(
+                        ENABLE_TRAFFICSTATS_RATE_LIMIT_CACHE, Binder.getCallingUid())) {
+            final NetworkStats.Entry entry = mTrafficStatsTotalCache.getOrCompute(
+                    IFACE_ALL, UID_ALL, () -> getTotalStatsInternal());
+            return getEntryValueForType(entry, type);
         }
 
-        final NetworkStats.Entry entry = mTrafficStatsTotalCache.getOrCompute(IFACE_ALL, UID_ALL,
-                () -> getTotalStatsInternal());
-
-        return getEntryValueForType(entry, type);
+        return getEntryValueForType(getTotalStatsInternal(), type);
     }
 
     @Override
@@ -2937,13 +2954,12 @@
             } catch (IOException e) {
                 pw.println("(failed to dump FastDataInput counters)");
             }
-            pw.print("trafficstats.cache.supported", mSupportTrafficStatsRateLimitCache);
+            pw.print("trafficstats.cache.alwaysuse", mAlwaysUseTrafficStatsRateLimitCache);
             pw.println();
             pw.print(TRAFFIC_STATS_CACHE_EXPIRY_DURATION_NAME,
-                    mDeps.getTrafficStatsRateLimitCacheExpiryDuration());
+                    mTrafficStatsRateLimitCacheExpiryDuration);
             pw.println();
-            pw.print(TRAFFIC_STATS_CACHE_MAX_ENTRIES_NAME,
-                    mDeps.getTrafficStatsRateLimitCacheMaxEntries());
+            pw.print(TRAFFIC_STATS_CACHE_MAX_ENTRIES_NAME, mTrafficStatsRateLimitCacheMaxEntries);
             pw.println();
 
             pw.decreaseIndent();
diff --git a/service/src/com/android/server/BpfNetMaps.java b/service/src/com/android/server/BpfNetMaps.java
index 1047232..a30735a 100644
--- a/service/src/com/android/server/BpfNetMaps.java
+++ b/service/src/com/android/server/BpfNetMaps.java
@@ -72,6 +72,7 @@
 import com.android.net.module.util.BpfDump;
 import com.android.net.module.util.BpfMap;
 import com.android.net.module.util.IBpfMap;
+import com.android.net.module.util.SingleWriterBpfMap;
 import com.android.net.module.util.Struct;
 import com.android.net.module.util.Struct.S32;
 import com.android.net.module.util.Struct.U32;
@@ -188,7 +189,7 @@
     @RequiresApi(Build.VERSION_CODES.TIRAMISU)
     private static IBpfMap<S32, U32> getConfigurationMap() {
         try {
-            return new BpfMap<>(
+            return new SingleWriterBpfMap<>(
                     CONFIGURATION_MAP_PATH, S32.class, U32.class);
         } catch (ErrnoException e) {
             throw new IllegalStateException("Cannot open netd configuration map", e);
@@ -198,7 +199,7 @@
     @RequiresApi(Build.VERSION_CODES.TIRAMISU)
     private static IBpfMap<S32, UidOwnerValue> getUidOwnerMap() {
         try {
-            return new BpfMap<>(
+            return new SingleWriterBpfMap<>(
                     UID_OWNER_MAP_PATH, S32.class, UidOwnerValue.class);
         } catch (ErrnoException e) {
             throw new IllegalStateException("Cannot open uid owner map", e);
@@ -208,7 +209,7 @@
     @RequiresApi(Build.VERSION_CODES.TIRAMISU)
     private static IBpfMap<S32, U8> getUidPermissionMap() {
         try {
-            return new BpfMap<>(
+            return new SingleWriterBpfMap<>(
                     UID_PERMISSION_MAP_PATH, S32.class, U8.class);
         } catch (ErrnoException e) {
             throw new IllegalStateException("Cannot open uid permission map", e);
@@ -218,6 +219,7 @@
     @RequiresApi(Build.VERSION_CODES.TIRAMISU)
     private static IBpfMap<CookieTagMapKey, CookieTagMapValue> getCookieTagMap() {
         try {
+            // Cannot use SingleWriterBpfMap because it's written by ClatCoordinator as well.
             return new BpfMap<>(COOKIE_TAG_MAP_PATH,
                     CookieTagMapKey.class, CookieTagMapValue.class);
         } catch (ErrnoException e) {
@@ -228,7 +230,7 @@
     @RequiresApi(Build.VERSION_CODES.TIRAMISU)
     private static IBpfMap<S32, U8> getDataSaverEnabledMap() {
         try {
-            return new BpfMap<>(
+            return new SingleWriterBpfMap<>(
                     DATA_SAVER_ENABLED_MAP_PATH, S32.class, U8.class);
         } catch (ErrnoException e) {
             throw new IllegalStateException("Cannot open data saver enabled map", e);
@@ -238,7 +240,7 @@
     @RequiresApi(Build.VERSION_CODES.TIRAMISU)
     private static IBpfMap<IngressDiscardKey, IngressDiscardValue> getIngressDiscardMap() {
         try {
-            return new BpfMap<>(INGRESS_DISCARD_MAP_PATH,
+            return new SingleWriterBpfMap<>(INGRESS_DISCARD_MAP_PATH,
                     IngressDiscardKey.class, IngressDiscardValue.class);
         } catch (ErrnoException e) {
             throw new IllegalStateException("Cannot open ingress discard map", e);
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index ca99935..ca2cb15 100755
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -77,6 +77,7 @@
 import static android.net.NetworkCapabilities.NET_CAPABILITY_FOREGROUND;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED;
@@ -403,6 +404,8 @@
     private static final String NETWORK_ARG = "networks";
     private static final String REQUEST_ARG = "requests";
     private static final String TRAFFICCONTROLLER_ARG = "trafficcontroller";
+    public static final String CLATEGRESS4RAWBPFMAP_ARG = "clatEgress4RawBpfMap";
+    public static final String CLATINGRESS6RAWBPFMAP_ARG = "clatIngress6RawBpfMap";
 
     private static final boolean DBG = true;
     private static final boolean DDBG = Log.isLoggable(TAG, Log.DEBUG);
@@ -487,6 +490,8 @@
     private final boolean mRequestRestrictedWifiEnabled;
     private final boolean mBackgroundFirewallChainEnabled;
 
+    private final boolean mUseDeclaredMethodsForCallbacksEnabled;
+
     /**
      * Uids ConnectivityService tracks blocked status of to send blocked status callbacks.
      * Key is uid based on mAsUid of registered networkRequestInfo
@@ -1848,6 +1853,8 @@
                 && mDeps.isFeatureEnabled(context, REQUEST_RESTRICTED_WIFI);
         mBackgroundFirewallChainEnabled = mDeps.isAtLeastV() && mDeps.isFeatureNotChickenedOut(
                 context, ConnectivityFlags.BACKGROUND_FIREWALL_CHAIN);
+        mUseDeclaredMethodsForCallbacksEnabled = mDeps.isFeatureEnabled(context,
+                ConnectivityFlags.USE_DECLARED_METHODS_FOR_CALLBACKS);
         mCarrierPrivilegeAuthenticator = mDeps.makeCarrierPrivilegeAuthenticator(
                 mContext, mTelephonyManager, mRequestRestrictedWifiEnabled,
                 this::handleUidCarrierPrivilegesLost, mHandler);
@@ -4141,6 +4148,12 @@
             boolean verbose = !CollectionUtils.contains(args, SHORT_ARG);
             dumpTrafficController(pw, fd, verbose);
             return;
+        } else if (CollectionUtils.contains(args, CLATEGRESS4RAWBPFMAP_ARG)) {
+            dumpClatBpfRawMap(pw, true /* isEgress4Map */);
+            return;
+        } else if (CollectionUtils.contains(args, CLATINGRESS6RAWBPFMAP_ARG)) {
+            dumpClatBpfRawMap(pw, false /* isEgress4Map */);
+            return;
         }
 
         pw.println("NetworkProviders for:");
@@ -4400,6 +4413,15 @@
         }
     }
 
+    private void dumpClatBpfRawMap(IndentingPrintWriter pw, boolean isEgress4Map) {
+        for (NetworkAgentInfo nai : networksSortedById()) {
+            if (nai.clatd != null) {
+                nai.clatd.dumpRawBpfMap(pw, isEgress4Map);
+                break;
+            }
+        }
+    }
+
     private void dumpAllRequestInfoLogsToLogcat() {
         try (PrintWriter logPw = new PrintWriter(new Writer() {
             @Override
@@ -4777,7 +4799,15 @@
                 final String logMsg = !TextUtils.isEmpty(redirectUrl)
                         ? " with redirect to " + redirectUrl
                         : "";
-                log(nai.toShortString() + " validation " + (valid ? "passed" : "failed") + logMsg);
+                final String statusMsg;
+                if (valid) {
+                    statusMsg = "passed";
+                } else if (!TextUtils.isEmpty(redirectUrl)) {
+                    statusMsg = "detected a portal";
+                } else {
+                    statusMsg = "failed";
+                }
+                log(nai.toShortString() + " validation " + statusMsg + logMsg);
             }
             if (valid != wasValidated) {
                 final FullScore oldScore = nai.getScore();
@@ -13257,11 +13287,12 @@
         requests.add(createDefaultInternetRequestForTransport(
                 TYPE_NONE, NetworkRequest.Type.TRACK_DEFAULT));
 
-        // request: restricted Satellite internet
+        // request: Satellite internet, satellite network could be restricted or constrained
         final NetworkCapabilities cap = new NetworkCapabilities.Builder()
                 .addCapability(NET_CAPABILITY_INTERNET)
                 .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
                 .removeCapability(NET_CAPABILITY_NOT_RESTRICTED)
+                .removeCapability(NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED)
                 .addTransportType(NetworkCapabilities.TRANSPORT_SATELLITE)
                 .build();
         requests.add(createNetworkRequest(NetworkRequest.Type.REQUEST, cap));
@@ -14020,4 +14051,16 @@
         enforceNetworkStackPermission(mContext);
         return mRoutingCoordinatorService;
     }
+
+    @Override
+    public long getEnabledConnectivityManagerFeatures() {
+        long features = 0;
+        // The bitmask must be built based on final properties initialized in the constructor, to
+        // ensure that it does not change over time and is always consistent between
+        // ConnectivityManager and ConnectivityService.
+        if (mUseDeclaredMethodsForCallbacksEnabled) {
+            features |= ConnectivityManager.FEATURE_USE_DECLARED_METHODS_FOR_CALLBACKS;
+        }
+        return features;
+    }
 }
diff --git a/service/src/com/android/server/TestNetworkService.java b/service/src/com/android/server/TestNetworkService.java
index 843b7b3..4d39d7d 100644
--- a/service/src/com/android/server/TestNetworkService.java
+++ b/service/src/com/android/server/TestNetworkService.java
@@ -267,6 +267,7 @@
         nc.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED);
         nc.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED);
         nc.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN);
+        nc.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED);
         nc.setNetworkSpecifier(new TestNetworkSpecifier(iface));
         nc.setAdministratorUids(administratorUids);
         if (!isMetered) {
diff --git a/service/src/com/android/server/connectivity/ClatCoordinator.java b/service/src/com/android/server/connectivity/ClatCoordinator.java
index 634a8fa..d886182 100644
--- a/service/src/com/android/server/connectivity/ClatCoordinator.java
+++ b/service/src/com/android/server/connectivity/ClatCoordinator.java
@@ -41,6 +41,7 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.IndentingPrintWriter;
+import com.android.net.module.util.BpfDump;
 import com.android.net.module.util.BpfMap;
 import com.android.net.module.util.IBpfMap;
 import com.android.net.module.util.InterfaceParams;
@@ -659,6 +660,8 @@
             final int pid = mDeps.startClatd(tunFd.getFileDescriptor(),
                     readSock6.getFileDescriptor(), writeSock6.getFileDescriptor(), iface, pfx96Str,
                     v4Str, v6Str);
+            // The file descriptors have been duplicated (dup2) to clatd in native_startClatd().
+            // Close these file descriptor stubs in finally block.
 
             // [6] Initialize and store clatd tracker object.
             mClatdTracker = new ClatdTracker(iface, ifIndex, tunIface, tunIfIndex, v4, v6, pfx96,
@@ -677,20 +680,17 @@
                     Log.e(TAG, "untagSocket cookie " + cookie + " failed: " + e2);
                 }
             }
-            try {
-                if (tunFd != null) {
-                    tunFd.close();
-                }
-                if (readSock6 != null) {
-                    readSock6.close();
-                }
-                if (writeSock6 != null) {
-                    writeSock6.close();
-                }
-            } catch (IOException e2) {
-                Log.e(TAG, "Fail to cleanup fd ", e);
-            }
             throw new IOException("Failed to start clat ", e);
+        } finally {
+            if (tunFd != null) {
+                tunFd.close();
+            }
+            if (readSock6 != null) {
+                readSock6.close();
+            }
+            if (writeSock6 != null) {
+                writeSock6.close();
+            }
         }
     }
 
@@ -795,6 +795,29 @@
     }
 
     /**
+     * Dump raw BPF map into base64 encoded strings {@literal "<base64 key>,<base64 value>"}.
+     * Allow to dump only one map in each call. For test only.
+     *
+     * @param pw print writer.
+     * @param isEgress4Map whether to dump the egress4 map (true) or the ingress6 map (false).
+     *
+     * Usage:
+     * $ dumpsys connectivity {clatEgress4RawBpfMap|clatIngress6RawBpfMap}
+     *
+     * Output:
+     * {@literal <base64 encoded key #1>,<base64 encoded value #1>}
+     * {@literal <base64 encoded key #2>,<base64 encoded value #2>}
+     * ..
+     */
+    public void dumpRawMap(@NonNull IndentingPrintWriter pw, boolean isEgress4Map) {
+        if (isEgress4Map) {
+            BpfDump.dumpRawMap(mEgressMap, pw);
+        } else {
+            BpfDump.dumpRawMap(mIngressMap, pw);
+        }
+    }
+
+    /**
      * Dump the coordinator information.
      *
      * @param pw print writer.
diff --git a/service/src/com/android/server/connectivity/ConnectivityFlags.java b/service/src/com/android/server/connectivity/ConnectivityFlags.java
index 7ea7f95..1ee1ed7 100644
--- a/service/src/com/android/server/connectivity/ConnectivityFlags.java
+++ b/service/src/com/android/server/connectivity/ConnectivityFlags.java
@@ -46,6 +46,9 @@
 
     public static final String DELAY_DESTROY_SOCKETS = "delay_destroy_sockets";
 
+    public static final String USE_DECLARED_METHODS_FOR_CALLBACKS =
+            "use_declared_methods_for_callbacks";
+
     private boolean mNoRematchAllRequestsOnRegister;
 
     /**
diff --git a/service/src/com/android/server/connectivity/Nat464Xlat.java b/service/src/com/android/server/connectivity/Nat464Xlat.java
index 489dab5..a979681 100644
--- a/service/src/com/android/server/connectivity/Nat464Xlat.java
+++ b/service/src/com/android/server/connectivity/Nat464Xlat.java
@@ -622,6 +622,18 @@
         }
     }
 
+    /**
+     * Dump the raw BPF maps in 464XLAT
+     *
+     * @param pw print writer.
+     * @param isEgress4Map whether to dump the egress4 map (true) or the ingress6 map (false).
+     */
+    public void dumpRawBpfMap(IndentingPrintWriter pw, boolean isEgress4Map) {
+        if (SdkLevel.isAtLeastT()) {
+            mClatCoordinator.dumpRawMap(pw, isEgress4Map);
+        }
+    }
+
     @Override
     public String toString() {
         return "mBaseIface: " + mBaseIface + ", mIface: " + mIface + ", mState: " + mState;
diff --git a/staticlibs/Android.bp b/staticlibs/Android.bp
index ede6d3f..e2834b0 100644
--- a/staticlibs/Android.bp
+++ b/staticlibs/Android.bp
@@ -135,6 +135,7 @@
         "device/com/android/net/module/util/BpfUtils.java",
         "device/com/android/net/module/util/IBpfMap.java",
         "device/com/android/net/module/util/JniUtil.java",
+        "device/com/android/net/module/util/SingleWriterBpfMap.java",
         "device/com/android/net/module/util/TcUtils.java",
     ],
     sdk_version: "module_current",
diff --git a/staticlibs/device/com/android/net/module/util/BpfDump.java b/staticlibs/device/com/android/net/module/util/BpfDump.java
index 7549e71..4227194 100644
--- a/staticlibs/device/com/android/net/module/util/BpfDump.java
+++ b/staticlibs/device/com/android/net/module/util/BpfDump.java
@@ -81,6 +81,26 @@
     }
 
     /**
+     * Dump the BpfMap entries with base64 format encoding.
+     */
+    public static <K extends Struct, V extends Struct> void dumpRawMap(IBpfMap<K, V> map,
+            PrintWriter pw) {
+        try {
+            if (map == null) {
+                pw.println("BPF map is null");
+                return;
+            }
+            if (map.isEmpty()) {
+                pw.println("No entries");
+                return;
+            }
+            map.forEach((k, v) -> pw.println(toBase64EncodedString(k, v)));
+        } catch (ErrnoException e) {
+            pw.println("Map dump end with error: " + Os.strerror(e.errno));
+        }
+    }
+
+    /**
      * Dump the BpfMap name and entries
      */
     public static <K extends Struct, V extends Struct> void dumpMap(IBpfMap<K, V> map,
diff --git a/staticlibs/device/com/android/net/module/util/SingleWriterBpfMap.java b/staticlibs/device/com/android/net/module/util/SingleWriterBpfMap.java
new file mode 100644
index 0000000..3eb59d8
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/SingleWriterBpfMap.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.net.module.util;
+
+import android.os.Build;
+import android.system.ErrnoException;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+
+import java.util.HashMap;
+import java.util.NoSuchElementException;
+
+/**
+ * Subclass of BpfMap for maps that are only ever written by one userspace writer.
+ *
+ * This class stores all map data in a userspace HashMap in addition to in the BPF map. This makes
+ * reads (but not iterations) much faster because they do not require a system call or converting
+ * the raw map read to the Value struct. See, e.g., b/343166906 .
+ *
+ * Users of this class must ensure that no BPF program ever writes to the map, and that all
+ * userspace writes to the map occur through this object. Other userspace code may still read from
+ * the map; only writes are required to go through this object.
+ *
+ * Reads and writes to this object are thread-safe and internally synchronized. The read and write
+ * methods are synchronized to ensure that current writers always result in a consistent internal
+ * state (without synchronization, two concurrent writes might update the underlying map and the
+ * cache in the opposite order, resulting in the cache being out of sync with the map).
+ *
+ * getNextKey and iteration over the map are not synchronized or cached and always access the
+ * isunderlying map. The values returned by these calls may be temporarily out of sync with the
+ * values read and written through this object.
+ *
+ * TODO: consider caching reads on iterations as well. This is not trivial because the semantics for
+ * iterating BPF maps require passing in the previously-returned key, and Java iterators only
+ * support iterating from the beginning. It could be done by implementing forEach and possibly by
+ * making getFirstKey and getNextKey private (if no callers are using them). Because HashMap is not
+ * thread-safe, implementing forEach would require either making that method synchronized (and
+ * block reads and updates from other threads until iteration is complete) or switching the
+ * internal HashMap to ConcurrentHashMap.
+ *
+ * @param <K> the key of the map.
+ * @param <V> the value of the map.
+ */
+@RequiresApi(Build.VERSION_CODES.S)
+public class SingleWriterBpfMap<K extends Struct, V extends Struct> extends BpfMap<K, V> {
+    // HashMap instead of ArrayMap because it performs better on larger maps, and many maps used in
+    // our code can contain hundreds of items.
+    private final HashMap<K, V> mCache = new HashMap<>();
+
+    protected SingleWriterBpfMap(@NonNull final String path, final int flag, final Class<K> key,
+            final Class<V> value) throws ErrnoException, NullPointerException {
+        super(path, flag, key, value);
+
+        if (flag != BPF_F_RDWR) {
+            throw new IllegalArgumentException(
+                    "Using " + getClass().getName() + " for read-only maps does not make sense");
+        }
+
+        // Populate cache with the current map contents.
+        K currentKey = super.getFirstKey();
+        while (currentKey != null) {
+            mCache.put(currentKey, super.getValue(currentKey));
+            currentKey = super.getNextKey(currentKey);
+        }
+    }
+
+    public SingleWriterBpfMap(@NonNull final String path, final Class<K> key,
+            final Class<V> value) throws ErrnoException, NullPointerException {
+        this(path, BPF_F_RDWR, key, value);
+    }
+
+    @Override
+    public synchronized void updateEntry(K key, V value) throws ErrnoException {
+        super.updateEntry(key, value);
+        mCache.put(key, value);
+    }
+
+    @Override
+    public synchronized void insertEntry(K key, V value)
+            throws ErrnoException, IllegalStateException {
+        super.insertEntry(key, value);
+        mCache.put(key, value);
+    }
+
+    @Override
+    public synchronized void replaceEntry(K key, V value)
+            throws ErrnoException, NoSuchElementException {
+        super.replaceEntry(key, value);
+        mCache.put(key, value);
+    }
+
+    @Override
+    public synchronized boolean insertOrReplaceEntry(K key, V value) throws ErrnoException {
+        final boolean ret = super.insertOrReplaceEntry(key, value);
+        mCache.put(key, value);
+        return ret;
+    }
+
+    @Override
+    public synchronized boolean deleteEntry(K key) throws ErrnoException {
+        final boolean ret = super.deleteEntry(key);
+        mCache.remove(key);
+        return ret;
+    }
+
+    @Override
+    public synchronized boolean containsKey(@NonNull K key) throws ErrnoException {
+        return mCache.containsKey(key);
+    }
+
+    @Override
+    public synchronized V getValue(@NonNull K key) throws ErrnoException {
+        return mCache.get(key);
+    }
+}
diff --git a/staticlibs/framework/com/android/net/module/util/NetworkStackConstants.java b/staticlibs/framework/com/android/net/module/util/NetworkStackConstants.java
index 19d8bbe..319d51a 100644
--- a/staticlibs/framework/com/android/net/module/util/NetworkStackConstants.java
+++ b/staticlibs/framework/com/android/net/module/util/NetworkStackConstants.java
@@ -180,6 +180,7 @@
     public static final int ICMPV6_NS_HEADER_LEN = 24;
     public static final int ICMPV6_NA_HEADER_LEN = 24;
     public static final int ICMPV6_ND_OPTION_TLLA_LEN = 8;
+    public static final int ICMPV6_ND_OPTION_SLLA_LEN = 8;
 
     public static final int NEIGHBOR_ADVERTISEMENT_FLAG_ROUTER    = 1 << 31;
     public static final int NEIGHBOR_ADVERTISEMENT_FLAG_SOLICITED = 1 << 30;
diff --git a/staticlibs/native/bpf_syscall_wrappers/include/BpfSyscallWrappers.h b/staticlibs/native/bpf_syscall_wrappers/include/BpfSyscallWrappers.h
index cb02de8..2a0e8e0 100644
--- a/staticlibs/native/bpf_syscall_wrappers/include/BpfSyscallWrappers.h
+++ b/staticlibs/native/bpf_syscall_wrappers/include/BpfSyscallWrappers.h
@@ -128,20 +128,16 @@
                             });
 }
 
-inline int mapRetrieve(const char* pathname, uint32_t flag) {
-    return bpfFdGet(pathname, flag);
-}
-
 inline int mapRetrieveRW(const char* pathname) {
-    return mapRetrieve(pathname, 0);
+    return bpfFdGet(pathname, 0);
 }
 
 inline int mapRetrieveRO(const char* pathname) {
-    return mapRetrieve(pathname, BPF_F_RDONLY);
+    return bpfFdGet(pathname, BPF_F_RDONLY);
 }
 
 inline int mapRetrieveWO(const char* pathname) {
-    return mapRetrieve(pathname, BPF_F_WRONLY);
+    return bpfFdGet(pathname, BPF_F_WRONLY);
 }
 
 inline int retrieveProgram(const char* pathname) {
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkCallback.kt b/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkCallback.kt
index f76916a..66362d4 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkCallback.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkCallback.kt
@@ -360,17 +360,29 @@
         timeoutMs: Long = defaultTimeoutMs,
         errorMsg: String? = null,
         test: (T) -> Boolean = { true }
-    ) = (poll(timeoutMs) ?: fail("Did not receive ${T::class.simpleName} after ${timeoutMs}ms"))
+    ) = (poll(timeoutMs) ?: failWithErrorReason(errorMsg,
+        "Did not receive ${T::class.simpleName} after ${timeoutMs}ms"))
             .also {
-                if (it !is T) fail("Expected callback ${T::class.simpleName}, got $it")
+                if (it !is T) {
+                    failWithErrorReason(
+                        errorMsg,
+                        "Expected callback ${T::class.simpleName}, got $it"
+                    )
+                }
                 if (ANY_NETWORK !== network && it.network != network) {
-                    fail("Expected network $network for callback : $it")
+                    failWithErrorReason(errorMsg, "Expected network $network for callback : $it")
                 }
                 if (!test(it)) {
-                    fail("${errorMsg ?: "Callback doesn't match predicate"} : $it")
+                    failWithErrorReason(errorMsg, "Callback doesn't match predicate : $it")
                 }
             } as T
 
+    // "Nothing" is the return type to declare a function never returns a value.
+    fun failWithErrorReason(errorMsg: String?, errorReason: String): Nothing {
+        val message = if (errorMsg != null) "$errorMsg : $errorReason" else errorReason
+        fail(message)
+    }
+
     inline fun <reified T : CallbackEntry> expect(
         network: HasNetwork,
         timeoutMs: Long = defaultTimeoutMs,
diff --git a/staticlibs/testutils/host/com/android/testutils/ConnectivityTestTargetPreparer.kt b/staticlibs/testutils/host/com/android/testutils/ConnectivityTestTargetPreparer.kt
index 600a623..6d03042 100644
--- a/staticlibs/testutils/host/com/android/testutils/ConnectivityTestTargetPreparer.kt
+++ b/staticlibs/testutils/host/com/android/testutils/ConnectivityTestTargetPreparer.kt
@@ -61,6 +61,7 @@
         setUpdaterNetworkingEnabled(testInfo, enableChain = true,
                 enablePkgs = UPDATER_PKGS.associateWith { false })
         runPreparerApk(testInfo)
+        refreshTime(testInfo)
     }
 
     private fun runPreparerApk(testInfo: TestInformation) {
@@ -141,6 +142,14 @@
                         .contains(":deny")
             }
 
+    private fun refreshTime(testInfo: TestInformation,) {
+        // Forces a synchronous time refresh using the network. Time is fetched synchronously but
+        // this does not guarantee that system time is updated when it returns.
+        // This avoids flakes where the system clock rolls back, for example when using test
+        // settings like test_url_expiration_time in NetworkMonitor.
+        testInfo.exec("cmd network_time_update_service force_refresh")
+    }
+
     override fun tearDown(testInfo: TestInformation, e: Throwable?) {
         if (isTearDownDisabled) return
         installer.tearDown(testInfo, e)
diff --git a/tests/common/java/android/net/NetworkCapabilitiesTest.java b/tests/common/java/android/net/NetworkCapabilitiesTest.java
index 6eb56c7b..0f0e2f1 100644
--- a/tests/common/java/android/net/NetworkCapabilitiesTest.java
+++ b/tests/common/java/android/net/NetworkCapabilitiesTest.java
@@ -61,6 +61,7 @@
 import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
 import static android.net.NetworkCapabilities.TRANSPORT_WIFI_AWARE;
 import static android.os.Process.INVALID_UID;
+
 import static com.android.modules.utils.build.SdkLevel.isAtLeastS;
 import static com.android.modules.utils.build.SdkLevel.isAtLeastT;
 import static com.android.modules.utils.build.SdkLevel.isAtLeastV;
@@ -68,6 +69,7 @@
 import static com.android.testutils.MiscAsserts.assertEmpty;
 import static com.android.testutils.MiscAsserts.assertThrows;
 import static com.android.testutils.ParcelUtils.assertParcelingIsLossless;
+
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -83,21 +85,25 @@
 import android.os.Build;
 import android.util.ArraySet;
 import android.util.Range;
+
 import androidx.test.filters.SmallTest;
+
 import com.android.testutils.CompatUtil;
 import com.android.testutils.ConnectivityModuleTest;
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
 import com.android.testutils.DevSdkIgnoreRunner;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-import java.util.Set;
+
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mockito;
 
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Set;
+
 @SmallTest
 @RunWith(DevSdkIgnoreRunner.class)
 // NetworkCapabilities is only updatable on S+, and this test covers behavior which implementation
@@ -1465,4 +1471,26 @@
         assertEquals("-SUPL-VALIDATED-CAPTIVE_PORTAL+MMS+OEM_PAID",
                 nc1.describeCapsDifferencesFrom(nc2));
     }
+
+    @Test
+    public void testInvalidCapability() {
+        final int invalidCapability = Integer.MAX_VALUE;
+        // Passing invalid capability does not throw
+        final NetworkCapabilities nc1 = new NetworkCapabilities.Builder()
+                .addCapability(NET_CAPABILITY_INTERNET)
+                .addForbiddenCapability(NET_CAPABILITY_NOT_ROAMING)
+                .removeCapability(invalidCapability)
+                .removeForbiddenCapability(invalidCapability)
+                .addCapability(invalidCapability)
+                .addForbiddenCapability(invalidCapability)
+                .build();
+
+        final NetworkCapabilities nc2 = new NetworkCapabilities.Builder()
+                .addCapability(NET_CAPABILITY_INTERNET)
+                .addForbiddenCapability(NET_CAPABILITY_NOT_ROAMING)
+                .build();
+
+        // nc1 and nc2 are the same since invalid capability is ignored
+        assertEquals(nc1, nc2);
+    }
 }
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java
index 50d6e76..e186c6b 100755
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java
@@ -22,6 +22,7 @@
 import static android.Manifest.permission.WRITE_DEVICE_CONFIG;
 import static android.content.pm.PackageManager.FEATURE_TELEPHONY;
 import static android.content.pm.PackageManager.FEATURE_WIFI;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_BACKGROUND;
 import static android.net.ConnectivityManager.TYPE_VPN;
 import static android.net.NetworkCapabilities.TRANSPORT_TEST;
 import static android.net.NetworkCapabilities.TRANSPORT_VPN;
@@ -51,6 +52,7 @@
 import static com.android.testutils.Cleanup.testAndCleanup;
 import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
 import static com.android.testutils.RecorderCallback.CallbackEntry.BLOCKED_STATUS_INT;
+import static com.android.testutils.TestPermissionUtil.runAsShell;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -969,19 +971,30 @@
         final TestableNetworkCallback otherUidCallback = new TestableNetworkCallback();
         final TestableNetworkCallback myUidCallback = new TestableNetworkCallback();
         if (SdkLevel.isAtLeastS()) {
-            final int otherUid =
-                    UserHandle.of(5 /* userId */).getUid(Process.FIRST_APPLICATION_UID);
+            // Using the same appId with the test to make sure otherUid has the internet permission.
+            // This works because the UID permission map only stores the app ID and not the whole
+            // UID. If the otherUid does not have the internet permission, network access from
+            // otherUid could be considered blocked on V+.
+            final int appId = UserHandle.getAppId(Process.myUid());
+            final int otherUid = UserHandle.of(5 /* userId */).getUid(appId);
             final Handler h = new Handler(Looper.getMainLooper());
             runWithShellPermissionIdentity(() -> {
                 registerSystemDefaultNetworkCallback(systemDefaultCallback, h);
                 registerDefaultNetworkCallbackForUid(otherUid, otherUidCallback, h);
                 registerDefaultNetworkCallbackForUid(Process.myUid(), myUidCallback, h);
             }, NETWORK_SETTINGS);
-            for (TestableNetworkCallback callback :
-                    List.of(systemDefaultCallback, otherUidCallback, myUidCallback)) {
+            for (TestableNetworkCallback callback : List.of(systemDefaultCallback, myUidCallback)) {
                 callback.expectAvailableCallbacks(defaultNetwork, false /* suspended */,
                         true /* validated */, false /* blocked */, TIMEOUT_MS);
             }
+            // On V+, ConnectivityService generates blockedReasons based on bpf map contents even if
+            // the otherUid does not exist on device. So if the background chain is enabled,
+            // otherUid is blocked.
+            final boolean isOtherUidBlocked = SdkLevel.isAtLeastV()
+                    && runAsShell(NETWORK_SETTINGS, () -> mCM.getFirewallChainEnabled(
+                            FIREWALL_CHAIN_BACKGROUND));
+            otherUidCallback.expectAvailableCallbacks(defaultNetwork, false /* suspended */,
+                    true /* validated */, isOtherUidBlocked, TIMEOUT_MS);
         }
 
         FileDescriptor fd = openSocketFdInOtherApp(TEST_HOST, 80, TIMEOUT_MS);
diff --git a/tests/cts/hostside/app2/Android.bp b/tests/cts/hostside/app2/Android.bp
index ad25562..12ea23b 100644
--- a/tests/cts/hostside/app2/Android.bp
+++ b/tests/cts/hostside/app2/Android.bp
@@ -35,4 +35,5 @@
         "general-tests",
         "sts",
     ],
+    sdk_version: "test_current",
 }
diff --git a/tests/cts/hostside/networkslicingtestapp/Android.bp b/tests/cts/hostside/networkslicingtestapp/Android.bp
index 100b6e4..c220000 100644
--- a/tests/cts/hostside/networkslicingtestapp/Android.bp
+++ b/tests/cts/hostside/networkslicingtestapp/Android.bp
@@ -44,6 +44,7 @@
         "CtsHostsideNetworkCapTestsAppDefaults",
     ],
     manifest: "AndroidManifestWithoutProperty.xml",
+    sdk_version: "test_current",
 }
 
 android_test_helper_app {
@@ -53,6 +54,7 @@
         "CtsHostsideNetworkCapTestsAppDefaults",
     ],
     manifest: "AndroidManifestWithProperty.xml",
+    sdk_version: "test_current",
 }
 
 android_test_helper_app {
@@ -63,4 +65,5 @@
     ],
     target_sdk_version: "33",
     manifest: "AndroidManifestWithoutProperty.xml",
+    sdk_version: "test_current",
 }
diff --git a/tests/cts/hostside/src/com/android/cts/net/HostsideVpnTests.java b/tests/cts/hostside/src/com/android/cts/net/HostsideVpnTests.java
index f0a87af..cea60f9 100644
--- a/tests/cts/hostside/src/com/android/cts/net/HostsideVpnTests.java
+++ b/tests/cts/hostside/src/com/android/cts/net/HostsideVpnTests.java
@@ -18,8 +18,6 @@
 
 import android.platform.test.annotations.RequiresDevice;
 
-import com.android.testutils.SkipPresubmit;
-
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
@@ -37,13 +35,11 @@
         uninstallPackage(TEST_APP2_PKG, true);
     }
 
-    @SkipPresubmit(reason = "Out of SLO flakiness")
     @Test
     public void testChangeUnderlyingNetworks() throws Exception {
         runDeviceTests(TEST_PKG, TEST_PKG + ".VpnTest", "testChangeUnderlyingNetworks");
     }
 
-    @SkipPresubmit(reason = "Out of SLO flakiness")
     @Test
     public void testDefault() throws Exception {
         runDeviceTests(TEST_PKG, TEST_PKG + ".VpnTest", "testDefault");
@@ -166,7 +162,6 @@
         runDeviceTests(TEST_PKG, TEST_PKG + ".VpnTest", "testBlockIncomingPackets");
     }
 
-    @SkipPresubmit(reason = "Out of SLO flakiness")
     @Test
     public void testSetVpnDefaultForUids() throws Exception {
         runDeviceTests(TEST_PKG, TEST_PKG + ".VpnTest", "testSetVpnDefaultForUids");
diff --git a/tests/cts/multidevices/Android.bp b/tests/cts/multidevices/Android.bp
index 5ac4229..5f9b3ef 100644
--- a/tests/cts/multidevices/Android.bp
+++ b/tests/cts/multidevices/Android.bp
@@ -14,12 +14,16 @@
 
 package {
     default_applicable_licenses: ["Android-Apache-2.0"],
+    default_team: "trendy_team_fwk_core_networking",
 }
 
 python_test_host {
     name: "CtsConnectivityMultiDevicesTestCases",
     main: "connectivity_multi_devices_test.py",
-    srcs: ["connectivity_multi_devices_test.py"],
+    srcs: [
+        "connectivity_multi_devices_test.py",
+        "tether_utils.py",
+    ],
     libs: [
         "mobly",
     ],
diff --git a/tests/cts/multidevices/connectivity_multi_devices_test.py b/tests/cts/multidevices/connectivity_multi_devices_test.py
index ab88504..417db99 100644
--- a/tests/cts/multidevices/connectivity_multi_devices_test.py
+++ b/tests/cts/multidevices/connectivity_multi_devices_test.py
@@ -1,23 +1,16 @@
 # Lint as: python3
 """Connectivity multi devices tests."""
-import base64
 import sys
-import uuid
-
-from mobly import asserts
 from mobly import base_test
 from mobly import test_runner
 from mobly import utils
 from mobly.controllers import android_device
+import tether_utils
+from tether_utils import UpstreamType
 
 CONNECTIVITY_MULTI_DEVICES_SNIPPET_PACKAGE = "com.google.snippet.connectivity"
 
 
-class UpstreamType:
-  CELLULAR = 1
-  WIFI = 2
-
-
 class ConnectivityMultiDevicesTest(base_test.BaseTestClass):
 
   def setup_class(self):
@@ -40,66 +33,27 @@
         raise_on_exception=True,
     )
 
-  @staticmethod
-  def generate_uuid32_base64():
-    """Generates a UUID32 and encodes it in Base64.
-
-    Returns:
-        str: The Base64-encoded UUID32 string. Which is 22 characters.
-    """
-    return base64.b64encode(uuid.uuid1().bytes).decode("utf-8").strip("=")
-
-  def _do_test_hotspot_for_upstream_type(self, upstream_type):
-    """Test hotspot with the specified upstream type.
-
-    This test create a hotspot, make the client connect
-    to it, and verify the packet is forwarded by the hotspot.
-    """
-    server = self.serverDevice.connectivity_multi_devices_snippet
-    client = self.clientDevice.connectivity_multi_devices_snippet
-
-    # Assert pre-conditions specific to each upstream type.
-    asserts.skip_if(not client.hasWifiFeature(), "Client requires Wifi feature")
-    asserts.skip_if(
-      not server.hasHotspotFeature(), "Server requires hotspot feature"
-    )
-    if upstream_type == UpstreamType.CELLULAR:
-      asserts.skip_if(
-          not server.hasTelephonyFeature(), "Server requires Telephony feature"
-      )
-      server.requestCellularAndEnsureDefault()
-    elif upstream_type == UpstreamType.WIFI:
-      asserts.skip_if(
-          not server.isStaApConcurrencySupported(),
-          "Server requires Wifi AP + STA concurrency",
-      )
-      server.ensureWifiIsDefault()
-    else:
-      raise ValueError(f"Invalid upstream type: {upstream_type}")
-
-    # Generate ssid/passphrase with random characters to make sure nearby devices won't
-    # connect unexpectedly. Note that total length of ssid cannot go over 32.
-    testSsid = "HOTSPOT-" + self.generate_uuid32_base64()
-    testPassphrase = self.generate_uuid32_base64()
-
-    try:
-      # Create a hotspot with fixed SSID and password.
-      server.startHotspot(testSsid, testPassphrase)
-
-      # Make the client connects to the hotspot.
-      client.connectToWifi(testSsid, testPassphrase, True)
-
-    finally:
-      if upstream_type == UpstreamType.CELLULAR:
-        server.unrequestCellular()
-      # Teardown the hotspot.
-      server.stopAllTethering()
-
   def test_hotspot_upstream_wifi(self):
-    self._do_test_hotspot_for_upstream_type(UpstreamType.WIFI)
+    try:
+      # Connectivity of the client verified by asserting the validated capability.
+      tether_utils.setup_hotspot_and_client_for_upstream_type(
+          self.serverDevice, self.clientDevice, UpstreamType.WIFI
+      )
+    finally:
+      tether_utils.cleanup_tethering_for_upstream_type(
+          self.serverDevice, UpstreamType.WIFI
+      )
 
   def test_hotspot_upstream_cellular(self):
-    self._do_test_hotspot_for_upstream_type(UpstreamType.CELLULAR)
+    try:
+      # Connectivity of the client verified by asserting the validated capability.
+      tether_utils.setup_hotspot_and_client_for_upstream_type(
+          self.serverDevice, self.clientDevice, UpstreamType.CELLULAR
+      )
+    finally:
+      tether_utils.cleanup_tethering_for_upstream_type(
+          self.serverDevice, UpstreamType.CELLULAR
+      )
 
 
 if __name__ == "__main__":
diff --git a/tests/cts/multidevices/snippet/ConnectivityMultiDevicesSnippet.kt b/tests/cts/multidevices/snippet/ConnectivityMultiDevicesSnippet.kt
index 258648f..8805edd 100644
--- a/tests/cts/multidevices/snippet/ConnectivityMultiDevicesSnippet.kt
+++ b/tests/cts/multidevices/snippet/ConnectivityMultiDevicesSnippet.kt
@@ -16,11 +16,11 @@
 
 package com.google.snippet.connectivity
 
+import android.Manifest.permission.NETWORK_SETTINGS
 import android.Manifest.permission.OVERRIDE_WIFI_CONFIG
 import android.content.pm.PackageManager.FEATURE_TELEPHONY
 import android.content.pm.PackageManager.FEATURE_WIFI
 import android.net.ConnectivityManager
-import android.net.Network
 import android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED
 import android.net.NetworkCapabilities.TRANSPORT_WIFI
 import android.net.NetworkRequest
@@ -30,20 +30,24 @@
 import android.net.wifi.SoftApConfiguration
 import android.net.wifi.SoftApConfiguration.SECURITY_TYPE_WPA2_PSK
 import android.net.wifi.WifiConfiguration
+import android.net.wifi.WifiInfo
 import android.net.wifi.WifiManager
 import android.net.wifi.WifiNetworkSpecifier
 import android.net.wifi.WifiSsid
 import androidx.test.platform.app.InstrumentationRegistry
+import com.android.testutils.AutoReleaseNetworkCallbackRule
 import com.android.testutils.ConnectUtil
 import com.android.testutils.NetworkCallbackHelper
-import com.android.testutils.RecorderCallback.CallbackEntry.Available
 import com.android.testutils.RecorderCallback.CallbackEntry.CapabilitiesChanged
 import com.android.testutils.TestableNetworkCallback
 import com.android.testutils.runAsShell
 import com.google.android.mobly.snippet.Snippet
 import com.google.android.mobly.snippet.rpc.Rpc
+import org.junit.Rule
 
 class ConnectivityMultiDevicesSnippet : Snippet {
+    @get:Rule
+    val networkCallbackRule = AutoReleaseNetworkCallbackRule()
     private val context = InstrumentationRegistry.getInstrumentation().getTargetContext()
     private val wifiManager = context.getSystemService(WifiManager::class.java)!!
     private val cm = context.getSystemService(ConnectivityManager::class.java)!!
@@ -88,10 +92,8 @@
     // Suppress warning because WifiManager methods to connect to a config are
     // documented not to be deprecated for privileged users.
     @Suppress("DEPRECATION")
-    fun connectToWifi(ssid: String, passphrase: String, requireValidation: Boolean): Network {
+    fun connectToWifi(ssid: String, passphrase: String): Long {
         val specifier = WifiNetworkSpecifier.Builder()
-            .setSsid(ssid)
-            .setWpa2Passphrase(passphrase)
             .setBand(ScanResult.WIFI_BAND_24_GHZ)
             .build()
         val wifiConfig = WifiConfiguration()
@@ -102,27 +104,28 @@
         wifiConfig.allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.TKIP)
         wifiConfig.allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.CCMP)
 
-        // Register network callback for the specific wifi.
+        // Add the test configuration and connect to it.
+        val connectUtil = ConnectUtil(context)
+        connectUtil.connectToWifiConfig(wifiConfig)
+
+        // Implement manual SSID matching. Specifying the SSID in
+        // NetworkSpecifier is ineffective
+        // (see WifiNetworkAgentSpecifier#canBeSatisfiedBy for details).
+        // Note that holding permission is necessary when waiting for
+        // the callbacks. The handler thread checks permission; if
+        // it's not present, the SSID will be redacted.
         val networkCallback = TestableNetworkCallback()
-        val wifiRequest = NetworkRequest.Builder().addTransportType(TRANSPORT_WIFI)
-            .setNetworkSpecifier(specifier)
-            .build()
-        cm.registerNetworkCallback(wifiRequest, networkCallback)
-
-        try {
-            // Add the test configuration and connect to it.
-            val connectUtil = ConnectUtil(context)
-            connectUtil.connectToWifiConfig(wifiConfig)
-
-            val event = networkCallback.expect<Available>()
-            if (requireValidation) {
-                networkCallback.eventuallyExpect<CapabilitiesChanged> {
-                    it.caps.hasCapability(NET_CAPABILITY_VALIDATED)
-                }
-            }
-            return event.network
-        } finally {
-            cm.unregisterNetworkCallback(networkCallback)
+        val wifiRequest = NetworkRequest.Builder().addTransportType(TRANSPORT_WIFI).build()
+        return runAsShell(NETWORK_SETTINGS) {
+            // Register the network callback is needed here.
+            // This is to avoid the race condition where callback is fired before
+            // acquiring permission.
+            networkCallbackRule.registerNetworkCallback(wifiRequest, networkCallback)
+            return@runAsShell networkCallback.eventuallyExpect<CapabilitiesChanged> {
+                // Remove double quotes.
+                val ssidFromCaps = (WifiInfo::sanitizeSsid)(it.caps.ssid)
+                ssidFromCaps == ssid && it.caps.hasCapability(NET_CAPABILITY_VALIDATED)
+            }.network.networkHandle
         }
     }
 
diff --git a/tests/cts/multidevices/tether_utils.py b/tests/cts/multidevices/tether_utils.py
new file mode 100644
index 0000000..a2d703c
--- /dev/null
+++ b/tests/cts/multidevices/tether_utils.py
@@ -0,0 +1,103 @@
+#  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.
+#
+#  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.
+
+import base64
+import uuid
+
+from mobly import asserts
+from mobly.controllers import android_device
+
+
+class UpstreamType:
+  CELLULAR = 1
+  WIFI = 2
+
+
+def generate_uuid32_base64() -> str:
+  """Generates a UUID32 and encodes it in Base64.
+
+  Returns:
+      str: The Base64-encoded UUID32 string. Which is 22 characters.
+  """
+  # Strip padding characters to make it safer for hotspot name length limit.
+  return base64.b64encode(uuid.uuid1().bytes).decode("utf-8").strip("=")
+
+
+def setup_hotspot_and_client_for_upstream_type(
+    server_device: android_device,
+    client_device: android_device,
+    upstream_type: UpstreamType,
+) -> (str, int):
+  """Setup the hotspot with a connected client with the specified upstream type.
+
+  This creates a hotspot, make the client connect
+  to it, and verify the packet is forwarded by the hotspot.
+  And returns interface name of both if successful.
+  """
+  server = server_device.connectivity_multi_devices_snippet
+  client = client_device.connectivity_multi_devices_snippet
+
+  # Assert pre-conditions specific to each upstream type.
+  asserts.skip_if(not client.hasWifiFeature(), "Client requires Wifi feature")
+  asserts.skip_if(
+      not server.hasHotspotFeature(), "Server requires hotspot feature"
+  )
+  if upstream_type == UpstreamType.CELLULAR:
+    asserts.skip_if(
+        not server.hasTelephonyFeature(), "Server requires Telephony feature"
+    )
+    server.requestCellularAndEnsureDefault()
+  elif upstream_type == UpstreamType.WIFI:
+    asserts.skip_if(
+        not server.isStaApConcurrencySupported(),
+        "Server requires Wifi AP + STA concurrency",
+    )
+    server.ensureWifiIsDefault()
+  else:
+    raise ValueError(f"Invalid upstream type: {upstream_type}")
+
+  # Generate ssid/passphrase with random characters to make sure nearby devices won't
+  # connect unexpectedly. Note that total length of ssid cannot go over 32.
+  test_ssid = "HOTSPOT-" + generate_uuid32_base64()
+  test_passphrase = generate_uuid32_base64()
+
+  # Create a hotspot with fixed SSID and password.
+  hotspot_interface = server.startHotspot(test_ssid, test_passphrase)
+
+  # Make the client connects to the hotspot.
+  client_network = client.connectToWifi(test_ssid, test_passphrase)
+
+  return hotspot_interface, client_network
+
+
+def cleanup_tethering_for_upstream_type(
+    server_device: android_device, upstream_type: UpstreamType
+) -> None:
+  server = server_device.connectivity_multi_devices_snippet
+  if upstream_type == UpstreamType.CELLULAR:
+    server.unrequestCellular()
+  # Teardown the hotspot.
+  server.stopAllTethering()
diff --git a/tests/cts/net/Android.bp b/tests/cts/net/Android.bp
index 768ba12..ae85701 100644
--- a/tests/cts/net/Android.bp
+++ b/tests/cts/net/Android.bp
@@ -180,4 +180,5 @@
         "cts",
         "general-tests",
     ],
+    sdk_version: "test_current",
 }
diff --git a/tests/cts/net/api23Test/Android.bp b/tests/cts/net/api23Test/Android.bp
index 2ec3a70..587d5a5 100644
--- a/tests/cts/net/api23Test/Android.bp
+++ b/tests/cts/net/api23Test/Android.bp
@@ -56,4 +56,5 @@
         ":CtsNetTestAppForApi23",
     ],
     per_testcase_directory: true,
+    sdk_version: "test_current",
 }
diff --git a/tests/cts/net/native/dns/Android.bp b/tests/cts/net/native/dns/Android.bp
index 8e24fba..de4a3bf 100644
--- a/tests/cts/net/native/dns/Android.bp
+++ b/tests/cts/net/native/dns/Android.bp
@@ -3,6 +3,11 @@
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
+filegroup {
+    name: "dns_async_test_default_map",
+    srcs: ["dns_async_test_default.map"],
+}
+
 cc_defaults {
     name: "dns_async_defaults",
 
@@ -20,6 +25,14 @@
     srcs: [
         "NativeDnsAsyncTest.cpp",
     ],
+    // This test runs on older platform versions, so many libraries (such as libbase and libc++)
+    // need to be linked statically. The test also needs to be linked with a version script to
+    // ensure that the statically-linked library isn't exported from the executable, where it
+    // would override the shared libraries that the platform itself uses.
+    // See http://b/333438055 for an example of what goes wrong when libc++ is partially exported
+    // from an executable.
+    version_script: ":dns_async_test_default_map",
+    stl: "libc++_static",
     shared_libs: [
         "libandroid",
         "liblog",
diff --git a/tests/cts/net/native/dns/dns_async_test_default.map b/tests/cts/net/native/dns/dns_async_test_default.map
new file mode 100644
index 0000000..e342e43
--- /dev/null
+++ b/tests/cts/net/native/dns/dns_async_test_default.map
@@ -0,0 +1,20 @@
+#
+# 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.
+#
+
+{
+  local:
+    *;
+};
diff --git a/tests/cts/net/src/android/net/cts/ApfIntegrationTest.kt b/tests/cts/net/src/android/net/cts/ApfIntegrationTest.kt
index a679498..0b6637d 100644
--- a/tests/cts/net/src/android/net/cts/ApfIntegrationTest.kt
+++ b/tests/cts/net/src/android/net/cts/ApfIntegrationTest.kt
@@ -51,9 +51,11 @@
 import android.util.Log
 import androidx.test.filters.RequiresDevice
 import androidx.test.platform.app.InstrumentationRegistry
+import com.android.compatibility.common.util.PropertyUtil.getFirstApiLevel
 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.compatibility.common.util.VsrTest
 import com.android.internal.util.HexDump
 import com.android.net.module.util.PacketReader
 import com.android.testutils.DevSdkIgnoreRule
@@ -191,8 +193,8 @@
             futureReply!!.complete(recvbuf.sliceArray(8..<length))
         }
 
-        fun sendPing(data: ByteArray) {
-            require(data.size == 56)
+        fun sendPing(data: ByteArray, payloadSize: Int) {
+            require(data.size == payloadSize)
 
             // rfc4443#section-4.1: Echo Request Message
             //   0                   1                   2                   3
@@ -299,6 +301,10 @@
         }
     }
 
+    @VsrTest(
+        requirements = ["VSR-5.3.12-001", "VSR-5.3.12-003", "VSR-5.3.12-004", "VSR-5.3.12-009",
+            "VSR-5.3.12-012"]
+    )
     @Test
     fun testApfCapabilities() {
         // APF became mandatory in Android 14 VSR.
@@ -318,7 +324,7 @@
 
         if (caps.apfVersionSupported > 4) {
             assertThat(caps.maximumApfProgramSize).isAtLeast(2048)
-            assertThat(caps.apfVersionSupported).isEqualTo(6000)  // v6.0000
+            assertThat(caps.apfVersionSupported).isEqualTo(6000) // v6.0000
         }
 
         // DEVICEs launching with Android 15 (AOSP experimental) or higher with CHIPSETs that set
@@ -349,6 +355,9 @@
         return HexDump.hexStringToByteArray(progHexString)
     }
 
+    @VsrTest(
+            requirements = ["VSR-5.3.12-007", "VSR-5.3.12-008", "VSR-5.3.12-010", "VSR-5.3.12-011"]
+    )
     @SkipPresubmit(reason = "This test takes longer than 1 minute, do not run it on presubmit.")
     // APF integration is mostly broken before V, only run the full read / write test on V+.
     @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
@@ -356,9 +365,15 @@
     fun testReadWriteProgram() {
         assumeApfVersionSupportAtLeast(4)
 
-        // Only test down to 2 bytes. The first byte always stays PASS.
+        val minReadWriteSize = if (getFirstApiLevel() >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
+            2
+        } else {
+            8
+        }
+
+        // The minReadWriteSize is 2 bytes. The first byte always stays PASS.
         val program = ByteArray(caps.maximumApfProgramSize)
-        for (i in caps.maximumApfProgramSize downTo 2) {
+        for (i in caps.maximumApfProgramSize downTo minReadWriteSize) {
             // Randomize bytes in range [1, i). And install first [0, i) bytes of program.
             // Note that only the very first instruction (PASS) is valid APF bytecode.
             Random.nextBytes(program, 1 /* fromIndex */, i /* toIndex */)
@@ -366,7 +381,15 @@
 
             // Compare entire memory region.
             val readResult = readProgram()
-            assertWithMessage("read/write $i byte prog failed").that(readResult).isEqualTo(program)
+            val errMsg = """
+                read/write $i byte prog failed.
+                In APFv4, the APF memory region MUST NOT be modified or cleared except by APF
+                instructions executed by the interpreter or by Android OS calls to the HAL. If this
+                requirement cannot be met, the firmware cannot declare that it supports APFv4 and
+                it should declare that it only supports APFv3(if counter is partially supported) or
+                APFv2.
+            """.trimIndent()
+            assertWithMessage(errMsg).that(readResult).isEqualTo(program)
         }
     }
 
@@ -385,9 +408,14 @@
     }
 
     // APF integration is mostly broken before V
+    @VsrTest(requirements = ["VSR-5.3.12-002", "VSR-5.3.12-005"])
     @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
     @Test
     fun testDropPingReply() {
+        // VSR-14 mandates APF to be turned on when the screen is off and the Wi-Fi link
+        // is idle or traffic is less than 10 Mbps. Before that, we don't mandate when the APF
+        // should be turned on.
+        assume().that(getVsrApiLevel()).isAtLeast(34)
         assumeApfVersionSupportAtLeast(4)
 
         // clear any active APF filter
@@ -396,8 +424,13 @@
         readProgram() // wait for install completion
 
         // Assert that initial ping does not get filtered.
-        val data = ByteArray(56).also { Random.nextBytes(it) }
-        packetReader.sendPing(data)
+        val payloadSize = if (getFirstApiLevel() >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
+            68
+        } else {
+            4
+        }
+        val data = ByteArray(payloadSize).also { Random.nextBytes(it) }
+        packetReader.sendPing(data, payloadSize)
         assertThat(packetReader.expectPingReply()).isEqualTo(data)
 
         // Generate an APF program that drops the next ping
@@ -417,16 +450,21 @@
         installProgram(program)
         readProgram() // wait for install completion
 
-        packetReader.sendPing(data)
+        packetReader.sendPing(data, payloadSize)
         packetReader.expectPingDropped()
     }
 
     fun clearApfMemory() = installProgram(ByteArray(caps.maximumApfProgramSize))
 
     // APF integration is mostly broken before V
+    @VsrTest(requirements = ["VSR-5.3.12-002", "VSR-5.3.12-005"])
     @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
     @Test
     fun testPrefilledMemorySlotsV4() {
+        // VSR-14 mandates APF to be turned on when the screen is off and the Wi-Fi link
+        // is idle or traffic is less than 10 Mbps. Before that, we don't mandate when the APF
+        // should be turned on.
+        assume().that(getVsrApiLevel()).isAtLeast(34)
         // Test v4 memory slots on both v4 and v6 interpreters.
         assumeApfVersionSupportAtLeast(4)
         clearApfMemory()
@@ -455,8 +493,13 @@
         readProgram() // wait for install completion
 
         // Trigger the program by sending a ping and waiting on the reply.
-        val data = ByteArray(56).also { Random.nextBytes(it) }
-        packetReader.sendPing(data)
+        val payloadSize = if (getFirstApiLevel() >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
+            68
+        } else {
+            4
+        }
+        val data = ByteArray(payloadSize).also { Random.nextBytes(it) }
+        packetReader.sendPing(data, payloadSize)
         packetReader.expectPingReply()
 
         val readResult = readProgram()
@@ -464,8 +507,52 @@
         expect.withMessage("PROGRAM_SIZE").that(buffer.getInt()).isEqualTo(program.size)
         expect.withMessage("RAM_LEN").that(buffer.getInt()).isEqualTo(caps.maximumApfProgramSize)
         expect.withMessage("IPV4_HEADER_SIZE").that(buffer.getInt()).isEqualTo(0)
-        // Ping packet (64) + IPv6 header (40) + ethernet header (14)
-        expect.withMessage("PACKET_SIZE").that(buffer.getInt()).isEqualTo(64 + 40 + 14)
+        // Ping packet payload + ICMPv6 header (8)  + IPv6 header (40) + ethernet header (14)
+        expect.withMessage("PACKET_SIZE").that(buffer.getInt()).isEqualTo(payloadSize + 8 + 40 + 14)
         expect.withMessage("FILTER_AGE_SECONDS").that(buffer.getInt()).isLessThan(5)
     }
+
+    // APF integration is mostly broken before V
+    @VsrTest(requirements = ["VSR-5.3.12-002", "VSR-5.3.12-005"])
+    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @Test
+    fun testFilterAgeIncreasesBetweenPackets() {
+        // VSR-14 mandates APF to be turned on when the screen is off and the Wi-Fi link
+        // is idle or traffic is less than 10 Mbps. Before that, we don't mandate when the APF
+        // should be turned on.
+        assume().that(getVsrApiLevel()).isAtLeast(34)
+        assumeApfVersionSupportAtLeast(4)
+        clearApfMemory()
+        val gen = ApfV4Generator(4)
+
+        // If not ICMPv6 Echo Reply -> PASS
+        gen.addPassIfNotIcmpv6EchoReply()
+
+        // Store all prefilled memory slots in counter region [500, 520)
+        val counterRegion = 500
+        gen.addLoadImmediate(R1, counterRegion)
+        gen.addLoadFromMemory(R0, MemorySlot.FILTER_AGE_SECONDS)
+        gen.addStoreData(R0, 0)
+
+        installProgram(gen.generate())
+        readProgram() // wait for install completion
+
+        val payloadSize = 56
+        val data = ByteArray(payloadSize).also { Random.nextBytes(it) }
+        packetReader.sendPing(data, payloadSize)
+        packetReader.expectPingReply()
+
+        var buffer = ByteBuffer.wrap(readProgram(), counterRegion, 4 /* length */)
+        val filterAgeSecondsOrig = buffer.getInt()
+
+        Thread.sleep(5100)
+
+        packetReader.sendPing(data, payloadSize)
+        packetReader.expectPingReply()
+
+        buffer = ByteBuffer.wrap(readProgram(), counterRegion, 4 /* length */)
+        val filterAgeSeconds = buffer.getInt()
+        // Assert that filter age has increased, but not too much.
+        assertThat(filterAgeSeconds - filterAgeSecondsOrig).isEqualTo(5)
+    }
 }
diff --git a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
index bf40462..633f2b6 100644
--- a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
@@ -309,6 +309,7 @@
 
     // Airplane Mode BroadcastReceiver Timeout
     private static final long AIRPLANE_MODE_CHANGE_TIMEOUT_MS = 10_000L;
+    private static final long CELL_DATA_AVAILABLE_TIMEOUT_MS = 120_000L;
 
     // Timeout for applying uids allowed on restricted networks
     private static final long APPLYING_UIDS_ALLOWED_ON_RESTRICTED_NETWORKS_TIMEOUT_MS = 3_000L;
@@ -2242,7 +2243,10 @@
             // connectToCell only registers a request, it cannot / does not need to be called twice
             mCtsNetUtils.ensureWifiConnected();
             if (verifyWifi) waitForAvailable(wifiCb);
-            if (supportTelephony) waitForAvailable(telephonyCb);
+            if (supportTelephony) {
+                telephonyCb.eventuallyExpect(
+                        CallbackEntry.AVAILABLE, CELL_DATA_AVAILABLE_TIMEOUT_MS);
+            }
         } finally {
             // Restore the previous state of airplane mode and permissions:
             runShellCommand("cmd connectivity airplane-mode "
diff --git a/tests/cts/net/src/android/net/cts/NetworkRequestTest.java b/tests/cts/net/src/android/net/cts/NetworkRequestTest.java
index 5b53839..ff10e1a 100644
--- a/tests/cts/net/src/android/net/cts/NetworkRequestTest.java
+++ b/tests/cts/net/src/android/net/cts/NetworkRequestTest.java
@@ -19,19 +19,19 @@
 import static android.net.NetworkCapabilities.NET_CAPABILITY_DUN;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_FOTA;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
-import static android.net.NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_MMS;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VPN;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_SUPL;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_TEMPORARILY_NOT_METERED;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED;
 import static android.net.NetworkCapabilities.TRANSPORT_BLUETOOTH;
 import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
 import static android.net.NetworkCapabilities.TRANSPORT_VPN;
 import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
 
-import static com.android.testutils.DevSdkIgnoreRuleKt.VANILLA_ICE_CREAM;
-
 import static com.google.common.truth.Truth.assertThat;
 
 import static junit.framework.Assert.fail;
@@ -66,9 +66,9 @@
 import com.android.networkstack.apishim.common.UnsupportedApiLevelException;
 import com.android.testutils.ConnectivityModuleTest;
 import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
 
-import org.junit.Ignore;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -512,30 +512,20 @@
         assertArrayEquals(netCapabilities, nr.getCapabilities());
     }
 
-    @Test @IgnoreUpTo(VANILLA_ICE_CREAM) @Ignore("b/338200742")
+    // Default capabilities and default forbidden capabilities must not be changed on U- because
+    // this could cause the system server crash when there is a module rollback (b/313030307)
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.R) @IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
     public void testDefaultCapabilities() {
         final NetworkRequest defaultNR = new NetworkRequest.Builder().build();
-        assertTrue(defaultNR.hasForbiddenCapability(NET_CAPABILITY_LOCAL_NETWORK));
-        assertFalse(defaultNR.hasCapability(NET_CAPABILITY_LOCAL_NETWORK));
+
+        assertEquals(4, defaultNR.getCapabilities().length);
+        assertTrue(defaultNR.hasCapability(NET_CAPABILITY_NOT_RESTRICTED));
+        assertTrue(defaultNR.hasCapability(NET_CAPABILITY_TRUSTED));
         assertTrue(defaultNR.hasCapability(NET_CAPABILITY_NOT_VPN));
+        assertTrue(defaultNR.hasCapability(NET_CAPABILITY_NOT_VCN_MANAGED));
 
-        final NetworkCapabilities emptyNC =
-                NetworkCapabilities.Builder.withoutDefaultCapabilities().build();
-        assertFalse(defaultNR.canBeSatisfiedBy(emptyNC));
-
-        // defaultNC represent the capabilities of a network agent, so they must not contain
-        // forbidden capabilities by default.
-        final NetworkCapabilities defaultNC = new NetworkCapabilities.Builder().build();
-        assertArrayEquals(new int[0], defaultNC.getForbiddenCapabilities());
-        // A default NR can be satisfied by default NC.
-        assertTrue(defaultNR.canBeSatisfiedBy(defaultNC));
-
-        // Conversely, network requests have forbidden capabilities by default to manage
-        // backward compatibility, so test that these forbidden capabilities are in place.
-        // Starting in V, NET_CAPABILITY_LOCAL_NETWORK is introduced but is not seen by
-        // default, thanks to a default forbidden capability in NetworkRequest.
-        defaultNC.addCapability(NET_CAPABILITY_LOCAL_NETWORK);
-        assertFalse(defaultNR.canBeSatisfiedBy(defaultNC));
+        assertEquals(0, defaultNR.getForbiddenCapabilities().length);
     }
 
     @Test
diff --git a/tests/cts/netpermission/internetpermission/Android.bp b/tests/cts/netpermission/internetpermission/Android.bp
index 7d5ca2f..e0424ac 100644
--- a/tests/cts/netpermission/internetpermission/Android.bp
+++ b/tests/cts/netpermission/internetpermission/Android.bp
@@ -31,4 +31,5 @@
         "general-tests",
     ],
     host_required: ["net-tests-utils-host-common"],
+    sdk_version: "test_current",
 }
diff --git a/tests/cts/netpermission/updatestatspermission/Android.bp b/tests/cts/netpermission/updatestatspermission/Android.bp
index 2fde1ce..689ce74 100644
--- a/tests/cts/netpermission/updatestatspermission/Android.bp
+++ b/tests/cts/netpermission/updatestatspermission/Android.bp
@@ -19,11 +19,17 @@
 
 android_test {
     name: "CtsNetTestCasesUpdateStatsPermission",
-    defaults: ["cts_defaults"],
+    defaults: [
+        "cts_defaults",
+        "framework-connectivity-test-defaults",
+    ],
 
     srcs: ["src/**/*.java"],
-
-    static_libs: ["ctstestrunner-axt"],
+    platform_apis: true,
+    static_libs: [
+        "ctstestrunner-axt",
+        "net-tests-utils",
+    ],
 
     // Tag this module as a cts test artifact
     test_suites: [
diff --git a/tests/cts/netpermission/updatestatspermission/src/android/net/cts/network/permission/UpdateStatsPermissionTest.java b/tests/cts/netpermission/updatestatspermission/src/android/net/cts/network/permission/UpdateStatsPermissionTest.java
index bea843c..56bad31 100644
--- a/tests/cts/netpermission/updatestatspermission/src/android/net/cts/network/permission/UpdateStatsPermissionTest.java
+++ b/tests/cts/netpermission/updatestatspermission/src/android/net/cts/network/permission/UpdateStatsPermissionTest.java
@@ -16,6 +16,10 @@
 
 package android.net.cts.networkpermission.updatestatspermission;
 
+import static android.Manifest.permission.NETWORK_SETTINGS;
+
+import static com.android.testutils.TestPermissionUtil.runAsShell;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 
@@ -25,6 +29,8 @@
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.modules.utils.build.SdkLevel;
+
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -67,6 +73,11 @@
         out.write(buf);
         out.close();
         socket.close();
+        // 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());
+        }
         long uidTxBytesAfter = TrafficStats.getUidTxBytes(Process.myUid());
         long uidTxDeltaBytes = uidTxBytesAfter - uidTxBytesBefore;
         assertTrue("uidtxb: " + uidTxBytesBefore + " -> " + uidTxBytesAfter + " delta="
diff --git a/tests/unit/java/com/android/metrics/NetworkNsdReportedMetricsTest.kt b/tests/unit/java/com/android/metrics/NetworkNsdReportedMetricsTest.kt
index aa28e5a..10ba6a4 100644
--- a/tests/unit/java/com/android/metrics/NetworkNsdReportedMetricsTest.kt
+++ b/tests/unit/java/com/android/metrics/NetworkNsdReportedMetricsTest.kt
@@ -163,7 +163,8 @@
         val sentQueryCount = 150
         val metrics = NetworkNsdReportedMetrics(clientId, deps)
         metrics.reportServiceDiscoveryStop(true /* isLegacy */, transactionId, durationMs,
-                foundCallbackCount, lostCallbackCount, servicesCount, sentQueryCount)
+                foundCallbackCount, lostCallbackCount, servicesCount, sentQueryCount,
+                true /* isServiceFromCache */)
 
         val eventCaptor = ArgumentCaptor.forClass(NetworkNsdReported::class.java)
         verify(deps).statsWrite(eventCaptor.capture())
@@ -179,6 +180,7 @@
             assertEquals(servicesCount, it.foundServiceCount)
             assertEquals(durationMs, it.eventDurationMillisec)
             assertEquals(sentQueryCount, it.sentQueryCount)
+            assertTrue(it.isKnownService)
         }
     }
 
diff --git a/tests/unit/java/com/android/server/ConnectivityServiceTest.java b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
index 44a8222..8526a9a 100755
--- a/tests/unit/java/com/android/server/ConnectivityServiceTest.java
+++ b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
@@ -2178,9 +2178,8 @@
             switch (name) {
                 case ConnectivityFlags.NO_REMATCH_ALL_REQUESTS_ON_REGISTER:
                 case ConnectivityFlags.CARRIER_SERVICE_CHANGED_USE_CALLBACK:
-                    return true;
                 case ConnectivityFlags.REQUEST_RESTRICTED_WIFI:
-                    return true;
+                case ConnectivityFlags.USE_DECLARED_METHODS_FOR_CALLBACKS:
                 case KEY_DESTROY_FROZEN_SOCKETS_VERSION:
                     return true;
                 default:
diff --git a/tests/unit/java/com/android/server/NsdServiceTest.java b/tests/unit/java/com/android/server/NsdServiceTest.java
index aece3f7..979e0a1 100644
--- a/tests/unit/java/com/android/server/NsdServiceTest.java
+++ b/tests/unit/java/com/android/server/NsdServiceTest.java
@@ -1196,7 +1196,7 @@
                 Instant.MAX /* expirationTime */);
 
         // Verify onServiceNameDiscovered callback
-        listener.onServiceNameDiscovered(foundInfo, false /* isServiceFromCache */);
+        listener.onServiceNameDiscovered(foundInfo, true /* isServiceFromCache */);
         verify(discListener, timeout(TIMEOUT_MS)).onServiceFound(argThat(info ->
                 info.getServiceName().equals(SERVICE_NAME)
                         // Service type in discovery callbacks has a dot at the end
@@ -1232,7 +1232,7 @@
         verify(mSocketProvider, timeout(CLEANUP_DELAY_MS + TIMEOUT_MS)).requestStopWhenInactive();
         verify(mMetrics).reportServiceDiscoveryStop(false /* isLegacy */, discId,
                 10L /* durationMs */, 1 /* foundCallbackCount */, 1 /* lostCallbackCount */,
-                1 /* servicesCount */, 3 /* sentQueryCount */);
+                1 /* servicesCount */, 3 /* sentQueryCount */, true /* isServiceFromCache */);
     }
 
     @Test
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSBlockedReasonsTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSBlockedReasonsTest.kt
index 0590fbb..3ad8de8 100644
--- a/tests/unit/java/com/android/server/connectivityservice/CSBlockedReasonsTest.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/CSBlockedReasonsTest.kt
@@ -387,6 +387,10 @@
                     BLOCKED_REASON_NETWORK_RESTRICTED or BLOCKED_REASON_APP_BACKGROUND
             )
         }
+        // waitForIdle since stubbing bpfNetMaps while CS handler thread calls
+        // bpfNetMaps.getNetPermForUid throws exception.
+        // ConnectivityService might haven't finished checking blocked status for all requests.
+        waitForIdle()
 
         // Disable background firewall chain
         doReturn(BLOCKED_REASON_NONE)
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSSatelliteNetworkFallbackTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSSatelliteNetworkTest.kt
similarity index 69%
rename from tests/unit/java/com/android/server/connectivityservice/CSSatelliteNetworkFallbackTest.kt
rename to tests/unit/java/com/android/server/connectivityservice/CSSatelliteNetworkTest.kt
index 9024641..88c2738 100644
--- a/tests/unit/java/com/android/server/connectivityservice/CSSatelliteNetworkFallbackTest.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/CSSatelliteNetworkTest.kt
@@ -24,6 +24,7 @@
 import android.net.NativeNetworkType
 import android.net.NetworkCapabilities
 import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED
 import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED
 import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING
 import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED
@@ -41,10 +42,13 @@
 import android.util.ArraySet
 import com.android.net.module.util.CollectionUtils
 import com.android.server.ConnectivityService.PREFERENCE_ORDER_SATELLITE_FALLBACK
+import com.android.testutils.DevSdkIgnoreRule
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
 import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.TestableNetworkCallback
 import com.android.testutils.visibleOnHandlerThread
 import org.junit.Assert
+import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.ArgumentMatchers.any
@@ -61,7 +65,10 @@
 @DevSdkIgnoreRunner.MonitorThreadLeak
 @RunWith(DevSdkIgnoreRunner::class)
 @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
-class CSSatelliteNetworkPreferredTest : CSTest() {
+class CSSatelliteNetworkTest : CSTest() {
+    @get:Rule
+    val ignoreRule = DevSdkIgnoreRule()
+
     /**
      * Test createMultiLayerNrisFromSatelliteNetworkPreferredUids returns correct
      * NetworkRequestInfo.
@@ -80,54 +87,81 @@
     }
 
     /**
-     * Test that SATELLITE_NETWORK_PREFERENCE_UIDS changes will send correct net id and uid ranges
-     * to netd.
+     * Test that satellite network satisfies satellite fallback per-app default network request and
+     * send correct net id and uid ranges to netd.
      */
-    @Test
-    fun testSatelliteNetworkPreferredUidsChanged() {
+    private fun doTestSatelliteNetworkFallbackUids(restricted: Boolean) {
         val netdInOrder = inOrder(netd)
 
-        val satelliteAgent = createSatelliteAgent("satellite0")
+        val satelliteAgent = createSatelliteAgent("satellite0", restricted)
         satelliteAgent.connect()
 
         val satelliteNetId = satelliteAgent.network.netId
+        val permission = if (restricted) {INetd.PERMISSION_SYSTEM} else {INetd.PERMISSION_NONE}
         netdInOrder.verify(netd).networkCreate(
-            nativeNetworkConfigPhysical(satelliteNetId, INetd.PERMISSION_NONE))
+            nativeNetworkConfigPhysical(satelliteNetId, permission))
 
         val uid1 = PRIMARY_USER_HANDLE.getUid(TEST_PACKAGE_UID)
         val uid2 = PRIMARY_USER_HANDLE.getUid(TEST_PACKAGE_UID2)
         val uid3 = SECONDARY_USER_HANDLE.getUid(TEST_PACKAGE_UID)
 
-        // Initial satellite network preferred uids status.
-        setAndUpdateSatelliteNetworkPreferredUids(setOf())
+        // Initial satellite network fallback uids status.
+        updateSatelliteNetworkFallbackUids(setOf())
         netdInOrder.verify(netd, never()).networkAddUidRangesParcel(any())
         netdInOrder.verify(netd, never()).networkRemoveUidRangesParcel(any())
 
-        // Set SATELLITE_NETWORK_PREFERENCE_UIDS setting and verify that net id and uid ranges
-        // send to netd
+        // Update satellite network fallback uids and verify that net id and uid ranges send to netd
         var uids = mutableSetOf(uid1, uid2, uid3)
         val uidRanges1 = toUidRangeStableParcels(uidRangesForUids(uids))
         val config1 = NativeUidRangeConfig(
             satelliteNetId, uidRanges1,
             PREFERENCE_ORDER_SATELLITE_FALLBACK
         )
-        setAndUpdateSatelliteNetworkPreferredUids(uids)
+        updateSatelliteNetworkFallbackUids(uids)
         netdInOrder.verify(netd).networkAddUidRangesParcel(config1)
         netdInOrder.verify(netd, never()).networkRemoveUidRangesParcel(any())
 
-        // Set SATELLITE_NETWORK_PREFERENCE_UIDS setting again and verify that old rules are removed
-        // and new rules are added.
+        // Update satellite network fallback uids and verify that net id and uid ranges send to netd
         uids = mutableSetOf(uid1)
         val uidRanges2: Array<UidRangeParcel?> = toUidRangeStableParcels(uidRangesForUids(uids))
         val config2 = NativeUidRangeConfig(
             satelliteNetId, uidRanges2,
             PREFERENCE_ORDER_SATELLITE_FALLBACK
         )
-        setAndUpdateSatelliteNetworkPreferredUids(uids)
+        updateSatelliteNetworkFallbackUids(uids)
         netdInOrder.verify(netd).networkRemoveUidRangesParcel(config1)
         netdInOrder.verify(netd).networkAddUidRangesParcel(config2)
     }
 
+    @Test
+    fun testSatelliteNetworkFallbackUids_restricted() {
+        doTestSatelliteNetworkFallbackUids(restricted = true)
+    }
+
+    @Test @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    fun testSatelliteNetworkFallbackUids_nonRestricted() {
+        doTestSatelliteNetworkFallbackUids(restricted = false)
+    }
+
+    private fun doTestSatelliteNeverBecomeDefaultNetwork(restricted: Boolean) {
+        val agent = createSatelliteAgent("satellite0", restricted)
+        agent.connect()
+        val defaultCb = TestableNetworkCallback()
+        cm.registerDefaultNetworkCallback(defaultCb)
+        // Satellite network must not become the default network
+        defaultCb.assertNoCallback()
+    }
+
+    @Test
+    fun testSatelliteNeverBecomeDefaultNetwork_restricted() {
+        doTestSatelliteNeverBecomeDefaultNetwork(restricted = true)
+    }
+
+    @Test @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    fun testSatelliteNeverBecomeDefaultNetwork_notRestricted() {
+        doTestSatelliteNeverBecomeDefaultNetwork(restricted = false)
+    }
+
     private fun assertCreateMultiLayerNrisFromSatelliteNetworkPreferredUids(uids: Set<Int>) {
         val nris: Set<ConnectivityService.NetworkRequestInfo> =
             service.createMultiLayerNrisFromSatelliteNetworkFallbackUids(uids)
@@ -140,7 +174,7 @@
         assertEquals(PREFERENCE_ORDER_SATELLITE_FALLBACK, nri.mPreferenceOrder)
     }
 
-    private fun setAndUpdateSatelliteNetworkPreferredUids(uids: Set<Int>) {
+    private fun updateSatelliteNetworkFallbackUids(uids: Set<Int>) {
         visibleOnHandlerThread(csHandler) {
             deps.satelliteNetworkFallbackUidUpdate!!.accept(uids)
         }
@@ -150,9 +184,9 @@
         NativeNetworkConfig(netId, NativeNetworkType.PHYSICAL, permission,
             false /* secure */, VpnManager.TYPE_VPN_NONE, false /* excludeLocalRoutes */)
 
-    private fun createSatelliteAgent(name: String): CSAgentWrapper {
+    private fun createSatelliteAgent(name: String, restricted: Boolean = true): CSAgentWrapper {
         return Agent(score = keepScore(), lp = lp(name),
-            nc = nc(TRANSPORT_SATELLITE, NET_CAPABILITY_INTERNET)
+            nc = satelliteNc(restricted)
         )
     }
 
@@ -176,17 +210,19 @@
         return uidRangesForUids(*CollectionUtils.toIntArray(uids))
     }
 
-    private fun nc(transport: Int, vararg caps: Int) = NetworkCapabilities.Builder().apply {
-        addTransportType(transport)
-        caps.forEach {
-            addCapability(it)
-        }
-        // Useful capabilities for everybody
-        addCapability(NET_CAPABILITY_NOT_RESTRICTED)
-        addCapability(NET_CAPABILITY_NOT_SUSPENDED)
-        addCapability(NET_CAPABILITY_NOT_ROAMING)
-        addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
-    }.build()
+    private fun satelliteNc(restricted: Boolean) =
+            NetworkCapabilities.Builder().apply {
+                addTransportType(TRANSPORT_SATELLITE)
+
+                addCapability(NET_CAPABILITY_INTERNET)
+                addCapability(NET_CAPABILITY_NOT_SUSPENDED)
+                addCapability(NET_CAPABILITY_NOT_ROAMING)
+                addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+                if (restricted) {
+                    removeCapability(NET_CAPABILITY_NOT_RESTRICTED)
+                }
+                removeCapability(NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED)
+            }.build()
 
     private fun lp(iface: String) = LinkProperties().apply {
         interfaceName = iface
diff --git a/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt b/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt
index 47a6763..ed72fd2 100644
--- a/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt
@@ -164,6 +164,7 @@
         it[ConnectivityFlags.INGRESS_TO_VPN_ADDRESS_FILTERING] = true
         it[ConnectivityFlags.BACKGROUND_FIREWALL_CHAIN] = true
         it[ConnectivityFlags.DELAY_DESTROY_SOCKETS] = true
+        it[ConnectivityFlags.USE_DECLARED_METHODS_FOR_CALLBACKS] = true
     }
     fun setFeatureEnabled(flag: String, enabled: Boolean) = enabledFeatures.set(flag, enabled)
 
diff --git a/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java b/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
index 9026481..c997b01 100644
--- a/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
+++ b/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
@@ -56,6 +56,7 @@
 import static android.net.TrafficStats.MB_IN_BYTES;
 import static android.net.TrafficStats.UID_REMOVED;
 import static android.net.TrafficStats.UID_TETHERING;
+import static android.net.connectivity.ConnectivityCompatChanges.ENABLE_TRAFFICSTATS_RATE_LIMIT_CACHE;
 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;
@@ -306,6 +307,7 @@
     private HandlerThread mObserverHandlerThread;
     final TestDependencies mDeps = new TestDependencies();
     final HashMap<String, Boolean> mFeatureFlags = new HashMap<>();
+    final HashMap<Long, Boolean> mCompatChanges = new HashMap<>();
 
     // This will set feature flags from @FeatureFlag annotations
     // into the map before setUp() runs.
@@ -594,7 +596,7 @@
         }
 
         @Override
-        public boolean supportTrafficStatsRateLimitCache(Context ctx) {
+        public boolean alwaysUseTrafficStatsRateLimitCache(Context ctx) {
             return mFeatureFlags.getOrDefault(TRAFFICSTATS_RATE_LIMIT_CACHE_ENABLED_FLAG, false);
         }
 
@@ -608,6 +610,14 @@
             return DEFAULT_TRAFFIC_STATS_CACHE_MAX_ENTRIES;
         }
 
+        @Override
+        public boolean isChangeEnabled(long changeId, int uid) {
+            return mCompatChanges.getOrDefault(changeId, true);
+        }
+
+        public void setChangeEnabled(long changeId, boolean enabled) {
+            mCompatChanges.put(changeId, enabled);
+        }
         @Nullable
         @Override
         public NetworkStats.Entry nativeGetTotalStat() {
@@ -2413,17 +2423,33 @@
 
     @FeatureFlag(name = TRAFFICSTATS_RATE_LIMIT_CACHE_ENABLED_FLAG, enabled = false)
     @Test
-    public void testTrafficStatsRateLimitCache_disabled() throws Exception {
-        doTestTrafficStatsRateLimitCache(false /* cacheEnabled */);
+    public void testTrafficStatsRateLimitCache_disabledWithCompatChangeEnabled() throws Exception {
+        mDeps.setChangeEnabled(ENABLE_TRAFFICSTATS_RATE_LIMIT_CACHE, true);
+        doTestTrafficStatsRateLimitCache(true /* expectCached */);
     }
 
     @FeatureFlag(name = TRAFFICSTATS_RATE_LIMIT_CACHE_ENABLED_FLAG)
     @Test
-    public void testTrafficStatsRateLimitCache_enabled() throws Exception {
-        doTestTrafficStatsRateLimitCache(true /* cacheEnabled */);
+    public void testTrafficStatsRateLimitCache_enabledWithCompatChangeEnabled() throws Exception {
+        mDeps.setChangeEnabled(ENABLE_TRAFFICSTATS_RATE_LIMIT_CACHE, true);
+        doTestTrafficStatsRateLimitCache(true /* expectCached */);
     }
 
-    private void doTestTrafficStatsRateLimitCache(boolean cacheEnabled) throws Exception {
+    @FeatureFlag(name = TRAFFICSTATS_RATE_LIMIT_CACHE_ENABLED_FLAG, enabled = false)
+    @Test
+    public void testTrafficStatsRateLimitCache_disabledWithCompatChangeDisabled() throws Exception {
+        mDeps.setChangeEnabled(ENABLE_TRAFFICSTATS_RATE_LIMIT_CACHE, false);
+        doTestTrafficStatsRateLimitCache(false /* expectCached */);
+    }
+
+    @FeatureFlag(name = TRAFFICSTATS_RATE_LIMIT_CACHE_ENABLED_FLAG)
+    @Test
+    public void testTrafficStatsRateLimitCache_enabledWithCompatChangeDisabled() throws Exception {
+        mDeps.setChangeEnabled(ENABLE_TRAFFICSTATS_RATE_LIMIT_CACHE, false);
+        doTestTrafficStatsRateLimitCache(true /* expectCached */);
+    }
+
+    private void doTestTrafficStatsRateLimitCache(boolean expectCached) 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();
@@ -2433,7 +2459,7 @@
         // Verify the values are cached.
         incrementCurrentTime(DEFAULT_TRAFFIC_STATS_CACHE_EXPIRY_DURATION_MS / 2);
         mockTrafficStatsValues(65L, 8L, 1055L, 9L);
-        if (cacheEnabled) {
+        if (expectCached) {
             assertTrafficStatsValues(TEST_IFACE, myUid, 64L, 3L, 1024L, 8L);
         } else {
             assertTrafficStatsValues(TEST_IFACE, myUid, 65L, 8L, 1055L, 9L);
diff --git a/thread/README.md b/thread/README.md
index f50e0cd..41b73ac 100644
--- a/thread/README.md
+++ b/thread/README.md
@@ -1,3 +1,18 @@
 # Thread
 
 Bring the [Thread](https://www.threadgroup.org/) networking protocol to Android.
+
+## Try Thread with Cuttlefish
+
+```
+# Get the code and go to the Android source code root directory
+
+source build/envsetup.sh
+lunch aosp_cf_x86_64_phone-trunk_staging-userdebug
+m
+
+launch_cvd
+```
+
+Open `https://localhost:8443/` in your web browser, you can find the Thread
+demoapp (with the Thread logo) in the cuttlefish instance. Open it and have fun with Thread!
diff --git a/thread/service/java/com/android/server/thread/NsdPublisher.java b/thread/service/java/com/android/server/thread/NsdPublisher.java
index 2c14f1d..d0cb9b8 100644
--- a/thread/service/java/com/android/server/thread/NsdPublisher.java
+++ b/thread/service/java/com/android/server/thread/NsdPublisher.java
@@ -19,11 +19,15 @@
 import static android.net.nsd.NsdManager.PROTOCOL_DNS_SD;
 
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.content.Context;
+import android.net.DnsResolver;
 import android.net.InetAddresses;
+import android.net.Network;
 import android.net.nsd.DiscoveryRequest;
 import android.net.nsd.NsdManager;
 import android.net.nsd.NsdServiceInfo;
+import android.os.CancellationSignal;
 import android.os.Handler;
 import android.os.RemoteException;
 import android.text.TextUtils;
@@ -34,6 +38,7 @@
 import com.android.server.thread.openthread.DnsTxtAttribute;
 import com.android.server.thread.openthread.INsdDiscoverServiceCallback;
 import com.android.server.thread.openthread.INsdPublisher;
+import com.android.server.thread.openthread.INsdResolveHostCallback;
 import com.android.server.thread.openthread.INsdResolveServiceCallback;
 import com.android.server.thread.openthread.INsdStatusReceiver;
 
@@ -41,6 +46,7 @@
 import java.net.InetAddress;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
@@ -56,24 +62,36 @@
  * {@code mHandler} itself.
  */
 public final class NsdPublisher extends INsdPublisher.Stub {
-    // TODO: b/321883491 - specify network for mDNS operations
     private static final String TAG = NsdPublisher.class.getSimpleName();
+
+    // TODO: b/321883491 - specify network for mDNS operations
+    @Nullable private Network mNetwork;
     private final NsdManager mNsdManager;
+    private final DnsResolver mDnsResolver;
     private final Handler mHandler;
     private final Executor mExecutor;
     private final SparseArray<RegistrationListener> mRegistrationListeners = new SparseArray<>(0);
     private final SparseArray<DiscoveryListener> mDiscoveryListeners = new SparseArray<>(0);
     private final SparseArray<ServiceInfoListener> mServiceInfoListeners = new SparseArray<>(0);
+    private final SparseArray<HostInfoListener> mHostInfoListeners = new SparseArray<>(0);
 
     @VisibleForTesting
-    public NsdPublisher(NsdManager nsdManager, Handler handler) {
+    public NsdPublisher(NsdManager nsdManager, DnsResolver dnsResolver, Handler handler) {
+        mNetwork = null;
         mNsdManager = nsdManager;
+        mDnsResolver = dnsResolver;
         mHandler = handler;
         mExecutor = runnable -> mHandler.post(runnable);
     }
 
     public static NsdPublisher newInstance(Context context, Handler handler) {
-        return new NsdPublisher(context.getSystemService(NsdManager.class), handler);
+        return new NsdPublisher(
+                context.getSystemService(NsdManager.class), DnsResolver.getInstance(), handler);
+    }
+
+    // TODO: b/321883491 - NsdPublisher should be disabled when mNetwork is null
+    public void setNetworkForHostResolution(@Nullable Network network) {
+        mNetwork = network;
     }
 
     @Override
@@ -291,6 +309,53 @@
         }
     }
 
+    @Override
+    public void resolveHost(String name, INsdResolveHostCallback callback, int listenerId) {
+        mHandler.post(() -> resolveHostInternal(name, callback, listenerId));
+    }
+
+    private void resolveHostInternal(
+            String name, INsdResolveHostCallback callback, int listenerId) {
+        checkOnHandlerThread();
+
+        String fullHostname = name + ".local";
+        CancellationSignal cancellationSignal = new CancellationSignal();
+        HostInfoListener listener =
+                new HostInfoListener(name, callback, cancellationSignal, listenerId);
+        mDnsResolver.query(
+                mNetwork,
+                fullHostname,
+                DnsResolver.FLAG_NO_CACHE_LOOKUP,
+                mExecutor,
+                cancellationSignal,
+                listener);
+        mHostInfoListeners.append(listenerId, listener);
+
+        Log.i(TAG, "Resolving host." + " Listener ID: " + listenerId + ", hostname: " + name);
+    }
+
+    @Override
+    public void stopHostResolution(int listenerId) {
+        mHandler.post(() -> stopHostResolutionInternal(listenerId));
+    }
+
+    private void stopHostResolutionInternal(int listenerId) {
+        checkOnHandlerThread();
+
+        HostInfoListener listener = mHostInfoListeners.get(listenerId);
+        if (listener == null) {
+            Log.w(
+                    TAG,
+                    "Failed to stop host resolution. Listener ID: "
+                            + listenerId
+                            + ". The listener is null.");
+            return;
+        }
+        Log.i(TAG, "Stopping host resolution. Listener: " + listener);
+        listener.cancel();
+        mHostInfoListeners.remove(listenerId);
+    }
+
     private void checkOnHandlerThread() {
         if (mHandler.getLooper().getThread() != Thread.currentThread()) {
             throw new IllegalStateException(
@@ -586,4 +651,78 @@
             return "ID: " + mListenerId + ", service name: " + mName + ", service type: " + mType;
         }
     }
+
+    class HostInfoListener implements DnsResolver.Callback<List<InetAddress>> {
+        private final String mHostname;
+        private final INsdResolveHostCallback mResolveHostCallback;
+        private final CancellationSignal mCancellationSignal;
+        private final int mListenerId;
+
+        HostInfoListener(
+                @NonNull String hostname,
+                INsdResolveHostCallback resolveHostCallback,
+                CancellationSignal cancellationSignal,
+                int listenerId) {
+            this.mHostname = hostname;
+            this.mResolveHostCallback = resolveHostCallback;
+            this.mCancellationSignal = cancellationSignal;
+            this.mListenerId = listenerId;
+        }
+
+        @Override
+        public void onAnswer(@NonNull List<InetAddress> answerList, int rcode) {
+            checkOnHandlerThread();
+
+            Log.i(
+                    TAG,
+                    "Host is resolved."
+                            + " Listener ID: "
+                            + mListenerId
+                            + ", hostname: "
+                            + mHostname
+                            + ", addresses: "
+                            + answerList
+                            + ", return code: "
+                            + rcode);
+            List<String> addressStrings = new ArrayList<>();
+            for (InetAddress address : answerList) {
+                addressStrings.add(address.getHostAddress());
+            }
+            try {
+                mResolveHostCallback.onHostResolved(mHostname, addressStrings);
+            } catch (RemoteException e) {
+                // do nothing if the client is dead
+            }
+            mHostInfoListeners.remove(mListenerId);
+        }
+
+        @Override
+        public void onError(@NonNull DnsResolver.DnsException error) {
+            checkOnHandlerThread();
+
+            Log.i(
+                    TAG,
+                    "Failed to resolve host."
+                            + " Listener ID: "
+                            + mListenerId
+                            + ", hostname: "
+                            + mHostname,
+                    error);
+            try {
+                mResolveHostCallback.onHostResolved(mHostname, Collections.emptyList());
+            } catch (RemoteException e) {
+                // do nothing if the client is dead
+            }
+            mHostInfoListeners.remove(mListenerId);
+        }
+
+        public String toString() {
+            return "ID: " + mListenerId + ", hostname: " + mHostname;
+        }
+
+        void cancel() {
+            mCancellationSignal.cancel();
+            mHostInfoListeners.remove(mListenerId);
+        }
+    }
 }
diff --git a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
index 02c6a09..737ec41 100644
--- a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
+++ b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
@@ -623,7 +623,10 @@
 
         return !mForceStopOtDaemonEnabled
                 && !mUserRestricted
-                && (!mAirplaneModeOn || enabledInAirplaneMode)
+                // FIXME(b/340744397): Note that here we need to call `isAirplaneModeOn()` to get
+                // the latest state of airplane mode but can't use `mIsAirplaneMode`. This is for
+                // avoiding the race conditions described in b/340744397
+                && (!isAirplaneModeOn() || enabledInAirplaneMode)
                 && mPersistentSettings.get(ThreadPersistentSettings.THREAD_ENABLED);
     }
 
@@ -714,6 +717,7 @@
                 if (mNetworkToInterface.containsKey(mUpstreamNetwork)) {
                     enableBorderRouting(mNetworkToInterface.get(mUpstreamNetwork));
                 }
+                mNsdPublisher.setNetworkForHostResolution(mUpstreamNetwork);
             }
         }
     }
@@ -1384,14 +1388,6 @@
             }
         }
 
-        private void notifyThreadEnabledUpdated(IStateCallback callback, int enabledState) {
-            try {
-                callback.onThreadEnableStateChanged(enabledState);
-            } catch (RemoteException ignored) {
-                // do nothing if the client is dead
-            }
-        }
-
         public void unregisterStateCallback(IStateCallback callback) {
             checkOnHandlerThread();
             if (!mStateCallbacks.containsKey(callback)) {
@@ -1458,15 +1454,19 @@
             }
         }
 
-        @Override
-        public void onThreadEnabledChanged(int state) {
-            mHandler.post(() -> onThreadEnabledChangedInternal(state));
-        }
-
-        private void onThreadEnabledChangedInternal(int state) {
+        private void onThreadEnabledChanged(int state, long listenerId) {
             checkOnHandlerThread();
-            for (IStateCallback callback : mStateCallbacks.keySet()) {
-                notifyThreadEnabledUpdated(callback, otStateToAndroidState(state));
+            boolean stateChanged = (mState == null || mState.threadEnabled != state);
+
+            for (var callbackEntry : mStateCallbacks.entrySet()) {
+                if (!stateChanged && callbackEntry.getValue().id != listenerId) {
+                    continue;
+                }
+                try {
+                    callbackEntry.getKey().onThreadEnableStateChanged(otStateToAndroidState(state));
+                } catch (RemoteException ignored) {
+                    // do nothing if the client is dead
+                }
             }
         }
 
@@ -1493,6 +1493,7 @@
             onInterfaceStateChanged(newState.isInterfaceUp);
             onDeviceRoleChanged(newState.deviceRole, listenerId);
             onPartitionIdChanged(newState.partitionId, listenerId);
+            onThreadEnabledChanged(newState.threadEnabled, listenerId);
             mState = newState;
 
             ActiveOperationalDataset newActiveDataset;
diff --git a/thread/tests/cts/AndroidTest.xml b/thread/tests/cts/AndroidTest.xml
index ffc181c..6eda1e9 100644
--- a/thread/tests/cts/AndroidTest.xml
+++ b/thread/tests/cts/AndroidTest.xml
@@ -38,6 +38,11 @@
         <option name="mainline-module-package-name" value="com.google.android.tethering" />
     </object>
 
+    <object type="module_controller"
+            class="com.android.tradefed.testtype.suite.module.DeviceFeatureModuleController">
+        <option name="required-feature" value="android.hardware.thread_network" />
+    </object>
+
     <!-- Install test -->
     <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
         <option name="test-file-name" value="CtsThreadNetworkTestCases.apk" />
diff --git a/thread/tests/cts/src/android/net/thread/cts/ActiveOperationalDatasetTest.java b/thread/tests/cts/src/android/net/thread/cts/ActiveOperationalDatasetTest.java
index 0e76930..996d22d 100644
--- a/thread/tests/cts/src/android/net/thread/cts/ActiveOperationalDatasetTest.java
+++ b/thread/tests/cts/src/android/net/thread/cts/ActiveOperationalDatasetTest.java
@@ -30,6 +30,8 @@
 import android.net.thread.ActiveOperationalDataset.Builder;
 import android.net.thread.ActiveOperationalDataset.SecurityPolicy;
 import android.net.thread.OperationalDatasetTimestamp;
+import android.net.thread.utils.ThreadFeatureCheckerRule;
+import android.net.thread.utils.ThreadFeatureCheckerRule.RequiresThreadFeature;
 import android.util.SparseArray;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -38,6 +40,7 @@
 import com.google.common.primitives.Bytes;
 import com.google.common.testing.EqualsTester;
 
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -46,6 +49,7 @@
 
 /** CTS tests for {@link ActiveOperationalDataset}. */
 @SmallTest
+@RequiresThreadFeature
 @RunWith(AndroidJUnit4.class)
 public final class ActiveOperationalDatasetTest {
     private static final int TYPE_ACTIVE_TIMESTAMP = 14;
@@ -81,6 +85,8 @@
     private static final ActiveOperationalDataset DEFAULT_DATASET =
             ActiveOperationalDataset.fromThreadTlvs(VALID_DATASET_TLVS);
 
+    @Rule public final ThreadFeatureCheckerRule mThreadRule = new ThreadFeatureCheckerRule();
+
     private static byte[] removeTlv(byte[] dataset, int type) {
         ByteArrayOutputStream os = new ByteArrayOutputStream(dataset.length);
         int i = 0;
diff --git a/thread/tests/cts/src/android/net/thread/cts/OperationalDatasetTimestampTest.java b/thread/tests/cts/src/android/net/thread/cts/OperationalDatasetTimestampTest.java
index 9be3d56..4d7c7f1 100644
--- a/thread/tests/cts/src/android/net/thread/cts/OperationalDatasetTimestampTest.java
+++ b/thread/tests/cts/src/android/net/thread/cts/OperationalDatasetTimestampTest.java
@@ -21,12 +21,15 @@
 import static org.junit.Assert.assertThrows;
 
 import android.net.thread.OperationalDatasetTimestamp;
+import android.net.thread.utils.ThreadFeatureCheckerRule;
+import android.net.thread.utils.ThreadFeatureCheckerRule.RequiresThreadFeature;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 
 import com.google.common.testing.EqualsTester;
 
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -34,8 +37,11 @@
 
 /** Tests for {@link OperationalDatasetTimestamp}. */
 @SmallTest
+@RequiresThreadFeature
 @RunWith(AndroidJUnit4.class)
 public final class OperationalDatasetTimestampTest {
+    @Rule public final ThreadFeatureCheckerRule mThreadRule = new ThreadFeatureCheckerRule();
+
     @Test
     public void fromInstant_tooLargeInstant_throwsIllegalArgument() {
         assertThrows(
diff --git a/thread/tests/cts/src/android/net/thread/cts/PendingOperationalDatasetTest.java b/thread/tests/cts/src/android/net/thread/cts/PendingOperationalDatasetTest.java
index 0bb18ce..76be054 100644
--- a/thread/tests/cts/src/android/net/thread/cts/PendingOperationalDatasetTest.java
+++ b/thread/tests/cts/src/android/net/thread/cts/PendingOperationalDatasetTest.java
@@ -28,6 +28,8 @@
 import android.net.thread.ActiveOperationalDataset.SecurityPolicy;
 import android.net.thread.OperationalDatasetTimestamp;
 import android.net.thread.PendingOperationalDataset;
+import android.net.thread.utils.ThreadFeatureCheckerRule;
+import android.net.thread.utils.ThreadFeatureCheckerRule.RequiresThreadFeature;
 import android.util.SparseArray;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -36,6 +38,7 @@
 import com.google.common.primitives.Bytes;
 import com.google.common.testing.EqualsTester;
 
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -44,8 +47,11 @@
 
 /** Tests for {@link PendingOperationalDataset}. */
 @SmallTest
+@RequiresThreadFeature
 @RunWith(AndroidJUnit4.class)
 public final class PendingOperationalDatasetTest {
+    @Rule public final ThreadFeatureCheckerRule mThreadRule = new ThreadFeatureCheckerRule();
+
     private static ActiveOperationalDataset createActiveDataset() throws Exception {
         SparseArray<byte[]> channelMask = new SparseArray<>(1);
         channelMask.put(0, new byte[] {0x00, 0x1f, (byte) 0xff, (byte) 0xe0});
diff --git a/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java b/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java
index dea4279..11c4819 100644
--- a/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java
+++ b/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java
@@ -43,7 +43,6 @@
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
 
-import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.fail;
 
@@ -174,6 +173,17 @@
     }
 
     @Test
+    public void subscribeThreadEnableState_getActiveDataset_onThreadEnableStateChangedNotCalled()
+            throws Exception {
+        EnabledStateListener listener = new EnabledStateListener(mController);
+        listener.expectThreadEnabledState(STATE_ENABLED);
+
+        getActiveOperationalDataset(mController);
+
+        listener.expectCallbackNotCalled();
+    }
+
+    @Test
     public void registerStateCallback_returnsUpdatedEnabledStates() throws Exception {
         CompletableFuture<Void> setFuture1 = new CompletableFuture<>();
         CompletableFuture<Void> setFuture2 = new CompletableFuture<>();
@@ -1016,7 +1026,11 @@
         }
 
         public void expectThreadEnabledState(int enabled) {
-            assertNotNull(mReadHead.poll(ENABLED_TIMEOUT_MILLIS, e -> (e == enabled)));
+            assertThat(mReadHead.poll(ENABLED_TIMEOUT_MILLIS, e -> (e == enabled))).isNotNull();
+        }
+
+        public void expectCallbackNotCalled() {
+            assertThat(mReadHead.poll(CALLBACK_TIMEOUT_MILLIS, e -> true)).isNull();
         }
 
         public void unregisterStateCallback() {
diff --git a/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkExceptionTest.java b/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkExceptionTest.java
index 7d9ae81..4de2e13 100644
--- a/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkExceptionTest.java
+++ b/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkExceptionTest.java
@@ -25,17 +25,23 @@
 import static org.junit.Assert.assertThrows;
 
 import android.net.thread.ThreadNetworkException;
+import android.net.thread.utils.ThreadFeatureCheckerRule;
+import android.net.thread.utils.ThreadFeatureCheckerRule.RequiresThreadFeature;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
 /** CTS tests for {@link ThreadNetworkException}. */
 @SmallTest
+@RequiresThreadFeature
 @RunWith(AndroidJUnit4.class)
 public final class ThreadNetworkExceptionTest {
+    @Rule public final ThreadFeatureCheckerRule mThreadRule = new ThreadFeatureCheckerRule();
+
     @Test
     public void constructor_validValues_valuesAreConnectlySet() throws Exception {
         ThreadNetworkException errorThreadDisabled =
diff --git a/thread/tests/integration/AndroidTest.xml b/thread/tests/integration/AndroidTest.xml
index 152c1c3..8f98941 100644
--- a/thread/tests/integration/AndroidTest.xml
+++ b/thread/tests/integration/AndroidTest.xml
@@ -31,6 +31,11 @@
         <option name="mainline-module-package-name" value="com.google.android.tethering" />
     </object>
 
+    <object type="module_controller"
+            class="com.android.tradefed.testtype.suite.module.DeviceFeatureModuleController">
+        <option name="required-feature" value="android.hardware.thread_network" />
+    </object>
+
     <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer" />
 
     <!-- Install test -->
diff --git a/thread/tests/integration/src/android/net/thread/utils/FullThreadDevice.java b/thread/tests/integration/src/android/net/thread/utils/FullThreadDevice.java
index 46cf562..d24059a 100644
--- a/thread/tests/integration/src/android/net/thread/utils/FullThreadDevice.java
+++ b/thread/tests/integration/src/android/net/thread/utils/FullThreadDevice.java
@@ -17,16 +17,16 @@
 
 import static android.net.thread.utils.IntegrationTestUtils.SERVICE_DISCOVERY_TIMEOUT;
 import static android.net.thread.utils.IntegrationTestUtils.waitFor;
-
 import static com.google.common.io.BaseEncoding.base16;
+import static java.util.concurrent.TimeUnit.SECONDS;
 
 import android.net.InetAddresses;
 import android.net.IpPrefix;
 import android.net.nsd.NsdServiceInfo;
 import android.net.thread.ActiveOperationalDataset;
-
+import android.os.Handler;
+import android.os.HandlerThread;
 import com.google.errorprone.annotations.FormatMethod;
-
 import java.io.BufferedReader;
 import java.io.BufferedWriter;
 import java.io.IOException;
@@ -39,6 +39,8 @@
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
 import java.util.concurrent.TimeoutException;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
@@ -60,10 +62,13 @@
     private static final float PING_TIMEOUT_0_1_SECOND = 0.1f;
     // 1 second timeout should be used when response is expected.
     private static final float PING_TIMEOUT_1_SECOND = 1f;
+    private static final int READ_LINE_TIMEOUT_SECONDS = 5;
 
     private final Process mProcess;
     private final BufferedReader mReader;
     private final BufferedWriter mWriter;
+    private final HandlerThread mReaderHandlerThread;
+    private final Handler mReaderHandler;
 
     private ActiveOperationalDataset mActiveOperationalDataset;
 
@@ -87,11 +92,15 @@
         }
         mReader = new BufferedReader(new InputStreamReader(mProcess.getInputStream()));
         mWriter = new BufferedWriter(new OutputStreamWriter(mProcess.getOutputStream()));
+        mReaderHandlerThread = new HandlerThread("FullThreadDeviceReader");
+        mReaderHandlerThread.start();
+        mReaderHandler = new Handler(mReaderHandlerThread.getLooper());
         mActiveOperationalDataset = null;
     }
 
     public void destroy() {
         mProcess.destroy();
+        mReaderHandlerThread.quit();
     }
 
     /**
@@ -213,7 +222,7 @@
     public String udpReceive() throws IOException {
         Pattern pattern =
                 Pattern.compile("> (\\d+) bytes from ([\\da-f:]+) (\\d+) ([\\x00-\\x7F]+)");
-        Matcher matcher = pattern.matcher(mReader.readLine());
+        Matcher matcher = pattern.matcher(readLine());
         matcher.matches();
 
         return matcher.group(4);
@@ -500,10 +509,27 @@
         }
     }
 
+    private String readLine() throws IOException {
+        final CompletableFuture<String> future = new CompletableFuture<>();
+        mReaderHandler.post(
+                () -> {
+                    try {
+                        future.complete(mReader.readLine());
+                    } catch (IOException e) {
+                        future.completeExceptionally(e);
+                    }
+                });
+        try {
+            return future.get(READ_LINE_TIMEOUT_SECONDS, SECONDS);
+        } catch (InterruptedException | ExecutionException | TimeoutException e) {
+            throw new IOException("Failed to read a line from ot-cli-ftd");
+        }
+    }
+
     private List<String> readUntilDone() throws IOException {
         ArrayList<String> result = new ArrayList<>();
         String line;
-        while ((line = mReader.readLine()) != null) {
+        while ((line = readLine()) != null) {
             if (line.equals("Done")) {
                 break;
             }
diff --git a/thread/tests/unit/AndroidTest.xml b/thread/tests/unit/AndroidTest.xml
index d16e423..58e9bdd 100644
--- a/thread/tests/unit/AndroidTest.xml
+++ b/thread/tests/unit/AndroidTest.xml
@@ -31,6 +31,11 @@
         <option name="mainline-module-package-name" value="com.google.android.tethering" />
     </object>
 
+    <object type="module_controller"
+            class="com.android.tradefed.testtype.suite.module.DeviceFeatureModuleController">
+        <option name="required-feature" value="android.hardware.thread_network" />
+    </object>
+
     <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
         <option name="test-file-name" value="ThreadNetworkUnitTests.apk" />
         <option name="check-min-sdk" value="true" />
diff --git a/thread/tests/unit/src/com/android/server/thread/NsdPublisherTest.java b/thread/tests/unit/src/com/android/server/thread/NsdPublisherTest.java
index 8886c73..3cae84f 100644
--- a/thread/tests/unit/src/com/android/server/thread/NsdPublisherTest.java
+++ b/thread/tests/unit/src/com/android/server/thread/NsdPublisherTest.java
@@ -16,6 +16,7 @@
 
 package com.android.server.thread;
 
+import static android.net.DnsResolver.ERROR_SYSTEM;
 import static android.net.nsd.NsdManager.FAILURE_INTERNAL_ERROR;
 import static android.net.nsd.NsdManager.PROTOCOL_DNS_SD;
 
@@ -30,15 +31,19 @@
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 
+import android.net.DnsResolver;
 import android.net.InetAddresses;
+import android.net.Network;
 import android.net.nsd.DiscoveryRequest;
 import android.net.nsd.NsdManager;
 import android.net.nsd.NsdServiceInfo;
+import android.os.CancellationSignal;
 import android.os.Handler;
 import android.os.test.TestLooper;
 
 import com.android.server.thread.openthread.DnsTxtAttribute;
 import com.android.server.thread.openthread.INsdDiscoverServiceCallback;
+import com.android.server.thread.openthread.INsdResolveHostCallback;
 import com.android.server.thread.openthread.INsdResolveServiceCallback;
 import com.android.server.thread.openthread.INsdStatusReceiver;
 
@@ -61,11 +66,14 @@
 /** Unit tests for {@link NsdPublisher}. */
 public final class NsdPublisherTest {
     @Mock private NsdManager mMockNsdManager;
+    @Mock private DnsResolver mMockDnsResolver;
 
     @Mock private INsdStatusReceiver mRegistrationReceiver;
     @Mock private INsdStatusReceiver mUnregistrationReceiver;
     @Mock private INsdDiscoverServiceCallback mDiscoverServiceCallback;
     @Mock private INsdResolveServiceCallback mResolveServiceCallback;
+    @Mock private INsdResolveHostCallback mResolveHostCallback;
+    @Mock private Network mNetwork;
 
     private TestLooper mTestLooper;
     private NsdPublisher mNsdPublisher;
@@ -637,6 +645,84 @@
     }
 
     @Test
+    public void resolveHost_hostResolved() throws Exception {
+        prepareTest();
+
+        mNsdPublisher.resolveHost("test", mResolveHostCallback, 10 /* listenerId */);
+        mTestLooper.dispatchAll();
+
+        ArgumentCaptor<DnsResolver.Callback<List<InetAddress>>> resolveHostCallbackArgumentCaptor =
+                ArgumentCaptor.forClass(DnsResolver.Callback.class);
+        verify(mMockDnsResolver, times(1))
+                .query(
+                        eq(mNetwork),
+                        eq("test.local"),
+                        eq(DnsResolver.FLAG_NO_CACHE_LOOKUP),
+                        any(Executor.class),
+                        any(CancellationSignal.class),
+                        resolveHostCallbackArgumentCaptor.capture());
+        resolveHostCallbackArgumentCaptor
+                .getValue()
+                .onAnswer(
+                        List.of(
+                                InetAddresses.parseNumericAddress("2001::1"),
+                                InetAddresses.parseNumericAddress("2001::2")),
+                        0);
+        mTestLooper.dispatchAll();
+
+        verify(mResolveHostCallback, times(1))
+                .onHostResolved("test", List.of("2001::1", "2001::2"));
+    }
+
+    @Test
+    public void resolveHost_errorReported() throws Exception {
+        prepareTest();
+
+        mNsdPublisher.resolveHost("test", mResolveHostCallback, 10 /* listenerId */);
+        mTestLooper.dispatchAll();
+
+        ArgumentCaptor<DnsResolver.Callback<List<InetAddress>>> resolveHostCallbackArgumentCaptor =
+                ArgumentCaptor.forClass(DnsResolver.Callback.class);
+        verify(mMockDnsResolver, times(1))
+                .query(
+                        eq(mNetwork),
+                        eq("test.local"),
+                        eq(DnsResolver.FLAG_NO_CACHE_LOOKUP),
+                        any(Executor.class),
+                        any(CancellationSignal.class),
+                        resolveHostCallbackArgumentCaptor.capture());
+        resolveHostCallbackArgumentCaptor
+                .getValue()
+                .onError(new DnsResolver.DnsException(ERROR_SYSTEM, null /* cause */));
+        mTestLooper.dispatchAll();
+
+        verify(mResolveHostCallback, times(1)).onHostResolved("test", Collections.emptyList());
+    }
+
+    @Test
+    public void stopHostResolution() throws Exception {
+        prepareTest();
+
+        mNsdPublisher.resolveHost("test", mResolveHostCallback, 10 /* listenerId */);
+        mTestLooper.dispatchAll();
+        ArgumentCaptor<CancellationSignal> cancellationSignalArgumentCaptor =
+                ArgumentCaptor.forClass(CancellationSignal.class);
+        verify(mMockDnsResolver, times(1))
+                .query(
+                        eq(mNetwork),
+                        eq("test.local"),
+                        eq(DnsResolver.FLAG_NO_CACHE_LOOKUP),
+                        any(Executor.class),
+                        cancellationSignalArgumentCaptor.capture(),
+                        any(DnsResolver.Callback.class));
+
+        mNsdPublisher.stopHostResolution(10 /* listenerId */);
+        mTestLooper.dispatchAll();
+
+        assertThat(cancellationSignalArgumentCaptor.getValue().isCanceled()).isTrue();
+    }
+
+    @Test
     public void reset_unregisterAll() {
         prepareTest();
 
@@ -780,6 +866,7 @@
     private void prepareTest() {
         mTestLooper = new TestLooper();
         Handler handler = new Handler(mTestLooper.getLooper());
-        mNsdPublisher = new NsdPublisher(mMockNsdManager, handler);
+        mNsdPublisher = new NsdPublisher(mMockNsdManager, mMockDnsResolver, handler);
+        mNsdPublisher.setNetworkForHostResolution(mNetwork);
     }
 }
diff --git a/thread/tests/utils/src/android/net/thread/utils/ThreadFeatureCheckerRule.java b/thread/tests/utils/src/android/net/thread/utils/ThreadFeatureCheckerRule.java
index bee9ceb..38a6e90 100644
--- a/thread/tests/utils/src/android/net/thread/utils/ThreadFeatureCheckerRule.java
+++ b/thread/tests/utils/src/android/net/thread/utils/ThreadFeatureCheckerRule.java
@@ -21,7 +21,6 @@
 import static org.junit.Assume.assumeTrue;
 
 import android.content.Context;
-import android.net.thread.ThreadNetworkManager;
 import android.os.SystemProperties;
 import android.os.VintfRuntimeInfo;
 
@@ -122,7 +121,10 @@
     /** Returns {@code true} if this device has the Thread feature supported. */
     private static boolean hasThreadFeature() {
         final Context context = ApplicationProvider.getApplicationContext();
-        return context.getSystemService(ThreadNetworkManager.class) != null;
+
+        // Use service name rather than `ThreadNetworkManager.class` to avoid
+        // `ClassNotFoundException` on U- devices.
+        return context.getSystemService("thread_network") != null;
     }
 
     /**