Merge "Fix legacy tether behavior for duplicate requests" into main
diff --git a/PREUPLOAD.cfg b/PREUPLOAD.cfg
index 39009cb..c301397 100644
--- a/PREUPLOAD.cfg
+++ b/PREUPLOAD.cfg
@@ -1,15 +1,13 @@
 [Builtin Hooks]
 bpfmt = true
 clang_format = true
-ktfmt = true
 
 [Builtin Hooks Options]
 clang_format = --commit ${PREUPLOAD_COMMIT} --style file --extensions c,h,cc,cpp,hpp
-ktfmt = --kotlinlang-style
 
 [Hook Scripts]
 checkstyle_hook = ${REPO_ROOT}/prebuilts/checkstyle/checkstyle.py --sha ${PREUPLOAD_COMMIT}
 
-ktlint_hook = ${REPO_ROOT}/prebuilts/ktlint/ktlint.py --no-verify-format -f ${PREUPLOAD_FILES}
+ktlint_hook = ${REPO_ROOT}/prebuilts/ktlint/ktlint.py --disabled-rules comment-wrapping -f ${PREUPLOAD_FILES}
 
 hidden_api_txt_checksorted_hook = ${REPO_ROOT}/tools/platform-compat/hiddenapi/checksorted_sha.sh ${PREUPLOAD_COMMIT} ${REPO_ROOT}
diff --git a/TEST_MAPPING b/TEST_MAPPING
index c1bc31e..1d2041b 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -427,6 +427,9 @@
   "automotive-mumd-presubmit": [
     {
       "name": "CtsNetTestCases"
+    },
+    {
+      "name": "CtsNetTestCasesUpdateStatsPermission"
     }
   ],
   "imports": [
diff --git a/Tethering/apex/Android.bp b/Tethering/apex/Android.bp
index 0c05354..19dd492 100644
--- a/Tethering/apex/Android.bp
+++ b/Tethering/apex/Android.bp
@@ -155,7 +155,10 @@
         "framework-connectivity",
         "framework-connectivity-t",
         "framework-tethering",
-    ],
+    ] + select(release_flag("RELEASE_MOVE_VCN_TO_MAINLINE"), {
+        true: ["framework-connectivity-b"],
+        default: [],
+    }),
     apex_available: ["com.android.tethering"],
 
     // The bootclasspath_fragments that provide APIs on which this depends.
@@ -195,6 +198,7 @@
             "android.net.http",
             "android.net.netstats",
             "android.net.util",
+            "android.net.vcn",
         ],
 
         // The following packages and all their subpackages currently only
diff --git a/Tethering/common/TetheringLib/src/android/net/TetheringManager.java b/Tethering/common/TetheringLib/src/android/net/TetheringManager.java
index e0ea0d4..824727e 100644
--- a/Tethering/common/TetheringLib/src/android/net/TetheringManager.java
+++ b/Tethering/common/TetheringLib/src/android/net/TetheringManager.java
@@ -388,7 +388,9 @@
         // up and be sent from a worker thread; later, they are always sent from the caller thread.
         // Considering that it's just oneway binder calls, and ordering is preserved, this seems
         // better than inconsistent behavior persisting after boot.
