Snap for 11908474 from 4756fedf846f5280d6e72867f42318e95f4a5209 to 24Q3-release

Change-Id: I083d4b645b8a53be10bf06348b737f613dc2c09e
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..155333a 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,7 +1361,7 @@
         // TODO: dump downstream4 map.
         if (CollectionUtils.contains(args, DUMPSYS_RAWMAP_ARG_STATS)) {
             try (IBpfMap<TetherStatsKey, TetherStatsValue> statsMap = mDeps.getBpfStatsMap()) {
-                dumpRawMap(statsMap, pw);
+                BpfDump.dumpRawMap(statsMap, pw);
             } catch (ErrnoException | IOException e) {
                 pw.println("Error dumping stats map: " + e);
             }
@@ -1383,7 +1369,7 @@
         }
         if (CollectionUtils.contains(args, DUMPSYS_RAWMAP_ARG_UPSTREAM4)) {
             try (IBpfMap<Tether4Key, Tether4Value> upstreamMap = mDeps.getBpfUpstream4Map()) {
-                dumpRawMap(upstreamMap, pw);
+                BpfDump.dumpRawMap(upstreamMap, pw);
             } catch (ErrnoException | IOException e) {
                 pw.println("Error dumping IPv4 map: " + e);
             }
diff --git a/common/flags.aconfig b/common/flags.aconfig
index 55a96ac..6c3e89d 100644
--- a/common/flags.aconfig
+++ b/common/flags.aconfig
@@ -99,3 +99,11 @@
   description: "Flag for oem deny chains blocked reasons API"
   bug: "328732146"
 }
+
+flag {
+  name: "blocked_reason_network_restricted"
+  is_exported: true
+  namespace: "android_core_networking"
+  description: "Flag for BLOCKED_REASON_NETWORK_RESTRICTED API"
+  bug: "339559837"
+}
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/module-lib-current.txt b/framework/api/module-lib-current.txt
index d233f3e..cd7307f 100644
--- a/framework/api/module-lib-current.txt
+++ b/framework/api/module-lib-current.txt
@@ -51,6 +51,7 @@
     field public static final int BLOCKED_REASON_DOZE = 2; // 0x2
     field public static final int BLOCKED_REASON_LOCKDOWN_VPN = 16; // 0x10
     field public static final int BLOCKED_REASON_LOW_POWER_STANDBY = 32; // 0x20
+    field @FlaggedApi("com.android.net.flags.blocked_reason_network_restricted") public static final int BLOCKED_REASON_NETWORK_RESTRICTED = 256; // 0x100
     field public static final int BLOCKED_REASON_NONE = 0; // 0x0
     field @FlaggedApi("com.android.net.flags.blocked_reason_oem_deny_chains") public static final int BLOCKED_REASON_OEM_DENY = 128; // 0x80
     field public static final int BLOCKED_REASON_RESTRICTED_MODE = 8; // 0x8
diff --git a/framework/src/android/net/ConnectivityManager.java b/framework/src/android/net/ConnectivityManager.java
index 48ed732..0b37fa5 100644
--- a/framework/src/android/net/ConnectivityManager.java
+++ b/framework/src/android/net/ConnectivityManager.java
@@ -132,6 +132,8 @@
                 "com.android.net.flags.metered_network_firewall_chains";
         static final String BLOCKED_REASON_OEM_DENY_CHAINS =
                 "com.android.net.flags.blocked_reason_oem_deny_chains";