-        if (connector != null) {
+        // If system server restarted, mConnectorSupplier might temporarily return a stale (i.e.
+        // dead) version of TetheringService.
+        if (connector != null && connector.isBinderAlive()) {
             mConnector = ITetheringConnector.Stub.asInterface(connector);
         } else {
             startPollingForConnector();
@@ -423,9 +425,8 @@
                 } catch (InterruptedException e) {
                     // Not much to do here, the system needs to wait for the connector
                 }
-
                 final IBinder connector = mConnectorSupplier.get();
-                if (connector != null) {
+                if (connector != null && connector.isBinderAlive()) {
                     onTetheringConnected(ITetheringConnector.Stub.asInterface(connector));
                     return;
                 }
diff --git a/Tethering/src/android/net/ip/IpServer.java b/Tethering/src/android/net/ip/IpServer.java
index ebc9e4e..fa6ce95 100644
--- a/Tethering/src/android/net/ip/IpServer.java
+++ b/Tethering/src/android/net/ip/IpServer.java
@@ -16,9 +16,16 @@
 
 package android.net.ip;
 
+import static android.net.INetd.LOCAL_NET_ID;
 import static android.net.RouteInfo.RTN_UNICAST;
 import static android.net.TetheringManager.CONNECTIVITY_SCOPE_GLOBAL;
 import static android.net.TetheringManager.CONNECTIVITY_SCOPE_LOCAL;
+import static android.net.TetheringManager.TETHERING_BLUETOOTH;
+import static android.net.TetheringManager.TETHERING_ETHERNET;
+import static android.net.TetheringManager.TETHERING_NCM;
+import static android.net.TetheringManager.TETHERING_WIFI;
+import static android.net.TetheringManager.TETHERING_WIFI_P2P;
+import static android.net.TetheringManager.TETHERING_WIGIG;
 import static android.net.TetheringManager.TETHER_ERROR_DHCPSERVER_ERROR;
 import static android.net.TetheringManager.TETHER_ERROR_ENABLE_FORWARDING_ERROR;
 import static android.net.TetheringManager.TETHER_ERROR_IFACE_CFG_ERROR;
@@ -45,7 +52,6 @@
 import android.net.MacAddress;
 import android.net.RouteInfo;
 import android.net.TetheredClient;
-import android.net.TetheringManager;
 import android.net.TetheringManager.TetheringRequest;
 import android.net.dhcp.DhcpLeaseParcelable;
 import android.net.dhcp.DhcpServerCallbacks;
@@ -589,8 +595,8 @@
             @NonNull final Inet4Address dnsServer, @NonNull LinkAddress serverAddr,
             @Nullable Inet4Address clientAddr) {
         final boolean changePrefixOnDecline =
-                (mInterfaceType == TetheringManager.TETHERING_NCM && clientAddr == null);
-        final int subnetPrefixLength = mInterfaceType == TetheringManager.TETHERING_WIFI_P2P
+                (mInterfaceType == TETHERING_NCM && clientAddr == null);
+        final int subnetPrefixLength = mInterfaceType == TETHERING_WIFI_P2P
                 ? mP2pLeasesSubnetPrefixLength : 0 /* default value */;
 
         return new DhcpServingParamsParcelExt()
@@ -690,10 +696,10 @@
         final IpPrefix ipv4Prefix = asIpPrefix(mIpv4Address);
 
         final Boolean setIfaceUp;
-        if (mInterfaceType == TetheringManager.TETHERING_WIFI
-                || mInterfaceType == TetheringManager.TETHERING_WIFI_P2P
-                || mInterfaceType == TetheringManager.TETHERING_ETHERNET
-                || mInterfaceType == TetheringManager.TETHERING_WIGIG) {
+        if (mInterfaceType == TETHERING_WIFI
+                || mInterfaceType == TETHERING_WIFI_P2P
+                || mInterfaceType == TETHERING_ETHERNET
+                || mInterfaceType == TETHERING_WIGIG) {
             // The WiFi and Ethernet stack has ownership of the interface up/down state.
             // It is unclear whether the Bluetooth or USB stacks will manage their own
             // state.
@@ -719,12 +725,12 @@
 
     private boolean shouldNotConfigureBluetoothInterface() {
         // Before T, bluetooth tethering configures the interface elsewhere.
-        return (mInterfaceType == TetheringManager.TETHERING_BLUETOOTH) && !SdkLevel.isAtLeastT();
+        return (mInterfaceType == TETHERING_BLUETOOTH) && !SdkLevel.isAtLeastT();
     }
 
     private boolean shouldUseWifiP2pDedicatedIp() {
         return mIsWifiP2pDedicatedIpEnabled
-                && mInterfaceType == TetheringManager.TETHERING_WIFI_P2P;
+                && mInterfaceType == TETHERING_WIFI_P2P;
     }
 
     private LinkAddress requestIpv4Address(final int scope, final boolean useLastAddress) {
@@ -844,12 +850,13 @@
         }
     }
 
-    private void removeRoutesFromLocalNetwork(@NonNull final List<RouteInfo> toBeRemoved) {
-        final int removalFailures = NetdUtils.removeRoutesFromLocalNetwork(
-                mNetd, toBeRemoved);
+    private void removeRoutesFromNetworkAndLinkProperties(int netId,
+            @NonNull final List<RouteInfo> toBeRemoved) {
+        final int removalFailures = NetdUtils.removeRoutesFromNetwork(
+                mNetd, netId, toBeRemoved);
         if (removalFailures > 0) {
-            mLog.e(String.format("Failed to remove %d IPv6 routes from local table.",
-                    removalFailures));
+            mLog.e("Failed to remove " + removalFailures
+                    + " IPv6 routes from network " + netId + ".");
         }
 
         for (RouteInfo route : toBeRemoved) mLinkProperties.removeRoute(route);
@@ -879,14 +886,15 @@
         }
     }
 
-    private void addRoutesToLocalNetwork(@NonNull final List<RouteInfo> toBeAdded) {
+    private void addRoutesToNetworkAndLinkProperties(int netId,
+            @NonNull final List<RouteInfo> toBeAdded) {
         // It's safe to call addInterfaceToNetwork() even if
-        // the interface is already in the local_network.
-        addInterfaceToNetwork(INetd.LOCAL_NET_ID, mIfaceName);
+        // the interface is already in the network.
+        addInterfaceToNetwork(netId, mIfaceName);
         try {
             // Add routes from local network. Note that adding routes that
             // already exist does not cause an error (EEXIST is silently ignored).
-            NetdUtils.addRoutesToLocalNetwork(mNetd, mIfaceName, toBeAdded);
+            NetdUtils.addRoutesToNetwork(mNetd, netId, mIfaceName, toBeAdded);
         } catch (IllegalStateException e) {
             mLog.e("Failed to add IPv4/v6 routes to local table: " + e);
             return;
@@ -899,7 +907,8 @@
             ArraySet<IpPrefix> deprecatedPrefixes, ArraySet<IpPrefix> newPrefixes) {
         // [1] Remove the routes that are deprecated.
         if (!deprecatedPrefixes.isEmpty()) {
-            removeRoutesFromLocalNetwork(getLocalRoutesFor(mIfaceName, deprecatedPrefixes));
+            removeRoutesFromNetworkAndLinkProperties(LOCAL_NET_ID,
+                    getLocalRoutesFor(mIfaceName, deprecatedPrefixes));
         }
 
         // [2] Add only the routes that have not previously been added.
@@ -910,7 +919,8 @@
             }
 
             if (!addedPrefixes.isEmpty()) {
-                addRoutesToLocalNetwork(getLocalRoutesFor(mIfaceName, addedPrefixes));
+                addRoutesToNetworkAndLinkProperties(LOCAL_NET_ID,
+                        getLocalRoutesFor(mIfaceName, addedPrefixes));
             }
         }
     }
@@ -1114,7 +1124,8 @@
             }
 
             try {
-                NetdUtils.tetherInterface(mNetd, mIfaceName, asIpPrefix(mIpv4Address));
+                NetdUtils.tetherInterface(mNetd, LOCAL_NET_ID, mIfaceName,
+                        asIpPrefix(mIpv4Address));
             } catch (RemoteException | ServiceSpecificException | IllegalStateException e) {
                 mLog.e("Error Tethering", e);
                 mLastError = TETHER_ERROR_TETHER_IFACE_ERROR;
@@ -1136,7 +1147,7 @@
             stopIPv6();
 
             try {
-                NetdUtils.untetherInterface(mNetd, mIfaceName);
+                NetdUtils.untetherInterface(mNetd, LOCAL_NET_ID, mIfaceName);
             } catch (RemoteException | ServiceSpecificException e) {
                 mLastError = TETHER_ERROR_UNTETHER_IFACE_ERROR;
                 mLog.e("Failed to untether interface: " + e);
@@ -1213,14 +1224,14 @@
                 return;
             }
 
-            // Remove deprecated routes from local network.
-            removeRoutesFromLocalNetwork(
-                    Collections.singletonList(getDirectConnectedRoute(deprecatedLinkAddress)));
+            // Remove deprecated routes from downstream network.
+            removeRoutesFromNetworkAndLinkProperties(LOCAL_NET_ID,
+                    List.of(getDirectConnectedRoute(deprecatedLinkAddress)));
             mLinkProperties.removeLinkAddress(deprecatedLinkAddress);
 
-            // Add new routes to local network.
-            addRoutesToLocalNetwork(
-                    Collections.singletonList(getDirectConnectedRoute(mIpv4Address)));
+            // Add new routes to downstream network.
+            addRoutesToNetworkAndLinkProperties(LOCAL_NET_ID,
+                    List.of(getDirectConnectedRoute(mIpv4Address)));
             mLinkProperties.addLinkAddress(mIpv4Address);
 
             // Update local DNS caching server with new IPv4 address, otherwise, dnsmasq doesn't
diff --git a/Tethering/src/com/android/networkstack/tethering/EntitlementManager.java b/Tethering/src/com/android/networkstack/tethering/EntitlementManager.java
index fb16226..a942166 100644
--- a/Tethering/src/com/android/networkstack/tethering/EntitlementManager.java
+++ b/Tethering/src/com/android/networkstack/tethering/EntitlementManager.java
@@ -154,7 +154,9 @@
 
             // Only launch entitlement UI for the current user if it is allowed to
             // change tethering. This usually means the system user or the admin users in HSUM.
-            if (SdkLevel.isAtLeastT()) {
+            // TODO (b/382624069): Figure out whether it is safe to call createContextAsUser
+            //  from secondary user. And re-enable the check or remove the code accordingly.
+            if (false) {
                 // Create a user context for the current foreground user as UserManager#isAdmin()
                 // operates on the context user.
                 final int currentUserId = getCurrentUser();
diff --git a/Tethering/src/com/android/networkstack/tethering/Tethering.java b/Tethering/src/com/android/networkstack/tethering/Tethering.java
index a96f66c..70934af 100644
--- a/Tethering/src/com/android/networkstack/tethering/Tethering.java
+++ b/Tethering/src/com/android/networkstack/tethering/Tethering.java
@@ -2089,7 +2089,7 @@
                 }
 
                 mRoutingCoordinator.maybeRemoveDeprecatedUpstreams();
-                mUpstreamNetworkMonitor.startObserveAllNetworks();
+                mUpstreamNetworkMonitor.startObserveUpstreamNetworks();
 
                 // TODO: De-duplicate with updateUpstreamWanted() below.
                 if (upstreamWanted()) {
diff --git a/Tethering/src/com/android/networkstack/tethering/UpstreamNetworkMonitor.java b/Tethering/src/com/android/networkstack/tethering/UpstreamNetworkMonitor.java
index 7a05d74..9705d84 100644
--- a/Tethering/src/com/android/networkstack/tethering/UpstreamNetworkMonitor.java
+++ b/Tethering/src/com/android/networkstack/tethering/UpstreamNetworkMonitor.java
@@ -24,6 +24,7 @@
 import static android.net.ConnectivityManager.TYPE_WIFI;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_DUN;
 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_RESTRICTED;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VPN;
 import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
@@ -44,6 +45,7 @@
 import androidx.annotation.Nullable;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.modules.utils.build.SdkLevel;
 import com.android.net.module.util.SharedLog;
 import com.android.networkstack.apishim.ConnectivityManagerShimImpl;
 import com.android.networkstack.apishim.common.ConnectivityManagerShim;
@@ -62,9 +64,10 @@
  * The owner of UNM gets it to register network callbacks by calling the
  * following methods :
  * Calling #startTrackDefaultNetwork() to track the system default network.
- * Calling #startObserveAllNetworks() to observe all networks. Listening all
- * networks is necessary while the expression of preferred upstreams remains
- * a list of legacy connectivity types.  In future, this can be revisited.
+ * Calling #startObserveUpstreamNetworks() to observe upstream networks.
+ * Listening all upstream networks is necessary while the expression of
+ * preferred upstreams remains a list of legacy connectivity types.
+ * In future, this can be revisited.
  * Calling #setTryCell() to request bringing up mobile DUN or HIPRI.
  *
  * The methods and data members of this class are only to be accessed and
@@ -94,7 +97,7 @@
     @VisibleForTesting
     public static final int TYPE_NONE = -1;
 
-    private static final int CALLBACK_LISTEN_ALL = 1;
+    private static final int CALLBACK_LISTEN_UPSTREAM = 1;
     private static final int CALLBACK_DEFAULT_INTERNET = 2;
     private static final int CALLBACK_MOBILE_REQUEST = 3;
 
@@ -116,7 +119,7 @@
     private HashSet<IpPrefix> mLocalPrefixes;
     private ConnectivityManager mCM;
     private EntitlementManager mEntitlementMgr;
-    private NetworkCallback mListenAllCallback;
+    private NetworkCallback mListenUpstreamCallback;
     private NetworkCallback mDefaultNetworkCallback;
     private NetworkCallback mMobileNetworkCallback;
 
@@ -157,20 +160,29 @@
         }
         ConnectivityManagerShim mCmShim = ConnectivityManagerShimImpl.newInstance(mContext);
         mDefaultNetworkCallback = new UpstreamNetworkCallback(CALLBACK_DEFAULT_INTERNET);
+        // TODO (b/382413665): By definition, a local network cannot be the system default,
+        //  because it does not provide internet capability. Figure out whether this
+        //  is enforced in ConnectivityService. Or what will happen for tethering if it happens.
         mCmShim.registerSystemDefaultNetworkCallback(mDefaultNetworkCallback, mHandler);
         if (mEntitlementMgr == null) {
             mEntitlementMgr = entitle;
         }
     }
 
-    /** Listen all networks. */
-    public void startObserveAllNetworks() {
+    /** Listen upstream networks. */
+    public void startObserveUpstreamNetworks() {
         stop();
 
-        final NetworkRequest listenAllRequest = new NetworkRequest.Builder()
-                .clearCapabilities().build();
-        mListenAllCallback = new UpstreamNetworkCallback(CALLBACK_LISTEN_ALL);
-        cm().registerNetworkCallback(listenAllRequest, mListenAllCallback, mHandler);
+        final NetworkRequest listenUpstreamRequest;
+        // Before V, only TV supports local agent on U, which doesn't support tethering.
+        if (SdkLevel.isAtLeastV()) {
+            listenUpstreamRequest = new NetworkRequest.Builder().clearCapabilities()
+                    .addForbiddenCapability(NET_CAPABILITY_LOCAL_NETWORK).build();
+        }  else {
+            listenUpstreamRequest = new NetworkRequest.Builder().clearCapabilities().build();
+        }
+        mListenUpstreamCallback = new UpstreamNetworkCallback(CALLBACK_LISTEN_UPSTREAM);
+        cm().registerNetworkCallback(listenUpstreamRequest, mListenUpstreamCallback, mHandler);
     }
 
     /**
@@ -183,8 +195,8 @@
     public void stop() {
         setTryCell(false);
 
-        releaseCallback(mListenAllCallback);
-        mListenAllCallback = null;
+        releaseCallback(mListenUpstreamCallback);
+        mListenUpstreamCallback = null;
 
         mNetworkMap.clear();
     }
@@ -535,10 +547,10 @@
                 return;
             }
 
-            // Any non-LISTEN_ALL callback will necessarily concern a network that will
-            // also match the LISTEN_ALL callback by construction of the LISTEN_ALL callback.
-            // So it's not useful to do this work for non-LISTEN_ALL callbacks.
-            if (mCallbackType == CALLBACK_LISTEN_ALL) {
+            // Any non-LISTEN_UPSTREAM callback will necessarily concern a network that will
+            // also match the LISTEN_UPSTREAM callback by construction of the LISTEN_UPSTREAM
+            // callback. So it's not useful to do this work for non-LISTEN_UPSTREAM callbacks.
+            if (mCallbackType == CALLBACK_LISTEN_UPSTREAM) {
                 recomputeLocalPrefixes();
             }
         }
@@ -555,10 +567,11 @@
             }
 
             handleLost(network);
-            // Any non-LISTEN_ALL callback will necessarily concern a network that will
-            // also match the LISTEN_ALL callback by construction of the LISTEN_ALL callback.
-            // So it's not useful to do this work for non-LISTEN_ALL callbacks.
-            if (mCallbackType == CALLBACK_LISTEN_ALL) {
+            // Any non-LISTEN_UPSTREAM callback will necessarily concern a network that will
+            // also match the LISTEN_UPSTREAM callback by construction of the
+            // LISTEN_UPSTREAM callback. So it's not useful to do this work for
+            // non-LISTEN_UPSTREAM callbacks.
+            if (mCallbackType == CALLBACK_LISTEN_UPSTREAM) {
                 recomputeLocalPrefixes();
             }
         }
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/EntitlementManagerTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/EntitlementManagerTest.java
index 51c2d56..16ebbbb 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/EntitlementManagerTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/EntitlementManagerTest.java
@@ -38,7 +38,6 @@
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
 import static com.android.networkstack.apishim.ConstantsShim.KEY_CARRIER_SUPPORTS_TETHERING_BOOL;
-import static com.android.testutils.DevSdkIgnoreRule.IgnoreAfter;
 import static com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
 import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
 
@@ -592,16 +591,8 @@
                 .onTetherProvisioningFailed(TETHERING_WIFI, FAILED_TETHERING_REASON);
     }
 
-    @IgnoreUpTo(SC_V2)
     @Test
-    public void testUiProvisioningMultiUser_aboveT() {
-        doTestUiProvisioningMultiUser(true, 1);
-        doTestUiProvisioningMultiUser(false, 0);
-    }
-
-    @IgnoreAfter(SC_V2)
-    @Test
-    public void testUiProvisioningMultiUser_belowT() {
+    public void testUiProvisioningMultiUser() {
         doTestUiProvisioningMultiUser(true, 1);
         doTestUiProvisioningMultiUser(false, 1);
     }
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/PrivateAddressCoordinatorTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/PrivateAddressCoordinatorTest.java
index 1608e1a..c329142 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/PrivateAddressCoordinatorTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/PrivateAddressCoordinatorTest.java
@@ -189,7 +189,6 @@
         final LinkAddress hotspotAddress = requestDownstreamAddress(mHotspotIpServer);
         final IpPrefix hotspotPrefix = asIpPrefix(hotspotAddress);
         assertNotEquals(asIpPrefix(mBluetoothAddress), hotspotPrefix);
-        releaseDownstream(mHotspotIpServer);
 
         // - Test previous enabled hotspot prefix(cached prefix) is reserved.
         when(mPrivateAddressCoordinator.getRandomInt()).thenReturn(
@@ -208,6 +207,7 @@
         assertNotEquals(asIpPrefix(mLegacyWifiP2pAddress), etherPrefix);
         assertNotEquals(asIpPrefix(mBluetoothAddress), etherPrefix);
         assertNotEquals(hotspotPrefix, etherPrefix);
+        releaseDownstream(mHotspotIpServer);
         releaseDownstream(mEthernetIpServer);
     }
 
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/TestConnectivityManager.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/TestConnectivityManager.java
index b2cbf75..51ba140 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TestConnectivityManager.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TestConnectivityManager.java
@@ -18,6 +18,7 @@
 
 import static android.net.NetworkCapabilities.NET_CAPABILITY_DUN;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK;
 import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
 
 import static com.android.networkstack.apishim.common.ShimUtils.isAtLeastS;
@@ -41,6 +42,8 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
+import com.android.modules.utils.build.SdkLevel;
+
 import java.util.Map;
 import java.util.Objects;
 
@@ -119,12 +122,15 @@
                 && mLegacyTypeMap.isEmpty();
     }
 
-    boolean isListeningForAll() {
-        final NetworkCapabilities empty = new NetworkCapabilities();
-        empty.clearAll();
+    boolean isListeningForUpstream() {
+        final NetworkCapabilities upstreamNc = new NetworkCapabilities();
+        upstreamNc.clearAll();
+        if (SdkLevel.isAtLeastV()) {
+            upstreamNc.addForbiddenCapability(NET_CAPABILITY_LOCAL_NETWORK);
+        }
 
         for (NetworkRequestInfo nri : mListening.values()) {
-            if (nri.request.networkCapabilities.equalRequestableCapabilities(empty)) {
+            if (nri.request.networkCapabilities.equalRequestableCapabilities(upstreamNc)) {
                 return true;
             }
         }
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
index e58a333..97758cf 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
@@ -1036,7 +1036,7 @@
         verify(mWifiManager).updateInterfaceIpState(TEST_WLAN_IFNAME, expectedState);
         verifyNoMoreInteractions(mWifiManager);
 
-        verify(mUpstreamNetworkMonitor).startObserveAllNetworks();
+        verify(mUpstreamNetworkMonitor).startObserveUpstreamNetworks();
         if (isLocalOnly) {
             // There are 2 IpServer state change events: STATE_AVAILABLE -> STATE_LOCAL_ONLY.
             verify(mNotificationUpdater, times(2)).onDownstreamChanged(DOWNSTREAM_NONE);
@@ -1264,7 +1264,7 @@
         // Start USB tethering with no current upstream.
         prepareUsbTethering();
         sendUsbBroadcast(true, true, TETHER_USB_RNDIS_FUNCTION);
-        inOrder.verify(mUpstreamNetworkMonitor).startObserveAllNetworks();
+        inOrder.verify(mUpstreamNetworkMonitor).startObserveUpstreamNetworks();
         inOrder.verify(mUpstreamNetworkMonitor).setTryCell(true);
 
         // Pretend cellular connected and expect the upstream to be set.
@@ -1863,7 +1863,7 @@
         // Start USB tethering with no current upstream.
         prepareUsbTethering();
         sendUsbBroadcast(true, true, TETHER_USB_RNDIS_FUNCTION);
-        inOrder.verify(mUpstreamNetworkMonitor).startObserveAllNetworks();
+        inOrder.verify(mUpstreamNetworkMonitor).startObserveUpstreamNetworks();
         inOrder.verify(mUpstreamNetworkMonitor).setTryCell(true);
         ArgumentCaptor<NetworkCallback> captor = ArgumentCaptor.forClass(NetworkCallback.class);
         inOrder.verify(mCm).requestNetwork(any(), eq(0), eq(TYPE_MOBILE_DUN), any(),
@@ -2591,7 +2591,7 @@
         verify(mNetd, times(1)).tetherStartWithConfiguration(any());
         verifyNoMoreInteractions(mNetd);
         verifyTetheringBroadcast(TEST_P2P_IFNAME, EXTRA_ACTIVE_LOCAL_ONLY);
-        verify(mUpstreamNetworkMonitor, times(1)).startObserveAllNetworks();
+        verify(mUpstreamNetworkMonitor, times(1)).startObserveUpstreamNetworks();
         // There are 2 IpServer state change events: STATE_AVAILABLE -> STATE_LOCAL_ONLY
         verify(mNotificationUpdater, times(2)).onDownstreamChanged(DOWNSTREAM_NONE);
 
@@ -3772,7 +3772,7 @@
         verifyInterfaceServingModeStarted(TEST_P2P_IFNAME);
         verifyTetheringBroadcast(TEST_P2P_IFNAME, EXTRA_AVAILABLE_TETHER);
         verifyTetheringBroadcast(TEST_P2P_IFNAME, EXTRA_ACTIVE_LOCAL_ONLY);
-        verify(mUpstreamNetworkMonitor).startObserveAllNetworks();
+        verify(mUpstreamNetworkMonitor).startObserveUpstreamNetworks();
         // Verify never enable upstream if only P2P active.
         verify(mUpstreamNetworkMonitor, never()).setTryCell(true);
         assertEquals(TETHER_ERROR_NO_ERROR, mTethering.getLastErrorForTest(TEST_P2P_IFNAME));
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/UpstreamNetworkMonitorTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/UpstreamNetworkMonitorTest.java
index 90fd709..f192492 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/UpstreamNetworkMonitorTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/UpstreamNetworkMonitorTest.java
@@ -36,7 +36,6 @@
 import static org.mockito.Mockito.anyInt;
 import static org.mockito.Mockito.anyString;
 import static org.mockito.Mockito.inOrder;
-import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
@@ -141,7 +140,7 @@
         assertTrue(mCM.hasNoCallbacks());
         mUNM.startTrackDefaultNetwork(mEntitleMgr);
 
-        mUNM.startObserveAllNetworks();
+        mUNM.startObserveUpstreamNetworks();
         assertEquals(1, mCM.mTrackingDefault.size());
 
         mUNM.stop();
@@ -149,13 +148,13 @@
     }
 
     @Test
-    public void testListensForAllNetworks() throws Exception {
+    public void testListensForUpstreamNetworks() throws Exception {
         assertTrue(mCM.mListening.isEmpty());
 
         mUNM.startTrackDefaultNetwork(mEntitleMgr);
-        mUNM.startObserveAllNetworks();
+        mUNM.startObserveUpstreamNetworks();
         assertFalse(mCM.mListening.isEmpty());
-        assertTrue(mCM.isListeningForAll());
+        assertTrue(mCM.isListeningForUpstream());
 
         mUNM.stop();
         assertTrue(mCM.onlyHasDefaultCallbacks());
@@ -179,7 +178,7 @@
             assertTrue(TestConnectivityManager.looksLikeDefaultRequest(requestCaptor.getValue()));
         }
 
-        mUNM.startObserveAllNetworks();
+        mUNM.startObserveUpstreamNetworks();
         verify(mCM, times(1)).registerNetworkCallback(
                 any(NetworkRequest.class), any(NetworkCallback.class), any(Handler.class));
 
@@ -192,7 +191,7 @@
         assertFalse(mUNM.mobileNetworkRequested());
         assertEquals(0, mCM.mRequested.size());
 
-        mUNM.startObserveAllNetworks();
+        mUNM.startObserveUpstreamNetworks();
         assertFalse(mUNM.mobileNetworkRequested());
         assertEquals(0, mCM.mRequested.size());
 
@@ -215,7 +214,7 @@
         assertFalse(mUNM.mobileNetworkRequested());
         assertEquals(0, mCM.mRequested.size());
 
-        mUNM.startObserveAllNetworks();
+        mUNM.startObserveUpstreamNetworks();
         verify(mCM, times(1)).registerNetworkCallback(
                 any(NetworkRequest.class), any(NetworkCallback.class), any(Handler.class));
         assertFalse(mUNM.mobileNetworkRequested());
@@ -251,7 +250,7 @@
         assertFalse(mUNM.mobileNetworkRequested());
         assertEquals(0, mCM.mRequested.size());
 
-        mUNM.startObserveAllNetworks();
+        mUNM.startObserveUpstreamNetworks();
         assertFalse(mUNM.mobileNetworkRequested());
         assertEquals(0, mCM.mRequested.size());
 
@@ -271,7 +270,7 @@
 
     @Test
     public void testUpdateMobileRequiresDun() throws Exception {
-        mUNM.startObserveAllNetworks();
+        mUNM.startObserveUpstreamNetworks();
 
         // Test going from no-DUN to DUN correctly re-registers callbacks.
         mUNM.setUpstreamConfig(false /* autoUpstream */, false /* dunRequired */);
@@ -301,7 +300,7 @@
         preferredTypes.add(TYPE_WIFI);
 
         mUNM.startTrackDefaultNetwork(mEntitleMgr);
-        mUNM.startObserveAllNetworks();
+        mUNM.startObserveUpstreamNetworks();
         // There are no networks, so there is nothing to select.
         assertSatisfiesLegacyType(TYPE_NONE, mUNM.selectPreferredUpstreamType(preferredTypes));
 
@@ -374,7 +373,7 @@
     @Test
     public void testGetCurrentPreferredUpstream() throws Exception {
         mUNM.startTrackDefaultNetwork(mEntitleMgr);
-        mUNM.startObserveAllNetworks();
+        mUNM.startObserveUpstreamNetworks();
         mUNM.setUpstreamConfig(true /* autoUpstream */, false /* dunRequired */);
         mUNM.setTryCell(true);
 
@@ -446,7 +445,7 @@
     @Test
     public void testLocalPrefixes() throws Exception {
         mUNM.startTrackDefaultNetwork(mEntitleMgr);
-        mUNM.startObserveAllNetworks();
+        mUNM.startObserveUpstreamNetworks();
 
         // [0] Test minimum set of local prefixes.
         Set<IpPrefix> local = mUNM.getLocalPrefixes();
@@ -558,7 +557,7 @@
         preferredTypes.add(TYPE_MOBILE_HIPRI);
         preferredTypes.add(TYPE_WIFI);
         mUNM.startTrackDefaultNetwork(mEntitleMgr);
-        mUNM.startObserveAllNetworks();
+        mUNM.startObserveUpstreamNetworks();
         // Setup wifi and make wifi as default network.
         final TestNetworkAgent wifiAgent = new TestNetworkAgent(mCM, WIFI_CAPABILITIES);
         wifiAgent.fakeConnect();
@@ -579,7 +578,7 @@
         final String ipv6Addr1 = "2001:db8:4:fd00:827a:bfff:fe6f:374d/64";
         final String ipv6Addr2 = "2003:aa8:3::123/64";
         mUNM.startTrackDefaultNetwork(mEntitleMgr);
-        mUNM.startObserveAllNetworks();
+        mUNM.startObserveUpstreamNetworks();
         mUNM.setUpstreamConfig(true /* autoUpstream */, false /* dunRequired */);
         mUNM.setTryCell(true);
 
diff --git a/bpf/loader/NetBpfLoad.cpp b/bpf/loader/NetBpfLoad.cpp
index 4834b09..ce144a7 100644
--- a/bpf/loader/NetBpfLoad.cpp
+++ b/bpf/loader/NetBpfLoad.cpp
@@ -1514,6 +1514,7 @@
         REQUIRE(5, 15, 136)
         REQUIRE(6, 1, 57)
         REQUIRE(6, 6, 0)
+        REQUIRE(6, 12, 0)
 
 #undef REQUIRE
 
@@ -1658,17 +1659,17 @@
     }
 
     // unreachable before U QPR3
-    {
+    if (exists(uprobestatsBpfLoader)) {
       ALOGI("done, transferring control to uprobestatsbpfload.");
       const char *args[] = {
           uprobestatsBpfLoader,
           NULL,
       };
       execve(args[0], (char **)args, envp);
+      ALOGI("unable to execute uprobestatsbpfload, transferring control to "
+            "platform bpfloader.");
     }
 
-    ALOGI("unable to execute uprobestatsbpfload, transferring control to "
-          "platform bpfloader.");
     // platform BpfLoader *needs* to run as root
     const char * args[] = { platformBpfLoader, NULL, };
     execve(args[0], (char**)args, envp);
diff --git a/common/flags.aconfig b/common/flags.aconfig
index 17ef94b..60a827b 100644
--- a/common/flags.aconfig
+++ b/common/flags.aconfig
@@ -156,3 +156,12 @@
   bug: "354619988"
   is_fixed_read_only: true
 }
+
+flag {
+  name: "ipv6_over_ble"
+  is_exported: true
+  namespace: "android_core_networking"
+  description: "API flag for IPv6 over BLE"
+  bug: "372936361"
+  is_fixed_read_only: true
+}
diff --git a/framework-t/Android.bp b/framework-t/Android.bp
index 26fc145..9d6d356 100644
--- a/framework-t/Android.bp
+++ b/framework-t/Android.bp
@@ -102,6 +102,7 @@
 java_library {
     name: "framework-connectivity-t-pre-jarjar",
     defaults: ["framework-connectivity-t-defaults"],
+    installable: false,
     libs: [
         "framework-bluetooth.stubs.module_lib",
         "framework-wifi.stubs.module_lib",
diff --git a/framework-t/src/android/net/TrafficStats.java b/framework-t/src/android/net/TrafficStats.java
index 81f2cf9..868033a 100644
--- a/framework-t/src/android/net/TrafficStats.java
+++ b/framework-t/src/android/net/TrafficStats.java
@@ -17,6 +17,7 @@
 package android.net;
 
 import static android.annotation.SystemApi.Client.MODULE_LIBRARIES;
+import static android.net.NetworkStats.UID_ALL;
 
 import static com.android.internal.annotations.VisibleForTesting.Visibility.PRIVATE;
 
@@ -33,21 +34,25 @@
 import android.content.Context;
 import android.media.MediaPlayer;
 import android.net.netstats.StatsResult;
+import android.net.netstats.TrafficStatsRateLimitCacheConfig;
 import android.os.Binder;
 import android.os.Build;
 import android.os.RemoteException;
 import android.os.StrictMode;
+import android.os.SystemClock;
 import android.util.Log;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.net.module.util.BinderUtils;
+import com.android.net.module.util.LruCacheWithExpiry;
 
 import java.io.FileDescriptor;
 import java.io.IOException;
 import java.net.DatagramSocket;
 import java.net.Socket;
 import java.net.SocketException;
-
+import java.util.function.LongSupplier;
 
 /**
  * Class that provides network traffic statistics. These statistics include
@@ -182,13 +187,48 @@
     /** @hide */
     public static final int TAG_SYSTEM_PROBE = 0xFFFFFF42;
 
+    private static final StatsResult EMPTY_STATS = new StatsResult(0L, 0L, 0L, 0L);
+
+    private static final Object sRateLimitCacheLock = new Object();
+
     @GuardedBy("TrafficStats.class")
+    @Nullable
     private static INetworkStatsService sStatsService;
 
     // The variable will only be accessed in the test, which is effectively
     // single-threaded.
+    @Nullable
     private static INetworkStatsService sStatsServiceForTest = null;
 
+    // This holds the configuration for the TrafficStats rate limit caches.
+    // It will be filled with the result of a query to the service the first time
+    // the caller invokes get*Stats APIs.
+    // This variable can be accessed from any thread with the lock held.
+    @GuardedBy("sRateLimitCacheLock")
+    @Nullable
+    private static TrafficStatsRateLimitCacheConfig sRateLimitCacheConfig;
+
+    // Cache for getIfaceStats and getTotalStats binder interfaces.
+    // This variable can be accessed from any thread with the lock held,
+    // while the cache itself is thread-safe and can be accessed outside
+    // the lock.
+    @GuardedBy("sRateLimitCacheLock")
+    @Nullable
+    private static LruCacheWithExpiry<String, StatsResult> sRateLimitIfaceCache;
+
+    // Cache for getUidStats binder interface.
+    // This variable can be accessed from any thread with the lock held,
+    // while the cache itself is thread-safe and can be accessed outside
+    // the lock.
+    @GuardedBy("sRateLimitCacheLock")
+    @Nullable
+    private static LruCacheWithExpiry<Integer, StatsResult> sRateLimitUidCache;
+
+    // The variable will only be accessed in the test, which is effectively
+    // single-threaded.
+    @Nullable
+    private static LongSupplier sTimeSupplierForTest = null;
+
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 130143562)
     private synchronized static INetworkStatsService getStatsService() {
         if (sStatsServiceForTest != null) return sStatsServiceForTest;
@@ -215,6 +255,28 @@
     }
 
     /**
+     * Set time supplier for test, or null to reset.
+     *
+     * @hide
+     */
+    @VisibleForTesting(visibility = PRIVATE)
+    public static void setTimeSupplierForTest(LongSupplier timeSupplier) {
+        sTimeSupplierForTest = timeSupplier;
+    }
+
+    /**
+     * Trigger query rate-limit cache config and initializing the caches.
+     *
+     * This is for test purpose.
+     *
+     * @hide
+     */
+    @VisibleForTesting(visibility = PRIVATE)
+    public static void reinitRateLimitCacheForTest() {
+        maybeGetConfigAndInitRateLimitCache(true /* forceReinit */);
+    }
+
+    /**
      * Snapshot of {@link NetworkStats} when the currently active profiling
      * session started, or {@code null} if no session active.
      *
@@ -254,6 +316,92 @@
         sStatsService = statsManager.getBinder();
     }
 
+    @Nullable
+    private static LruCacheWithExpiry<String, StatsResult> maybeGetRateLimitIfaceCache() {
+        if (!maybeGetConfigAndInitRateLimitCache(false /* forceReinit */)) return null;
+        synchronized (sRateLimitCacheLock) {
+            return sRateLimitIfaceCache;
+        }
+    }
+
+    @Nullable
+    private static LruCacheWithExpiry<Integer, StatsResult> maybeGetRateLimitUidCache() {
+        if (!maybeGetConfigAndInitRateLimitCache(false /* forceReinit */)) return null;
+        synchronized (sRateLimitCacheLock) {
+            return sRateLimitUidCache;
+        }
+    }
+
+    /**
+     * Gets the rate limit cache configuration and init caches if null.
+     *
+     * Gets the configuration from the service as the configuration
+     * is not expected to change dynamically. And use it to initialize
+     * rate-limit cache if not yet initialized.
+     *
+     * @return whether the rate-limit cache is enabled.
+     *
+     * @hide
+     */
+    private static boolean maybeGetConfigAndInitRateLimitCache(boolean forceReinit) {
+        // Access the service outside the lock to avoid potential deadlocks. This is
+        // especially important when the caller is a system component (e.g.,
+        // NetworkPolicyManagerService) that might hold other locks that the service
+        // also needs.
+        // Although this introduces a race condition where multiple threads might
+        // query the service concurrently, it's acceptable in this case because the
+        // configuration doesn't change dynamically. The configuration only needs to
+        // be fetched once before initializing the cache.
+        synchronized (sRateLimitCacheLock) {
+            if (sRateLimitCacheConfig != null && !forceReinit) {
+                return sRateLimitCacheConfig.isCacheEnabled;
+            }
+        }
+
+        final TrafficStatsRateLimitCacheConfig config;
+        try {
+            config = getStatsService().getRateLimitCacheConfig();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+
+        synchronized (sRateLimitCacheLock) {
+            if (sRateLimitCacheConfig == null || forceReinit) {
+                sRateLimitCacheConfig = config;
+                initRateLimitCacheLocked();
+            }
+        }
+        return config.isCacheEnabled;
+    }
+
+    @GuardedBy("sRateLimitCacheLock")
+    private static void initRateLimitCacheLocked() {
+        // Set up rate limiting caches.
+        // Use uid cache with UID_ALL to cache total stats.
+        if (sRateLimitCacheConfig.isCacheEnabled) {
+            // A time supplier which is monotonic until device reboots, and counts
+            // time spent in sleep. This is needed to ensure the get*Stats caller
+            // won't get stale value after system time adjustment or waking up from sleep.
+            final LongSupplier realtimeSupplier = (sTimeSupplierForTest != null
+                    ? sTimeSupplierForTest : () -> SystemClock.elapsedRealtime());
+            sRateLimitIfaceCache = new LruCacheWithExpiry<String, StatsResult>(
+                    realtimeSupplier,
+                    sRateLimitCacheConfig.expiryDurationMs,
+                    sRateLimitCacheConfig.maxEntries,
+                    (statsResult) -> !isEmpty(statsResult)
+            );
+            sRateLimitUidCache = new LruCacheWithExpiry<Integer, StatsResult>(
+                    realtimeSupplier,
+                    sRateLimitCacheConfig.expiryDurationMs,
+                    sRateLimitCacheConfig.maxEntries,
+                    (statsResult) -> !isEmpty(statsResult)
+            );
+        } else {
+            sRateLimitIfaceCache = null;
+            sRateLimitUidCache = null;
+        }
+    }
+
     /**
      * Attach the socket tagger implementation to the current process, to
      * get notified when a socket's {@link FileDescriptor} is assigned to
@@ -736,6 +884,14 @@
             android.Manifest.permission.NETWORK_STACK,
             android.Manifest.permission.NETWORK_SETTINGS})
     public static void clearRateLimitCaches() {
+        final LruCacheWithExpiry<String, StatsResult> ifaceCache = maybeGetRateLimitIfaceCache();
+        if (ifaceCache != null) {
+            ifaceCache.clear();
+        }
+        final LruCacheWithExpiry<Integer, StatsResult> uidCache = maybeGetRateLimitUidCache();
+        if (uidCache != null) {
+            uidCache.clear();
+        }
         try {
             getStatsService().clearTrafficStatsRateLimitCaches();
         } catch (RemoteException e) {
@@ -985,35 +1141,76 @@
 
     /** @hide */
     public static long getUidStats(int uid, int type) {
-        final StatsResult stats;
+        return fetchStats(maybeGetRateLimitUidCache(), uid,
+                () -> getStatsService().getUidStats(uid), type);
+    }
+
+    // Note: This method calls to the service, do not invoke this method with lock held.
+    private static <K> long fetchStats(
+            @Nullable LruCacheWithExpiry<K, StatsResult> cache, K key,
+            BinderUtils.ThrowingSupplier<StatsResult, RemoteException> statsFetcher, int type) {
         try {
-            stats = getStatsService().getUidStats(uid);
+            final StatsResult stats;
+            if (cache != null) {
+                stats = fetchStatsWithCache(cache, key, statsFetcher);
+            } else {
+                // Cache is not enabled, fetch directly from service.
+                stats = statsFetcher.get();
+            }
+            return getEntryValueForType(stats, type);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
-        return getEntryValueForType(stats, type);
+    }
+
+    // Note: This method calls to the service, do not invoke this method with lock held.
+    @Nullable
+    private static <K> StatsResult fetchStatsWithCache(LruCacheWithExpiry<K, StatsResult> cache,
+            K key, BinderUtils.ThrowingSupplier<StatsResult, RemoteException> statsFetcher)
+            throws RemoteException {
+        // Attempt to retrieve from the cache first.
+        StatsResult stats = cache.get(key);
+
+        // Although the cache instance is thread-safe, this can still introduce a
+        // race condition between threads of the same process, potentially
+        // returning non-monotonic results. This is because there is no lock
+        // between get, fetch, and put operations. This is considered acceptable
+        // because varying thread execution speeds can also cause non-monotonic
+        // results, even with locking.
+        if (stats == null) {
+            // Cache miss, fetch from the service.
+            stats = statsFetcher.get();
+
+            // Update the cache with the fetched result if valid.
+            if (stats != null && !isEmpty(stats)) {
+                final StatsResult cachedValue = cache.putIfAbsent(key, stats);
+                if (cachedValue != null) {
+                    // Some other thread cached a value after this thread
+                    // originally got a cache miss. Return the cached value
+                    // to ensure all returned values after caching are consistent.
+                    return cachedValue;
+                }
+            }
+        }
+        return stats;
+    }
+
+    private static boolean isEmpty(StatsResult stats) {
+        return stats.equals(EMPTY_STATS);
     }
 
     /** @hide */
     public static long getTotalStats(int type) {
-        final StatsResult stats;
-        try {
-            stats = getStatsService().getTotalStats();
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        }
-        return getEntryValueForType(stats, type);
+        // In practice, Bpf doesn't use UID_ALL for storing per-UID stats.
+        // Use uid cache with UID_ALL to cache total stats.
+        return fetchStats(maybeGetRateLimitUidCache(), UID_ALL,
+                () -> getStatsService().getTotalStats(), type);
     }
 
     /** @hide */
     public static long getIfaceStats(String iface, int type) {
-        final StatsResult stats;
-        try {
-            stats = getStatsService().getIfaceStats(iface);
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        }
-        return getEntryValueForType(stats, type);
+        return fetchStats(maybeGetRateLimitIfaceCache(), iface,
+                () -> getStatsService().getIfaceStats(iface), type);
     }
 
     /**
diff --git a/framework/Android.bp b/framework/Android.bp
index a93a532..a1c6a15 100644
--- a/framework/Android.bp
+++ b/framework/Android.bp
@@ -75,6 +75,7 @@
             // the module builds against API (the parcelable declarations exist in framework.aidl)
             "frameworks/base/core/java", // For framework parcelables
             "frameworks/native/aidl/binder", // For PersistableBundle.aidl
+            "packages/modules/Connectivity/Tethering/common/TetheringLib/src",
         ],
     },
     stub_only_libs: [
@@ -143,6 +144,7 @@
 java_library {
     name: "framework-connectivity-pre-jarjar",
     defaults: ["framework-module-defaults"],
+    installable: false,
     min_sdk_version: "30",
     static_libs: [
         "framework-connectivity-pre-jarjar-without-cronet",
@@ -334,6 +336,7 @@
     aidl: {
         include_dirs: [
             "packages/modules/Connectivity/framework/aidl-export",
+            "packages/modules/Connectivity/Tethering/common/TetheringLib/src",
             "frameworks/native/aidl/binder", // For PersistableBundle.aidl
         ],
     },
diff --git a/framework/src/android/net/CaptivePortal.java b/framework/src/android/net/CaptivePortal.java
index 4a7b601..4c534f3 100644
--- a/framework/src/android/net/CaptivePortal.java
+++ b/framework/src/android/net/CaptivePortal.java
@@ -18,10 +18,19 @@
 import android.annotation.NonNull;
 import android.annotation.RequiresPermission;
 import android.annotation.SystemApi;
+import android.annotation.TargetApi;
+import android.os.Binder;
+import android.os.Build;
 import android.os.IBinder;
+import android.os.OutcomeReceiver;
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.os.RemoteException;
+import android.os.ServiceSpecificException;
+import android.system.OsConstants;
+
+import java.util.Objects;
+import java.util.concurrent.Executor;
 
 /**
  * A class allowing apps handling the {@link ConnectivityManager#ACTION_CAPTIVE_PORTAL_SIGN_IN}
@@ -69,6 +78,15 @@
     @SystemApi
     public static final int APP_REQUEST_REEVALUATION_REQUIRED = APP_REQUEST_BASE + 0;
 
+    /**
+     * Binder object used for tracking the lifetime of the process, so CS can perform cleanup if
+     * the CaptivePortal app dies. This binder is not parcelled as part of this object. It is
+     * created in the client process and sent to the server by setDelegateUid so that the server
+     * can use it to register a death recipient.
+     *
+     */
+    private final Binder mLifetimeBinder = new Binder();
+
     private final IBinder mBinder;
 
     /** @hide */
@@ -167,4 +185,56 @@
     @SystemApi
     public void logEvent(int eventId, @NonNull String packageName) {
     }
+
+    /**
+     * Sets the UID of the app that is allowed to perform network traffic for captive
+     * portal login.
+     *
+     * This app will be allowed to communicate directly on the captive
+     * portal by binding to the {@link android.net.Network} extra passed in the
+     * ACTION_CAPTIVE_PORTAL_SIGN_IN broadcast that contained this object.
+     *
+     * Communication will bypass network access restrictions such as VPNs and
+     * Private DNS settings, so the delegated UID must be trusted to ensure that only
+     * traffic intended for captive portal login binds to that network.
+     *
+     * By default, no UID is delegated. The delegation can be cleared by calling
+     * this method again with {@link android.os.Process.INVALID_UID}. Only one UID can
+     * be delegated at any given time.
+     *
+     * The operation is asynchronous. The uid is only guaranteed to have access when
+     * the provided OutcomeReceiver is called.
+     *
+     * @hide
+     */
+    @RequiresPermission(NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK)
+    // OutcomeReceiver is not available on R, but the mainline version of this
+    // class is only available on S+.
+    @TargetApi(Build.VERSION_CODES.S)
+    public void setDelegateUid(int uid, @NonNull Executor executor,
+            @NonNull final OutcomeReceiver<Void, ServiceSpecificException> receiver) {
+        Objects.requireNonNull(executor);
+        Objects.requireNonNull(receiver);
+        try {
+            ICaptivePortal.Stub.asInterface(mBinder).setDelegateUid(
+                    uid,
+                    mLifetimeBinder,
+                    new IIntResultListener.Stub() {
+                        @Override
+                        public void onResult(int resultCode) {
+                            if (resultCode != 0) {
+                                final String msg = "Fail to set the delegate UID " + uid
+                                        + ", error: " + OsConstants.errnoName(resultCode);
+                                executor.execute(() -> {
+                                    receiver.onError(new ServiceSpecificException(resultCode, msg));
+                                });
+                            } else {
+                                executor.execute(() -> receiver.onResult(null));
+                            }
+                        }
+                    });
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
 }
diff --git a/framework/src/android/net/ConnectivityManager.java b/framework/src/android/net/ConnectivityManager.java
index 1ebc4a3..009344d 100644
--- a/framework/src/android/net/ConnectivityManager.java
+++ b/framework/src/android/net/ConnectivityManager.java
@@ -1873,7 +1873,7 @@
     public NetworkCapabilities[] getDefaultNetworkCapabilitiesForUser(int userId) {
         try {
             return mService.getDefaultNetworkCapabilitiesForUser(
-                    userId, mContext.getOpPackageName(), getAttributionTag());
+                    userId, mContext.getOpPackageName(), mContext.getAttributionTag());
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -1967,7 +1967,7 @@
             @NonNull String packageName) {
         try {
             return mService.getRedactedLinkPropertiesForPackage(
-                    lp, uid, packageName, getAttributionTag());
+                    lp, uid, packageName, mContext.getAttributionTag());
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -1993,7 +1993,7 @@
     public NetworkCapabilities getNetworkCapabilities(@Nullable Network network) {
         try {
             return mService.getNetworkCapabilities(
-                    network, mContext.getOpPackageName(), getAttributionTag());
+                    network, mContext.getOpPackageName(), mContext.getAttributionTag());
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -2027,7 +2027,7 @@
             int uid, @NonNull String packageName) {
         try {
             return mService.getRedactedNetworkCapabilitiesForPackage(nc, uid, packageName,
-                    getAttributionTag());
+                    mContext.getAttributionTag());
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -2752,21 +2752,13 @@
         checkLegacyRoutingApiAccess();
         try {
             return mService.requestRouteToHostAddress(networkType, hostAddress.getAddress(),
-                    mContext.getOpPackageName(), getAttributionTag());
+                    mContext.getOpPackageName(), mContext.getAttributionTag());
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
     }
 
     /**
-     * @return the context's attribution tag
-     */
-    // TODO: Remove method and replace with direct call once R code is pushed to AOSP
-    private @Nullable String getAttributionTag() {
-        return mContext.getAttributionTag();
-    }
-
-    /**
      * Returns the value of the setting for background data usage. If false,
      * applications should not use the network if the application is not in the
      * foreground. Developers should respect this setting, and check the value
@@ -4705,12 +4697,12 @@
                 if (reqType == LISTEN) {
                     request = mService.listenForNetwork(
                             need, messenger, binder, callbackFlags, callingPackageName,
-                            getAttributionTag(), declaredMethodsFlag);
+                            mContext.getAttributionTag(), declaredMethodsFlag);
                 } else {
                     request = mService.requestNetwork(
                             asUid, need, reqType.ordinal(), messenger, timeoutMs, binder,
-                            legacyType, callbackFlags, callingPackageName, getAttributionTag(),
-                            declaredMethodsFlag);
+                            legacyType, callbackFlags, callingPackageName,
+                            mContext.getAttributionTag(), declaredMethodsFlag);
                 }
                 if (request != null) {
                     sCallbacks.put(request, callback);
@@ -5127,7 +5119,7 @@
         try {
             mService.pendingRequestForNetwork(
                     request.networkCapabilities, operation, mContext.getOpPackageName(),
-                    getAttributionTag());
+                    mContext.getAttributionTag());
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         } catch (ServiceSpecificException e) {
@@ -5276,7 +5268,7 @@
         try {
             mService.pendingListenForNetwork(
                     request.networkCapabilities, operation, mContext.getOpPackageName(),
-                    getAttributionTag());
+                    mContext.getAttributionTag());
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         } catch (ServiceSpecificException e) {
diff --git a/framework/src/android/net/ICaptivePortal.aidl b/framework/src/android/net/ICaptivePortal.aidl
index e35f8d4..5cbb428 100644
--- a/framework/src/android/net/ICaptivePortal.aidl
+++ b/framework/src/android/net/ICaptivePortal.aidl
@@ -16,6 +16,9 @@
 
 package android.net;
 
+import android.net.IIntResultListener;
+import android.os.IBinder;
+
 /**
  * Interface to inform NetworkMonitor of decisions of app handling captive portal.
  * @hide
@@ -23,4 +26,5 @@
 oneway interface ICaptivePortal {
     void appRequest(int request);
     void appResponse(int response);
+    void setDelegateUid(int uid, IBinder binder, IIntResultListener listener);
 }
diff --git a/framework/src/android/net/NetworkAgentConfig.java b/framework/src/android/net/NetworkAgentConfig.java
index da12a0a..deaa734 100644
--- a/framework/src/android/net/NetworkAgentConfig.java
+++ b/framework/src/android/net/NetworkAgentConfig.java
@@ -272,6 +272,27 @@
         return mVpnRequiresValidation;
     }
 
+    /**
+     * Whether the native network creation should be skipped.
+     *
+     * If set, the native network and routes should be maintained by the caller.
+     *
+     * @hide
+     */
+    private boolean mSkipNativeNetworkCreation = false;
+
+
+    /**
+     * @return Whether the native network creation should be skipped.
+     * @hide
+     */
+    // TODO: Expose API when ready.
+    // @FlaggedApi(Flags.FLAG_TETHERING_NETWORK_AGENT)
+    // @SystemApi(client = MODULE_LIBRARIES) when ready.
+    public boolean shouldSkipNativeNetworkCreation() {
+        return mSkipNativeNetworkCreation;
+    }
+
     /** @hide */
     public NetworkAgentConfig() {
     }
@@ -293,6 +314,7 @@
             mLegacyExtraInfo = nac.mLegacyExtraInfo;
             excludeLocalRouteVpn = nac.excludeLocalRouteVpn;
             mVpnRequiresValidation = nac.mVpnRequiresValidation;
+            mSkipNativeNetworkCreation = nac.mSkipNativeNetworkCreation;
         }
     }
 
@@ -484,6 +506,26 @@
         }
 
         /**
+         * Sets the native network creation should be skipped.
+         *
+         * @return this builder, to facilitate chaining.
+         * @hide
+         */
+        @NonNull
+        // TODO: Expose API when ready.
+        // @FlaggedApi(Flags.FLAG_TETHERING_NETWORK_AGENT)
+        // @SystemApi(client = MODULE_LIBRARIES) when ready.
+        public Builder setSkipNativeNetworkCreation(boolean skipNativeNetworkCreation) {
+            if (!SdkLevel.isAtLeastV()) {
+                // Local agents are supported starting on U on TVs and on V on everything else.
+                // Thus, only support this flag on V+.
+                throw new UnsupportedOperationException("Method is not supported");
+            }
+            mConfig.mSkipNativeNetworkCreation = skipNativeNetworkCreation;
+            return this;
+        }
+
+        /**
          * Returns the constructed {@link NetworkAgentConfig} object.
          */
         @NonNull
@@ -510,7 +552,8 @@
                 && Objects.equals(legacySubTypeName, that.legacySubTypeName)
                 && Objects.equals(mLegacyExtraInfo, that.mLegacyExtraInfo)
                 && excludeLocalRouteVpn == that.excludeLocalRouteVpn
-                && mVpnRequiresValidation == that.mVpnRequiresValidation;
+                && mVpnRequiresValidation == that.mVpnRequiresValidation
+                && mSkipNativeNetworkCreation == that.mSkipNativeNetworkCreation;
     }
 
     @Override
@@ -518,7 +561,8 @@
         return Objects.hash(allowBypass, explicitlySelected, acceptUnvalidated,
                 acceptPartialConnectivity, provisioningNotificationDisabled, subscriberId,
                 skip464xlat, legacyType, legacySubType, legacyTypeName, legacySubTypeName,
-                mLegacyExtraInfo, excludeLocalRouteVpn, mVpnRequiresValidation);
+                mLegacyExtraInfo, excludeLocalRouteVpn, mVpnRequiresValidation,
+                mSkipNativeNetworkCreation);
     }
 
     @Override