+        static final String BLOCKED_REASON_NETWORK_RESTRICTED =
+                "com.android.net.flags.blocked_reason_network_restricted";
     }
 
     /**
@@ -928,6 +930,17 @@
     public static final int BLOCKED_REASON_OEM_DENY = 1 << 7;
 
     /**
+     * Flag to indicate that an app does not have permission to access the specified network,
+     * for example, because it does not have the {@link android.Manifest.permission#INTERNET}
+     * permission.
+     *
+     * @hide
+     */
+    @FlaggedApi(Flags.BLOCKED_REASON_NETWORK_RESTRICTED)
+    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+    public static final int BLOCKED_REASON_NETWORK_RESTRICTED = 1 << 8;
+
+    /**
      * Flag to indicate that an app is subject to Data saver restrictions that would
      * result in its metered network access being blocked.
      *
@@ -968,6 +981,7 @@
             BLOCKED_REASON_LOW_POWER_STANDBY,
             BLOCKED_REASON_APP_BACKGROUND,
             BLOCKED_REASON_OEM_DENY,
+            BLOCKED_REASON_NETWORK_RESTRICTED,
             BLOCKED_METERED_REASON_DATA_SAVER,
             BLOCKED_METERED_REASON_USER_RESTRICTED,
             BLOCKED_METERED_REASON_ADMIN_DISABLED,
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..576e806 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 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..f42e8a5 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__);
@@ -278,7 +303,7 @@
 
     if (isAtLeastV && isX86() && !isKernel64Bit()) {
         ALOGE("Android V requires X86 kernel to be 64-bit.");
-        return 1;
+        if (!isTV()) return 1;
     }
 
     if (isAtLeastV) {
@@ -306,7 +331,6 @@
 
         if (bad && !isGSI()) {
             ALOGE("Unsupported kernel version (%07x).", kernelVersion());
-            sleep(60);
         }
     }
 
@@ -331,7 +355,7 @@
          * 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/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/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index ae1564a..ca99935 100755
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -39,6 +39,7 @@
 import static android.net.ConnectivityManager.BLOCKED_REASON_APP_BACKGROUND;
 import static android.net.ConnectivityManager.BLOCKED_REASON_LOCKDOWN_VPN;
 import static android.net.ConnectivityManager.BLOCKED_REASON_NONE;
+import static android.net.ConnectivityManager.BLOCKED_REASON_NETWORK_RESTRICTED;
 import static android.net.ConnectivityManager.CALLBACK_IP_CHANGED;
 import static android.net.ConnectivityManager.CONNECTIVITY_ACTION;
 import static android.net.ConnectivityManager.FIREWALL_CHAIN_BACKGROUND;
@@ -11270,7 +11271,7 @@
         final boolean metered = nai.networkCapabilities.isMetered();
         final boolean vpnBlocked = isUidBlockedByVpn(nri.mAsUid, mVpnBlockedUidRanges);
         callCallbackForRequest(nri, nai, ConnectivityManager.CALLBACK_AVAILABLE,
-                getBlockedState(blockedReasons, metered, vpnBlocked));
+                getBlockedState(nri.mAsUid, blockedReasons, metered, vpnBlocked));
     }
 
     // Notify the requests on this NAI that the network is now lingered.
@@ -11279,7 +11280,21 @@
         notifyNetworkCallbacks(nai, ConnectivityManager.CALLBACK_LOSING, lingerTime);
     }
 
-    private static int getBlockedState(int reasons, boolean metered, boolean vpnBlocked) {
+    private int getPermissionBlockedState(final int uid, final int reasons) {
+        // Before V, the blocked reasons come from NPMS, and that code already behaves as if the
+        // change was disabled: apps without the internet permission will never be told they are
+        // blocked.
+        if (!mDeps.isAtLeastV()) return reasons;
+
+        if (hasInternetPermission(uid)) return reasons;
+
+        return mDeps.isChangeEnabled(NETWORK_BLOCKED_WITHOUT_INTERNET_PERMISSION, uid)
+                ? reasons | BLOCKED_REASON_NETWORK_RESTRICTED
+                : BLOCKED_REASON_NONE;
+    }
+
+    private int getBlockedState(int uid, int reasons, boolean metered, boolean vpnBlocked) {
+        reasons = getPermissionBlockedState(uid, reasons);
         if (!metered) reasons &= ~BLOCKED_METERED_REASON_MASK;
         return vpnBlocked
                 ? reasons | BLOCKED_REASON_LOCKDOWN_VPN
@@ -11320,8 +11335,10 @@
                     ? isUidBlockedByVpn(nri.mAsUid, newBlockedUidRanges)
                     : oldVpnBlocked;
 
-            final int oldBlockedState = getBlockedState(blockedReasons, oldMetered, oldVpnBlocked);
-            final int newBlockedState = getBlockedState(blockedReasons, newMetered, newVpnBlocked);
+            final int oldBlockedState = getBlockedState(
+                    nri.mAsUid, blockedReasons, oldMetered, oldVpnBlocked);
+            final int newBlockedState = getBlockedState(
+                    nri.mAsUid, blockedReasons, newMetered, newVpnBlocked);
             if (oldBlockedState != newBlockedState) {
                 callCallbackForRequest(nri, nai, ConnectivityManager.CALLBACK_BLK_CHANGED,
                         newBlockedState);
@@ -11340,8 +11357,9 @@
             final boolean vpnBlocked = isUidBlockedByVpn(uid, mVpnBlockedUidRanges);
 
             final int oldBlockedState = getBlockedState(
-                    mUidBlockedReasons.get(uid, BLOCKED_REASON_NONE), metered, vpnBlocked);
-            final int newBlockedState = getBlockedState(blockedReasons, metered, vpnBlocked);
+                    uid, mUidBlockedReasons.get(uid, BLOCKED_REASON_NONE), metered, vpnBlocked);
+            final int newBlockedState =
+                    getBlockedState(uid, blockedReasons, metered, vpnBlocked);
             if (oldBlockedState == newBlockedState) {
                 continue;
             }
diff --git a/service/src/com/android/server/connectivity/ClatCoordinator.java b/service/src/com/android/server/connectivity/ClatCoordinator.java
index 634a8fa..2933324 100644
--- a/service/src/com/android/server/connectivity/ClatCoordinator.java
+++ b/service/src/com/android/server/connectivity/ClatCoordinator.java
@@ -659,6 +659,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 +679,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();
+            }
         }
     }
 
diff --git a/staticlibs/device/com/android/net/module/util/BpfDump.java b/staticlibs/device/com/android/net/module/util/BpfDump.java
index 7549e71..914c4ee 100644
--- a/staticlibs/device/com/android/net/module/util/BpfDump.java
+++ b/staticlibs/device/com/android/net/module/util/BpfDump.java
@@ -81,6 +81,22 @@
     }
 
     /**
+     * Dump the BpfMap entries with base64 format encoding.
+     */
+    public static <K extends Struct, V extends Struct> void dumpRawMap(IBpfMap<K, V> map,
+            PrintWriter pw) throws ErrnoException {
+        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)));
+    }
+
+    /**
      * Dump the BpfMap name and entries
      */
     public static <K extends Struct, V extends Struct> void dumpMap(IBpfMap<K, V> map,
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/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..e88c105 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;
@@ -977,11 +979,18 @@
                 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/net/src/android/net/cts/ApfIntegrationTest.kt b/tests/cts/net/src/android/net/cts/ApfIntegrationTest.kt
index a679498..90fb7a9 100644
--- a/tests/cts/net/src/android/net/cts/ApfIntegrationTest.kt
+++ b/tests/cts/net/src/android/net/cts/ApfIntegrationTest.kt
@@ -51,6 +51,7 @@
 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
@@ -191,8 +192,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
@@ -318,7 +319,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
@@ -356,9 +357,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 +373,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)
         }
     }
 
@@ -388,6 +403,10 @@
     @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 +415,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,7 +441,7 @@
         installProgram(program)
         readProgram() // wait for install completion
 
-        packetReader.sendPing(data)
+        packetReader.sendPing(data, payloadSize)
         packetReader.expectPingDropped()
     }
 
@@ -427,6 +451,10 @@
     @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 +483,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 +497,51 @@
         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
+    @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/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt b/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt
index d2e46af..06bdca6 100644
--- a/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt
+++ b/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt
@@ -27,6 +27,7 @@
 import android.net.ConnectivityManager
 import android.net.IDnsResolver
 import android.net.INetd
+import android.net.INetd.PERMISSION_INTERNET
 import android.net.LinkProperties
 import android.net.NetworkCapabilities.NET_CAPABILITY_CAPTIVE_PORTAL
 import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
@@ -225,6 +226,9 @@
         override fun getSystemProperties() = mock(MockableSystemProperties::class.java)
         override fun makeNetIdManager() = TestNetIdManager()
         override fun getBpfNetMaps(context: Context?, netd: INetd?) = mock(BpfNetMaps::class.java)
+                .also {
+                    doReturn(PERMISSION_INTERNET).`when`(it).getNetPermForUid(anyInt())
+                }
         override fun isChangeEnabled(changeId: Long, uid: Int) = true
 
         override fun makeMultinetworkPolicyTracker(
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 6e63807..44a8222 100755
--- a/tests/unit/java/com/android/server/ConnectivityServiceTest.java
+++ b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
@@ -53,6 +53,7 @@
 import static android.net.ConnectivityManager.BLOCKED_METERED_REASON_MASK;
 import static android.net.ConnectivityManager.BLOCKED_METERED_REASON_USER_RESTRICTED;
 import static android.net.ConnectivityManager.BLOCKED_REASON_BATTERY_SAVER;
+import static android.net.ConnectivityManager.BLOCKED_REASON_DOZE;
 import static android.net.ConnectivityManager.BLOCKED_REASON_NONE;
 import static android.net.ConnectivityManager.CONNECTIVITY_ACTION;
 import static android.net.ConnectivityManager.EXTRA_DEVICE_TYPE;
@@ -9880,6 +9881,28 @@
         assertNetworkInfo(TYPE_MOBILE, DetailedState.CONNECTED);
         assertExtraInfoFromCmPresent(mCellAgent);
 
+        // Remove PERMISSION_INTERNET and disable NETWORK_BLOCKED_WITHOUT_INTERNET_PERMISSION
+        doReturn(INetd.PERMISSION_NONE).when(mBpfNetMaps).getNetPermForUid(Process.myUid());
+        mDeps.setChangeIdEnabled(false,
+                NETWORK_BLOCKED_WITHOUT_INTERNET_PERMISSION, Process.myUid());
+
+        setBlockedReasonChanged(BLOCKED_REASON_DOZE);
+        if (mDeps.isAtLeastV()) {
+            // On V+, network access from app that does not have INTERNET permission is considered
+            // not blocked if NETWORK_BLOCKED_WITHOUT_INTERNET_PERMISSION is disabled.
+            // So blocked status does not change from BLOCKED_REASON_NONE
+            cellNetworkCallback.assertNoCallback();
+            detailedCallback.assertNoCallback();
+        } else {
+            // On U-, onBlockedStatusChanged callback is called with blocked reasons CS receives
+            // from NPMS callback regardless of permission app has.
+            // Note that this cannot actually happen because on U-, NPMS will never notify any
+            // blocked reasons for apps that don't have the INTERNET permission.
+            cellNetworkCallback.expect(BLOCKED_STATUS, mCellAgent, cb -> cb.getBlocked());
+            detailedCallback.expect(BLOCKED_STATUS_INT, mCellAgent,
+                    cb -> cb.getReason() == BLOCKED_REASON_DOZE);
+        }
+
         mCm.unregisterNetworkCallback(cellNetworkCallback);
     }
 
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 2694916..0590fbb 100644
--- a/tests/unit/java/com/android/server/connectivityservice/CSBlockedReasonsTest.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/CSBlockedReasonsTest.kt
@@ -20,6 +20,7 @@
 import android.net.ConnectivityManager.BLOCKED_METERED_REASON_USER_RESTRICTED
 import android.net.ConnectivityManager.BLOCKED_REASON_APP_BACKGROUND
 import android.net.ConnectivityManager.BLOCKED_REASON_DOZE