@@ -539,6 +583,7 @@
                 + ", legacyExtraInfo = '" + mLegacyExtraInfo + '\''
                 + ", excludeLocalRouteVpn = '" + excludeLocalRouteVpn + '\''
                 + ", vpnRequiresValidation = '" + mVpnRequiresValidation + '\''
+                + ", skipNativeNetworkCreation = '" + mSkipNativeNetworkCreation + '\''
                 + "}";
     }
 
@@ -563,33 +608,35 @@
         out.writeString(mLegacyExtraInfo);
         out.writeInt(excludeLocalRouteVpn ? 1 : 0);
         out.writeInt(mVpnRequiresValidation ? 1 : 0);
+        out.writeInt(mSkipNativeNetworkCreation ? 1 : 0);
     }
 
     public static final @NonNull Creator<NetworkAgentConfig> CREATOR =
             new Creator<NetworkAgentConfig>() {
-        @Override
-        public NetworkAgentConfig createFromParcel(Parcel in) {
-            NetworkAgentConfig networkAgentConfig = new NetworkAgentConfig();
-            networkAgentConfig.allowBypass = in.readInt() != 0;
-            networkAgentConfig.explicitlySelected = in.readInt() != 0;
-            networkAgentConfig.acceptUnvalidated = in.readInt() != 0;
-            networkAgentConfig.acceptPartialConnectivity = in.readInt() != 0;
-            networkAgentConfig.subscriberId = in.readString();
-            networkAgentConfig.provisioningNotificationDisabled = in.readInt() != 0;
-            networkAgentConfig.skip464xlat = in.readInt() != 0;
-            networkAgentConfig.legacyType = in.readInt();
-            networkAgentConfig.legacyTypeName = in.readString();
-            networkAgentConfig.legacySubType = in.readInt();
-            networkAgentConfig.legacySubTypeName = in.readString();
-            networkAgentConfig.mLegacyExtraInfo = in.readString();
-            networkAgentConfig.excludeLocalRouteVpn = in.readInt() != 0;
-            networkAgentConfig.mVpnRequiresValidation = in.readInt() != 0;
-            return networkAgentConfig;
-        }
+                @Override
+                public NetworkAgentConfig createFromParcel(Parcel in) {
+                    NetworkAgentConfig networkAgentConfig = new NetworkAgentConfig();
+                    networkAgentConfig.allowBypass = in.readInt() != 0;
+                    networkAgentConfig.explicitlySelected = in.readInt() != 0;
+                    networkAgentConfig.acceptUnvalidated = in.readInt() != 0;
+                    networkAgentConfig.acceptPartialConnectivity = in.readInt() != 0;
+                    networkAgentConfig.subscriberId = in.readString();
+                    networkAgentConfig.provisioningNotificationDisabled = in.readInt() != 0;
+                    networkAgentConfig.skip464xlat = in.readInt() != 0;
+                    networkAgentConfig.legacyType = in.readInt();
+                    networkAgentConfig.legacyTypeName = in.readString();
+                    networkAgentConfig.legacySubType = in.readInt();
+                    networkAgentConfig.legacySubTypeName = in.readString();
+                    networkAgentConfig.mLegacyExtraInfo = in.readString();
+                    networkAgentConfig.excludeLocalRouteVpn = in.readInt() != 0;
+                    networkAgentConfig.mVpnRequiresValidation = in.readInt() != 0;
+                    networkAgentConfig.mSkipNativeNetworkCreation = in.readInt() != 0;
+                    return networkAgentConfig;
+                }
 
-        @Override
-        public NetworkAgentConfig[] newArray(int size) {
-            return new NetworkAgentConfig[size];
-        }
-    };
+                @Override
+                public NetworkAgentConfig[] newArray(int size) {
+                    return new NetworkAgentConfig[size];
+                }
+            };
 }
diff --git a/framework/src/android/net/NetworkCapabilities.java b/framework/src/android/net/NetworkCapabilities.java
index 4a50397..c6b62ee 100644
--- a/framework/src/android/net/NetworkCapabilities.java
+++ b/framework/src/android/net/NetworkCapabilities.java
@@ -359,6 +359,7 @@
         mSubIds = new ArraySet<>();
         mUnderlyingNetworks = null;
         mEnterpriseId = 0;
+        mReservationId = RES_ID_UNSET;
     }
 
     /**
@@ -393,6 +394,7 @@
         // necessary.
         mUnderlyingNetworks = nc.mUnderlyingNetworks;
         mEnterpriseId = nc.mEnterpriseId;
+        mReservationId = nc.mReservationId;
     }
 
     /**
@@ -2233,7 +2235,8 @@
                 && (onlyImmutable || satisfiedByUids(nc))
                 && (onlyImmutable || satisfiedBySSID(nc))
                 && (onlyImmutable || satisfiedByRequestor(nc))
-                && (onlyImmutable || satisfiedBySubscriptionIds(nc)));
+                && (onlyImmutable || satisfiedBySubscriptionIds(nc)))
+                && satisfiedByReservationId(nc);
     }
 
     /**
@@ -2347,7 +2350,8 @@
                 && equalsAdministratorUids(that)
                 && equalsSubscriptionIds(that)
                 && equalsUnderlyingNetworks(that)
-                && equalsEnterpriseCapabilitiesId(that);
+                && equalsEnterpriseCapabilitiesId(that)
+                && equalsReservationId(that);
     }
 
     @Override
@@ -2373,7 +2377,9 @@
                 + Arrays.hashCode(mAdministratorUids) * 67
                 + Objects.hashCode(mSubIds) * 71
                 + Objects.hashCode(mUnderlyingNetworks) * 73
-                + mEnterpriseId * 79;
+                + mEnterpriseId * 79
+                + mReservationId * 83;
+
     }
 
     @Override
@@ -2411,6 +2417,7 @@
         dest.writeIntArray(CollectionUtils.toIntArray(mSubIds));
         dest.writeTypedList(mUnderlyingNetworks);
         dest.writeInt(mEnterpriseId & ALL_VALID_ENTERPRISE_IDS);
+        dest.writeInt(mReservationId);
     }
 
     public static final @android.annotation.NonNull Creator<NetworkCapabilities> CREATOR =
@@ -2446,6 +2453,7 @@
                 }
                 netCap.setUnderlyingNetworks(in.createTypedArrayList(Network.CREATOR));
                 netCap.mEnterpriseId = in.readInt() & ALL_VALID_ENTERPRISE_IDS;
+                netCap.mReservationId = in.readInt();
                 return netCap;
             }
             @Override
@@ -2548,6 +2556,11 @@
                     NetworkCapabilities::enterpriseIdNameOf, "&");
         }
 
+        if (mReservationId != RES_ID_UNSET) {
+            final boolean isReservationOffer = (mReservationId == RES_ID_MATCH_ALL_RESERVATIONS);
+            sb.append(" ReservationId: ").append(isReservationOffer ? "*" : mReservationId);
+        }
+
         sb.append(" UnderlyingNetworks: ");
         if (mUnderlyingNetworks != null) {
             sb.append("[");
@@ -2876,6 +2889,65 @@
     }
 
     /**
+     * The reservation ID used by non-reservable Networks and "regular" NetworkOffers.
+     *
+     * Note that {@code NetworkRequest#FIRST_REQUEST_ID} is 1;
+     * @hide
+     */
+    public static final int RES_ID_UNSET = 0;
+
+    /**
+     * The reservation ID used by special NetworkOffers that handle RESERVATION requests.
+     *
+     * NetworkOffers with {@code RES_ID_MATCH_ALL_RESERVATIONS} *only* receive onNetworkNeeded()
+     * callbacks for {@code NetworkRequest.Type.RESERVATION}.
+     * @hide
+     */
+    public static final int RES_ID_MATCH_ALL_RESERVATIONS = -1;
+
+    /**
+     * Unique ID that identifies the network reservation.
+     */
+    private int mReservationId;
+
+    /**
+     * Returns the reservation ID
+     * @hide
+     */
+    public int getReservationId() {
+        return mReservationId;
+    }
+
+    /**
+     * Set the reservation ID
+     * @hide
+     */
+    public void setReservationId(int resId) {
+        mReservationId = resId;
+    }
+
+    private boolean equalsReservationId(@NonNull NetworkCapabilities nc) {
+        return mReservationId == nc.mReservationId;
+    }
+
+    private boolean satisfiedByReservationId(@NonNull NetworkCapabilities nc) {
+        if (mReservationId == RES_ID_UNSET) {
+            // To maintain regular NetworkRequest semantics, a request with a zero reservationId
+            // matches an offer or network with any reservationId except MATCH_ALL_RESERVATIONS.
+            return nc.mReservationId != RES_ID_MATCH_ALL_RESERVATIONS;
+        }
+        // A request with a non-zero reservationId matches only an offer or network with that exact
+        // reservationId (required to match the network that will eventually come up) or
+        // MATCH_ALL_RESERVATIONS (required to match the blanket reservation offer).
+        if (nc.mReservationId == RES_ID_MATCH_ALL_RESERVATIONS) {
+            return true;
+        }
+        return mReservationId == nc.mReservationId;
+    }
+
+
+
+    /**
      * Returns a bitmask of all the applicable redactions (based on the permissions held by the
      * receiving app) to be performed on this object.
      *
diff --git a/framework/src/android/net/NetworkRequest.java b/framework/src/android/net/NetworkRequest.java
index 89572b3..5ae25ab 100644
--- a/framework/src/android/net/NetworkRequest.java
+++ b/framework/src/android/net/NetworkRequest.java
@@ -193,6 +193,16 @@
      *       callbacks about the single, highest scoring current network
      *       (if any) that matches the specified NetworkCapabilities, or
      *
+     *     - RESERVATION requests behave identically to those of type REQUEST.
+     *       For example, unlike LISTEN, they cause networks to remain
+     *       connected, and they match exactly one network (the best one).
+     *       A RESERVATION generates a unique reservationId in its
+     *       NetworkCapabilities by copying the requestId which affects
+     *       matching. A NetworkProvider can register a "blanket" NetworkOffer
+     *       with reservationId = MATCH_ALL_RESERVATIONS to indicate that it
+     *       is capable of generating NetworkOffers in response to RESERVATION
+     *       requests.
+     *
      *     - TRACK_DEFAULT, which causes the framework to issue callbacks for
      *       the single, highest scoring current network (if any) that will
      *       be chosen for an app, but which cannot cause the framework to
@@ -229,6 +239,7 @@
         BACKGROUND_REQUEST,
         TRACK_SYSTEM_DEFAULT,
         LISTEN_FOR_BEST,
+        RESERVATION,
     };
 
     /**
@@ -247,6 +258,12 @@
         }
         requestId = rId;
         networkCapabilities = nc;
+        if (type == Type.RESERVATION) {
+            // Conceptually, the reservationId is not related to the requestId; however, the
+            // requestId fulfills the same uniqueness requirements that are needed for the
+            // reservationId, so it can be reused for this purpose.
+            networkCapabilities.setReservationId(rId);
+        }
         this.legacyType = legacyType;
         this.type = type;
     }
@@ -703,7 +720,7 @@
      * @hide
      */
     public boolean isRequest() {
-        return type == Type.REQUEST || type == Type.BACKGROUND_REQUEST;
+        return type == Type.REQUEST || type == Type.BACKGROUND_REQUEST || type == Type.RESERVATION;
     }
 
     /**
diff --git a/framework/src/android/net/connectivity/ConnectivityCompatChanges.java b/framework/src/android/net/connectivity/ConnectivityCompatChanges.java
index 51df8ab..0536263 100644
--- a/framework/src/android/net/connectivity/ConnectivityCompatChanges.java
+++ b/framework/src/android/net/connectivity/ConnectivityCompatChanges.java
@@ -135,6 +135,17 @@
     @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
     public static final long ENABLE_TRAFFICSTATS_RATE_LIMIT_CACHE = 74210811L;
 
+    /**
+     * Restrict local network access.
+     *
+     * Apps targeting a release after V will require permissions to access the local network.
+     *
+     * @hide
+     */
+    @ChangeId
+    @EnabledSince(targetSdkVersion = Build.VERSION_CODES.CUR_DEVELOPMENT)
+    public static final long RESTRICT_LOCAL_NETWORK = 365139289L;
+
     private ConnectivityCompatChanges() {
     }
 }
diff --git a/nearby/tests/cts/fastpair/AndroidManifest.xml b/nearby/tests/cts/fastpair/AndroidManifest.xml
index 472f4f0..9e1ec70 100644
--- a/nearby/tests/cts/fastpair/AndroidManifest.xml
+++ b/nearby/tests/cts/fastpair/AndroidManifest.xml
@@ -21,6 +21,7 @@
   <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
   <uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
   <uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
+  <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
 
   <application>
     <uses-library android:name="android.test.runner"/>
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 3291223..58d1808 100644
--- a/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyManagerTest.java
+++ b/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyManagerTest.java
@@ -26,7 +26,6 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.junit.Assert.assertThrows;
-import static org.junit.Assume.assumeTrue;
 
 import android.app.UiAutomation;
 import android.bluetooth.test_utils.EnableBluetoothRule;
@@ -79,10 +78,10 @@
 
     @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[] 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];
-    private static final byte[] AUTHENTICITY_KEY = new byte[]{0, 1, 1, 1};
+    private static final byte[] AUTHENTICITY_KEY = new byte[] {0, 1, 1, 1};
     private static final String DEVICE_NAME = "test_device";
     private static final int BLE_MEDIUM = 1;
 
@@ -91,43 +90,45 @@
     private UiAutomation mUiAutomation =
             InstrumentationRegistry.getInstrumentation().getUiAutomation();
 
-    private ScanRequest mScanRequest = new ScanRequest.Builder()
-            .setScanType(ScanRequest.SCAN_TYPE_FAST_PAIR)
-            .setScanMode(ScanRequest.SCAN_MODE_LOW_LATENCY)
-            .setBleEnabled(true)
-            .build();
+    private ScanRequest mScanRequest =
+            new ScanRequest.Builder()
+                    .setScanType(ScanRequest.SCAN_TYPE_FAST_PAIR)
+                    .setScanMode(ScanRequest.SCAN_MODE_LOW_LATENCY)
+                    .setBleEnabled(true)
+                    .build();
     private PresenceDevice.Builder mBuilder =
             new PresenceDevice.Builder("deviceId", SALT, SECRET_ID, META_DATA_ENCRYPTION_KEY);
 
-    private  ScanCallback mScanCallback = new ScanCallback() {
-        @Override
-        public void onDiscovered(@NonNull NearbyDevice device) {
-        }
+    private ScanCallback mScanCallback =
+            new ScanCallback() {
+                @Override
+                public void onDiscovered(@NonNull NearbyDevice device) {}
 
-        @Override
-        public void onUpdated(@NonNull NearbyDevice device) {
-        }
+                @Override
+                public void onUpdated(@NonNull NearbyDevice device) {}
 
-        @Override
-        public void onLost(@NonNull NearbyDevice device) {
-        }
+                @Override
+                public void onLost(@NonNull NearbyDevice device) {}
 
-        @Override
-        public void onError(int errorCode) {
-        }
-    };
+                @Override
+                public void onError(int errorCode) {}
+            };
 
     private static final Executor EXECUTOR = Executors.newSingleThreadExecutor();
 
     @Before
     public void setUp() {
-        mUiAutomation.adoptShellPermissionIdentity(READ_DEVICE_CONFIG, WRITE_DEVICE_CONFIG,
-                WRITE_ALLOWLISTED_DEVICE_CONFIG, BLUETOOTH_PRIVILEGED);
-        String nameSpace = SdkLevel.isAtLeastU() ? DeviceConfig.NAMESPACE_NEARBY
-                : DeviceConfig.NAMESPACE_TETHERING;
-        DeviceConfig.setProperty(nameSpace,
-                "nearby_enable_presence_broadcast_legacy",
-                "true", false);
+        mUiAutomation.adoptShellPermissionIdentity(
+                READ_DEVICE_CONFIG,
+                WRITE_DEVICE_CONFIG,
+                WRITE_ALLOWLISTED_DEVICE_CONFIG,
+                BLUETOOTH_PRIVILEGED);
+        String nameSpace =
+                SdkLevel.isAtLeastU()
+                        ? DeviceConfig.NAMESPACE_NEARBY
+                        : DeviceConfig.NAMESPACE_TETHERING;
+        DeviceConfig.setProperty(
+                nameSpace, "nearby_enable_presence_broadcast_legacy", "true", false);
 
         mContext = InstrumentationRegistry.getContext();
         mNearbyManager = mContext.getSystemService(NearbyManager.class);
@@ -144,8 +145,9 @@
     @SdkSuppress(minSdkVersion = 32, codeName = "T")
     public void test_startScan_noPrivilegedPermission() {
         mUiAutomation.dropShellPermissionIdentity();
-        assertThrows(SecurityException.class, () -> mNearbyManager
-                .startScan(mScanRequest, EXECUTOR, mScanCallback));
+        assertThrows(
+                SecurityException.class,
+                () -> mNearbyManager.startScan(mScanRequest, EXECUTOR, mScanCallback));
     }
 
     @Test
@@ -159,23 +161,25 @@
     @Test
     @SdkSuppress(minSdkVersion = 32, codeName = "T")
     public void testStartStopBroadcast() throws InterruptedException {
-        PrivateCredential credential = new PrivateCredential.Builder(SECRET_ID, AUTHENTICITY_KEY,
-                META_DATA_ENCRYPTION_KEY, DEVICE_NAME)
-                .setIdentityType(IDENTITY_TYPE_PRIVATE)
-                .build();
+        PrivateCredential credential =
+                new PrivateCredential.Builder(
+                                SECRET_ID, AUTHENTICITY_KEY, META_DATA_ENCRYPTION_KEY, DEVICE_NAME)
+                        .setIdentityType(IDENTITY_TYPE_PRIVATE)
+                        .build();
         BroadcastRequest broadcastRequest =
                 new PresenceBroadcastRequest.Builder(
-                        Collections.singletonList(BLE_MEDIUM), SALT, credential)
+                                Collections.singletonList(BLE_MEDIUM), SALT, credential)
                         .addAction(123)
                         .build();
 
         CountDownLatch latch = new CountDownLatch(1);
-        BroadcastCallback callback = status -> {
-            latch.countDown();
-            assertThat(status).isEqualTo(BroadcastCallback.STATUS_OK);
-        };
-        mNearbyManager.startBroadcast(broadcastRequest, Executors.newSingleThreadExecutor(),
-                callback);
+        BroadcastCallback callback =
+                status -> {
+                    latch.countDown();
+                    assertThat(status).isEqualTo(BroadcastCallback.STATUS_OK);
+                };
+        mNearbyManager.startBroadcast(
+                broadcastRequest, Executors.newSingleThreadExecutor(), callback);
         latch.await(10, TimeUnit.SECONDS);
         mNearbyManager.stopBroadcast(callback);
     }