+import android.net.ConnectivityManager.BLOCKED_REASON_NETWORK_RESTRICTED
 import android.net.ConnectivityManager.BLOCKED_REASON_NONE
 import android.net.ConnectivityManager.FIREWALL_CHAIN_BACKGROUND
 import android.net.ConnectivityManager.FIREWALL_CHAIN_DOZABLE
@@ -27,6 +28,7 @@
 import android.net.ConnectivityManager.FIREWALL_RULE_ALLOW
 import android.net.ConnectivityManager.FIREWALL_RULE_DENY
 import android.net.ConnectivitySettingsManager
+import android.net.INetd.PERMISSION_NONE
 import android.net.Network
 import android.net.NetworkCapabilities
 import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
@@ -36,6 +38,7 @@
 import android.net.NetworkCapabilities.TRANSPORT_CELLULAR
 import android.net.NetworkCapabilities.TRANSPORT_WIFI
 import android.net.NetworkRequest
+import android.net.connectivity.ConnectivityCompatChanges.NETWORK_BLOCKED_WITHOUT_INTERNET_PERMISSION
 import android.os.Build
 import android.os.Process
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
@@ -144,6 +147,12 @@
         cm.setDataSaverEnabled(false)
         cellCb.expectBlockedStatusChanged(cellAgent.network, BLOCKED_REASON_APP_BACKGROUND)
 
+        // waitForIdle since stubbing bpfNetMaps while CS handler thread calls
+        // bpfNetMaps.getNetPermForUid throws exception.
+        // The expectBlockedStatusChanged just above guarantees that the onBlockedStatusChanged
+        // method on this callback was called, but it does not guarantee that ConnectivityService
+        // has finished processing all onBlockedStatusChanged callbacks for all requests.
+        waitForIdle()
         // Enable data saver
         doReturn(BLOCKED_REASON_APP_BACKGROUND or BLOCKED_METERED_REASON_DATA_SAVER)
                 .`when`(bpfNetMaps).getUidNetworkingBlockedReasons(Process.myUid())
@@ -186,6 +195,12 @@
                 blockedReason = BLOCKED_REASON_DOZE
         )
 
+        // waitForIdle since stubbing bpfNetMaps while CS handler thread calls
+        // bpfNetMaps.getNetPermForUid throws exception.
+        // The expectBlockedStatusChanged just above guarantees that the onBlockedStatusChanged
+        // method on this callback was called, but it does not guarantee that ConnectivityService
+        // has finished processing all onBlockedStatusChanged callbacks for all requests.
+        waitForIdle()
         // Set RULE_ALLOW on metered deny chain
         doReturn(BLOCKED_REASON_DOZE)
                 .`when`(bpfNetMaps).getUidNetworkingBlockedReasons(Process.myUid())
@@ -343,4 +358,61 @@
         cm.unregisterNetworkCallback(wifiCb)
         cm.unregisterNetworkCallback(cb)
     }