@@ -197,9 +201,8 @@
     }
 
     @Test
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM)
     public void testsetPoweredOffFindingEphemeralIds() {
-        // Replace with minSdkVersion when Build.VERSION_CODES.VANILLA_ICE_CREAM can be used.
-        assumeTrue(SdkLevel.isAtLeastV());
         // Only test supporting devices.
         if (mNearbyManager.getPoweredOffFindingMode()
                 == NearbyManager.POWERED_OFF_FINDING_MODE_UNSUPPORTED) return;
@@ -208,24 +211,22 @@
     }
 
     @Test
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM)
     public void testsetPoweredOffFindingEphemeralIds_noPrivilegedPermission() {
-        // Replace with minSdkVersion when Build.VERSION_CODES.VANILLA_ICE_CREAM can be used.
-        assumeTrue(SdkLevel.isAtLeastV());
         // Only test supporting devices.
         if (mNearbyManager.getPoweredOffFindingMode()
                 == NearbyManager.POWERED_OFF_FINDING_MODE_UNSUPPORTED) return;
 
         mUiAutomation.dropShellPermissionIdentity();
 
-        assertThrows(SecurityException.class,
+        assertThrows(
+                SecurityException.class,
                 () -> mNearbyManager.setPoweredOffFindingEphemeralIds(List.of(new byte[20])));
     }
 
-
     @Test
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM)
     public void testSetAndGetPoweredOffFindingMode_enabled() {
-        // Replace with minSdkVersion when Build.VERSION_CODES.VANILLA_ICE_CREAM can be used.
-        assumeTrue(SdkLevel.isAtLeastV());
         // Only test supporting devices.
         if (mNearbyManager.getPoweredOffFindingMode()
                 == NearbyManager.POWERED_OFF_FINDING_MODE_UNSUPPORTED) return;
@@ -234,30 +235,26 @@
         // enableLocation() has dropped shell permission identity.
         mUiAutomation.adoptShellPermissionIdentity(BLUETOOTH_PRIVILEGED);
 
-        mNearbyManager.setPoweredOffFindingMode(
-                NearbyManager.POWERED_OFF_FINDING_MODE_ENABLED);
+        mNearbyManager.setPoweredOffFindingMode(NearbyManager.POWERED_OFF_FINDING_MODE_ENABLED);
         assertThat(mNearbyManager.getPoweredOffFindingMode())
                 .isEqualTo(NearbyManager.POWERED_OFF_FINDING_MODE_ENABLED);
     }
 
     @Test
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM)
     public void testSetAndGetPoweredOffFindingMode_disabled() {
-        // Replace with minSdkVersion when Build.VERSION_CODES.VANILLA_ICE_CREAM can be used.
-        assumeTrue(SdkLevel.isAtLeastV());
         // Only test supporting devices.
         if (mNearbyManager.getPoweredOffFindingMode()
                 == NearbyManager.POWERED_OFF_FINDING_MODE_UNSUPPORTED) return;
 
-        mNearbyManager.setPoweredOffFindingMode(
-                NearbyManager.POWERED_OFF_FINDING_MODE_DISABLED);
+        mNearbyManager.setPoweredOffFindingMode(NearbyManager.POWERED_OFF_FINDING_MODE_DISABLED);
         assertThat(mNearbyManager.getPoweredOffFindingMode())
                 .isEqualTo(NearbyManager.POWERED_OFF_FINDING_MODE_DISABLED);
     }
 
     @Test
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM)
     public void testSetPoweredOffFindingMode_noPrivilegedPermission() {
-        // Replace with minSdkVersion when Build.VERSION_CODES.VANILLA_ICE_CREAM can be used.
-        assumeTrue(SdkLevel.isAtLeastV());
         // Only test supporting devices.
         if (mNearbyManager.getPoweredOffFindingMode()
                 == NearbyManager.POWERED_OFF_FINDING_MODE_UNSUPPORTED) return;
@@ -265,14 +262,16 @@
         enableLocation();
         mUiAutomation.dropShellPermissionIdentity();
 
-        assertThrows(SecurityException.class, () -> mNearbyManager
-                .setPoweredOffFindingMode(NearbyManager.POWERED_OFF_FINDING_MODE_ENABLED));
+        assertThrows(
+                SecurityException.class,
+                () ->
+                        mNearbyManager.setPoweredOffFindingMode(
+                                NearbyManager.POWERED_OFF_FINDING_MODE_ENABLED));
     }
 
     @Test
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM)
     public void testGetPoweredOffFindingMode_noPrivilegedPermission() {
-        // Replace with minSdkVersion when Build.VERSION_CODES.VANILLA_ICE_CREAM can be used.
-        assumeTrue(SdkLevel.isAtLeastV());
         // Only test supporting devices.
         if (mNearbyManager.getPoweredOffFindingMode()
                 == NearbyManager.POWERED_OFF_FINDING_MODE_UNSUPPORTED) return;
diff --git a/nearby/tests/unit/src/com/android/server/nearby/managers/BluetoothFinderManagerTest.java b/nearby/tests/unit/src/com/android/server/nearby/managers/BluetoothFinderManagerTest.java
index 32286e1..a36084b 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/managers/BluetoothFinderManagerTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/managers/BluetoothFinderManagerTest.java
@@ -18,7 +18,6 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import static org.junit.Assume.assumeTrue;
 import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.anyBoolean;
 import static org.mockito.Mockito.anyInt;
@@ -29,12 +28,13 @@
 import android.hardware.bluetooth.finder.Eid;
 import android.hardware.bluetooth.finder.IBluetoothFinder;
 import android.nearby.PoweredOffFindingEphemeralId;
+import android.os.Build;
 import android.os.IBinder;
 import android.os.IBinder.DeathRecipient;
 import android.os.RemoteException;
 import android.os.ServiceSpecificException;
 
-import com.android.modules.utils.build.SdkLevel;
+import androidx.test.filters.SdkSuppress;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -44,6 +44,7 @@
 
 import java.util.List;
 
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM)
 public class BluetoothFinderManagerTest {
     private BluetoothFinderManager mBluetoothFinderManager;
     private boolean mGetServiceCalled = false;
@@ -71,8 +72,6 @@
 
     @Before
     public void setup() {
-        // Replace with minSdkVersion when Build.VERSION_CODES.VANILLA_ICE_CREAM can be used.
-        assumeTrue(SdkLevel.isAtLeastV());
         MockitoAnnotations.initMocks(this);
         mBluetoothFinderManager = new BluetoothFinderManagerSpy();
     }
@@ -80,16 +79,16 @@
     @Test
     public void testSendEids() throws Exception {
         byte[] eidBytes1 = {
-                (byte) 0xe1, (byte) 0xde, (byte) 0x1d, (byte) 0xe1, (byte) 0xde, (byte) 0x1d,
-                (byte) 0xe1, (byte) 0xde, (byte) 0x1d, (byte) 0xe1, (byte) 0xde, (byte) 0x1d,
-                (byte) 0xe1, (byte) 0xde, (byte) 0x1d, (byte) 0xe1, (byte) 0xde, (byte) 0x1d,
-                (byte) 0xe1, (byte) 0xde
+            (byte) 0xe1, (byte) 0xde, (byte) 0x1d, (byte) 0xe1, (byte) 0xde, (byte) 0x1d,
+            (byte) 0xe1, (byte) 0xde, (byte) 0x1d, (byte) 0xe1, (byte) 0xde, (byte) 0x1d,
+            (byte) 0xe1, (byte) 0xde, (byte) 0x1d, (byte) 0xe1, (byte) 0xde, (byte) 0x1d,
+            (byte) 0xe1, (byte) 0xde
         };
         byte[] eidBytes2 = {
-                (byte) 0xf2, (byte) 0xef, (byte) 0x2e, (byte) 0xf2, (byte) 0xef, (byte) 0x2e,
-                (byte) 0xf2, (byte) 0xef, (byte) 0x2e, (byte) 0xf2, (byte) 0xef, (byte) 0x2e,
-                (byte) 0xf2, (byte) 0xef, (byte) 0x2e, (byte) 0xf2, (byte) 0xef, (byte) 0x2e,
-                (byte) 0xf2, (byte) 0xef
+            (byte) 0xf2, (byte) 0xef, (byte) 0x2e, (byte) 0xf2, (byte) 0xef, (byte) 0x2e,
+            (byte) 0xf2, (byte) 0xef, (byte) 0x2e, (byte) 0xf2, (byte) 0xef, (byte) 0x2e,
+            (byte) 0xf2, (byte) 0xef, (byte) 0x2e, (byte) 0xf2, (byte) 0xef, (byte) 0x2e,
+            (byte) 0xf2, (byte) 0xef
         };
         PoweredOffFindingEphemeralId ephemeralId1 = new PoweredOffFindingEphemeralId();
         PoweredOffFindingEphemeralId ephemeralId2 = new PoweredOffFindingEphemeralId();
@@ -105,8 +104,7 @@
 
     @Test
     public void testSendEids_remoteException() throws Exception {
-        doThrow(new RemoteException())
-                .when(mIBluetoothFinderMock).sendEids(any());
+        doThrow(new RemoteException()).when(mIBluetoothFinderMock).sendEids(any());
         mBluetoothFinderManager.sendEids(List.of());
 
         // Verify that we get the service again following a RemoteException.
@@ -117,8 +115,7 @@
 
     @Test
     public void testSendEids_serviceSpecificException() throws Exception {
-        doThrow(new ServiceSpecificException(1))
-                .when(mIBluetoothFinderMock).sendEids(any());
+        doThrow(new ServiceSpecificException(1)).when(mIBluetoothFinderMock).sendEids(any());
         mBluetoothFinderManager.sendEids(List.of());
     }
 
@@ -134,7 +131,8 @@
     @Test
     public void testSetPoweredOffFinderMode_remoteException() throws Exception {
         doThrow(new RemoteException())
-                .when(mIBluetoothFinderMock).setPoweredOffFinderMode(anyBoolean());
+                .when(mIBluetoothFinderMock)
+                .setPoweredOffFinderMode(anyBoolean());
         mBluetoothFinderManager.setPoweredOffFinderMode(true);
 
         // Verify that we get the service again following a RemoteException.
@@ -146,7 +144,8 @@
     @Test
     public void testSetPoweredOffFinderMode_serviceSpecificException() throws Exception {
         doThrow(new ServiceSpecificException(1))
-                .when(mIBluetoothFinderMock).setPoweredOffFinderMode(anyBoolean());
+                .when(mIBluetoothFinderMock)
+                .setPoweredOffFinderMode(anyBoolean());
         mBluetoothFinderManager.setPoweredOffFinderMode(true);
     }
 
diff --git a/networksecurity/service/Android.bp b/networksecurity/service/Android.bp
index a41e6a0..f27acb7 100644
--- a/networksecurity/service/Android.bp
+++ b/networksecurity/service/Android.bp
@@ -34,6 +34,7 @@
 
     static_libs: [
         "auto_value_annotations",
+        "android.security.flags-aconfig-java-export",
     ],
 
     plugins: [
diff --git a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyInstaller.java b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyInstaller.java
index 4ca97eb..9970667 100644
--- a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyInstaller.java
+++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyInstaller.java
@@ -83,7 +83,7 @@
         DirectoryUtils.makeDir(mRootDirectory);
 
         if (!compatVersion.install(newContent, version)) {
-            Log.e(TAG, "Failed to install logs for compatibility version " + compatibilityVersion);
+            Log.e(TAG, "Failed to install logs version " + version);
             return false;
         }
         Log.i(TAG, "New logs installed at " + compatVersion.getLogsDir());
diff --git a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyService.java b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyService.java
index 92b2b09..782e6b5 100644
--- a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyService.java
+++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyService.java
@@ -13,15 +13,20 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
 package com.android.server.net.ct;
 
+import static android.security.Flags.certificateTransparencyConfiguration;
+
+import static com.android.net.ct.flags.Flags.certificateTransparencyJob;
+import static com.android.net.ct.flags.Flags.certificateTransparencyService;
+
 import android.annotation.RequiresApi;
 import android.content.Context;
 import android.net.ct.ICertificateTransparencyManager;
 import android.os.Build;
 import android.provider.DeviceConfig;
 
-import com.android.net.ct.flags.Flags;
 import com.android.server.SystemService;
 
 /** Implementation of the Certificate Transparency service. */
@@ -36,8 +41,11 @@
      */
     public static boolean enabled(Context context) {
         return DeviceConfig.getBoolean(
-                        Config.NAMESPACE_NETWORK_SECURITY, Config.FLAG_SERVICE_ENABLED, false)
-                && Flags.certificateTransparencyService();
+                        Config.NAMESPACE_NETWORK_SECURITY,
+                        Config.FLAG_SERVICE_ENABLED,
+                        /* defaultValue= */ false)
+                && certificateTransparencyService()
+                && certificateTransparencyConfiguration();
     }
 
     /** Creates a new {@link CertificateTransparencyService} object. */
@@ -52,7 +60,6 @@
                         downloadHelper,
                         signatureVerifier,
                         new CertificateTransparencyInstaller());
-
         mFlagsListener =
                 new CertificateTransparencyFlagsListener(dataStore, signatureVerifier, downloader);
         mCertificateTransparencyJob =
@@ -65,10 +72,9 @@
      * @see com.android.server.SystemService#onBootPhase
      */
     public void onBootPhase(int phase) {
-
         switch (phase) {
             case SystemService.PHASE_BOOT_COMPLETED:
-                if (Flags.certificateTransparencyJob()) {
+                if (certificateTransparencyJob()) {
                     mCertificateTransparencyJob.initialize();
                 } else {
                     mFlagsListener.initialize();
diff --git a/service-b/Android.bp b/service-b/Android.bp
new file mode 100644
index 0000000..47439ee
--- /dev/null
+++ b/service-b/Android.bp
@@ -0,0 +1,40 @@
+//
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package {
+    default_team: "trendy_team_enigma",
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+// TODO: b/380331248 This lib is a non-jarjared version of "service-connectivity-b-platform"
+// It will only be included in the Tethering module when the build system flag
+// RELEASE_MOVE_VCN_TO_MAINLINE is enabled. Including "service-connectivity-b-platform"
+// in Tethering will break art branch check because that lib lives in framework/base.
+// Once VCN is moved to Connectivity/, "service-connectivity-b-platform" can be cleaned up.
+java_library {
+    name: "service-connectivity-b-pre-jarjar",
+    defaults: ["service-connectivity-b-pre-jarjar-defaults"],
+    libs: ["service-connectivity-pre-jarjar"],
+
+    sdk_version: "system_server_current",
+
+    // TODO(b/210962470): Bump this to B
+    min_sdk_version: "30",
+
+    apex_available: [
+        "com.android.tethering",
+    ],
+}
diff --git a/service-t/Android.bp b/service-t/Android.bp
index 787e94e..d2e2a80 100644
--- a/service-t/Android.bp
+++ b/service-t/Android.bp
@@ -101,7 +101,7 @@
     min_sdk_version: "21",
     lint: {
         error_checks: ["NewApi"],
-
+        baseline_filename: "lint-baseline-service-connectivity-mdns-standalone-build-test.xml",
     },
     srcs: [
         "src/com/android/server/connectivity/mdns/**/*.java",
diff --git a/service-t/lint-baseline-service-connectivity-mdns-standalone-build-test.xml b/service-t/lint-baseline-service-connectivity-mdns-standalone-build-test.xml
new file mode 100644
index 0000000..232d31c
--- /dev/null
+++ b/service-t/lint-baseline-service-connectivity-mdns-standalone-build-test.xml
@@ -0,0 +1,972 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<issues format="6" by="lint 8.4.0-alpha08" type="baseline" client="" dependencies="true" name="" variant="all" version="8.4.0-alpha08">
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadEngine.java"
+            line="37"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadEngine.java"
+            line="37"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadEngine.java"
+            line="37"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadEngine.java"
+            line="37"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadEngine.java"
+            line="37"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+</issues>
diff --git a/service-t/src/com/android/server/connectivity/mdns/EnqueueMdnsQueryCallable.java b/service-t/src/com/android/server/connectivity/mdns/EnqueueMdnsQueryCallable.java
index f55db93..81ba530 100644
--- a/service-t/src/com/android/server/connectivity/mdns/EnqueueMdnsQueryCallable.java
+++ b/service-t/src/com/android/server/connectivity/mdns/EnqueueMdnsQueryCallable.java
@@ -139,11 +139,8 @@
                 // Base service type
                 questions.add(new MdnsPointerRecord(serviceTypeLabels, expectUnicastResponse));
                 for (String subtype : subtypes) {
-                    final String[] labels = new String[serviceTypeLabels.length + 2];
-                    labels[0] = MdnsConstants.SUBTYPE_PREFIX + subtype;
-                    labels[1] = MdnsConstants.SUBTYPE_LABEL;
-                    System.arraycopy(serviceTypeLabels, 0, labels, 2, serviceTypeLabels.length);
-
+                    final String[] labels = MdnsUtils.constructFullSubtype(serviceTypeLabels,
+                            MdnsConstants.SUBTYPE_PREFIX + subtype);
                     questions.add(new MdnsPointerRecord(labels, expectUnicastResponse));
                 }
             }
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsFeatureFlags.java b/service-t/src/com/android/server/connectivity/mdns/MdnsFeatureFlags.java
index 4e27fef..c4a9110 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsFeatureFlags.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsFeatureFlags.java
@@ -89,6 +89,11 @@
             "nsd_cached_services_retention_time";
     public static final int DEFAULT_CACHED_SERVICES_RETENTION_TIME_MILLISECONDS = 10000;
 
+    /**
+     * A feature flag to control whether the accurate delay callback should be enabled.
+     */
+    public static final String NSD_ACCURATE_DELAY_CALLBACK = "nsd_accurate_delay_callback";
+
     // Flag for offload feature
     public final boolean mIsMdnsOffloadFeatureEnabled;
 
@@ -122,6 +127,9 @@
     // Retention Time for cached services
     public final long mCachedServicesRetentionTime;
 
+    // Flag for accurate delay callback
+    public final boolean mIsAccurateDelayCallbackEnabled;
+
     @Nullable
     private final FlagOverrideProvider mOverrideProvider;
 
@@ -218,6 +226,14 @@
     }
 
     /**
+     * Indicates whether {@link #NSD_ACCURATE_DELAY_CALLBACK} is enabled, including for testing.
+     */
+    public boolean isAccurateDelayCallbackEnabled() {
+        return mIsAccurateDelayCallbackEnabled
+                || isForceEnabledForTest(NSD_ACCURATE_DELAY_CALLBACK);
+    }
+
+    /**
      * The constructor for {@link MdnsFeatureFlags}.
      */
     public MdnsFeatureFlags(boolean isOffloadFeatureEnabled,
@@ -231,6 +247,7 @@
             boolean avoidAdvertisingEmptyTxtRecords,
             boolean isCachedServicesRemovalEnabled,
             long cachedServicesRetentionTime,
+            boolean isAccurateDelayCallbackEnabled,
             @Nullable FlagOverrideProvider overrideProvider) {
         mIsMdnsOffloadFeatureEnabled = isOffloadFeatureEnabled;
         mIncludeInetAddressRecordsInProbing = includeInetAddressRecordsInProbing;
@@ -243,6 +260,7 @@
         mAvoidAdvertisingEmptyTxtRecords = avoidAdvertisingEmptyTxtRecords;
         mIsCachedServicesRemovalEnabled = isCachedServicesRemovalEnabled;
         mCachedServicesRetentionTime = cachedServicesRetentionTime;
+        mIsAccurateDelayCallbackEnabled = isAccurateDelayCallbackEnabled;
         mOverrideProvider = overrideProvider;
     }
 
@@ -266,6 +284,7 @@
         private boolean mAvoidAdvertisingEmptyTxtRecords;
         private boolean mIsCachedServicesRemovalEnabled;
         private long mCachedServicesRetentionTime;
+        private boolean mIsAccurateDelayCallbackEnabled;
         private FlagOverrideProvider mOverrideProvider;
 
         /**
@@ -283,6 +302,7 @@
             mAvoidAdvertisingEmptyTxtRecords = true; // Default enabled.
             mIsCachedServicesRemovalEnabled = false;
             mCachedServicesRetentionTime = DEFAULT_CACHED_SERVICES_RETENTION_TIME_MILLISECONDS;
+            mIsAccurateDelayCallbackEnabled = false;
             mOverrideProvider = null;
         }
 
@@ -409,6 +429,16 @@
         }
 
         /**
+         * Set whether the accurate delay callback is enabled.
+         *
+         * @see #NSD_ACCURATE_DELAY_CALLBACK
+         */
+        public Builder setIsAccurateDelayCallbackEnabled(boolean isAccurateDelayCallbackEnabled) {
+            mIsAccurateDelayCallbackEnabled = isAccurateDelayCallbackEnabled;
+            return this;
+        }
+
+        /**
          * Builds a {@link MdnsFeatureFlags} with the arguments supplied to this builder.
          */
         public MdnsFeatureFlags build() {
@@ -423,6 +453,7 @@
                     mAvoidAdvertisingEmptyTxtRecords,
                     mIsCachedServicesRemovalEnabled,
                     mCachedServicesRetentionTime,
+                    mIsAccurateDelayCallbackEnabled,
                     mOverrideProvider);
         }
     }
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java b/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java
index c3cb776..bfef5d9 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java
@@ -1482,22 +1482,14 @@
 
     private static String[] splitFullyQualifiedName(
             @NonNull NsdServiceInfo info, @NonNull String[] serviceType) {
-        final String[] split = new String[serviceType.length + 1];
-        split[0] = info.getServiceName();
-        System.arraycopy(serviceType, 0, split, 1, serviceType.length);
-
-        return split;
+        return CollectionUtils.prependArray(String.class, serviceType, info.getServiceName());
     }
 
     private static String[] splitServiceType(@NonNull NsdServiceInfo info) {
         // String.split(pattern, 0) removes trailing empty strings, which would appear when
         // splitting "domain.name." (with a dot a the end), so this is what is needed here.
         final String[] split = info.getServiceType().split("\\.", 0);
-        final String[] type = new String[split.length + 1];
-        System.arraycopy(split, 0, type, 0, split.length);
-        type[split.length] = LOCAL_TLD;
-
-        return type;
+        return CollectionUtils.appendArray(String.class, split, LOCAL_TLD);
     }
 
     /** Returns whether there will be an SRV record when registering the {@code info}. */
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
index a43486e..7a93fec 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
@@ -20,6 +20,7 @@
 import static com.android.server.connectivity.mdns.MdnsSearchOptions.AGGRESSIVE_QUERY_MODE;
 import static com.android.server.connectivity.mdns.MdnsServiceCache.ServiceExpiredCallback;
 import static com.android.server.connectivity.mdns.MdnsServiceCache.findMatchedResponse;
+import static com.android.server.connectivity.mdns.MdnsQueryScheduler.ScheduledQueryTaskArgs;
 import static com.android.server.connectivity.mdns.util.MdnsUtils.Clock;
 import static com.android.server.connectivity.mdns.util.MdnsUtils.buildMdnsServiceInfoFromResponse;
 
@@ -37,6 +38,7 @@
 import com.android.net.module.util.CollectionUtils;
 import com.android.net.module.util.DnsUtils;
 import com.android.net.module.util.SharedLog;
+import com.android.net.module.util.TimerFileDescriptor;
 import com.android.server.connectivity.mdns.util.MdnsUtils;
 
 import java.io.IOException;
@@ -94,6 +96,9 @@
     private final boolean removeServiceAfterTtlExpires =
             MdnsConfigs.removeServiceAfterTtlExpires();
     private final Clock clock;
+    // Use TimerFileDescriptor for query scheduling, which allows for more accurate sending of
+    // queries.
+    @NonNull private final TimerFileDescriptor timerFd;
 
     @Nullable private MdnsSearchOptions searchOptions;
 
@@ -139,8 +144,7 @@
         public void handleMessage(Message msg) {
             switch (msg.what) {
                 case EVENT_START_QUERYTASK: {
-                    final MdnsQueryScheduler.ScheduledQueryTaskArgs taskArgs =
-                            (MdnsQueryScheduler.ScheduledQueryTaskArgs) msg.obj;
+                    final ScheduledQueryTaskArgs taskArgs = (ScheduledQueryTaskArgs) msg.obj;
                     // QueryTask should be run immediately after being created (not be scheduled in
                     // advance). Because the result of "makeResponsesForResolve" depends on answers
                     // that were received before it is called, so to take into account all answers
@@ -174,7 +178,7 @@
                     final long now = clock.elapsedRealtime();
                     lastSentTime = now;
                     final long minRemainingTtl = getMinRemainingTtl(now);
-                    MdnsQueryScheduler.ScheduledQueryTaskArgs args =
+                    final ScheduledQueryTaskArgs args =
                             mdnsQueryScheduler.scheduleNextRun(
                                     sentResult.taskArgs.config,
                                     minRemainingTtl,
@@ -189,10 +193,14 @@
                     sharedLog.log(String.format("Query sent with transactionId: %d. "
                                     + "Next run: sessionId: %d, in %d ms",
                             sentResult.transactionId, args.sessionId, timeToNextTaskMs));
-                    dependencies.sendMessageDelayed(
-                            handler,
-                            handler.obtainMessage(EVENT_START_QUERYTASK, args),
-                            timeToNextTaskMs);
+                    if (featureFlags.isAccurateDelayCallbackEnabled()) {
+                        setDelayedTask(args, timeToNextTaskMs);
+                    } else {
+                        dependencies.sendMessageDelayed(
+                                handler,
+                                handler.obtainMessage(EVENT_START_QUERYTASK, args),
+                                timeToNextTaskMs);
+                    }
                     break;
                 }
                 default:
@@ -254,6 +262,14 @@
                 return List.of(new DatagramPacket(queryBuffer, 0, queryBuffer.length, address));
             }
         }
+
+        /**
+         * @see TimerFileDescriptor
+         */
+        @Nullable
+        public TimerFileDescriptor createTimerFd(@NonNull Handler handler) {
+            return new TimerFileDescriptor(handler);
+        }
     }
 
     /**
@@ -301,6 +317,7 @@
         this.mdnsQueryScheduler = new MdnsQueryScheduler();
         this.cacheKey = new MdnsServiceCache.CacheKey(serviceType, socketKey);
         this.featureFlags = featureFlags;
+        this.timerFd = dependencies.createTimerFd(handler);
     }
 
     /**
@@ -317,6 +334,13 @@
                 ? serviceCache.getCachedServices(cacheKey) : Collections.emptyList();
     }
 
+    private void setDelayedTask(ScheduledQueryTaskArgs args, long timeToNextTaskMs) {
+        timerFd.cancelTask();
+        timerFd.setDelayedTask(new TimerFileDescriptor.MessageTask(
+                        handler.obtainMessage(EVENT_START_QUERYTASK, args)),
+                timeToNextTaskMs);
+    }
+
     /**
      * Registers {@code listener} for receiving discovery event of mDNS service instances, and
      * starts
@@ -363,7 +387,7 @@
         }
         final long minRemainingTtl = getMinRemainingTtl(now);
         if (hadReply) {
-            MdnsQueryScheduler.ScheduledQueryTaskArgs args =
+            final ScheduledQueryTaskArgs args =
                     mdnsQueryScheduler.scheduleNextRun(
                             taskConfig,
                             minRemainingTtl,
@@ -377,10 +401,14 @@
             final long timeToNextTaskMs = calculateTimeToNextTask(args, now);
             sharedLog.log(String.format("Schedule a query. Next run: sessionId: %d, in %d ms",
                     args.sessionId, timeToNextTaskMs));
-            dependencies.sendMessageDelayed(
-                    handler,
-                    handler.obtainMessage(EVENT_START_QUERYTASK, args),
-                    timeToNextTaskMs);
+            if (featureFlags.isAccurateDelayCallbackEnabled()) {
+                setDelayedTask(args, timeToNextTaskMs);
+            } else {
+                dependencies.sendMessageDelayed(
+                        handler,
+                        handler.obtainMessage(EVENT_START_QUERYTASK, args),
+                        timeToNextTaskMs);
+            }
         } else {
             final List<MdnsResponse> servicesToResolve = makeResponsesForResolve(socketKey);
             final QueryTask queryTask = new QueryTask(
@@ -420,7 +448,11 @@
     }
 
     private void removeScheduledTask() {
-        dependencies.removeMessages(handler, EVENT_START_QUERYTASK);
+        if (featureFlags.isAccurateDelayCallbackEnabled()) {
+            timerFd.cancelTask();
+        } else {
+            dependencies.removeMessages(handler, EVENT_START_QUERYTASK);
+        }
         sharedLog.log("Remove EVENT_START_QUERYTASK"
                 + ", current session: " + currentSessionId);
         ++currentSessionId;
@@ -506,10 +538,13 @@
                 }
             }
         }
-        if (dependencies.hasMessages(handler, EVENT_START_QUERYTASK)) {
+        final boolean hasScheduledTask = featureFlags.isAccurateDelayCallbackEnabled()
+                ? timerFd.hasDelayedTask()
+                : dependencies.hasMessages(handler, EVENT_START_QUERYTASK);
+        if (hasScheduledTask) {
             final long now = clock.elapsedRealtime();
             final long minRemainingTtl = getMinRemainingTtl(now);
-            MdnsQueryScheduler.ScheduledQueryTaskArgs args =
+            final ScheduledQueryTaskArgs args =
                     mdnsQueryScheduler.maybeRescheduleCurrentRun(now, minRemainingTtl,
                             lastSentTime, currentSessionId + 1,
                             searchOptions.numOfQueriesBeforeBackoff());
@@ -518,10 +553,14 @@
                 final long timeToNextTaskMs = calculateTimeToNextTask(args, now);
                 sharedLog.log(String.format("Reschedule a query. Next run: sessionId: %d, in %d ms",
                         args.sessionId, timeToNextTaskMs));
-                dependencies.sendMessageDelayed(
-                        handler,
-                        handler.obtainMessage(EVENT_START_QUERYTASK, args),
-                        timeToNextTaskMs);
+                if (featureFlags.isAccurateDelayCallbackEnabled()) {
+                    setDelayedTask(args, timeToNextTaskMs);
+                } else {
+                    dependencies.sendMessageDelayed(
+                            handler,
+                            handler.obtainMessage(EVENT_START_QUERYTASK, args),
+                            timeToNextTaskMs);
+                }
             }
         }
     }
@@ -686,10 +725,10 @@
     private static class QuerySentArguments {
         private final int transactionId;
         private final List<String> subTypes = new ArrayList<>();
-        private final MdnsQueryScheduler.ScheduledQueryTaskArgs taskArgs;
+        private final ScheduledQueryTaskArgs taskArgs;
 
         QuerySentArguments(int transactionId, @NonNull List<String> subTypes,
-                @NonNull MdnsQueryScheduler.ScheduledQueryTaskArgs taskArgs) {
+                @NonNull ScheduledQueryTaskArgs taskArgs) {
             this.transactionId = transactionId;
             this.subTypes.addAll(subTypes);
             this.taskArgs = taskArgs;
@@ -698,14 +737,14 @@
 
     // A FutureTask that enqueues a single query, and schedule a new FutureTask for the next task.
     private class QueryTask implements Runnable {
-        private final MdnsQueryScheduler.ScheduledQueryTaskArgs taskArgs;
+        private final ScheduledQueryTaskArgs taskArgs;
         private final List<MdnsResponse> servicesToResolve = new ArrayList<>();
         private final List<String> subtypes = new ArrayList<>();
         private final boolean sendDiscoveryQueries;
         private final List<MdnsResponse> existingServices = new ArrayList<>();
         private final boolean onlyUseIpv6OnIpv6OnlyNetworks;
         private final SocketKey socketKey;
-        QueryTask(@NonNull MdnsQueryScheduler.ScheduledQueryTaskArgs taskArgs,
+        QueryTask(@NonNull ScheduledQueryTaskArgs taskArgs,
                 @NonNull Collection<MdnsResponse> servicesToResolve,
                 @NonNull Collection<String> subtypes, boolean sendDiscoveryQueries,
                 @NonNull Collection<MdnsResponse> existingServices,
@@ -771,7 +810,7 @@
         return minRemainingTtl == Long.MAX_VALUE ? 0 : minRemainingTtl;
     }
 
-    private static long calculateTimeToNextTask(MdnsQueryScheduler.ScheduledQueryTaskArgs args,
+    private static long calculateTimeToNextTask(ScheduledQueryTaskArgs args,
             long now) {
         return Math.max(args.timeToRun - now, 0);
     }
diff --git a/service-t/src/com/android/server/connectivity/mdns/util/MdnsUtils.java b/service-t/src/com/android/server/connectivity/mdns/util/MdnsUtils.java
index 41b15dd..282ca9a 100644
--- a/service-t/src/com/android/server/connectivity/mdns/util/MdnsUtils.java
+++ b/service-t/src/com/android/server/connectivity/mdns/util/MdnsUtils.java
@@ -28,6 +28,7 @@
 import android.util.ArraySet;
 import android.util.Pair;
 
+import com.android.net.module.util.CollectionUtils;
 import com.android.server.connectivity.mdns.MdnsConstants;
 import com.android.server.connectivity.mdns.MdnsInetAddressRecord;
 import com.android.server.connectivity.mdns.MdnsPacket;
@@ -273,11 +274,8 @@
      * of ["_printer", "_sub", "_http", "_tcp"].
      */
     public static String[] constructFullSubtype(String[] serviceType, String subtype) {
-        String[] fullSubtype = new String[serviceType.length + 2];
-        fullSubtype[0] = subtype;
-        fullSubtype[1] = MdnsConstants.SUBTYPE_LABEL;
-        System.arraycopy(serviceType, 0, fullSubtype, 2, serviceType.length);
-        return fullSubtype;
+        return CollectionUtils.prependArray(String.class, serviceType, subtype,
+                MdnsConstants.SUBTYPE_LABEL);
     }
 
     /** A wrapper class of {@link SystemClock} to be mocked in unit tests. */
diff --git a/service-t/src/com/android/server/ethernet/EthernetServiceImpl.java b/service-t/src/com/android/server/ethernet/EthernetServiceImpl.java
index b8689d6..21b9b1d 100644
--- a/service-t/src/com/android/server/ethernet/EthernetServiceImpl.java
+++ b/service-t/src/com/android/server/ethernet/EthernetServiceImpl.java
@@ -317,6 +317,6 @@
     @Override
     public List<String> getInterfaceList() {
         PermissionUtils.enforceAccessNetworkStatePermission(mContext, TAG);
-        return mTracker.getInterfaceList();
+        return mTracker.getEthernetInterfaceList();
     }
 }
diff --git a/service-t/src/com/android/server/ethernet/EthernetTracker.java b/service-t/src/com/android/server/ethernet/EthernetTracker.java
index 5228aab..6079413 100644
--- a/service-t/src/com/android/server/ethernet/EthernetTracker.java
+++ b/service-t/src/com/android/server/ethernet/EthernetTracker.java
@@ -21,6 +21,7 @@
 import static android.net.TestNetworkManager.TEST_TAP_PREFIX;
 
 import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE;
+import static com.android.net.module.util.netlink.NetlinkConstants.IFF_UP;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -40,7 +41,6 @@
 import android.os.Handler;
 import android.os.RemoteCallbackList;
 import android.os.RemoteException;
-import android.os.ServiceSpecificException;
 import android.system.OsConstants;
 import android.text.TextUtils;
 import android.util.ArrayMap;
@@ -56,13 +56,17 @@
 import com.android.net.module.util.ip.NetlinkMonitor;
 import com.android.net.module.util.netlink.NetlinkConstants;
 import com.android.net.module.util.netlink.NetlinkMessage;
+import com.android.net.module.util.netlink.NetlinkUtils;
 import com.android.net.module.util.netlink.RtNetlinkLinkMessage;
 import com.android.net.module.util.netlink.StructIfinfoMsg;
 import com.android.server.connectivity.ConnectivityResources;
 
 import java.io.FileDescriptor;
 import java.net.InetAddress;
+import java.net.NetworkInterface;
+import java.net.SocketException;
 import java.util.ArrayList;
+import java.util.Enumeration;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Objects;
@@ -105,7 +109,7 @@
 
     /**
      * Track test interfaces if true, don't track otherwise.
-     * Volatile is needed as getInterfaceList() does not run on the handler thread.
+     * Volatile is needed as getEthernetInterfaceList() does not run on the handler thread.
      */
     private volatile boolean mIncludeTestInterfaces = false;
 
@@ -398,26 +402,27 @@
         return mFactory.getAvailableInterfaces(includeRestricted);
     }
 
-    List<String> getInterfaceList() {
+    List<String> getEthernetInterfaceList() {
         final List<String> interfaceList = new ArrayList<String>();
-        final String[] ifaces;
+        final Enumeration<NetworkInterface> ifaces;
         try {
-            ifaces = mNetd.interfaceGetList();
-        } catch (RemoteException e) {
-            Log.e(TAG, "Could not get list of interfaces " + e);
+            ifaces = NetworkInterface.getNetworkInterfaces();
+        } catch (SocketException e) {
+            Log.e(TAG, "Failed to get ethernet interfaces: ", e);
             return interfaceList;
         }
 
         // There is a possible race with setIncludeTestInterfaces() which can affect
         // isValidEthernetInterface (it returns true for test interfaces if setIncludeTestInterfaces
         // is set to true).
-        // setIncludeTestInterfaces() is only used in tests, and since getInterfaceList() does not
-        // run on the handler thread, the behavior around setIncludeTestInterfaces() is
+        // setIncludeTestInterfaces() is only used in tests, and since getEthernetInterfaceList()
+        // does not run on the handler thread, the behavior around setIncludeTestInterfaces() is
         // indeterminate either way. This can easily be circumvented by waiting on a callback from
         // a test interface after calling setIncludeTestInterfaces() before calling this function.
         // In production code, this has no effect.
-        for (String iface : ifaces) {
-            if (isValidEthernetInterface(iface)) interfaceList.add(iface);
+        while (ifaces.hasMoreElements()) {
+            NetworkInterface iface = ifaces.nextElement();
+            if (isValidEthernetInterface(iface.getName())) interfaceList.add(iface.getName());
         }
         return interfaceList;
     }
@@ -593,14 +598,8 @@
             // Read the flags before attempting to bring up the interface. If the interface is
             // already running an UP event is created after adding the interface.
             config = NetdUtils.getInterfaceConfigParcel(mNetd, iface);
-            // Only bring the interface up when ethernet is enabled.
-            if (mIsEthernetEnabled) {
-                // As a side-effect, NetdUtils#setInterfaceUp() also clears the interface's IPv4
-                // address and readds it which *could* lead to unexpected behavior in the future.
-                NetdUtils.setInterfaceUp(mNetd, iface);
-            } else {
-                NetdUtils.setInterfaceDown(mNetd, iface);
-            }
+            // Only bring the interface up when ethernet is enabled, otherwise set interface down.
+            setInterfaceUpState(iface, mIsEthernetEnabled);
         } catch (IllegalStateException e) {
             // Either the system is crashing or the interface has disappeared. Just ignore the
             // error; we haven't modified any state because we only do that if our calls succeed.
@@ -660,15 +659,7 @@
             return;
         }
 
-        if (up) {
-            // WARNING! setInterfaceUp() clears the IPv4 address and readds it. Calling
-            // enableInterface() on an active interface can lead to a provisioning failure which
-            // will cause IpClient to be restarted.
-            // TODO: use netlink directly rather than calling into netd.
-            NetdUtils.setInterfaceUp(mNetd, iface);
-        } else {
-            NetdUtils.setInterfaceDown(mNetd, iface);
-        }
+        setInterfaceUpState(iface, up);
         cb.onResult(iface);
     }
 
@@ -707,10 +698,6 @@
     }
 
     private void maybeTrackInterface(String iface) {
-        if (!isValidEthernetInterface(iface)) {
-            return;
-        }
-
         // If we don't already track this interface, and if this interface matches
         // our regex, start tracking it.
         if (mFactory.hasInterface(iface) || iface.equals(mTetheringInterface)) {
@@ -730,13 +717,9 @@
     }
 
     private void trackAvailableInterfaces() {
-        try {
-            final String[] ifaces = mNetd.interfaceGetList();
-            for (String iface : ifaces) {
-                maybeTrackInterface(iface);
-            }
-        } catch (RemoteException | ServiceSpecificException e) {
-            Log.e(TAG, "Could not get list of interfaces " + e);
+        final List<String> ifaces = getEthernetInterfaceList();
+        for (String iface : ifaces) {
+            maybeTrackInterface(iface);
         }
     }
 
@@ -978,11 +961,7 @@
             }
 
             for (String iface : interfaces) {
-                if (enabled) {
-                    NetdUtils.setInterfaceUp(mNetd, iface);
-                } else {
-                    NetdUtils.setInterfaceDown(mNetd, iface);
-                }
+                setInterfaceUpState(iface, enabled);
             }
             broadcastEthernetStateChange(mIsEthernetEnabled);
         });
@@ -1016,6 +995,12 @@
         mListeners.finishBroadcast();
     }
 