+
+    private fun doTestBlockedReasonsNoInternetPermission(blockedByNoInternetPermission: Boolean) {
+        doReturn(PERMISSION_NONE).`when`(bpfNetMaps).getNetPermForUid(Process.myUid())
+
+        val wifiCb = DetailedBlockedStatusCallback()
+        cm.requestNetwork(wifiRequest(), wifiCb)
+        val wifiAgent = Agent(nc = wifiNc())
+        wifiAgent.connect()
+        val expectedBlockedReason = if (blockedByNoInternetPermission) {
+            BLOCKED_REASON_NETWORK_RESTRICTED
+        } else {
+            BLOCKED_REASON_NONE
+        }
+        wifiCb.expectAvailableCallbacks(
+                wifiAgent.network,
+                validated = false,
+                blockedReason = expectedBlockedReason
+        )
+
+        // Enable background firewall chain
+        doReturn(BLOCKED_REASON_APP_BACKGROUND)
+                .`when`(bpfNetMaps).getUidNetworkingBlockedReasons(Process.myUid())
+        cm.setFirewallChainEnabled(FIREWALL_CHAIN_BACKGROUND, true)
+        if (blockedByNoInternetPermission) {
+            wifiCb.expectBlockedStatusChanged(
+                    wifiAgent.network,
+                    BLOCKED_REASON_NETWORK_RESTRICTED or BLOCKED_REASON_APP_BACKGROUND
+            )
+        }
+
+        // Disable background firewall chain
+        doReturn(BLOCKED_REASON_NONE)
+                .`when`(bpfNetMaps).getUidNetworkingBlockedReasons(Process.myUid())
+        cm.setFirewallChainEnabled(FIREWALL_CHAIN_BACKGROUND, false)
+        if (blockedByNoInternetPermission) {
+            wifiCb.expectBlockedStatusChanged(
+                    wifiAgent.network,
+                    BLOCKED_REASON_NETWORK_RESTRICTED
+            )
+        } else {
+            // No callback is expected since blocked reasons does not change from
+            // BLOCKED_REASON_NONE.
+            wifiCb.assertNoCallback()
+        }
+    }
+
+    @Test
+    fun testBlockedReasonsNoInternetPermission_changeDisabled() {
+        deps.setChangeIdEnabled(false, NETWORK_BLOCKED_WITHOUT_INTERNET_PERMISSION)
+        doTestBlockedReasonsNoInternetPermission(blockedByNoInternetPermission = false)
+    }
+
+    @Test
+    fun testBlockedReasonsNoInternetPermission_changeEnabled() {
+        deps.setChangeIdEnabled(true, NETWORK_BLOCKED_WITHOUT_INTERNET_PERMISSION)
+        doTestBlockedReasonsNoInternetPermission(blockedByNoInternetPermission = true)
+    }
 }
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSIngressDiscardRuleTests.kt b/tests/unit/java/com/android/server/connectivityservice/CSIngressDiscardRuleTests.kt
index bb7fb51..93f6e81 100644
--- a/tests/unit/java/com/android/server/connectivityservice/CSIngressDiscardRuleTests.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/CSIngressDiscardRuleTests.kt
@@ -37,6 +37,7 @@
 import com.android.testutils.TestableNetworkCallback
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.InOrder
 import org.mockito.Mockito.inOrder
 import org.mockito.Mockito.never
 import org.mockito.Mockito.timeout
@@ -96,6 +97,11 @@
     private val LOCAL_IPV6_ADDRRESS = InetAddresses.parseNumericAddress("fe80::1234")
     private val LOCAL_IPV6_LINK_ADDRRESS = LinkAddress(LOCAL_IPV6_ADDRRESS, 64)
 