+    private void setInterfaceUpState(@NonNull String interfaceName, boolean up) {
+        if (!NetlinkUtils.setInterfaceFlags(interfaceName, up ? IFF_UP : ~IFF_UP)) {
+            Log.e(TAG, "Failed to set interface " + interfaceName + (up ? " up" : " down"));
+        }
+    }
+
     void dump(FileDescriptor fd, IndentingPrintWriter pw, String[] args) {
         postAndWaitForRunnable(() -> {
             pw.println(getClass().getSimpleName());
diff --git a/service-t/src/com/android/server/net/NetworkStatsService.java b/service-t/src/com/android/server/net/NetworkStatsService.java
index fb712a1..a8e3203 100644
--- a/service-t/src/com/android/server/net/NetworkStatsService.java
+++ b/service-t/src/com/android/server/net/NetworkStatsService.java
@@ -493,7 +493,8 @@
     @Nullable
     private final TrafficStatsRateLimitCache mTrafficStatsUidCache;
     // A feature flag to control whether the client-side rate limit cache should be enabled.
-    static final String TRAFFICSTATS_CLIENT_RATE_LIMIT_CACHE_ENABLED_FLAG =
+    @VisibleForTesting
+    public static final String TRAFFICSTATS_CLIENT_RATE_LIMIT_CACHE_ENABLED_FLAG =
             "trafficstats_client_rate_limit_cache_enabled_flag";
     static final String TRAFFICSTATS_SERVICE_RATE_LIMIT_CACHE_ENABLED_FLAG =
             "trafficstats_rate_limit_cache_enabled_flag";
diff --git a/service-t/src/com/android/server/net/TrafficStatsRateLimitCache.java b/service-t/src/com/android/server/net/TrafficStatsRateLimitCache.java
index 667aad1..4f99d1b 100644
--- a/service-t/src/com/android/server/net/TrafficStatsRateLimitCache.java
+++ b/service-t/src/com/android/server/net/TrafficStatsRateLimitCache.java
@@ -29,7 +29,10 @@
 /**
  * A thread-safe cache for storing and retrieving {@link NetworkStats.Entry} objects,
  * with an adjustable expiry duration to manage data freshness.
+ *
+ * @deprecated Use {@link LruCacheWithExpiry} instead.
  */
+// TODO: Remove this when service side rate limit cache solution is removed.
 class TrafficStatsRateLimitCache extends
         LruCacheWithExpiry<TrafficStatsRateLimitCache.TrafficStatsCacheKey, NetworkStats.Entry> {
 
@@ -41,7 +44,7 @@
      * @param maxSize Maximum number of entries.
      */
     TrafficStatsRateLimitCache(@NonNull Clock clock, long expiryDurationMs, int maxSize) {
-        super(clock, expiryDurationMs, maxSize, it -> !it.isEmpty());
+        super(()-> clock.millis(), expiryDurationMs, maxSize, it -> !it.isEmpty());
     }
 
     public static class TrafficStatsCacheKey {
diff --git a/service/Android.bp b/service/Android.bp
index fd3d4a3..2659ebf 100644
--- a/service/Android.bp
+++ b/service/Android.bp
@@ -207,6 +207,7 @@
     },
     visibility: [
         "//packages/modules/Connectivity/service-t",
+        "//packages/modules/Connectivity/service-b",
         "//packages/modules/Connectivity/networksecurity:__subpackages__",
         "//packages/modules/Connectivity/tests:__subpackages__",
         "//packages/modules/Connectivity/thread/service:__subpackages__",
@@ -252,7 +253,11 @@
         "service-networksecurity-pre-jarjar",
         service_remoteauth_pre_jarjar_lib,
         "service-thread-pre-jarjar",
-    ],
+    ] + select(release_flag("RELEASE_MOVE_VCN_TO_MAINLINE"), {
+        true: ["service-connectivity-b-pre-jarjar"],
+        default: [],
+    }),
+
     // The below libraries are not actually needed to build since no source is compiled
     // (only combining prebuilt static_libs), but they are necessary so that R8 has the right
     // references to optimize the code. Without these, there will be missing class warnings and
@@ -338,6 +343,7 @@
     name: "service-connectivity-jarjar-gen",
     tool_files: [
         ":service-connectivity-pre-jarjar{.jar}",
+        ":service-connectivity-b-pre-jarjar{.jar}",
         ":service-connectivity-tiramisu-pre-jarjar{.jar}",
         "jarjar-excludes.txt",
     ],
@@ -347,6 +353,7 @@
     out: ["service_connectivity_jarjar_rules.txt"],
     cmd: "$(location jarjar-rules-generator) " +
         "$(location :service-connectivity-pre-jarjar{.jar}) " +
+        "$(location :service-connectivity-b-pre-jarjar{.jar}) " +
         "$(location :service-connectivity-tiramisu-pre-jarjar{.jar}) " +
         "--prefix android.net.connectivity " +
         "--excludes $(location jarjar-excludes.txt) " +
diff --git a/service/jarjar-excludes.txt b/service/jarjar-excludes.txt
index 7bd3862..9076b53 100644
--- a/service/jarjar-excludes.txt
+++ b/service/jarjar-excludes.txt
@@ -1,3 +1,4 @@
 # Classes loaded by SystemServer via their hardcoded name, so they can't be jarjared
 com\.android\.server\.ConnectivityServiceInitializer(\$.+)?
+com\.android\.server\.ConnectivityServiceInitializerB(\$.+)?
 com\.android\.server\.NetworkStatsServiceInitializer(\$.+)?
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index f3b97bc..bad7246 100644
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -121,6 +121,9 @@
 import static android.net.connectivity.ConnectivityCompatChanges.NETWORK_BLOCKED_WITHOUT_INTERNET_PERMISSION;
 import static android.os.Process.INVALID_UID;
 import static android.os.Process.VPN_UID;
+import static android.system.OsConstants.ENOENT;
+import static android.system.OsConstants.ENOTCONN;
+import static android.system.OsConstants.EOPNOTSUPP;
 import static android.system.OsConstants.ETH_P_ALL;
 import static android.system.OsConstants.IPPROTO_TCP;
 import static android.system.OsConstants.IPPROTO_UDP;
@@ -195,6 +198,7 @@
 import android.net.IConnectivityDiagnosticsCallback;
 import android.net.IConnectivityManager;
 import android.net.IDnsResolver;
+import android.net.IIntResultListener;
 import android.net.INetd;
 import android.net.INetworkActivityListener;
 import android.net.INetworkAgent;
@@ -6340,8 +6344,20 @@
         }
     }
 
-    private class CaptivePortalImpl extends ICaptivePortal.Stub {
+    public class CaptivePortalImpl extends ICaptivePortal.Stub implements IBinder.DeathRecipient {
         private final Network mNetwork;
+        // Binder object to track the lifetime of the setDelegateUid caller for cleanup purposes.
+        //
+        // Note that in theory it can happen that there are multiple callers for a given
+        // object. For example, the app that receives the CaptivePortal object from the Intent
+        // fired by startCaptivePortalAppInternal could send the object to another process, or
+        // clone it. Only the first of these objects that calls setDelegateUid will properly
+        // register a death recipient. Calls from the other objects will work, but only the
+        // first object's death will cause the death recipient to fire.
+        // TODO: track all callers by callerBinder instead of CaptivePortalImpl, store callerBinder
+        // in a Set. When the death recipient fires, we can remove the callingBinder from the set,
+        // and when the set is empty, we can clear the delegated UID.
+        private IBinder mDelegateUidCaller;
 
         private CaptivePortalImpl(Network network) {
             mNetwork = network;
@@ -6381,6 +6397,55 @@
             }
         }
 
+        private int handleSetDelegateUid(int uid, @NonNull final IBinder callerBinder) {
+            if (mDelegateUidCaller == null) {
+                mDelegateUidCaller = callerBinder;
+                try {
+                    // While technically unnecessary, it is safe to register a DeathRecipient for
+                    // a cleanup operation (where uid = INVALID_UID).
+                    mDelegateUidCaller.linkToDeath(this, 0);
+                } catch (RemoteException e) {
+                    // remote has died, return early.
+                    return ENOTCONN;
+                }
+            }
+
+            final NetworkAgentInfo nai = getNetworkAgentInfoForNetwork(mNetwork);
+            if (nai == null) return ENOENT; // network does not exist anymore.
+            if (nai.isDestroyed()) return ENOENT; // network has already been destroyed.
+
+            // TODO: consider allowing the uid to bypass VPN on all networks before V.
+            if (!mDeps.isAtLeastV()) return EOPNOTSUPP;
+
+            // Check whether there has already been a delegate UID configured, if so, perform
+            // cleanup and disallow bypassing VPN for that UID if no other caller is delegating
+            // this UID.
+            // TODO: consider using exceptions instead of errnos.
+            final int errno = nai.removeCaptivePortalDelegateUid(this);
+            if (errno != 0) return errno;
+
+            // If uid == INVALID_UID, we are done.
+            if (uid == INVALID_UID) return 0;
+            return nai.setCaptivePortalDelegateUid(this, uid);
+        }
+
+        @Override
+        public void setDelegateUid(int uid, @NonNull final IBinder callerBinder,
+                @NonNull final IIntResultListener listener) {
+            Objects.requireNonNull(callerBinder);
+            Objects.requireNonNull(listener);
+            enforceAnyPermissionOf(mContext, NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK);
+
+            mHandler.post(() -> {
+                final int errno = handleSetDelegateUid(uid, callerBinder);
+                try {
+                    listener.onResult(errno);
+                } catch (RemoteException e) {
+                    // remote has died, nothing to do.
+                }
+            });
+        }
+
         @Nullable
         private NetworkMonitorManager getNetworkMonitorManager(final Network network) {
             // getNetworkAgentInfoForNetwork is thread-safe
@@ -6390,6 +6455,13 @@
             // nai.networkMonitor() is thread-safe
             return nai.networkMonitor();
         }
+
+        @Override
+        public void binderDied() {
+            // Cleanup invalid UID and restore the VPN bypass rule. Because mDelegateUidCaller is
+            // never reset, it cannot be null in this context.
+            mHandler.post(() -> handleSetDelegateUid(INVALID_UID, mDelegateUidCaller));
+        }
     }
 
     public boolean avoidBadWifi() {
@@ -8343,6 +8415,7 @@
                 enforceNetworkStackOrSettingsPermission();
                 // Fall-through since other checks are the same with normal requests.
             case REQUEST:
+            case RESERVATION:
                 networkCapabilities = new NetworkCapabilities(networkCapabilities);
                 enforceNetworkRequestPermissions(networkCapabilities, callingPackageName,
                         callingAttributionTag, callingUid);
diff --git a/service/src/com/android/server/connectivity/NetworkAgentInfo.java b/service/src/com/android/server/connectivity/NetworkAgentInfo.java
index 94b655f..2b00386 100644
--- a/service/src/com/android/server/connectivity/NetworkAgentInfo.java
+++ b/service/src/com/android/server/connectivity/NetworkAgentInfo.java
@@ -25,6 +25,9 @@
 import static android.net.NetworkCapabilities.TRANSPORT_TEST;
 import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
 import static android.net.NetworkCapabilities.transportNamesOf;
+import static android.system.OsConstants.EIO;
+import static android.system.OsConstants.EEXIST;
+import static android.system.OsConstants.ENOENT;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -57,9 +60,11 @@
 import android.os.Handler;
 import android.os.IBinder;
 import android.os.RemoteException;
+import android.os.ServiceSpecificException;
 import android.os.SystemClock;
 import android.telephony.data.EpsBearerQosSessionAttributes;
 import android.telephony.data.NrQosSessionAttributes;
+import android.util.ArrayMap;
 import android.util.ArraySet;
 import android.util.Log;
 import android.util.Pair;
@@ -70,6 +75,7 @@
 import com.android.internal.util.WakeupMessage;
 import com.android.net.module.util.HandlerUtils;
 import com.android.server.ConnectivityService;
+import com.android.server.ConnectivityService.CaptivePortalImpl;
 
 import java.io.PrintWriter;
 import java.net.Inet4Address;
@@ -574,6 +580,10 @@
     // For fast lookups. Indexes into mInactivityTimers by request ID.
     private final SparseArray<InactivityTimer> mInactivityTimerForRequest = new SparseArray<>();
 
+    // Map of delegated UIDs used to bypass VPN and its captive portal app caller.
+    private final ArrayMap<CaptivePortalImpl, Integer> mCaptivePortalDelegateUids =
+            new ArrayMap<>();
+
     // Inactivity expiry timer. Armed whenever mInactivityTimers is non-empty, regardless of
     // whether the network is inactive or not. Always set to the expiry of the mInactivityTimers
     // that expires last. When the timer fires, all inactivity state is cleared, and if the network
@@ -626,6 +636,7 @@
     private final Context mContext;
     private final Handler mHandler;
     private final QosCallbackTracker mQosCallbackTracker;
+    private final INetd mNetd;
 
     private final long mCreationTime;
 
@@ -655,6 +666,7 @@
         mConnServiceDeps = deps;
         setScore(score); // uses members connService, networkCapabilities and networkAgentConfig
         clatd = new Nat464Xlat(this, netd, dnsResolver, deps);
+        mNetd = netd;
         mContext = context;
         mHandler = handler;
         this.factorySerialNumber = factorySerialNumber;
@@ -1549,6 +1561,52 @@
         }
     }
 