+    fun verifyNoMoreIngressDiscardRuleChange(inorder: InOrder) {
+        inorder.verify(bpfNetMaps, never()).setIngressDiscardRule(any(), any())
+        inorder.verify(bpfNetMaps, never()).removeIngressDiscardRule(any())
+    }
+
     @Test
     fun testVpnIngressDiscardRule_UpdateVpnAddress() {
         // non-VPN network whose address will be not duplicated with VPN address
@@ -148,7 +154,7 @@
 
         // IngressDiscardRule is added to the VPN address
         inorder.verify(bpfNetMaps).setIngressDiscardRule(IPV6_ADDRESS, VPN_IFNAME)
-        inorder.verifyNoMoreInteractions()
+        verifyNoMoreIngressDiscardRuleChange(inorder)
 
         // The VPN interface name is changed
         val newlp = lp(VPN_IFNAME2, IPV6_LINK_ADDRESS, LOCAL_IPV6_LINK_ADDRRESS)
@@ -157,7 +163,7 @@
 
         // IngressDiscardRule is updated with the new interface name
         inorder.verify(bpfNetMaps).setIngressDiscardRule(IPV6_ADDRESS, VPN_IFNAME2)
-        inorder.verifyNoMoreInteractions()
+        verifyNoMoreIngressDiscardRuleChange(inorder)
 
         agent.disconnect()
         inorder.verify(bpfNetMaps, timeout(TIMEOUT_MS)).removeIngressDiscardRule(IPV6_ADDRESS)
@@ -206,10 +212,10 @@
         // IngressDiscardRule for IPV6_ADDRESS2 is removed but IngressDiscardRule for
         // IPV6_LINK_ADDRESS is not added since Wi-Fi also uses IPV6_LINK_ADDRESS
         inorder.verify(bpfNetMaps).removeIngressDiscardRule(IPV6_ADDRESS2)
-        inorder.verifyNoMoreInteractions()
+        verifyNoMoreIngressDiscardRuleChange(inorder)
 
         vpnAgent.disconnect()
-        inorder.verifyNoMoreInteractions()
+        verifyNoMoreIngressDiscardRuleChange(inorder)
 
         cm.unregisterNetworkCallback(cb)
     }
@@ -225,7 +231,7 @@
 
         // IngressDiscardRule is added to the VPN address
         inorder.verify(bpfNetMaps).setIngressDiscardRule(IPV6_ADDRESS, VPN_IFNAME)
-        inorder.verifyNoMoreInteractions()
+        verifyNoMoreIngressDiscardRuleChange(inorder)
 
         val nr = nr(TRANSPORT_WIFI)
         val cb = TestableNetworkCallback()
@@ -247,7 +253,7 @@
         // IngressDiscardRule is added to the VPN address since the VPN address is not duplicated
         // with the Wi-Fi address
         inorder.verify(bpfNetMaps).setIngressDiscardRule(IPV6_ADDRESS, VPN_IFNAME)
-        inorder.verifyNoMoreInteractions()
+        verifyNoMoreIngressDiscardRuleChange(inorder)
 
         // The Wi-Fi address is changed back to the same address as the VPN interface
         wifiAgent.sendLinkProperties(wifiLp)
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 6f56f12..47a6763 100644
--- a/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt
@@ -27,6 +27,7 @@
 import android.content.res.Resources
 import android.net.ConnectivityManager
 import android.net.INetd
+import android.net.INetd.PERMISSION_INTERNET
 import android.net.InetAddresses
 import android.net.LinkProperties
 import android.net.LocalNetworkConfig
@@ -89,6 +90,7 @@
 import org.junit.Rule
 import org.junit.rules.TestName
 import org.mockito.AdditionalAnswers.delegatesTo
+import org.mockito.ArgumentMatchers.anyInt
 import org.mockito.Mockito.doAnswer
 import org.mockito.Mockito.doReturn
 import org.mockito.Mockito.mock
@@ -187,7 +189,9 @@
     val connResources = makeMockConnResources(sysResources, packageManager)
 
     val netd = mock<INetd>()
-    val bpfNetMaps = mock<BpfNetMaps>()
+    val bpfNetMaps = mock<BpfNetMaps>().also {
+        doReturn(PERMISSION_INTERNET).`when`(it).getNetPermForUid(anyInt())
+    }
     val clatCoordinator = mock<ClatCoordinator>()
     val networkRequestStateStatsMetrics = mock<NetworkRequestStateStatsMetrics>()
     val proxyTracker = ProxyTracker(
diff --git a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
index 02c6a09..af9abdf 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);
     }
 
@@ -1384,14 +1387,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 +1453,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 +1492,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/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/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;
     }
 
     /**