+    private int allowBypassVpnOnNetwork(boolean allow, int uid, int netId) {
+        try {
+            mNetd.networkAllowBypassVpnOnNetwork(allow, uid, netId);
+            return 0;
+        } catch (RemoteException e) {
+            // Netd has crashed, and this process is about to crash as well.
+            return EIO;
+        } catch (ServiceSpecificException e) {
+            return e.errorCode;
+        }
+    }
+
+    /**
+     * Set the delegate UID of the app that is allowed to perform network traffic for captive
+     * portal login, and configure the netd bypass rule with this delegated UID.
+     *
+     * @param caller the captive portal app to that delegated UID
+     * @param uid the delegated UID of the captive portal app.
+     * @return Return 0 if set the UID and VPN bypass rule successfully or bypass rule corresponding
+     *                to this UID already exists otherwise return errno.
+     */
+    public int setCaptivePortalDelegateUid(@NonNull final CaptivePortalImpl caller, int uid) {
+        final int errorCode = allowBypassVpnOnNetwork(true /* allow */, uid, network.netId);
+        if (errorCode == 0 || errorCode == EEXIST) {
+            mCaptivePortalDelegateUids.put(caller, uid);
+        }
+        return errorCode == EEXIST ? 0 : errorCode;
+    }
+
+    /**
+     * Remove the delegate UID of the app that is allowed to perform network traffic for captive
+     * portal login, and remove the netd bypass rule if no other caller is delegating this UID.
+     *
+     * @param caller the captive portal app to that delegated UID.
+     * @return Return 0 if remove the UID and VPN bypass rule successfully or bypass rule
+     *                corresponding to this UID doesn't exist otherwise return errno.
+     */
+    public int removeCaptivePortalDelegateUid(@NonNull final CaptivePortalImpl caller) {
+        final Integer maybeDelegateUid = mCaptivePortalDelegateUids.remove(caller);
+        if (maybeDelegateUid == null) return 0;
+        if (mCaptivePortalDelegateUids.values().contains(maybeDelegateUid)) return 0;
+        final int errorCode =
+                allowBypassVpnOnNetwork(false /* allow */, maybeDelegateUid, network.netId);
+        return errorCode == ENOENT ? 0 : errorCode;
+    }
+
     private static boolean areAllowedUidsAcceptableFromNetworkAgent(
             @NonNull final NetworkCapabilities nc, final boolean hasAutomotiveFeature,
             @NonNull final ConnectivityService.Dependencies deps,
diff --git a/staticlibs/Android.bp b/staticlibs/Android.bp
index b4a3b8a..71e09fe 100644
--- a/staticlibs/Android.bp
+++ b/staticlibs/Android.bp
@@ -438,7 +438,10 @@
     srcs: [
         "device/com/android/net/module/util/FdEventsReader.java",
         "device/com/android/net/module/util/HandlerUtils.java",
+        "device/com/android/net/module/util/JniUtil.java",
         "device/com/android/net/module/util/SharedLog.java",
+        "device/com/android/net/module/util/TimerFdUtils.java",
+        "device/com/android/net/module/util/TimerFileDescriptor.java",
         "framework/com/android/net/module/util/ByteUtils.java",
         "framework/com/android/net/module/util/CollectionUtils.java",
         "framework/com/android/net/module/util/DnsUtils.java",
diff --git a/staticlibs/client-libs/netd/com/android/net/module/util/NetdUtils.java b/staticlibs/client-libs/netd/com/android/net/module/util/NetdUtils.java
index d99eedc..8b2fe58 100644
--- a/staticlibs/client-libs/netd/com/android/net/module/util/NetdUtils.java
+++ b/staticlibs/client-libs/netd/com/android/net/module/util/NetdUtils.java
@@ -149,21 +149,21 @@
     }
 
     /** Setup interface for tethering. */
-    public static void tetherInterface(final INetd netd, final String iface, final IpPrefix dest)
-            throws RemoteException, ServiceSpecificException {
-        tetherInterface(netd, iface, dest, 20 /* maxAttempts */, 50 /* pollingIntervalMs */);
+    public static void tetherInterface(final INetd netd, int netId, final String iface,
+            final IpPrefix dest) throws RemoteException, ServiceSpecificException {
+        tetherInterface(netd, netId, iface, dest, 20 /* maxAttempts */, 50 /* pollingIntervalMs */);
     }
 
     /** Setup interface with configurable retries for tethering. */
-    public static void tetherInterface(final INetd netd, final String iface, final IpPrefix dest,
-            int maxAttempts, int pollingIntervalMs)
+    public static void tetherInterface(final INetd netd, int netId, final String iface,
+            final IpPrefix dest, int maxAttempts, int pollingIntervalMs)
             throws RemoteException, ServiceSpecificException {
         netd.tetherInterfaceAdd(iface);
-        networkAddInterface(netd, iface, maxAttempts, pollingIntervalMs);
+        networkAddInterface(netd, netId, iface, maxAttempts, pollingIntervalMs);
         // Activate a route to dest and IPv6 link local.
-        modifyRoute(netd, ModifyOperation.ADD, INetd.LOCAL_NET_ID,
+        modifyRoute(netd, ModifyOperation.ADD, netId,
                 new RouteInfo(dest, null, iface, RTN_UNICAST));
-        modifyRoute(netd, ModifyOperation.ADD, INetd.LOCAL_NET_ID,
+        modifyRoute(netd, ModifyOperation.ADD, netId,
                 new RouteInfo(new IpPrefix("fe80::/64"), null, iface, RTN_UNICAST));
     }
 
@@ -174,12 +174,12 @@
      * in use in netd because the ConnectivityService thread hasn't processed the disconnect yet.
      * See b/158269544 for detail.
      */
-    private static void networkAddInterface(final INetd netd, final String iface,
+    private static void networkAddInterface(final INetd netd, int netId, final String iface,
             int maxAttempts, int pollingIntervalMs)
             throws ServiceSpecificException, RemoteException {
         for (int i = 1; i <= maxAttempts; i++) {
             try {
-                netd.networkAddInterface(INetd.LOCAL_NET_ID, iface);
+                netd.networkAddInterface(netId, iface);
                 return;
             } catch (ServiceSpecificException e) {
                 if (e.errorCode == EBUSY && i < maxAttempts) {
@@ -194,37 +194,38 @@
     }
 
     /** Reset interface for tethering. */
-    public static void untetherInterface(final INetd netd, String iface)
+    public static void untetherInterface(final INetd netd, int netId, String iface)
             throws RemoteException, ServiceSpecificException {
         try {
             netd.tetherInterfaceRemove(iface);
         } finally {
-            netd.networkRemoveInterface(INetd.LOCAL_NET_ID, iface);
+            netd.networkRemoveInterface(netId, iface);
         }
     }
 
-    /** Add |routes| to local network. */
-    public static void addRoutesToLocalNetwork(final INetd netd, final String iface,
+    /** Add |routes| to the given network. */
+    public static void addRoutesToNetwork(final INetd netd, int netId, final String iface,
             final List<RouteInfo> routes) {
 
         for (RouteInfo route : routes) {
             if (!route.isDefaultRoute()) {
-                modifyRoute(netd, ModifyOperation.ADD, INetd.LOCAL_NET_ID, route);
+                modifyRoute(netd, ModifyOperation.ADD, netId, route);
             }
         }
 
         // IPv6 link local should be activated always.
-        modifyRoute(netd, ModifyOperation.ADD, INetd.LOCAL_NET_ID,
+        modifyRoute(netd, ModifyOperation.ADD, netId,
                 new RouteInfo(new IpPrefix("fe80::/64"), null, iface, RTN_UNICAST));
     }
 
-    /** Remove routes from local network. */
-    public static int removeRoutesFromLocalNetwork(final INetd netd, final List<RouteInfo> routes) {
+    /** Remove routes from the given network. */
+    public static int removeRoutesFromNetwork(final INetd netd, int netId,
+            final List<RouteInfo> routes) {
         int failures = 0;
 
         for (RouteInfo route : routes) {
             try {
-                modifyRoute(netd, ModifyOperation.REMOVE, INetd.LOCAL_NET_ID, route);
+                modifyRoute(netd, ModifyOperation.REMOVE, netId, route);
             } catch (IllegalStateException e) {
                 failures++;
             }
diff --git a/staticlibs/client-libs/tests/unit/src/com/android/net/module/util/NetdUtilsTest.java b/staticlibs/client-libs/tests/unit/src/com/android/net/module/util/NetdUtilsTest.java
index 5069672..c2fbb56 100644
--- a/staticlibs/client-libs/tests/unit/src/com/android/net/module/util/NetdUtilsTest.java
+++ b/staticlibs/client-libs/tests/unit/src/com/android/net/module/util/NetdUtilsTest.java
@@ -16,7 +16,6 @@
 
 package com.android.net.module.util;
 
-import static android.net.INetd.LOCAL_NET_ID;
 import static android.system.OsConstants.EBUSY;
 
 import static com.android.testutils.MiscAsserts.assertThrows;
@@ -63,6 +62,7 @@
 
     private static final String IFACE = "TEST_IFACE";
     private static final IpPrefix TEST_IPPREFIX = new IpPrefix("192.168.42.1/24");
+    private static final int TEST_NET_ID = 123;
 
     @Before
     public void setUp() throws Exception {
@@ -134,7 +134,7 @@
             }
 
             throw new ServiceSpecificException(EBUSY);
-        }).when(mNetd).networkAddInterface(LOCAL_NET_ID, IFACE);
+        }).when(mNetd).networkAddInterface(TEST_NET_ID, IFACE);
     }
 
     class Counter {
@@ -163,7 +163,7 @@
         setNetworkAddInterfaceOutcome(new ServiceSpecificException(expectedCode), expectedTries);
 
         try {
-            NetdUtils.tetherInterface(mNetd, IFACE, TEST_IPPREFIX, 20, 0);
+            NetdUtils.tetherInterface(mNetd, TEST_NET_ID, IFACE, TEST_IPPREFIX, 20, 0);
             fail("Expect throw ServiceSpecificException");
         } catch (ServiceSpecificException e) {
             assertEquals(e.errorCode, expectedCode);
@@ -177,7 +177,7 @@
         setNetworkAddInterfaceOutcome(new RemoteException(), expectedTries);
 
         try {
-            NetdUtils.tetherInterface(mNetd, IFACE, TEST_IPPREFIX, 20, 0);
+            NetdUtils.tetherInterface(mNetd, TEST_NET_ID, IFACE, TEST_IPPREFIX, 20, 0);
             fail("Expect throw RemoteException");
         } catch (RemoteException e) { }
 
@@ -187,18 +187,19 @@
 
     private void verifyNetworkAddInterfaceFails(int expectedTries) throws Exception {
         verify(mNetd).tetherInterfaceAdd(IFACE);
-        verify(mNetd, times(expectedTries)).networkAddInterface(LOCAL_NET_ID, IFACE);
+        verify(mNetd, times(expectedTries)).networkAddInterface(TEST_NET_ID, IFACE);
         verify(mNetd, never()).networkAddRoute(anyInt(), anyString(), any(), any());
+
         verifyNoMoreInteractions(mNetd);
     }
 
     private void verifyTetherInterfaceSucceeds(int expectedTries) throws Exception {
         setNetworkAddInterfaceOutcome(null, expectedTries);
 
-        NetdUtils.tetherInterface(mNetd, IFACE, TEST_IPPREFIX);
+        NetdUtils.tetherInterface(mNetd, TEST_NET_ID, IFACE, TEST_IPPREFIX);
         verify(mNetd).tetherInterfaceAdd(IFACE);
-        verify(mNetd, times(expectedTries)).networkAddInterface(LOCAL_NET_ID, IFACE);
-        verify(mNetd, times(2)).networkAddRoute(eq(LOCAL_NET_ID), eq(IFACE), any(), any());
+        verify(mNetd, times(expectedTries)).networkAddInterface(TEST_NET_ID, IFACE);
+        verify(mNetd, times(2)).networkAddRoute(eq(TEST_NET_ID), eq(IFACE), any(), any());
         verifyNoMoreInteractions(mNetd);
         reset(mNetd);
     }
diff --git a/staticlibs/device/com/android/net/module/util/TimerFdUtils.java b/staticlibs/device/com/android/net/module/util/TimerFdUtils.java
index 310dbc9..c7ed911 100644
--- a/staticlibs/device/com/android/net/module/util/TimerFdUtils.java
+++ b/staticlibs/device/com/android/net/module/util/TimerFdUtils.java
@@ -26,12 +26,13 @@
  */
 public class TimerFdUtils {
     static {
-        if (Process.myUid() == Process.SYSTEM_UID) {
+        final String jniLibName = JniUtil.getJniLibraryName(TimerFdUtils.class.getPackage());
+        if (jniLibName.equals("android_net_connectivity_com_android_net_module_util_jni")) {
             // This library is part of service-connectivity.jar when in the system server,
             // so libservice-connectivity.so is the library to load.
             System.loadLibrary("service-connectivity");
         } else {
-            System.loadLibrary(JniUtil.getJniLibraryName(TimerFdUtils.class.getPackage()));
+            System.loadLibrary(jniLibName);
         }
     }
 
diff --git a/staticlibs/device/com/android/net/module/util/TimerFileDescriptor.java b/staticlibs/device/com/android/net/module/util/TimerFileDescriptor.java
new file mode 100644
index 0000000..a8c0f17
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/TimerFileDescriptor.java
@@ -0,0 +1,261 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util;
+
+import static android.os.MessageQueue.OnFileDescriptorEventListener.EVENT_ERROR;
+import static android.os.MessageQueue.OnFileDescriptorEventListener.EVENT_INPUT;
+
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.MessageQueue;
+import android.os.ParcelFileDescriptor;
+import android.util.CloseGuard;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.io.IOException;
+
+/**
+ * Represents a Timer file descriptor object used for scheduling tasks with precise delays.
+ * Compared to {@link Handler#postDelayed}, this class offers enhanced accuracy for delayed
+ * callbacks by accounting for periods when the device is in deep sleep.
+ *
+ *  <p> This class is designed for use exclusively from the handler thread.
+ *
+ * **Usage Examples:**
+ *
+ * ** Scheduling recurring tasks with the same TimerFileDescriptor **
+ *
+ * ```java
+ * // Create a TimerFileDescriptor
+ * final TimerFileDescriptor timerFd = new TimerFileDescriptor(handler);
+ *
+ * // Schedule a new task with a delay.
+ * timerFd.setDelayedTask(() -> taskToExecute(), delayTime);
+ *
+ * // Once the delay has elapsed, and the task is running, schedule another task.
+ * timerFd.setDelayedTask(() -> anotherTaskToExecute(), anotherDelayTime);
+ *
+ * // Remember to close the TimerFileDescriptor after all tasks have finished running.
+ * timerFd.close();
+ * ```
+ */
+public class TimerFileDescriptor {
+    private static final String TAG = TimerFileDescriptor.class.getSimpleName();
+    // EVENT_ERROR may be generated even if not specified, as per its javadoc.
+    private static final int FD_EVENTS = EVENT_INPUT | EVENT_ERROR;
+    private final CloseGuard mGuard = new CloseGuard();
+    @NonNull
+    private final Handler mHandler;
+    @NonNull
+    private final MessageQueue mQueue;
+    @NonNull
+    private final ParcelFileDescriptor mParcelFileDescriptor;
+    private final int mFdInt;
+    @Nullable
+    private ITask mTask;
+
+    /**
+     * An interface for defining tasks that can be executed using a {@link Handler}.
+     */
+    public interface ITask {
+        /**
+         * Executes the task using the provided {@link Handler}.
+         *
+         * @param handler The {@link Handler} to use for executing the task.
+         */
+        void post(Handler handler);
+    }
+
+    /**
+     * A task that sends a {@link Message} using a {@link Handler}.
+     */
+    public static class MessageTask implements ITask {
+        private final Message mMessage;
+
+        public MessageTask(Message message) {
+            mMessage = message;
+        }
+
+        /**
+         * Sends the {@link Message} using the provided {@link Handler}.
+         *
+         * @param handler The {@link Handler} to use for sending the message.
+         */
+        @Override
+        public void post(Handler handler) {
+            handler.sendMessage(mMessage);
+        }
+
+        /**
+         * Get scheduled message
+         */
+        public Message getMessage() {
+            return mMessage;
+        }
+    }
+
+    /**
+     * A task that posts a {@link Runnable} to a {@link Handler}.
+     */
+    public static class RunnableTask implements ITask {
+        private final Runnable mRunnable;
+
+        public RunnableTask(Runnable runnable) {
+            mRunnable = runnable;
+        }
+
+        /**
+         * Posts the {@link Runnable} to the provided {@link Handler}.
+         *
+         * @param handler The {@link Handler} to use for posting the runnable.
+         */
+        @Override
+        public void post(Handler handler) {
+            handler.post(mRunnable);
+        }
+    }
+
+    /**
+     * TimerFileDescriptor constructor
+     *
+     * Note: The constructor is currently safe to call on another thread because it only sets final
+     * members and registers the event to be called on the handler.
+     */
+    public TimerFileDescriptor(@NonNull Handler handler) {
+        mFdInt = TimerFdUtils.createTimerFileDescriptor();
+        mParcelFileDescriptor = ParcelFileDescriptor.adoptFd(mFdInt);
+        mHandler = handler;
+        mQueue = handler.getLooper().getQueue();
+        registerFdEventListener();
+
+        mGuard.open("close");
+    }
+
+    /**
+     * Set a task to be executed after a specified delay.
+     *
+     * <p> A task can only be scheduled once at a time. Cancel previous scheduled task before the
+     *     new task is scheduled.
+     *
+     * @param task the task to be executed
+     * @param delayMs the delay time in milliseconds
+     * @throws IllegalArgumentException if try to replace the current scheduled task
+     * @throws IllegalArgumentException if the delay time is less than 0
+     */
+    public void setDelayedTask(@NonNull ITask task, long delayMs) {
+        ensureRunningOnCorrectThread();
+        if (mTask != null) {
+            throw new IllegalArgumentException("task is already scheduled");
+        }
+        if (delayMs <= 0L) {
+            task.post(mHandler);
+            return;
+        }
+
+        if (TimerFdUtils.setExpirationTime(mFdInt, delayMs)) {
+            mTask = task;
+        }
+    }
+
+    /**
+     * Cancel the scheduled task.
+     */
+    public void cancelTask() {
+        ensureRunningOnCorrectThread();
+        if (mTask == null) return;
+
+        TimerFdUtils.setExpirationTime(mFdInt, 0 /* delayMs */);
+        mTask = null;
+    }
+
+    /**
+     * Check if there is a scheduled task.
+     */
+    public boolean hasDelayedTask() {
+        ensureRunningOnCorrectThread();
+        return mTask != null;
+    }
+
+    /**
+     * Close the TimerFileDescriptor. This implementation closes the underlying
+     * OS resources allocated to represent this stream.
+     */
+    public void close() {
+        ensureRunningOnCorrectThread();
+        unregisterAndDestroyFd();
+    }
+
+    private void registerFdEventListener() {
+        mQueue.addOnFileDescriptorEventListener(
+                mParcelFileDescriptor.getFileDescriptor(),
+                FD_EVENTS,
+                (fd, events) -> {
+                    if (!isRunning()) {
+                        return 0;
+                    }
+                    if ((events & EVENT_INPUT) != 0) {
+                        handleExpiration();
+                    }
+                    return FD_EVENTS;
+                });
+    }
+
+    private boolean isRunning() {
+        return mParcelFileDescriptor.getFileDescriptor().valid();
+    }
+
+    private void handleExpiration() {
+        // Execute the task
+        if (mTask != null) {
+            mTask.post(mHandler);
+            mTask = null;
+        }
+    }
+
+    private void unregisterAndDestroyFd() {
+        if (mGuard != null) {
+            mGuard.close();
+        }
+
+        mQueue.removeOnFileDescriptorEventListener(mParcelFileDescriptor.getFileDescriptor());
+        try {
+            mParcelFileDescriptor.close();
+        } catch (IOException exception) {
+            Log.e(TAG, "close ParcelFileDescriptor failed. ", exception);
+        }
+    }
+
+    private void ensureRunningOnCorrectThread() {
+        if (mHandler.getLooper() != Looper.myLooper()) {
+            throw new IllegalStateException(
+                    "Not running on Handler thread: " + Thread.currentThread().getName());
+        }
+    }
+
+    @SuppressWarnings("Finalize")
+    @Override
+    protected void finalize() throws Throwable {
+        if (mGuard != null) {
+            mGuard.warnIfOpen();
+        }
+        super.finalize();
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/netlink/NetlinkUtils.java b/staticlibs/device/com/android/net/module/util/netlink/NetlinkUtils.java
index 541a375..e2544d3 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/NetlinkUtils.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/NetlinkUtils.java
@@ -55,6 +55,7 @@
 import java.nio.ByteBuffer;
 import java.nio.ByteOrder;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.List;
 import java.util.Objects;
 import java.util.function.Consumer;
@@ -469,4 +470,31 @@
             // Nothing we can do here
         }
     }
+
+    /**
+     * Sends a netlink request to set flags for given interface
+     *
+     * @param interfaceName The name of the network interface to query.
+     * @param flags power-of-two integer flags to set or unset. A flag to set should be passed as
+     *        is as a power-of-two value, and a flag to remove should be passed inversed as -1 with
+     *        a single bit down. For example: IFF_UP, ~IFF_BROADCAST...
+     * @return true if the request finished successfully, otherwise false.
+     */
+    public static boolean setInterfaceFlags(@NonNull String interfaceName, int... flags) {
+        final RtNetlinkLinkMessage ntMsg =
+                RtNetlinkLinkMessage.createSetFlagsMessage(interfaceName, /*seqNo*/ 0, flags);
+        if (ntMsg == null) {
+            Log.e(TAG, "Failed to create message to set interface flags for interface "
+                    + interfaceName + ", input flags are: " + Arrays.toString(flags));
+            return false;
+        }
+        final byte[] msg = ntMsg.pack(ByteOrder.nativeOrder());
+        try {
+            NetlinkUtils.sendOneShotKernelMessage(NETLINK_ROUTE, msg);
+            return true;
+        } catch (ErrnoException e) {
+            Log.e(TAG, "Failed to set flags for: " + interfaceName, e);
+            return false;
+        }
+    }
 }
diff --git a/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkLinkMessage.java b/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkLinkMessage.java
index 037d95f..1afe3b8 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkLinkMessage.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkLinkMessage.java
@@ -312,6 +312,57 @@
                 DEFAULT_MTU, null, null);
     }
 
+    /**
+     * Creates an {@link RtNetlinkLinkMessage} instance that can be used to set the flags of a
+     * network interface.
+     *
+     * @param interfaceName The name of the network interface to query.
+     * @param sequenceNumber The sequence number for the Netlink message.
+     * @param flags power-of-two integer flags to set or unset. A flag to set should be passed as
+     *        is as a power-of-two value, and a flag to remove should be passed inversed as -1 with
+     *        a single bit down. For example: IFF_UP, ~IFF_BROADCAST...
+     * @return An `RtNetlinkLinkMessage` instance representing the request to query the interface.
+     */
+    @Nullable
+    public static RtNetlinkLinkMessage createSetFlagsMessage(@NonNull String interfaceName,
+            int sequenceNumber, int... flags) {
+        return createSetFlagsMessage(
+                interfaceName, sequenceNumber, new OsAccess(), flags);
+    }
+
+    @VisibleForTesting
+    @Nullable
+    protected static RtNetlinkLinkMessage createSetFlagsMessage(
+            @NonNull String interfaceName, int sequenceNumber, @NonNull OsAccess osAccess,
+            int... flags) {
+        final int interfaceIndex = osAccess.if_nametoindex(interfaceName);
+        if (interfaceIndex == OsAccess.INVALID_INTERFACE_INDEX) {
+            return null;
+        }
+
+        int flagsBits = 0;
+        int changeBits = 0;
+        for (int f : flags) {
+            if (Integer.bitCount(f) == 1) {
+                flagsBits |= f;
+                changeBits |= f;
+            } else if (Integer.bitCount(~f) == 1) {
+                flagsBits &= f;
+                changeBits |= ~f;
+            } else {
+                return null;
+            }
+        }
+        // RTM_NEWLINK is used here for create, modify, or notify changes about a internet
+        // interface, including change in administrative state. While RTM_SETLINK is used to
+        // modify an existing link rather than creating a new one.
+        return RtNetlinkLinkMessage.build(
+                new StructNlMsgHdr(/*payloadLen*/ 0, RTM_NEWLINK, NLM_F_REQUEST, sequenceNumber),
+                new StructIfinfoMsg((short) AF_UNSPEC, /*type*/ 0, interfaceIndex,
+                        flagsBits, changeBits),
+                DEFAULT_MTU, /*hardwareAddress*/ null, /*interfaceName*/ null);
+    }
+
     @Override
     public String toString() {
         return "RtNetlinkLinkMessage{ "
diff --git a/staticlibs/framework/com/android/net/module/util/CollectionUtils.java b/staticlibs/framework/com/android/net/module/util/CollectionUtils.java
index f3d8c4a..760d849 100644
--- a/staticlibs/framework/com/android/net/module/util/CollectionUtils.java
+++ b/staticlibs/framework/com/android/net/module/util/CollectionUtils.java
@@ -22,6 +22,7 @@
 import android.util.Pair;
 import android.util.SparseArray;
 
+import java.lang.reflect.Array;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
@@ -413,4 +414,68 @@
         }
         return -1;
     }
+
+    /**
+     * Concatenates multiple arrays of the same type into a single new array.
+     */
+    public static byte[] concatArrays(@NonNull byte[]... arr) {
+        int size = 0;
+        for (byte[] a : arr) {
+            size += a.length;
+        }
+        final byte[] result = new byte[size];
+        int offset = 0;
+        for (byte[] a : arr) {
+            System.arraycopy(a, 0, result, offset, a.length);
+            offset += a.length;
+        }
+        return result;
+    }
+
+    /**
+     * Concatenates multiple arrays of the same type into a single new array.
+     */
+    public static <T> T[] concatArrays(@NonNull Class<T> clazz, @NonNull T[]... arr) {
+        int size = 0;
+        for (T[] a : arr) {
+            size += a.length;
+        }
+        final T[] result = (T[]) Array.newInstance(clazz, size);
+        int offset = 0;
+        for (T[] a : arr) {
+            System.arraycopy(a, 0, result, offset, a.length);
+            offset += a.length;
+        }
+        return result;
+    }
+
+    /**
+     * Prepends the elements of a variable number of prefixes to an existing array (suffix).
+     */
+    public static byte[] prependArray(@NonNull byte[] suffix, @NonNull byte... prefixes) {
+        return concatArrays(prefixes, suffix);
+    }
+
+    /**
+     * Prepends the elements of a variable number of prefixes to an existing array (suffix).
+     */
+    public static <T> T[] prependArray(@NonNull Class<T> clazz, @NonNull T[] suffix,
+            @NonNull T... prefixes) {
+        return concatArrays(clazz, prefixes, suffix);
+    }
+
+    /**
+     * Appends the elements of a variable number of suffixes to an existing array (prefix).
+     */
+    public static byte[] appendArray(@NonNull byte[] prefix, @NonNull byte... suffixes) {
+        return concatArrays(prefix, suffixes);
+    }
+
+    /**
+     * Appends the elements of a variable number of suffixes to an existing array (prefix).
+     */
+    public static <T> T[] appendArray(@NonNull Class<T> clazz, @NonNull T[] prefix,
+            @NonNull T... suffixes) {
+        return concatArrays(clazz, prefix, suffixes);
+    }
 }
diff --git a/staticlibs/framework/com/android/net/module/util/HexDump.java b/staticlibs/framework/com/android/net/module/util/HexDump.java
index a22c258..409f611 100644
--- a/staticlibs/framework/com/android/net/module/util/HexDump.java
+++ b/staticlibs/framework/com/android/net/module/util/HexDump.java
@@ -202,7 +202,7 @@
         if (c >= 'A' && c <= 'F') return (c - 'A' + 10);
         if (c >= 'a' && c <= 'f') return (c - 'a' + 10);
 
-        throw new RuntimeException("Invalid hex char '" + c + "'");
+        throw new IllegalArgumentException("Invalid hex char '" + c + "'");
     }
 
     /**
diff --git a/staticlibs/framework/com/android/net/module/util/LruCacheWithExpiry.java b/staticlibs/framework/com/android/net/module/util/LruCacheWithExpiry.java
index 80088b9..96d995a 100644
--- a/staticlibs/framework/com/android/net/module/util/LruCacheWithExpiry.java
+++ b/staticlibs/framework/com/android/net/module/util/LruCacheWithExpiry.java
@@ -22,13 +22,13 @@
 
 import com.android.internal.annotations.GuardedBy;
 
-import java.time.Clock;
 import java.util.Objects;
+import java.util.function.LongSupplier;
 import java.util.function.Predicate;
 import java.util.function.Supplier;
 
 /**
- * An LRU cache that stores key-value pairs with an expiry time.
+ * A thread-safe LRU cache that stores key-value pairs with an expiry time.
  *
  * <p>This cache uses an {@link LruCache} to store entries and evicts the least
  * recently used entries when the cache reaches its maximum capacity. It also
@@ -41,7 +41,7 @@
  * @hide
  */
 public class LruCacheWithExpiry<K, V> {
-    private final Clock mClock;
+    private final LongSupplier mTimeSupplier;
     private final long mExpiryDurationMs;
     @GuardedBy("mMap")
     private final LruCache<K, CacheValue<V>> mMap;
@@ -50,16 +50,17 @@
     /**
      * Constructs a new {@link LruCacheWithExpiry} with the specified parameters.
      *
-     * @param clock            The {@link Clock} to use for determining timestamps.
+     * @param timeSupplier     The {@link java.util.function.LongSupplier} to use for
+     *                         determining timestamps.
      * @param expiryDurationMs The expiry duration for cached entries in milliseconds.
      * @param maxSize          The maximum number of entries to hold in the cache.
      * @param shouldCacheValue A {@link Predicate} that determines whether a given value should be
      *                         cached. This can be used to filter out certain values from being
      *                         stored in the cache.
      */
-    public LruCacheWithExpiry(@NonNull Clock clock, long expiryDurationMs, int maxSize,
-            Predicate<V> shouldCacheValue) {
-        mClock = clock;
+    public LruCacheWithExpiry(@NonNull LongSupplier timeSupplier, long expiryDurationMs,
+            int maxSize, Predicate<V> shouldCacheValue) {
+        mTimeSupplier = timeSupplier;
         mExpiryDurationMs = expiryDurationMs;
         mMap = new LruCache<>(maxSize);
         mShouldCacheValue = shouldCacheValue;
@@ -119,7 +120,26 @@
     public void put(@NonNull K key, @NonNull V value) {
         Objects.requireNonNull(value);
         synchronized (mMap) {
-            mMap.put(key, new CacheValue<>(mClock.millis(), value));
+            mMap.put(key, new CacheValue<>(mTimeSupplier.getAsLong(), value));
+        }
+    }
+
+    /**
+     * Stores a value in the cache if absent, associated with the given key.
+     *
+     * @param key   The key to associate with the value.
+     * @param value The value to store in the cache.
+     * @return The existing value associated with the key, if present; otherwise, null.
+     */
+    @Nullable
+    public V putIfAbsent(@NonNull K key, @NonNull V value) {
+        Objects.requireNonNull(value);
+        synchronized (mMap) {
+            final V existingValue = get(key);
+            if (existingValue == null) {
+                put(key, value);
+            }
+            return existingValue;
         }
     }
 
@@ -133,7 +153,7 @@
     }
 
     private boolean isExpired(long timestamp) {
-        return mClock.millis() > timestamp + mExpiryDurationMs;
+        return mTimeSupplier.getAsLong() > timestamp + mExpiryDurationMs;
     }
 
     private static class CacheValue<V> {
diff --git a/staticlibs/framework/com/android/net/module/util/NetworkStackConstants.java b/staticlibs/framework/com/android/net/module/util/NetworkStackConstants.java
index 5d588cc..4878334 100644
--- a/staticlibs/framework/com/android/net/module/util/NetworkStackConstants.java
+++ b/staticlibs/framework/com/android/net/module/util/NetworkStackConstants.java
@@ -120,6 +120,8 @@
             (byte) 0, (byte) 0, (byte) 0, (byte) 0,
             (byte) 0, (byte) 0, (byte) 0, (byte) 0,
             (byte) 0, (byte) 0, (byte) 0, (byte) 0 });
+    public static final Inet4Address IPV4_ADDR_ALL_HOST_MULTICAST =
+            (Inet4Address) InetAddresses.parseNumericAddress("224.0.0.1");
 
     /**
      * CLAT constants
diff --git a/staticlibs/tests/unit/Android.bp b/staticlibs/tests/unit/Android.bp
index 8c54e6a..9d1d291 100644
--- a/staticlibs/tests/unit/Android.bp
+++ b/staticlibs/tests/unit/Android.bp
@@ -55,6 +55,7 @@
         // For mockito extended
         "libdexmakerjvmtiagent",
         "libstaticjvmtiagent",
+        "libcom_android_net_moduletests_util_jni",
     ],
     jarjar_rules: "jarjar-rules.txt",
     test_suites: ["device-tests"],
diff --git a/staticlibs/tests/unit/jni/Android.bp b/staticlibs/tests/unit/jni/Android.bp
new file mode 100644
index 0000000..e456471
--- /dev/null
+++ b/staticlibs/tests/unit/jni/Android.bp
@@ -0,0 +1,39 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+    default_team: "trendy_team_fwk_core_networking",
+    // See: http://go/android-license-faq
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+cc_library_shared {
+    name: "libcom_android_net_moduletests_util_jni",
+    cflags: [
+        "-Wall",
+        "-Werror",
+        "-Wno-unused-parameter",
+        "-Wthread-safety",
+    ],
+    srcs: [
+        "com_android_net_moduletests_util/onload.cpp",
+    ],
+    static_libs: [
+        "libnet_utils_device_common_timerfdjni",
+    ],
+    shared_libs: [
+        "liblog",
+        "libnativehelper",
+    ],
+}
diff --git a/staticlibs/tests/unit/jni/com_android_net_moduletests_util/onload.cpp b/staticlibs/tests/unit/jni/com_android_net_moduletests_util/onload.cpp
new file mode 100644
index 0000000..a035540
--- /dev/null
+++ b/staticlibs/tests/unit/jni/com_android_net_moduletests_util/onload.cpp
@@ -0,0 +1,42 @@
+/*
+ * 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.
+ */
+
+#include "jni.h"
+#include <nativehelper/JNIHelp.h>
+
+#define LOG_TAG "NetworkStaticLibTestsJni"
+#include <android/log.h>
+
+namespace android {
+
+int register_com_android_net_module_util_TimerFdUtils(JNIEnv *env,
+                                                      char const *class_name);
+
+extern "C" jint JNI_OnLoad(JavaVM *vm, void *) {
+  JNIEnv *env;
+  if (vm->GetEnv(reinterpret_cast<void **>(&env), JNI_VERSION_1_6) != JNI_OK) {
+    __android_log_print(ANDROID_LOG_FATAL, LOG_TAG, "ERROR: GetEnv failed");
+    return JNI_ERR;
+  }
+
+  if (register_com_android_net_module_util_TimerFdUtils(
+          env, "com/android/net/moduletests/util/TimerFdUtils") < 0)
+    return JNI_ERR;
+
+  return JNI_VERSION_1_6;
+}
+
+}; // namespace android
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/CollectionUtilsTest.kt b/staticlibs/tests/unit/src/com/android/net/module/util/CollectionUtilsTest.kt
index 7244803..1aa943e 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/CollectionUtilsTest.kt
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/CollectionUtilsTest.kt
@@ -19,7 +19,7 @@
 import android.util.SparseArray
 import androidx.test.filters.SmallTest
 import androidx.test.runner.AndroidJUnit4
-import com.android.testutils.assertThrows
+import kotlin.test.assertContentEquals
 import org.junit.Test
 import org.junit.runner.RunWith
 import kotlin.test.assertEquals
@@ -196,4 +196,88 @@
         assertEquals(1, CollectionUtils.getIndexForValue(sparseArray, value1Copy))
         assertEquals(2, CollectionUtils.getIndexForValue(sparseArray, value2))
     }
+
+    @Test
+    fun testConcatEmptyByteArrays() {
+        assertContentEquals(
+                byteArrayOf(),
+                CollectionUtils.concatArrays(byteArrayOf(), byteArrayOf())
+        )
+    }
+
+    @Test
+    fun testConcatEmptyStringArrays() {
+        assertContentEquals(
+                arrayOf<String>(),
+                CollectionUtils.concatArrays(
+                        String::class.java,
+                        arrayOf<String>(),
+                        arrayOf<String>()
+                )
+        )
+    }
+
+    @Test
+    fun testConcatByteArrays() {
+        val byteArr1 = byteArrayOf(1, 2, 3)
+        val byteArr2 = byteArrayOf(4, 5, 6)
+        val byteArr3 = byteArrayOf()
+        val byteArrExpected = byteArrayOf(1, 2, 3, 4, 5, 6)
+        assertContentEquals(
+                byteArrExpected,
+                CollectionUtils.concatArrays(byteArr1, byteArr2, byteArr3)
+        )
+    }
+
+    @Test
+    fun testConcatStringArrays() {
+        val stringArr1 = arrayOf("1", "2", "3")
+        val stringArr2 = arrayOf("4", "5", "6")
+        val strinvArr3 = arrayOf<String>()
+        val stringArrExpected = arrayOf("1", "2", "3", "4", "5", "6")
+        assertContentEquals(
+                stringArrExpected,
+                CollectionUtils.concatArrays(String::class.java, stringArr1, stringArr2, strinvArr3)
+        )
+    }
+
+    @Test
+    fun testPrependByteArrays() {
+        val byteArr2 = byteArrayOf(4, 5, 6)
+        val byteArrExpected = byteArrayOf(1, 2, 3, 4, 5, 6)
+        assertContentEquals(
+                byteArrExpected,
+                CollectionUtils.prependArray(byteArr2, 1, 2, 3)
+        )
+    }
+
+    @Test
+    fun testPrependStringArrays() {
+        val stringArr2 = arrayOf("4", "5", "6")
+        val stringArrExpected = arrayOf("1", "2", "3", "4", "5", "6")
+        assertContentEquals(
+                stringArrExpected,
+                CollectionUtils.prependArray(String::class.java, stringArr2, "1", "2", "3")
+        )
+    }
+
+    @Test
+    fun testAppendByteArrays() {
+        val byteArr1 = byteArrayOf(1, 2, 3)
+        val byteArrExpected = byteArrayOf(1, 2, 3, 4, 5, 6)
+        assertContentEquals(
+                byteArrExpected,
+                CollectionUtils.appendArray(byteArr1, 4, 5, 6)
+        )
+    }
+
+    @Test
+    fun testAppendStringArrays() {
+        val stringArr1 = arrayOf("1", "2", "3")
+        val stringArrExpected = arrayOf("1", "2", "3", "4", "5", "6")
+        assertContentEquals(
+                stringArrExpected,
+                CollectionUtils.appendArray(String::class.java, stringArr1, "4", "5", "6")
+        )
+    }
 }
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/HexDumpTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/HexDumpTest.java
index 5a15585..f81978a 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/HexDumpTest.java
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/HexDumpTest.java
@@ -18,6 +18,7 @@
 
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
 
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
@@ -50,6 +51,11 @@
     }
 
     @Test
+    public void testInvalidHexStringToByteArray() {
+        assertThrows(IllegalArgumentException.class, () -> HexDump.hexStringToByteArray("abxX"));
+    }
+
+    @Test
     public void testIntegerToByteArray() {
         assertArrayEquals(new byte[]{(byte) 0xff, (byte) 0x00, (byte) 0x00, (byte) 0x04},
                 HexDump.toByteArray((int) 0xff000004));
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/LruCacheWithExpiryTest.kt b/staticlibs/tests/unit/src/com/android/net/module/util/LruCacheWithExpiryTest.kt
new file mode 100644
index 0000000..b6af892
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/LruCacheWithExpiryTest.kt
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util
+
+import com.android.testutils.DevSdkIgnoreRunner
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.util.function.LongSupplier
+
+@RunWith(DevSdkIgnoreRunner::class)
+class LruCacheWithExpiryTest {
+
+    companion object {
+        private const val CACHE_SIZE = 2
+        private const val EXPIRY_DURATION_MS = 1000L
+    }
+
+    private val timeSupplier = object : LongSupplier {
+        private var currentTimeMillis = 0L
+        override fun getAsLong(): Long = currentTimeMillis
+        fun advanceTime(millis: Long) { currentTimeMillis += millis }
+    }
+
+    private val cache = LruCacheWithExpiry<Int, String>(
+            timeSupplier, EXPIRY_DURATION_MS, CACHE_SIZE) { true }
+
+    @Test
+    fun testPutIfAbsent_keyNotPresent() {
+        val value = cache.putIfAbsent(1, "value1")
+        assertNull(value)
+        assertEquals("value1", cache.get(1))
+    }
+
+    @Test
+    fun testPutIfAbsent_keyPresent() {
+        cache.put(1, "value1")
+        val value = cache.putIfAbsent(1, "value2")
+        assertEquals("value1", value)
+        assertEquals("value1", cache.get(1))
+    }
+
+    @Test
+    fun testPutIfAbsent_keyPresentButExpired() {
+        cache.put(1, "value1")
+        // Advance time to expire the entry
+        timeSupplier.advanceTime(EXPIRY_DURATION_MS + 1)
+        val value = cache.putIfAbsent(1, "value2")
+        assertNull(value)
+        assertEquals("value2", cache.get(1))
+    }
+
+    @Test
+    fun testPutIfAbsent_maxSizeReached() {
+        cache.put(1, "value1")
+        cache.put(2, "value2")
+        cache.putIfAbsent(3, "value3") // This should evict the least recently used entry (1)
+        assertNull(cache.get(1))
+        assertEquals("value2", cache.get(2))
+        assertEquals("value3", cache.get(3))
+    }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/TimerFileDescriptorTest.kt b/staticlibs/tests/unit/src/com/android/net/module/util/TimerFileDescriptorTest.kt
new file mode 100644
index 0000000..f5e47c9
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/TimerFileDescriptorTest.kt
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util
+
+import android.os.Build
+import android.os.ConditionVariable
+import android.os.Handler
+import android.os.HandlerThread
+import android.os.Looper
+import android.os.Message
+import androidx.test.filters.SmallTest
+import com.android.net.module.util.TimerFileDescriptor.ITask
+import com.android.net.module.util.TimerFileDescriptor.MessageTask
+import com.android.net.module.util.TimerFileDescriptor.RunnableTask
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.tryTest
+import com.android.testutils.visibleOnHandlerThread
+import org.junit.After
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.time.Duration
+import java.time.Instant
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+
+private const val MSG_TEST = 1
+
+@DevSdkIgnoreRunner.MonitorThreadLeak
+@RunWith(DevSdkIgnoreRunner::class)
+@SmallTest
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+class TimerFileDescriptorTest {
+    private class TestHandler(looper: Looper) : Handler(looper) {
+        override fun handleMessage(msg: Message) {
+            val cv = msg.obj as ConditionVariable
+            cv.open()
+        }
+    }
+    private val thread = HandlerThread(TimerFileDescriptorTest::class.simpleName).apply { start() }
+    private val handler by lazy { TestHandler(thread.looper) }
+
+    @After
+    fun tearDown() {
+        thread.quitSafely()
+        thread.join()
+    }
+
+    private fun assertDelayedTaskPost(
+            timerFd: TimerFileDescriptor,
+            task: ITask,
+            cv: ConditionVariable
+    ) {
+        val delayTime = 10L
+        val startTime1 = Instant.now()
+        handler.post { timerFd.setDelayedTask(task, delayTime) }
+        assertTrue(cv.block(100L /* timeoutMs*/))
+        assertTrue(Duration.between(startTime1, Instant.now()).toMillis() >= delayTime)
+    }
+
+    @Test
+    fun testSetDelayedTask() {
+        val timerFd = TimerFileDescriptor(handler)
+        tryTest {
+            // Verify the delayed task is executed with the self-implemented ITask
+            val cv1 = ConditionVariable()
+            assertDelayedTaskPost(timerFd, { cv1.open() }, cv1)
+
+            // Verify the delayed task is executed with the RunnableTask
+            val cv2 = ConditionVariable()
+            assertDelayedTaskPost(timerFd, RunnableTask{ cv2.open() }, cv2)
+
+            // Verify the delayed task is executed with the MessageTask
+            val cv3 = ConditionVariable()
+            assertDelayedTaskPost(timerFd, MessageTask(handler.obtainMessage(MSG_TEST, cv3)), cv3)
+        } cleanup {
+            visibleOnHandlerThread(handler) { timerFd.close() }
+        }
+    }
+
+    @Test
+    fun testCancelTask() {
+        // The task is posted and canceled within the same handler loop, so the short delay used
+        // here won't cause flakes.
+        val delayTime = 10L
+        val timerFd = TimerFileDescriptor(handler)
+        val cv = ConditionVariable()
+        tryTest {
+            handler.post {
+                timerFd.setDelayedTask({ cv.open() }, delayTime)
+                assertTrue(timerFd.hasDelayedTask())
+                timerFd.cancelTask()
+                assertFalse(timerFd.hasDelayedTask())
+            }
+            assertFalse(cv.block(20L /* timeoutMs*/))
+        } cleanup {
+            visibleOnHandlerThread(handler) { timerFd.close() }
+        }
+    }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkLinkMessageTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkLinkMessageTest.java
index bd0e31d..8104e3a 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkLinkMessageTest.java
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkLinkMessageTest.java
@@ -306,6 +306,28 @@
     }
 
     @Test
+    public void testCreateSetInterfaceFlagsMessage() {
+        final String expectedHexBytes =
+                "20000000100001006824000000000000"    // struct nlmsghdr
+                        + "00000000080000000100000001000100"; // struct ifinfomsg
+        final String interfaceName = "wlan0";
+        final int interfaceIndex = 8;
+        final int sequenceNumber = 0x2468;
+
+        when(mOsAccess.if_nametoindex(interfaceName)).thenReturn(interfaceIndex);
+
+        final RtNetlinkLinkMessage msg = RtNetlinkLinkMessage.createSetFlagsMessage(
+                interfaceName,
+                sequenceNumber,
+                mOsAccess,
+                NetlinkConstants.IFF_UP,
+                ~NetlinkConstants.IFF_LOWER_UP);
+        assertNotNull(msg);
+        final byte[] bytes = msg.pack(ByteOrder.LITTLE_ENDIAN);  // For testing.
+        assertEquals(expectedHexBytes, HexDump.toHexString(bytes));
+    }
+
+    @Test
     public void testToString() {
         final ByteBuffer byteBuffer = toByteBuffer(RTM_NEWLINK_HEX);
         byteBuffer.order(ByteOrder.LITTLE_ENDIAN);  // For testing.
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/ConnectivityDiagnosticsCollector.kt b/staticlibs/testutils/devicetests/com/android/testutils/ConnectivityDiagnosticsCollector.kt
index e5b8471..0624e5f 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/ConnectivityDiagnosticsCollector.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/ConnectivityDiagnosticsCollector.kt
@@ -43,9 +43,14 @@
 import androidx.test.platform.app.InstrumentationRegistry
 import com.android.modules.utils.build.SdkLevel.isAtLeastS
 import java.io.ByteArrayOutputStream
+import java.io.CharArrayWriter
 import java.io.File
 import java.io.FileOutputStream
+import java.io.FileReader
+import java.io.OutputStream
+import java.io.OutputStreamWriter
 import java.io.PrintWriter
+import java.io.Reader
 import java.time.ZonedDateTime
 import java.time.format.DateTimeFormatter
 import java.util.concurrent.CompletableFuture
@@ -80,7 +85,38 @@
         var instance: ConnectivityDiagnosticsCollector? = null
     }
 
+    /**
+     * Indicates tcpdump should be started and written to the diagnostics file on test case failure.
+     */
+    annotation class CollectTcpdumpOnFailure
+
+    private class DumpThread(
+        // Keep a reference to the ParcelFileDescriptor otherwise GC would close it
+        private val fd: ParcelFileDescriptor,
+        private val reader: Reader
+    ) : Thread() {
+        private val writer = CharArrayWriter()
+        override fun run() {
+            reader.copyTo(writer)
+        }
+
+        fun closeAndWriteTo(output: OutputStream?) {
+            join()
+            fd.close()
+            if (output != null) {
+                val outputWriter = OutputStreamWriter(output)
+                outputWriter.write("--- tcpdump stopped at ${ZonedDateTime.now()} ---\n")
+                writer.writeTo(outputWriter)
+            }
+        }
+    }
+
+    private data class TcpdumpRun(val pid: Int, val reader: DumpThread)
+
     private var failureHeader: String? = null
+
+    // Accessed from the test listener methods which are synchronized by junit (see TestListener)
+    private var tcpdumpRun: TcpdumpRun? = null
     private val buffer = ByteArrayOutputStream()
     private val failureHeaderExtras = mutableMapOf<String, Any>()
     private val collectorDir: File by lazy {
@@ -157,7 +193,57 @@
         flushBufferToFileMetric(testData, baseFilename)
     }
 
+    override fun onTestStart(testData: DataRecord, description: Description) {
+        val tcpdumpAnn = description.annotations.firstOrNull { it is CollectTcpdumpOnFailure }
+                as? CollectTcpdumpOnFailure
+        if (tcpdumpAnn != null) {
+            startTcpdumpForTestcaseIfSupported()
+        }
+    }
+
+    private fun startTcpdumpForTestcaseIfSupported() {
+        if (!DeviceInfoUtils.isDebuggable()) {
+            Log.d(TAG, "Cannot start tcpdump, build is not debuggable")
+            return
+        }
+        if (tcpdumpRun != null) {
+            Log.e(TAG, "Cannot start tcpdump: it is already running")
+            return
+        }
+        // executeShellCommand won't tokenize quoted arguments containing spaces (like pcap filters)
+        // properly, so pass in the command in stdin instead of using sh -c 'command'
+        val fds = instrumentation.uiAutomation.executeShellCommandRw("sh")
+
+        val stdout = fds[0]
+        val stdin = fds[1]
+        ParcelFileDescriptor.AutoCloseOutputStream(stdin).use { writer ->
+            // Echo the current pid, and replace it (with exec) with the tcpdump process, so the
+            // tcpdump pid is known.
+            writer.write(
+                "echo $$; exec su 0 tcpdump -n -i any -U -xx".encodeToByteArray()
+            )
+        }
+        val reader = FileReader(stdout.fileDescriptor).buffered()
+        val tcpdumpPid = Integer.parseInt(reader.readLine())
+        val dumpThread = DumpThread(stdout, reader)
+        dumpThread.start()
+        tcpdumpRun = TcpdumpRun(tcpdumpPid, dumpThread)
+    }
+
+    private fun stopTcpdumpIfRunning(output: OutputStream?) {
+        val run = tcpdumpRun ?: return
+        // Send SIGTERM for graceful shutdown of tcpdump so that it can flush its output
+        executeCommandBlocking("su 0 kill ${run.pid}")
+        run.reader.closeAndWriteTo(output)
+        tcpdumpRun = null
+    }
+
     override fun onTestEnd(testData: DataRecord, description: Description) {
+        // onTestFail is called before onTestEnd, so if the test failed tcpdump would already have
+        // been stopped and output dumped. Here this stops tcpdump if the test succeeded, throwing
+        // away its output.
+        stopTcpdumpIfRunning(output = null)
+
         // Tests may call methods like collectDumpsysConnectivity to collect diagnostics at any time
         // during the run, for example to observe state at various points to investigate a flake
         // and compare passing/failing cases.
@@ -196,6 +282,7 @@
                 fos.write("\n".toByteArray())
             }
             fos.write(buffer.toByteArray())
+            stopTcpdumpIfRunning(fos)
         }
         failureHeader = null
         buffer.reset()
diff --git a/staticlibs/testutils/host/java/com/android/testutils/ConnectivityTestTargetPreparer.kt b/staticlibs/testutils/host/java/com/android/testutils/ConnectivityTestTargetPreparer.kt
index f6168af..a99359b 100644
--- a/staticlibs/testutils/host/java/com/android/testutils/ConnectivityTestTargetPreparer.kt
+++ b/staticlibs/testutils/host/java/com/android/testutils/ConnectivityTestTargetPreparer.kt
@@ -18,12 +18,15 @@
 
 import com.android.ddmlib.testrunner.TestResult
 import com.android.tradefed.config.Option
+import com.android.tradefed.invoker.ExecutionFiles.FilesKey
 import com.android.tradefed.invoker.TestInformation
+import com.android.tradefed.log.LogUtil.CLog
 import com.android.tradefed.result.CollectingTestListener
 import com.android.tradefed.result.ddmlib.DefaultRemoteAndroidTestRunner
 import com.android.tradefed.targetprep.BaseTargetPreparer
 import com.android.tradefed.targetprep.TargetSetupError
 import com.android.tradefed.targetprep.suite.SuiteApkInstaller
+import java.io.File
 
 private const val CONNECTIVITY_CHECKER_APK = "ConnectivityTestPreparer.apk"
 private const val CONNECTIVITY_PKG_NAME = "com.android.testutils.connectivitypreparer"
@@ -48,7 +51,41 @@
  * --test-arg com.android.testutils.ConnectivityTestTargetPreparer:ignore-wifi-check:true".
  */
 open class ConnectivityTestTargetPreparer : BaseTargetPreparer() {
-    private val installer = SuiteApkInstaller()
+    private val installer = ApkInstaller()
+
+    private class ApkInstaller : SuiteApkInstaller() {
+        override fun getLocalPathForFilename(
+            testInfo: TestInformation,
+            apkFileName: String
+        ): File {
+            if (apkFileName == CONNECTIVITY_CHECKER_APK) {
+                // For the connectivity checker APK, explicitly look for it in the directory of the
+                // host-side preparer.
+                // This preparer is part of the net-tests-utils-host-common library, which includes
+                // the checker APK via device_common_data in its build rule. Both need to be at the
+                // same version so that the preparer calls the right test methods in the checker
+                // APK.
+                // The default strategy for finding test files is to do a recursive search in test
+                // directories, which may find wrong files in wrong directories. In particular,
+                // if some MTS test includes the checker APK, and that test is linked to a module
+                // that boards the train at a version different from this target preparer, there
+                // could be a version difference between the APK and the host-side preparer.
+                // Explicitly looking for the APK in the host-side preparer directory ensures that
+                // it uses the version that was packaged together with the host-side preparer.
+                val testsDir = testInfo.executionFiles().get(FilesKey.TESTS_DIRECTORY)
+                val f = File(testsDir, "net-tests-utils-host-common/$CONNECTIVITY_CHECKER_APK")
+                if (f.isFile) {
+                    return f
+                }
+                // When running locally via atest, device_common_data does cause the APK to be put
+                // into the test temp directory, so recursive search is still used to find it in the
+                // directory of the test module that is being run. This is fine because atest runs
+                // are on local trees that do not have versioning problems.
+                CLog.i("APK not found at $f, falling back to recursive search")
+            }
+            return super.getLocalPathForFilename(testInfo, apkFileName)
+        }
+    }
 
     @Option(
         name = IGNORE_WIFI_CHECK,
@@ -179,7 +216,7 @@
                 .contains(":deny")
         }
 
-    private fun refreshTime(testInfo: TestInformation,) {
+    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
diff --git a/staticlibs/testutils/host/python/tether_utils.py b/staticlibs/testutils/host/python/tether_utils.py
index 702b596..c63de1f 100644
--- a/staticlibs/testutils/host/python/tether_utils.py
+++ b/staticlibs/testutils/host/python/tether_utils.py
@@ -108,3 +108,6 @@
     server.unregisterAll()
   # Teardown the hotspot.
   server.stopAllTethering()
+  # Some test cases would disable wifi, e.g. cellular upstream tests.
+  # Reconnect to it if feasible.
+  server.reconnectWifiIfSupported()
diff --git a/tests/common/java/android/net/CaptivePortalTest.java b/tests/common/java/android/net/CaptivePortalTest.java
index 15d3398..6655827 100644
--- a/tests/common/java/android/net/CaptivePortalTest.java
+++ b/tests/common/java/android/net/CaptivePortalTest.java
@@ -19,6 +19,7 @@
 import static org.junit.Assert.assertEquals;
 
 import android.os.Build;
+import android.os.IBinder;
 import android.os.RemoteException;
 
 import androidx.test.filters.SmallTest;
@@ -55,6 +56,10 @@
             mCode = request;
         }
 
+        @Override
+        public void setDelegateUid(int uid, IBinder binder, IIntResultListener listener) {
+        }
+
         // This is only @Override on R-
         public void logEvent(int eventId, String packageName) throws RemoteException {
             mCode = eventId;
diff --git a/tests/common/java/android/net/NetworkAgentConfigTest.kt b/tests/common/java/android/net/NetworkAgentConfigTest.kt
index d640a73..fe869f8 100644
--- a/tests/common/java/android/net/NetworkAgentConfigTest.kt
+++ b/tests/common/java/android/net/NetworkAgentConfigTest.kt
@@ -20,6 +20,7 @@
 import androidx.test.runner.AndroidJUnit4
 import com.android.modules.utils.build.SdkLevel.isAtLeastS
 import com.android.modules.utils.build.SdkLevel.isAtLeastT
+import com.android.modules.utils.build.SdkLevel.isAtLeastV
 import com.android.testutils.ConnectivityModuleTest
 import com.android.testutils.assertParcelingIsLossless
 import org.junit.Assert.assertEquals
@@ -47,6 +48,9 @@
                 setLocalRoutesExcludedForVpn(true)
                 setVpnRequiresValidation(true)
             }
+            if (isAtLeastV()) {
+                setSkipNativeNetworkCreation(true)
+            }
         }.build()
         assertParcelingIsLossless(config)
     }
@@ -71,6 +75,9 @@
                 setLocalRoutesExcludedForVpn(true)
                 setVpnRequiresValidation(true)
             }
+            if (isAtLeastV()) {
+                setSkipNativeNetworkCreation(true)
+            }
         }.build()
 
         assertTrue(config.isExplicitlySelected())
@@ -79,6 +86,9 @@
         assertFalse(config.isPartialConnectivityAcceptable())
         assertTrue(config.isUnvalidatedConnectivityAcceptable())
         assertEquals("TEST_NETWORK", config.getLegacyTypeName())
+        if (isAtLeastV()) {
+            assertTrue(config.shouldSkipNativeNetworkCreation())
+        }
         if (isAtLeastT()) {
             assertTrue(config.areLocalRoutesExcludedForVpn())
             assertTrue(config.isVpnValidationRequired())
diff --git a/tests/common/java/android/net/NetworkCapabilitiesTest.java b/tests/common/java/android/net/NetworkCapabilitiesTest.java
index 0f0e2f1..d694637 100644
--- a/tests/common/java/android/net/NetworkCapabilitiesTest.java
+++ b/tests/common/java/android/net/NetworkCapabilitiesTest.java
@@ -382,6 +382,7 @@
             netCap.setAllowedUids(allowedUids);
             netCap.setSubscriptionIds(Set.of(TEST_SUBID1, TEST_SUBID2));
             netCap.setUids(uids);
+            netCap.setReservationId(42);
         }
 
         netCap.setOwnerUid(123);
@@ -1493,4 +1494,42 @@
         // nc1 and nc2 are the same since invalid capability is ignored
         assertEquals(nc1, nc2);
     }
+
+    @Test
+    public void testReservationIdMatching() {
+        final NetworkCapabilities requestNc = new NetworkCapabilities();
+        final NetworkCapabilities reservationNc = new NetworkCapabilities();
+        reservationNc.setReservationId(42);
+
+        final NetworkCapabilities reservedNetworkNc = new NetworkCapabilities(reservationNc);
+        final NetworkCapabilities otherNetworkNc = new NetworkCapabilities();
+        final NetworkCapabilities otherReservedNetworkNc = new NetworkCapabilities();
+        otherReservedNetworkNc.setReservationId(99);
+        final NetworkCapabilities offerNc = new NetworkCapabilities();
+        offerNc.setReservationId(NetworkCapabilities.RES_ID_MATCH_ALL_RESERVATIONS);
+
+        // A regular request can match any network or offer except one with MATCH_ALL_RESERVATIONS
+        assertTrue(requestNc.satisfiedByNetworkCapabilities(reservedNetworkNc));
+        assertTrue(requestNc.satisfiedByNetworkCapabilities(otherNetworkNc));
+        assertTrue(requestNc.satisfiedByNetworkCapabilities(otherReservedNetworkNc));
+        assertFalse(requestNc.satisfiedByNetworkCapabilities(offerNc));
+
+        // A reservation request can only match the reservedNetwork and the blanket offer.
+        assertTrue(reservationNc.satisfiedByNetworkCapabilities(reservedNetworkNc));
+        assertFalse(reservationNc.satisfiedByNetworkCapabilities(otherNetworkNc));
+        assertFalse(reservationNc.satisfiedByNetworkCapabilities(otherReservedNetworkNc));
+        assertTrue(reservationNc.satisfiedByNetworkCapabilities(offerNc));
+    }
+
+    @Test @IgnoreUpTo(SC_V2)
+    public void testReservationIdEquals() {
+        final NetworkCapabilities nc = new NetworkCapabilities();
+        nc.setReservationId(42);
+        final NetworkCapabilities other = new NetworkCapabilities(nc);
+
+        assertEquals(nc, other);
+
+        nc.setReservationId(43);
+        assertNotEquals(nc, other);
+    }
 }
diff --git a/tests/cts/multidevices/snippet/ConnectivityMultiDevicesSnippet.kt b/tests/cts/multidevices/snippet/ConnectivityMultiDevicesSnippet.kt
index 49688cc..6da7e9a 100644
--- a/tests/cts/multidevices/snippet/ConnectivityMultiDevicesSnippet.kt
+++ b/tests/cts/multidevices/snippet/ConnectivityMultiDevicesSnippet.kt
@@ -89,6 +89,11 @@
         ctsNetUtils.expectNetworkIsSystemDefault(network)
     }
 
+    @Rpc(description = "Reconnect to wifi if supported.")
+    fun reconnectWifiIfSupported() {
+        ctsNetUtils.reconnectWifiIfSupported()
+    }
+
     @Rpc(description = "Unregister all connections.")
     fun unregisterAll() {
         cbHelper.unregisterAll()
diff --git a/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt b/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt
index 9be579b..5b2c9f7 100644
--- a/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt
+++ b/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt
@@ -77,6 +77,7 @@
 import com.android.testutils.assertThrows
 import com.android.testutils.runAsShell
 import com.android.testutils.waitForIdle
+import com.google.common.truth.Truth.assertThat
 import java.io.IOException
 import java.net.Inet6Address
 import java.net.Socket
@@ -1089,4 +1090,24 @@
         setEthernetEnabled(true)
         listener.eventuallyExpect(iface, STATE_LINK_UP, ROLE_CLIENT)
     }
+
+    @Test
+    fun testGetInterfaceList_disableEnableEthernet() {
+        // Test that interface list can be obtained when ethernet is disabled.
+        setEthernetEnabled(false)
+        // Create two test interfaces and check the return list contains the interface names.
+        val iface1 = createInterface()
+        val iface2 = createInterface()
+        var ifaces = em.getInterfaceList()
+        assertThat(ifaces).containsAtLeast(iface1.name, iface2.name)
+
+        // Remove one existing test interface and check the return list doesn't contain the
+        // removed interface name.
+        removeInterface(iface1)
+        ifaces = em.getInterfaceList()
+        assertThat(ifaces).doesNotContain(iface1.name)
+        assertThat(ifaces).contains(iface2.name)
+
+        removeInterface(iface2)
+    }
 }
diff --git a/tests/cts/net/src/android/net/cts/NetworkRequestTest.java b/tests/cts/net/src/android/net/cts/NetworkRequestTest.java
index ff10e1a..2226f4c 100644
--- a/tests/cts/net/src/android/net/cts/NetworkRequestTest.java
+++ b/tests/cts/net/src/android/net/cts/NetworkRequestTest.java
@@ -16,6 +16,7 @@
 
 package android.net.cts;
 
+import static android.net.ConnectivityManager.TYPE_NONE;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_DUN;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_FOTA;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
@@ -130,7 +131,7 @@
         verifyNoCapabilities(builder.build());
     }
 
-    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+    @Test
     public void testTemporarilyNotMeteredCapability() {
         assertTrue(new NetworkRequest.Builder()
                 .addCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED).build()
@@ -157,7 +158,6 @@
     }
 
     @Test
-    @IgnoreUpTo(Build.VERSION_CODES.Q)
     public void testSpecifier() {
         assertNull(new NetworkRequest.Builder().build().getNetworkSpecifier());
         final WifiNetworkSpecifier specifier = new WifiNetworkSpecifier.Builder()
@@ -192,7 +192,6 @@
     }
 
     @Test
-    @IgnoreUpTo(Build.VERSION_CODES.Q)
     public void testRequestorPackageName() {
         assertNull(new NetworkRequest.Builder().build().getRequestorPackageName());
         final String pkgName = "android.net.test";
@@ -216,7 +215,6 @@
     }
 
     @Test
-    @IgnoreUpTo(Build.VERSION_CODES.Q)
     public void testCanBeSatisfiedBy() {
         final LocalNetworkSpecifier specifier1 = new LocalNetworkSpecifier(1234 /* id */);
         final LocalNetworkSpecifier specifier2 = new LocalNetworkSpecifier(5678 /* id */);
@@ -284,7 +282,6 @@
     }
 
     @Test
-    @IgnoreUpTo(Build.VERSION_CODES.Q)
     public void testInvariantInCanBeSatisfiedBy() {
         // Test invariant that result of NetworkRequest.canBeSatisfiedBy() should be the same with
         // NetworkCapabilities.satisfiedByNetworkCapabilities().
@@ -388,7 +385,7 @@
                 otherUidsRequest.canBeSatisfiedBy(ncWithOtherUid));
     }
 
-    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+    @Test
     public void testRequestorUid() {
         final NetworkCapabilities nc = new NetworkCapabilities();
         // Verify default value is INVALID_UID
@@ -558,4 +555,32 @@
                 .setBssidPattern(ARBITRARY_ADDRESS, ARBITRARY_ADDRESS)
                 .build();
     }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.R)
+    public void testNetworkReservation() {
+        final NetworkCapabilities nc = new NetworkCapabilities();
+        final NetworkCapabilities blanketOffer = new NetworkCapabilities(nc);
+        blanketOffer.setReservationId(NetworkCapabilities.RES_ID_MATCH_ALL_RESERVATIONS);
+        final NetworkCapabilities specificOffer = new NetworkCapabilities(nc);
+        specificOffer.setReservationId(42);
+        final NetworkCapabilities otherSpecificOffer = new NetworkCapabilities(nc);
+        otherSpecificOffer.setReservationId(43);
+        final NetworkCapabilities regularOffer = new NetworkCapabilities(nc);
+
+        final NetworkRequest reservationNR = new NetworkRequest(new NetworkCapabilities(nc),
+                TYPE_NONE, 42 /* rId */, NetworkRequest.Type.RESERVATION);
+        final NetworkRequest requestNR = new NetworkRequest(new NetworkCapabilities(nc),
+                TYPE_NONE, 42 /* rId */, NetworkRequest.Type.REQUEST);
+
+        assertTrue(reservationNR.canBeSatisfiedBy(blanketOffer));
+        assertTrue(reservationNR.canBeSatisfiedBy(specificOffer));
+        assertFalse(reservationNR.canBeSatisfiedBy(otherSpecificOffer));
+        assertFalse(reservationNR.canBeSatisfiedBy(regularOffer));
+
+        assertFalse(requestNR.canBeSatisfiedBy(blanketOffer));
+        assertTrue(requestNR.canBeSatisfiedBy(specificOffer));
+        assertTrue(requestNR.canBeSatisfiedBy(otherSpecificOffer));
+        assertTrue(requestNR.canBeSatisfiedBy(regularOffer));
+    }
 }
diff --git a/tests/cts/net/src/android/net/cts/NetworkStatsManagerTest.java b/tests/cts/net/src/android/net/cts/NetworkStatsManagerTest.java
index e3d7240..005f6ad 100644
--- a/tests/cts/net/src/android/net/cts/NetworkStatsManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/NetworkStatsManagerTest.java
@@ -75,6 +75,7 @@
 import com.android.compatibility.common.util.SystemUtil;
 import com.android.modules.utils.build.SdkLevel;
 import com.android.testutils.AutoReleaseNetworkCallbackRule;
+import com.android.testutils.ConnectivityDiagnosticsCollector;
 import com.android.testutils.ConnectivityModuleTest;
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRunner;
@@ -759,6 +760,7 @@
                 bucket.getRxBytes(), bucket.getTxBytes()));
     }
 
+    @ConnectivityDiagnosticsCollector.CollectTcpdumpOnFailure
     @Test
     public void testUidTagStateDetails() throws Exception {
         for (int i = 0; i < mNetworkInterfacesToTest.length; ++i) {
diff --git a/tests/cts/netpermission/updatestatspermission/AndroidTest.xml b/tests/cts/netpermission/updatestatspermission/AndroidTest.xml
index fb6c814..82994c4 100644
--- a/tests/cts/netpermission/updatestatspermission/AndroidTest.xml
+++ b/tests/cts/netpermission/updatestatspermission/AndroidTest.xml
@@ -20,6 +20,7 @@
     <option name="config-descriptor:metadata" key="parameter" value="instant_app" />
     <option name="config-descriptor:metadata" key="parameter" value="not_multi_abi" />
     <option name="config-descriptor:metadata" key="parameter" value="secondary_user" />
+    <option name="config-descriptor:metadata" key="parameter" value="secondary_user_on_secondary_display" />
     <option name="not-shardable" value="true" />
     <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
         <option name="cleanup-apks" value="true" />
diff --git a/tests/unit/java/android/net/TrafficStatsTest.kt b/tests/unit/java/android/net/TrafficStatsTest.kt
new file mode 100644
index 0000000..0f85daf
--- /dev/null
+++ b/tests/unit/java/android/net/TrafficStatsTest.kt
@@ -0,0 +1,251 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net
+
+import android.net.TrafficStats.UNSUPPORTED
+import android.net.netstats.StatsResult
+import android.net.netstats.TrafficStatsRateLimitCacheConfig
+import android.os.Build
+import com.android.server.net.NetworkStatsService.TRAFFICSTATS_CLIENT_RATE_LIMIT_CACHE_ENABLED_FLAG
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.com.android.testutils.SetFeatureFlagsRule
+import com.android.testutils.com.android.testutils.SetFeatureFlagsRule.FeatureFlag
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.clearInvocations
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import java.util.HashMap
+import java.util.function.LongSupplier
+
+const val TEST_EXPIRY_DURATION_MS = 1000
+const val TEST_IFACE = "wlan0"
+
+@RunWith(DevSdkIgnoreRunner::class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
+class TrafficStatsTest {
+    private val binder = mock(INetworkStatsService::class.java)
+    private val myUid = android.os.Process.myUid()
+    private val mockMyUidStatsResult = StatsResult(5L, 6L, 7L, 8L)
+    private val mockIfaceStatsResult = StatsResult(7L, 3L, 10L, 21L)
+    private val mockTotalStatsResult = StatsResult(8L, 1L, 5L, 2L)
+    private val secondUidStatsResult = StatsResult(3L, 7L, 10L, 5L)
+    private val secondIfaceStatsResult = StatsResult(9L, 8L, 7L, 6L)
+    private val secondTotalStatsResult = StatsResult(4L, 3L, 2L, 1L)
+    private val emptyStatsResult = StatsResult(0L, 0L, 0L, 0L)
+    private val unsupportedStatsResult =
+            StatsResult(UNSUPPORTED.toLong(), UNSUPPORTED.toLong(),
+                    UNSUPPORTED.toLong(), UNSUPPORTED.toLong())
+
+    private val cacheDisabledConfig = TrafficStatsRateLimitCacheConfig.Builder()
+            .setIsCacheEnabled(false)
+            .setExpiryDurationMs(0)
+            .setMaxEntries(0)
+            .build()
+    private val cacheEnabledConfig = TrafficStatsRateLimitCacheConfig.Builder()
+            .setIsCacheEnabled(true)
+            .setExpiryDurationMs(TEST_EXPIRY_DURATION_MS)
+            .setMaxEntries(100)
+            .build()
+    private val mTestTimeSupplier = TestTimeSupplier()
+
+    private val featureFlags = HashMap<String, Boolean>()
+
+    // This will set feature flags from @FeatureFlag annotations
+    // into the map before setUp() runs.
+    @get:Rule
+    val setFeatureFlagsRule = SetFeatureFlagsRule(
+            { name, enabled -> featureFlags.put(name, enabled == true) },
+            { name -> featureFlags.getOrDefault(name, false) }
+    )
+
+    class TestTimeSupplier : LongSupplier {
+        private var currentTimeMillis = 0L
+
+        override fun getAsLong() = currentTimeMillis
+
+        fun advanceTime(millis: Int) {
+            currentTimeMillis += millis
+        }
+    }
+
+    @Before
+    fun setUp() {
+        TrafficStats.setServiceForTest(binder)
+        TrafficStats.setTimeSupplierForTest(mTestTimeSupplier)
+        mockStats(mockMyUidStatsResult, mockIfaceStatsResult, mockTotalStatsResult)
+        if (featureFlags.getOrDefault(TRAFFICSTATS_CLIENT_RATE_LIMIT_CACHE_ENABLED_FLAG, false)) {
+            doReturn(cacheEnabledConfig).`when`(binder).getRateLimitCacheConfig()
+        } else {
+            doReturn(cacheDisabledConfig).`when`(binder).getRateLimitCacheConfig()
+        }
+        TrafficStats.reinitRateLimitCacheForTest()
+    }
+
+    @After
+    fun tearDown() {
+        TrafficStats.setServiceForTest(null)
+        TrafficStats.setTimeSupplierForTest(null)
+        TrafficStats.reinitRateLimitCacheForTest()
+    }
+
+    private fun assertUidStats(uid: Int, stats: StatsResult) {
+        assertEquals(stats.rxBytes, TrafficStats.getUidRxBytes(uid))
+        assertEquals(stats.rxPackets, TrafficStats.getUidRxPackets(uid))
+        assertEquals(stats.txBytes, TrafficStats.getUidTxBytes(uid))
+        assertEquals(stats.txPackets, TrafficStats.getUidTxPackets(uid))
+    }
+
+    private fun assertIfaceStats(iface: String, stats: StatsResult) {
+        assertEquals(stats.rxBytes, TrafficStats.getRxBytes(iface))
+        assertEquals(stats.rxPackets, TrafficStats.getRxPackets(iface))
+        assertEquals(stats.txBytes, TrafficStats.getTxBytes(iface))
+        assertEquals(stats.txPackets, TrafficStats.getTxPackets(iface))
+    }
+
+    private fun assertTotalStats(stats: StatsResult) {
+        assertEquals(stats.rxBytes, TrafficStats.getTotalRxBytes())
+        assertEquals(stats.rxPackets, TrafficStats.getTotalRxPackets())
+        assertEquals(stats.txBytes, TrafficStats.getTotalTxBytes())
+        assertEquals(stats.txPackets, TrafficStats.getTotalTxPackets())
+    }
+
+    private fun mockStats(uidStats: StatsResult?, ifaceStats: StatsResult?,
+                          totalStats: StatsResult?) {
+        doReturn(uidStats).`when`(binder).getUidStats(myUid)
+        doReturn(ifaceStats).`when`(binder).getIfaceStats(TEST_IFACE)
+        doReturn(totalStats).`when`(binder).getTotalStats()
+    }
+
+    private fun assertStats(uidStats: StatsResult, ifaceStats: StatsResult,
+                            totalStats: StatsResult) {
+        assertUidStats(myUid, uidStats)
+        assertIfaceStats(TEST_IFACE, ifaceStats)
+        assertTotalStats(totalStats)
+    }
+
+    private fun assertStatsFetchInvocations(wantedInvocations: Int) {
+        verify(binder, times(wantedInvocations)).getUidStats(myUid)
+        verify(binder, times(wantedInvocations)).getIfaceStats(TEST_IFACE)
+        verify(binder, times(wantedInvocations)).getTotalStats()
+    }
+
+    @FeatureFlag(name = TRAFFICSTATS_CLIENT_RATE_LIMIT_CACHE_ENABLED_FLAG)
+    @Test
+    fun testRateLimitCacheExpiry_cacheEnabled() {
+        // Initial fetch, verify binder calls.
+        assertStats(mockMyUidStatsResult, mockIfaceStatsResult, mockTotalStatsResult)
+        assertStatsFetchInvocations(1)
+
+        // Advance time within expiry, verify cached values used.
+        clearInvocations(binder)
+        mockStats(secondUidStatsResult, secondIfaceStatsResult, secondTotalStatsResult)
+        mTestTimeSupplier.advanceTime(1)
+        assertStats(mockMyUidStatsResult, mockIfaceStatsResult, mockTotalStatsResult)
+        assertStatsFetchInvocations(0)
+
+        // Advance time to expire cache, verify new values fetched.
+        clearInvocations(binder)
+        mTestTimeSupplier.advanceTime(TEST_EXPIRY_DURATION_MS)
+        assertStats(secondUidStatsResult, secondIfaceStatsResult, secondTotalStatsResult)
+        assertStatsFetchInvocations(1)
+    }
+
+    @FeatureFlag(name = TRAFFICSTATS_CLIENT_RATE_LIMIT_CACHE_ENABLED_FLAG, enabled = false)
+    @Test
+    fun testRateLimitCacheExpiry_cacheDisabled() {
+        // Initial fetch, verify binder calls.
+        assertStats(mockMyUidStatsResult, mockIfaceStatsResult, mockTotalStatsResult)
+        assertStatsFetchInvocations(4)
+
+        // Advance time within expiry, verify new values fetched.
+        clearInvocations(binder)
+        mockStats(secondUidStatsResult, secondIfaceStatsResult, secondTotalStatsResult)
+        mTestTimeSupplier.advanceTime(1)
+        assertStats(secondUidStatsResult, secondIfaceStatsResult, secondTotalStatsResult)
+        assertStatsFetchInvocations(4)
+    }
+
+    @FeatureFlag(name = TRAFFICSTATS_CLIENT_RATE_LIMIT_CACHE_ENABLED_FLAG)
+    @Test
+    fun testInvalidStatsNotCached_cacheEnabled() {
+        doTestInvalidStatsNotCached()
+    }
+
+    @FeatureFlag(name = TRAFFICSTATS_CLIENT_RATE_LIMIT_CACHE_ENABLED_FLAG, enabled = false)
+    @Test
+    fun testInvalidStatsNotCached_cacheDisabled() {
+        doTestInvalidStatsNotCached()
+    }
+
+    private fun doTestInvalidStatsNotCached() {
+        // Mock null stats, this usually happens when the query is not valid,
+        // e.g. query uid stats of other application.
+        mockStats(null, null, null)
+        assertStats(unsupportedStatsResult, unsupportedStatsResult, unsupportedStatsResult)
+        assertStatsFetchInvocations(4)
+
+        // Verify null stats is not cached, and mock empty stats. This usually
+        // happens when queries with non-existent interface names.
+        clearInvocations(binder)
+        mockStats(emptyStatsResult, emptyStatsResult, emptyStatsResult)
+        assertStats(emptyStatsResult, emptyStatsResult, emptyStatsResult)
+        assertStatsFetchInvocations(4)
+
+        // Verify empty result is also not cached.
+        clearInvocations(binder)
+        assertStats(emptyStatsResult, emptyStatsResult, emptyStatsResult)
+        assertStatsFetchInvocations(4)
+    }
+
+    @FeatureFlag(name = TRAFFICSTATS_CLIENT_RATE_LIMIT_CACHE_ENABLED_FLAG)
+    @Test
+    fun testClearRateLimitCaches_cacheEnabled() {
+        doTestClearRateLimitCaches(true)
+    }
+
+    @FeatureFlag(name = TRAFFICSTATS_CLIENT_RATE_LIMIT_CACHE_ENABLED_FLAG, enabled = false)
+    @Test
+    fun testClearRateLimitCaches_cacheDisabled() {
+        doTestClearRateLimitCaches(false)
+    }
+
+    private fun doTestClearRateLimitCaches(cacheEnabled: Boolean) {
+        // Initial fetch, verify binder calls.
+        assertStats(mockMyUidStatsResult, mockIfaceStatsResult, mockTotalStatsResult)
+        assertStatsFetchInvocations(if (cacheEnabled) 1 else 4)
+
+        // Verify cached values are used.
+        clearInvocations(binder)
+        assertStats(mockMyUidStatsResult, mockIfaceStatsResult, mockTotalStatsResult)
+        assertStatsFetchInvocations(if (cacheEnabled) 0 else 4)
+
+        // Clear caches, verify fetching from the service.
+        clearInvocations(binder)
+        TrafficStats.clearRateLimitCaches()
+        mockStats(secondUidStatsResult, secondIfaceStatsResult, secondTotalStatsResult)
+        assertStats(secondUidStatsResult, secondIfaceStatsResult, secondTotalStatsResult)
+        assertStatsFetchInvocations(if (cacheEnabled) 1 else 4)
+    }
+}
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java
index da0bc88..ed95e4b 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java
@@ -60,6 +60,7 @@
 
 import com.android.net.module.util.CollectionUtils;
 import com.android.net.module.util.SharedLog;
+import com.android.net.module.util.TimerFileDescriptor;
 import com.android.server.connectivity.mdns.MdnsServiceInfo.TextEntry;
 import com.android.server.connectivity.mdns.util.MdnsUtils;
 import com.android.testutils.DevSdkIgnoreRule;
@@ -127,6 +128,8 @@
     private SharedLog mockSharedLog;
     @Mock
     private MdnsServiceTypeClient.Dependencies mockDeps;
+    @Mock
+    private TimerFileDescriptor mockTimerFd;
     @Captor
     private ArgumentCaptor<MdnsServiceInfo> serviceInfoCaptor;
 
@@ -145,6 +148,7 @@
     private Message delayMessage = null;
     private Handler realHandler = null;
     private MdnsFeatureFlags featureFlags = MdnsFeatureFlags.newBuilder().build();
+    private TimerFileDescriptor.MessageTask task = null;
 
     @Before
     @SuppressWarnings("DoNotMock")
@@ -244,10 +248,21 @@
             return true;
         }).when(mockDeps).sendMessage(any(Handler.class), any(Message.class));
 
-        client = makeMdnsServiceTypeClient();
+        doAnswer(inv -> {
+            realHandler = (Handler) inv.getArguments()[0];
+            return mockTimerFd;
+        }).when(mockDeps).createTimerFd(any(Handler.class));
+
+        doAnswer(inv -> {
+            task = (TimerFileDescriptor.MessageTask) inv.getArguments()[0];
+            latestDelayMs = (long) inv.getArguments()[1];
+            return null;
+        }).when(mockTimerFd).setDelayedTask(any(), anyLong());
+
+        client = makeMdnsServiceTypeClient(featureFlags);
     }
 
-    private MdnsServiceTypeClient makeMdnsServiceTypeClient() {
+    private MdnsServiceTypeClient makeMdnsServiceTypeClient(MdnsFeatureFlags featureFlags) {
         return new MdnsServiceTypeClient(SERVICE_TYPE, mockSocketClient, currentThreadExecutor,
                 mockDecoderClock, socketKey, mockSharedLog, thread.getLooper(), mockDeps,
                 serviceCache, featureFlags);
@@ -1926,9 +1941,7 @@
 
     @Test
     public void testSendQueryWithKnownAnswers() throws Exception {
-        client = new MdnsServiceTypeClient(SERVICE_TYPE, mockSocketClient, currentThreadExecutor,
-                mockDecoderClock, socketKey, mockSharedLog, thread.getLooper(), mockDeps,
-                serviceCache,
+        client = makeMdnsServiceTypeClient(
                 MdnsFeatureFlags.newBuilder().setIsQueryWithKnownAnswerEnabled(true).build());
 
         doCallRealMethod().when(mockDeps).getDatagramPacketsFromMdnsPacket(
@@ -1990,9 +2003,7 @@
 
     @Test
     public void testSendQueryWithSubTypeWithKnownAnswers() throws Exception {
-        client = new MdnsServiceTypeClient(SERVICE_TYPE, mockSocketClient, currentThreadExecutor,
-                mockDecoderClock, socketKey, mockSharedLog, thread.getLooper(), mockDeps,
-                serviceCache,
+        client = makeMdnsServiceTypeClient(
                 MdnsFeatureFlags.newBuilder().setIsQueryWithKnownAnswerEnabled(true).build());
 
         doCallRealMethod().when(mockDeps).getDatagramPacketsFromMdnsPacket(
@@ -2114,6 +2125,66 @@
         assertEquals(9680L, latestDelayMs);
     }
 
+    @Test
+    public void sendQueries_AccurateDelayCallback() {
+        client = makeMdnsServiceTypeClient(
+                MdnsFeatureFlags.newBuilder().setIsAccurateDelayCallbackEnabled(true).build());
+
+        final int numOfQueriesBeforeBackoff = 2;
+        final MdnsSearchOptions searchOptions = MdnsSearchOptions.newBuilder()
+                .addSubtype(SUBTYPE)
+                .setQueryMode(AGGRESSIVE_QUERY_MODE)
+                .setNumOfQueriesBeforeBackoff(numOfQueriesBeforeBackoff)
+                .build();
+        startSendAndReceive(mockListenerOne, searchOptions);
+        verify(mockTimerFd, times(1)).cancelTask();
+
+        // Verify that the first query has been sent.
+        verifyAndSendQuery(0 /* index */, 0 /* timeInMs */, true /* expectsUnicastResponse */,
+                true /* multipleSocketDiscovery */, 1 /* scheduledCount */,
+                1 /* sendMessageCount */, true /* useAccurateDelayCallback */);
+        // Verify that the task cancellation occurred before scheduling another query.
+        verify(mockTimerFd, times(2)).cancelTask();
+
+        // Verify that the second query has been sent
+        verifyAndSendQuery(1 /* index */, 0 /* timeInMs */, false /* expectsUnicastResponse */,
+                true /* multipleSocketDiscovery */, 2 /* scheduledCount */,
+                2 /* sendMessageCount */, true /* useAccurateDelayCallback */);
+        // Verify that the task cancellation occurred before scheduling another query.
+        verify(mockTimerFd, times(3)).cancelTask();
+
+        // Verify that the third query has been sent
+        verifyAndSendQuery(2 /* index */, TIME_BETWEEN_RETRANSMISSION_QUERIES_IN_BURST_MS,
+                false /* expectsUnicastResponse */, true /* multipleSocketDiscovery */,
+                3 /* scheduledCount */, 3 /* sendMessageCount */,
+                true /* useAccurateDelayCallback */);
+        // Verify that the task cancellation occurred before scheduling another query.
+        verify(mockTimerFd, times(4)).cancelTask();
+
+        // In backoff mode, the current scheduled task will be canceled and reschedule if the
+        // 0.8 * smallestRemainingTtl is larger than time to next run.
+        long currentTime = TEST_TTL / 2 + TEST_ELAPSED_REALTIME;
+        doReturn(currentTime).when(mockDecoderClock).elapsedRealtime();
+        doReturn(true).when(mockTimerFd).hasDelayedTask();
+        processResponse(createResponse(
+                "service-instance-1", "192.0.2.123", 5353,
+                SERVICE_TYPE_LABELS,
+                Collections.emptyMap(), TEST_TTL), socketKey);
+        // Verify that the task cancellation occurred twice.
+        verify(mockTimerFd, times(6)).cancelTask();
+        assertNotNull(task);
+        verifyAndSendQuery(3 /* index */, (long) (TEST_TTL / 2 * 0.8) /* timeInMs */,
+                true /* expectsUnicastResponse */, true /* multipleSocketDiscovery */,
+                5 /* scheduledCount */, 4 /* sendMessageCount */,
+                true /* useAccurateDelayCallback */);
+        // Verify that the task cancellation occurred before scheduling another query.
+        verify(mockTimerFd, times(7)).cancelTask();
+
+        // Stop sending packets.
+        stopSendAndReceive(mockListenerOne);
+        verify(mockTimerFd, times(8)).cancelTask();
+    }
+
     private static MdnsServiceInfo matchServiceName(String name) {
         return argThat(info -> info.getServiceInstanceName().equals(name));
     }
@@ -2127,9 +2198,22 @@
 
     private void verifyAndSendQuery(int index, long timeInMs, boolean expectsUnicastResponse,
             boolean multipleSocketDiscovery, int scheduledCount) {
-        // Dispatch the message
-        if (delayMessage != null && realHandler != null) {
-            dispatchMessage();
+        verifyAndSendQuery(index, timeInMs, expectsUnicastResponse,
+                multipleSocketDiscovery, scheduledCount, index + 1 /* sendMessageCount */,
+                false /* useAccurateDelayCallback */);
+    }
+
+    private void verifyAndSendQuery(int index, long timeInMs, boolean expectsUnicastResponse,
+            boolean multipleSocketDiscovery, int scheduledCount, int sendMessageCount,
+            boolean useAccurateDelayCallback) {
+        if (useAccurateDelayCallback && task != null && realHandler != null) {
+            runOnHandler(() -> realHandler.dispatchMessage(task.getMessage()));
+            task = null;
+        } else {
+            // Dispatch the message
+            if (delayMessage != null && realHandler != null) {
+                dispatchMessage();
+            }
         }
         assertEquals(timeInMs, latestDelayMs);
         currentThreadExecutor.getAndClearLastScheduledRunnable().run();
@@ -2152,11 +2236,15 @@
                         eq(socketKey), eq(false));
             }
         }
-        verify(mockDeps, times(index + 1))
+        verify(mockDeps, times(sendMessageCount))
                 .sendMessage(any(Handler.class), any(Message.class));
         // Verify the task has been scheduled.
-        verify(mockDeps, times(scheduledCount))
-                .sendMessageDelayed(any(Handler.class), any(Message.class), anyLong());
+        if (useAccurateDelayCallback) {
+            verify(mockTimerFd, times(scheduledCount)).setDelayedTask(any(), anyLong());
+        } else {
+            verify(mockDeps, times(scheduledCount))
+                    .sendMessageDelayed(any(Handler.class), any(Message.class), anyLong());
+        }
     }
 
     private static String[] getTestServiceName(String instanceName) {
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentCreationTests.kt b/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentCreationTests.kt
index cb98454..16a30aa 100644
--- a/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentCreationTests.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentCreationTests.kt
@@ -55,25 +55,28 @@
 @RunWith(DevSdkIgnoreRunner::class)
 @SmallTest
 @IgnoreUpTo(Build.VERSION_CODES.R)
-class CSLocalAgentCreationTests(
-        private val sdkLevel: Int,
-        private val isTv: Boolean,
-        private val addLocalNetCapToRequest: Boolean
-) : CSTest() {
+class CSLocalAgentCreationTests : CSTest() {
+    @Parameterized.Parameter(0) lateinit var params: TestParams
+
+    data class TestParams(
+            val sdkLevel: Int,
+            val isTv: Boolean = false,
+            val addLocalNetCapToRequest: Boolean = true)
+
     companion object {
         @JvmStatic
         @Parameterized.Parameters
         fun arguments() = listOf(
-                arrayOf(VERSION_V, false /* isTv */, true /* addLocalNetCapToRequest */),
-                arrayOf(VERSION_V, false /* isTv */, false /* addLocalNetCapToRequest */),
-                arrayOf(VERSION_V, true /* isTv */, true /* addLocalNetCapToRequest */),
-                arrayOf(VERSION_V, true /* isTv */, false /* addLocalNetCapToRequest */),
-                arrayOf(VERSION_U, false /* isTv */, true /* addLocalNetCapToRequest */),
-                arrayOf(VERSION_U, false /* isTv */, false /* addLocalNetCapToRequest */),
-                arrayOf(VERSION_U, true /* isTv */, true /* addLocalNetCapToRequest */),
-                arrayOf(VERSION_U, true /* isTv */, false /* addLocalNetCapToRequest */),
-                arrayOf(VERSION_T, false /* isTv */, false /* addLocalNetCapToRequest */),
-                arrayOf(VERSION_T, true /* isTv */, false /* addLocalNetCapToRequest */),
+                TestParams(VERSION_V, isTv = false, addLocalNetCapToRequest = true),
+                TestParams(VERSION_V, isTv = false, addLocalNetCapToRequest = false),
+                TestParams(VERSION_V, isTv = true, addLocalNetCapToRequest = true),
+                TestParams(VERSION_V, isTv = true, addLocalNetCapToRequest = false),
+                TestParams(VERSION_U, isTv = false, addLocalNetCapToRequest = true),
+                TestParams(VERSION_U, isTv = false, addLocalNetCapToRequest = false),
+                TestParams(VERSION_U, isTv = true, addLocalNetCapToRequest = true),
+                TestParams(VERSION_U, isTv = true, addLocalNetCapToRequest = false),
+                TestParams(VERSION_T, isTv = false, addLocalNetCapToRequest = false),
+                TestParams(VERSION_T, isTv = true, addLocalNetCapToRequest = false),
         )
     }
 
@@ -84,11 +87,11 @@
     @Test
     fun testLocalAgents() {
         val netdInOrder = inOrder(netd)
-        deps.setBuildSdk(sdkLevel)
-        doReturn(isTv).`when`(packageManager).hasSystemFeature(FEATURE_LEANBACK)
+        deps.setBuildSdk(params.sdkLevel)
+        doReturn(params.isTv).`when`(packageManager).hasSystemFeature(FEATURE_LEANBACK)
         val allNetworksCb = TestableNetworkCallback()
         val request = NetworkRequest.Builder()
-        if (addLocalNetCapToRequest) {
+        if (params.addLocalNetCapToRequest) {
             request.addCapability(NET_CAPABILITY_LOCAL_NETWORK)
         }
         cm.registerNetworkCallback(request.build(), allNetworksCb)
@@ -96,7 +99,8 @@
             addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
             addCapability(NET_CAPABILITY_LOCAL_NETWORK)
         }.build()
-        val localAgent = if (sdkLevel >= VERSION_V || sdkLevel == VERSION_U && isTv) {
+        val localAgent = if (params.sdkLevel >= VERSION_V
+                || params.sdkLevel == VERSION_U && params.isTv) {
             Agent(nc = ncTemplate, score = keepConnectedScore(), lnc = defaultLnc())
         } else {
             assertFailsWith<IllegalArgumentException> { Agent(nc = ncTemplate, lnc = defaultLnc()) }
@@ -106,7 +110,7 @@
         localAgent.connect()
         netdInOrder.verify(netd).networkCreate(
                 makeNativeNetworkConfigLocal(localAgent.network.netId, INetd.PERMISSION_NONE))
-        if (addLocalNetCapToRequest) {
+        if (params.addLocalNetCapToRequest) {
             assertEquals(localAgent.network, allNetworksCb.expect<Available>().network)
         } else {
             allNetworksCb.assertNoCallback(NO_CALLBACK_TIMEOUT_MS)
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSNetworkReservationTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSNetworkReservationTest.kt
new file mode 100644
index 0000000..a159697
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivityservice/CSNetworkReservationTest.kt
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server
+
+import android.net.ConnectivityManager
+import android.net.ConnectivityManager.NetworkCallback
+import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED
+import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
+import android.net.NetworkCapabilities.RES_ID_MATCH_ALL_RESERVATIONS
+import android.net.NetworkCapabilities.TRANSPORT_ETHERNET
+import android.net.NetworkProvider
+import android.net.NetworkRequest
+import android.net.NetworkScore
+import android.os.Build
+import android.os.Messenger
+import android.os.Process.INVALID_UID
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.TestableNetworkOfferCallback
+import org.junit.Test
+import org.junit.runner.RunWith
+
+
+private val ETHERNET_SCORE = NetworkScore.Builder().build()
+private val ETHERNET_CAPS = NetworkCapabilities.Builder()
+        .addTransportType(TRANSPORT_ETHERNET)
+        .addCapability(NET_CAPABILITY_INTERNET)
+        .addCapability(NET_CAPABILITY_NOT_CONGESTED)
+        .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+        .build()
+
+private const val TIMEOUT_MS = 5_000L
+private const val NO_CB_TIMEOUT_MS = 200L
+
+@RunWith(DevSdkIgnoreRunner::class)
+@IgnoreUpTo(Build.VERSION_CODES.R)
+class CSNetworkReservationTest : CSTest() {
+    // TODO: remove this helper once reserveNetwork is added.
+    // NetworkCallback does not currently do anything. It's just here so the API stays consistent
+    // with the eventual ConnectivityManager API.
+    private fun ConnectivityManager.reserveNetwork(req: NetworkRequest, cb: NetworkCallback) {
+        service.requestNetwork(INVALID_UID, req.networkCapabilities,
+                NetworkRequest.Type.RESERVATION.ordinal, Messenger(csHandler), 0 /* timeout */,
+                null /* binder */, ConnectivityManager.TYPE_NONE, NetworkCallback.FLAG_NONE,
+                context.packageName, context.attributionTag, NetworkCallback.DECLARED_METHODS_ALL)
+    }
+
+    fun NetworkCapabilities.copyWithReservationId(resId: Int) = NetworkCapabilities(this).also {
+        it.reservationId = resId
+    }
+
+    @Test
+    fun testReservationTriggersOnNetworkNeeded() {
+        val provider = NetworkProvider(context, csHandlerThread.looper, "Ethernet provider")
+        val blanketOfferCb = TestableNetworkOfferCallback(TIMEOUT_MS, NO_CB_TIMEOUT_MS)
+
+        cm.registerNetworkProvider(provider)
+
+        val blanketCaps = ETHERNET_CAPS.copyWithReservationId(RES_ID_MATCH_ALL_RESERVATIONS)
+        provider.registerNetworkOffer(ETHERNET_SCORE, blanketCaps, {r -> r.run()}, blanketOfferCb)
+
+        val req = NetworkRequest.Builder().addTransportType(TRANSPORT_ETHERNET).build()
+        val cb = NetworkCallback()
+        cm.reserveNetwork(req, cb)
+
+        blanketOfferCb.expectOnNetworkNeeded(blanketCaps)
+
+        // TODO: also test onNetworkUnneeded is called once ConnectivityManager supports the
+        // reserveNetwork API.
+    }
+}
diff --git a/thread/demoapp/AndroidManifest.xml b/thread/demoapp/AndroidManifest.xml
index c31bb71..fddc151 100644
--- a/thread/demoapp/AndroidManifest.xml
+++ b/thread/demoapp/AndroidManifest.xml
@@ -33,6 +33,7 @@
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
                 <category android:name="android.intent.category.LAUNCHER" />
+                <category android:name="android.intent.category.LEANBACK_LAUNCHER" />
             </intent-filter>
         </activity>
     </application>
diff --git a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
index 30d5a02..c55096b 100644
--- a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
+++ b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
@@ -351,14 +351,14 @@
         }
 
         otDaemon.initialize(
-                mTunIfController.getTunFd(),
                 shouldEnableThread(),
                 newOtDaemonConfig(mPersistentSettings.getConfiguration()),
+                mTunIfController.getTunFd(),
                 mNsdPublisher,
                 getMeshcopTxtAttributes(mResources.get()),
-                mOtDaemonCallbackProxy,
                 mCountryCodeSupplier.get(),
-                FeatureFlags.isTrelEnabled());
+                FeatureFlags.isTrelEnabled(),
+                mOtDaemonCallbackProxy);
         otDaemon.asBinder().linkToDeath(() -> mHandler.post(this::onOtDaemonDied), 0);
         mOtDaemon = otDaemon;
         mHandler.post(mNat64CidrController::maybeUpdateNat64Cidr);
diff --git a/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java b/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java
index aeeed65..875a4ad 100644
--- a/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java
+++ b/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java
@@ -16,7 +16,6 @@
 
 package android.net.thread;
 
-import static android.Manifest.permission.MANAGE_TEST_NETWORKS;
 import static android.net.InetAddresses.parseNumericAddress;
 import static android.net.thread.utils.IntegrationTestUtils.DEFAULT_DATASET;
 import static android.net.thread.utils.IntegrationTestUtils.buildIcmpv4EchoReply;
@@ -38,7 +37,6 @@
 
 import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ECHO_REPLY_TYPE;
 import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ECHO_REQUEST_TYPE;
-import static com.android.testutils.TestPermissionUtil.runAsShell;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -56,7 +54,6 @@
 import android.net.RouteInfo;
 import android.net.thread.utils.FullThreadDevice;
 import android.net.thread.utils.InfraNetworkDevice;
-import android.net.thread.utils.IntegrationTestUtils;
 import android.net.thread.utils.OtDaemonController;
 import android.net.thread.utils.TestTunNetworkUtils;
 import android.net.thread.utils.ThreadFeatureCheckerRule;
diff --git a/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.kt b/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.kt
index 07d0390..316f570 100644
--- a/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.kt
+++ b/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.kt
@@ -388,7 +388,12 @@
         raMsg ?: return pioList
 
         val buf = ByteBuffer.wrap(raMsg)
-        val ipv6Header = Struct.parse(Ipv6Header::class.java, buf)
+        val ipv6Header = try {
+            Struct.parse(Ipv6Header::class.java, buf)
+        } catch (e: IllegalArgumentException) {
+            // the packet is not IPv6
+            return pioList
+        }
         if (ipv6Header.nextHeader != OsConstants.IPPROTO_ICMPV6.toByte()) {
             return pioList
         }