Merge "Add testTetherClatBpfOffloadUdp to TetheringTest" into main
diff --git a/PREUPLOAD.cfg b/PREUPLOAD.cfg
index 83619d6..39009cb 100644
--- a/PREUPLOAD.cfg
+++ b/PREUPLOAD.cfg
@@ -1,6 +1,15 @@
+[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 -f ${PREUPLOAD_FILES}
+ktlint_hook = ${REPO_ROOT}/prebuilts/ktlint/ktlint.py --no-verify-format -f ${PREUPLOAD_FILES}
 
 hidden_api_txt_checksorted_hook = ${REPO_ROOT}/tools/platform-compat/hiddenapi/checksorted_sha.sh ${PREUPLOAD_COMMIT} ${REPO_ROOT}
diff --git a/Tethering/Android.bp b/Tethering/Android.bp
index b4426a6..d04660d 100644
--- a/Tethering/Android.bp
+++ b/Tethering/Android.bp
@@ -69,7 +69,7 @@
         "android.hardware.tetheroffload.control-V1.0-java",
         "android.hardware.tetheroffload.control-V1.1-java",
         "android.hidl.manager-V1.2-java",
-        "net-utils-tethering",
+        "net-utils-connectivity-apks",
         "netd-client",
         "tetheringstatsprotos",
     ],
diff --git a/Tethering/apex/Android.bp b/Tethering/apex/Android.bp
index 8ed5ac0..8d96066 100644
--- a/Tethering/apex/Android.bp
+++ b/Tethering/apex/Android.bp
@@ -215,6 +215,7 @@
             "android.net.nsd",
             "android.net.thread",
             "android.net.wear",
+            "android.net.http.internal",
         ],
     },
 }
diff --git a/Tethering/common/TetheringLib/Android.bp b/Tethering/common/TetheringLib/Android.bp
index 39a7540..203d828 100644
--- a/Tethering/common/TetheringLib/Android.bp
+++ b/Tethering/common/TetheringLib/Android.bp
@@ -98,6 +98,9 @@
         ":framework-tethering-srcs",
     ],
     libs: ["framework-connectivity.stubs.module_lib"],
+    static_libs: [
+        "com.android.net.flags-aconfig-java",
+    ],
     aidl: {
         include_dirs: [
             "packages/modules/Connectivity/framework/aidl-export",
diff --git a/Tethering/common/TetheringLib/src/android/net/TetheringManager.java b/Tethering/common/TetheringLib/src/android/net/TetheringManager.java
index 8b3102a..1f6011a 100644
--- a/Tethering/common/TetheringLib/src/android/net/TetheringManager.java
+++ b/Tethering/common/TetheringLib/src/android/net/TetheringManager.java
@@ -38,6 +38,7 @@
 import android.util.Log;
 
 import com.android.internal.annotations.GuardedBy;
+import com.android.net.flags.Flags;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -62,16 +63,6 @@
  */
 @SystemApi
 public class TetheringManager {
-    // TODO : remove this class when udc-mainline-prod is abandoned and android.net.flags.Flags is
-    // available here
-    /** @hide */
-    public static class Flags {
-        static final String TETHERING_REQUEST_WITH_SOFT_AP_CONFIG =
-                "com.android.net.flags.tethering_request_with_soft_ap_config";
-        static final String TETHERING_REQUEST_VIRTUAL =
-                "com.android.net.flags.tethering_request_virtual";
-    }
-
     private static final String TAG = TetheringManager.class.getSimpleName();
     private static final int DEFAULT_TIMEOUT_MS = 60_000;
     private static final long CONNECTOR_POLL_INTERVAL_MILLIS = 200L;
@@ -198,9 +189,13 @@
 
     /**
      * VIRTUAL tethering type.
+     *
+     * This tethering type is for providing external network to virtual machines
+     * running on top of Android devices, which are created and managed by
+     * AVF(Android Virtualization Framework).
      * @hide
      */
-    @FlaggedApi(Flags.TETHERING_REQUEST_VIRTUAL)
+    @FlaggedApi(Flags.FLAG_TETHERING_REQUEST_VIRTUAL)
     @SystemApi
     public static final int TETHERING_VIRTUAL = 7;
 
@@ -701,7 +696,7 @@
         /**
          * @hide
          */
-        @FlaggedApi(Flags.TETHERING_REQUEST_WITH_SOFT_AP_CONFIG)
+        @FlaggedApi(Flags.FLAG_TETHERING_REQUEST_WITH_SOFT_AP_CONFIG)
         public TetheringRequest(@NonNull final TetheringRequestParcel request) {
             mRequestParcel = request;
         }
@@ -710,7 +705,7 @@
             mRequestParcel = in.readParcelable(TetheringRequestParcel.class.getClassLoader());
         }
 
-        @FlaggedApi(Flags.TETHERING_REQUEST_WITH_SOFT_AP_CONFIG)
+        @FlaggedApi(Flags.FLAG_TETHERING_REQUEST_WITH_SOFT_AP_CONFIG)
         @NonNull
         public static final Creator<TetheringRequest> CREATOR = new Creator<>() {
             @Override
@@ -724,13 +719,13 @@
             }
         };
 
-        @FlaggedApi(Flags.TETHERING_REQUEST_WITH_SOFT_AP_CONFIG)
+        @FlaggedApi(Flags.FLAG_TETHERING_REQUEST_WITH_SOFT_AP_CONFIG)
         @Override
         public int describeContents() {
             return 0;
         }
 
-        @FlaggedApi(Flags.TETHERING_REQUEST_WITH_SOFT_AP_CONFIG)
+        @FlaggedApi(Flags.FLAG_TETHERING_REQUEST_WITH_SOFT_AP_CONFIG)
         @Override
         public void writeToParcel(@NonNull Parcel dest, int flags) {
             dest.writeParcelable(mRequestParcel, flags);
@@ -1379,6 +1374,9 @@
     @RequiresPermission(Manifest.permission.ACCESS_NETWORK_STATE)
     public void registerTetheringEventCallback(@NonNull Executor executor,
             @NonNull TetheringEventCallback callback) {
+        Objects.requireNonNull(executor);
+        Objects.requireNonNull(callback);
+
         final String callerPkg = mContext.getOpPackageName();
         Log.i(TAG, "registerTetheringEventCallback caller:" + callerPkg);
 
@@ -1533,6 +1531,8 @@
             Manifest.permission.ACCESS_NETWORK_STATE
     })
     public void unregisterTetheringEventCallback(@NonNull final TetheringEventCallback callback) {
+        Objects.requireNonNull(callback);
+
         final String callerPkg = mContext.getOpPackageName();
         Log.i(TAG, "unregisterTetheringEventCallback caller:" + callerPkg);
 
diff --git a/Tethering/src/android/net/ip/IpServer.java b/Tethering/src/android/net/ip/IpServer.java
index 5cdd6ab..b807544 100644
--- a/Tethering/src/android/net/ip/IpServer.java
+++ b/Tethering/src/android/net/ip/IpServer.java
@@ -72,7 +72,6 @@
 import com.android.net.module.util.InterfaceParams;
 import com.android.net.module.util.NetdUtils;
 import com.android.net.module.util.RoutingCoordinatorManager;
-import com.android.net.module.util.SdkUtil.LateSdk;
 import com.android.net.module.util.SharedLog;
 import com.android.net.module.util.SyncStateMachine.StateInfo;
 import com.android.net.module.util.ip.InterfaceController;
@@ -239,11 +238,8 @@
     private final INetd mNetd;
     @NonNull
     private final BpfCoordinator mBpfCoordinator;
-    // Contains null if the connectivity module is unsupported, as the routing coordinator is not
-    // available. Must use LateSdk because MessageUtils enumerates fields in this class, so it
-    // must be able to find all classes at runtime.
     @NonNull
-    private final LateSdk<RoutingCoordinatorManager> mRoutingCoordinator;
+    private final RoutingCoordinatorManager mRoutingCoordinator;
     private final Callback mCallback;
     private final InterfaceController mInterfaceCtrl;
     private final PrivateAddressCoordinator mPrivateAddressCoordinator;
@@ -301,7 +297,7 @@
     public IpServer(
             String ifaceName, Handler handler, int interfaceType, SharedLog log,
             INetd netd, @NonNull BpfCoordinator bpfCoordinator,
-            @Nullable LateSdk<RoutingCoordinatorManager> routingCoordinator, Callback callback,
+            RoutingCoordinatorManager routingCoordinatorManager, Callback callback,
             TetheringConfiguration config, PrivateAddressCoordinator addressCoordinator,
             TetheringMetrics tetheringMetrics, Dependencies deps) {
         super(ifaceName, USE_SYNC_SM ? null : handler.getLooper());
@@ -309,7 +305,7 @@
         mLog = log.forSubComponent(ifaceName);
         mNetd = netd;
         mBpfCoordinator = bpfCoordinator;
-        mRoutingCoordinator = routingCoordinator;
+        mRoutingCoordinator = routingCoordinatorManager;
         mCallback = callback;
         mInterfaceCtrl = new InterfaceController(ifaceName, mNetd, mLog);
         mIfaceName = ifaceName;
@@ -825,47 +821,25 @@
 
     private void addInterfaceToNetwork(final int netId, @NonNull final String ifaceName) {
         try {
-            if (SdkLevel.isAtLeastS() && null != mRoutingCoordinator.value) {
-                // TODO : remove this call in favor of using the LocalNetworkConfiguration
-                // correctly, which will let ConnectivityService do it automatically.
-                mRoutingCoordinator.value.addInterfaceToNetwork(netId, ifaceName);
-            } else {
-                mNetd.networkAddInterface(netId, ifaceName);
-            }
-        } catch (ServiceSpecificException | RemoteException e) {
+            // TODO : remove this call in favor of using the LocalNetworkConfiguration
+            // correctly, which will let ConnectivityService do it automatically.
+            mRoutingCoordinator.addInterfaceToNetwork(netId, ifaceName);
+        } catch (ServiceSpecificException e) {
             mLog.e("Failed to add " + mIfaceName + " to local table: ", e);
         }
     }
 
-    private void addInterfaceForward(@NonNull final String fromIface,
-            @NonNull final String toIface) throws ServiceSpecificException, RemoteException {
-        if (SdkLevel.isAtLeastS() && null != mRoutingCoordinator.value) {
-            mRoutingCoordinator.value.addInterfaceForward(fromIface, toIface);
-        } else {
-            mNetd.tetherAddForward(fromIface, toIface);
-            mNetd.ipfwdAddInterfaceForward(fromIface, toIface);
-        }
+    private void addInterfaceForward(@NonNull final String fromIface, @NonNull final String toIface)
+            throws ServiceSpecificException {
+        mRoutingCoordinator.addInterfaceForward(fromIface, toIface);
     }
 
     private void removeInterfaceForward(@NonNull final String fromIface,
             @NonNull final String toIface) {
-        if (SdkLevel.isAtLeastS() && null != mRoutingCoordinator.value) {
-            try {
-                mRoutingCoordinator.value.removeInterfaceForward(fromIface, toIface);
-            } catch (ServiceSpecificException e) {
-                mLog.e("Exception in removeInterfaceForward", e);
-            }
-        } else {
-            try {
-                mNetd.ipfwdRemoveInterfaceForward(fromIface, toIface);
-            } catch (RemoteException | ServiceSpecificException e) {
-                mLog.e("Exception in ipfwdRemoveInterfaceForward", e);
-            }
-            try {
-                mNetd.tetherRemoveForward(fromIface, toIface);
-            } catch (RemoteException | ServiceSpecificException e) {
-                mLog.e("Exception in disableNat", e);
-            }
+        try {
+            mRoutingCoordinator.removeInterfaceForward(fromIface, toIface);
+        } catch (RuntimeException e) {
+            mLog.e("Exception in removeInterfaceForward", e);
         }
     }
 
@@ -1370,7 +1344,7 @@
                         mBpfCoordinator.maybeAttachProgram(mIfaceName, ifname);
                         try {
                             addInterfaceForward(mIfaceName, ifname);
-                        } catch (RemoteException | ServiceSpecificException e) {
+                        } catch (RuntimeException e) {
                             mLog.e("Exception enabling iface forward", e);
                             cleanupUpstream();
                             mLastError = TETHER_ERROR_ENABLE_FORWARDING_ERROR;
diff --git a/Tethering/src/com/android/networkstack/tethering/Tethering.java b/Tethering/src/com/android/networkstack/tethering/Tethering.java
index 163fe24..d62f18f 100644
--- a/Tethering/src/com/android/networkstack/tethering/Tethering.java
+++ b/Tethering/src/com/android/networkstack/tethering/Tethering.java
@@ -138,7 +138,6 @@
 import com.android.net.module.util.HandlerUtils;
 import com.android.net.module.util.NetdUtils;
 import com.android.net.module.util.RoutingCoordinatorManager;
-import com.android.net.module.util.SdkUtil.LateSdk;
 import com.android.net.module.util.SharedLog;
 import com.android.networkstack.apishim.common.BluetoothPanShim;
 import com.android.networkstack.apishim.common.BluetoothPanShim.TetheredInterfaceCallbackShim;
@@ -246,10 +245,7 @@
     private final Handler mHandler;
     private final INetd mNetd;
     private final NetdCallback mNetdCallback;
-    // Contains null if the connectivity module is unsupported, as the routing coordinator is not
-    // available. Must use LateSdk because MessageUtils enumerates fields in this class, so it
-    // must be able to find all classes at runtime.
-    @NonNull private final LateSdk<RoutingCoordinatorManager> mRoutingCoordinator;
+    private final RoutingCoordinatorManager mRoutingCoordinator;
     private final UserRestrictionActionListener mTetheringRestriction;
     private final ActiveDataSubIdListener mActiveDataSubIdListener;
     private final ConnectedClientsTracker mConnectedClientsTracker;
@@ -294,11 +290,11 @@
         mLog.mark("Tethering.constructed");
         mDeps = deps;
         mContext = mDeps.getContext();
-        mNetd = mDeps.getINetd(mContext);
-        mRoutingCoordinator = mDeps.getRoutingCoordinator(mContext);
+        mNetd = mDeps.getINetd(mContext, mLog);
+        mRoutingCoordinator = mDeps.getRoutingCoordinator(mContext, mLog);
         mLooper = mDeps.makeTetheringLooper();
         mNotificationUpdater = mDeps.makeNotificationUpdater(mContext, mLooper);
-        mTetheringMetrics = mDeps.makeTetheringMetrics();
+        mTetheringMetrics = mDeps.makeTetheringMetrics(mContext);
 
         // This is intended to ensrure that if something calls startTethering(bluetooth) just after
         // bluetooth is enabled. Before onServiceConnected is called, store the calls into this
@@ -2397,9 +2393,6 @@
                 hasCallingPermission(NETWORK_SETTINGS)
                         || hasCallingPermission(PERMISSION_MAINLINE_NETWORK_STACK)
                         || hasCallingPermission(NETWORK_STACK);
-        if (callback == null) {
-            throw new NullPointerException();
-        }
         mHandler.post(() -> {
             mTetheringEventCallbacks.register(callback, new CallbackCookie(hasListPermission));
             final TetheringCallbackStartedParcel parcel = new TetheringCallbackStartedParcel();
@@ -2437,9 +2430,6 @@
 
     /** Unregister tethering event callback */
     void unregisterTetheringEventCallback(ITetheringEventCallback callback) {
-        if (callback == null) {
-            throw new NullPointerException();
-        }
         mHandler.post(() -> {
             mTetheringEventCallbacks.unregister(callback);
         });
diff --git a/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java b/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java
index 54dbf6c..5d9d349 100644
--- a/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java
+++ b/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java
@@ -36,7 +36,7 @@
 
 import com.android.modules.utils.build.SdkLevel;
 import com.android.net.module.util.RoutingCoordinatorManager;
-import com.android.net.module.util.SdkUtil.LateSdk;
+import com.android.net.module.util.RoutingCoordinatorService;
 import com.android.net.module.util.SharedLog;
 import com.android.networkstack.apishim.BluetoothPanShimImpl;
 import com.android.networkstack.apishim.common.BluetoothPanShim;
@@ -120,20 +120,26 @@
     /**
      * Get a reference to INetd to be used by tethering.
      */
-    public INetd getINetd(Context context) {
-        return INetd.Stub.asInterface(
-                (IBinder) context.getSystemService(Context.NETD_SERVICE));
+    public INetd getINetd(Context context, SharedLog log) {
+        final INetd netd =
+                INetd.Stub.asInterface((IBinder) context.getSystemService(Context.NETD_SERVICE));
+        if (netd == null) {
+            log.wtf("INetd is null");
+        }
+        return netd;
     }
 
     /**
-     * Get the routing coordinator, or null if below S.
+     * Get the routing coordinator.
      */
-    @Nullable
-    public LateSdk<RoutingCoordinatorManager> getRoutingCoordinator(Context context) {
-        if (!SdkLevel.isAtLeastS()) return new LateSdk<>(null);
-        return new LateSdk<>(
-                new RoutingCoordinatorManager(
-                        context, ConnectivityInternalApiUtil.getRoutingCoordinator(context)));
+    public RoutingCoordinatorManager getRoutingCoordinator(Context context, SharedLog log) {
+        IBinder binder;
+        if (!SdkLevel.isAtLeastS()) {
+            binder = new RoutingCoordinatorService(getINetd(context, log));
+        } else {
+            binder = ConnectivityInternalApiUtil.getRoutingCoordinator(context);
+        }
+        return new RoutingCoordinatorManager(context, binder);
     }
 
     /**
@@ -187,8 +193,8 @@
     /**
      * Make the TetheringMetrics to be used by tethering.
      */
-    public TetheringMetrics makeTetheringMetrics() {
-        return new TetheringMetrics();
+    public TetheringMetrics makeTetheringMetrics(Context ctx) {
+        return new TetheringMetrics(ctx);
     }
 
     /**
diff --git a/Tethering/src/com/android/networkstack/tethering/TetheringService.java b/Tethering/src/com/android/networkstack/tethering/TetheringService.java
index a147a4a..454cbf1 100644
--- a/Tethering/src/com/android/networkstack/tethering/TetheringService.java
+++ b/Tethering/src/com/android/networkstack/tethering/TetheringService.java
@@ -164,6 +164,8 @@
         @Override
         public void registerTetheringEventCallback(ITetheringEventCallback callback,
                 String callerPkg) {
+            // Silently ignore call if the callback is null. This can only happen via reflection.
+            if (callback == null) return;
             try {
                 if (!hasTetherAccessPermission()) {
                     callback.onCallbackStopped(TETHER_ERROR_NO_ACCESS_TETHERING_PERMISSION);
@@ -176,6 +178,8 @@
         @Override
         public void unregisterTetheringEventCallback(ITetheringEventCallback callback,
                 String callerPkg) {
+            // Silently ignore call if the callback is null. This can only happen via reflection.
+            if (callback == null) return;
             try {
                 if (!hasTetherAccessPermission()) {
                     callback.onCallbackStopped(TETHER_ERROR_NO_ACCESS_TETHERING_PERMISSION);
diff --git a/Tethering/src/com/android/networkstack/tethering/metrics/TetheringMetrics.java b/Tethering/src/com/android/networkstack/tethering/metrics/TetheringMetrics.java
index 814afcd..2202106 100644
--- a/Tethering/src/com/android/networkstack/tethering/metrics/TetheringMetrics.java
+++ b/Tethering/src/com/android/networkstack/tethering/metrics/TetheringMetrics.java
@@ -22,6 +22,12 @@
 import static android.net.NetworkCapabilities.TRANSPORT_LOWPAN;
 import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
 import static android.net.NetworkCapabilities.TRANSPORT_WIFI_AWARE;
+import static android.net.NetworkStats.DEFAULT_NETWORK_YES;
+import static android.net.NetworkStats.METERED_YES;
+import static android.net.NetworkTemplate.MATCH_BLUETOOTH;
+import static android.net.NetworkTemplate.MATCH_ETHERNET;
+import static android.net.NetworkTemplate.MATCH_MOBILE;
+import static android.net.NetworkTemplate.MATCH_WIFI;
 import static android.net.TetheringManager.TETHERING_BLUETOOTH;
 import static android.net.TetheringManager.TETHERING_ETHERNET;
 import static android.net.TetheringManager.TETHERING_NCM;
@@ -46,7 +52,9 @@
 import static android.net.TetheringManager.TETHER_ERROR_UNTETHER_IFACE_ERROR;
 
 import android.annotation.Nullable;
+import android.content.Context;
 import android.net.NetworkCapabilities;
+import android.net.NetworkTemplate;
 import android.stats.connectivity.DownstreamType;
 import android.stats.connectivity.ErrorCode;
 import android.stats.connectivity.UpstreamType;
@@ -57,6 +65,8 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.VisibleForTesting;
 
+import com.android.modules.utils.build.SdkLevel;
+import com.android.net.module.util.DeviceConfigUtils;
 import com.android.networkstack.tethering.UpstreamNetworkState;
 
 import java.util.ArrayList;
@@ -78,31 +88,100 @@
     private static final String SETTINGS_PKG_NAME = "com.android.settings";
     private static final String SYSTEMUI_PKG_NAME = "com.android.systemui";
     private static final String GMS_PKG_NAME = "com.google.android.gms";
+    /**
+     * A feature flag to control whether upstream data usage metrics should be enabled.
+     */
+    private static final String TETHER_UPSTREAM_DATA_USAGE_METRICS =
+            "tether_upstream_data_usage_metrics";
     private final SparseArray<NetworkTetheringReported.Builder> mBuilderMap = new SparseArray<>();
     private final SparseArray<Long> mDownstreamStartTime = new SparseArray<Long>();
     private final ArrayList<RecordUpstreamEvent> mUpstreamEventList = new ArrayList<>();
+    private final Context mContext;
+    private final Dependencies mDependencies;
     private UpstreamType mCurrentUpstream = null;
     private Long mCurrentUpStreamStartTime = 0L;
 
+    /**
+     * Dependencies of TetheringMetrics, for injection in tests.
+     */
+    @VisibleForTesting
+    public static class Dependencies {
+        /**
+         * @see TetheringStatsLog
+         */
+        public void write(NetworkTetheringReported reported) {
+            TetheringStatsLog.write(
+                    TetheringStatsLog.NETWORK_TETHERING_REPORTED,
+                    reported.getErrorCode().getNumber(),
+                    reported.getDownstreamType().getNumber(),
+                    reported.getUpstreamType().getNumber(),
+                    reported.getUserType().getNumber(),
+                    reported.getUpstreamEvents().toByteArray(),
+                    reported.getDurationMillis());
+        }
+
+        /**
+         * @see System#currentTimeMillis()
+         */
+        public long timeNow() {
+            return System.currentTimeMillis();
+        }
+
+        /**
+         * Indicates whether {@link #TETHER_UPSTREAM_DATA_USAGE_METRICS} is enabled.
+         */
+        public boolean isUpstreamDataUsageMetricsEnabled(Context context) {
+            // Getting data usage requires building a NetworkTemplate. However, the
+            // NetworkTemplate#Builder API was introduced in Android T.
+            return SdkLevel.isAtLeastT() && DeviceConfigUtils.isTetheringFeatureNotChickenedOut(
+                    context, TETHER_UPSTREAM_DATA_USAGE_METRICS);
+        }
+    }
 
     /**
-     * Return the current system time in milliseconds.
-     * @return the current system time in milliseconds.
+     * Constructor for the TetheringMetrics class.
+     *
+     * @param context The Context object used to access system services.
      */
-    public long timeNow() {
-        return System.currentTimeMillis();
+    public TetheringMetrics(Context context) {
+        this(context, new Dependencies());
+    }
+
+    TetheringMetrics(Context context, Dependencies dependencies) {
+        mContext = context;
+        mDependencies = dependencies;
+    }
+
+    private static class DataUsage {
+        final long mTxBytes;
+        final long mRxBytes;
+
+        DataUsage(long txBytes, long rxBytes) {
+            mTxBytes = txBytes;
+            mRxBytes = rxBytes;
+        }
+
+        public long getTxBytes() {
+            return mTxBytes;
+        }
+
+        public long getRxBytes() {
+            return mRxBytes;
+        }
     }
 
     private static class RecordUpstreamEvent {
-        public final long mStartTime;
-        public final long mStopTime;
-        public final UpstreamType mUpstreamType;
+        final long mStartTime;
+        final long mStopTime;
+        final UpstreamType mUpstreamType;
+        final DataUsage mDataUsage;
 
         RecordUpstreamEvent(final long startTime, final long stopTime,
-                final UpstreamType upstream) {
+                final UpstreamType upstream, final DataUsage dataUsage) {
             mStartTime = startTime;
             mStopTime = stopTime;
             mUpstreamType = upstream;
+            mDataUsage = dataUsage;
         }
     }
 
@@ -123,7 +202,7 @@
                 .setUpstreamEvents(UpstreamEvents.newBuilder())
                 .setDurationMillis(0);
         mBuilderMap.put(downstreamType, statsBuilder);
-        mDownstreamStartTime.put(downstreamType, timeNow());
+        mDownstreamStartTime.put(downstreamType, mDependencies.timeNow());
     }
 
     /**
@@ -140,6 +219,15 @@
         statsBuilder.setErrorCode(errorCodeToEnum(errCode));
     }
 
+    private DataUsage calculateDataUsage(@Nullable UpstreamType upstream) {
+        if (upstream != null && mDependencies.isUpstreamDataUsageMetricsEnabled(mContext)
+                && isUsageSupportedForUpstreamType(upstream)) {
+            // TODO: Implement data usage calculation for the upstream type.
+            return new DataUsage(0L, 0L);
+        }
+        return new DataUsage(0L, 0L);
+    }
+
     /**
      * Update the list of upstream types and their duration whenever the current upstream type
      * changes.
@@ -149,10 +237,11 @@
         UpstreamType upstream = transportTypeToUpstreamTypeEnum(ns);
         if (upstream.equals(mCurrentUpstream)) return;
 
-        final long newTime = timeNow();
+        final long newTime = mDependencies.timeNow();
         if (mCurrentUpstream != null) {
+            final DataUsage dataUsage = calculateDataUsage(upstream);
             mUpstreamEventList.add(new RecordUpstreamEvent(mCurrentUpStreamStartTime, newTime,
-                    mCurrentUpstream));
+                    mCurrentUpstream, dataUsage));
         }
         mCurrentUpstream = upstream;
         mCurrentUpStreamStartTime = newTime;
@@ -203,13 +292,14 @@
             final long startTime = Math.max(downstreamStartTime, event.mStartTime);
             // Handle completed upstream events.
             addUpstreamEvent(upstreamEventsBuilder, startTime, event.mStopTime,
-                    event.mUpstreamType, 0L /* txBytes */, 0L /* rxBytes */);
+                    event.mUpstreamType, event.mDataUsage.mTxBytes, event.mDataUsage.mRxBytes);
         }
         final long startTime = Math.max(downstreamStartTime, mCurrentUpStreamStartTime);
-        final long stopTime = timeNow();
+        final long stopTime = mDependencies.timeNow();
         // Handle the last upstream event.
+        final DataUsage dataUsage = calculateDataUsage(mCurrentUpstream);
         addUpstreamEvent(upstreamEventsBuilder, startTime, stopTime, mCurrentUpstream,
-                0L /* txBytes */, 0L /* rxBytes */);
+                dataUsage.mTxBytes, dataUsage.mRxBytes);
         statsBuilder.setUpstreamEvents(upstreamEventsBuilder);
         statsBuilder.setDurationMillis(stopTime - downstreamStartTime);
     }
@@ -248,15 +338,7 @@
     @VisibleForTesting
     public void write(@NonNull final NetworkTetheringReported reported) {
         final byte[] upstreamEvents = reported.getUpstreamEvents().toByteArray();
-
-        TetheringStatsLog.write(
-                TetheringStatsLog.NETWORK_TETHERING_REPORTED,
-                reported.getErrorCode().getNumber(),
-                reported.getDownstreamType().getNumber(),
-                reported.getUpstreamType().getNumber(),
-                reported.getUserType().getNumber(),
-                upstreamEvents,
-                reported.getDurationMillis());
+        mDependencies.write(reported);
         if (DBG) {
             Log.d(
                     TAG,
@@ -374,4 +456,64 @@
 
         return UpstreamType.UT_UNKNOWN;
     }
+
+    /**
+     * Check whether tethering metrics' data usage can be collected for a given upstream type.
+     *
+     * @param type the upstream type
+     */
+    public static boolean isUsageSupportedForUpstreamType(@NonNull UpstreamType type) {
+        switch(type) {
+            case UT_CELLULAR:
+            case UT_WIFI:
+            case UT_BLUETOOTH:
+            case UT_ETHERNET:
+                return true;
+            default:
+                break;
+        }
+        return false;
+    }
+
+    /**
+     * Build NetworkTemplate for the given upstream type.
+     *
+     * <p> NetworkTemplate.Builder API was introduced in Android T.
+     *
+     * @param type the upstream type
+     * @return A NetworkTemplate object with a corresponding match rule or null if tethering
+     * metrics' data usage cannot be collected for a given upstream type.
+     */
+    @Nullable
+    public static NetworkTemplate buildNetworkTemplateForUpstreamType(@NonNull UpstreamType type) {
+        if (!isUsageSupportedForUpstreamType(type)) return null;
+
+        switch (type) {
+            case UT_CELLULAR:
+                // TODO: Handle the DUN connection, which is not a default network.
+                return new NetworkTemplate.Builder(MATCH_MOBILE)
+                        .setMeteredness(METERED_YES)
+                        .setDefaultNetworkStatus(DEFAULT_NETWORK_YES)
+                        .build();
+            case UT_WIFI:
+                return new NetworkTemplate.Builder(MATCH_WIFI)
+                        .setMeteredness(METERED_YES)
+                        .setDefaultNetworkStatus(DEFAULT_NETWORK_YES)
+                        .build();
+            case UT_BLUETOOTH:
+                return new NetworkTemplate.Builder(MATCH_BLUETOOTH)
+                        .setMeteredness(METERED_YES)
+                        .setDefaultNetworkStatus(DEFAULT_NETWORK_YES)
+                        .build();
+            case UT_ETHERNET:
+                return new NetworkTemplate.Builder(MATCH_ETHERNET)
+                        .setMeteredness(METERED_YES)
+                        .setDefaultNetworkStatus(DEFAULT_NETWORK_YES)
+                        .build();
+            default:
+                Log.e(TAG, "Unsupported UpstreamType: " + type.name());
+                break;
+        }
+        return null;
+    }
 }
diff --git a/Tethering/src/com/android/networkstack/tethering/util/VersionedBroadcastListener.java b/Tethering/src/com/android/networkstack/tethering/util/VersionedBroadcastListener.java
index c9e75c0..5eb1551 100644
--- a/Tethering/src/com/android/networkstack/tethering/util/VersionedBroadcastListener.java
+++ b/Tethering/src/com/android/networkstack/tethering/util/VersionedBroadcastListener.java
@@ -62,8 +62,8 @@
         if (DBG) Log.d(mTag, "startListening");
         if (mReceiver != null) return;
 
-        mReceiver = new Receiver(mTag, mGenerationNumber, mCallback);
-        mContext.registerReceiver(mReceiver, mFilter, null, mHandler);
+        mReceiver = new Receiver(mTag, mGenerationNumber, mCallback, mHandler);
+        mContext.registerReceiver(mReceiver, mFilter);
     }
 
     /** Stop listening to intent broadcast. */
@@ -77,30 +77,35 @@
     }
 
     private static class Receiver extends BroadcastReceiver {
-        public final String tag;
-        public final AtomicInteger atomicGenerationNumber;
-        public final Consumer<Intent> callback;
+        final String mTag;
+        final AtomicInteger mAtomicGenerationNumber;
+        final Consumer<Intent> mCallback;
         // Used to verify this receiver is still current.
-        public final int generationNumber;
+        final int mGenerationNumber;
+        private final Handler mHandler;
 
-        Receiver(String tag, AtomicInteger atomicGenerationNumber, Consumer<Intent> callback) {
-            this.tag = tag;
-            this.atomicGenerationNumber = atomicGenerationNumber;
-            this.callback = callback;
-            generationNumber = atomicGenerationNumber.incrementAndGet();
+        Receiver(String tag, AtomicInteger atomicGenerationNumber, Consumer<Intent> callback,
+                Handler handler) {
+            mTag = tag;
+            mAtomicGenerationNumber = atomicGenerationNumber;
+            mCallback = callback;
+            mGenerationNumber = atomicGenerationNumber.incrementAndGet();
+            mHandler = handler;
         }
 
         @Override
         public void onReceive(Context context, Intent intent) {
-            final int currentGenerationNumber = atomicGenerationNumber.get();
+            mHandler.post(() -> {
+                final int currentGenerationNumber = mAtomicGenerationNumber.get();
 
-            if (DBG) {
-                Log.d(tag, "receiver generationNumber=" + generationNumber
-                        + ", current generationNumber=" + currentGenerationNumber);
-            }
-            if (generationNumber != currentGenerationNumber) return;
+                if (DBG) {
+                    Log.d(mTag, "receiver generationNumber=" + mGenerationNumber
+                            + ", current generationNumber=" + currentGenerationNumber);
+                }
+                if (mGenerationNumber != currentGenerationNumber) return;
 
-            callback.accept(intent);
+                mCallback.accept(intent);
+            });
         }
     }
 }
diff --git a/Tethering/tests/unit/src/android/net/ip/IpServerTest.java b/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
index 748f23c..177296a 100644
--- a/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
+++ b/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
@@ -51,6 +51,7 @@
 import static org.mockito.Matchers.anyBoolean;
 import static org.mockito.Matchers.anyString;
 import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.clearInvocations;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.doThrow;
@@ -174,8 +175,7 @@
     @Mock private RouterAdvertisementDaemon mRaDaemon;
     @Mock private IpServer.Dependencies mDependencies;
     @Mock private PrivateAddressCoordinator mAddressCoordinator;
-    private final LateSdk<RoutingCoordinatorManager> mRoutingCoordinatorManager =
-            new LateSdk<>(SdkLevel.isAtLeastS() ? mock(RoutingCoordinatorManager.class) : null);
+    @Mock private RoutingCoordinatorManager mRoutingCoordinatorManager;
     @Mock private NetworkStatsManager mStatsManager;
     @Mock private TetheringConfiguration mTetherConfig;
     @Mock private TetheringMetrics mTetheringMetrics;
@@ -280,24 +280,6 @@
         when(mTetherConfig.isBpfOffloadEnabled()).thenReturn(DEFAULT_USING_BPF_OFFLOAD);
         when(mTetherConfig.useLegacyDhcpServer()).thenReturn(false /* default value */);
 
-        // Simulate the behavior of RoutingCoordinator
-        if (null != mRoutingCoordinatorManager.value) {
-            doAnswer(it -> {
-                final String fromIface = (String) it.getArguments()[0];
-                final String toIface = (String) it.getArguments()[1];
-                mNetd.tetherAddForward(fromIface, toIface);
-                mNetd.ipfwdAddInterfaceForward(fromIface, toIface);
-                return null;
-            }).when(mRoutingCoordinatorManager.value).addInterfaceForward(any(), any());
-            doAnswer(it -> {
-                final String fromIface = (String) it.getArguments()[0];
-                final String toIface = (String) it.getArguments()[1];
-                mNetd.ipfwdRemoveInterfaceForward(fromIface, toIface);
-                mNetd.tetherRemoveForward(fromIface, toIface);
-                return null;
-            }).when(mRoutingCoordinatorManager.value).removeInterfaceForward(any(), any());
-        }
-
         setUpDhcpServer();
     }
 
@@ -455,105 +437,114 @@
         // Telling the state machine about its upstream interface triggers
         // a little more configuration.
         dispatchTetherConnectionChanged(UPSTREAM_IFACE);
-        InOrder inOrder = inOrder(mNetd, mBpfCoordinator);
+        InOrder inOrder = inOrder(mBpfCoordinator, mRoutingCoordinatorManager);
 
         // Add the forwarding pair <IFACE_NAME, UPSTREAM_IFACE>.
         inOrder.verify(mBpfCoordinator).maybeAddUpstreamToLookupTable(UPSTREAM_IFINDEX,
                 UPSTREAM_IFACE);
         inOrder.verify(mBpfCoordinator).maybeAttachProgram(IFACE_NAME, UPSTREAM_IFACE);
-        inOrder.verify(mNetd).tetherAddForward(IFACE_NAME, UPSTREAM_IFACE);
-        inOrder.verify(mNetd).ipfwdAddInterfaceForward(IFACE_NAME, UPSTREAM_IFACE);
+        inOrder.verify(mRoutingCoordinatorManager).addInterfaceForward(IFACE_NAME, UPSTREAM_IFACE);
 
-        verifyNoMoreInteractions(mNetd, mCallback, mBpfCoordinator);
+        verifyNoMoreInteractions(mCallback, mBpfCoordinator, mRoutingCoordinatorManager);
     }
 
     @Test
     public void handlesChangingUpstream() throws Exception {
         initTetheredStateMachine(TETHERING_BLUETOOTH, UPSTREAM_IFACE);
 
+        clearInvocations(mBpfCoordinator, mRoutingCoordinatorManager);
         dispatchTetherConnectionChanged(UPSTREAM_IFACE2);
-        InOrder inOrder = inOrder(mNetd, mBpfCoordinator);
+        InOrder inOrder = inOrder(mBpfCoordinator, mRoutingCoordinatorManager);
 
         // Remove the forwarding pair <IFACE_NAME, UPSTREAM_IFACE>.
         inOrder.verify(mBpfCoordinator).maybeDetachProgram(IFACE_NAME, UPSTREAM_IFACE);
-        inOrder.verify(mNetd).ipfwdRemoveInterfaceForward(IFACE_NAME, UPSTREAM_IFACE);
-        inOrder.verify(mNetd).tetherRemoveForward(IFACE_NAME, UPSTREAM_IFACE);
+        inOrder.verify(mRoutingCoordinatorManager)
+                .removeInterfaceForward(IFACE_NAME, UPSTREAM_IFACE);
 
         // Add the forwarding pair <IFACE_NAME, UPSTREAM_IFACE2>.
         inOrder.verify(mBpfCoordinator).maybeAddUpstreamToLookupTable(UPSTREAM_IFINDEX2,
                 UPSTREAM_IFACE2);
         inOrder.verify(mBpfCoordinator).maybeAttachProgram(IFACE_NAME, UPSTREAM_IFACE2);
-        inOrder.verify(mNetd).tetherAddForward(IFACE_NAME, UPSTREAM_IFACE2);
-        inOrder.verify(mNetd).ipfwdAddInterfaceForward(IFACE_NAME, UPSTREAM_IFACE2);
+        inOrder.verify(mRoutingCoordinatorManager).addInterfaceForward(IFACE_NAME, UPSTREAM_IFACE2);
 
-        verifyNoMoreInteractions(mNetd, mCallback, mBpfCoordinator);
+        verifyNoMoreInteractions(mCallback, mBpfCoordinator, mRoutingCoordinatorManager);
     }
 
     @Test
     public void handlesChangingUpstreamNatFailure() throws Exception {
         initTetheredStateMachine(TETHERING_WIFI, UPSTREAM_IFACE);
 
-        doThrow(RemoteException.class).when(mNetd).tetherAddForward(IFACE_NAME, UPSTREAM_IFACE2);
+        doThrow(RuntimeException.class)
+                .when(mRoutingCoordinatorManager)
+                .addInterfaceForward(IFACE_NAME, UPSTREAM_IFACE2);
 
         dispatchTetherConnectionChanged(UPSTREAM_IFACE2);
-        InOrder inOrder = inOrder(mNetd, mBpfCoordinator);
+        InOrder inOrder = inOrder(mBpfCoordinator, mRoutingCoordinatorManager);
 
         // Remove the forwarding pair <IFACE_NAME, UPSTREAM_IFACE>.
         inOrder.verify(mBpfCoordinator).maybeDetachProgram(IFACE_NAME, UPSTREAM_IFACE);
-        inOrder.verify(mNetd).ipfwdRemoveInterfaceForward(IFACE_NAME, UPSTREAM_IFACE);
-        inOrder.verify(mNetd).tetherRemoveForward(IFACE_NAME, UPSTREAM_IFACE);
+        inOrder.verify(mRoutingCoordinatorManager)
+                .removeInterfaceForward(IFACE_NAME, UPSTREAM_IFACE);
 
         // Add the forwarding pair <IFACE_NAME, UPSTREAM_IFACE2> and expect that failed on
-        // tetherAddForward.
+        // addInterfaceForward.
         inOrder.verify(mBpfCoordinator).maybeAddUpstreamToLookupTable(UPSTREAM_IFINDEX2,
                 UPSTREAM_IFACE2);
         inOrder.verify(mBpfCoordinator).maybeAttachProgram(IFACE_NAME, UPSTREAM_IFACE2);
-        inOrder.verify(mNetd).tetherAddForward(IFACE_NAME, UPSTREAM_IFACE2);
+        inOrder.verify(mRoutingCoordinatorManager).addInterfaceForward(IFACE_NAME, UPSTREAM_IFACE2);
 
         // Remove the forwarding pair <IFACE_NAME, UPSTREAM_IFACE2> to fallback.
         inOrder.verify(mBpfCoordinator).maybeDetachProgram(IFACE_NAME, UPSTREAM_IFACE2);
-        inOrder.verify(mNetd).ipfwdRemoveInterfaceForward(IFACE_NAME, UPSTREAM_IFACE2);
-        inOrder.verify(mNetd).tetherRemoveForward(IFACE_NAME, UPSTREAM_IFACE2);
+        inOrder.verify(mRoutingCoordinatorManager)
+                .removeInterfaceForward(IFACE_NAME, UPSTREAM_IFACE2);
     }
 
     @Test
     public void handlesChangingUpstreamInterfaceForwardingFailure() throws Exception {
         initTetheredStateMachine(TETHERING_WIFI, UPSTREAM_IFACE);
 
-        doThrow(RemoteException.class).when(mNetd).ipfwdAddInterfaceForward(
-                IFACE_NAME, UPSTREAM_IFACE2);
+        doThrow(RuntimeException.class)
+                .when(mRoutingCoordinatorManager)
+                .addInterfaceForward(IFACE_NAME, UPSTREAM_IFACE2);
 
         dispatchTetherConnectionChanged(UPSTREAM_IFACE2);
-        InOrder inOrder = inOrder(mNetd, mBpfCoordinator);
+        InOrder inOrder = inOrder(mBpfCoordinator, mRoutingCoordinatorManager);
 
         // Remove the forwarding pair <IFACE_NAME, UPSTREAM_IFACE>.
         inOrder.verify(mBpfCoordinator).maybeDetachProgram(IFACE_NAME, UPSTREAM_IFACE);
-        inOrder.verify(mNetd).ipfwdRemoveInterfaceForward(IFACE_NAME, UPSTREAM_IFACE);
-        inOrder.verify(mNetd).tetherRemoveForward(IFACE_NAME, UPSTREAM_IFACE);
+        inOrder.verify(mRoutingCoordinatorManager)
+                .removeInterfaceForward(IFACE_NAME, UPSTREAM_IFACE);
 
         // Add the forwarding pair <IFACE_NAME, UPSTREAM_IFACE2> and expect that failed on
         // ipfwdAddInterfaceForward.
         inOrder.verify(mBpfCoordinator).maybeAddUpstreamToLookupTable(UPSTREAM_IFINDEX2,
                 UPSTREAM_IFACE2);
         inOrder.verify(mBpfCoordinator).maybeAttachProgram(IFACE_NAME, UPSTREAM_IFACE2);
-        inOrder.verify(mNetd).tetherAddForward(IFACE_NAME, UPSTREAM_IFACE2);
-        inOrder.verify(mNetd).ipfwdAddInterfaceForward(IFACE_NAME, UPSTREAM_IFACE2);
+        inOrder.verify(mRoutingCoordinatorManager).addInterfaceForward(IFACE_NAME, UPSTREAM_IFACE2);
 
         // Remove the forwarding pair <IFACE_NAME, UPSTREAM_IFACE2> to fallback.
         inOrder.verify(mBpfCoordinator).maybeDetachProgram(IFACE_NAME, UPSTREAM_IFACE2);
-        inOrder.verify(mNetd).ipfwdRemoveInterfaceForward(IFACE_NAME, UPSTREAM_IFACE2);
-        inOrder.verify(mNetd).tetherRemoveForward(IFACE_NAME, UPSTREAM_IFACE2);
+        inOrder.verify(mRoutingCoordinatorManager)
+                .removeInterfaceForward(IFACE_NAME, UPSTREAM_IFACE2);
     }
 
     @Test
     public void canUnrequestTetheringWithUpstream() throws Exception {
         initTetheredStateMachine(TETHERING_BLUETOOTH, UPSTREAM_IFACE);
 
+        clearInvocations(
+                mNetd, mCallback, mAddressCoordinator, mBpfCoordinator, mRoutingCoordinatorManager);
         dispatchCommand(IpServer.CMD_TETHER_UNREQUESTED);
-        InOrder inOrder = inOrder(mNetd, mCallback, mAddressCoordinator, mBpfCoordinator);
+        InOrder inOrder =
+                inOrder(
+                        mNetd,
+                        mCallback,
+                        mAddressCoordinator,
+                        mBpfCoordinator,
+                        mRoutingCoordinatorManager);
         inOrder.verify(mBpfCoordinator).maybeDetachProgram(IFACE_NAME, UPSTREAM_IFACE);
-        inOrder.verify(mNetd).ipfwdRemoveInterfaceForward(IFACE_NAME, UPSTREAM_IFACE);
-        inOrder.verify(mNetd).tetherRemoveForward(IFACE_NAME, UPSTREAM_IFACE);
+        inOrder.verify(mRoutingCoordinatorManager)
+                .removeInterfaceForward(IFACE_NAME, UPSTREAM_IFACE);
         inOrder.verify(mBpfCoordinator).updateIpv6UpstreamInterface(
                 mIpServer, NO_UPSTREAM, NO_PREFIXES);
         // When tethering stops, upstream interface is set to zero and thus clearing all upstream
@@ -572,7 +563,8 @@
                 mIpServer, STATE_AVAILABLE, TETHER_ERROR_NO_ERROR);
         inOrder.verify(mCallback).updateLinkProperties(
                 eq(mIpServer), any(LinkProperties.class));
-        verifyNoMoreInteractions(mNetd, mCallback, mAddressCoordinator, mBpfCoordinator);
+        verifyNoMoreInteractions(
+                mNetd, mCallback, mAddressCoordinator, mBpfCoordinator, mRoutingCoordinatorManager);
     }
 
     @Test
@@ -624,10 +616,14 @@
     public void shouldTearDownUsbOnUpstreamError() throws Exception {
         initTetheredStateMachine(TETHERING_USB, null);
 
-        doThrow(RemoteException.class).when(mNetd).tetherAddForward(anyString(), anyString());
+        doThrow(RuntimeException.class)
+                .when(mRoutingCoordinatorManager)
+                .addInterfaceForward(anyString(), anyString());
         dispatchTetherConnectionChanged(UPSTREAM_IFACE);
-        InOrder usbTeardownOrder = inOrder(mNetd, mCallback);
-        usbTeardownOrder.verify(mNetd).tetherAddForward(IFACE_NAME, UPSTREAM_IFACE);
+        InOrder usbTeardownOrder = inOrder(mNetd, mCallback, mRoutingCoordinatorManager);
+        usbTeardownOrder
+                .verify(mRoutingCoordinatorManager)
+                .addInterfaceForward(IFACE_NAME, UPSTREAM_IFACE);
 
         usbTeardownOrder.verify(mNetd, times(2)).interfaceSetCfg(
                 argThat(cfg -> IFACE_NAME.equals(cfg.ifName)));
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 df7141f..6ba5d48 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
@@ -191,7 +191,6 @@
 import com.android.net.module.util.CollectionUtils;
 import com.android.net.module.util.InterfaceParams;
 import com.android.net.module.util.RoutingCoordinatorManager;
-import com.android.net.module.util.SdkUtil.LateSdk;
 import com.android.net.module.util.SharedLog;
 import com.android.net.module.util.ip.IpNeighborMonitor;
 import com.android.networkstack.apishim.common.BluetoothPanShim;
@@ -292,6 +291,7 @@
     @Mock private BluetoothPanShim mBluetoothPanShim;
     @Mock private TetheredInterfaceRequestShim mTetheredInterfaceRequestShim;
     @Mock private TetheringMetrics mTetheringMetrics;
+    @Mock private RoutingCoordinatorManager mRoutingCoordinatorManager;
 
     private final MockIpServerDependencies mIpServerDependencies =
             spy(new MockIpServerDependencies());
@@ -484,10 +484,10 @@
             return mEntitleMgr;
         }
 
-        @Nullable
         @Override
-        public LateSdk<RoutingCoordinatorManager> getRoutingCoordinator(final Context context) {
-            return new LateSdk<>(null);
+        public RoutingCoordinatorManager getRoutingCoordinator(final Context context,
+                SharedLog log) {
+            return mRoutingCoordinatorManager;
         }
 
         @Override
@@ -498,7 +498,7 @@
         }
 
         @Override
-        public INetd getINetd(Context context) {
+        public INetd getINetd(Context context, SharedLog log) {
             return mNetd;
         }
 
@@ -528,7 +528,7 @@
         }
 
         @Override
-        public TetheringMetrics makeTetheringMetrics() {
+        public TetheringMetrics makeTetheringMetrics(Context ctx) {
             return mTetheringMetrics;
         }
 
@@ -1082,8 +1082,8 @@
         UpstreamNetworkState upstreamState = buildMobileIPv4UpstreamState();
         runUsbTethering(upstreamState);
 
-        verify(mNetd, times(1)).tetherAddForward(TEST_RNDIS_IFNAME, TEST_MOBILE_IFNAME);
-        verify(mNetd, times(1)).ipfwdAddInterfaceForward(TEST_RNDIS_IFNAME, TEST_MOBILE_IFNAME);
+        verify(mRoutingCoordinatorManager, times(1))
+                .addInterfaceForward(TEST_RNDIS_IFNAME, TEST_MOBILE_IFNAME);
 
         sendIPv6TetherUpdates(upstreamState);
         assertSetIfaceToDadProxy(0 /* numOfCalls */, "" /* ifaceName */);
@@ -1111,8 +1111,8 @@
         UpstreamNetworkState upstreamState = buildMobileIPv6UpstreamState();
         runUsbTethering(upstreamState);
 
-        verify(mNetd, times(1)).tetherAddForward(TEST_RNDIS_IFNAME, TEST_MOBILE_IFNAME);
-        verify(mNetd, times(1)).ipfwdAddInterfaceForward(TEST_RNDIS_IFNAME, TEST_MOBILE_IFNAME);
+        verify(mRoutingCoordinatorManager, times(1))
+                .addInterfaceForward(TEST_RNDIS_IFNAME, TEST_MOBILE_IFNAME);
 
         sendIPv6TetherUpdates(upstreamState);
         // TODO: add interfaceParams to compare in verify.
@@ -1127,8 +1127,8 @@
         UpstreamNetworkState upstreamState = buildMobileDualStackUpstreamState();
         runUsbTethering(upstreamState);
 
-        verify(mNetd, times(1)).tetherAddForward(TEST_RNDIS_IFNAME, TEST_MOBILE_IFNAME);
-        verify(mNetd, times(1)).ipfwdAddInterfaceForward(TEST_RNDIS_IFNAME, TEST_MOBILE_IFNAME);
+        verify(mRoutingCoordinatorManager, times(1))
+                .addInterfaceForward(TEST_RNDIS_IFNAME, TEST_MOBILE_IFNAME);
         verify(mRouterAdvertisementDaemon, times(1)).start();
         verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).startWithCallbacks(
                 any(), any());
@@ -1145,13 +1145,12 @@
         UpstreamNetworkState upstreamState = buildMobile464xlatUpstreamState();
         runUsbTethering(upstreamState);
 
-        verify(mNetd, times(1)).tetherAddForward(TEST_RNDIS_IFNAME, TEST_XLAT_MOBILE_IFNAME);
-        verify(mNetd, times(1)).tetherAddForward(TEST_RNDIS_IFNAME, TEST_MOBILE_IFNAME);
+        verify(mRoutingCoordinatorManager, times(1))
+                .addInterfaceForward(TEST_RNDIS_IFNAME, TEST_XLAT_MOBILE_IFNAME);
+        verify(mRoutingCoordinatorManager, times(1))
+                .addInterfaceForward(TEST_RNDIS_IFNAME, TEST_MOBILE_IFNAME);
         verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).startWithCallbacks(
                 any(), any());
-        verify(mNetd, times(1)).ipfwdAddInterfaceForward(TEST_RNDIS_IFNAME,
-                TEST_XLAT_MOBILE_IFNAME);
-        verify(mNetd, times(1)).ipfwdAddInterfaceForward(TEST_RNDIS_IFNAME, TEST_MOBILE_IFNAME);
 
         sendIPv6TetherUpdates(upstreamState);
         assertSetIfaceToDadProxy(1 /* numOfCalls */, TEST_MOBILE_IFNAME /* ifaceName */);
@@ -1172,10 +1171,10 @@
         UpstreamNetworkState upstreamState = buildMobileIPv6UpstreamState();
         runUsbTethering(upstreamState);
 
-        verify(mNetd, times(1)).tetherAddForward(TEST_NCM_IFNAME, TEST_MOBILE_IFNAME);
+        verify(mRoutingCoordinatorManager, times(1))
+                .addInterfaceForward(TEST_NCM_IFNAME, TEST_MOBILE_IFNAME);
         verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).startWithCallbacks(
                 any(), any());
-        verify(mNetd, times(1)).ipfwdAddInterfaceForward(TEST_NCM_IFNAME, TEST_MOBILE_IFNAME);
 
         // Then 464xlat comes up
         upstreamState = buildMobile464xlatUpstreamState();
@@ -1187,12 +1186,11 @@
         mLooper.dispatchAll();
 
         // Forwarding is added for 464xlat
-        verify(mNetd, times(1)).tetherAddForward(TEST_NCM_IFNAME, TEST_XLAT_MOBILE_IFNAME);
-        verify(mNetd, times(1)).ipfwdAddInterfaceForward(TEST_NCM_IFNAME,
-                TEST_XLAT_MOBILE_IFNAME);
+        verify(mRoutingCoordinatorManager, times(1))
+                .addInterfaceForward(TEST_NCM_IFNAME, TEST_XLAT_MOBILE_IFNAME);
         // Forwarding was not re-added for v6 (still times(1))
-        verify(mNetd, times(1)).tetherAddForward(TEST_NCM_IFNAME, TEST_MOBILE_IFNAME);
-        verify(mNetd, times(1)).ipfwdAddInterfaceForward(TEST_NCM_IFNAME, TEST_MOBILE_IFNAME);
+        verify(mRoutingCoordinatorManager, times(1))
+                .addInterfaceForward(TEST_NCM_IFNAME, TEST_MOBILE_IFNAME);
         // DHCP not restarted on downstream (still times(1))
         verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).startWithCallbacks(
                 any(), any());
@@ -3435,8 +3433,7 @@
         runUsbTethering(upstreamState);
 
         verify(mNetd).interfaceGetList();
-        verify(mNetd).tetherAddForward(expectedIface, TEST_MOBILE_IFNAME);
-        verify(mNetd).ipfwdAddInterfaceForward(expectedIface, TEST_MOBILE_IFNAME);
+        verify(mRoutingCoordinatorManager).addInterfaceForward(expectedIface, TEST_MOBILE_IFNAME);
 
         verify(mRouterAdvertisementDaemon).start();
         verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS)).startWithCallbacks(
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/metrics/TetheringMetricsTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/metrics/TetheringMetricsTest.java
index e2c924c..fbc2893 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/metrics/TetheringMetricsTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/metrics/TetheringMetricsTest.java
@@ -22,6 +22,10 @@
 import static android.net.NetworkCapabilities.TRANSPORT_LOWPAN;
 import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
 import static android.net.NetworkCapabilities.TRANSPORT_WIFI_AWARE;
+import static android.net.NetworkTemplate.MATCH_BLUETOOTH;
+import static android.net.NetworkTemplate.MATCH_ETHERNET;
+import static android.net.NetworkTemplate.MATCH_MOBILE;
+import static android.net.NetworkTemplate.MATCH_WIFI;
 import static android.net.TetheringManager.TETHERING_BLUETOOTH;
 import static android.net.TetheringManager.TETHERING_ETHERNET;
 import static android.net.TetheringManager.TETHERING_NCM;
@@ -46,11 +50,15 @@
 import static android.net.TetheringManager.TETHER_ERROR_UNSUPPORTED;
 import static android.net.TetheringManager.TETHER_ERROR_UNTETHER_IFACE_ERROR;
 
-import static org.mockito.Mockito.reset;
-import static org.mockito.Mockito.spy;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.verify;
 
+import android.content.Context;
 import android.net.NetworkCapabilities;
+import android.net.NetworkTemplate;
+import android.os.Build;
 import android.stats.connectivity.DownstreamType;
 import android.stats.connectivity.ErrorCode;
 import android.stats.connectivity.UpstreamType;
@@ -60,21 +68,32 @@
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.networkstack.tethering.UpstreamNetworkState;
+import com.android.networkstack.tethering.metrics.TetheringMetrics.Dependencies;
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
 
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
 @RunWith(AndroidJUnit4.class)
 @SmallTest
 public final class TetheringMetricsTest {
+    @Rule public final DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule();
+
     private static final String TEST_CALLER_PKG = "com.test.caller.pkg";
     private static final String SETTINGS_PKG = "com.android.settings";
     private static final String SYSTEMUI_PKG = "com.android.systemui";
     private static final String GMS_PKG = "com.google.android.gms";
     private static final long TEST_START_TIME = 1670395936033L;
     private static final long SECOND_IN_MILLIS = 1_000L;
+    private static final int MATCH_NONE = -1;
+
+    @Mock private Context mContext;
+    @Mock private Dependencies mDeps;
 
     private TetheringMetrics mTetheringMetrics;
     private final NetworkTetheringReported.Builder mStatsBuilder =
@@ -82,22 +101,14 @@
 
     private long mElapsedRealtime;
 
-    private class MockTetheringMetrics extends TetheringMetrics {
-        @Override
-        public void write(final NetworkTetheringReported reported) {}
-        @Override
-        public long timeNow() {
-            return currentTimeMillis();
-        }
-    }
-
     private long currentTimeMillis() {
         return TEST_START_TIME + mElapsedRealtime;
     }
 
     private void incrementCurrentTime(final long duration) {
         mElapsedRealtime += duration;
-        mTetheringMetrics.timeNow();
+        final long currentTimeMillis = currentTimeMillis();
+        doReturn(currentTimeMillis).when(mDeps).timeNow();
     }
 
     private long getElapsedRealtime() {
@@ -106,12 +117,14 @@
 
     private void clearElapsedRealtime() {
         mElapsedRealtime = 0;
+        doReturn(TEST_START_TIME).when(mDeps).timeNow();
     }
 
     @Before
     public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
-        mTetheringMetrics = spy(new MockTetheringMetrics());
+        doReturn(TEST_START_TIME).when(mDeps).timeNow();
+        mTetheringMetrics = new TetheringMetrics(mContext, mDeps);
         mElapsedRealtime = 0L;
     }
 
@@ -126,7 +139,7 @@
                 .setUpstreamEvents(upstreamEvents)
                 .setDurationMillis(duration)
                 .build();
-        verify(mTetheringMetrics).write(expectedReport);
+        verify(mDeps).write(expectedReport);
     }
 
     private void updateErrorAndSendReport(final int downstream, final int error) {
@@ -162,6 +175,7 @@
 
     private void runDownstreamTypesTest(final int type, final DownstreamType expectedResult)
             throws Exception {
+        mTetheringMetrics = new TetheringMetrics(mContext, mDeps);
         mTetheringMetrics.createBuilder(type, TEST_CALLER_PKG);
         final long duration = 2 * SECOND_IN_MILLIS;
         incrementCurrentTime(duration);
@@ -172,9 +186,7 @@
 
         verifyReport(expectedResult, ErrorCode.EC_NO_ERROR, UserType.USER_UNKNOWN,
                 upstreamEvents, getElapsedRealtime());
-        reset(mTetheringMetrics);
         clearElapsedRealtime();
-        mTetheringMetrics.cleanup();
     }
 
     @Test
@@ -189,6 +201,7 @@
 
     private void runErrorCodesTest(final int errorCode, final ErrorCode expectedResult)
             throws Exception {
+        mTetheringMetrics = new TetheringMetrics(mContext, mDeps);
         mTetheringMetrics.createBuilder(TETHERING_WIFI, TEST_CALLER_PKG);
         mTetheringMetrics.maybeUpdateUpstreamType(buildUpstreamState(TRANSPORT_WIFI));
         final long duration = 2 * SECOND_IN_MILLIS;
@@ -199,9 +212,7 @@
         addUpstreamEvent(upstreamEvents, UpstreamType.UT_WIFI, duration, 0L, 0L);
         verifyReport(DownstreamType.DS_TETHERING_WIFI, expectedResult, UserType.USER_UNKNOWN,
                     upstreamEvents, getElapsedRealtime());
-        reset(mTetheringMetrics);
         clearElapsedRealtime();
-        mTetheringMetrics.cleanup();
     }
 
     @Test
@@ -231,6 +242,7 @@
 
     private void runUserTypesTest(final String callerPkg, final UserType expectedResult)
             throws Exception {
+        mTetheringMetrics = new TetheringMetrics(mContext, mDeps);
         mTetheringMetrics.createBuilder(TETHERING_WIFI, callerPkg);
         final long duration = 1 * SECOND_IN_MILLIS;
         incrementCurrentTime(duration);
@@ -241,9 +253,7 @@
         addUpstreamEvent(upstreamEvents, UpstreamType.UT_NO_NETWORK, duration, 0L, 0L);
         verifyReport(DownstreamType.DS_TETHERING_WIFI, ErrorCode.EC_NO_ERROR, expectedResult,
                     upstreamEvents, getElapsedRealtime());
-        reset(mTetheringMetrics);
         clearElapsedRealtime();
-        mTetheringMetrics.cleanup();
     }
 
     @Test
@@ -256,6 +266,7 @@
 
     private void runUpstreamTypesTest(final UpstreamNetworkState ns,
             final UpstreamType expectedResult) throws Exception {
+        mTetheringMetrics = new TetheringMetrics(mContext, mDeps);
         mTetheringMetrics.createBuilder(TETHERING_WIFI, TEST_CALLER_PKG);
         mTetheringMetrics.maybeUpdateUpstreamType(ns);
         final long duration = 2 * SECOND_IN_MILLIS;
@@ -266,9 +277,7 @@
         addUpstreamEvent(upstreamEvents, expectedResult, duration, 0L, 0L);
         verifyReport(DownstreamType.DS_TETHERING_WIFI, ErrorCode.EC_NO_ERROR,
                 UserType.USER_UNKNOWN, upstreamEvents, getElapsedRealtime());
-        reset(mTetheringMetrics);
         clearElapsedRealtime();
-        mTetheringMetrics.cleanup();
     }
 
     @Test
@@ -379,4 +388,44 @@
                 UserType.USER_SETTINGS, upstreamEvents,
                 currentTimeMillis() - wifiTetheringStartTime);
     }
+
+    private void runUsageSupportedForUpstreamTypeTest(final UpstreamType upstreamType,
+            final boolean isSupported) {
+        final boolean result = TetheringMetrics.isUsageSupportedForUpstreamType(upstreamType);
+        assertEquals(isSupported, result);
+    }
+
+    @Test
+    public void testUsageSupportedForUpstreamTypeTest() {
+        runUsageSupportedForUpstreamTypeTest(UpstreamType.UT_CELLULAR, true /* isSupported */);
+        runUsageSupportedForUpstreamTypeTest(UpstreamType.UT_WIFI, true /* isSupported */);
+        runUsageSupportedForUpstreamTypeTest(UpstreamType.UT_BLUETOOTH, true /* isSupported */);
+        runUsageSupportedForUpstreamTypeTest(UpstreamType.UT_ETHERNET, true /* isSupported */);
+        runUsageSupportedForUpstreamTypeTest(UpstreamType.UT_WIFI_AWARE, false /* isSupported */);
+        runUsageSupportedForUpstreamTypeTest(UpstreamType.UT_LOWPAN, false /* isSupported */);
+        runUsageSupportedForUpstreamTypeTest(UpstreamType.UT_UNKNOWN, false /* isSupported */);
+    }
+
+    private void runBuildNetworkTemplateForUpstreamType(final UpstreamType upstreamType,
+            final int matchRule)  {
+        final NetworkTemplate template =
+                TetheringMetrics.buildNetworkTemplateForUpstreamType(upstreamType);
+        if (matchRule == MATCH_NONE) {
+            assertNull(template);
+        } else {
+            assertEquals(matchRule, template.getMatchRule());
+        }
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+    public void testBuildNetworkTemplateForUpstreamType() {
+        runBuildNetworkTemplateForUpstreamType(UpstreamType.UT_CELLULAR, MATCH_MOBILE);
+        runBuildNetworkTemplateForUpstreamType(UpstreamType.UT_WIFI, MATCH_WIFI);
+        runBuildNetworkTemplateForUpstreamType(UpstreamType.UT_BLUETOOTH, MATCH_BLUETOOTH);
+        runBuildNetworkTemplateForUpstreamType(UpstreamType.UT_ETHERNET, MATCH_ETHERNET);
+        runBuildNetworkTemplateForUpstreamType(UpstreamType.UT_WIFI_AWARE, MATCH_NONE);
+        runBuildNetworkTemplateForUpstreamType(UpstreamType.UT_LOWPAN, MATCH_NONE);
+        runBuildNetworkTemplateForUpstreamType(UpstreamType.UT_UNKNOWN, MATCH_NONE);
+    }
 }
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/util/VersionedBroadcastListenerTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/util/VersionedBroadcastListenerTest.java
index b7dc66e..ed4f3da 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/util/VersionedBroadcastListenerTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/util/VersionedBroadcastListenerTest.java
@@ -16,6 +16,8 @@
 
 package com.android.networkstack.tethering.util;
 
+import static com.android.testutils.HandlerUtils.waitForIdle;
+
 import static org.junit.Assert.assertEquals;
 import static org.mockito.Mockito.reset;
 
@@ -23,7 +25,7 @@
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.os.Handler;
-import android.os.Looper;
+import android.os.HandlerThread;
 import android.os.UserHandle;
 
 import androidx.test.filters.SmallTest;
@@ -33,7 +35,6 @@
 
 import org.junit.After;
 import org.junit.Before;
-import org.junit.BeforeClass;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
@@ -44,9 +45,11 @@
 public class VersionedBroadcastListenerTest {
     private static final String TAG = VersionedBroadcastListenerTest.class.getSimpleName();
     private static final String ACTION_TEST = "action.test.happy.broadcasts";
+    private static final long TEST_TIMEOUT_MS = 10_000L;
 
     @Mock private Context mContext;
     private BroadcastInterceptingContext mServiceContext;
+    private HandlerThread mHandlerThread;
     private Handler mHandler;
     private VersionedBroadcastListener mListener;
     private int mCallbackCount;
@@ -61,18 +64,13 @@
         }
     }
 
-    @BeforeClass
-    public static void setUpBeforeClass() throws Exception {
-        if (Looper.myLooper() == null) {
-            Looper.prepare();
-        }
-    }
-
     @Before public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
         reset(mContext);
+        mHandlerThread = new HandlerThread(TAG);
+        mHandlerThread.start();
+        mHandler = new Handler(mHandlerThread.getLooper());
         mServiceContext = new MockContext(mContext);
-        mHandler = new Handler(Looper.myLooper());
         mCallbackCount = 0;
         final IntentFilter filter = new IntentFilter();
         filter.addAction(ACTION_TEST);
@@ -85,11 +83,15 @@
             mListener.stopListening();
             mListener = null;
         }
+        mHandlerThread.quitSafely();
+        mHandlerThread.join(TEST_TIMEOUT_MS);
     }
 
     private void sendBroadcast() {
         final Intent intent = new Intent(ACTION_TEST);
         mServiceContext.sendStickyBroadcastAsUser(intent, UserHandle.ALL);
+        // Sending the broadcast is synchronous, but the receiver just posts on the handler
+        waitForIdle(mHandler, TEST_TIMEOUT_MS);
     }
 
     @Test
diff --git a/bpf_progs/Android.bp b/bpf_progs/Android.bp
index 1958aa8..f6717c5 100644
--- a/bpf_progs/Android.bp
+++ b/bpf_progs/Android.bp
@@ -38,6 +38,7 @@
     cflags: [
         "-Wall",
         "-Werror",
+        "-Wextra",
     ],
     sdk_version: "30",
     min_sdk_version: "30",
@@ -65,73 +66,46 @@
 bpf {
     name: "block.o",
     srcs: ["block.c"],
-    btf: true,
-    cflags: [
-        "-Wall",
-        "-Werror",
-    ],
     sub_dir: "net_shared",
 }
 
 bpf {
     name: "dscpPolicy.o",
     srcs: ["dscpPolicy.c"],
-    btf: true,
-    cflags: [
-        "-Wall",
-        "-Werror",
-    ],
     sub_dir: "net_shared",
 }
 
+// Ships to Android S, the bpfloader of which fails to parse BTF enabled .o's.
 bpf {
     name: "offload.o",
     srcs: ["offload.c"],
-    cflags: [
-        "-Wall",
-        "-Werror",
-    ],
+    btf: false,
 }
 
+// This version ships to Android T+ which uses mainline netbpfload.
 bpf {
     name: "offload@mainline.o",
     srcs: ["offload@mainline.c"],
-    btf: true,
-    cflags: [
-        "-Wall",
-        "-Werror",
-        "-DMAINLINE",
-    ],
+    cflags: ["-DMAINLINE"],
 }
 
+// Ships to Android S, the bpfloader of which fails to parse BTF enabled .o's.
 bpf {
     name: "test.o",
     srcs: ["test.c"],
-    cflags: [
-        "-Wall",
-        "-Werror",
-    ],
+    btf: false,
 }
 
+// This version ships to Android T+ which uses mainline netbpfload.
 bpf {
     name: "test@mainline.o",
     srcs: ["test@mainline.c"],
-    btf: true,
-    cflags: [
-        "-Wall",
-        "-Werror",
-        "-DMAINLINE",
-    ],
+    cflags: ["-DMAINLINE"],
 }
 
 bpf {
     name: "clatd.o",
     srcs: ["clatd.c"],
-    btf: true,
-    cflags: [
-        "-Wall",
-        "-Werror",
-    ],
     sub_dir: "net_shared",
 }
 
@@ -139,11 +113,6 @@
     // WARNING: Android T's non-updatable netd depends on 'netd' string for xt_bpf programs it loads
     name: "netd.o",
     srcs: ["netd.c"],
-    btf: true,
-    cflags: [
-        "-Wall",
-        "-Werror",
-    ],
     // WARNING: Android T's non-updatable netd depends on 'netd_shared' string for xt_bpf programs
     sub_dir: "netd_shared",
 }
diff --git a/bpf_progs/block.c b/bpf_progs/block.c
index 152dda6..0e2dba9 100644
--- a/bpf_progs/block.c
+++ b/bpf_progs/block.c
@@ -14,24 +14,16 @@
  * limitations under the License.
  */
 
-#include <linux/types.h>
-#include <linux/bpf.h>
-#include <netinet/in.h>
-#include <stdint.h>
-
 // The resulting .o needs to load on Android T+
 #define BPFLOADER_MIN_VER BPFLOADER_MAINLINE_T_VERSION
 
-#include "bpf_helpers.h"
-
-static const int ALLOW = 1;
-static const int DISALLOW = 0;
+#include "bpf_net_helpers.h"
 
 DEFINE_BPF_MAP_GRW(blocked_ports_map, ARRAY, int, uint64_t,
         1024 /* 64K ports -> 1024 u64s */, AID_SYSTEM)
 
 static inline __always_inline int block_port(struct bpf_sock_addr *ctx) {
-    if (!ctx->user_port) return ALLOW;
+    if (!ctx->user_port) return BPF_ALLOW;
 
     switch (ctx->protocol) {
         case IPPROTO_TCP:
@@ -42,7 +34,7 @@
         case IPPROTO_SCTP:
             break;
         default:
-            return ALLOW; // unknown protocols are allowed
+            return BPF_ALLOW; // unknown protocols are allowed
     }
 
     int key = ctx->user_port >> 6;
@@ -51,10 +43,10 @@
     uint64_t *val = bpf_blocked_ports_map_lookup_elem(&key);
     // Lookup should never fail in reality, but if it does return here to keep the
     // BPF verifier happy.
-    if (!val) return ALLOW;
+    if (!val) return BPF_ALLOW;
 
-    if ((*val >> shift) & 1) return DISALLOW;
-    return ALLOW;
+    if ((*val >> shift) & 1) return BPF_DISALLOW;
+    return BPF_ALLOW;
 }
 
 // the program need to be accessible/loadable by netd (from netd updatable plugin)
@@ -75,4 +67,3 @@
 
 LICENSE("Apache 2.0");
 CRITICAL("ConnectivityNative");
-DISABLE_BTF_ON_USER_BUILDS();
diff --git a/bpf_progs/bpf_net_helpers.h b/bpf_progs/bpf_net_helpers.h
index 1511ee5..a86c3e6 100644
--- a/bpf_progs/bpf_net_helpers.h
+++ b/bpf_progs/bpf_net_helpers.h
@@ -17,21 +17,72 @@
 #pragma once
 
 #include <linux/bpf.h>
+#include <linux/if.h>
+#include <linux/if_ether.h>
 #include <linux/if_packet.h>
-#include <stdbool.h>
-#include <stdint.h>
-
+#include <linux/in.h>
+#include <linux/in6.h>
+#include <linux/ip.h>
+#include <linux/ipv6.h>
+#include <linux/pkt_cls.h>
+#include <linux/tcp.h>
 // bionic kernel uapi linux/udp.h header is munged...
 #define __kernel_udphdr udphdr
 #include <linux/udp.h>
+#include <stdbool.h>
+#include <stdint.h>
+
+#include "bpf_helpers.h"
+
+// IP flags. (from kernel's include/net/ip.h)
+#define IP_CE      0x8000  // Flag: "Congestion" (really reserved 'evil bit')
+#define IP_DF      0x4000  // Flag: "Don't Fragment"
+#define IP_MF      0x2000  // Flag: "More Fragments"
+#define IP_OFFSET  0x1FFF  // "Fragment Offset" part
+
+// IPv6 fragmentation header. (from kernel's include/net/ipv6.h)
+struct frag_hdr {
+    __u8   nexthdr;
+    __u8   reserved;        // always zero
+    __be16 frag_off;        // 13 bit offset, 2 bits zero, 1 bit "More Fragments"
+    __be32 identification;
+};
+
+// ----- Helper functions for offsets to fields -----
+
+// They all assume simple IP packets:
+//   - no VLAN ethernet tags
+//   - no IPv4 options (see IPV4_HLEN/TCP4_OFFSET/UDP4_OFFSET)
+//   - no IPv6 extension headers
+//   - no TCP options (see TCP_HLEN)
+
+//#define ETH_HLEN sizeof(struct ethhdr)
+#define IP4_HLEN sizeof(struct iphdr)
+#define IP6_HLEN sizeof(struct ipv6hdr)
+#define TCP_HLEN sizeof(struct tcphdr)
+#define UDP_HLEN sizeof(struct udphdr)
 
 // Offsets from beginning of L4 (TCP/UDP) header
 #define TCP_OFFSET(field) offsetof(struct tcphdr, field)
 #define UDP_OFFSET(field) offsetof(struct udphdr, field)
 
-// Offsets from beginning of L3 (IPv4/IPv6) header
+// Offsets from beginning of L3 (IPv4) header
 #define IP4_OFFSET(field) offsetof(struct iphdr, field)
+#define IP4_TCP_OFFSET(field) (IP4_HLEN + TCP_OFFSET(field))
+#define IP4_UDP_OFFSET(field) (IP4_HLEN + UDP_OFFSET(field))
+
+// Offsets from beginning of L3 (IPv6) header
 #define IP6_OFFSET(field) offsetof(struct ipv6hdr, field)
+#define IP6_TCP_OFFSET(field) (IP6_HLEN + TCP_OFFSET(field))
+#define IP6_UDP_OFFSET(field) (IP6_HLEN + UDP_OFFSET(field))
+
+// Offsets from beginning of L2 (ie. Ethernet) header (which must be present)
+#define ETH_IP4_OFFSET(field) (ETH_HLEN + IP4_OFFSET(field))
+#define ETH_IP4_TCP_OFFSET(field) (ETH_HLEN + IP4_TCP_OFFSET(field))
+#define ETH_IP4_UDP_OFFSET(field) (ETH_HLEN + IP4_UDP_OFFSET(field))
+#define ETH_IP6_OFFSET(field) (ETH_HLEN + IP6_OFFSET(field))
+#define ETH_IP6_TCP_OFFSET(field) (ETH_HLEN + IP6_TCP_OFFSET(field))
+#define ETH_IP6_UDP_OFFSET(field) (ETH_HLEN + IP6_UDP_OFFSET(field))
 
 // this returns 0 iff skb->sk is NULL
 static uint64_t (*bpf_get_socket_cookie)(struct __sk_buff* skb) = (void*)BPF_FUNC_get_socket_cookie;
@@ -83,7 +134,7 @@
 
 // try to make the first 'len' header bytes readable/writable via direct packet access
 // (note: AFAIK there is no way to ask for only direct packet read without also getting write)
-static inline __always_inline void try_make_writable(struct __sk_buff* skb, int len) {
+static inline __always_inline void try_make_writable(struct __sk_buff* skb, unsigned len) {
     if (len > skb->len) len = skb->len;
     if (skb->data_end - skb->data < len) bpf_skb_pull_data(skb, len);
 }
@@ -103,3 +154,10 @@
 struct updatetime_bool { bool updatetime; };
 #define NO_UPDATETIME ((struct updatetime_bool){ .updatetime = false })
 #define UPDATETIME ((struct updatetime_bool){ .updatetime = true })
+
+// Return value for xt_bpf (netfilter match extension) programs
+static const int XTBPF_NOMATCH = 0;
+static const int XTBPF_MATCH = 1;
+
+static const int BPF_DISALLOW = 0;
+static const int BPF_ALLOW = 1;
diff --git a/bpf_progs/clatd.c b/bpf_progs/clatd.c
index f83e5ae..2d4551e 100644
--- a/bpf_progs/clatd.c
+++ b/bpf_progs/clatd.c
@@ -14,44 +14,13 @@
  * limitations under the License.
  */
 
-#include <linux/bpf.h>
-#include <linux/if.h>
-#include <linux/if_ether.h>
-#include <linux/in.h>
-#include <linux/in6.h>
-#include <linux/ip.h>
-#include <linux/ipv6.h>
-#include <linux/pkt_cls.h>
-#include <linux/swab.h>
-#include <stdbool.h>
-#include <stdint.h>
-
-// bionic kernel uapi linux/udp.h header is munged...
-#define __kernel_udphdr udphdr
-#include <linux/udp.h>
-
 // The resulting .o needs to load on Android T+
 #define BPFLOADER_MIN_VER BPFLOADER_MAINLINE_T_VERSION
 
-#include "bpf_helpers.h"
 #include "bpf_net_helpers.h"
 #include "clatd.h"
 #include "clat_mark.h"
 
-// IP flags. (from kernel's include/net/ip.h)
-#define IP_CE      0x8000  // Flag: "Congestion" (really reserved 'evil bit')
-#define IP_DF      0x4000  // Flag: "Don't Fragment"
-#define IP_MF      0x2000  // Flag: "More Fragments"
-#define IP_OFFSET  0x1FFF  // "Fragment Offset" part
-
-// from kernel's include/net/ipv6.h
-struct frag_hdr {
-    __u8   nexthdr;
-    __u8   reserved;        // always zero
-    __be16 frag_off;        // 13 bit offset, 2 bits zero, 1 bit "More Fragments"
-    __be32 identification;
-};
-
 DEFINE_BPF_MAP_GRW(clat_ingress6_map, HASH, ClatIngress6Key, ClatIngress6Value, 16, AID_SYSTEM)
 
 static inline __always_inline int nat64(struct __sk_buff* skb,
@@ -177,7 +146,7 @@
 
     // Calculate the IPv4 one's complement checksum of the IPv4 header.
     __wsum sum4 = 0;
-    for (int i = 0; i < sizeof(ip) / sizeof(__u16); ++i) {
+    for (unsigned i = 0; i < sizeof(ip) / sizeof(__u16); ++i) {
         sum4 += ((__u16*)&ip)[i];
     }
     // Note that sum4 is guaranteed to be non-zero by virtue of ip.version == 4
@@ -188,7 +157,7 @@
     // Calculate the *negative* IPv6 16-bit one's complement checksum of the IPv6 header.
     __wsum sum6 = 0;
     // We'll end up with a non-zero sum due to ip6->version == 6 (which has '0' bits)
-    for (int i = 0; i < sizeof(*ip6) / sizeof(__u16); ++i) {
+    for (unsigned i = 0; i < sizeof(*ip6) / sizeof(__u16); ++i) {
         sum6 += ~((__u16*)ip6)[i];  // note the bitwise negation
     }
 
@@ -321,7 +290,7 @@
 
     // Calculate the IPv4 one's complement checksum of the IPv4 header.
     __wsum sum4 = 0;
-    for (int i = 0; i < sizeof(*ip4) / sizeof(__u16); ++i) {
+    for (unsigned i = 0; i < sizeof(*ip4) / sizeof(__u16); ++i) {
         sum4 += ((__u16*)ip4)[i];
     }
     // Note that sum4 is guaranteed to be non-zero by virtue of ip4->version == 4
@@ -387,7 +356,7 @@
     // Calculate the IPv6 16-bit one's complement checksum of the IPv6 header.
     __wsum sum6 = 0;
     // We'll end up with a non-zero sum due to ip6.version == 6
-    for (int i = 0; i < sizeof(ip6) / sizeof(__u16); ++i) {
+    for (unsigned i = 0; i < sizeof(ip6) / sizeof(__u16); ++i) {
         sum6 += ((__u16*)&ip6)[i];
     }
 
@@ -430,4 +399,3 @@
 
 LICENSE("Apache 2.0");
 CRITICAL("Connectivity");
-DISABLE_BTF_ON_USER_BUILDS();
diff --git a/bpf_progs/dscpPolicy.c b/bpf_progs/dscpPolicy.c
index ed114e4..93542ee 100644
--- a/bpf_progs/dscpPolicy.c
+++ b/bpf_progs/dscpPolicy.c
@@ -14,27 +14,13 @@
  * limitations under the License.
  */
 
-#include <linux/bpf.h>
-#include <linux/if_ether.h>
-#include <linux/if_packet.h>
-#include <linux/ip.h>
-#include <linux/ipv6.h>
-#include <linux/pkt_cls.h>
-#include <linux/tcp.h>
-#include <linux/types.h>
-#include <netinet/in.h>
-#include <netinet/udp.h>
-#include <stdint.h>
-#include <string.h>
-
 // The resulting .o needs to load on Android T+
 #define BPFLOADER_MIN_VER BPFLOADER_MAINLINE_T_VERSION
 
-#include "bpf_helpers.h"
+#include "bpf_net_helpers.h"
 #include "dscpPolicy.h"
 
 #define ECN_MASK 3
-#define IP4_OFFSET(field, header) ((header) + offsetof(struct iphdr, field))
 #define UPDATE_TOS(dscp, tos) ((dscp) << 2) | ((tos) & ECN_MASK)
 
 DEFINE_BPF_MAP_GRW(socket_policy_cache_map, HASH, uint64_t, RuleEntry, CACHE_MAP_SIZE, AID_SYSTEM)
@@ -131,9 +117,9 @@
         if (existing_rule->dscp_val < 0) return;
         if (ipv4) {
             uint8_t newTos = UPDATE_TOS(existing_rule->dscp_val, tos);
-            bpf_l3_csum_replace(skb, IP4_OFFSET(check, l2_header_size), htons(tos), htons(newTos),
+            bpf_l3_csum_replace(skb, l2_header_size + IP4_OFFSET(check), htons(tos), htons(newTos),
                                 sizeof(uint16_t));
-            bpf_skb_store_bytes(skb, IP4_OFFSET(tos, l2_header_size), &newTos, sizeof(newTos), 0);
+            bpf_skb_store_bytes(skb, l2_header_size + IP4_OFFSET(tos), &newTos, sizeof(newTos), 0);
         } else {
             __be32 new_first_be32 =
                 htonl(ntohl(old_first_be32) & 0xF03FFFFF | (existing_rule->dscp_val << 22));
@@ -211,8 +197,8 @@
     // Need to store bytes after updating map or program will not load.
     if (ipv4) {
         uint8_t new_tos = UPDATE_TOS(new_dscp, tos);
-        bpf_l3_csum_replace(skb, IP4_OFFSET(check, l2_header_size), htons(tos), htons(new_tos), 2);
-        bpf_skb_store_bytes(skb, IP4_OFFSET(tos, l2_header_size), &new_tos, sizeof(new_tos), 0);
+        bpf_l3_csum_replace(skb, l2_header_size + IP4_OFFSET(check), htons(tos), htons(new_tos), 2);
+        bpf_skb_store_bytes(skb, l2_header_size + IP4_OFFSET(tos), &new_tos, sizeof(new_tos), 0);
     } else {
         __be32 new_first_be32 = htonl(ntohl(old_first_be32) & 0xF03FFFFF | (new_dscp << 22));
         bpf_skb_store_bytes(skb, l2_header_size, &new_first_be32, sizeof(__be32),
@@ -238,4 +224,3 @@
 
 LICENSE("Apache 2.0");
 CRITICAL("Connectivity");
-DISABLE_BTF_ON_USER_BUILDS();
diff --git a/bpf_progs/dscpPolicy.h b/bpf_progs/dscpPolicy.h
index e565966..ea84655 100644
--- a/bpf_progs/dscpPolicy.h
+++ b/bpf_progs/dscpPolicy.h
@@ -37,17 +37,6 @@
 // Returns 'a == b' as boolean
 #define v6_equal(a, b) (!v6_not_equal((a), (b)))
 
-// TODO: these are already defined in packages/modules/Connectivity/bpf_progs/bpf_net_helpers.h.
-// smove to common location in future.
-static uint64_t (*bpf_get_socket_cookie)(struct __sk_buff* skb) =
-        (void*)BPF_FUNC_get_socket_cookie;
-static int (*bpf_skb_store_bytes)(struct __sk_buff* skb, __u32 offset, const void* from, __u32 len,
-                                  __u64 flags) = (void*)BPF_FUNC_skb_store_bytes;
-static int (*bpf_l3_csum_replace)(struct __sk_buff* skb, __u32 offset, __u64 from, __u64 to,
-                                  __u64 flags) = (void*)BPF_FUNC_l3_csum_replace;
-static long (*bpf_skb_ecn_set_ce)(struct __sk_buff* skb) =
-        (void*)BPF_FUNC_skb_ecn_set_ce;
-
 typedef struct {
     struct in6_addr src_ip;
     struct in6_addr dst_ip;
diff --git a/bpf_progs/netd.c b/bpf_progs/netd.c
index b3cde45..4248a46 100644
--- a/bpf_progs/netd.c
+++ b/bpf_progs/netd.c
@@ -17,19 +17,6 @@
 // The resulting .o needs to load on Android T+
 #define BPFLOADER_MIN_VER BPFLOADER_MAINLINE_T_VERSION
 
-#include <bpf_helpers.h>
-#include <linux/bpf.h>
-#include <linux/if.h>
-#include <linux/if_ether.h>
-#include <linux/if_packet.h>
-#include <linux/in.h>
-#include <linux/in6.h>
-#include <linux/ip.h>
-#include <linux/ipv6.h>
-#include <linux/pkt_cls.h>
-#include <linux/tcp.h>
-#include <stdbool.h>
-#include <stdint.h>
 #include "bpf_net_helpers.h"
 #include "netd.h"
 
@@ -38,10 +25,6 @@
 static const int PASS = 1;
 static const int DROP_UNLESS_DNS = 2;  // internal to our program
 
-// This is used for xt_bpf program only.
-static const int BPF_NOMATCH = 0;
-static const int BPF_MATCH = 1;
-
 // Used for 'bool enable_tracing'
 static const bool TRACE_ON = true;
 static const bool TRACE_OFF = false;
@@ -184,7 +167,7 @@
     static __always_inline inline void update_##the_stats_map(const struct __sk_buff* const skb, \
                                                               const TypeOfKey* const key,        \
                                                               const struct egress_bool egress,   \
-                                                              const struct kver_uint kver) {     \
+                                                     __unused const struct kver_uint kver) {     \
         StatsValue* value = bpf_##the_stats_map##_lookup_elem(key);                              \
         if (!value) {                                                                            \
             StatsValue newValue = {};                                                            \
@@ -524,22 +507,12 @@
     return match;
 }
 
-// This program is optional, and enables tracing on Android U+, 5.8+ on user builds.
-DEFINE_BPF_PROG_EXT("cgroupskb/ingress/stats$trace_user", AID_ROOT, AID_SYSTEM,
-                    bpf_cgroup_ingress_trace_user, KVER_5_8, KVER_INF,
-                    BPFLOADER_MAINLINE_U_VERSION, BPFLOADER_MAX_VER, OPTIONAL,
-                    "fs_bpf_netd_readonly", "",
-                    IGNORE_ON_ENG, LOAD_ON_USER, IGNORE_ON_USERDEBUG)
-(struct __sk_buff* skb) {
-    return bpf_traffic_account(skb, INGRESS, TRACE_ON, KVER_5_8);
-}
-
-// This program is required, and enables tracing on Android U+, 5.8+, userdebug/eng.
+// Tracing on Android U+ 5.8+
 DEFINE_BPF_PROG_EXT("cgroupskb/ingress/stats$trace", AID_ROOT, AID_SYSTEM,
                     bpf_cgroup_ingress_trace, KVER_5_8, KVER_INF,
                     BPFLOADER_MAINLINE_U_VERSION, BPFLOADER_MAX_VER, MANDATORY,
                     "fs_bpf_netd_readonly", "",
-                    LOAD_ON_ENG, IGNORE_ON_USER, LOAD_ON_USERDEBUG)
+                    LOAD_ON_ENG, LOAD_ON_USER, LOAD_ON_USERDEBUG)
 (struct __sk_buff* skb) {
     return bpf_traffic_account(skb, INGRESS, TRACE_ON, KVER_5_8);
 }
@@ -556,22 +529,12 @@
     return bpf_traffic_account(skb, INGRESS, TRACE_OFF, KVER_NONE);
 }
 
-// This program is optional, and enables tracing on Android U+, 5.8+ on user builds.
-DEFINE_BPF_PROG_EXT("cgroupskb/egress/stats$trace_user", AID_ROOT, AID_SYSTEM,
-                    bpf_cgroup_egress_trace_user, KVER_5_8, KVER_INF,
-                    BPFLOADER_MAINLINE_U_VERSION, BPFLOADER_MAX_VER, OPTIONAL,
-                    "fs_bpf_netd_readonly", "",
-                    IGNORE_ON_ENG, LOAD_ON_USER, IGNORE_ON_USERDEBUG)
-(struct __sk_buff* skb) {
-    return bpf_traffic_account(skb, EGRESS, TRACE_ON, KVER_5_8);
-}
-
-// This program is required, and enables tracing on Android U+, 5.8+, userdebug/eng.
+// Tracing on Android U+ 5.8+
 DEFINE_BPF_PROG_EXT("cgroupskb/egress/stats$trace", AID_ROOT, AID_SYSTEM,
                     bpf_cgroup_egress_trace, KVER_5_8, KVER_INF,
                     BPFLOADER_MAINLINE_U_VERSION, BPFLOADER_MAX_VER, MANDATORY,
                     "fs_bpf_netd_readonly", "",
-                    LOAD_ON_ENG, IGNORE_ON_USER, LOAD_ON_USERDEBUG)
+                    LOAD_ON_ENG, LOAD_ON_USER, LOAD_ON_USERDEBUG)
 (struct __sk_buff* skb) {
     return bpf_traffic_account(skb, EGRESS, TRACE_ON, KVER_5_8);
 }
@@ -599,12 +562,12 @@
     if (sock_uid == AID_SYSTEM) {
         uint64_t cookie = bpf_get_socket_cookie(skb);
         UidTagValue* utag = bpf_cookie_tag_map_lookup_elem(&cookie);
-        if (utag && utag->uid == AID_CLAT) return BPF_NOMATCH;
+        if (utag && utag->uid == AID_CLAT) return XTBPF_NOMATCH;
     }
 
     uint32_t key = skb->ifindex;
     update_iface_stats_map(skb, &key, EGRESS, KVER_NONE);
-    return BPF_MATCH;
+    return XTBPF_MATCH;
 }
 
 // WARNING: Android T's non-updatable netd depends on the name of this program.
@@ -617,7 +580,7 @@
 
     uint32_t key = skb->ifindex;
     update_iface_stats_map(skb, &key, INGRESS, KVER_NONE);
-    return BPF_MATCH;
+    return XTBPF_MATCH;
 }
 
 DEFINE_SYS_BPF_PROG("schedact/ingress/account", AID_ROOT, AID_NET_ADMIN,
@@ -635,7 +598,7 @@
 DEFINE_XTBPF_PROG("skfilter/allowlist/xtbpf", AID_ROOT, AID_NET_ADMIN, xt_bpf_allowlist_prog)
 (struct __sk_buff* skb) {
     uint32_t sock_uid = bpf_get_socket_uid(skb);
-    if (is_system_uid(sock_uid)) return BPF_MATCH;
+    if (is_system_uid(sock_uid)) return XTBPF_MATCH;
 
     // kernel's DEFAULT_OVERFLOWUID is 65534, this is the overflow 'nobody' uid,
     // usually this being returned means that skb->sk is NULL during RX
@@ -643,11 +606,11 @@
     // packets to an unconnected udp socket.
     // But it can also happen for egress from a timewait socket.
     // Let's treat such cases as 'root' which is_system_uid()
-    if (sock_uid == 65534) return BPF_MATCH;
+    if (sock_uid == 65534) return XTBPF_MATCH;
 
     UidOwnerValue* allowlistMatch = bpf_uid_owner_map_lookup_elem(&sock_uid);
-    if (allowlistMatch) return allowlistMatch->rule & HAPPY_BOX_MATCH ? BPF_MATCH : BPF_NOMATCH;
-    return BPF_NOMATCH;
+    if (allowlistMatch) return allowlistMatch->rule & HAPPY_BOX_MATCH ? XTBPF_MATCH : XTBPF_NOMATCH;
+    return XTBPF_NOMATCH;
 }
 
 // WARNING: Android T's non-updatable netd depends on the name of this program.
@@ -656,8 +619,8 @@
     uint32_t sock_uid = bpf_get_socket_uid(skb);
     UidOwnerValue* denylistMatch = bpf_uid_owner_map_lookup_elem(&sock_uid);
     uint32_t penalty_box = PENALTY_BOX_USER_MATCH | PENALTY_BOX_ADMIN_MATCH;
-    if (denylistMatch) return denylistMatch->rule & penalty_box ? BPF_MATCH : BPF_NOMATCH;
-    return BPF_NOMATCH;
+    if (denylistMatch) return denylistMatch->rule & penalty_box ? XTBPF_MATCH : XTBPF_NOMATCH;
+    return XTBPF_NOMATCH;
 }
 
 static __always_inline inline uint8_t get_app_permissions() {
@@ -676,9 +639,8 @@
 
 DEFINE_NETD_BPF_PROG_KVER("cgroupsock/inet_create", AID_ROOT, AID_ROOT, inet_socket_create,
                           KVER_4_14)
-(struct bpf_sock* sk) {
-    // A return value of 1 means allow, everything else means deny.
-    return (get_app_permissions() & BPF_PERMISSION_INTERNET) ? 1 : 0;
+(__unused struct bpf_sock* sk) {
+    return (get_app_permissions() & BPF_PERMISSION_INTERNET) ? BPF_ALLOW : BPF_DISALLOW;
 }
 
 DEFINE_NETD_V_BPF_PROG_KVER("cgroupsockrelease/inet_release", AID_ROOT, AID_ROOT,
@@ -690,7 +652,7 @@
     return 1;
 }
 
-static __always_inline inline int check_localhost(struct bpf_sock_addr *ctx) {
+static __always_inline inline int check_localhost(__unused struct bpf_sock_addr *ctx) {
     // See include/uapi/linux/bpf.h:
     //
     // struct bpf_sock_addr {
@@ -705,7 +667,7 @@
     //   __u32 msg_src_ip6[4];	// BE, R: 1,2,4,8-byte, W: 4,8-byte
     //   __bpf_md_ptr(struct bpf_sock *, sk);
     // };
-    return 1;
+    return BPF_ALLOW;
 }
 
 DEFINE_NETD_V_BPF_PROG_KVER("connect4/inet4_connect", AID_ROOT, AID_ROOT, inet4_connect, KVER_4_14)
@@ -743,7 +705,7 @@
     // Tell kernel to return 'original' kernel reply (instead of the bpf modified buffer)
     // This is important if the answer is larger than PAGE_SIZE (max size this bpf hook can provide)
     ctx->optlen = 0;
-    return 1; // ALLOW
+    return BPF_ALLOW;
 }
 
 DEFINE_NETD_V_BPF_PROG_KVER("setsockopt/prog", AID_ROOT, AID_ROOT, setsockopt_prog, KVER_5_4)
@@ -751,9 +713,8 @@
     // Tell kernel to use/process original buffer provided by userspace.
     // This is important if it is larger than PAGE_SIZE (max size this bpf hook can handle).
     ctx->optlen = 0;
-    return 1; // ALLOW
+    return BPF_ALLOW;
 }
 
 LICENSE("Apache 2.0");
 CRITICAL("Connectivity and netd");
-DISABLE_BTF_ON_USER_BUILDS();
diff --git a/bpf_progs/offload.c b/bpf_progs/offload.c
index 4f152bf..7e1184d 100644
--- a/bpf_progs/offload.c
+++ b/bpf_progs/offload.c
@@ -14,16 +14,6 @@
  * limitations under the License.
  */
 
-#include <linux/if.h>
-#include <linux/ip.h>
-#include <linux/ipv6.h>
-#include <linux/pkt_cls.h>
-#include <linux/tcp.h>
-
-// bionic kernel uapi linux/udp.h header is munged...
-#define __kernel_udphdr udphdr
-#include <linux/udp.h>
-
 #ifdef MAINLINE
 // BTF is incompatible with bpfloaders < v0.10, hence for S (v0.2) we must
 // ship a different file than for later versions, but we need bpfloader v0.25+
@@ -35,61 +25,16 @@
 #define BPFLOADER_MAX_VER BPFLOADER_T_VERSION
 #endif /* MAINLINE */
 
-// Warning: values other than AID_ROOT don't work for map uid on BpfLoader < v0.21
-#define TETHERING_UID AID_ROOT
-
-#define TETHERING_GID AID_NETWORK_STACK
-
-#include "bpf_helpers.h"
 #include "bpf_net_helpers.h"
 #include "offload.h"
 
-// From kernel:include/net/ip.h
-#define IP_DF 0x4000  // Flag: "Don't Fragment"
-
-// ----- Helper functions for offsets to fields -----
-
-// They all assume simple IP packets:
-//   - no VLAN ethernet tags
-//   - no IPv4 options (see IPV4_HLEN/TCP4_OFFSET/UDP4_OFFSET)
-//   - no IPv6 extension headers
-//   - no TCP options (see TCP_HLEN)
-
-//#define ETH_HLEN sizeof(struct ethhdr)
-#define IP4_HLEN sizeof(struct iphdr)
-#define IP6_HLEN sizeof(struct ipv6hdr)
-#define TCP_HLEN sizeof(struct tcphdr)
-#define UDP_HLEN sizeof(struct udphdr)
-
-// Offsets from beginning of L4 (TCP/UDP) header
-#define TCP_OFFSET(field) offsetof(struct tcphdr, field)
-#define UDP_OFFSET(field) offsetof(struct udphdr, field)
-
-// Offsets from beginning of L3 (IPv4) header
-#define IP4_OFFSET(field) offsetof(struct iphdr, field)
-#define IP4_TCP_OFFSET(field) (IP4_HLEN + TCP_OFFSET(field))
-#define IP4_UDP_OFFSET(field) (IP4_HLEN + UDP_OFFSET(field))
-
-// Offsets from beginning of L3 (IPv6) header
-#define IP6_OFFSET(field) offsetof(struct ipv6hdr, field)
-#define IP6_TCP_OFFSET(field) (IP6_HLEN + TCP_OFFSET(field))
-#define IP6_UDP_OFFSET(field) (IP6_HLEN + UDP_OFFSET(field))
-
-// Offsets from beginning of L2 (ie. Ethernet) header (which must be present)
-#define ETH_IP4_OFFSET(field) (ETH_HLEN + IP4_OFFSET(field))
-#define ETH_IP4_TCP_OFFSET(field) (ETH_HLEN + IP4_TCP_OFFSET(field))
-#define ETH_IP4_UDP_OFFSET(field) (ETH_HLEN + IP4_UDP_OFFSET(field))
-#define ETH_IP6_OFFSET(field) (ETH_HLEN + IP6_OFFSET(field))
-#define ETH_IP6_TCP_OFFSET(field) (ETH_HLEN + IP6_TCP_OFFSET(field))
-#define ETH_IP6_UDP_OFFSET(field) (ETH_HLEN + IP6_UDP_OFFSET(field))
-
 // ----- Tethering Error Counters -----
 
 // Note that pre-T devices with Mediatek chipsets may have a kernel bug (bad patch
 // "[ALPS05162612] bpf: fix ubsan error") making it impossible to write to non-zero
 // offset of bpf map ARRAYs.  This file (offload.o) loads on S+, but luckily this
 // array is only written by bpf code, and only read by userspace.
-DEFINE_BPF_MAP_RO(tether_error_map, ARRAY, uint32_t, uint32_t, BPF_TETHER_ERR__MAX, TETHERING_GID)
+DEFINE_BPF_MAP_RO(tether_error_map, ARRAY, uint32_t, uint32_t, BPF_TETHER_ERR__MAX, AID_NETWORK_STACK)
 
 #define COUNT_AND_RETURN(counter, ret) do {                     \
     uint32_t code = BPF_TETHER_ERR_ ## counter;                 \
@@ -107,27 +52,27 @@
 // ----- Tethering Data Stats and Limits -----
 
 // Tethering stats, indexed by upstream interface.
-DEFINE_BPF_MAP_GRW(tether_stats_map, HASH, TetherStatsKey, TetherStatsValue, 16, TETHERING_GID)
+DEFINE_BPF_MAP_GRW(tether_stats_map, HASH, TetherStatsKey, TetherStatsValue, 16, AID_NETWORK_STACK)
 
 // Tethering data limit, indexed by upstream interface.
 // (tethering allowed when stats[iif].rxBytes + stats[iif].txBytes < limit[iif])
-DEFINE_BPF_MAP_GRW(tether_limit_map, HASH, TetherLimitKey, TetherLimitValue, 16, TETHERING_GID)
+DEFINE_BPF_MAP_GRW(tether_limit_map, HASH, TetherLimitKey, TetherLimitValue, 16, AID_NETWORK_STACK)
 
 // ----- IPv6 Support -----
 
 DEFINE_BPF_MAP_GRW(tether_downstream6_map, HASH, TetherDownstream6Key, Tether6Value, 64,
-                   TETHERING_GID)
+                   AID_NETWORK_STACK)
 
 DEFINE_BPF_MAP_GRW(tether_downstream64_map, HASH, TetherDownstream64Key, TetherDownstream64Value,
-                   1024, TETHERING_GID)
+                   1024, AID_NETWORK_STACK)
 
 DEFINE_BPF_MAP_GRW(tether_upstream6_map, HASH, TetherUpstream6Key, Tether6Value, 64,
-                   TETHERING_GID)
+                   AID_NETWORK_STACK)
 
 static inline __always_inline int do_forward6(struct __sk_buff* skb,
                                               const struct rawip_bool rawip,
                                               const struct stream_bool stream,
-                                              const struct kver_uint kver) {
+                                              __unused const struct kver_uint kver) {
     const bool is_ethernet = !rawip.rawip;
 
     // Must be meta-ethernet IPv6 frame
@@ -302,13 +247,13 @@
     return bpf_redirect(v->oif, 0 /* this is effectively BPF_F_EGRESS */);
 }
 
-DEFINE_BPF_PROG("schedcls/tether_downstream6_ether", TETHERING_UID, TETHERING_GID,
+DEFINE_BPF_PROG("schedcls/tether_downstream6_ether", AID_ROOT, AID_NETWORK_STACK,
                 sched_cls_tether_downstream6_ether)
 (struct __sk_buff* skb) {
     return do_forward6(skb, ETHER, DOWNSTREAM, KVER_NONE);
 }
 
-DEFINE_BPF_PROG("schedcls/tether_upstream6_ether", TETHERING_UID, TETHERING_GID,
+DEFINE_BPF_PROG("schedcls/tether_upstream6_ether", AID_ROOT, AID_NETWORK_STACK,
                 sched_cls_tether_upstream6_ether)
 (struct __sk_buff* skb) {
     return do_forward6(skb, ETHER, UPSTREAM, KVER_NONE);
@@ -328,42 +273,42 @@
 // and in system/netd/tests/binder_test.cpp NetdBinderTest TetherOffloadForwarding.
 //
 // Hence, these mandatory (must load successfully) implementations for 4.14+ kernels:
-DEFINE_BPF_PROG_KVER("schedcls/tether_downstream6_rawip$4_14", TETHERING_UID, TETHERING_GID,
+DEFINE_BPF_PROG_KVER("schedcls/tether_downstream6_rawip$4_14", AID_ROOT, AID_NETWORK_STACK,
                      sched_cls_tether_downstream6_rawip_4_14, KVER_4_14)
 (struct __sk_buff* skb) {
     return do_forward6(skb, RAWIP, DOWNSTREAM, KVER_4_14);
 }
 
-DEFINE_BPF_PROG_KVER("schedcls/tether_upstream6_rawip$4_14", TETHERING_UID, TETHERING_GID,
+DEFINE_BPF_PROG_KVER("schedcls/tether_upstream6_rawip$4_14", AID_ROOT, AID_NETWORK_STACK,
                      sched_cls_tether_upstream6_rawip_4_14, KVER_4_14)
 (struct __sk_buff* skb) {
     return do_forward6(skb, RAWIP, UPSTREAM, KVER_4_14);
 }
 
 // and define no-op stubs for pre-4.14 kernels.
-DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_downstream6_rawip$stub", TETHERING_UID, TETHERING_GID,
+DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_downstream6_rawip$stub", AID_ROOT, AID_NETWORK_STACK,
                            sched_cls_tether_downstream6_rawip_stub, KVER_NONE, KVER_4_14)
-(struct __sk_buff* skb) {
+(__unused struct __sk_buff* skb) {
     return TC_ACT_PIPE;
 }
 
-DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_upstream6_rawip$stub", TETHERING_UID, TETHERING_GID,
+DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_upstream6_rawip$stub", AID_ROOT, AID_NETWORK_STACK,
                            sched_cls_tether_upstream6_rawip_stub, KVER_NONE, KVER_4_14)
-(struct __sk_buff* skb) {
+(__unused struct __sk_buff* skb) {
     return TC_ACT_PIPE;
 }
 
 // ----- IPv4 Support -----
 
-DEFINE_BPF_MAP_GRW(tether_downstream4_map, HASH, Tether4Key, Tether4Value, 1024, TETHERING_GID)
+DEFINE_BPF_MAP_GRW(tether_downstream4_map, HASH, Tether4Key, Tether4Value, 1024, AID_NETWORK_STACK)
 
-DEFINE_BPF_MAP_GRW(tether_upstream4_map, HASH, Tether4Key, Tether4Value, 1024, TETHERING_GID)
+DEFINE_BPF_MAP_GRW(tether_upstream4_map, HASH, Tether4Key, Tether4Value, 1024, AID_NETWORK_STACK)
 
 static inline __always_inline int do_forward4_bottom(struct __sk_buff* skb,
         const int l2_header_size, void* data, const void* data_end,
         struct ethhdr* eth, struct iphdr* ip, const struct rawip_bool rawip,
         const struct stream_bool stream, const struct updatetime_bool updatetime,
-        const bool is_tcp, const struct kver_uint kver) {
+        const bool is_tcp, __unused const struct kver_uint kver) {
     const bool is_ethernet = !rawip.rawip;
     struct tcphdr* tcph = is_tcp ? (void*)(ip + 1) : NULL;
     struct udphdr* udph = is_tcp ? NULL : (void*)(ip + 1);
@@ -593,7 +538,7 @@
 
     // Calculate the IPv4 one's complement checksum of the IPv4 header.
     __wsum sum4 = 0;
-    for (int i = 0; i < sizeof(*ip) / sizeof(__u16); ++i) {
+    for (unsigned i = 0; i < sizeof(*ip) / sizeof(__u16); ++i) {
         sum4 += ((__u16*)ip)[i];
     }
     // Note that sum4 is guaranteed to be non-zero by virtue of ip4->version == 4
@@ -656,25 +601,25 @@
 
 // Full featured (required) implementations for 5.8+ kernels (these are S+ by definition)
 
-DEFINE_BPF_PROG_KVER("schedcls/tether_downstream4_rawip$5_8", TETHERING_UID, TETHERING_GID,
+DEFINE_BPF_PROG_KVER("schedcls/tether_downstream4_rawip$5_8", AID_ROOT, AID_NETWORK_STACK,
                      sched_cls_tether_downstream4_rawip_5_8, KVER_5_8)
 (struct __sk_buff* skb) {
     return do_forward4(skb, RAWIP, DOWNSTREAM, UPDATETIME, KVER_5_8);
 }
 
-DEFINE_BPF_PROG_KVER("schedcls/tether_upstream4_rawip$5_8", TETHERING_UID, TETHERING_GID,
+DEFINE_BPF_PROG_KVER("schedcls/tether_upstream4_rawip$5_8", AID_ROOT, AID_NETWORK_STACK,
                      sched_cls_tether_upstream4_rawip_5_8, KVER_5_8)
 (struct __sk_buff* skb) {
     return do_forward4(skb, RAWIP, UPSTREAM, UPDATETIME, KVER_5_8);
 }
 
-DEFINE_BPF_PROG_KVER("schedcls/tether_downstream4_ether$5_8", TETHERING_UID, TETHERING_GID,
+DEFINE_BPF_PROG_KVER("schedcls/tether_downstream4_ether$5_8", AID_ROOT, AID_NETWORK_STACK,
                      sched_cls_tether_downstream4_ether_5_8, KVER_5_8)
 (struct __sk_buff* skb) {
     return do_forward4(skb, ETHER, DOWNSTREAM, UPDATETIME, KVER_5_8);
 }
 
-DEFINE_BPF_PROG_KVER("schedcls/tether_upstream4_ether$5_8", TETHERING_UID, TETHERING_GID,
+DEFINE_BPF_PROG_KVER("schedcls/tether_upstream4_ether$5_8", AID_ROOT, AID_NETWORK_STACK,
                      sched_cls_tether_upstream4_ether_5_8, KVER_5_8)
 (struct __sk_buff* skb) {
     return do_forward4(skb, ETHER, UPSTREAM, UPDATETIME, KVER_5_8);
@@ -684,7 +629,7 @@
 // (optional, because we need to be able to fallback for 4.14/4.19/5.4 pre-S kernels)
 
 DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_rawip$opt",
-                                    TETHERING_UID, TETHERING_GID,
+                                    AID_ROOT, AID_NETWORK_STACK,
                                     sched_cls_tether_downstream4_rawip_opt,
                                     KVER_4_14, KVER_5_8)
 (struct __sk_buff* skb) {
@@ -692,7 +637,7 @@
 }
 
 DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_rawip$opt",
-                                    TETHERING_UID, TETHERING_GID,
+                                    AID_ROOT, AID_NETWORK_STACK,
                                     sched_cls_tether_upstream4_rawip_opt,
                                     KVER_4_14, KVER_5_8)
 (struct __sk_buff* skb) {
@@ -700,7 +645,7 @@
 }
 
 DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_ether$opt",
-                                    TETHERING_UID, TETHERING_GID,
+                                    AID_ROOT, AID_NETWORK_STACK,
                                     sched_cls_tether_downstream4_ether_opt,
                                     KVER_4_14, KVER_5_8)
 (struct __sk_buff* skb) {
@@ -708,7 +653,7 @@
 }
 
 DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_ether$opt",
-                                    TETHERING_UID, TETHERING_GID,
+                                    AID_ROOT, AID_NETWORK_STACK,
                                     sched_cls_tether_upstream4_ether_opt,
                                     KVER_4_14, KVER_5_8)
 (struct __sk_buff* skb) {
@@ -729,13 +674,13 @@
 
 // RAWIP: Required for 5.4-R kernels -- which always support bpf_skb_change_head().
 
-DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_rawip$5_4", TETHERING_UID, TETHERING_GID,
+DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_rawip$5_4", AID_ROOT, AID_NETWORK_STACK,
                            sched_cls_tether_downstream4_rawip_5_4, KVER_5_4, KVER_5_8)
 (struct __sk_buff* skb) {
     return do_forward4(skb, RAWIP, DOWNSTREAM, NO_UPDATETIME, KVER_5_4);
 }
 
-DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_rawip$5_4", TETHERING_UID, TETHERING_GID,
+DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_rawip$5_4", AID_ROOT, AID_NETWORK_STACK,
                            sched_cls_tether_upstream4_rawip_5_4, KVER_5_4, KVER_5_8)
 (struct __sk_buff* skb) {
     return do_forward4(skb, RAWIP, UPSTREAM, NO_UPDATETIME, KVER_5_4);
@@ -745,7 +690,7 @@
 // [Note: fallback for 4.14/4.19 (P/Q) kernels is below in stub section]
 
 DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_rawip$4_14",
-                                    TETHERING_UID, TETHERING_GID,
+                                    AID_ROOT, AID_NETWORK_STACK,
                                     sched_cls_tether_downstream4_rawip_4_14,
                                     KVER_4_14, KVER_5_4)
 (struct __sk_buff* skb) {
@@ -753,7 +698,7 @@
 }
 
 DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_rawip$4_14",
-                                    TETHERING_UID, TETHERING_GID,
+                                    AID_ROOT, AID_NETWORK_STACK,
                                     sched_cls_tether_upstream4_rawip_4_14,
                                     KVER_4_14, KVER_5_4)
 (struct __sk_buff* skb) {
@@ -762,13 +707,13 @@
 
 // ETHER: Required for 4.14-Q/R, 4.19-Q/R & 5.4-R kernels.
 
-DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_ether$4_14", TETHERING_UID, TETHERING_GID,
+DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_ether$4_14", AID_ROOT, AID_NETWORK_STACK,
                            sched_cls_tether_downstream4_ether_4_14, KVER_4_14, KVER_5_8)
 (struct __sk_buff* skb) {
     return do_forward4(skb, ETHER, DOWNSTREAM, NO_UPDATETIME, KVER_4_14);
 }
 
-DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_ether$4_14", TETHERING_UID, TETHERING_GID,
+DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_ether$4_14", AID_ROOT, AID_NETWORK_STACK,
                            sched_cls_tether_upstream4_ether_4_14, KVER_4_14, KVER_5_8)
 (struct __sk_buff* skb) {
     return do_forward4(skb, ETHER, UPSTREAM, NO_UPDATETIME, KVER_4_14);
@@ -778,43 +723,43 @@
 
 // RAWIP: 4.9-P/Q, 4.14-P/Q & 4.19-Q kernels -- without bpf_skb_change_head() for tc programs
 
-DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_rawip$stub", TETHERING_UID, TETHERING_GID,
+DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_rawip$stub", AID_ROOT, AID_NETWORK_STACK,
                            sched_cls_tether_downstream4_rawip_stub, KVER_NONE, KVER_5_4)
-(struct __sk_buff* skb) {
+(__unused struct __sk_buff* skb) {
     return TC_ACT_PIPE;
 }
 
-DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_rawip$stub", TETHERING_UID, TETHERING_GID,
+DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_rawip$stub", AID_ROOT, AID_NETWORK_STACK,
                            sched_cls_tether_upstream4_rawip_stub, KVER_NONE, KVER_5_4)
-(struct __sk_buff* skb) {
+(__unused struct __sk_buff* skb) {
     return TC_ACT_PIPE;
 }
 
 // ETHER: 4.9-P/Q kernel
 
-DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_ether$stub", TETHERING_UID, TETHERING_GID,
+DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_ether$stub", AID_ROOT, AID_NETWORK_STACK,
                            sched_cls_tether_downstream4_ether_stub, KVER_NONE, KVER_4_14)
-(struct __sk_buff* skb) {
+(__unused struct __sk_buff* skb) {
     return TC_ACT_PIPE;
 }
 
-DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_ether$stub", TETHERING_UID, TETHERING_GID,
+DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_ether$stub", AID_ROOT, AID_NETWORK_STACK,
                            sched_cls_tether_upstream4_ether_stub, KVER_NONE, KVER_4_14)
-(struct __sk_buff* skb) {
+(__unused struct __sk_buff* skb) {
     return TC_ACT_PIPE;
 }
 
 // ----- XDP Support -----
 
-DEFINE_BPF_MAP_GRW(tether_dev_map, DEVMAP_HASH, uint32_t, uint32_t, 64, TETHERING_GID)
+DEFINE_BPF_MAP_GRW(tether_dev_map, DEVMAP_HASH, uint32_t, uint32_t, 64, AID_NETWORK_STACK)
 
-static inline __always_inline int do_xdp_forward6(struct xdp_md *ctx, const struct rawip_bool rawip,
-        const struct stream_bool stream) {
+static inline __always_inline int do_xdp_forward6(__unused struct xdp_md *ctx,
+        __unused const struct rawip_bool rawip, __unused const struct stream_bool stream) {
     return XDP_PASS;
 }
 
-static inline __always_inline int do_xdp_forward4(struct xdp_md *ctx, const struct rawip_bool rawip,
-        const struct stream_bool stream) {
+static inline __always_inline int do_xdp_forward4(__unused struct xdp_md *ctx,
+        __unused const struct rawip_bool rawip, __unused const struct stream_bool stream) {
     return XDP_PASS;
 }
 
@@ -853,7 +798,7 @@
 }
 
 #define DEFINE_XDP_PROG(str, func) \
-    DEFINE_BPF_PROG_KVER(str, TETHERING_UID, TETHERING_GID, func, KVER_5_9)(struct xdp_md *ctx)
+    DEFINE_BPF_PROG_KVER(str, AID_ROOT, AID_NETWORK_STACK, func, KVER_5_9)(struct xdp_md *ctx)
 
 DEFINE_XDP_PROG("xdp/tether_downstream_ether",
                  xdp_tether_downstream_ether) {
@@ -877,4 +822,3 @@
 
 LICENSE("Apache 2.0");
 CRITICAL("Connectivity (Tethering)");
-DISABLE_BTF_ON_USER_BUILDS();
diff --git a/bpf_progs/test.c b/bpf_progs/test.c
index 6a4471c..bce402e 100644
--- a/bpf_progs/test.c
+++ b/bpf_progs/test.c
@@ -14,10 +14,6 @@
  * limitations under the License.
  */
 
-#include <linux/if_ether.h>
-#include <linux/in.h>
-#include <linux/ip.h>
-
 #ifdef MAINLINE
 // BTF is incompatible with bpfloaders < v0.10, hence for S (v0.2) we must
 // ship a different file than for later versions, but we need bpfloader v0.25+
@@ -29,30 +25,24 @@
 #define BPFLOADER_MAX_VER BPFLOADER_T_VERSION
 #endif /* MAINLINE */
 
-// Warning: values other than AID_ROOT don't work for map uid on BpfLoader < v0.21
-#define TETHERING_UID AID_ROOT
-
-#define TETHERING_GID AID_NETWORK_STACK
-
 // This is non production code, only used for testing
 // Needed because the bitmap array definition is non-kosher for pre-T OS devices.
 #define THIS_BPF_PROGRAM_IS_FOR_TEST_PURPOSES_ONLY
 
-#include "bpf_helpers.h"
 #include "bpf_net_helpers.h"
 #include "offload.h"
 
 // Used only by TetheringPrivilegedTests, not by production code.
 DEFINE_BPF_MAP_GRW(tether_downstream6_map, HASH, TetherDownstream6Key, Tether6Value, 16,
-                   TETHERING_GID)
+                   AID_NETWORK_STACK)
 DEFINE_BPF_MAP_GRW(tether2_downstream6_map, HASH, TetherDownstream6Key, Tether6Value, 16,
-                   TETHERING_GID)
+                   AID_NETWORK_STACK)
 DEFINE_BPF_MAP_GRW(tether3_downstream6_map, HASH, TetherDownstream6Key, Tether6Value, 16,
-                   TETHERING_GID)
+                   AID_NETWORK_STACK)
 // Used only by BpfBitmapTest, not by production code.
-DEFINE_BPF_MAP_GRW(bitmap, ARRAY, int, uint64_t, 2, TETHERING_GID)
+DEFINE_BPF_MAP_GRW(bitmap, ARRAY, int, uint64_t, 2, AID_NETWORK_STACK)
 
-DEFINE_BPF_PROG_KVER("xdp/drop_ipv4_udp_ether", TETHERING_UID, TETHERING_GID,
+DEFINE_BPF_PROG_KVER("xdp/drop_ipv4_udp_ether", AID_ROOT, AID_NETWORK_STACK,
                       xdp_test, KVER_5_9)
 (struct xdp_md *ctx) {
     void *data = (void *)(long)ctx->data;
@@ -71,4 +61,3 @@
 }
 
 LICENSE("Apache 2.0");
-DISABLE_BTF_ON_USER_BUILDS();
diff --git a/common/FlaggedApi.bp b/common/FlaggedApi.bp
index 21be1d3..39ff2d4 100644
--- a/common/FlaggedApi.bp
+++ b/common/FlaggedApi.bp
@@ -22,6 +22,16 @@
     visibility: ["//packages/modules/Connectivity:__subpackages__"],
 }
 
+java_aconfig_library {
+    name: "com.android.net.flags-aconfig-java",
+    aconfig_declarations: "com.android.net.flags-aconfig",
+    defaults: ["framework-minus-apex-aconfig-java-defaults"],
+    min_sdk_version: "30",
+    apex_available: [
+        "com.android.tethering",
+    ],
+}
+
 aconfig_declarations {
     name: "com.android.net.thread.flags-aconfig",
     package: "com.android.net.thread.flags",
@@ -30,6 +40,16 @@
     visibility: ["//packages/modules/Connectivity:__subpackages__"],
 }
 
+java_aconfig_library {
+    name: "com.android.net.thread.flags-aconfig-java",
+    aconfig_declarations: "com.android.net.thread.flags-aconfig",
+    defaults: ["framework-minus-apex-aconfig-java-defaults"],
+    min_sdk_version: "30",
+    apex_available: [
+        "com.android.tethering",
+    ],
+}
+
 aconfig_declarations {
     name: "nearby_flags",
     package: "com.android.nearby.flags",
@@ -37,3 +57,30 @@
     srcs: ["nearby_flags.aconfig"],
     visibility: ["//packages/modules/Connectivity:__subpackages__"],
 }
+
+java_aconfig_library {
+    name: "com.android.nearby.flags-aconfig-java",
+    aconfig_declarations: "nearby_flags",
+    defaults: ["framework-minus-apex-aconfig-java-defaults"],
+    min_sdk_version: "30",
+    apex_available: [
+        "com.android.tethering",
+    ],
+}
+
+aconfig_declarations {
+    name: "com.android.networksecurity.flags-aconfig",
+    package: "com.android.net.ct.flags",
+    container: "com.android.tethering",
+    srcs: ["networksecurity_flags.aconfig"],
+    visibility: ["//packages/modules/Connectivity:__subpackages__"],
+}
+
+java_aconfig_library {
+    name: "networksecurity_flags_java_lib",
+    aconfig_declarations: "com.android.networksecurity.flags-aconfig",
+    min_sdk_version: "30",
+    defaults: ["framework-minus-apex-aconfig-java-defaults"],
+    apex_available: ["com.android.tethering"],
+    visibility: ["//packages/modules/Connectivity:__subpackages__"],
+}
diff --git a/common/flags.aconfig b/common/flags.aconfig
index b320b61..45cbb78 100644
--- a/common/flags.aconfig
+++ b/common/flags.aconfig
@@ -10,6 +10,7 @@
   namespace: "android_core_networking"
   description: "Set data saver through ConnectivityManager API"
   bug: "297836825"
+  is_fixed_read_only: true
 }
 
 flag {
@@ -18,6 +19,7 @@
   namespace: "android_core_networking"
   description: "This flag controls whether isUidNetworkingBlocked is supported"
   bug: "297836825"
+  is_fixed_read_only: true
 }
 
 flag {
@@ -26,6 +28,7 @@
   namespace: "android_core_networking"
   description: "Block network access for apps in a low importance background state"
   bug: "304347838"
+  is_fixed_read_only: true
 }
 
 flag {
@@ -34,6 +37,7 @@
   namespace: "android_core_networking_ipsec"
   description: "The flag controls the access for getIpSecTransformState and IpSecTransformState"
   bug: "308011229"
+  is_fixed_read_only: true
 }
 
 flag {
@@ -42,6 +46,7 @@
   namespace: "android_core_networking"
   description: "The flag controls the access for the parcelable TetheringRequest with getSoftApConfiguration/setSoftApConfiguration API"
   bug: "216524590"
+  is_fixed_read_only: true
 }
 
 flag {
@@ -50,6 +55,7 @@
   namespace: "android_core_networking"
   description: "Flag for API to support requesting restricted wifi"
   bug: "315835605"
+  is_fixed_read_only: true
 }
 
 flag {
@@ -58,6 +64,7 @@
   namespace: "android_core_networking"
   description: "Flag for local network capability API"
   bug: "313000440"
+  is_fixed_read_only: true
 }
 
 flag {
@@ -66,6 +73,7 @@
   namespace: "android_core_networking"
   description: "Flag for satellite transport API"
   bug: "320514105"
+  is_fixed_read_only: true
 }
 
 flag {
@@ -74,6 +82,7 @@
   namespace: "android_core_networking"
   description: "Flag for API to support nsd subtypes"
   bug: "265095929"
+  is_fixed_read_only: true
 }
 
 flag {
@@ -82,6 +91,7 @@
   namespace: "android_core_networking"
   description: "Flag for API to register nsd offload engine"
   bug: "301713539"
+  is_fixed_read_only: true
 }
 
 flag {
@@ -90,6 +100,7 @@
   namespace: "android_core_networking"
   description: "Flag for metered network firewall chain API"
   bug: "332628891"
+  is_fixed_read_only: true
 }
 
 flag {
@@ -98,6 +109,7 @@
   namespace: "android_core_networking"
   description: "Flag for oem deny chains blocked reasons API"
   bug: "328732146"
+  is_fixed_read_only: true
 }
 
 flag {
@@ -106,6 +118,7 @@
   namespace: "android_core_networking"
   description: "Flag for BLOCKED_REASON_NETWORK_RESTRICTED API"
   bug: "339559837"
+  is_fixed_read_only: true
 }
 
 flag {
@@ -114,6 +127,7 @@
   namespace: "android_core_networking"
   description: "Flag for NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED API"
   bug: "343823469"
+  is_fixed_read_only: true
 }
 
 flag {
@@ -122,4 +136,14 @@
   namespace: "android_core_networking"
   description: "Flag for introducing TETHERING_VIRTUAL type"
   bug: "340376953"
+  is_fixed_read_only: true
+}
+
+flag {
+  name: "netstats_add_entries"
+  is_exported: true
+  namespace: "android_core_networking"
+  description: "Flag for NetworkStats#addEntries API"
+  bug: "335680025"
+  is_fixed_read_only: true
 }
diff --git a/common/networksecurity_flags.aconfig b/common/networksecurity_flags.aconfig
new file mode 100644
index 0000000..ef8ffcd
--- /dev/null
+++ b/common/networksecurity_flags.aconfig
@@ -0,0 +1,9 @@
+package: "com.android.net.ct.flags"
+container: "com.android.tethering"
+flag {
+    name: "certificate_transparency_service"
+    is_exported: true
+    namespace: "network_security"
+    description: "Enable service for certificate transparency log list data"
+    bug: "319829948"
+}
diff --git a/common/thread_flags.aconfig b/common/thread_flags.aconfig
index 43acd1b..0edb7a8 100644
--- a/common/thread_flags.aconfig
+++ b/common/thread_flags.aconfig
@@ -8,3 +8,11 @@
     description: "Controls whether the Android Thread feature is enabled"
     bug: "301473012"
 }
+
+flag {
+    name: "configuration_enabled"
+    is_exported: true
+    namespace: "thread_network"
+    description: "Controls whether the Android Thread configuration is enabled"
+    bug: "342519412"
+}
\ No newline at end of file
diff --git a/framework-t/Android.bp b/framework-t/Android.bp
index f076f5b..a05a529 100644
--- a/framework-t/Android.bp
+++ b/framework-t/Android.bp
@@ -62,6 +62,8 @@
     static_libs: [
         // Cannot go to framework-connectivity because mid_sdk checks require 31.
         "modules-utils-binary-xml",
+        "com.android.nearby.flags-aconfig-java",
+        "com.android.net.thread.flags-aconfig-java",
     ],
     impl_only_libs: [
         // The build system will use framework-bluetooth module_current stubs, because
@@ -97,20 +99,6 @@
     visibility: ["//packages/modules/Connectivity:__subpackages__"],
 }
 
-// The filegroup lists files that are necessary for verifying building mdns as a standalone,
-// for use with service-connectivity-mdns-standalone-build-test
-// This filegroup should never be included in anywhere in the module build. It is only used for
-// building service-connectivity-mdns-standalone-build-test target. The files will be renamed by
-// copybara to prevent them from being shadowed by the bootclasspath copies.
-filegroup {
-    name: "framework-connectivity-t-mdns-standalone-build-sources",
-    srcs: [
-        "src/android/net/nsd/OffloadEngine.java",
-        "src/android/net/nsd/OffloadServiceInfo.java",
-    ],
-    visibility: ["//packages/modules/Connectivity:__subpackages__"],
-}
-
 java_library {
     name: "framework-connectivity-t-pre-jarjar",
     defaults: ["framework-connectivity-t-defaults"],
@@ -137,6 +125,12 @@
     // framework-connectivity-pre-jarjar match at runtime.
     jarjar_rules: ":framework-connectivity-jarjar-rules",
     stub_only_libs: [
+        // static_libs is not used to compile stubs. So libs which have
+        // been included in static_libs might still need to
+        // be in stub_only_libs to be usable when generating the API stubs.
+        "com.android.net.flags-aconfig-java",
+        "com.android.nearby.flags-aconfig-java",
+        "com.android.net.thread.flags-aconfig-java",
         // Use prebuilt framework-connectivity stubs to avoid circular dependencies
         "sdk_module-lib_current_framework-connectivity",
     ],
diff --git a/framework-t/api/system-current.txt b/framework-t/api/system-current.txt
index 1f1953c..2354882 100644
--- a/framework-t/api/system-current.txt
+++ b/framework-t/api/system-current.txt
@@ -310,6 +310,7 @@
   public final class NetworkStats implements java.lang.Iterable<android.net.NetworkStats.Entry> android.os.Parcelable {
     ctor public NetworkStats(long, int);
     method @NonNull public android.net.NetworkStats add(@NonNull android.net.NetworkStats);
+    method @FlaggedApi("com.android.net.flags.netstats_add_entries") @NonNull public android.net.NetworkStats addEntries(@NonNull java.util.List<android.net.NetworkStats.Entry>);
     method @NonNull public android.net.NetworkStats addEntry(@NonNull android.net.NetworkStats.Entry);
     method public android.net.NetworkStats clone();
     method public int describeContents();
@@ -497,16 +498,26 @@
     field @NonNull public static final android.os.Parcelable.Creator<android.net.thread.PendingOperationalDataset> CREATOR;
   }
 
+  @FlaggedApi("com.android.net.thread.flags.configuration_enabled") public final class ThreadConfiguration implements android.os.Parcelable {
+    method public int describeContents();
+    method public boolean isDhcpv6PdEnabled();
+    method public boolean isNat64Enabled();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.net.thread.ThreadConfiguration> CREATOR;
+  }
+
   @FlaggedApi("com.android.net.thread.flags.thread_enabled") public final class ThreadNetworkController {
     method public void createRandomizedDataset(@NonNull String, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<android.net.thread.ActiveOperationalDataset,android.net.thread.ThreadNetworkException>);
     method public int getThreadVersion();
     method public static boolean isAttached(int);
     method @RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED") public void join(@NonNull android.net.thread.ActiveOperationalDataset, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.net.thread.ThreadNetworkException>);
     method @RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED") public void leave(@NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.net.thread.ThreadNetworkException>);
+    method @FlaggedApi("com.android.net.thread.flags.configuration_enabled") @RequiresPermission(android.Manifest.permission.THREAD_NETWORK_PRIVILEGED) public void registerConfigurationCallback(@NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<android.net.thread.ThreadConfiguration>);
     method @RequiresPermission(allOf={android.Manifest.permission.ACCESS_NETWORK_STATE, "android.permission.THREAD_NETWORK_PRIVILEGED"}) public void registerOperationalDatasetCallback(@NonNull java.util.concurrent.Executor, @NonNull android.net.thread.ThreadNetworkController.OperationalDatasetCallback);
     method @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public void registerStateCallback(@NonNull java.util.concurrent.Executor, @NonNull android.net.thread.ThreadNetworkController.StateCallback);
     method @RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED") public void scheduleMigration(@NonNull android.net.thread.PendingOperationalDataset, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.net.thread.ThreadNetworkException>);
     method @RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED") public void setEnabled(boolean, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.net.thread.ThreadNetworkException>);
+    method @FlaggedApi("com.android.net.thread.flags.configuration_enabled") @RequiresPermission(android.Manifest.permission.THREAD_NETWORK_PRIVILEGED) public void unregisterConfigurationCallback(@NonNull java.util.function.Consumer<android.net.thread.ThreadConfiguration>);
     method @RequiresPermission(allOf={android.Manifest.permission.ACCESS_NETWORK_STATE, "android.permission.THREAD_NETWORK_PRIVILEGED"}) public void unregisterOperationalDatasetCallback(@NonNull android.net.thread.ThreadNetworkController.OperationalDatasetCallback);
     method @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public void unregisterStateCallback(@NonNull android.net.thread.ThreadNetworkController.StateCallback);
     field public static final int DEVICE_ROLE_CHILD = 2; // 0x2
diff --git a/framework-t/src/android/net/IpSecManager.java b/framework-t/src/android/net/IpSecManager.java
index 3f74e1c..39e2b5b 100644
--- a/framework-t/src/android/net/IpSecManager.java
+++ b/framework-t/src/android/net/IpSecManager.java
@@ -65,13 +65,6 @@
 public class IpSecManager {
     private static final String TAG = "IpSecManager";
 
-    // TODO : remove this class when udc-mainline-prod is abandoned and android.net.flags.Flags is
-    // available here
-    /** @hide */
-    public static class Flags {
-        static final String IPSEC_TRANSFORM_STATE = "com.android.net.flags.ipsec_transform_state";
-    }
-
     /**
      * Feature flag to declare the kernel support of updating IPsec SAs.
      *
diff --git a/framework-t/src/android/net/IpSecTransform.java b/framework-t/src/android/net/IpSecTransform.java
index 70c9bc8..35bd008 100644
--- a/framework-t/src/android/net/IpSecTransform.java
+++ b/framework-t/src/android/net/IpSecTransform.java
@@ -15,7 +15,6 @@
  */
 package android.net;
 
-import static android.net.IpSecManager.Flags.IPSEC_TRANSFORM_STATE;
 import static android.net.IpSecManager.INVALID_RESOURCE_ID;
 
 import android.annotation.CallbackExecutor;
@@ -35,6 +34,7 @@
 import android.util.Log;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.net.flags.Flags;
 
 import dalvik.system.CloseGuard;
 
@@ -220,7 +220,7 @@
      *     occurs.
      * @see IpSecTransformState
      */
-    @FlaggedApi(IPSEC_TRANSFORM_STATE)
+    @FlaggedApi(Flags.FLAG_IPSEC_TRANSFORM_STATE)
     public void requestIpSecTransformState(
             @CallbackExecutor @NonNull Executor executor,
             @NonNull OutcomeReceiver<IpSecTransformState, RuntimeException> callback) {
diff --git a/framework-t/src/android/net/IpSecTransformState.java b/framework-t/src/android/net/IpSecTransformState.java
index 5b80ae2..b6628ee 100644
--- a/framework-t/src/android/net/IpSecTransformState.java
+++ b/framework-t/src/android/net/IpSecTransformState.java
@@ -15,8 +15,6 @@
  */
 package android.net;
 
-import static android.net.IpSecManager.Flags.IPSEC_TRANSFORM_STATE;
-
 import static com.android.internal.annotations.VisibleForTesting.Visibility;
 
 import android.annotation.FlaggedApi;
@@ -26,6 +24,7 @@
 import android.os.SystemClock;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.net.flags.Flags;
 import com.android.net.module.util.HexDump;
 
 import java.util.Objects;
@@ -39,7 +38,7 @@
  * IpSecTransformStates at two timestamps. By comparing the changes in packet counts and sequence
  * numbers, callers can estimate IPsec data loss in the inbound direction.
  */
-@FlaggedApi(IPSEC_TRANSFORM_STATE)
+@FlaggedApi(Flags.FLAG_IPSEC_TRANSFORM_STATE)
 public final class IpSecTransformState implements Parcelable {
     private final long mTimestamp;
     private final long mTxHighestSequenceNumber;
@@ -197,7 +196,7 @@
      * <p>Except for testing, IPsec callers normally do not instantiate {@link IpSecTransformState}
      * themselves but instead get a reference via {@link IpSecTransformState}
      */
-    @FlaggedApi(IPSEC_TRANSFORM_STATE)
+    @FlaggedApi(Flags.FLAG_IPSEC_TRANSFORM_STATE)
     public static final class Builder {
         private long mTimestamp;
         private long mTxHighestSequenceNumber;
diff --git a/framework-t/src/android/net/NetworkStats.java b/framework-t/src/android/net/NetworkStats.java
index e9a3f58..a2c4fc3 100644
--- a/framework-t/src/android/net/NetworkStats.java
+++ b/framework-t/src/android/net/NetworkStats.java
@@ -18,6 +18,7 @@
 
 import static com.android.net.module.util.NetworkStatsUtils.multiplySafeByRational;
 
+import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -33,6 +34,7 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.modules.utils.build.SdkLevel;
+import com.android.net.flags.Flags;
 import com.android.net.module.util.CollectionUtils;
 
 import libcore.util.EmptyArray;
@@ -845,6 +847,21 @@
     }
 
     /**
+     * Adds multiple entries to a copy of this NetworkStats instance.
+     *
+     * @param entries The entries to add.
+     * @return A new NetworkStats instance with the added entries.
+     */
+    @FlaggedApi(Flags.FLAG_NETSTATS_ADD_ENTRIES)
+    public @NonNull NetworkStats addEntries(@NonNull final List<Entry> entries) {
+        final NetworkStats newStats = this.clone();
+        for (final Entry entry : Objects.requireNonNull(entries)) {
+            newStats.combineValues(entry);
+        }
+        return newStats;
+    }
+
+    /**
      * Add given values with an existing row, or create a new row if
      * {@link #findIndex(String, int, int, int, int, int, int)} is unable to find match. Can
      * also be used to subtract values from existing rows.
diff --git a/framework-t/src/android/net/nsd/DiscoveryRequest.java b/framework-t/src/android/net/nsd/DiscoveryRequest.java
index b0b71ea..b344943 100644
--- a/framework-t/src/android/net/nsd/DiscoveryRequest.java
+++ b/framework-t/src/android/net/nsd/DiscoveryRequest.java
@@ -24,12 +24,14 @@
 import android.os.Parcelable;
 import android.text.TextUtils;
 
+import com.android.net.flags.Flags;
+
 import java.util.Objects;
 
 /**
  * Encapsulates parameters for {@link NsdManager#discoverServices}.
  */
-@FlaggedApi(NsdManager.Flags.NSD_SUBTYPES_SUPPORT_ENABLED)
+@FlaggedApi(Flags.FLAG_NSD_SUBTYPES_SUPPORT_ENABLED)
 public final class DiscoveryRequest implements Parcelable {
     private final int mProtocolType;
 
diff --git a/framework-t/src/android/net/nsd/NsdManager.java b/framework-t/src/android/net/nsd/NsdManager.java
index b21e22a..116bea6 100644
--- a/framework-t/src/android/net/nsd/NsdManager.java
+++ b/framework-t/src/android/net/nsd/NsdManager.java
@@ -52,6 +52,7 @@
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.modules.utils.build.SdkLevel;
+import com.android.net.flags.Flags;
 import com.android.net.module.util.CollectionUtils;
 
 import java.lang.annotation.Retention;
@@ -148,22 +149,6 @@
     private static final String TAG = NsdManager.class.getSimpleName();
     private static final boolean DBG = false;
 
-    // TODO : remove this class when udc-mainline-prod is abandoned and android.net.flags.Flags is
-    // available here
-    /** @hide */
-    public static class Flags {
-        static final String REGISTER_NSD_OFFLOAD_ENGINE_API =
-                "com.android.net.flags.register_nsd_offload_engine_api";
-        static final String NSD_SUBTYPES_SUPPORT_ENABLED =
-                "com.android.net.flags.nsd_subtypes_support_enabled";
-        static final String ADVERTISE_REQUEST_API =
-                "com.android.net.flags.advertise_request_api";
-        static final String NSD_CUSTOM_HOSTNAME_ENABLED =
-                "com.android.net.flags.nsd_custom_hostname_enabled";
-        static final String NSD_CUSTOM_TTL_ENABLED =
-                "com.android.net.flags.nsd_custom_ttl_enabled";
-    }
-
     /**
      * A regex for the acceptable format of a type or subtype label.
      * @hide
@@ -451,7 +436,7 @@
      *
      * @hide
      */
-    @FlaggedApi(NsdManager.Flags.REGISTER_NSD_OFFLOAD_ENGINE_API)
+    @FlaggedApi(Flags.FLAG_REGISTER_NSD_OFFLOAD_ENGINE_API)
     @SystemApi
     @RequiresPermission(anyOf = {NETWORK_SETTINGS, PERMISSION_MAINLINE_NETWORK_STACK,
             NETWORK_STACK})
@@ -489,7 +474,7 @@
      *
      * @hide
      */
-    @FlaggedApi(NsdManager.Flags.REGISTER_NSD_OFFLOAD_ENGINE_API)
+    @FlaggedApi(Flags.FLAG_REGISTER_NSD_OFFLOAD_ENGINE_API)
     @SystemApi
     @RequiresPermission(anyOf = {NETWORK_SETTINGS, PERMISSION_MAINLINE_NETWORK_STACK,
             NETWORK_STACK})
@@ -1506,7 +1491,7 @@
      * @param listener  The listener notifies of a successful discovery and is used
      * to stop discovery on this serviceType through a call on {@link #stopServiceDiscovery}.
      */
-    @FlaggedApi(Flags.NSD_SUBTYPES_SUPPORT_ENABLED)
+    @FlaggedApi(Flags.FLAG_NSD_SUBTYPES_SUPPORT_ENABLED)
     public void discoverServices(@NonNull DiscoveryRequest discoveryRequest,
             @NonNull Executor executor, @NonNull DiscoveryListener listener) {
         int key = putListener(listener, executor, discoveryRequest);
diff --git a/framework-t/src/android/net/nsd/NsdServiceInfo.java b/framework-t/src/android/net/nsd/NsdServiceInfo.java
index d8cccb2..18c59d9 100644
--- a/framework-t/src/android/net/nsd/NsdServiceInfo.java
+++ b/framework-t/src/android/net/nsd/NsdServiceInfo.java
@@ -30,6 +30,7 @@
 import android.util.ArraySet;
 import android.util.Log;
 
+import com.android.net.flags.Flags;
 import com.android.net.module.util.InetAddressUtils;
 
 import java.io.UnsupportedEncodingException;
@@ -527,7 +528,7 @@
      * Only one subtype will be registered if multiple elements of {@code subtypes} have the same
      * case-insensitive value.
      */
-    @FlaggedApi(NsdManager.Flags.NSD_SUBTYPES_SUPPORT_ENABLED)
+    @FlaggedApi(Flags.FLAG_NSD_SUBTYPES_SUPPORT_ENABLED)
     public void setSubtypes(@NonNull Set<String> subtypes) {
         mSubtypes.clear();
         mSubtypes.addAll(subtypes);
@@ -540,7 +541,7 @@
      * NsdManager.DiscoveryListener}), the return value may or may not include the subtypes of this
      * service.
      */
-    @FlaggedApi(NsdManager.Flags.NSD_SUBTYPES_SUPPORT_ENABLED)
+    @FlaggedApi(Flags.FLAG_NSD_SUBTYPES_SUPPORT_ENABLED)
     @NonNull
     public Set<String> getSubtypes() {
         return Collections.unmodifiableSet(mSubtypes);
diff --git a/framework-t/src/android/net/nsd/OffloadEngine.java b/framework-t/src/android/net/nsd/OffloadEngine.java
index 9015985..06655fa 100644
--- a/framework-t/src/android/net/nsd/OffloadEngine.java
+++ b/framework-t/src/android/net/nsd/OffloadEngine.java
@@ -21,6 +21,8 @@
 import android.annotation.NonNull;
 import android.annotation.SystemApi;
 
+import com.android.net.flags.Flags;
+
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 
@@ -34,7 +36,7 @@
  *
  * @hide
  */
-@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")
+@FlaggedApi(Flags.FLAG_REGISTER_NSD_OFFLOAD_ENGINE_API)
 @SystemApi
 public interface OffloadEngine {
     /**
diff --git a/framework-t/src/android/net/nsd/OffloadServiceInfo.java b/framework-t/src/android/net/nsd/OffloadServiceInfo.java
index 98dc83a..e4b2f43 100644
--- a/framework-t/src/android/net/nsd/OffloadServiceInfo.java
+++ b/framework-t/src/android/net/nsd/OffloadServiceInfo.java
@@ -26,6 +26,7 @@
 import android.os.Parcel;
 import android.os.Parcelable;
 
+import com.android.net.flags.Flags;
 import com.android.net.module.util.HexDump;
 
 import java.util.Arrays;
@@ -40,7 +41,7 @@
  *
  * @hide
  */
-@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")
+@FlaggedApi(Flags.FLAG_REGISTER_NSD_OFFLOAD_ENGINE_API)
 @SystemApi
 @RequiresApi(Build.VERSION_CODES.TIRAMISU)
 public final class OffloadServiceInfo implements Parcelable {
diff --git a/framework/Android.bp b/framework/Android.bp
index 4eda0aa..4c4f792 100644
--- a/framework/Android.bp
+++ b/framework/Android.bp
@@ -63,6 +63,7 @@
         ":framework-connectivity-sources",
         ":net-utils-framework-common-srcs",
         ":framework-connectivity-api-shared-srcs",
+        ":framework-networksecurity-sources",
     ],
     aidl: {
         generate_get_transaction_name: true,
@@ -86,12 +87,14 @@
         "framework-wifi.stubs.module_lib",
     ],
     static_libs: [
+        "com.android.net.flags-aconfig-java",
         // Not using the latest stable version because all functions in the latest version of
         // mdns_aidl_interface are deprecated.
         "mdns_aidl_interface-V1-java",
         "modules-utils-backgroundthread",
         "modules-utils-build",
         "modules-utils-preconditions",
+        "networksecurity_flags_java_lib",
         "framework-connectivity-javastream-protos",
     ],
     impl_only_static_libs: [
@@ -108,14 +111,13 @@
     ],
 }
 
+// Library to allow Cronet to use hidden APIs
 java_library {
-    name: "framework-connectivity-pre-jarjar",
+    name: "framework-connectivity-pre-jarjar-without-cronet",
     defaults: [
         "framework-connectivity-defaults",
     ],
     static_libs: [
-        "httpclient_api",
-        "httpclient_impl",
         // Framework-connectivity-pre-jarjar is identical to framework-connectivity
         // implementation, but without the jarjar rules. However, framework-connectivity
         // is not based on framework-connectivity-pre-jarjar, it's rebuilt from source
@@ -133,6 +135,21 @@
         "framework-tethering.impl",
         "framework-wifi.stubs.module_lib",
     ],
+    visibility: ["//external/cronet:__subpackages__"],
+}
+
+java_library {
+    name: "framework-connectivity-pre-jarjar",
+    defaults: ["framework-module-defaults"],
+    min_sdk_version: "30",
+    static_libs: [
+        "framework-connectivity-pre-jarjar-without-cronet",
+        "httpclient_api",
+        "httpclient_impl",
+    ],
+    apex_available: [
+        "com.android.tethering",
+    ],
     visibility: ["//packages/modules/Connectivity:__subpackages__"],
 }
 
@@ -192,6 +209,7 @@
     },
     aconfig_declarations: [
         "com.android.net.flags-aconfig",
+        "com.android.networksecurity.flags-aconfig",
     ],
 }
 
@@ -292,6 +310,7 @@
     srcs: [
         ":framework-connectivity-sources",
         ":framework-connectivity-tiramisu-updatable-sources",
+        ":framework-networksecurity-sources",
         ":framework-nearby-java-sources",
         ":framework-thread-sources",
     ],
diff --git a/framework/src/android/net/BpfNetMapsUtils.java b/framework/src/android/net/BpfNetMapsUtils.java
index 282a11e..1760fa7 100644
--- a/framework/src/android/net/BpfNetMapsUtils.java
+++ b/framework/src/android/net/BpfNetMapsUtils.java
@@ -261,6 +261,12 @@
             IBpfMap<S32, UidOwnerValue> uidOwnerMap,
             IBpfMap<S32, U8> dataSaverEnabledMap
     ) {
+        // System uids are not blocked by firewall chains, see bpf_progs/netd.c
+        // TODO: b/348513058 - use UserHandle.isCore() once it is accessible
+        if (UserHandle.getAppId(uid) < Process.FIRST_APPLICATION_UID) {
+            return BLOCKED_REASON_NONE;
+        }
+
         final long uidRuleConfig;
         final long uidMatch;
         try {
@@ -331,12 +337,6 @@
     ) {
         throwIfPreT("isUidBlockedByFirewallChains is not available on pre-T devices");
 
-        // System uids are not blocked by firewall chains, see bpf_progs/netd.c
-        // TODO: b/348513058 - use UserHandle.isCore() once it is accessible
-        if (UserHandle.getAppId(uid) < Process.FIRST_APPLICATION_UID) {
-            return false;
-        }
-
         final int blockedReasons = getUidNetworkingBlockedReasons(
                 uid,
                 configurationMap,
diff --git a/framework/src/android/net/ConnectivityManager.java b/framework/src/android/net/ConnectivityManager.java
index 5e41dd9..63a6cd2 100644
--- a/framework/src/android/net/ConnectivityManager.java
+++ b/framework/src/android/net/ConnectivityManager.java
@@ -78,6 +78,7 @@
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.modules.utils.build.SdkLevel;
+import com.android.net.flags.Flags;
 
 import libcore.net.event.NetworkEventDispatcher;
 
@@ -125,24 +126,6 @@
     private static final String TAG = "ConnectivityManager";
     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
 
-    // TODO : remove this class when udc-mainline-prod is abandoned and android.net.flags.Flags is
-    // available here
-    /** @hide */
-    public static class Flags {
-        static final String SET_DATA_SAVER_VIA_CM =
-                "com.android.net.flags.set_data_saver_via_cm";
-        static final String SUPPORT_IS_UID_NETWORKING_BLOCKED =
-                "com.android.net.flags.support_is_uid_networking_blocked";
-        static final String BASIC_BACKGROUND_RESTRICTIONS_ENABLED =
-                "com.android.net.flags.basic_background_restrictions_enabled";
-        static final String METERED_NETWORK_FIREWALL_CHAINS =
-                "com.android.net.flags.metered_network_firewall_chains";
-        static final String BLOCKED_REASON_OEM_DENY_CHAINS =
-                "com.android.net.flags.blocked_reason_oem_deny_chains";
-        static final String BLOCKED_REASON_NETWORK_RESTRICTED =
-                "com.android.net.flags.blocked_reason_network_restricted";
-    }
-
     /**
      * A change in network connectivity has occurred. A default connection has either
      * been established or lost. The NetworkInfo for the affected network is
@@ -919,7 +902,7 @@
      *
      * @hide
      */
-    @FlaggedApi(Flags.BASIC_BACKGROUND_RESTRICTIONS_ENABLED)
+    @FlaggedApi(Flags.FLAG_BASIC_BACKGROUND_RESTRICTIONS_ENABLED)
     @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
     public static final int BLOCKED_REASON_APP_BACKGROUND = 1 << 6;
 
@@ -932,7 +915,7 @@
      * @see #FIREWALL_CHAIN_OEM_DENY_3
      * @hide
      */
-    @FlaggedApi(Flags.BLOCKED_REASON_OEM_DENY_CHAINS)
+    @FlaggedApi(Flags.FLAG_BLOCKED_REASON_OEM_DENY_CHAINS)
     @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
     public static final int BLOCKED_REASON_OEM_DENY = 1 << 7;
 
@@ -943,7 +926,7 @@
      *
      * @hide
      */
-    @FlaggedApi(Flags.BLOCKED_REASON_NETWORK_RESTRICTED)
+    @FlaggedApi(Flags.FLAG_BLOCKED_REASON_NETWORK_RESTRICTED)
     @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
     public static final int BLOCKED_REASON_NETWORK_RESTRICTED = 1 << 8;
 
@@ -1052,7 +1035,7 @@
      * exempted for specific situations while in the background.
      * @hide
      */
-    @FlaggedApi(Flags.BASIC_BACKGROUND_RESTRICTIONS_ENABLED)
+    @FlaggedApi(Flags.FLAG_BASIC_BACKGROUND_RESTRICTIONS_ENABLED)
     @SystemApi(client = MODULE_LIBRARIES)
     public static final int FIREWALL_CHAIN_BACKGROUND = 6;
 
@@ -1120,7 +1103,7 @@
      * @hide
      */
     // TODO: Merge this chain with data saver and support setFirewallChainEnabled
-    @FlaggedApi(Flags.METERED_NETWORK_FIREWALL_CHAINS)
+    @FlaggedApi(Flags.FLAG_METERED_NETWORK_FIREWALL_CHAINS)
     @SystemApi(client = MODULE_LIBRARIES)
     public static final int FIREWALL_CHAIN_METERED_ALLOW = 10;
 
@@ -1139,7 +1122,7 @@
      * @hide
      */
     // TODO: Support setFirewallChainEnabled to control this chain
-    @FlaggedApi(Flags.METERED_NETWORK_FIREWALL_CHAINS)
+    @FlaggedApi(Flags.FLAG_METERED_NETWORK_FIREWALL_CHAINS)
     @SystemApi(client = MODULE_LIBRARIES)
     public static final int FIREWALL_CHAIN_METERED_DENY_USER = 11;
 
@@ -1158,7 +1141,7 @@
      * @hide
      */
     // TODO: Support setFirewallChainEnabled to control this chain
-    @FlaggedApi(Flags.METERED_NETWORK_FIREWALL_CHAINS)
+    @FlaggedApi(Flags.FLAG_METERED_NETWORK_FIREWALL_CHAINS)
     @SystemApi(client = MODULE_LIBRARIES)
     public static final int FIREWALL_CHAIN_METERED_DENY_ADMIN = 12;
 
@@ -4163,6 +4146,8 @@
          */
         @FilteredCallback(methodId = METHOD_ONAVAILABLE_5ARGS,
                 calledByCallbackId = CALLBACK_AVAILABLE,
+                // If this list is modified, ConnectivityService#addAvailableStateUpdateCallbacks
+                // needs to be updated too.
                 mayCall = { METHOD_ONAVAILABLE_4ARGS,
                         METHOD_ONLOCALNETWORKINFOCHANGED,
                         METHOD_ONBLOCKEDSTATUSCHANGED_INT })
@@ -4193,6 +4178,8 @@
          */
         @FilteredCallback(methodId = METHOD_ONAVAILABLE_4ARGS,
                 calledByCallbackId = CALLBACK_TRANSITIVE_CALLS_ONLY,
+                // If this list is modified, ConnectivityService#addAvailableStateUpdateCallbacks
+                // needs to be updated too.
                 mayCall = { METHOD_ONAVAILABLE_1ARG,
                         METHOD_ONNETWORKSUSPENDED,
                         METHOD_ONCAPABILITIESCHANGED,
@@ -4489,6 +4476,7 @@
     public static final int CALLBACK_BLK_CHANGED                = 11;
     /** @hide */
     public static final int CALLBACK_LOCAL_NETWORK_INFO_CHANGED = 12;
+    // When adding new IDs, note CallbackQueue assumes callback IDs are at most 16 bits.
 
 
     /** @hide */
@@ -6448,7 +6436,7 @@
      * @throws IllegalStateException if failed.
      * @hide
      */
-    @FlaggedApi(Flags.SET_DATA_SAVER_VIA_CM)
+    @FlaggedApi(Flags.FLAG_SET_DATA_SAVER_VIA_CM)
     @SystemApi(client = MODULE_LIBRARIES)
     @RequiresPermission(anyOf = {
             android.Manifest.permission.NETWORK_SETTINGS,
@@ -6709,7 +6697,7 @@
     // is provided by linux file group permission AID_NET_BW_ACCT and the
     // selinux context fs_bpf_net*.
     // Only the system server process and the network stack have access.
-    @FlaggedApi(Flags.SUPPORT_IS_UID_NETWORKING_BLOCKED)
+    @FlaggedApi(Flags.FLAG_SUPPORT_IS_UID_NETWORKING_BLOCKED)
     @SystemApi(client = MODULE_LIBRARIES)
     // Note b/326143935 kernel bug can trigger crash on some T device.
     @RequiresApi(VERSION_CODES.UPSIDE_DOWN_CAKE)
diff --git a/framework/src/android/net/NetworkCapabilities.java b/framework/src/android/net/NetworkCapabilities.java
index 6a14bde..4a50397 100644
--- a/framework/src/android/net/NetworkCapabilities.java
+++ b/framework/src/android/net/NetworkCapabilities.java
@@ -41,6 +41,7 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.modules.utils.build.SdkLevel;
+import com.android.net.flags.Flags;
 import com.android.net.module.util.BitUtils;
 import com.android.net.module.util.CollectionUtils;
 import com.android.net.module.util.NetworkCapabilitiesUtils;
@@ -124,22 +125,6 @@
 public final class NetworkCapabilities implements Parcelable {
     private static final String TAG = "NetworkCapabilities";
 
-    // TODO : remove this class when udc-mainline-prod is abandoned and android.net.flags.Flags is
-    // available here
-    /** @hide */
-    public static class Flags {
-        static final String FLAG_FORBIDDEN_CAPABILITY =
-                "com.android.net.flags.forbidden_capability";
-        static final String FLAG_NET_CAPABILITY_LOCAL_NETWORK =
-                "com.android.net.flags.net_capability_local_network";
-        static final String REQUEST_RESTRICTED_WIFI =
-                "com.android.net.flags.request_restricted_wifi";
-        static final String SUPPORT_TRANSPORT_SATELLITE =
-                "com.android.net.flags.support_transport_satellite";
-        static final String NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED =
-                "com.android.net.flags.net_capability_not_bandwidth_constrained";
-    }
-
     /**
      * Mechanism to support redaction of fields in NetworkCapabilities that are guarded by specific
      * app permissions.
@@ -761,7 +746,7 @@
      * usage on constrained networks, such as disabling network access to apps that are not in the
      * foreground.
      */
-    @FlaggedApi(Flags.NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED)
+    @FlaggedApi(Flags.FLAG_NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED)
     public static final int NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED = 37;
 
     private static final int MAX_NET_CAPABILITY = NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED;
@@ -1374,7 +1359,7 @@
     /**
      * Indicates this network uses a Satellite transport.
      */
-    @FlaggedApi(Flags.SUPPORT_TRANSPORT_SATELLITE)
+    @FlaggedApi(Flags.FLAG_SUPPORT_TRANSPORT_SATELLITE)
     public static final int TRANSPORT_SATELLITE = 10;
 
     /** @hide */
@@ -2864,7 +2849,7 @@
      * @return
      */
     @NonNull
-    @FlaggedApi(Flags.REQUEST_RESTRICTED_WIFI)
+    @FlaggedApi(Flags.FLAG_REQUEST_RESTRICTED_WIFI)
     public Set<Integer> getSubscriptionIds() {
         return new ArraySet<>(mSubIds);
     }
diff --git a/framework/src/android/net/NetworkRequest.java b/framework/src/android/net/NetworkRequest.java
index 502ac6f..89572b3 100644
--- a/framework/src/android/net/NetworkRequest.java
+++ b/framework/src/android/net/NetworkRequest.java
@@ -50,6 +50,8 @@
 import android.text.TextUtils;
 import android.util.Range;
 
+import com.android.net.flags.Flags;
+
 import java.util.Arrays;
 import java.util.List;
 import java.util.Objects;
@@ -144,12 +146,6 @@
  * Look up the specific capability to learn whether its usage requires this self-certification.
  */
 public class NetworkRequest implements Parcelable {
-
-    /** @hide */
-    public static class Flags {
-        static final String REQUEST_RESTRICTED_WIFI =
-                "com.android.net.flags.request_restricted_wifi";
-    }
     /**
      * The first requestId value that will be allocated.
      * @hide only used by ConnectivityService.
@@ -616,7 +612,7 @@
          * @param subIds A {@code Set} that represents subscription IDs.
          */
         @NonNull
-        @FlaggedApi(Flags.REQUEST_RESTRICTED_WIFI)
+        @FlaggedApi(Flags.FLAG_REQUEST_RESTRICTED_WIFI)
         public Builder setSubscriptionIds(@NonNull Set<Integer> subIds) {
             mNetworkCapabilities.setSubscriptionIds(subIds);
             return this;
@@ -880,7 +876,7 @@
      * @return Set of Integer values for this instance.
      */
     @NonNull
-    @FlaggedApi(Flags.REQUEST_RESTRICTED_WIFI)
+    @FlaggedApi(Flags.FLAG_REQUEST_RESTRICTED_WIFI)
     public Set<Integer> getSubscriptionIds() {
         // No need to make a defensive copy here as NC#getSubscriptionIds() already returns
         // a new set.
diff --git a/nearby/framework/Android.bp b/nearby/framework/Android.bp
index 41a28a0..f84ddcf 100644
--- a/nearby/framework/Android.bp
+++ b/nearby/framework/Android.bp
@@ -54,6 +54,7 @@
     ],
     static_libs: [
         "modules-utils-preconditions",
+        "com.android.nearby.flags-aconfig-java",
     ],
     visibility: [
         "//packages/modules/Connectivity/nearby/tests:__subpackages__",
diff --git a/nearby/framework/java/android/nearby/NearbyManager.java b/nearby/framework/java/android/nearby/NearbyManager.java
index cae653d..39adee3 100644
--- a/nearby/framework/java/android/nearby/NearbyManager.java
+++ b/nearby/framework/java/android/nearby/NearbyManager.java
@@ -37,6 +37,7 @@
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.util.Preconditions;
+import com.android.nearby.flags.Flags;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -84,7 +85,7 @@
      * Return value of {@link #getPoweredOffFindingMode()} when this powered off finding is not
      * supported the device.
      */
-    @FlaggedApi("com.android.nearby.flags.powered_off_finding")
+    @FlaggedApi(Flags.FLAG_POWERED_OFF_FINDING)
     public static final int POWERED_OFF_FINDING_MODE_UNSUPPORTED = 0;
 
     /**
@@ -92,7 +93,7 @@
      * #setPoweredOffFindingMode(int)} when powered off finding is supported but disabled. The
      * device will not start to advertise when powered off.
      */
-    @FlaggedApi("com.android.nearby.flags.powered_off_finding")
+    @FlaggedApi(Flags.FLAG_POWERED_OFF_FINDING)
     public static final int POWERED_OFF_FINDING_MODE_DISABLED = 1;
 
     /**
@@ -100,7 +101,7 @@
      * #setPoweredOffFindingMode(int)} when powered off finding is enabled. The device will start to
      * advertise when powered off.
      */
-    @FlaggedApi("com.android.nearby.flags.powered_off_finding")
+    @FlaggedApi(Flags.FLAG_POWERED_OFF_FINDING)
     public static final int POWERED_OFF_FINDING_MODE_ENABLED = 2;
 
     /**
@@ -526,7 +527,7 @@
      *
      * @throws IllegalArgumentException if the length of one of the EIDs is not 20 bytes
      */
-    @FlaggedApi("com.android.nearby.flags.powered_off_finding")
+    @FlaggedApi(Flags.FLAG_POWERED_OFF_FINDING)
     @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)
     public void setPoweredOffFindingEphemeralIds(@NonNull List<byte[]> eids) {
         Objects.requireNonNull(eids);
@@ -570,7 +571,7 @@
      * @throws IllegalStateException if called with {@link #POWERED_OFF_FINDING_MODE_ENABLED} when
      * Bluetooth or location services are disabled
      */
-    @FlaggedApi("com.android.nearby.flags.powered_off_finding")
+    @FlaggedApi(Flags.FLAG_POWERED_OFF_FINDING)
     @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)
     public void setPoweredOffFindingMode(@PoweredOffFindingMode int poweredOffFindingMode) {
         Preconditions.checkArgument(
@@ -602,7 +603,7 @@
      * #POWERED_OFF_FINDING_MODE_ENABLED} if this was the last value set by {@link
      * #setPoweredOffFindingMode(int)}
      */
-    @FlaggedApi("com.android.nearby.flags.powered_off_finding")
+    @FlaggedApi(Flags.FLAG_POWERED_OFF_FINDING)
     @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)
     public @PoweredOffFindingMode int getPoweredOffFindingMode() {
         if (!isPoweredOffFindingSupported()) {
diff --git a/netbpfload/Android.bp b/netbpfload/Android.bp
index 908bb13..b8c0ce7 100644
--- a/netbpfload/Android.bp
+++ b/netbpfload/Android.bp
@@ -48,10 +48,7 @@
         "libbase",
         "liblog",
     ],
-    srcs: [
-        "loader.cpp",
-        "NetBpfLoad.cpp",
-    ],
+    srcs: ["NetBpfLoad.cpp"],
     apex_available: [
         "com.android.tethering",
         "//apex_available:platform",
diff --git a/netbpfload/NetBpfLoad.cpp b/netbpfload/NetBpfLoad.cpp
index 0d4a5c4..00362b4 100644
--- a/netbpfload/NetBpfLoad.cpp
+++ b/netbpfload/NetBpfLoad.cpp
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2017-2023 The Android Open Source Project
+ * Copyright (C) 2018-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.
@@ -14,50 +14,1138 @@
  * limitations under the License.
  */
 
-#ifndef LOG_TAG
 #define LOG_TAG "NetBpfLoad"
-#endif
 
 #include <arpa/inet.h>
+#include <cstdlib>
 #include <dirent.h>
 #include <elf.h>
+#include <errno.h>
 #include <error.h>
 #include <fcntl.h>
+#include <fstream>
 #include <inttypes.h>
+#include <iostream>
 #include <linux/bpf.h>
+#include <linux/elf.h>
 #include <linux/unistd.h>
+#include <log/log.h>
 #include <net/if.h>
+#include <optional>
 #include <stdint.h>
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
-#include <unistd.h>
-
+#include <string>
 #include <sys/mman.h>
 #include <sys/socket.h>
 #include <sys/stat.h>
 #include <sys/types.h>
+#include <sys/utsname.h>
+#include <sys/wait.h>
+#include <sysexits.h>
+#include <unistd.h>
+#include <unordered_map>
+#include <vector>
 
-#include <android/api-level.h>
+#include <android-base/cmsg.h>
+#include <android-base/file.h>
 #include <android-base/logging.h>
 #include <android-base/macros.h>
 #include <android-base/properties.h>
 #include <android-base/stringprintf.h>
 #include <android-base/strings.h>
 #include <android-base/unique_fd.h>
-#include <log/log.h>
+#include <android/api-level.h>
 
 #include "BpfSyscallWrappers.h"
 #include "bpf/BpfUtils.h"
-#include "loader.h"
+#include "bpf/bpf_map_def.h"
+
+using android::base::EndsWith;
+using android::base::StartsWith;
+using android::base::unique_fd;
+using std::ifstream;
+using std::ios;
+using std::optional;
+using std::string;
+using std::vector;
 
 namespace android {
 namespace bpf {
 
-using base::StartsWith;
-using base::EndsWith;
-using std::string;
-using std::vector;
+// Bpf programs may specify per-program & per-map selinux_context and pin_subdir.
+//
+// The BpfLoader needs to convert these bpf.o specified strings into an enum
+// for internal use (to check that valid values were specified for the specific
+// location of the bpf.o file).
+//
+// It also needs to map selinux_context's into pin_subdir's.
+// This is because of how selinux_context is actually implemented via pin+rename.
+//
+// Thus 'domain' enumerates all selinux_context's/pin_subdir's that the BpfLoader
+// is aware of.  Thus there currently needs to be a 1:1 mapping between the two.
+//
+enum class domain : int {
+    unspecified = 0,    // means just use the default for that specific pin location
+    tethering,          // (S+) fs_bpf_tethering     /sys/fs/bpf/tethering
+    net_private,        // (T+) fs_bpf_net_private   /sys/fs/bpf/net_private
+    net_shared,         // (T+) fs_bpf_net_shared    /sys/fs/bpf/net_shared
+    netd_readonly,      // (T+) fs_bpf_netd_readonly /sys/fs/bpf/netd_readonly
+    netd_shared,        // (T+) fs_bpf_netd_shared   /sys/fs/bpf/netd_shared
+};
+
+static constexpr domain AllDomains[] = {
+    domain::unspecified,
+    domain::tethering,
+    domain::net_private,
+    domain::net_shared,
+    domain::netd_readonly,
+    domain::netd_shared,
+};
+
+static constexpr bool specified(domain d) {
+    return d != domain::unspecified;
+}
+
+struct Location {
+    const char* const dir = "";
+    const char* const prefix = "";
+};
+
+// Returns the build type string (from ro.build.type).
+const std::string& getBuildType() {
+    static std::string t = android::base::GetProperty("ro.build.type", "unknown");
+    return t;
+}
+
+// The following functions classify the 3 Android build types.
+inline bool isEng() {
+    return getBuildType() == "eng";
+}
+
+inline bool isUser() {
+    return getBuildType() == "user";
+}
+
+inline bool isUserdebug() {
+    return getBuildType() == "userdebug";
+}
+
+#define BPF_FS_PATH "/sys/fs/bpf/"
+
+// Size of the BPF log buffer for verifier logging
+#define BPF_LOAD_LOG_SZ 0xfffff
+
+static unsigned int page_size = static_cast<unsigned int>(getpagesize());
+
+constexpr const char* lookupSelinuxContext(const domain d) {
+    switch (d) {
+        case domain::unspecified:   return "";
+        case domain::tethering:     return "fs_bpf_tethering";
+        case domain::net_private:   return "fs_bpf_net_private";
+        case domain::net_shared:    return "fs_bpf_net_shared";
+        case domain::netd_readonly: return "fs_bpf_netd_readonly";
+        case domain::netd_shared:   return "fs_bpf_netd_shared";
+    }
+}
+
+domain getDomainFromSelinuxContext(const char s[BPF_SELINUX_CONTEXT_CHAR_ARRAY_SIZE]) {
+    for (domain d : AllDomains) {
+        // Not sure how to enforce this at compile time, so abort() bpfloader at boot instead
+        if (strlen(lookupSelinuxContext(d)) >= BPF_SELINUX_CONTEXT_CHAR_ARRAY_SIZE) abort();
+        if (!strncmp(s, lookupSelinuxContext(d), BPF_SELINUX_CONTEXT_CHAR_ARRAY_SIZE)) return d;
+    }
+    ALOGE("unrecognized selinux_context '%-32s'", s);
+    // Note: we *can* just abort() here as we only load bpf .o files shipped
+    // in the same mainline module / apex as NetBpfLoad itself.
+    abort();
+}
+
+constexpr const char* lookupPinSubdir(const domain d, const char* const unspecified = "") {
+    switch (d) {
+        case domain::unspecified:   return unspecified;
+        case domain::tethering:     return "tethering/";
+        case domain::net_private:   return "net_private/";
+        case domain::net_shared:    return "net_shared/";
+        case domain::netd_readonly: return "netd_readonly/";
+        case domain::netd_shared:   return "netd_shared/";
+    }
+};
+
+domain getDomainFromPinSubdir(const char s[BPF_PIN_SUBDIR_CHAR_ARRAY_SIZE]) {
+    for (domain d : AllDomains) {
+        // Not sure how to enforce this at compile time, so abort() bpfloader at boot instead
+        if (strlen(lookupPinSubdir(d)) >= BPF_PIN_SUBDIR_CHAR_ARRAY_SIZE) abort();
+        if (!strncmp(s, lookupPinSubdir(d), BPF_PIN_SUBDIR_CHAR_ARRAY_SIZE)) return d;
+    }
+    ALOGE("unrecognized pin_subdir '%-32s'", s);
+    // Note: we *can* just abort() here as we only load bpf .o files shipped
+    // in the same mainline module / apex as NetBpfLoad itself.
+    abort();
+}
+
+static string pathToObjName(const string& path) {
+    // extract everything after the final slash, ie. this is the filename 'foo@1.o' or 'bar.o'
+    string filename = android::base::Split(path, "/").back();
+    // strip off everything from the final period onwards (strip '.o' suffix), ie. 'foo@1' or 'bar'
+    string name = filename.substr(0, filename.find_last_of('.'));
+    // strip any potential @1 suffix, this will leave us with just 'foo' or 'bar'
+    // this can be used to provide duplicate programs (mux based on the bpfloader version)
+    return name.substr(0, name.find_last_of('@'));
+}
+
+typedef struct {
+    const char* name;
+    enum bpf_prog_type type;
+    enum bpf_attach_type attach_type;
+} sectionType;
+
+/*
+ * Map section name prefixes to program types, the section name will be:
+ *   SECTION(<prefix>/<name-of-program>)
+ * For example:
+ *   SECTION("tracepoint/sched_switch_func") where sched_switch_funcs
+ * is the name of the program, and tracepoint is the type.
+ *
+ * However, be aware that you should not be directly using the SECTION() macro.
+ * Instead use the DEFINE_(BPF|XDP)_(PROG|MAP)... & LICENSE/CRITICAL macros.
+ *
+ * Programs shipped inside the tethering apex should be limited to networking stuff,
+ * as KPROBE, PERF_EVENT, TRACEPOINT are dangerous to use from mainline updatable code,
+ * since they are less stable abi/api and may conflict with platform uses of bpf.
+ */
+sectionType sectionNameTypes[] = {
+        {"bind4/",             BPF_PROG_TYPE_CGROUP_SOCK_ADDR, BPF_CGROUP_INET4_BIND},
+        {"bind6/",             BPF_PROG_TYPE_CGROUP_SOCK_ADDR, BPF_CGROUP_INET6_BIND},
+        {"cgroupskb/",         BPF_PROG_TYPE_CGROUP_SKB},
+        {"cgroupsock/",        BPF_PROG_TYPE_CGROUP_SOCK},
+        {"cgroupsockcreate/",  BPF_PROG_TYPE_CGROUP_SOCK,      BPF_CGROUP_INET_SOCK_CREATE},
+        {"cgroupsockrelease/", BPF_PROG_TYPE_CGROUP_SOCK,      BPF_CGROUP_INET_SOCK_RELEASE},
+        {"connect4/",          BPF_PROG_TYPE_CGROUP_SOCK_ADDR, BPF_CGROUP_INET4_CONNECT},
+        {"connect6/",          BPF_PROG_TYPE_CGROUP_SOCK_ADDR, BPF_CGROUP_INET6_CONNECT},
+        {"egress/",            BPF_PROG_TYPE_CGROUP_SKB,       BPF_CGROUP_INET_EGRESS},
+        {"getsockopt/",        BPF_PROG_TYPE_CGROUP_SOCKOPT,   BPF_CGROUP_GETSOCKOPT},
+        {"ingress/",           BPF_PROG_TYPE_CGROUP_SKB,       BPF_CGROUP_INET_INGRESS},
+        {"postbind4/",         BPF_PROG_TYPE_CGROUP_SOCK,      BPF_CGROUP_INET4_POST_BIND},
+        {"postbind6/",         BPF_PROG_TYPE_CGROUP_SOCK,      BPF_CGROUP_INET6_POST_BIND},
+        {"recvmsg4/",          BPF_PROG_TYPE_CGROUP_SOCK_ADDR, BPF_CGROUP_UDP4_RECVMSG},
+        {"recvmsg6/",          BPF_PROG_TYPE_CGROUP_SOCK_ADDR, BPF_CGROUP_UDP6_RECVMSG},
+        {"schedact/",          BPF_PROG_TYPE_SCHED_ACT},
+        {"schedcls/",          BPF_PROG_TYPE_SCHED_CLS},
+        {"sendmsg4/",          BPF_PROG_TYPE_CGROUP_SOCK_ADDR, BPF_CGROUP_UDP4_SENDMSG},
+        {"sendmsg6/",          BPF_PROG_TYPE_CGROUP_SOCK_ADDR, BPF_CGROUP_UDP6_SENDMSG},
+        {"setsockopt/",        BPF_PROG_TYPE_CGROUP_SOCKOPT,   BPF_CGROUP_SETSOCKOPT},
+        {"skfilter/",          BPF_PROG_TYPE_SOCKET_FILTER},
+        {"sockops/",           BPF_PROG_TYPE_SOCK_OPS,         BPF_CGROUP_SOCK_OPS},
+        {"sysctl",             BPF_PROG_TYPE_CGROUP_SYSCTL,    BPF_CGROUP_SYSCTL},
+        {"xdp/",               BPF_PROG_TYPE_XDP},
+};
+
+typedef struct {
+    enum bpf_prog_type type;
+    enum bpf_attach_type attach_type;
+    string name;
+    vector<char> data;
+    vector<char> rel_data;
+    optional<struct bpf_prog_def> prog_def;
+
+    unique_fd prog_fd; // fd after loading
+} codeSection;
+
+static int readElfHeader(ifstream& elfFile, Elf64_Ehdr* eh) {
+    elfFile.seekg(0);
+    if (elfFile.fail()) return -1;
+
+    if (!elfFile.read((char*)eh, sizeof(*eh))) return -1;
+
+    return 0;
+}
+
+// Reads all section header tables into an Shdr array
+static int readSectionHeadersAll(ifstream& elfFile, vector<Elf64_Shdr>& shTable) {
+    Elf64_Ehdr eh;
+    int ret = 0;
+
+    ret = readElfHeader(elfFile, &eh);
+    if (ret) return ret;
+
+    elfFile.seekg(eh.e_shoff);
+    if (elfFile.fail()) return -1;
+
+    // Read shdr table entries
+    shTable.resize(eh.e_shnum);
+
+    if (!elfFile.read((char*)shTable.data(), (eh.e_shnum * eh.e_shentsize))) return -ENOMEM;
+
+    return 0;
+}
+
+// Read a section by its index - for ex to get sec hdr strtab blob
+static int readSectionByIdx(ifstream& elfFile, int id, vector<char>& sec) {
+    vector<Elf64_Shdr> shTable;
+    int ret = readSectionHeadersAll(elfFile, shTable);
+    if (ret) return ret;
+
+    elfFile.seekg(shTable[id].sh_offset);
+    if (elfFile.fail()) return -1;
+
+    sec.resize(shTable[id].sh_size);
+    if (!elfFile.read(sec.data(), shTable[id].sh_size)) return -1;
+
+    return 0;
+}
+
+// Read whole section header string table
+static int readSectionHeaderStrtab(ifstream& elfFile, vector<char>& strtab) {
+    Elf64_Ehdr eh;
+    int ret = readElfHeader(elfFile, &eh);
+    if (ret) return ret;
+
+    ret = readSectionByIdx(elfFile, eh.e_shstrndx, strtab);
+    if (ret) return ret;
+
+    return 0;
+}
+
+// Get name from offset in strtab
+static int getSymName(ifstream& elfFile, int nameOff, string& name) {
+    int ret;
+    vector<char> secStrTab;
+
+    ret = readSectionHeaderStrtab(elfFile, secStrTab);
+    if (ret) return ret;
+
+    if (nameOff >= (int)secStrTab.size()) return -1;
+
+    name = string((char*)secStrTab.data() + nameOff);
+    return 0;
+}
+
+// Reads a full section by name - example to get the GPL license
+static int readSectionByName(const char* name, ifstream& elfFile, vector<char>& data) {
+    vector<char> secStrTab;
+    vector<Elf64_Shdr> shTable;
+    int ret;
+
+    ret = readSectionHeadersAll(elfFile, shTable);
+    if (ret) return ret;
+
+    ret = readSectionHeaderStrtab(elfFile, secStrTab);
+    if (ret) return ret;
+
+    for (int i = 0; i < (int)shTable.size(); i++) {
+        char* secname = secStrTab.data() + shTable[i].sh_name;
+        if (!secname) continue;
+
+        if (!strcmp(secname, name)) {
+            vector<char> dataTmp;
+            dataTmp.resize(shTable[i].sh_size);
+
+            elfFile.seekg(shTable[i].sh_offset);
+            if (elfFile.fail()) return -1;
+
+            if (!elfFile.read((char*)dataTmp.data(), shTable[i].sh_size)) return -1;
+
+            data = dataTmp;
+            return 0;
+        }
+    }
+    return -2;
+}
+
+unsigned int readSectionUint(const char* name, ifstream& elfFile) {
+    vector<char> theBytes;
+    int ret = readSectionByName(name, elfFile, theBytes);
+    if (ret) {
+        ALOGE("Couldn't find section %s.", name);
+        abort();
+    } else if (theBytes.size() < sizeof(unsigned int)) {
+        ALOGE("Section %s is too short.", name);
+        abort();
+    } else {
+        // decode first 4 bytes as LE32 uint, there will likely be more bytes due to alignment.
+        unsigned int value = static_cast<unsigned char>(theBytes[3]);
+        value <<= 8;
+        value += static_cast<unsigned char>(theBytes[2]);
+        value <<= 8;
+        value += static_cast<unsigned char>(theBytes[1]);
+        value <<= 8;
+        value += static_cast<unsigned char>(theBytes[0]);
+        ALOGI("Section %s value is %u [0x%x]", name, value, value);
+        return value;
+    }
+}
+
+static int readSectionByType(ifstream& elfFile, int type, vector<char>& data) {
+    int ret;
+    vector<Elf64_Shdr> shTable;
+
+    ret = readSectionHeadersAll(elfFile, shTable);
+    if (ret) return ret;
+
+    for (int i = 0; i < (int)shTable.size(); i++) {
+        if ((int)shTable[i].sh_type != type) continue;
+
+        vector<char> dataTmp;
+        dataTmp.resize(shTable[i].sh_size);
+
+        elfFile.seekg(shTable[i].sh_offset);
+        if (elfFile.fail()) return -1;
+
+        if (!elfFile.read((char*)dataTmp.data(), shTable[i].sh_size)) return -1;
+
+        data = dataTmp;
+        return 0;
+    }
+    return -2;
+}
+
+static bool symCompare(Elf64_Sym a, Elf64_Sym b) {
+    return (a.st_value < b.st_value);
+}
+
+static int readSymTab(ifstream& elfFile, int sort, vector<Elf64_Sym>& data) {
+    int ret, numElems;
+    Elf64_Sym* buf;
+    vector<char> secData;
+
+    ret = readSectionByType(elfFile, SHT_SYMTAB, secData);
+    if (ret) return ret;
+
+    buf = (Elf64_Sym*)secData.data();
+    numElems = (secData.size() / sizeof(Elf64_Sym));
+    data.assign(buf, buf + numElems);
+
+    if (sort) std::sort(data.begin(), data.end(), symCompare);
+    return 0;
+}
+
+static enum bpf_prog_type getSectionType(string& name) {
+    for (auto& snt : sectionNameTypes)
+        if (StartsWith(name, snt.name)) return snt.type;
+
+    return BPF_PROG_TYPE_UNSPEC;
+}
+
+static int readProgDefs(ifstream& elfFile, vector<struct bpf_prog_def>& pd) {
+    vector<char> pdData;
+    int ret = readSectionByName("progs", elfFile, pdData);
+    if (ret) return ret;
+
+    if (pdData.size() % sizeof(struct bpf_prog_def)) {
+        ALOGE("readProgDefs failed due to improper sized progs section, %zu %% %zu != 0",
+              pdData.size(), sizeof(struct bpf_prog_def));
+        return -1;
+    };
+
+    pd.resize(pdData.size() / sizeof(struct bpf_prog_def));
+
+    const char* dataPtr = pdData.data();
+    for (auto& p : pd) {
+        // Copy the structure from the ELF file and move to the next one.
+        memcpy(&p, dataPtr, sizeof(struct bpf_prog_def));
+        dataPtr += sizeof(struct bpf_prog_def);
+    }
+    return 0;
+}
+
+static int getSectionSymNames(ifstream& elfFile, const string& sectionName, vector<string>& names,
+                              optional<unsigned> symbolType = std::nullopt) {
+    int ret;
+    string name;
+    vector<Elf64_Sym> symtab;
+    vector<Elf64_Shdr> shTable;
+
+    ret = readSymTab(elfFile, 1 /* sort */, symtab);
+    if (ret) return ret;
+
+    // Get index of section
+    ret = readSectionHeadersAll(elfFile, shTable);
+    if (ret) return ret;
+
+    int sec_idx = -1;
+    for (int i = 0; i < (int)shTable.size(); i++) {
+        ret = getSymName(elfFile, shTable[i].sh_name, name);
+        if (ret) return ret;
+
+        if (!name.compare(sectionName)) {
+            sec_idx = i;
+            break;
+        }
+    }
+
+    // No section found with matching name
+    if (sec_idx == -1) {
+        ALOGW("No %s section could be found in elf object", sectionName.c_str());
+        return -1;
+    }
+
+    for (int i = 0; i < (int)symtab.size(); i++) {
+        if (symbolType.has_value() && ELF_ST_TYPE(symtab[i].st_info) != symbolType) continue;
+
+        if (symtab[i].st_shndx == sec_idx) {
+            string s;
+            ret = getSymName(elfFile, symtab[i].st_name, s);
+            if (ret) return ret;
+            names.push_back(s);
+        }
+    }
+
+    return 0;
+}
+
+// Read a section by its index - for ex to get sec hdr strtab blob
+static int readCodeSections(ifstream& elfFile, vector<codeSection>& cs) {
+    vector<Elf64_Shdr> shTable;
+    int entries, ret = 0;
+
+    ret = readSectionHeadersAll(elfFile, shTable);
+    if (ret) return ret;
+    entries = shTable.size();
+
+    vector<struct bpf_prog_def> pd;
+    ret = readProgDefs(elfFile, pd);
+    if (ret) return ret;
+    vector<string> progDefNames;
+    ret = getSectionSymNames(elfFile, "progs", progDefNames);
+    if (!pd.empty() && ret) return ret;
+
+    for (int i = 0; i < entries; i++) {
+        string name;
+        codeSection cs_temp;
+        cs_temp.type = BPF_PROG_TYPE_UNSPEC;
+
+        ret = getSymName(elfFile, shTable[i].sh_name, name);
+        if (ret) return ret;
+
+        enum bpf_prog_type ptype = getSectionType(name);
+
+        if (ptype == BPF_PROG_TYPE_UNSPEC) continue;
+
+        // This must be done before '/' is replaced with '_'.
+        for (auto& snt : sectionNameTypes)
+            if (StartsWith(name, snt.name)) cs_temp.attach_type = snt.attach_type;
+
+        string oldName = name;
+
+        // convert all slashes to underscores
+        std::replace(name.begin(), name.end(), '/', '_');
+
+        cs_temp.type = ptype;
+        cs_temp.name = name;
+
+        ret = readSectionByIdx(elfFile, i, cs_temp.data);
+        if (ret) return ret;
+        ALOGV("Loaded code section %d (%s)", i, name.c_str());
+
+        vector<string> csSymNames;
+        ret = getSectionSymNames(elfFile, oldName, csSymNames, STT_FUNC);
+        if (ret || !csSymNames.size()) return ret;
+        for (size_t i = 0; i < progDefNames.size(); ++i) {
+            if (!progDefNames[i].compare(csSymNames[0] + "_def")) {
+                cs_temp.prog_def = pd[i];
+                break;
+            }
+        }
+
+        // Check for rel section
+        if (cs_temp.data.size() > 0 && i < entries) {
+            ret = getSymName(elfFile, shTable[i + 1].sh_name, name);
+            if (ret) return ret;
+
+            if (name == (".rel" + oldName)) {
+                ret = readSectionByIdx(elfFile, i + 1, cs_temp.rel_data);
+                if (ret) return ret;
+                ALOGV("Loaded relo section %d (%s)", i, name.c_str());
+            }
+        }
+
+        if (cs_temp.data.size() > 0) {
+            cs.push_back(std::move(cs_temp));
+            ALOGV("Adding section %d to cs list", i);
+        }
+    }
+    return 0;
+}
+
+static int getSymNameByIdx(ifstream& elfFile, int index, string& name) {
+    vector<Elf64_Sym> symtab;
+    int ret = 0;
+
+    ret = readSymTab(elfFile, 0 /* !sort */, symtab);
+    if (ret) return ret;
+
+    if (index >= (int)symtab.size()) return -1;
+
+    return getSymName(elfFile, symtab[index].st_name, name);
+}
+
+static bool mapMatchesExpectations(const unique_fd& fd, const string& mapName,
+                                   const struct bpf_map_def& mapDef, const enum bpf_map_type type) {
+    // bpfGetFd... family of functions require at minimum a 4.14 kernel,
+    // so on 4.9-T kernels just pretend the map matches our expectations.
+    // Additionally we'll get almost equivalent test coverage on newer devices/kernels.
+    // This is because the primary failure mode we're trying to detect here
+    // is either a source code misconfiguration (which is likely kernel independent)
+    // or a newly introduced kernel feature/bug (which is unlikely to get backported to 4.9).
+    if (!isAtLeastKernelVersion(4, 14, 0)) return true;
+
+    // Assuming fd is a valid Bpf Map file descriptor then
+    // all the following should always succeed on a 4.14+ kernel.
+    // If they somehow do fail, they'll return -1 (and set errno),
+    // which should then cause (among others) a key_size mismatch.
+    int fd_type = bpfGetFdMapType(fd);
+    int fd_key_size = bpfGetFdKeySize(fd);
+    int fd_value_size = bpfGetFdValueSize(fd);
+    int fd_max_entries = bpfGetFdMaxEntries(fd);
+    int fd_map_flags = bpfGetFdMapFlags(fd);
+
+    // DEVMAPs are readonly from the bpf program side's point of view, as such
+    // the kernel in kernel/bpf/devmap.c dev_map_init_map() will set the flag
+    int desired_map_flags = (int)mapDef.map_flags;
+    if (type == BPF_MAP_TYPE_DEVMAP || type == BPF_MAP_TYPE_DEVMAP_HASH)
+        desired_map_flags |= BPF_F_RDONLY_PROG;
+
+    // The .h file enforces that this is a power of two, and page size will
+    // also always be a power of two, so this logic is actually enough to
+    // force it to be a multiple of the page size, as required by the kernel.
+    unsigned int desired_max_entries = mapDef.max_entries;
+    if (type == BPF_MAP_TYPE_RINGBUF) {
+        if (desired_max_entries < page_size) desired_max_entries = page_size;
+    }
+
+    // The following checks should *never* trigger, if one of them somehow does,
+    // it probably means a bpf .o file has been changed/replaced at runtime
+    // and bpfloader was manually rerun (normally it should only run *once*
+    // early during the boot process).
+    // Another possibility is that something is misconfigured in the code:
+    // most likely a shared map is declared twice differently.
+    // But such a change should never be checked into the source tree...
+    if ((fd_type == type) &&
+        (fd_key_size == (int)mapDef.key_size) &&
+        (fd_value_size == (int)mapDef.value_size) &&
+        (fd_max_entries == (int)desired_max_entries) &&
+        (fd_map_flags == desired_map_flags)) {
+        return true;
+    }
+
+    ALOGE("bpf map name %s mismatch: desired/found: "
+          "type:%d/%d key:%u/%d value:%u/%d entries:%u/%d flags:%u/%d",
+          mapName.c_str(), type, fd_type, mapDef.key_size, fd_key_size, mapDef.value_size,
+          fd_value_size, mapDef.max_entries, fd_max_entries, desired_map_flags, fd_map_flags);
+    return false;
+}
+
+static int createMaps(const char* elfPath, ifstream& elfFile, vector<unique_fd>& mapFds,
+                      const char* prefix, const unsigned int bpfloader_ver) {
+    int ret;
+    vector<char> mdData;
+    vector<struct bpf_map_def> md;
+    vector<string> mapNames;
+    string objName = pathToObjName(string(elfPath));
+
+    ret = readSectionByName("maps", elfFile, mdData);
+    if (ret == -2) return 0;  // no maps to read
+    if (ret) return ret;
+
+    if (mdData.size() % sizeof(struct bpf_map_def)) {
+        ALOGE("createMaps failed due to improper sized maps section, %zu %% %zu != 0",
+              mdData.size(), sizeof(struct bpf_map_def));
+        return -1;
+    };
+
+    md.resize(mdData.size() / sizeof(struct bpf_map_def));
+
+    const char* dataPtr = mdData.data();
+    for (auto& m : md) {
+        // Copy the structure from the ELF file and move to the next one.
+        memcpy(&m, dataPtr, sizeof(struct bpf_map_def));
+        dataPtr += sizeof(struct bpf_map_def);
+    }
+
+    ret = getSectionSymNames(elfFile, "maps", mapNames);
+    if (ret) return ret;
+
+    unsigned kvers = kernelVersion();
+
+    for (int i = 0; i < (int)mapNames.size(); i++) {
+        if (md[i].zero != 0) abort();
+
+        if (bpfloader_ver < md[i].bpfloader_min_ver) {
+            ALOGI("skipping map %s which requires bpfloader min ver 0x%05x", mapNames[i].c_str(),
+                  md[i].bpfloader_min_ver);
+            mapFds.push_back(unique_fd());
+            continue;
+        }
+
+        if (bpfloader_ver >= md[i].bpfloader_max_ver) {
+            ALOGI("skipping map %s which requires bpfloader max ver 0x%05x", mapNames[i].c_str(),
+                  md[i].bpfloader_max_ver);
+            mapFds.push_back(unique_fd());
+            continue;
+        }
+
+        if (kvers < md[i].min_kver) {
+            ALOGI("skipping map %s which requires kernel version 0x%x >= 0x%x",
+                  mapNames[i].c_str(), kvers, md[i].min_kver);
+            mapFds.push_back(unique_fd());
+            continue;
+        }
+
+        if (kvers >= md[i].max_kver) {
+            ALOGI("skipping map %s which requires kernel version 0x%x < 0x%x",
+                  mapNames[i].c_str(), kvers, md[i].max_kver);
+            mapFds.push_back(unique_fd());
+            continue;
+        }
+
+        if ((md[i].ignore_on_eng && isEng()) || (md[i].ignore_on_user && isUser()) ||
+            (md[i].ignore_on_userdebug && isUserdebug())) {
+            ALOGI("skipping map %s which is ignored on %s builds", mapNames[i].c_str(),
+                  getBuildType().c_str());
+            mapFds.push_back(unique_fd());
+            continue;
+        }
+
+        if ((isArm() && isKernel32Bit() && md[i].ignore_on_arm32) ||
+            (isArm() && isKernel64Bit() && md[i].ignore_on_aarch64) ||
+            (isX86() && isKernel32Bit() && md[i].ignore_on_x86_32) ||
+            (isX86() && isKernel64Bit() && md[i].ignore_on_x86_64) ||
+            (isRiscV() && md[i].ignore_on_riscv64)) {
+            ALOGI("skipping map %s which is ignored on %s", mapNames[i].c_str(),
+                  describeArch());
+            mapFds.push_back(unique_fd());
+            continue;
+        }
+
+        enum bpf_map_type type = md[i].type;
+        if (type == BPF_MAP_TYPE_DEVMAP && !isAtLeastKernelVersion(4, 14, 0)) {
+            // On Linux Kernels older than 4.14 this map type doesn't exist, but it can kind
+            // of be approximated: ARRAY has the same userspace api, though it is not usable
+            // by the same ebpf programs.  However, that's okay because the bpf_redirect_map()
+            // helper doesn't exist on 4.9-T anyway (so the bpf program would fail to load,
+            // and thus needs to be tagged as 4.14+ either way), so there's nothing useful you
+            // could do with a DEVMAP anyway (that isn't already provided by an ARRAY)...
+            // Hence using an ARRAY instead of a DEVMAP simply makes life easier for userspace.
+            type = BPF_MAP_TYPE_ARRAY;
+        }
+        if (type == BPF_MAP_TYPE_DEVMAP_HASH && !isAtLeastKernelVersion(5, 4, 0)) {
+            // On Linux Kernels older than 5.4 this map type doesn't exist, but it can kind
+            // of be approximated: HASH has the same userspace visible api.
+            // However it cannot be used by ebpf programs in the same way.
+            // Since bpf_redirect_map() only requires 4.14, a program using a DEVMAP_HASH map
+            // would fail to load (due to trying to redirect to a HASH instead of DEVMAP_HASH).
+            // One must thus tag any BPF_MAP_TYPE_DEVMAP_HASH + bpf_redirect_map() using
+            // programs as being 5.4+...
+            type = BPF_MAP_TYPE_HASH;
+        }
+
+        // The .h file enforces that this is a power of two, and page size will
+        // also always be a power of two, so this logic is actually enough to
+        // force it to be a multiple of the page size, as required by the kernel.
+        unsigned int max_entries = md[i].max_entries;
+        if (type == BPF_MAP_TYPE_RINGBUF) {
+            if (max_entries < page_size) max_entries = page_size;
+        }
+
+        domain selinux_context = getDomainFromSelinuxContext(md[i].selinux_context);
+        if (specified(selinux_context)) {
+            ALOGV("map %s selinux_context [%-32s] -> %d -> '%s' (%s)", mapNames[i].c_str(),
+                  md[i].selinux_context, static_cast<int>(selinux_context),
+                  lookupSelinuxContext(selinux_context), lookupPinSubdir(selinux_context));
+        }
+
+        domain pin_subdir = getDomainFromPinSubdir(md[i].pin_subdir);
+        if (specified(pin_subdir)) {
+            ALOGV("map %s pin_subdir [%-32s] -> %d -> '%s'", mapNames[i].c_str(), md[i].pin_subdir,
+                  static_cast<int>(pin_subdir), lookupPinSubdir(pin_subdir));
+        }
+
+        // Format of pin location is /sys/fs/bpf/<pin_subdir|prefix>map_<objName>_<mapName>
+        // except that maps shared across .o's have empty <objName>
+        // Note: <objName> refers to the extension-less basename of the .o file (without @ suffix).
+        string mapPinLoc = string(BPF_FS_PATH) + lookupPinSubdir(pin_subdir, prefix) + "map_" +
+                           (md[i].shared ? "" : objName) + "_" + mapNames[i];
+        bool reuse = false;
+        unique_fd fd;
+        int saved_errno;
+
+        if (access(mapPinLoc.c_str(), F_OK) == 0) {
+            fd.reset(mapRetrieveRO(mapPinLoc.c_str()));
+            saved_errno = errno;
+            ALOGD("bpf_create_map reusing map %s, ret: %d", mapNames[i].c_str(), fd.get());
+            reuse = true;
+        } else {
+            union bpf_attr req = {
+              .map_type = type,
+              .key_size = md[i].key_size,
+              .value_size = md[i].value_size,
+              .max_entries = max_entries,
+              .map_flags = md[i].map_flags,
+            };
+            if (isAtLeastKernelVersion(4, 15, 0))
+                strlcpy(req.map_name, mapNames[i].c_str(), sizeof(req.map_name));
+            fd.reset(bpf(BPF_MAP_CREATE, req));
+            saved_errno = errno;
+            ALOGD("bpf_create_map name %s, ret: %d", mapNames[i].c_str(), fd.get());
+        }
+
+        if (!fd.ok()) return -saved_errno;
+
+        // When reusing a pinned map, we need to check the map type/sizes/etc match, but for
+        // safety (since reuse code path is rare) run these checks even if we just created it.
+        // We assume failure is due to pinned map mismatch, hence the 'NOT UNIQUE' return code.
+        if (!mapMatchesExpectations(fd, mapNames[i], md[i], type)) return -ENOTUNIQ;
+
+        if (!reuse) {
+            if (specified(selinux_context)) {
+                string createLoc = string(BPF_FS_PATH) + lookupPinSubdir(selinux_context) +
+                                   "tmp_map_" + objName + "_" + mapNames[i];
+                ret = bpfFdPin(fd, createLoc.c_str());
+                if (ret) {
+                    int err = errno;
+                    ALOGE("create %s -> %d [%d:%s]", createLoc.c_str(), ret, err, strerror(err));
+                    return -err;
+                }
+                ret = renameat2(AT_FDCWD, createLoc.c_str(),
+                                AT_FDCWD, mapPinLoc.c_str(), RENAME_NOREPLACE);
+                if (ret) {
+                    int err = errno;
+                    ALOGE("rename %s %s -> %d [%d:%s]", createLoc.c_str(), mapPinLoc.c_str(), ret,
+                          err, strerror(err));
+                    return -err;
+                }
+            } else {
+                ret = bpfFdPin(fd, mapPinLoc.c_str());
+                if (ret) {
+                    int err = errno;
+                    ALOGE("pin %s -> %d [%d:%s]", mapPinLoc.c_str(), ret, err, strerror(err));
+                    return -err;
+                }
+            }
+            ret = chmod(mapPinLoc.c_str(), md[i].mode);
+            if (ret) {
+                int err = errno;
+                ALOGE("chmod(%s, 0%o) = %d [%d:%s]", mapPinLoc.c_str(), md[i].mode, ret, err,
+                      strerror(err));
+                return -err;
+            }
+            ret = chown(mapPinLoc.c_str(), (uid_t)md[i].uid, (gid_t)md[i].gid);
+            if (ret) {
+                int err = errno;
+                ALOGE("chown(%s, %u, %u) = %d [%d:%s]", mapPinLoc.c_str(), md[i].uid, md[i].gid,
+                      ret, err, strerror(err));
+                return -err;
+            }
+        }
+
+        int mapId = bpfGetFdMapId(fd);
+        if (mapId == -1) {
+            ALOGE("bpfGetFdMapId failed, ret: %d [%d]", mapId, errno);
+        } else {
+            ALOGI("map %s id %d", mapPinLoc.c_str(), mapId);
+        }
+
+        mapFds.push_back(std::move(fd));
+    }
+
+    return ret;
+}
+
+static void applyRelo(void* insnsPtr, Elf64_Addr offset, int fd) {
+    int insnIndex;
+    struct bpf_insn *insn, *insns;
+
+    insns = (struct bpf_insn*)(insnsPtr);
+
+    insnIndex = offset / sizeof(struct bpf_insn);
+    insn = &insns[insnIndex];
+
+    // Occasionally might be useful for relocation debugging, but pretty spammy
+    if (0) {
+        ALOGV("applying relo to instruction at byte offset: %llu, "
+              "insn offset %d, insn %llx",
+              (unsigned long long)offset, insnIndex, *(unsigned long long*)insn);
+    }
+
+    if (insn->code != (BPF_LD | BPF_IMM | BPF_DW)) {
+        ALOGE("invalid relo for insn %d: code 0x%x", insnIndex, insn->code);
+        return;
+    }
+
+    insn->imm = fd;
+    insn->src_reg = BPF_PSEUDO_MAP_FD;
+}
+
+static void applyMapRelo(ifstream& elfFile, vector<unique_fd> &mapFds, vector<codeSection>& cs) {
+    vector<string> mapNames;
+
+    int ret = getSectionSymNames(elfFile, "maps", mapNames);
+    if (ret) return;
+
+    for (int k = 0; k != (int)cs.size(); k++) {
+        Elf64_Rel* rel = (Elf64_Rel*)(cs[k].rel_data.data());
+        int n_rel = cs[k].rel_data.size() / sizeof(*rel);
+
+        for (int i = 0; i < n_rel; i++) {
+            int symIndex = ELF64_R_SYM(rel[i].r_info);
+            string symName;
+
+            ret = getSymNameByIdx(elfFile, symIndex, symName);
+            if (ret) return;
+
+            // Find the map fd and apply relo
+            for (int j = 0; j < (int)mapNames.size(); j++) {
+                if (!mapNames[j].compare(symName)) {
+                    applyRelo(cs[k].data.data(), rel[i].r_offset, mapFds[j]);
+                    break;
+                }
+            }
+        }
+    }
+}
+
+static int loadCodeSections(const char* elfPath, vector<codeSection>& cs, const string& license,
+                            const char* prefix, const unsigned int bpfloader_ver) {
+    unsigned kvers = kernelVersion();
+
+    if (!kvers) {
+        ALOGE("unable to get kernel version");
+        return -EINVAL;
+    }
+
+    string objName = pathToObjName(string(elfPath));
+
+    for (int i = 0; i < (int)cs.size(); i++) {
+        unique_fd& fd = cs[i].prog_fd;
+        int ret;
+        string name = cs[i].name;
+
+        if (!cs[i].prog_def.has_value()) {
+            ALOGE("[%d] '%s' missing program definition! bad bpf.o build?", i, name.c_str());
+            return -EINVAL;
+        }
+
+        unsigned min_kver = cs[i].prog_def->min_kver;
+        unsigned max_kver = cs[i].prog_def->max_kver;
+        ALOGD("cs[%d].name:%s min_kver:%x .max_kver:%x (kvers:%x)", i, name.c_str(), min_kver,
+             max_kver, kvers);
+        if (kvers < min_kver) continue;
+        if (kvers >= max_kver) continue;
+
+        unsigned bpfMinVer = cs[i].prog_def->bpfloader_min_ver;
+        unsigned bpfMaxVer = cs[i].prog_def->bpfloader_max_ver;
+        domain selinux_context = getDomainFromSelinuxContext(cs[i].prog_def->selinux_context);
+        domain pin_subdir = getDomainFromPinSubdir(cs[i].prog_def->pin_subdir);
+
+        ALOGD("cs[%d].name:%s requires bpfloader version [0x%05x,0x%05x)", i, name.c_str(),
+              bpfMinVer, bpfMaxVer);
+        if (bpfloader_ver < bpfMinVer) continue;
+        if (bpfloader_ver >= bpfMaxVer) continue;
+
+        if ((cs[i].prog_def->ignore_on_eng && isEng()) ||
+            (cs[i].prog_def->ignore_on_user && isUser()) ||
+            (cs[i].prog_def->ignore_on_userdebug && isUserdebug())) {
+            ALOGD("cs[%d].name:%s is ignored on %s builds", i, name.c_str(),
+                  getBuildType().c_str());
+            continue;
+        }
+
+        if ((isArm() && isKernel32Bit() && cs[i].prog_def->ignore_on_arm32) ||
+            (isArm() && isKernel64Bit() && cs[i].prog_def->ignore_on_aarch64) ||
+            (isX86() && isKernel32Bit() && cs[i].prog_def->ignore_on_x86_32) ||
+            (isX86() && isKernel64Bit() && cs[i].prog_def->ignore_on_x86_64) ||
+            (isRiscV() && cs[i].prog_def->ignore_on_riscv64)) {
+            ALOGD("cs[%d].name:%s is ignored on %s", i, name.c_str(), describeArch());
+            continue;
+        }
+
+        if (specified(selinux_context)) {
+            ALOGV("prog %s selinux_context [%-32s] -> %d -> '%s' (%s)", name.c_str(),
+                  cs[i].prog_def->selinux_context, static_cast<int>(selinux_context),
+                  lookupSelinuxContext(selinux_context), lookupPinSubdir(selinux_context));
+        }
+
+        if (specified(pin_subdir)) {
+            ALOGV("prog %s pin_subdir [%-32s] -> %d -> '%s'", name.c_str(),
+                  cs[i].prog_def->pin_subdir, static_cast<int>(pin_subdir),
+                  lookupPinSubdir(pin_subdir));
+        }
+
+        // strip any potential $foo suffix
+        // this can be used to provide duplicate programs
+        // conditionally loaded based on running kernel version
+        name = name.substr(0, name.find_last_of('$'));
+
+        bool reuse = false;
+        // Format of pin location is
+        // /sys/fs/bpf/<prefix>prog_<objName>_<progName>
+        string progPinLoc = string(BPF_FS_PATH) + lookupPinSubdir(pin_subdir, prefix) + "prog_" +
+                            objName + '_' + string(name);
+        if (access(progPinLoc.c_str(), F_OK) == 0) {
+            fd.reset(retrieveProgram(progPinLoc.c_str()));
+            ALOGD("New bpf prog load reusing prog %s, ret: %d (%s)", progPinLoc.c_str(), fd.get(),
+                  (!fd.ok() ? std::strerror(errno) : "no error"));
+            reuse = true;
+        } else {
+            vector<char> log_buf(BPF_LOAD_LOG_SZ, 0);
+
+            union bpf_attr req = {
+              .prog_type = cs[i].type,
+              .kern_version = kvers,
+              .license = ptr_to_u64(license.c_str()),
+              .insns = ptr_to_u64(cs[i].data.data()),
+              .insn_cnt = static_cast<__u32>(cs[i].data.size() / sizeof(struct bpf_insn)),
+              .log_level = 1,
+              .log_buf = ptr_to_u64(log_buf.data()),
+              .log_size = static_cast<__u32>(log_buf.size()),
+              .expected_attach_type = cs[i].attach_type,
+            };
+            if (isAtLeastKernelVersion(4, 15, 0))
+                strlcpy(req.prog_name, cs[i].name.c_str(), sizeof(req.prog_name));
+            fd.reset(bpf(BPF_PROG_LOAD, req));
+
+            ALOGD("BPF_PROG_LOAD call for %s (%s) returned fd: %d (%s)", elfPath,
+                  cs[i].name.c_str(), fd.get(), (!fd.ok() ? std::strerror(errno) : "no error"));
+
+            if (!fd.ok()) {
+                vector<string> lines = android::base::Split(log_buf.data(), "\n");
+
+                ALOGW("BPF_PROG_LOAD - BEGIN log_buf contents:");
+                for (const auto& line : lines) ALOGW("%s", line.c_str());
+                ALOGW("BPF_PROG_LOAD - END log_buf contents.");
+
+                if (cs[i].prog_def->optional) {
+                    ALOGW("failed program is marked optional - continuing...");
+                    continue;
+                }
+                ALOGE("non-optional program failed to load.");
+            }
+        }
+
+        if (!fd.ok()) return fd.get();
+
+        if (!reuse) {
+            if (specified(selinux_context)) {
+                string createLoc = string(BPF_FS_PATH) + lookupPinSubdir(selinux_context) +
+                                   "tmp_prog_" + objName + '_' + string(name);
+                ret = bpfFdPin(fd, createLoc.c_str());
+                if (ret) {
+                    int err = errno;
+                    ALOGE("create %s -> %d [%d:%s]", createLoc.c_str(), ret, err, strerror(err));
+                    return -err;
+                }
+                ret = renameat2(AT_FDCWD, createLoc.c_str(),
+                                AT_FDCWD, progPinLoc.c_str(), RENAME_NOREPLACE);
+                if (ret) {
+                    int err = errno;
+                    ALOGE("rename %s %s -> %d [%d:%s]", createLoc.c_str(), progPinLoc.c_str(), ret,
+                          err, strerror(err));
+                    return -err;
+                }
+            } else {
+                ret = bpfFdPin(fd, progPinLoc.c_str());
+                if (ret) {
+                    int err = errno;
+                    ALOGE("create %s -> %d [%d:%s]", progPinLoc.c_str(), ret, err, strerror(err));
+                    return -err;
+                }
+            }
+            if (chmod(progPinLoc.c_str(), 0440)) {
+                int err = errno;
+                ALOGE("chmod %s 0440 -> [%d:%s]", progPinLoc.c_str(), err, strerror(err));
+                return -err;
+            }
+            if (chown(progPinLoc.c_str(), (uid_t)cs[i].prog_def->uid,
+                      (gid_t)cs[i].prog_def->gid)) {
+                int err = errno;
+                ALOGE("chown %s %d %d -> [%d:%s]", progPinLoc.c_str(), cs[i].prog_def->uid,
+                      cs[i].prog_def->gid, err, strerror(err));
+                return -err;
+            }
+        }
+
+        int progId = bpfGetFdProgId(fd);
+        if (progId == -1) {
+            ALOGE("bpfGetFdProgId failed, ret: %d [%d]", progId, errno);
+        } else {
+            ALOGI("prog %s id %d", progPinLoc.c_str(), progId);
+        }
+    }
+
+    return 0;
+}
+
+int loadProg(const char* const elfPath, bool* const isCritical, const unsigned int bpfloader_ver,
+             const char* const prefix) {
+    vector<char> license;
+    vector<char> critical;
+    vector<codeSection> cs;
+    vector<unique_fd> mapFds;
+    int ret;
+
+    if (!isCritical) return -1;
+    *isCritical = false;
+
+    ifstream elfFile(elfPath, ios::in | ios::binary);
+    if (!elfFile.is_open()) return -1;
+
+    ret = readSectionByName("critical", elfFile, critical);
+    *isCritical = !ret;
+
+    ret = readSectionByName("license", elfFile, license);
+    if (ret) {
+        ALOGE("Couldn't find license in %s", elfPath);
+        return ret;
+    } else {
+        ALOGD("Loading %s%s ELF object %s with license %s",
+              *isCritical ? "critical for " : "optional", *isCritical ? (char*)critical.data() : "",
+              elfPath, (char*)license.data());
+    }
+
+    unsigned int bpfLoaderMinVer = readSectionUint("bpfloader_min_ver", elfFile);
+    unsigned int bpfLoaderMaxVer = readSectionUint("bpfloader_max_ver", elfFile);
+
+    // inclusive lower bound check
+    if (bpfloader_ver < bpfLoaderMinVer) {
+        ALOGI("BpfLoader version 0x%05x ignoring ELF object %s with min ver 0x%05x",
+              bpfloader_ver, elfPath, bpfLoaderMinVer);
+        return 0;
+    }
+
+    // exclusive upper bound check
+    if (bpfloader_ver >= bpfLoaderMaxVer) {
+        ALOGI("BpfLoader version 0x%05x ignoring ELF object %s with max ver 0x%05x",
+              bpfloader_ver, elfPath, bpfLoaderMaxVer);
+        return 0;
+    }
+
+    ALOGI("BpfLoader version 0x%05x processing ELF object %s with ver [0x%05x,0x%05x)",
+          bpfloader_ver, elfPath, bpfLoaderMinVer, bpfLoaderMaxVer);
+
+    ret = readCodeSections(elfFile, cs);
+    if (ret) {
+        ALOGE("Couldn't read all code sections in %s", elfPath);
+        return ret;
+    }
+
+    ret = createMaps(elfPath, elfFile, mapFds, prefix, bpfloader_ver);
+    if (ret) {
+        ALOGE("Failed to create maps: (ret=%d) in %s", ret, elfPath);
+        return ret;
+    }
+
+    for (int i = 0; i < (int)mapFds.size(); i++)
+        ALOGV("map_fd found at %d is %d in %s", i, mapFds[i].get(), elfPath);
+
+    applyMapRelo(elfFile, mapFds, cs);
+
+    ret = loadCodeSections(elfPath, cs, string(license.data()), prefix, bpfloader_ver);
+    if (ret) ALOGE("Failed to load programs, loadCodeSections ret=%d", ret);
+
+    return ret;
+}
 
 static bool exists(const char* const path) {
     int v = access(path, F_OK);
@@ -112,7 +1200,7 @@
             progPath += s;
 
             bool critical;
-            int ret = loadProg(progPath.c_str(), &critical, bpfloader_ver, location);
+            int ret = loadProg(progPath.c_str(), &critical, bpfloader_ver, location.prefix);
             if (ret) {
                 if (critical) retVal = ret;
                 ALOGE("Failed to load object: %s, ret: %s", progPath.c_str(), std::strerror(-ret));
diff --git a/netbpfload/loader.cpp b/netbpfload/loader.cpp
deleted file mode 100644
index 5141095..0000000
--- a/netbpfload/loader.cpp
+++ /dev/null
@@ -1,1194 +0,0 @@
-/*
- * Copyright (C) 2018-2023 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.
- */
-
-#define LOG_TAG "NetBpfLoad"
-
-#include <errno.h>
-#include <fcntl.h>
-#include <linux/bpf.h>
-#include <linux/elf.h>
-#include <log/log.h>
-#include <stdint.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-#include <sysexits.h>
-#include <sys/stat.h>
-#include <sys/utsname.h>
-#include <sys/wait.h>
-#include <unistd.h>
-
-#include "BpfSyscallWrappers.h"
-#include "bpf/BpfUtils.h"
-#include "bpf/bpf_map_def.h"
-#include "loader.h"
-
-#include <cstdlib>
-#include <fstream>
-#include <iostream>
-#include <optional>
-#include <string>
-#include <unordered_map>
-#include <vector>
-
-#include <android-base/cmsg.h>
-#include <android-base/file.h>
-#include <android-base/properties.h>
-#include <android-base/strings.h>
-#include <android-base/unique_fd.h>
-
-#define BPF_FS_PATH "/sys/fs/bpf/"
-
-// Size of the BPF log buffer for verifier logging
-#define BPF_LOAD_LOG_SZ 0xfffff
-
-// Unspecified attach type is 0 which is BPF_CGROUP_INET_INGRESS.
-#define BPF_ATTACH_TYPE_UNSPEC BPF_CGROUP_INET_INGRESS
-
-using android::base::StartsWith;
-using android::base::unique_fd;
-using std::ifstream;
-using std::ios;
-using std::optional;
-using std::string;
-using std::vector;
-
-namespace android {
-namespace bpf {
-
-const std::string& getBuildType() {
-    static std::string t = android::base::GetProperty("ro.build.type", "unknown");
-    return t;
-}
-
-static unsigned int page_size = static_cast<unsigned int>(getpagesize());
-
-constexpr const char* lookupSelinuxContext(const domain d, const char* const unspecified = "") {
-    switch (d) {
-        case domain::unspecified:   return unspecified;
-        case domain::tethering:     return "fs_bpf_tethering";
-        case domain::net_private:   return "fs_bpf_net_private";
-        case domain::net_shared:    return "fs_bpf_net_shared";
-        case domain::netd_readonly: return "fs_bpf_netd_readonly";
-        case domain::netd_shared:   return "fs_bpf_netd_shared";
-        default:                    return "(unrecognized)";
-    }
-}
-
-domain getDomainFromSelinuxContext(const char s[BPF_SELINUX_CONTEXT_CHAR_ARRAY_SIZE]) {
-    for (domain d : AllDomains) {
-        // Not sure how to enforce this at compile time, so abort() bpfloader at boot instead
-        if (strlen(lookupSelinuxContext(d)) >= BPF_SELINUX_CONTEXT_CHAR_ARRAY_SIZE) abort();
-        if (!strncmp(s, lookupSelinuxContext(d), BPF_SELINUX_CONTEXT_CHAR_ARRAY_SIZE)) return d;
-    }
-    ALOGW("ignoring unrecognized selinux_context '%-32s'", s);
-    // We should return 'unrecognized' here, however: returning unspecified will
-    // result in the system simply using the default context, which in turn
-    // will allow future expansion by adding more restrictive selinux types.
-    // Older bpfloader will simply ignore that, and use the less restrictive default.
-    // This does mean you CANNOT later add a *less* restrictive type than the default.
-    //
-    // Note: we cannot just abort() here as this might be a mainline module shipped optional update
-    return domain::unspecified;
-}
-
-constexpr const char* lookupPinSubdir(const domain d, const char* const unspecified = "") {
-    switch (d) {
-        case domain::unspecified:   return unspecified;
-        case domain::tethering:     return "tethering/";
-        case domain::net_private:   return "net_private/";
-        case domain::net_shared:    return "net_shared/";
-        case domain::netd_readonly: return "netd_readonly/";
-        case domain::netd_shared:   return "netd_shared/";
-        default:                    return "(unrecognized)";
-    }
-};
-
-domain getDomainFromPinSubdir(const char s[BPF_PIN_SUBDIR_CHAR_ARRAY_SIZE]) {
-    for (domain d : AllDomains) {
-        // Not sure how to enforce this at compile time, so abort() bpfloader at boot instead
-        if (strlen(lookupPinSubdir(d)) >= BPF_PIN_SUBDIR_CHAR_ARRAY_SIZE) abort();
-        if (!strncmp(s, lookupPinSubdir(d), BPF_PIN_SUBDIR_CHAR_ARRAY_SIZE)) return d;
-    }
-    ALOGE("unrecognized pin_subdir '%-32s'", s);
-    // pin_subdir affects the object's full pathname,
-    // and thus using the default would change the location and thus our code's ability to find it,
-    // hence this seems worth treating as a true error condition.
-    //
-    // Note: we cannot just abort() here as this might be a mainline module shipped optional update
-    // However, our callers will treat this as an error, and stop loading the specific .o,
-    // which will fail bpfloader if the .o is marked critical.
-    return domain::unrecognized;
-}
-
-static string pathToObjName(const string& path) {
-    // extract everything after the final slash, ie. this is the filename 'foo@1.o' or 'bar.o'
-    string filename = android::base::Split(path, "/").back();
-    // strip off everything from the final period onwards (strip '.o' suffix), ie. 'foo@1' or 'bar'
-    string name = filename.substr(0, filename.find_last_of('.'));
-    // strip any potential @1 suffix, this will leave us with just 'foo' or 'bar'
-    // this can be used to provide duplicate programs (mux based on the bpfloader version)
-    return name.substr(0, name.find_last_of('@'));
-}
-
-typedef struct {
-    const char* name;
-    enum bpf_prog_type type;
-    enum bpf_attach_type expected_attach_type;
-} sectionType;
-
-/*
- * Map section name prefixes to program types, the section name will be:
- *   SECTION(<prefix>/<name-of-program>)
- * For example:
- *   SECTION("tracepoint/sched_switch_func") where sched_switch_funcs
- * is the name of the program, and tracepoint is the type.
- *
- * However, be aware that you should not be directly using the SECTION() macro.
- * Instead use the DEFINE_(BPF|XDP)_(PROG|MAP)... & LICENSE/CRITICAL macros.
- *
- * Programs shipped inside the tethering apex should be limited to networking stuff,
- * as KPROBE, PERF_EVENT, TRACEPOINT are dangerous to use from mainline updatable code,
- * since they are less stable abi/api and may conflict with platform uses of bpf.
- */
-sectionType sectionNameTypes[] = {
-        {"bind4/",             BPF_PROG_TYPE_CGROUP_SOCK_ADDR, BPF_CGROUP_INET4_BIND},
-        {"bind6/",             BPF_PROG_TYPE_CGROUP_SOCK_ADDR, BPF_CGROUP_INET6_BIND},
-        {"cgroupskb/",         BPF_PROG_TYPE_CGROUP_SKB,       BPF_ATTACH_TYPE_UNSPEC},
-        {"cgroupsock/",        BPF_PROG_TYPE_CGROUP_SOCK,      BPF_ATTACH_TYPE_UNSPEC},
-        {"cgroupsockcreate/",  BPF_PROG_TYPE_CGROUP_SOCK,      BPF_CGROUP_INET_SOCK_CREATE},
-        {"cgroupsockrelease/", BPF_PROG_TYPE_CGROUP_SOCK,      BPF_CGROUP_INET_SOCK_RELEASE},
-        {"connect4/",          BPF_PROG_TYPE_CGROUP_SOCK_ADDR, BPF_CGROUP_INET4_CONNECT},
-        {"connect6/",          BPF_PROG_TYPE_CGROUP_SOCK_ADDR, BPF_CGROUP_INET6_CONNECT},
-        {"egress/",            BPF_PROG_TYPE_CGROUP_SKB,       BPF_CGROUP_INET_EGRESS},
-        {"getsockopt/",        BPF_PROG_TYPE_CGROUP_SOCKOPT,   BPF_CGROUP_GETSOCKOPT},
-        {"ingress/",           BPF_PROG_TYPE_CGROUP_SKB,       BPF_CGROUP_INET_INGRESS},
-        {"lwt_in/",            BPF_PROG_TYPE_LWT_IN,           BPF_ATTACH_TYPE_UNSPEC},
-        {"lwt_out/",           BPF_PROG_TYPE_LWT_OUT,          BPF_ATTACH_TYPE_UNSPEC},
-        {"lwt_seg6local/",     BPF_PROG_TYPE_LWT_SEG6LOCAL,    BPF_ATTACH_TYPE_UNSPEC},
-        {"lwt_xmit/",          BPF_PROG_TYPE_LWT_XMIT,         BPF_ATTACH_TYPE_UNSPEC},
-        {"postbind4/",         BPF_PROG_TYPE_CGROUP_SOCK,      BPF_CGROUP_INET4_POST_BIND},
-        {"postbind6/",         BPF_PROG_TYPE_CGROUP_SOCK,      BPF_CGROUP_INET6_POST_BIND},
-        {"recvmsg4/",          BPF_PROG_TYPE_CGROUP_SOCK_ADDR, BPF_CGROUP_UDP4_RECVMSG},
-        {"recvmsg6/",          BPF_PROG_TYPE_CGROUP_SOCK_ADDR, BPF_CGROUP_UDP6_RECVMSG},
-        {"schedact/",          BPF_PROG_TYPE_SCHED_ACT,        BPF_ATTACH_TYPE_UNSPEC},
-        {"schedcls/",          BPF_PROG_TYPE_SCHED_CLS,        BPF_ATTACH_TYPE_UNSPEC},
-        {"sendmsg4/",          BPF_PROG_TYPE_CGROUP_SOCK_ADDR, BPF_CGROUP_UDP4_SENDMSG},
-        {"sendmsg6/",          BPF_PROG_TYPE_CGROUP_SOCK_ADDR, BPF_CGROUP_UDP6_SENDMSG},
-        {"setsockopt/",        BPF_PROG_TYPE_CGROUP_SOCKOPT,   BPF_CGROUP_SETSOCKOPT},
-        {"skfilter/",          BPF_PROG_TYPE_SOCKET_FILTER,    BPF_ATTACH_TYPE_UNSPEC},
-        {"sockops/",           BPF_PROG_TYPE_SOCK_OPS,         BPF_CGROUP_SOCK_OPS},
-        {"sysctl",             BPF_PROG_TYPE_CGROUP_SYSCTL,    BPF_CGROUP_SYSCTL},
-        {"xdp/",               BPF_PROG_TYPE_XDP,              BPF_ATTACH_TYPE_UNSPEC},
-};
-
-typedef struct {
-    enum bpf_prog_type type;
-    enum bpf_attach_type expected_attach_type;
-    string name;
-    vector<char> data;
-    vector<char> rel_data;
-    optional<struct bpf_prog_def> prog_def;
-
-    unique_fd prog_fd; /* fd after loading */
-} codeSection;
-
-static int readElfHeader(ifstream& elfFile, Elf64_Ehdr* eh) {
-    elfFile.seekg(0);
-    if (elfFile.fail()) return -1;
-
-    if (!elfFile.read((char*)eh, sizeof(*eh))) return -1;
-
-    return 0;
-}
-
-/* Reads all section header tables into an Shdr array */
-static int readSectionHeadersAll(ifstream& elfFile, vector<Elf64_Shdr>& shTable) {
-    Elf64_Ehdr eh;
-    int ret = 0;
-
-    ret = readElfHeader(elfFile, &eh);
-    if (ret) return ret;
-
-    elfFile.seekg(eh.e_shoff);
-    if (elfFile.fail()) return -1;
-
-    /* Read shdr table entries */
-    shTable.resize(eh.e_shnum);
-
-    if (!elfFile.read((char*)shTable.data(), (eh.e_shnum * eh.e_shentsize))) return -ENOMEM;
-
-    return 0;
-}
-
-/* Read a section by its index - for ex to get sec hdr strtab blob */
-static int readSectionByIdx(ifstream& elfFile, int id, vector<char>& sec) {
-    vector<Elf64_Shdr> shTable;
-    int ret = readSectionHeadersAll(elfFile, shTable);
-    if (ret) return ret;
-
-    elfFile.seekg(shTable[id].sh_offset);
-    if (elfFile.fail()) return -1;
-
-    sec.resize(shTable[id].sh_size);
-    if (!elfFile.read(sec.data(), shTable[id].sh_size)) return -1;
-
-    return 0;
-}
-
-/* Read whole section header string table */
-static int readSectionHeaderStrtab(ifstream& elfFile, vector<char>& strtab) {
-    Elf64_Ehdr eh;
-    int ret = readElfHeader(elfFile, &eh);
-    if (ret) return ret;
-
-    ret = readSectionByIdx(elfFile, eh.e_shstrndx, strtab);
-    if (ret) return ret;
-
-    return 0;
-}
-
-/* Get name from offset in strtab */
-static int getSymName(ifstream& elfFile, int nameOff, string& name) {
-    int ret;
-    vector<char> secStrTab;
-
-    ret = readSectionHeaderStrtab(elfFile, secStrTab);
-    if (ret) return ret;
-
-    if (nameOff >= (int)secStrTab.size()) return -1;
-
-    name = string((char*)secStrTab.data() + nameOff);
-    return 0;
-}
-
-/* Reads a full section by name - example to get the GPL license */
-static int readSectionByName(const char* name, ifstream& elfFile, vector<char>& data) {
-    vector<char> secStrTab;
-    vector<Elf64_Shdr> shTable;
-    int ret;
-
-    ret = readSectionHeadersAll(elfFile, shTable);
-    if (ret) return ret;
-
-    ret = readSectionHeaderStrtab(elfFile, secStrTab);
-    if (ret) return ret;
-
-    for (int i = 0; i < (int)shTable.size(); i++) {
-        char* secname = secStrTab.data() + shTable[i].sh_name;
-        if (!secname) continue;
-
-        if (!strcmp(secname, name)) {
-            vector<char> dataTmp;
-            dataTmp.resize(shTable[i].sh_size);
-
-            elfFile.seekg(shTable[i].sh_offset);
-            if (elfFile.fail()) return -1;
-
-            if (!elfFile.read((char*)dataTmp.data(), shTable[i].sh_size)) return -1;
-
-            data = dataTmp;
-            return 0;
-        }
-    }
-    return -2;
-}
-
-unsigned int readSectionUint(const char* name, ifstream& elfFile, unsigned int defVal) {
-    vector<char> theBytes;
-    int ret = readSectionByName(name, elfFile, theBytes);
-    if (ret) {
-        ALOGD("Couldn't find section %s (defaulting to %u [0x%x]).", name, defVal, defVal);
-        return defVal;
-    } else if (theBytes.size() < sizeof(unsigned int)) {
-        ALOGE("Section %s too short (defaulting to %u [0x%x]).", name, defVal, defVal);
-        return defVal;
-    } else {
-        // decode first 4 bytes as LE32 uint, there will likely be more bytes due to alignment.
-        unsigned int value = static_cast<unsigned char>(theBytes[3]);
-        value <<= 8;
-        value += static_cast<unsigned char>(theBytes[2]);
-        value <<= 8;
-        value += static_cast<unsigned char>(theBytes[1]);
-        value <<= 8;
-        value += static_cast<unsigned char>(theBytes[0]);
-        ALOGI("Section %s value is %u [0x%x]", name, value, value);
-        return value;
-    }
-}
-
-static int readSectionByType(ifstream& elfFile, int type, vector<char>& data) {
-    int ret;
-    vector<Elf64_Shdr> shTable;
-
-    ret = readSectionHeadersAll(elfFile, shTable);
-    if (ret) return ret;
-
-    for (int i = 0; i < (int)shTable.size(); i++) {
-        if ((int)shTable[i].sh_type != type) continue;
-
-        vector<char> dataTmp;
-        dataTmp.resize(shTable[i].sh_size);
-
-        elfFile.seekg(shTable[i].sh_offset);
-        if (elfFile.fail()) return -1;
-
-        if (!elfFile.read((char*)dataTmp.data(), shTable[i].sh_size)) return -1;
-
-        data = dataTmp;
-        return 0;
-    }
-    return -2;
-}
-
-static bool symCompare(Elf64_Sym a, Elf64_Sym b) {
-    return (a.st_value < b.st_value);
-}
-
-static int readSymTab(ifstream& elfFile, int sort, vector<Elf64_Sym>& data) {
-    int ret, numElems;
-    Elf64_Sym* buf;
-    vector<char> secData;
-
-    ret = readSectionByType(elfFile, SHT_SYMTAB, secData);
-    if (ret) return ret;
-
-    buf = (Elf64_Sym*)secData.data();
-    numElems = (secData.size() / sizeof(Elf64_Sym));
-    data.assign(buf, buf + numElems);
-
-    if (sort) std::sort(data.begin(), data.end(), symCompare);
-    return 0;
-}
-
-static enum bpf_prog_type getSectionType(string& name) {
-    for (auto& snt : sectionNameTypes)
-        if (StartsWith(name, snt.name)) return snt.type;
-
-    return BPF_PROG_TYPE_UNSPEC;
-}
-
-static enum bpf_attach_type getExpectedAttachType(string& name) {
-    for (auto& snt : sectionNameTypes)
-        if (StartsWith(name, snt.name)) return snt.expected_attach_type;
-    return BPF_ATTACH_TYPE_UNSPEC;
-}
-
-/*
-static string getSectionName(enum bpf_prog_type type)
-{
-    for (auto& snt : sectionNameTypes)
-        if (snt.type == type)
-            return string(snt.name);
-
-    return "UNKNOWN SECTION NAME " + std::to_string(type);
-}
-*/
-
-static int readProgDefs(ifstream& elfFile, vector<struct bpf_prog_def>& pd,
-                        size_t sizeOfBpfProgDef) {
-    vector<char> pdData;
-    int ret = readSectionByName("progs", elfFile, pdData);
-    if (ret) return ret;
-
-    if (pdData.size() % sizeOfBpfProgDef) {
-        ALOGE("readProgDefs failed due to improper sized progs section, %zu %% %zu != 0",
-              pdData.size(), sizeOfBpfProgDef);
-        return -1;
-    };
-
-    int progCount = pdData.size() / sizeOfBpfProgDef;
-    pd.resize(progCount);
-    size_t trimmedSize = std::min(sizeOfBpfProgDef, sizeof(struct bpf_prog_def));
-
-    const char* dataPtr = pdData.data();
-    for (auto& p : pd) {
-        // First we zero initialize
-        memset(&p, 0, sizeof(p));
-        // Then we set non-zero defaults
-        p.bpfloader_max_ver = DEFAULT_BPFLOADER_MAX_VER;  // v1.0
-        // Then we copy over the structure prefix from the ELF file.
-        memcpy(&p, dataPtr, trimmedSize);
-        // Move to next struct in the ELF file
-        dataPtr += sizeOfBpfProgDef;
-    }
-    return 0;
-}
-
-static int getSectionSymNames(ifstream& elfFile, const string& sectionName, vector<string>& names,
-                              optional<unsigned> symbolType = std::nullopt) {
-    int ret;
-    string name;
-    vector<Elf64_Sym> symtab;
-    vector<Elf64_Shdr> shTable;
-
-    ret = readSymTab(elfFile, 1 /* sort */, symtab);
-    if (ret) return ret;
-
-    /* Get index of section */
-    ret = readSectionHeadersAll(elfFile, shTable);
-    if (ret) return ret;
-
-    int sec_idx = -1;
-    for (int i = 0; i < (int)shTable.size(); i++) {
-        ret = getSymName(elfFile, shTable[i].sh_name, name);
-        if (ret) return ret;
-
-        if (!name.compare(sectionName)) {
-            sec_idx = i;
-            break;
-        }
-    }
-
-    /* No section found with matching name*/
-    if (sec_idx == -1) {
-        ALOGW("No %s section could be found in elf object", sectionName.c_str());
-        return -1;
-    }
-
-    for (int i = 0; i < (int)symtab.size(); i++) {
-        if (symbolType.has_value() && ELF_ST_TYPE(symtab[i].st_info) != symbolType) continue;
-
-        if (symtab[i].st_shndx == sec_idx) {
-            string s;
-            ret = getSymName(elfFile, symtab[i].st_name, s);
-            if (ret) return ret;
-            names.push_back(s);
-        }
-    }
-
-    return 0;
-}
-
-/* Read a section by its index - for ex to get sec hdr strtab blob */
-static int readCodeSections(ifstream& elfFile, vector<codeSection>& cs, size_t sizeOfBpfProgDef) {
-    vector<Elf64_Shdr> shTable;
-    int entries, ret = 0;
-
-    ret = readSectionHeadersAll(elfFile, shTable);
-    if (ret) return ret;
-    entries = shTable.size();
-
-    vector<struct bpf_prog_def> pd;
-    ret = readProgDefs(elfFile, pd, sizeOfBpfProgDef);
-    if (ret) return ret;
-    vector<string> progDefNames;
-    ret = getSectionSymNames(elfFile, "progs", progDefNames);
-    if (!pd.empty() && ret) return ret;
-
-    for (int i = 0; i < entries; i++) {
-        string name;
-        codeSection cs_temp;
-        cs_temp.type = BPF_PROG_TYPE_UNSPEC;
-
-        ret = getSymName(elfFile, shTable[i].sh_name, name);
-        if (ret) return ret;
-
-        enum bpf_prog_type ptype = getSectionType(name);
-
-        if (ptype == BPF_PROG_TYPE_UNSPEC) continue;
-
-        // This must be done before '/' is replaced with '_'.
-        cs_temp.expected_attach_type = getExpectedAttachType(name);
-
-        string oldName = name;
-
-        // convert all slashes to underscores
-        std::replace(name.begin(), name.end(), '/', '_');
-
-        cs_temp.type = ptype;
-        cs_temp.name = name;
-
-        ret = readSectionByIdx(elfFile, i, cs_temp.data);
-        if (ret) return ret;
-        ALOGV("Loaded code section %d (%s)", i, name.c_str());
-
-        vector<string> csSymNames;
-        ret = getSectionSymNames(elfFile, oldName, csSymNames, STT_FUNC);
-        if (ret || !csSymNames.size()) return ret;
-        for (size_t i = 0; i < progDefNames.size(); ++i) {
-            if (!progDefNames[i].compare(csSymNames[0] + "_def")) {
-                cs_temp.prog_def = pd[i];
-                break;
-            }
-        }
-
-        /* Check for rel section */
-        if (cs_temp.data.size() > 0 && i < entries) {
-            ret = getSymName(elfFile, shTable[i + 1].sh_name, name);
-            if (ret) return ret;
-
-            if (name == (".rel" + oldName)) {
-                ret = readSectionByIdx(elfFile, i + 1, cs_temp.rel_data);
-                if (ret) return ret;
-                ALOGV("Loaded relo section %d (%s)", i, name.c_str());
-            }
-        }
-
-        if (cs_temp.data.size() > 0) {
-            cs.push_back(std::move(cs_temp));
-            ALOGV("Adding section %d to cs list", i);
-        }
-    }
-    return 0;
-}
-
-static int getSymNameByIdx(ifstream& elfFile, int index, string& name) {
-    vector<Elf64_Sym> symtab;
-    int ret = 0;
-
-    ret = readSymTab(elfFile, 0 /* !sort */, symtab);
-    if (ret) return ret;
-
-    if (index >= (int)symtab.size()) return -1;
-
-    return getSymName(elfFile, symtab[index].st_name, name);
-}
-
-static bool mapMatchesExpectations(const unique_fd& fd, const string& mapName,
-                                   const struct bpf_map_def& mapDef, const enum bpf_map_type type) {
-    // bpfGetFd... family of functions require at minimum a 4.14 kernel,
-    // so on 4.9-T kernels just pretend the map matches our expectations.
-    // Additionally we'll get almost equivalent test coverage on newer devices/kernels.
-    // This is because the primary failure mode we're trying to detect here
-    // is either a source code misconfiguration (which is likely kernel independent)
-    // or a newly introduced kernel feature/bug (which is unlikely to get backported to 4.9).
-    if (!isAtLeastKernelVersion(4, 14, 0)) return true;
-
-    // Assuming fd is a valid Bpf Map file descriptor then
-    // all the following should always succeed on a 4.14+ kernel.
-    // If they somehow do fail, they'll return -1 (and set errno),
-    // which should then cause (among others) a key_size mismatch.
-    int fd_type = bpfGetFdMapType(fd);
-    int fd_key_size = bpfGetFdKeySize(fd);
-    int fd_value_size = bpfGetFdValueSize(fd);
-    int fd_max_entries = bpfGetFdMaxEntries(fd);
-    int fd_map_flags = bpfGetFdMapFlags(fd);
-
-    // DEVMAPs are readonly from the bpf program side's point of view, as such
-    // the kernel in kernel/bpf/devmap.c dev_map_init_map() will set the flag
-    int desired_map_flags = (int)mapDef.map_flags;
-    if (type == BPF_MAP_TYPE_DEVMAP || type == BPF_MAP_TYPE_DEVMAP_HASH)
-        desired_map_flags |= BPF_F_RDONLY_PROG;
-
-    // The .h file enforces that this is a power of two, and page size will
-    // also always be a power of two, so this logic is actually enough to
-    // force it to be a multiple of the page size, as required by the kernel.
-    unsigned int desired_max_entries = mapDef.max_entries;
-    if (type == BPF_MAP_TYPE_RINGBUF) {
-        if (desired_max_entries < page_size) desired_max_entries = page_size;
-    }
-
-    // The following checks should *never* trigger, if one of them somehow does,
-    // it probably means a bpf .o file has been changed/replaced at runtime
-    // and bpfloader was manually rerun (normally it should only run *once*
-    // early during the boot process).
-    // Another possibility is that something is misconfigured in the code:
-    // most likely a shared map is declared twice differently.
-    // But such a change should never be checked into the source tree...
-    if ((fd_type == type) &&
-        (fd_key_size == (int)mapDef.key_size) &&
-        (fd_value_size == (int)mapDef.value_size) &&
-        (fd_max_entries == (int)desired_max_entries) &&
-        (fd_map_flags == desired_map_flags)) {
-        return true;
-    }
-
-    ALOGE("bpf map name %s mismatch: desired/found: "
-          "type:%d/%d key:%u/%d value:%u/%d entries:%u/%d flags:%u/%d",
-          mapName.c_str(), type, fd_type, mapDef.key_size, fd_key_size, mapDef.value_size,
-          fd_value_size, mapDef.max_entries, fd_max_entries, desired_map_flags, fd_map_flags);
-    return false;
-}
-
-static int createMaps(const char* elfPath, ifstream& elfFile, vector<unique_fd>& mapFds,
-                      const char* prefix, const size_t sizeOfBpfMapDef,
-                      const unsigned int bpfloader_ver) {
-    int ret;
-    vector<char> mdData;
-    vector<struct bpf_map_def> md;
-    vector<string> mapNames;
-    string objName = pathToObjName(string(elfPath));
-
-    ret = readSectionByName("maps", elfFile, mdData);
-    if (ret == -2) return 0;  // no maps to read
-    if (ret) return ret;
-
-    if (mdData.size() % sizeOfBpfMapDef) {
-        ALOGE("createMaps failed due to improper sized maps section, %zu %% %zu != 0",
-              mdData.size(), sizeOfBpfMapDef);
-        return -1;
-    };
-
-    int mapCount = mdData.size() / sizeOfBpfMapDef;
-    md.resize(mapCount);
-    size_t trimmedSize = std::min(sizeOfBpfMapDef, sizeof(struct bpf_map_def));
-
-    const char* dataPtr = mdData.data();
-    for (auto& m : md) {
-        // First we zero initialize
-        memset(&m, 0, sizeof(m));
-        // Then we set non-zero defaults
-        m.bpfloader_max_ver = DEFAULT_BPFLOADER_MAX_VER;  // v1.0
-        m.max_kver = 0xFFFFFFFFu;                         // matches KVER_INF from bpf_helpers.h
-        // Then we copy over the structure prefix from the ELF file.
-        memcpy(&m, dataPtr, trimmedSize);
-        // Move to next struct in the ELF file
-        dataPtr += sizeOfBpfMapDef;
-    }
-
-    ret = getSectionSymNames(elfFile, "maps", mapNames);
-    if (ret) return ret;
-
-    unsigned kvers = kernelVersion();
-
-    for (int i = 0; i < (int)mapNames.size(); i++) {
-        if (md[i].zero != 0) abort();
-
-        if (bpfloader_ver < md[i].bpfloader_min_ver) {
-            ALOGI("skipping map %s which requires bpfloader min ver 0x%05x", mapNames[i].c_str(),
-                  md[i].bpfloader_min_ver);
-            mapFds.push_back(unique_fd());
-            continue;
-        }
-
-        if (bpfloader_ver >= md[i].bpfloader_max_ver) {
-            ALOGI("skipping map %s which requires bpfloader max ver 0x%05x", mapNames[i].c_str(),
-                  md[i].bpfloader_max_ver);
-            mapFds.push_back(unique_fd());
-            continue;
-        }
-
-        if (kvers < md[i].min_kver) {
-            ALOGI("skipping map %s which requires kernel version 0x%x >= 0x%x",
-                  mapNames[i].c_str(), kvers, md[i].min_kver);
-            mapFds.push_back(unique_fd());
-            continue;
-        }
-
-        if (kvers >= md[i].max_kver) {
-            ALOGI("skipping map %s which requires kernel version 0x%x < 0x%x",
-                  mapNames[i].c_str(), kvers, md[i].max_kver);
-            mapFds.push_back(unique_fd());
-            continue;
-        }
-
-        if ((md[i].ignore_on_eng && isEng()) || (md[i].ignore_on_user && isUser()) ||
-            (md[i].ignore_on_userdebug && isUserdebug())) {
-            ALOGI("skipping map %s which is ignored on %s builds", mapNames[i].c_str(),
-                  getBuildType().c_str());
-            mapFds.push_back(unique_fd());
-            continue;
-        }
-
-        if ((isArm() && isKernel32Bit() && md[i].ignore_on_arm32) ||
-            (isArm() && isKernel64Bit() && md[i].ignore_on_aarch64) ||
-            (isX86() && isKernel32Bit() && md[i].ignore_on_x86_32) ||
-            (isX86() && isKernel64Bit() && md[i].ignore_on_x86_64) ||
-            (isRiscV() && md[i].ignore_on_riscv64)) {
-            ALOGI("skipping map %s which is ignored on %s", mapNames[i].c_str(),
-                  describeArch());
-            mapFds.push_back(unique_fd());
-            continue;
-        }
-
-        enum bpf_map_type type = md[i].type;
-        if (type == BPF_MAP_TYPE_DEVMAP && !isAtLeastKernelVersion(4, 14, 0)) {
-            // On Linux Kernels older than 4.14 this map type doesn't exist, but it can kind
-            // of be approximated: ARRAY has the same userspace api, though it is not usable
-            // by the same ebpf programs.  However, that's okay because the bpf_redirect_map()
-            // helper doesn't exist on 4.9-T anyway (so the bpf program would fail to load,
-            // and thus needs to be tagged as 4.14+ either way), so there's nothing useful you
-            // could do with a DEVMAP anyway (that isn't already provided by an ARRAY)...
-            // Hence using an ARRAY instead of a DEVMAP simply makes life easier for userspace.
-            type = BPF_MAP_TYPE_ARRAY;
-        }
-        if (type == BPF_MAP_TYPE_DEVMAP_HASH && !isAtLeastKernelVersion(5, 4, 0)) {
-            // On Linux Kernels older than 5.4 this map type doesn't exist, but it can kind
-            // of be approximated: HASH has the same userspace visible api.
-            // However it cannot be used by ebpf programs in the same way.
-            // Since bpf_redirect_map() only requires 4.14, a program using a DEVMAP_HASH map
-            // would fail to load (due to trying to redirect to a HASH instead of DEVMAP_HASH).
-            // One must thus tag any BPF_MAP_TYPE_DEVMAP_HASH + bpf_redirect_map() using
-            // programs as being 5.4+...
-            type = BPF_MAP_TYPE_HASH;
-        }
-
-        // The .h file enforces that this is a power of two, and page size will
-        // also always be a power of two, so this logic is actually enough to
-        // force it to be a multiple of the page size, as required by the kernel.
-        unsigned int max_entries = md[i].max_entries;
-        if (type == BPF_MAP_TYPE_RINGBUF) {
-            if (max_entries < page_size) max_entries = page_size;
-        }
-
-        domain selinux_context = getDomainFromSelinuxContext(md[i].selinux_context);
-        if (specified(selinux_context)) {
-            ALOGI("map %s selinux_context [%-32s] -> %d -> '%s' (%s)", mapNames[i].c_str(),
-                  md[i].selinux_context, static_cast<int>(selinux_context),
-                  lookupSelinuxContext(selinux_context), lookupPinSubdir(selinux_context));
-        }
-
-        domain pin_subdir = getDomainFromPinSubdir(md[i].pin_subdir);
-        if (unrecognized(pin_subdir)) return -ENOTDIR;
-        if (specified(pin_subdir)) {
-            ALOGI("map %s pin_subdir [%-32s] -> %d -> '%s'", mapNames[i].c_str(), md[i].pin_subdir,
-                  static_cast<int>(pin_subdir), lookupPinSubdir(pin_subdir));
-        }
-
-        // Format of pin location is /sys/fs/bpf/<pin_subdir|prefix>map_<objName>_<mapName>
-        // except that maps shared across .o's have empty <objName>
-        // Note: <objName> refers to the extension-less basename of the .o file (without @ suffix).
-        string mapPinLoc = string(BPF_FS_PATH) + lookupPinSubdir(pin_subdir, prefix) + "map_" +
-                           (md[i].shared ? "" : objName) + "_" + mapNames[i];
-        bool reuse = false;
-        unique_fd fd;
-        int saved_errno;
-
-        if (access(mapPinLoc.c_str(), F_OK) == 0) {
-            fd.reset(mapRetrieveRO(mapPinLoc.c_str()));
-            saved_errno = errno;
-            ALOGD("bpf_create_map reusing map %s, ret: %d", mapNames[i].c_str(), fd.get());
-            reuse = true;
-        } else {
-            union bpf_attr req = {
-              .map_type = type,
-              .key_size = md[i].key_size,
-              .value_size = md[i].value_size,
-              .max_entries = max_entries,
-              .map_flags = md[i].map_flags,
-            };
-            if (isAtLeastKernelVersion(4, 15, 0))
-                strlcpy(req.map_name, mapNames[i].c_str(), sizeof(req.map_name));
-            fd.reset(bpf(BPF_MAP_CREATE, req));
-            saved_errno = errno;
-            ALOGD("bpf_create_map name %s, ret: %d", mapNames[i].c_str(), fd.get());
-        }
-
-        if (!fd.ok()) return -saved_errno;
-
-        // When reusing a pinned map, we need to check the map type/sizes/etc match, but for
-        // safety (since reuse code path is rare) run these checks even if we just created it.
-        // We assume failure is due to pinned map mismatch, hence the 'NOT UNIQUE' return code.
-        if (!mapMatchesExpectations(fd, mapNames[i], md[i], type)) return -ENOTUNIQ;
-
-        if (!reuse) {
-            if (specified(selinux_context)) {
-                string createLoc = string(BPF_FS_PATH) + lookupPinSubdir(selinux_context) +
-                                   "tmp_map_" + objName + "_" + mapNames[i];
-                ret = bpfFdPin(fd, createLoc.c_str());
-                if (ret) {
-                    int err = errno;
-                    ALOGE("create %s -> %d [%d:%s]", createLoc.c_str(), ret, err, strerror(err));
-                    return -err;
-                }
-                ret = renameat2(AT_FDCWD, createLoc.c_str(),
-                                AT_FDCWD, mapPinLoc.c_str(), RENAME_NOREPLACE);
-                if (ret) {
-                    int err = errno;
-                    ALOGE("rename %s %s -> %d [%d:%s]", createLoc.c_str(), mapPinLoc.c_str(), ret,
-                          err, strerror(err));
-                    return -err;
-                }
-            } else {
-                ret = bpfFdPin(fd, mapPinLoc.c_str());
-                if (ret) {
-                    int err = errno;
-                    ALOGE("pin %s -> %d [%d:%s]", mapPinLoc.c_str(), ret, err, strerror(err));
-                    return -err;
-                }
-            }
-            ret = chmod(mapPinLoc.c_str(), md[i].mode);
-            if (ret) {
-                int err = errno;
-                ALOGE("chmod(%s, 0%o) = %d [%d:%s]", mapPinLoc.c_str(), md[i].mode, ret, err,
-                      strerror(err));
-                return -err;
-            }
-            ret = chown(mapPinLoc.c_str(), (uid_t)md[i].uid, (gid_t)md[i].gid);
-            if (ret) {
-                int err = errno;
-                ALOGE("chown(%s, %u, %u) = %d [%d:%s]", mapPinLoc.c_str(), md[i].uid, md[i].gid,
-                      ret, err, strerror(err));
-                return -err;
-            }
-        }
-
-        int mapId = bpfGetFdMapId(fd);
-        if (mapId == -1) {
-            ALOGE("bpfGetFdMapId failed, ret: %d [%d]", mapId, errno);
-        } else {
-            ALOGI("map %s id %d", mapPinLoc.c_str(), mapId);
-        }
-
-        mapFds.push_back(std::move(fd));
-    }
-
-    return ret;
-}
-
-/* For debugging, dump all instructions */
-static void dumpIns(char* ins, int size) {
-    for (int row = 0; row < size / 8; row++) {
-        ALOGE("%d: ", row);
-        for (int j = 0; j < 8; j++) {
-            ALOGE("%3x ", ins[(row * 8) + j]);
-        }
-        ALOGE("\n");
-    }
-}
-
-/* For debugging, dump all code sections from cs list */
-static void dumpAllCs(vector<codeSection>& cs) {
-    for (int i = 0; i < (int)cs.size(); i++) {
-        ALOGE("Dumping cs %d, name %s", int(i), cs[i].name.c_str());
-        dumpIns((char*)cs[i].data.data(), cs[i].data.size());
-        ALOGE("-----------");
-    }
-}
-
-static void applyRelo(void* insnsPtr, Elf64_Addr offset, int fd) {
-    int insnIndex;
-    struct bpf_insn *insn, *insns;
-
-    insns = (struct bpf_insn*)(insnsPtr);
-
-    insnIndex = offset / sizeof(struct bpf_insn);
-    insn = &insns[insnIndex];
-
-    // Occasionally might be useful for relocation debugging, but pretty spammy
-    if (0) {
-        ALOGV("applying relo to instruction at byte offset: %llu, "
-              "insn offset %d, insn %llx",
-              (unsigned long long)offset, insnIndex, *(unsigned long long*)insn);
-    }
-
-    if (insn->code != (BPF_LD | BPF_IMM | BPF_DW)) {
-        ALOGE("Dumping all instructions till ins %d", insnIndex);
-        ALOGE("invalid relo for insn %d: code 0x%x", insnIndex, insn->code);
-        dumpIns((char*)insnsPtr, (insnIndex + 3) * 8);
-        return;
-    }
-
-    insn->imm = fd;
-    insn->src_reg = BPF_PSEUDO_MAP_FD;
-}
-
-static void applyMapRelo(ifstream& elfFile, vector<unique_fd> &mapFds, vector<codeSection>& cs) {
-    vector<string> mapNames;
-
-    int ret = getSectionSymNames(elfFile, "maps", mapNames);
-    if (ret) return;
-
-    for (int k = 0; k != (int)cs.size(); k++) {
-        Elf64_Rel* rel = (Elf64_Rel*)(cs[k].rel_data.data());
-        int n_rel = cs[k].rel_data.size() / sizeof(*rel);
-
-        for (int i = 0; i < n_rel; i++) {
-            int symIndex = ELF64_R_SYM(rel[i].r_info);
-            string symName;
-
-            ret = getSymNameByIdx(elfFile, symIndex, symName);
-            if (ret) return;
-
-            /* Find the map fd and apply relo */
-            for (int j = 0; j < (int)mapNames.size(); j++) {
-                if (!mapNames[j].compare(symName)) {
-                    applyRelo(cs[k].data.data(), rel[i].r_offset, mapFds[j]);
-                    break;
-                }
-            }
-        }
-    }
-}
-
-static int loadCodeSections(const char* elfPath, vector<codeSection>& cs, const string& license,
-                            const char* prefix, const unsigned int bpfloader_ver) {
-    unsigned kvers = kernelVersion();
-
-    if (!kvers) {
-        ALOGE("unable to get kernel version");
-        return -EINVAL;
-    }
-
-    string objName = pathToObjName(string(elfPath));
-
-    for (int i = 0; i < (int)cs.size(); i++) {
-        unique_fd& fd = cs[i].prog_fd;
-        int ret;
-        string name = cs[i].name;
-
-        if (!cs[i].prog_def.has_value()) {
-            ALOGE("[%d] '%s' missing program definition! bad bpf.o build?", i, name.c_str());
-            return -EINVAL;
-        }
-
-        unsigned min_kver = cs[i].prog_def->min_kver;
-        unsigned max_kver = cs[i].prog_def->max_kver;
-        ALOGD("cs[%d].name:%s min_kver:%x .max_kver:%x (kvers:%x)", i, name.c_str(), min_kver,
-             max_kver, kvers);
-        if (kvers < min_kver) continue;
-        if (kvers >= max_kver) continue;
-
-        unsigned bpfMinVer = cs[i].prog_def->bpfloader_min_ver;
-        unsigned bpfMaxVer = cs[i].prog_def->bpfloader_max_ver;
-        domain selinux_context = getDomainFromSelinuxContext(cs[i].prog_def->selinux_context);
-        domain pin_subdir = getDomainFromPinSubdir(cs[i].prog_def->pin_subdir);
-        // Note: make sure to only check for unrecognized *after* verifying bpfloader
-        // version limits include this bpfloader's version.
-
-        ALOGD("cs[%d].name:%s requires bpfloader version [0x%05x,0x%05x)", i, name.c_str(),
-              bpfMinVer, bpfMaxVer);
-        if (bpfloader_ver < bpfMinVer) continue;
-        if (bpfloader_ver >= bpfMaxVer) continue;
-
-        if ((cs[i].prog_def->ignore_on_eng && isEng()) ||
-            (cs[i].prog_def->ignore_on_user && isUser()) ||
-            (cs[i].prog_def->ignore_on_userdebug && isUserdebug())) {
-            ALOGD("cs[%d].name:%s is ignored on %s builds", i, name.c_str(),
-                  getBuildType().c_str());
-            continue;
-        }
-
-        if ((isArm() && isKernel32Bit() && cs[i].prog_def->ignore_on_arm32) ||
-            (isArm() && isKernel64Bit() && cs[i].prog_def->ignore_on_aarch64) ||
-            (isX86() && isKernel32Bit() && cs[i].prog_def->ignore_on_x86_32) ||
-            (isX86() && isKernel64Bit() && cs[i].prog_def->ignore_on_x86_64) ||
-            (isRiscV() && cs[i].prog_def->ignore_on_riscv64)) {
-            ALOGD("cs[%d].name:%s is ignored on %s", i, name.c_str(), describeArch());
-            continue;
-        }
-
-        if (unrecognized(pin_subdir)) return -ENOTDIR;
-
-        if (specified(selinux_context)) {
-            ALOGI("prog %s selinux_context [%-32s] -> %d -> '%s' (%s)", name.c_str(),
-                  cs[i].prog_def->selinux_context, static_cast<int>(selinux_context),
-                  lookupSelinuxContext(selinux_context), lookupPinSubdir(selinux_context));
-        }
-
-        if (specified(pin_subdir)) {
-            ALOGI("prog %s pin_subdir [%-32s] -> %d -> '%s'", name.c_str(),
-                  cs[i].prog_def->pin_subdir, static_cast<int>(pin_subdir),
-                  lookupPinSubdir(pin_subdir));
-        }
-
-        // strip any potential $foo suffix
-        // this can be used to provide duplicate programs
-        // conditionally loaded based on running kernel version
-        name = name.substr(0, name.find_last_of('$'));
-
-        bool reuse = false;
-        // Format of pin location is
-        // /sys/fs/bpf/<prefix>prog_<objName>_<progName>
-        string progPinLoc = string(BPF_FS_PATH) + lookupPinSubdir(pin_subdir, prefix) + "prog_" +
-                            objName + '_' + string(name);
-        if (access(progPinLoc.c_str(), F_OK) == 0) {
-            fd.reset(retrieveProgram(progPinLoc.c_str()));
-            ALOGD("New bpf prog load reusing prog %s, ret: %d (%s)", progPinLoc.c_str(), fd.get(),
-                  (!fd.ok() ? std::strerror(errno) : "no error"));
-            reuse = true;
-        } else {
-            vector<char> log_buf(BPF_LOAD_LOG_SZ, 0);
-
-            union bpf_attr req = {
-              .prog_type = cs[i].type,
-              .kern_version = kvers,
-              .license = ptr_to_u64(license.c_str()),
-              .insns = ptr_to_u64(cs[i].data.data()),
-              .insn_cnt = static_cast<__u32>(cs[i].data.size() / sizeof(struct bpf_insn)),
-              .log_level = 1,
-              .log_buf = ptr_to_u64(log_buf.data()),
-              .log_size = static_cast<__u32>(log_buf.size()),
-              .expected_attach_type = cs[i].expected_attach_type,
-            };
-            if (isAtLeastKernelVersion(4, 15, 0))
-                strlcpy(req.prog_name, cs[i].name.c_str(), sizeof(req.prog_name));
-            fd.reset(bpf(BPF_PROG_LOAD, req));
-
-            ALOGD("BPF_PROG_LOAD call for %s (%s) returned fd: %d (%s)", elfPath,
-                  cs[i].name.c_str(), fd.get(), (!fd.ok() ? std::strerror(errno) : "no error"));
-
-            if (!fd.ok()) {
-                vector<string> lines = android::base::Split(log_buf.data(), "\n");
-
-                ALOGW("BPF_PROG_LOAD - BEGIN log_buf contents:");
-                for (const auto& line : lines) ALOGW("%s", line.c_str());
-                ALOGW("BPF_PROG_LOAD - END log_buf contents.");
-
-                if (cs[i].prog_def->optional) {
-                    ALOGW("failed program is marked optional - continuing...");
-                    continue;
-                }
-                ALOGE("non-optional program failed to load.");
-            }
-        }
-
-        if (!fd.ok()) return fd.get();
-
-        if (!reuse) {
-            if (specified(selinux_context)) {
-                string createLoc = string(BPF_FS_PATH) + lookupPinSubdir(selinux_context) +
-                                   "tmp_prog_" + objName + '_' + string(name);
-                ret = bpfFdPin(fd, createLoc.c_str());
-                if (ret) {
-                    int err = errno;
-                    ALOGE("create %s -> %d [%d:%s]", createLoc.c_str(), ret, err, strerror(err));
-                    return -err;
-                }
-                ret = renameat2(AT_FDCWD, createLoc.c_str(),
-                                AT_FDCWD, progPinLoc.c_str(), RENAME_NOREPLACE);
-                if (ret) {
-                    int err = errno;
-                    ALOGE("rename %s %s -> %d [%d:%s]", createLoc.c_str(), progPinLoc.c_str(), ret,
-                          err, strerror(err));
-                    return -err;
-                }
-            } else {
-                ret = bpfFdPin(fd, progPinLoc.c_str());
-                if (ret) {
-                    int err = errno;
-                    ALOGE("create %s -> %d [%d:%s]", progPinLoc.c_str(), ret, err, strerror(err));
-                    return -err;
-                }
-            }
-            if (chmod(progPinLoc.c_str(), 0440)) {
-                int err = errno;
-                ALOGE("chmod %s 0440 -> [%d:%s]", progPinLoc.c_str(), err, strerror(err));
-                return -err;
-            }
-            if (chown(progPinLoc.c_str(), (uid_t)cs[i].prog_def->uid,
-                      (gid_t)cs[i].prog_def->gid)) {
-                int err = errno;
-                ALOGE("chown %s %d %d -> [%d:%s]", progPinLoc.c_str(), cs[i].prog_def->uid,
-                      cs[i].prog_def->gid, err, strerror(err));
-                return -err;
-            }
-        }
-
-        int progId = bpfGetFdProgId(fd);
-        if (progId == -1) {
-            ALOGE("bpfGetFdProgId failed, ret: %d [%d]", progId, errno);
-        } else {
-            ALOGI("prog %s id %d", progPinLoc.c_str(), progId);
-        }
-    }
-
-    return 0;
-}
-
-int loadProg(const char* const elfPath, bool* const isCritical, const unsigned int bpfloader_ver,
-             const Location& location) {
-    vector<char> license;
-    vector<char> critical;
-    vector<codeSection> cs;
-    vector<unique_fd> mapFds;
-    int ret;
-
-    if (!isCritical) return -1;
-    *isCritical = false;
-
-    ifstream elfFile(elfPath, ios::in | ios::binary);
-    if (!elfFile.is_open()) return -1;
-
-    ret = readSectionByName("critical", elfFile, critical);
-    *isCritical = !ret;
-
-    ret = readSectionByName("license", elfFile, license);
-    if (ret) {
-        ALOGE("Couldn't find license in %s", elfPath);
-        return ret;
-    } else {
-        ALOGD("Loading %s%s ELF object %s with license %s",
-              *isCritical ? "critical for " : "optional", *isCritical ? (char*)critical.data() : "",
-              elfPath, (char*)license.data());
-    }
-
-    // the following default values are for bpfloader V0.0 format which does not include them
-    unsigned int bpfLoaderMinVer =
-            readSectionUint("bpfloader_min_ver", elfFile, DEFAULT_BPFLOADER_MIN_VER);
-    unsigned int bpfLoaderMaxVer =
-            readSectionUint("bpfloader_max_ver", elfFile, DEFAULT_BPFLOADER_MAX_VER);
-    unsigned int bpfLoaderMinRequiredVer =
-            readSectionUint("bpfloader_min_required_ver", elfFile, 0);
-    size_t sizeOfBpfMapDef =
-            readSectionUint("size_of_bpf_map_def", elfFile, DEFAULT_SIZEOF_BPF_MAP_DEF);
-    size_t sizeOfBpfProgDef =
-            readSectionUint("size_of_bpf_prog_def", elfFile, DEFAULT_SIZEOF_BPF_PROG_DEF);
-
-    // inclusive lower bound check
-    if (bpfloader_ver < bpfLoaderMinVer) {
-        ALOGI("BpfLoader version 0x%05x ignoring ELF object %s with min ver 0x%05x",
-              bpfloader_ver, elfPath, bpfLoaderMinVer);
-        return 0;
-    }
-
-    // exclusive upper bound check
-    if (bpfloader_ver >= bpfLoaderMaxVer) {
-        ALOGI("BpfLoader version 0x%05x ignoring ELF object %s with max ver 0x%05x",
-              bpfloader_ver, elfPath, bpfLoaderMaxVer);
-        return 0;
-    }
-
-    if (bpfloader_ver < bpfLoaderMinRequiredVer) {
-        ALOGI("BpfLoader version 0x%05x failing due to ELF object %s with required min ver 0x%05x",
-              bpfloader_ver, elfPath, bpfLoaderMinRequiredVer);
-        return -1;
-    }
-
-    ALOGI("BpfLoader version 0x%05x processing ELF object %s with ver [0x%05x,0x%05x)",
-          bpfloader_ver, elfPath, bpfLoaderMinVer, bpfLoaderMaxVer);
-
-    if (sizeOfBpfMapDef < DEFAULT_SIZEOF_BPF_MAP_DEF) {
-        ALOGE("sizeof(bpf_map_def) of %zu is too small (< %d)", sizeOfBpfMapDef,
-              DEFAULT_SIZEOF_BPF_MAP_DEF);
-        return -1;
-    }
-
-    if (sizeOfBpfProgDef < DEFAULT_SIZEOF_BPF_PROG_DEF) {
-        ALOGE("sizeof(bpf_prog_def) of %zu is too small (< %d)", sizeOfBpfProgDef,
-              DEFAULT_SIZEOF_BPF_PROG_DEF);
-        return -1;
-    }
-
-    ret = readCodeSections(elfFile, cs, sizeOfBpfProgDef);
-    if (ret) {
-        ALOGE("Couldn't read all code sections in %s", elfPath);
-        return ret;
-    }
-
-    /* Just for future debugging */
-    if (0) dumpAllCs(cs);
-
-    ret = createMaps(elfPath, elfFile, mapFds, location.prefix, sizeOfBpfMapDef, bpfloader_ver);
-    if (ret) {
-        ALOGE("Failed to create maps: (ret=%d) in %s", ret, elfPath);
-        return ret;
-    }
-
-    for (int i = 0; i < (int)mapFds.size(); i++)
-        ALOGV("map_fd found at %d is %d in %s", i, mapFds[i].get(), elfPath);
-
-    applyMapRelo(elfFile, mapFds, cs);
-
-    ret = loadCodeSections(elfPath, cs, string(license.data()), location.prefix, bpfloader_ver);
-    if (ret) ALOGE("Failed to load programs, loadCodeSections ret=%d", ret);
-
-    return ret;
-}
-
-}  // namespace bpf
-}  // namespace android
diff --git a/netbpfload/loader.h b/netbpfload/loader.h
deleted file mode 100644
index 4da6830..0000000
--- a/netbpfload/loader.h
+++ /dev/null
@@ -1,94 +0,0 @@
-/*
- * Copyright (C) 2018-2023 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.
- */
-
-#pragma once
-
-#include <linux/bpf.h>
-
-#include <fstream>
-
-namespace android {
-namespace bpf {
-
-// Bpf programs may specify per-program & per-map selinux_context and pin_subdir.
-//
-// The BpfLoader needs to convert these bpf.o specified strings into an enum
-// for internal use (to check that valid values were specified for the specific
-// location of the bpf.o file).
-//
-// It also needs to map selinux_context's into pin_subdir's.
-// This is because of how selinux_context is actually implemented via pin+rename.
-//
-// Thus 'domain' enumerates all selinux_context's/pin_subdir's that the BpfLoader
-// is aware of.  Thus there currently needs to be a 1:1 mapping between the two.
-//
-enum class domain : int {
-    unrecognized = -1,  // invalid for this version of the bpfloader
-    unspecified = 0,    // means just use the default for that specific pin location
-    tethering,          // (S+) fs_bpf_tethering     /sys/fs/bpf/tethering
-    net_private,        // (T+) fs_bpf_net_private   /sys/fs/bpf/net_private
-    net_shared,         // (T+) fs_bpf_net_shared    /sys/fs/bpf/net_shared
-    netd_readonly,      // (T+) fs_bpf_netd_readonly /sys/fs/bpf/netd_readonly
-    netd_shared,        // (T+) fs_bpf_netd_shared   /sys/fs/bpf/netd_shared
-};
-
-// Note: this does not include domain::unrecognized, but does include domain::unspecified
-static constexpr domain AllDomains[] = {
-    domain::unspecified,
-    domain::tethering,
-    domain::net_private,
-    domain::net_shared,
-    domain::netd_readonly,
-    domain::netd_shared,
-};
-
-static constexpr bool unrecognized(domain d) {
-    return d == domain::unrecognized;
-}
-
-// Note: this doesn't handle unrecognized, handle it first.
-static constexpr bool specified(domain d) {
-    return d != domain::unspecified;
-}
-
-struct Location {
-    const char* const dir = "";
-    const char* const prefix = "";
-};
-
-// BPF loader implementation. Loads an eBPF ELF object
-int loadProg(const char* elfPath, bool* isCritical, const unsigned int bpfloader_ver,
-             const Location &location = {});
-
-// Exposed for testing
-unsigned int readSectionUint(const char* name, std::ifstream& elfFile, unsigned int defVal);
-
-// Returns the build type string (from ro.build.type).
-const std::string& getBuildType();
-
-// The following functions classify the 3 Android build types.
-inline bool isEng() {
-    return getBuildType() == "eng";
-}
-inline bool isUser() {
-    return getBuildType() == "user";
-}
-inline bool isUserdebug() {
-    return getBuildType() == "userdebug";
-}
-
-}  // namespace bpf
-}  // namespace android
diff --git a/netbpfload/netbpfload.33rc b/netbpfload/netbpfload.33rc
index 493731f..eb937dd 100644
--- a/netbpfload/netbpfload.33rc
+++ b/netbpfload/netbpfload.33rc
@@ -18,4 +18,3 @@
     rlimit memlock 1073741824 1073741824
     oneshot
     reboot_on_failure reboot,netbpfload-failed
-    override
diff --git a/netd/BpfBaseTest.cpp b/netd/BpfBaseTest.cpp
index c979a7b..34dfbb4 100644
--- a/netd/BpfBaseTest.cpp
+++ b/netd/BpfBaseTest.cpp
@@ -56,7 +56,7 @@
 
 TEST_F(BpfBasicTest, TestCgroupMounted) {
     std::string cg2_path;
-    ASSERT_EQ(true, CgroupGetControllerPath(CGROUPV2_CONTROLLER_NAME, &cg2_path));
+    ASSERT_EQ(true, CgroupGetControllerPath(CGROUPV2_HIERARCHY_NAME, &cg2_path));
     ASSERT_EQ(0, access(cg2_path.c_str(), R_OK));
     ASSERT_EQ(0, access((cg2_path + "/cgroup.controllers").c_str(), R_OK));
 }
diff --git a/networksecurity/OWNERS b/networksecurity/OWNERS
new file mode 100644
index 0000000..1a4130a
--- /dev/null
+++ b/networksecurity/OWNERS
@@ -0,0 +1,4 @@
+# Bug component: 1479456
+
+sandrom@google.com
+tweek@google.com
diff --git a/tests/cts/hostside-network-policy/aidl/Android.bp b/networksecurity/framework/Android.bp
similarity index 72%
rename from tests/cts/hostside-network-policy/aidl/Android.bp
rename to networksecurity/framework/Android.bp
index b182090..2b77926 100644
--- a/tests/cts/hostside-network-policy/aidl/Android.bp
+++ b/networksecurity/framework/Android.bp
@@ -1,3 +1,4 @@
+//
 // Copyright (C) 2024 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
@@ -11,16 +12,20 @@
 // 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_framework_backstage_power",
+    default_team: "trendy_team_platform_security",
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
-java_test_helper_library {
-    name: "CtsHostsideNetworkPolicyTestsAidl",
-    sdk_version: "current",
+filegroup {
+    name: "framework-networksecurity-sources",
     srcs: [
-        "com/android/cts/netpolicy/hostside/*.aidl",
+        "src/**/*.java",
+        "src/**/*.aidl",
+    ],
+    path: "src",
+    visibility: [
+        "//packages/modules/Connectivity:__subpackages__",
     ],
 }
diff --git a/networksecurity/framework/src/android/net/ct/CertificateTransparencyManager.java b/networksecurity/framework/src/android/net/ct/CertificateTransparencyManager.java
new file mode 100644
index 0000000..94521ae
--- /dev/null
+++ b/networksecurity/framework/src/android/net/ct/CertificateTransparencyManager.java
@@ -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 android.net.ct;
+
+import android.annotation.FlaggedApi;
+import android.annotation.SystemService;
+
+import com.android.net.ct.flags.Flags;
+
+/**
+ * Provides the primary API for the Certificate Transparency Manager.
+ *
+ * @hide
+ */
+@FlaggedApi(Flags.FLAG_CERTIFICATE_TRANSPARENCY_SERVICE)
+@SystemService(CertificateTransparencyManager.SERVICE_NAME)
+public final class CertificateTransparencyManager {
+
+    public static final String SERVICE_NAME = "certificate_transparency";
+
+    /**
+     * Creates a new CertificateTransparencyManager instance.
+     *
+     * @hide
+     */
+    public CertificateTransparencyManager() {}
+}
diff --git a/tests/cts/hostside-network-policy/aidl/com/android/cts/netpolicy/hostside/NetworkCheckResult.aidl b/networksecurity/framework/src/android/net/ct/ICertificateTransparencyManager.aidl
similarity index 61%
rename from tests/cts/hostside-network-policy/aidl/com/android/cts/netpolicy/hostside/NetworkCheckResult.aidl
rename to networksecurity/framework/src/android/net/ct/ICertificateTransparencyManager.aidl
index 7aac2ab..b5bce7f 100644
--- a/tests/cts/hostside-network-policy/aidl/com/android/cts/netpolicy/hostside/NetworkCheckResult.aidl
+++ b/networksecurity/framework/src/android/net/ct/ICertificateTransparencyManager.aidl
@@ -1,11 +1,11 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
+/**
+ * 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
+ *     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,
@@ -13,14 +13,10 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+package android.net.ct;
 
-package com.android.cts.netpolicy.hostside;
-
-import android.net.NetworkInfo;
-
-@JavaDerive(toString=true)
-parcelable NetworkCheckResult {
-   boolean connected;
-   String details;
-   NetworkInfo networkInfo;
-}
\ No newline at end of file
+/**
+* Interface for communicating with CertificateTransparencyService.
+* @hide
+*/
+interface ICertificateTransparencyManager {}
diff --git a/networksecurity/service/Android.bp b/networksecurity/service/Android.bp
new file mode 100644
index 0000000..e33abd5
--- /dev/null
+++ b/networksecurity/service/Android.bp
@@ -0,0 +1,41 @@
+// 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_platform_security",
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+// Main lib for Certificate Transparency services.
+java_library {
+    name: "service-networksecurity-pre-jarjar",
+    defaults: ["framework-system-server-module-defaults"],
+    visibility: ["//packages/modules/Connectivity:__subpackages__"],
+
+    srcs: [
+        "src/**/*.java",
+    ],
+
+    libs: [
+        "framework-configinfrastructure",
+        "framework-connectivity-pre-jarjar",
+        "service-connectivity-pre-jarjar",
+    ],
+
+    // This is included in service-connectivity which is 30+
+    // TODO (b/293613362): allow APEXes to have service jars with higher min_sdk than the APEX
+    // (service-connectivity is only used on 31+) and use 31 here
+    min_sdk_version: "30",
+    sdk_version: "system_server_current",
+    apex_available: ["com.android.tethering"],
+}
diff --git a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyFlagsListener.java b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyFlagsListener.java
new file mode 100644
index 0000000..8dd5951
--- /dev/null
+++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyFlagsListener.java
@@ -0,0 +1,67 @@
+/*
+ * 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.net.ct;
+
+import static android.provider.DeviceConfig.NAMESPACE_TETHERING;
+
+import android.content.Context;
+import android.provider.DeviceConfig;
+import android.provider.DeviceConfig.Properties;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.modules.utils.build.SdkLevel;
+
+import java.util.concurrent.Executors;
+
+/** Listener class for the Certificate Transparency Phenotype flags. */
+class CertificateTransparencyFlagsListener implements DeviceConfig.OnPropertiesChangedListener {
+
+    private static final String TAG = "CertificateTransparency";
+
+    private static final String VERSION = "version";
+    private static final String CONTENT_URL = "content_url";
+    private static final String METADATA_URL = "metadata_url";
+
+    CertificateTransparencyFlagsListener(Context context) {}
+
+    void initialize() {
+        DeviceConfig.addOnPropertiesChangedListener(
+                NAMESPACE_TETHERING, Executors.newSingleThreadExecutor(), this);
+        // TODO: handle property changes triggering on boot before registering this listener.
+    }
+
+    @Override
+    public void onPropertiesChanged(Properties properties) {
+        if (!SdkLevel.isAtLeastV() || !NAMESPACE_TETHERING.equals(properties.getNamespace())) {
+            return;
+        }
+
+        String newVersion = DeviceConfig.getString(NAMESPACE_TETHERING, VERSION, "");
+        String newContentUrl = DeviceConfig.getString(NAMESPACE_TETHERING, CONTENT_URL, "");
+        String newMetadataUrl = DeviceConfig.getString(NAMESPACE_TETHERING, METADATA_URL, "");
+        if (TextUtils.isEmpty(newVersion)
+                || TextUtils.isEmpty(newContentUrl)
+                || TextUtils.isEmpty(newMetadataUrl)) {
+            return;
+        }
+
+        Log.d(TAG, "newVersion=" + newVersion);
+        Log.d(TAG, "newContentUrl=" + newContentUrl);
+        Log.d(TAG, "newMetadataUrl=" + newMetadataUrl);
+        // TODO: start download of URLs.
+    }
+}
diff --git a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyService.java b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyService.java
new file mode 100644
index 0000000..406a57f
--- /dev/null
+++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyService.java
@@ -0,0 +1,68 @@
+/*
+ * 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.net.ct;
+
+import android.annotation.RequiresApi;
+import android.content.Context;
+import android.net.ct.ICertificateTransparencyManager;
+import android.os.Build;
+import android.util.Log;
+
+import com.android.net.ct.flags.Flags;
+import com.android.net.module.util.DeviceConfigUtils;
+import com.android.server.SystemService;
+
+/** Implementation of the Certificate Transparency service. */
+@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+public class CertificateTransparencyService extends ICertificateTransparencyManager.Stub {
+
+    private static final String TAG = "CertificateTransparency";
+    private static final String CERTIFICATE_TRANSPARENCY_ENABLED =
+            "certificate_transparency_service_enabled";
+
+    private final CertificateTransparencyFlagsListener mFlagsListener;
+
+    /**
+     * @return true if the CertificateTransparency service is enabled.
+     */
+    public static boolean enabled(Context context) {
+        // TODO: replace isTetheringFeatureEnabled with CT namespace flag.
+        return DeviceConfigUtils.isTetheringFeatureEnabled(
+                        context, CERTIFICATE_TRANSPARENCY_ENABLED)
+                && Flags.certificateTransparencyService();
+    }
+
+    /** Creates a new {@link CertificateTransparencyService} object. */
+    public CertificateTransparencyService(Context context) {
+        mFlagsListener = new CertificateTransparencyFlagsListener(context);
+    }
+
+    /**
+     * Called by {@link com.android.server.ConnectivityServiceInitializer}.
+     *
+     * @see com.android.server.SystemService#onBootPhase
+     */
+    public void onBootPhase(int phase) {
+
+        switch (phase) {
+            case SystemService.PHASE_BOOT_COMPLETED:
+                Log.d(TAG, "setting up flags listeners");
+                mFlagsListener.initialize();
+                break;
+            default:
+        }
+    }
+}
diff --git a/service-t/Android.bp b/service-t/Android.bp
index 779f354..32dbcaa 100644
--- a/service-t/Android.bp
+++ b/service-t/Android.bp
@@ -59,6 +59,7 @@
         "framework-wifi",
         "service-connectivity-pre-jarjar",
         "service-nearby-pre-jarjar",
+        "service-networksecurity-pre-jarjar",
         "service-thread-pre-jarjar",
         service_remoteauth_pre_jarjar_lib,
         "ServiceConnectivityResources",
@@ -104,7 +105,6 @@
     },
     srcs: [
         "src/com/android/server/connectivity/mdns/**/*.java",
-        ":framework-connectivity-t-mdns-standalone-build-sources",
         ":service-mdns-droidstubs",
     ],
     exclude_srcs: [
diff --git a/service-t/src/com/android/server/ConnectivityServiceInitializer.java b/service-t/src/com/android/server/ConnectivityServiceInitializer.java
index 1ac2f6e..5d23fdc 100644
--- a/service-t/src/com/android/server/ConnectivityServiceInitializer.java
+++ b/service-t/src/com/android/server/ConnectivityServiceInitializer.java
@@ -28,6 +28,7 @@
 import com.android.server.ethernet.EthernetService;
 import com.android.server.ethernet.EthernetServiceImpl;
 import com.android.server.nearby.NearbyService;
+import com.android.server.net.ct.CertificateTransparencyService;
 import com.android.server.thread.ThreadNetworkService;
 
 /**
@@ -43,6 +44,7 @@
     private final NearbyService mNearbyService;
     private final EthernetServiceImpl mEthernetServiceImpl;
     private final ThreadNetworkService mThreadNetworkService;
+    private final CertificateTransparencyService mCertificateTransparencyService;
 
     public ConnectivityServiceInitializer(Context context) {
         super(context);
@@ -55,6 +57,7 @@
         mNsdService = createNsdService(context);
         mNearbyService = createNearbyService(context);
         mThreadNetworkService = createThreadNetworkService(context);
+        mCertificateTransparencyService = createCertificateTransparencyService(context);
     }
 
     @Override
@@ -111,6 +114,10 @@
         if (mThreadNetworkService != null) {
             mThreadNetworkService.onBootPhase(phase);
         }
+
+        if (SdkLevel.isAtLeastV() && mCertificateTransparencyService != null) {
+            mCertificateTransparencyService.onBootPhase(phase);
+        }
     }
 
     /**
@@ -186,4 +193,13 @@
         }
         return new ThreadNetworkService(context);
     }
+
+    /** Return CertificateTransparencyService instance if enable, otherwise null. */
+    @Nullable
+    private CertificateTransparencyService createCertificateTransparencyService(
+            final Context context) {
+        return SdkLevel.isAtLeastV() && CertificateTransparencyService.enabled(context)
+                ? new CertificateTransparencyService(context)
+                : null;
+    }
 }
diff --git a/service-t/src/com/android/server/NsdService.java b/service-t/src/com/android/server/NsdService.java
index 64624ae..5f672e7 100644
--- a/service-t/src/com/android/server/NsdService.java
+++ b/service-t/src/com/android/server/NsdService.java
@@ -93,6 +93,7 @@
 import com.android.modules.utils.build.SdkLevel;
 import com.android.net.module.util.CollectionUtils;
 import com.android.net.module.util.DeviceConfigUtils;
+import com.android.net.module.util.DnsUtils;
 import com.android.net.module.util.HandlerUtils;
 import com.android.net.module.util.InetAddressUtils;
 import com.android.net.module.util.PermissionUtils;
@@ -768,7 +769,7 @@
             private Set<String> dedupSubtypeLabels(Collection<String> subtypes) {
                 final Map<String, String> subtypeMap = new LinkedHashMap<>(subtypes.size());
                 for (String subtype : subtypes) {
-                    subtypeMap.put(MdnsUtils.toDnsLowerCase(subtype), subtype);
+                    subtypeMap.put(DnsUtils.toDnsUpperCase(subtype), subtype);
                 }
                 return new ArraySet<>(subtypeMap.values());
             }
@@ -1935,6 +1936,8 @@
                         mContext, MdnsFeatureFlags.NSD_AGGRESSIVE_QUERY_MODE))
                 .setIsQueryWithKnownAnswerEnabled(mDeps.isFeatureEnabled(
                         mContext, MdnsFeatureFlags.NSD_QUERY_WITH_KNOWN_ANSWER))
+                .setAvoidAdvertisingEmptyTxtRecords(mDeps.isTetheringFeatureNotChickenedOut(
+                        mContext, MdnsFeatureFlags.NSD_AVOID_ADVERTISING_EMPTY_TXT_RECORDS))
                 .setOverrideProvider(flag -> mDeps.isFeatureEnabled(
                         mContext, FORCE_ENABLE_FLAG_FOR_TEST_PREFIX + flag))
                 .build();
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 54943c7..f55db93 100644
--- a/service-t/src/com/android/server/connectivity/mdns/EnqueueMdnsQueryCallable.java
+++ b/service-t/src/com/android/server/connectivity/mdns/EnqueueMdnsQueryCallable.java
@@ -24,6 +24,7 @@
 import android.util.Pair;
 
 import com.android.net.module.util.CollectionUtils;
+import com.android.net.module.util.DnsUtils;
 import com.android.net.module.util.SharedLog;
 import com.android.server.connectivity.mdns.util.MdnsUtils;
 
@@ -193,7 +194,7 @@
                         // Ignore any PTR records that don't match the current query.
                         if (!CollectionUtils.any(questions,
                                 q -> q instanceof MdnsPointerRecord
-                                        && MdnsUtils.equalsDnsLabelIgnoreDnsCase(
+                                        && DnsUtils.equalsDnsLabelIgnoreDnsCase(
                                                 q.getName(), ptrRecord.getName()))) {
                             continue;
                         }
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java b/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java
index b870477..9c52eca 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java
@@ -41,6 +41,7 @@
 import com.android.connectivity.resources.R;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.net.module.util.CollectionUtils;
+import com.android.net.module.util.DnsUtils;
 import com.android.net.module.util.SharedLog;
 import com.android.server.connectivity.ConnectivityResources;
 import com.android.server.connectivity.mdns.util.MdnsUtils;
@@ -272,7 +273,7 @@
             return true;
         }
         // Check if it conflicts with the default hostname.
-        return MdnsUtils.equalsIgnoreDnsCase(newInfo.getHostname(), mDeviceHostName[0]);
+        return DnsUtils.equalsIgnoreDnsCase(newInfo.getHostname(), mDeviceHostName[0]);
     }
 
     private void updateRegistrationUntilNoConflict(
@@ -436,8 +437,8 @@
                     continue;
                 }
                 final NsdServiceInfo other = mPendingRegistrations.valueAt(i).getServiceInfo();
-                if (MdnsUtils.equalsIgnoreDnsCase(info.getServiceName(), other.getServiceName())
-                        && MdnsUtils.equalsIgnoreDnsCase(info.getServiceType(),
+                if (DnsUtils.equalsIgnoreDnsCase(info.getServiceName(), other.getServiceName())
+                        && DnsUtils.equalsIgnoreDnsCase(info.getServiceType(),
                         other.getServiceType())) {
                     return mPendingRegistrations.keyAt(i);
                 }
@@ -463,13 +464,13 @@
                 final NsdServiceInfo otherInfo = otherRegistration.getServiceInfo();
                 final int otherServiceId = mPendingRegistrations.keyAt(i);
                 if (clientUid != otherRegistration.mClientUid
-                        && MdnsUtils.equalsIgnoreDnsCase(
+                        && DnsUtils.equalsIgnoreDnsCase(
                                 info.getHostname(), otherInfo.getHostname())) {
                     return otherServiceId;
                 }
                 if (!info.getHostAddresses().isEmpty()
                         && !otherInfo.getHostAddresses().isEmpty()
-                        && MdnsUtils.equalsIgnoreDnsCase(
+                        && DnsUtils.equalsIgnoreDnsCase(
                                 info.getHostname(), otherInfo.getHostname())) {
                     return otherServiceId;
                 }
@@ -849,7 +850,7 @@
                 sharedLog.wtf("Invalid priority in config_nsdOffloadServicesPriority: " + entry);
                 continue;
             }
-            priorities.put(MdnsUtils.toDnsLowerCase(priorityAndType[1]), priority);
+            priorities.put(DnsUtils.toDnsUpperCase(priorityAndType[1]), priority);
         }
         return priorities;
     }
@@ -995,7 +996,7 @@
             @NonNull Registration registration, byte[] rawOffloadPacket) {
         final NsdServiceInfo nsdServiceInfo = registration.getServiceInfo();
         final Integer mapPriority = mServiceTypeToOffloadPriority.get(
-                MdnsUtils.toDnsLowerCase(nsdServiceInfo.getServiceType()));
+                DnsUtils.toDnsUpperCase(nsdServiceInfo.getServiceType()));
         // Higher values of priority are less prioritized
         final int priority = mapPriority == null ? Integer.MAX_VALUE : mapPriority;
         final OffloadServiceInfo offloadServiceInfo = new OffloadServiceInfo(
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsDiscoveryManager.java b/service-t/src/com/android/server/connectivity/mdns/MdnsDiscoveryManager.java
index 0ab7a76..7fa605a 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsDiscoveryManager.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsDiscoveryManager.java
@@ -30,6 +30,7 @@
 import androidx.annotation.GuardedBy;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.net.module.util.DnsUtils;
 import com.android.net.module.util.SharedLog;
 import com.android.server.connectivity.mdns.util.MdnsUtils;
 
@@ -66,8 +67,8 @@
 
         public void put(@NonNull String serviceType, @NonNull SocketKey socketKey,
                 @NonNull MdnsServiceTypeClient client) {
-            final String dnsLowerServiceType = MdnsUtils.toDnsLowerCase(serviceType);
-            final Pair<String, SocketKey> perSocketServiceType = new Pair<>(dnsLowerServiceType,
+            final String dnsUpperServiceType = DnsUtils.toDnsUpperCase(serviceType);
+            final Pair<String, SocketKey> perSocketServiceType = new Pair<>(dnsUpperServiceType,
                     socketKey);
             clients.put(perSocketServiceType, client);
         }
@@ -75,18 +76,18 @@
         @Nullable
         public MdnsServiceTypeClient get(
                 @NonNull String serviceType, @NonNull SocketKey socketKey) {
-            final String dnsLowerServiceType = MdnsUtils.toDnsLowerCase(serviceType);
-            final Pair<String, SocketKey> perSocketServiceType = new Pair<>(dnsLowerServiceType,
+            final String dnsUpperServiceType = DnsUtils.toDnsUpperCase(serviceType);
+            final Pair<String, SocketKey> perSocketServiceType = new Pair<>(dnsUpperServiceType,
                     socketKey);
             return clients.getOrDefault(perSocketServiceType, null);
         }
 
         public List<MdnsServiceTypeClient> getByServiceType(@NonNull String serviceType) {
-            final String dnsLowerServiceType = MdnsUtils.toDnsLowerCase(serviceType);
+            final String dnsUpperServiceType = DnsUtils.toDnsUpperCase(serviceType);
             final List<MdnsServiceTypeClient> list = new ArrayList<>();
             for (int i = 0; i < clients.size(); i++) {
                 final Pair<String, SocketKey> perSocketServiceType = clients.keyAt(i);
-                if (dnsLowerServiceType.equals(perSocketServiceType.first)) {
+                if (dnsUpperServiceType.equals(perSocketServiceType.first)) {
                     list.add(clients.valueAt(i));
                 }
             }
@@ -389,12 +390,18 @@
      */
     public void dump(PrintWriter pw) {
         discoveryExecutor.checkAndRunOnHandlerThread(() -> {
-            pw.println();
+            pw.println("Clients:");
             // Dump ServiceTypeClients
             for (MdnsServiceTypeClient serviceTypeClient
                     : perSocketServiceTypeClients.getAllMdnsServiceTypeClient()) {
                 serviceTypeClient.dump(pw);
             }
+            pw.println();
+            // Dump ServiceCache
+            pw.println("Cached services:");
+            if (serviceCache != null) {
+                serviceCache.dump(pw, "  ");
+            }
         });
     }
 }
\ No newline at end of file
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 c264f25..709dc79 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsFeatureFlags.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsFeatureFlags.java
@@ -67,6 +67,12 @@
      */
     public static final String NSD_QUERY_WITH_KNOWN_ANSWER = "nsd_query_with_known_answer";
 
+    /**
+     * A feature flag to avoid advertising empty TXT records, as per RFC 6763 6.1.
+     */
+    public static final String NSD_AVOID_ADVERTISING_EMPTY_TXT_RECORDS =
+            "nsd_avoid_advertising_empty_txt_records";
+
     // Flag for offload feature
     public final boolean mIsMdnsOffloadFeatureEnabled;
 
@@ -91,6 +97,9 @@
     // Flag for query with known-answer
     public final boolean mIsQueryWithKnownAnswerEnabled;
 
+    // Flag for avoiding advertising empty TXT records
+    public final boolean mAvoidAdvertisingEmptyTxtRecords;
+
     @Nullable
     private final FlagOverrideProvider mOverrideProvider;
 
@@ -142,6 +151,15 @@
     }
 
     /**
+     * Indicates whether {@link #NSD_AVOID_ADVERTISING_EMPTY_TXT_RECORDS} is enabled, including for
+     * testing.
+     */
+    public boolean avoidAdvertisingEmptyTxtRecords() {
+        return mAvoidAdvertisingEmptyTxtRecords
+                || isForceEnabledForTest(NSD_AVOID_ADVERTISING_EMPTY_TXT_RECORDS);
+    }
+
+    /**
      * The constructor for {@link MdnsFeatureFlags}.
      */
     public MdnsFeatureFlags(boolean isOffloadFeatureEnabled,
@@ -152,6 +170,7 @@
             boolean isUnicastReplyEnabled,
             boolean isAggressiveQueryModeEnabled,
             boolean isQueryWithKnownAnswerEnabled,
+            boolean avoidAdvertisingEmptyTxtRecords,
             @Nullable FlagOverrideProvider overrideProvider) {
         mIsMdnsOffloadFeatureEnabled = isOffloadFeatureEnabled;
         mIncludeInetAddressRecordsInProbing = includeInetAddressRecordsInProbing;
@@ -161,6 +180,7 @@
         mIsUnicastReplyEnabled = isUnicastReplyEnabled;
         mIsAggressiveQueryModeEnabled = isAggressiveQueryModeEnabled;
         mIsQueryWithKnownAnswerEnabled = isQueryWithKnownAnswerEnabled;
+        mAvoidAdvertisingEmptyTxtRecords = avoidAdvertisingEmptyTxtRecords;
         mOverrideProvider = overrideProvider;
     }
 
@@ -181,6 +201,7 @@
         private boolean mIsUnicastReplyEnabled;
         private boolean mIsAggressiveQueryModeEnabled;
         private boolean mIsQueryWithKnownAnswerEnabled;
+        private boolean mAvoidAdvertisingEmptyTxtRecords;
         private FlagOverrideProvider mOverrideProvider;
 
         /**
@@ -195,6 +216,7 @@
             mIsUnicastReplyEnabled = true; // Default enabled.
             mIsAggressiveQueryModeEnabled = false;
             mIsQueryWithKnownAnswerEnabled = false;
+            mAvoidAdvertisingEmptyTxtRecords = true; // Default enabled.
             mOverrideProvider = null;
         }
 
@@ -291,6 +313,16 @@
         }
 
         /**
+         * Set whether to avoid advertising empty TXT records.
+         *
+         * @see #NSD_AVOID_ADVERTISING_EMPTY_TXT_RECORDS
+         */
+        public Builder setAvoidAdvertisingEmptyTxtRecords(boolean avoidAdvertisingEmptyTxtRecords) {
+            mAvoidAdvertisingEmptyTxtRecords = avoidAdvertisingEmptyTxtRecords;
+            return this;
+        }
+
+        /**
          * Builds a {@link MdnsFeatureFlags} with the arguments supplied to this builder.
          */
         public MdnsFeatureFlags build() {
@@ -302,6 +334,7 @@
                     mIsUnicastReplyEnabled,
                     mIsAggressiveQueryModeEnabled,
                     mIsQueryWithKnownAnswerEnabled,
+                    mAvoidAdvertisingEmptyTxtRecords,
                     mOverrideProvider);
         }
     }
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClient.java b/service-t/src/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClient.java
index fcfb15f..c575d40 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClient.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClient.java
@@ -29,7 +29,9 @@
 import android.util.ArrayMap;
 import android.util.Log;
 
+import com.android.net.module.util.CollectionUtils;
 import com.android.net.module.util.SharedLog;
+import com.android.server.connectivity.mdns.util.MdnsUtils;
 
 import java.io.IOException;
 import java.net.DatagramPacket;
@@ -225,6 +227,12 @@
             Log.wtf(TAG, "No mDns packets to send");
             return;
         }
+        // Check all packets with the same address
+        if (!MdnsUtils.checkAllPacketsWithSameAddress(packets)) {
+            Log.wtf(TAG, "Some mDNS packets have a different target address. addresses="
+                    + CollectionUtils.map(packets, DatagramPacket::getSocketAddress));
+            return;
+        }
 
         final boolean isIpv6 = ((InetSocketAddress) packets.get(0).getSocketAddress())
                 .getAddress() instanceof Inet6Address;
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsNsecRecord.java b/service-t/src/com/android/server/connectivity/mdns/MdnsNsecRecord.java
index 1239180..a5b8803 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsNsecRecord.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsNsecRecord.java
@@ -20,7 +20,7 @@
 import android.text.TextUtils;
 
 import com.android.net.module.util.CollectionUtils;
-import com.android.server.connectivity.mdns.util.MdnsUtils;
+import com.android.net.module.util.DnsUtils;
 
 import java.io.IOException;
 import java.util.ArrayList;
@@ -153,7 +153,7 @@
     @Override
     public int hashCode() {
         return Objects.hash(super.hashCode(),
-                Arrays.hashCode(MdnsUtils.toDnsLabelsLowerCase(mNextDomain)),
+                Arrays.hashCode(DnsUtils.toDnsLabelsUpperCase(mNextDomain)),
                 Arrays.hashCode(mTypes));
     }
 
@@ -167,7 +167,7 @@
         }
 
         return super.equals(other)
-                && MdnsUtils.equalsDnsLabelIgnoreDnsCase(mNextDomain,
+                && DnsUtils.equalsDnsLabelIgnoreDnsCase(mNextDomain,
                 ((MdnsNsecRecord) other).mNextDomain)
                 && Arrays.equals(mTypes, ((MdnsNsecRecord) other).mTypes);
     }
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsPacketWriter.java b/service-t/src/com/android/server/connectivity/mdns/MdnsPacketWriter.java
index 6879a64..cf788be 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsPacketWriter.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsPacketWriter.java
@@ -16,8 +16,8 @@
 
 package com.android.server.connectivity.mdns;
 
+import com.android.net.module.util.DnsUtils;
 import com.android.server.connectivity.mdns.MdnsServiceInfo.TextEntry;
-import com.android.server.connectivity.mdns.util.MdnsUtils;
 
 import java.io.IOException;
 import java.net.DatagramPacket;
@@ -180,7 +180,7 @@
             int existingOffset = entry.getKey();
             String[] existingLabels = entry.getValue();
 
-            if (MdnsUtils.equalsDnsLabelIgnoreDnsCase(existingLabels, labels)) {
+            if (DnsUtils.equalsDnsLabelIgnoreDnsCase(existingLabels, labels)) {
                 writePointer(existingOffset);
                 return;
             } else if (MdnsRecord.labelsAreSuffix(existingLabels, labels)) {
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsPointerRecord.java b/service-t/src/com/android/server/connectivity/mdns/MdnsPointerRecord.java
index e5c90a4..39bf653 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsPointerRecord.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsPointerRecord.java
@@ -20,7 +20,7 @@
 
 import androidx.annotation.VisibleForTesting;
 
-import com.android.server.connectivity.mdns.util.MdnsUtils;
+import com.android.net.module.util.DnsUtils;
 
 import java.io.IOException;
 import java.util.Arrays;
@@ -68,7 +68,7 @@
     }
 
     public boolean hasSubtype() {
-        return (name != null) && (name.length > 2) && MdnsUtils.equalsIgnoreDnsCase(name[1],
+        return (name != null) && (name.length > 2) && DnsUtils.equalsIgnoreDnsCase(name[1],
                 MdnsConstants.SUBTYPE_LABEL);
     }
 
@@ -83,7 +83,7 @@
 
     @Override
     public int hashCode() {
-        return (super.hashCode() * 31) + Arrays.hashCode(MdnsUtils.toDnsLabelsLowerCase(pointer));
+        return (super.hashCode() * 31) + Arrays.hashCode(DnsUtils.toDnsLabelsUpperCase(pointer));
     }
 
     @Override
@@ -95,7 +95,7 @@
             return false;
         }
 
-        return super.equals(other) && MdnsUtils.equalsDnsLabelIgnoreDnsCase(pointer,
+        return super.equals(other) && DnsUtils.equalsDnsLabelIgnoreDnsCase(pointer,
                 ((MdnsPointerRecord) other).pointer);
     }
 }
\ No newline at end of file
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsProber.java b/service-t/src/com/android/server/connectivity/mdns/MdnsProber.java
index e88947a..4a44fff 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsProber.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsProber.java
@@ -23,8 +23,8 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.net.module.util.CollectionUtils;
+import com.android.net.module.util.DnsUtils;
 import com.android.net.module.util.SharedLog;
-import com.android.server.connectivity.mdns.util.MdnsUtils;
 
 import java.util.ArrayList;
 import java.util.Collections;
@@ -114,7 +114,7 @@
         private static boolean containsName(@NonNull List<MdnsRecord> records,
                 @NonNull String[] name) {
             return CollectionUtils.any(records,
-                    r -> MdnsUtils.equalsDnsLabelIgnoreDnsCase(name, r.getName()));
+                    r -> DnsUtils.equalsDnsLabelIgnoreDnsCase(name, r.getName()));
         }
     }
 
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsQueryScheduler.java b/service-t/src/com/android/server/connectivity/mdns/MdnsQueryScheduler.java
index 3fcf0d4..cfeca5d 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsQueryScheduler.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsQueryScheduler.java
@@ -63,17 +63,22 @@
      * rescheduling is not necessary.
      */
     @Nullable
-    public ScheduledQueryTaskArgs maybeRescheduleCurrentRun(long now,
-            long minRemainingTtl, long lastSentTime, long sessionId) {
+    public ScheduledQueryTaskArgs maybeRescheduleCurrentRun(
+            long now,
+            long minRemainingTtl,
+            long lastSentTime,
+            long sessionId,
+            int numOfQueriesBeforeBackoff) {
         if (mLastScheduledQueryTaskArgs == null) {
             return null;
         }
-        if (!mLastScheduledQueryTaskArgs.config.shouldUseQueryBackoff()) {
+        if (!mLastScheduledQueryTaskArgs.config.shouldUseQueryBackoff(numOfQueriesBeforeBackoff)) {
             return null;
         }
 
         final long timeToRun = calculateTimeToRun(mLastScheduledQueryTaskArgs,
-                mLastScheduledQueryTaskArgs.config, now, minRemainingTtl, lastSentTime);
+                mLastScheduledQueryTaskArgs.config, now, minRemainingTtl, lastSentTime,
+                numOfQueriesBeforeBackoff, false /* forceEnableBackoff */);
 
         if (timeToRun <= mLastScheduledQueryTaskArgs.timeToRun) {
             return null;
@@ -95,14 +100,18 @@
             long minRemainingTtl,
             long now,
             long lastSentTime,
-            long sessionId) {
-        final QueryTaskConfig nextRunConfig = currentConfig.getConfigForNextRun();
-        final long timeToRun;
-        if (mLastScheduledQueryTaskArgs == null) {
+            long sessionId,
+            int queryMode,
+            int numOfQueriesBeforeBackoff,
+            boolean forceEnableBackoff) {
+        final QueryTaskConfig nextRunConfig = currentConfig.getConfigForNextRun(queryMode);
+        long timeToRun;
+        if (mLastScheduledQueryTaskArgs == null && !forceEnableBackoff) {
             timeToRun = now + nextRunConfig.delayUntilNextTaskWithoutBackoffMs;
         } else {
             timeToRun = calculateTimeToRun(mLastScheduledQueryTaskArgs,
-                    nextRunConfig, now, minRemainingTtl, lastSentTime);
+                    nextRunConfig, now, minRemainingTtl, lastSentTime, numOfQueriesBeforeBackoff,
+                    forceEnableBackoff);
         }
         mLastScheduledQueryTaskArgs = new ScheduledQueryTaskArgs(nextRunConfig, timeToRun,
                 minRemainingTtl + now,
@@ -121,10 +130,12 @@
         return mLastScheduledQueryTaskArgs;
     }
 
-    private static long calculateTimeToRun(@NonNull ScheduledQueryTaskArgs taskArgs,
-            QueryTaskConfig queryTaskConfig, long now, long minRemainingTtl, long lastSentTime) {
+    private static long calculateTimeToRun(@Nullable ScheduledQueryTaskArgs taskArgs,
+            QueryTaskConfig queryTaskConfig, long now, long minRemainingTtl, long lastSentTime,
+            int numOfQueriesBeforeBackoff, boolean forceEnableBackoff) {
         final long baseDelayInMs = queryTaskConfig.delayUntilNextTaskWithoutBackoffMs;
-        if (!queryTaskConfig.shouldUseQueryBackoff()) {
+        if (!(forceEnableBackoff
+                || queryTaskConfig.shouldUseQueryBackoff(numOfQueriesBeforeBackoff))) {
             return lastSentTime + baseDelayInMs;
         }
         if (minRemainingTtl <= 0) {
@@ -133,7 +144,7 @@
             return lastSentTime + baseDelayInMs;
         }
         // If the next TTL expiration time hasn't changed, then use previous calculated timeToRun.
-        if (lastSentTime < now
+        if (lastSentTime < now && taskArgs != null
                 && taskArgs.minTtlExpirationTimeWhenScheduled == now + minRemainingTtl) {
             // Use the original scheduling time if the TTL has not changed, to avoid continuously
             // rescheduling to 80% of the remaining TTL as time passes
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsRecord.java b/service-t/src/com/android/server/connectivity/mdns/MdnsRecord.java
index b865319..d464ca7 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsRecord.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsRecord.java
@@ -25,7 +25,7 @@
 
 import androidx.annotation.VisibleForTesting;
 
-import com.android.server.connectivity.mdns.util.MdnsUtils;
+import com.android.net.module.util.DnsUtils;
 
 import java.io.IOException;
 import java.util.Arrays;
@@ -139,7 +139,7 @@
         }
 
         for (int i = 0; i < list1.length; ++i) {
-            if (!MdnsUtils.equalsIgnoreDnsCase(list1[i], list2[i + offset])) {
+            if (!DnsUtils.equalsIgnoreDnsCase(list1[i], list2[i + offset])) {
                 return false;
             }
         }
@@ -284,13 +284,13 @@
 
         MdnsRecord otherRecord = (MdnsRecord) other;
 
-        return MdnsUtils.equalsDnsLabelIgnoreDnsCase(name, otherRecord.name) && (type
+        return DnsUtils.equalsDnsLabelIgnoreDnsCase(name, otherRecord.name) && (type
                 == otherRecord.type);
     }
 
     @Override
     public int hashCode() {
-        return Objects.hash(Arrays.hashCode(MdnsUtils.toDnsLabelsLowerCase(name)), type);
+        return Objects.hash(Arrays.hashCode(DnsUtils.toDnsLabelsUpperCase(name)), type);
     }
 
     /**
@@ -311,7 +311,7 @@
 
         public Key(int recordType, String[] recordName) {
             this.recordType = recordType;
-            this.recordName = MdnsUtils.toDnsLabelsLowerCase(recordName);
+            this.recordName = DnsUtils.toDnsLabelsUpperCase(recordName);
         }
 
         @Override
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 36f3982..c3cb776 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java
@@ -38,6 +38,7 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.net.module.util.CollectionUtils;
+import com.android.net.module.util.DnsUtils;
 import com.android.net.module.util.HexDump;
 import com.android.server.connectivity.mdns.util.MdnsUtils;
 
@@ -226,11 +227,13 @@
         /**
          * Create a ServiceRegistration with only update the subType.
          */
-        ServiceRegistration withSubtypes(@NonNull Set<String> newSubtypes) {
+        ServiceRegistration withSubtypes(@NonNull Set<String> newSubtypes,
+                @NonNull MdnsFeatureFlags featureFlags) {
             NsdServiceInfo newServiceInfo = new NsdServiceInfo(serviceInfo);
             newServiceInfo.setSubtypes(newSubtypes);
             return new ServiceRegistration(srvRecord.record.getServiceHost(), newServiceInfo,
-                    repliedServiceCount, sentPacketCount, exiting, isProbing, ttl);
+                    repliedServiceCount, sentPacketCount, exiting, isProbing, ttl,
+                    featureFlags);
         }
 
         /**
@@ -238,7 +241,7 @@
          */
         ServiceRegistration(@NonNull String[] deviceHostname, @NonNull NsdServiceInfo serviceInfo,
                 int repliedServiceCount, int sentPacketCount, boolean exiting, boolean isProbing,
-                @Nullable Duration ttl) {
+                @Nullable Duration ttl, @NonNull MdnsFeatureFlags featureFlags) {
             this.serviceInfo = serviceInfo;
 
             final long nonNameRecordsTtlMillis;
@@ -309,7 +312,8 @@
                                 // Service name is verified unique after probing
                                 true /* cacheFlush */,
                                 nonNameRecordsTtlMillis,
-                                attrsToTextEntries(serviceInfo.getAttributes())),
+                                attrsToTextEntries(
+                                        serviceInfo.getAttributes(), featureFlags)),
                         false /* sharedName */);
 
                 allRecords.addAll(ptrRecords);
@@ -392,9 +396,10 @@
          * @param serviceInfo Service to advertise
          */
         ServiceRegistration(@NonNull String[] deviceHostname, @NonNull NsdServiceInfo serviceInfo,
-                int repliedServiceCount, int sentPacketCount, @Nullable Duration ttl) {
+                int repliedServiceCount, int sentPacketCount, @Nullable Duration ttl,
+                @NonNull MdnsFeatureFlags featureFlags) {
             this(deviceHostname, serviceInfo,repliedServiceCount, sentPacketCount,
-                    false /* exiting */, true /* isProbing */, ttl);
+                    false /* exiting */, true /* isProbing */, ttl, featureFlags);
         }
 
         void setProbing(boolean probing) {
@@ -445,7 +450,7 @@
                     "Service ID must already exist for an update request: " + serviceId);
         }
         final ServiceRegistration updatedRegistration = existingRegistration.withSubtypes(
-                subtypes);
+                subtypes, mMdnsFeatureFlags);
         mServices.put(serviceId, updatedRegistration);
     }
 
@@ -476,7 +481,8 @@
 
         final ServiceRegistration registration = new ServiceRegistration(
                 mDeviceHostname, serviceInfo, NO_PACKET /* repliedServiceCount */,
-                NO_PACKET /* sentPacketCount */, ttl);
+                NO_PACKET /* sentPacketCount */, ttl,
+                mMdnsFeatureFlags);
         mServices.put(serviceId, registration);
 
         // Remove existing exiting service
@@ -494,8 +500,8 @@
         }
         for (int i = 0; i < mServices.size(); i++) {
             final NsdServiceInfo info = mServices.valueAt(i).serviceInfo;
-            if (MdnsUtils.equalsIgnoreDnsCase(serviceName, info.getServiceName())
-                    && MdnsUtils.equalsIgnoreDnsCase(serviceType, info.getServiceType())) {
+            if (DnsUtils.equalsIgnoreDnsCase(serviceName, info.getServiceName())
+                    && DnsUtils.equalsIgnoreDnsCase(serviceType, info.getServiceType())) {
                 return mServices.keyAt(i);
             }
         }
@@ -547,8 +553,17 @@
         return new MdnsProber.ProbingInfo(serviceId, probingRecords);
     }
 
-    private static List<MdnsServiceInfo.TextEntry> attrsToTextEntries(Map<String, byte[]> attrs) {
-        final List<MdnsServiceInfo.TextEntry> out = new ArrayList<>(attrs.size());
+    private static List<MdnsServiceInfo.TextEntry> attrsToTextEntries(
+            @NonNull Map<String, byte[]> attrs, @NonNull MdnsFeatureFlags featureFlags) {
+        final List<MdnsServiceInfo.TextEntry> out = new ArrayList<>(
+                attrs.size() == 0 ? 1 : attrs.size());
+        if (featureFlags.avoidAdvertisingEmptyTxtRecords() && attrs.size() == 0) {
+            // As per RFC6763 6.1, empty TXT records are not allowed, but records containing a
+            // single empty String must be treated as equivalent.
+            out.add(new MdnsServiceInfo.TextEntry("", MdnsServiceInfo.TextEntry.VALUE_NONE));
+            return out;
+        }
+
         for (Map.Entry<String, byte[]> attr : attrs.entrySet()) {
             out.add(new MdnsServiceInfo.TextEntry(attr.getKey(), attr.getValue()));
         }
@@ -821,7 +836,7 @@
              must match the question qtype unless the qtype is "ANY" (255) or the rrtype is
              "CNAME" (5), and the record rrclass must match the question qclass unless the
              qclass is "ANY" (255) */
-            if (!MdnsUtils.equalsDnsLabelIgnoreDnsCase(info.record.getName(), question.getName())) {
+            if (!DnsUtils.equalsDnsLabelIgnoreDnsCase(info.record.getName(), question.getName())) {
                 continue;
             }
             hasFullyOwnedNameMatch |= !info.isSharedName;
@@ -1232,7 +1247,7 @@
             return RecordConflictType.NO_CONFLICT;
         }
 
-        if (!MdnsUtils.equalsDnsLabelIgnoreDnsCase(record.getName(), fullServiceName)) {
+        if (!DnsUtils.equalsDnsLabelIgnoreDnsCase(record.getName(), fullServiceName)) {
             return RecordConflictType.NO_CONFLICT;
         }
 
@@ -1270,7 +1285,7 @@
         }
 
         // Different names. There won't be a conflict.
-        if (!MdnsUtils.equalsIgnoreDnsCase(
+        if (!DnsUtils.equalsIgnoreDnsCase(
                 record.getName()[0], registration.serviceInfo.getHostname())) {
             return RecordConflictType.NO_CONFLICT;
         }
@@ -1351,7 +1366,7 @@
             int id = mServices.keyAt(i);
             ServiceRegistration service = mServices.valueAt(i);
             if (service.exiting) continue;
-            if (MdnsUtils.equalsIgnoreDnsCase(service.serviceInfo.getHostname(), hostname)) {
+            if (DnsUtils.equalsIgnoreDnsCase(service.serviceInfo.getHostname(), hostname)) {
                 consumer.accept(id, service);
             }
         }
@@ -1402,7 +1417,8 @@
         if (existing == null) return null;
 
         final ServiceRegistration newService = new ServiceRegistration(mDeviceHostname, newInfo,
-                existing.repliedServiceCount, existing.sentPacketCount, existing.ttl);
+                existing.repliedServiceCount, existing.sentPacketCount, existing.ttl,
+                mMdnsFeatureFlags);
         mServices.put(serviceId, newService);
         return makeProbingInfo(serviceId, newService);
     }
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsResponse.java b/service-t/src/com/android/server/connectivity/mdns/MdnsResponse.java
index 05ad1be..2957da5 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsResponse.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsResponse.java
@@ -19,11 +19,14 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.net.Network;
+import android.os.SystemClock;
+import android.text.TextUtils;
 
 import com.android.internal.annotations.VisibleForTesting;
-import com.android.server.connectivity.mdns.util.MdnsUtils;
+import com.android.net.module.util.DnsUtils;
 
 import java.io.IOException;
+import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Iterator;
@@ -111,7 +114,7 @@
      * pointer record is already present in the response with the same TTL.
      */
     public synchronized boolean addPointerRecord(MdnsPointerRecord pointerRecord) {
-        if (!MdnsUtils.equalsDnsLabelIgnoreDnsCase(serviceName, pointerRecord.getPointer())) {
+        if (!DnsUtils.equalsDnsLabelIgnoreDnsCase(serviceName, pointerRecord.getPointer())) {
             throw new IllegalArgumentException(
                     "Pointer records for different service names cannot be added");
         }
@@ -305,13 +308,13 @@
         boolean dropAddressRecords = false;
 
         for (MdnsInetAddressRecord inetAddressRecord : getInet4AddressRecords()) {
-            if (!MdnsUtils.equalsDnsLabelIgnoreDnsCase(
+            if (!DnsUtils.equalsDnsLabelIgnoreDnsCase(
                     this.serviceRecord.getServiceHost(), inetAddressRecord.getName())) {
                 dropAddressRecords = true;
             }
         }
         for (MdnsInetAddressRecord inetAddressRecord : getInet6AddressRecords()) {
-            if (!MdnsUtils.equalsDnsLabelIgnoreDnsCase(
+            if (!DnsUtils.equalsDnsLabelIgnoreDnsCase(
                     this.serviceRecord.getServiceHost(), inetAddressRecord.getName())) {
                 dropAddressRecords = true;
             }
@@ -426,4 +429,18 @@
 
         return count;
     }
+
+    @Override
+    public String toString() {
+        return "Name: " + TextUtils.join(".", serviceName)
+                + ", pointerRecords: " + pointerRecords
+                + ", serviceRecord: " + serviceRecord
+                + ", textRecord: " + textRecord
+                + ", inet4AddressRecords: " + inet4AddressRecords
+                + ", inet6AddressRecords: " + inet6AddressRecords
+                + ", interfaceIndex: " + interfaceIndex
+                + ", network: " + network
+                + ", lastUpdateTime: " + Instant.now().minusMillis(
+                        SystemClock.elapsedRealtime() - lastUpdateTime);
+    }
 }
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsResponseDecoder.java b/service-t/src/com/android/server/connectivity/mdns/MdnsResponseDecoder.java
index b812bb4..52e76ad 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsResponseDecoder.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsResponseDecoder.java
@@ -22,6 +22,7 @@
 import android.util.ArrayMap;
 import android.util.Pair;
 
+import com.android.net.module.util.DnsUtils;
 import com.android.server.connectivity.mdns.util.MdnsUtils;
 
 import java.io.EOFException;
@@ -49,7 +50,7 @@
             List<MdnsResponse> responses, String[] pointer) {
         if (responses != null) {
             for (MdnsResponse response : responses) {
-                if (MdnsUtils.equalsDnsLabelIgnoreDnsCase(response.getServiceName(), pointer)) {
+                if (DnsUtils.equalsDnsLabelIgnoreDnsCase(response.getServiceName(), pointer)) {
                     return response;
                 }
             }
@@ -65,7 +66,7 @@
                 if (serviceRecord == null) {
                     continue;
                 }
-                if (MdnsUtils.equalsDnsLabelIgnoreDnsCase(serviceRecord.getServiceHost(),
+                if (DnsUtils.equalsDnsLabelIgnoreDnsCase(serviceRecord.getServiceHost(),
                         hostName)) {
                     return response;
                 }
@@ -318,7 +319,7 @@
             if (serviceRecord == null) {
                 continue;
             }
-            if (MdnsUtils.equalsDnsLabelIgnoreDnsCase(serviceRecord.getServiceHost(), hostName)) {
+            if (DnsUtils.equalsDnsLabelIgnoreDnsCase(serviceRecord.getServiceHost(), hostName)) {
                 if (result == null) {
                     result = new ArrayList<>(/* initialCapacity= */ responses.size());
                 }
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceCache.java b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceCache.java
index e9a41d1..591ed8b 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceCache.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceCache.java
@@ -16,10 +16,10 @@
 
 package com.android.server.connectivity.mdns;
 
+import static com.android.net.module.util.DnsUtils.equalsIgnoreDnsCase;
+import static com.android.net.module.util.DnsUtils.toDnsUpperCase;
 import static com.android.server.connectivity.mdns.MdnsResponse.EXPIRATION_NEVER;
 import static com.android.server.connectivity.mdns.util.MdnsUtils.ensureRunningOnHandlerThread;
-import static com.android.server.connectivity.mdns.util.MdnsUtils.equalsIgnoreDnsCase;
-import static com.android.server.connectivity.mdns.util.MdnsUtils.toDnsLowerCase;
 
 import static java.lang.Math.min;
 
@@ -32,6 +32,7 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.connectivity.mdns.util.MdnsUtils;
 
+import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Iterator;
@@ -49,16 +50,16 @@
  */
 public class MdnsServiceCache {
     static class CacheKey {
-        @NonNull final String mLowercaseServiceType;
+        @NonNull final String mUpperCaseServiceType;
         @NonNull final SocketKey mSocketKey;
 
         CacheKey(@NonNull String serviceType, @NonNull SocketKey socketKey) {
-            mLowercaseServiceType = toDnsLowerCase(serviceType);
+            mUpperCaseServiceType = toDnsUpperCase(serviceType);
             mSocketKey = socketKey;
         }
 
         @Override public int hashCode() {
-            return Objects.hash(mLowercaseServiceType, mSocketKey);
+            return Objects.hash(mUpperCaseServiceType, mSocketKey);
         }
 
         @Override public boolean equals(Object other) {
@@ -68,9 +69,14 @@
             if (!(other instanceof CacheKey)) {
                 return false;
             }
-            return Objects.equals(mLowercaseServiceType, ((CacheKey) other).mLowercaseServiceType)
+            return Objects.equals(mUpperCaseServiceType, ((CacheKey) other).mUpperCaseServiceType)
                     && Objects.equals(mSocketKey, ((CacheKey) other).mSocketKey);
         }
+
+        @Override
+        public String toString() {
+            return "CacheKey{ ServiceType=" + mUpperCaseServiceType + ", " + mSocketKey + " }";
+        }
     }
     /**
      * A map of cached services. Key is composed of service type and socket. Value is the list of
@@ -233,6 +239,21 @@
     }
 
     /**
+     * Remove services which matches the given type and socket.
+     *
+     * @param cacheKey the target CacheKey.
+     */
+    public void removeServices(@NonNull CacheKey cacheKey) {
+        ensureRunningOnHandlerThread(mHandler);
+        // Remove all services
+        if (mCachedServices.remove(cacheKey) == null) {
+            return;
+        }
+        // Update the next expiration check time if services are removed.
+        mNextExpirationTime = getNextExpirationTime(mClock.elapsedRealtime());
+    }
+
+    /**
      * Register a callback to listen to service expiration.
      *
      * <p> Registering the same callback instance twice is a no-op, since MdnsServiceTypeClient
@@ -338,6 +359,22 @@
         mNextExpirationTime = getNextExpirationTime(now);
     }
 
+    /**
+     * Dump ServiceCache state.
+     */
+    public void dump(PrintWriter pw, String indent) {
+        ensureRunningOnHandlerThread(mHandler);
+        // IndentingPrintWriter cannot be used on the mDNS stack build. So, manually add an indent.
+        for (int i = 0; i < mCachedServices.size(); i++) {
+            final CacheKey key = mCachedServices.keyAt(i);
+            pw.println(indent + key);
+            for (MdnsResponse response : mCachedServices.valueAt(i)) {
+                pw.println(indent + "  Response{ " + response + " }");
+            }
+            pw.println();
+        }
+    }
+
     /*** Callbacks for listening service expiration */
     public interface ServiceExpiredCallback {
         /*** Notify the service is expired */
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceInfo.java b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceInfo.java
index 1ec9e39..a16fcf7 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceInfo.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceInfo.java
@@ -16,8 +16,6 @@
 
 package com.android.server.connectivity.mdns;
 
-import static com.android.server.connectivity.mdns.MdnsSocket.INTERFACE_INDEX_UNSPECIFIED;
-
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.net.Network;
@@ -33,7 +31,6 @@
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
-import java.util.Locale;
 import java.util.Map;
 import java.util.TreeMap;
 
@@ -355,6 +352,13 @@
 
     /** Represents a DNS TXT key-value pair defined by RFC 6763. */
     public static final class TextEntry implements Parcelable {
+        /**
+         * The value to use for attributes with no value.
+         *
+         * <p>As per RFC6763 P.16, attributes may have no value, which is different from having an
+         * empty value (which would be an empty byte array).
+         */
+        public static final byte[] VALUE_NONE = null;
         public static final Parcelable.Creator<TextEntry> CREATOR =
                 new Parcelable.Creator<TextEntry>() {
                     @Override
@@ -389,7 +393,7 @@
             // 2. If there is no '=' in a DNS-SD TXT record string, then it is a
             // boolean attribute, simply identified as being present, with no value.
             if (delimitPos < 0) {
-                return new TextEntry(new String(textBytes, US_ASCII), (byte[]) null);
+                return new TextEntry(new String(textBytes, US_ASCII), VALUE_NONE);
             } else if (delimitPos == 0) {
                 return null;
             }
@@ -400,13 +404,13 @@
 
         /** Creates a new {@link TextEntry} with given key and value of a UTF-8 string. */
         public TextEntry(String key, String value) {
-            this(key, value == null ? null : value.getBytes(UTF_8));
+            this(key, value == null ? VALUE_NONE : value.getBytes(UTF_8));
         }
 
         /** Creates a new {@link TextEntry} with given key and value of a byte array. */
         public TextEntry(String key, byte[] value) {
             this.key = key;
-            this.value = value == null ? null : value.clone();
+            this.value = value == VALUE_NONE ? VALUE_NONE : value.clone();
         }
 
         private TextEntry(Parcel in) {
@@ -419,22 +423,26 @@
         }
 
         public byte[] getValue() {
-            return value == null ? null : value.clone();
+            return value == VALUE_NONE ? VALUE_NONE : value.clone();
         }
 
         /** Converts this {@link TextEntry} instance to '=' separated byte array. */
         public byte[] toBytes() {
             final byte[] keyBytes = key.getBytes(US_ASCII);
-            if (value == null) {
+            if (value == VALUE_NONE) {
                 return keyBytes;
             }
             return ByteUtils.concat(keyBytes, new byte[]{'='}, value);
         }
 
+        public boolean isEmpty() {
+            return TextUtils.isEmpty(key) && (value == VALUE_NONE || value.length == 0);
+        }
+
         /** Converts this {@link TextEntry} instance to '=' separated string. */
         @Override
         public String toString() {
-            if (value == null) {
+            if (value == VALUE_NONE) {
                 return key;
             }
             return key + "=" + new String(value, UTF_8);
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceRecord.java b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceRecord.java
index 0d6a9ec..fd716d2 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceRecord.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceRecord.java
@@ -20,7 +20,7 @@
 
 import androidx.annotation.VisibleForTesting;
 
-import com.android.server.connectivity.mdns.util.MdnsUtils;
+import com.android.net.module.util.DnsUtils;
 
 import java.io.IOException;
 import java.util.Arrays;
@@ -151,7 +151,7 @@
     public int hashCode() {
         return (super.hashCode() * 31)
                 + Objects.hash(servicePriority, serviceWeight,
-                Arrays.hashCode(MdnsUtils.toDnsLabelsLowerCase(serviceHost)),
+                Arrays.hashCode(DnsUtils.toDnsLabelsUpperCase(serviceHost)),
                 servicePort);
     }
 
@@ -168,7 +168,7 @@
         return super.equals(other)
                 && (servicePriority == otherRecord.servicePriority)
                 && (serviceWeight == otherRecord.serviceWeight)
-                && MdnsUtils.equalsDnsLabelIgnoreDnsCase(serviceHost, otherRecord.serviceHost)
+                && DnsUtils.equalsDnsLabelIgnoreDnsCase(serviceHost, otherRecord.serviceHost)
                 && (servicePort == otherRecord.servicePort);
     }
 }
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 b3bdbe0..4b55ea9 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
@@ -16,6 +16,7 @@
 
 package com.android.server.connectivity.mdns;
 
+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.util.MdnsUtils.Clock;
@@ -33,6 +34,7 @@
 import androidx.annotation.VisibleForTesting;
 
 import com.android.net.module.util.CollectionUtils;
+import com.android.net.module.util.DnsUtils;
 import com.android.net.module.util.SharedLog;
 import com.android.server.connectivity.mdns.util.MdnsUtils;
 
@@ -120,11 +122,11 @@
          * @return true if the service name was not discovered before.
          */
         boolean setServiceDiscovered(@NonNull String serviceName) {
-            return discoveredServiceNames.add(MdnsUtils.toDnsLowerCase(serviceName));
+            return discoveredServiceNames.add(DnsUtils.toDnsUpperCase(serviceName));
         }
 
         void unsetServiceDiscovered(@NonNull String serviceName) {
-            discoveredServiceNames.remove(MdnsUtils.toDnsLowerCase(serviceName));
+            discoveredServiceNames.remove(DnsUtils.toDnsUpperCase(serviceName));
         }
     }
 
@@ -147,7 +149,8 @@
                     final List<MdnsResponse> servicesToResolve = makeResponsesForResolve(socketKey);
                     final QueryTask queryTask = new QueryTask(taskArgs, servicesToResolve,
                             getAllDiscoverySubtypes(), needSendDiscoveryQueries(listeners),
-                            getExistingServices());
+                            getExistingServices(), searchOptions.onlyUseIpv6OnIpv6OnlyNetworks(),
+                            socketKey);
                     executor.submit(queryTask);
                     break;
                 }
@@ -178,7 +181,10 @@
                                     minRemainingTtl,
                                     now,
                                     lastSentTime,
-                                    sentResult.taskArgs.sessionId
+                                    sentResult.taskArgs.sessionId,
+                                    searchOptions.getQueryMode(),
+                                    searchOptions.numOfQueriesBeforeBackoff(),
+                                    false /* forceEnableBackoff */
                             );
                     dependencies.sendMessageDelayed(
                             handler,
@@ -303,8 +309,8 @@
         serviceCache.unregisterServiceExpiredCallback(cacheKey);
     }
 
-    private static MdnsServiceInfo buildMdnsServiceInfoFromResponse(
-            @NonNull MdnsResponse response, @NonNull String[] serviceTypeLabels) {
+    private static MdnsServiceInfo buildMdnsServiceInfoFromResponse(@NonNull MdnsResponse response,
+            @NonNull String[] serviceTypeLabels, long elapsedRealtimeMillis) {
         String[] hostName = null;
         int port = 0;
         if (response.hasServiceRecord()) {
@@ -351,7 +357,7 @@
                 textEntries,
                 response.getInterfaceIndex(),
                 response.getNetwork(),
-                now.plusMillis(response.getMinRemainingTtl(now.toEpochMilli())));
+                now.plusMillis(response.getMinRemainingTtl(elapsedRealtimeMillis)));
     }
 
     private List<MdnsResponse> getExistingServices() {
@@ -380,8 +386,8 @@
         if (existingInfo == null) {
             for (MdnsResponse existingResponse : serviceCache.getCachedServices(cacheKey)) {
                 if (!responseMatchesOptions(existingResponse, searchOptions)) continue;
-                final MdnsServiceInfo info =
-                        buildMdnsServiceInfoFromResponse(existingResponse, serviceTypeLabels);
+                final MdnsServiceInfo info = buildMdnsServiceInfoFromResponse(
+                        existingResponse, serviceTypeLabels, clock.elapsedRealtime());
                 listener.onServiceNameDiscovered(info, true /* isServiceFromCache */);
                 listenerInfo.setServiceDiscovered(info.getServiceInstanceName());
                 if (existingResponse.isComplete()) {
@@ -392,14 +398,13 @@
         }
         // Remove the next scheduled periodical task.
         removeScheduledTask();
-        mdnsQueryScheduler.cancelScheduledRun();
-        // Keep tracking the ScheduledFuture for the task so we can cancel it if caller is not
-        // interested anymore.
-        final QueryTaskConfig taskConfig = new QueryTaskConfig(
-                searchOptions.getQueryMode(),
-                searchOptions.onlyUseIpv6OnIpv6OnlyNetworks(),
-                searchOptions.numOfQueriesBeforeBackoff(),
-                socketKey);
+        final boolean forceEnableBackoff =
+                (searchOptions.getQueryMode() == AGGRESSIVE_QUERY_MODE && hadReply);
+        // Keep the latest scheduled run for rescheduling if there is a service in the cache.
+        if (!(forceEnableBackoff)) {
+            mdnsQueryScheduler.cancelScheduledRun();
+        }
+        final QueryTaskConfig taskConfig = new QueryTaskConfig(searchOptions.getQueryMode());
         final long now = clock.elapsedRealtime();
         if (lastSentTime == 0) {
             lastSentTime = now;
@@ -412,7 +417,10 @@
                             minRemainingTtl,
                             now,
                             lastSentTime,
-                            currentSessionId
+                            currentSessionId,
+                            searchOptions.getQueryMode(),
+                            searchOptions.numOfQueriesBeforeBackoff(),
+                            forceEnableBackoff
                     );
             dependencies.sendMessageDelayed(
                     handler,
@@ -424,7 +432,8 @@
                     mdnsQueryScheduler.scheduleFirstRun(taskConfig, now,
                             minRemainingTtl, currentSessionId), servicesToResolve,
                     getAllDiscoverySubtypes(), needSendDiscoveryQueries(listeners),
-                    getExistingServices());
+                    getExistingServices(), searchOptions.onlyUseIpv6OnIpv6OnlyNetworks(),
+                    socketKey);
             executor.submit(queryTask);
         }
 
@@ -458,7 +467,7 @@
             @NonNull MdnsSearchOptions options) {
         final boolean matchesInstanceName = options.getResolveInstanceName() == null
                 // DNS is case-insensitive, so ignore case in the comparison
-                || MdnsUtils.equalsIgnoreDnsCase(options.getResolveInstanceName(),
+                || DnsUtils.equalsIgnoreDnsCase(options.getResolveInstanceName(),
                 response.getServiceInstanceName());
 
         // If discovery is requiring some subtypes, the response must have one that matches a
@@ -468,7 +477,7 @@
         final boolean matchesSubtype = options.getSubtypes().size() == 0
                 || CollectionUtils.any(options.getSubtypes(), requiredSub ->
                 CollectionUtils.any(responseSubtypes, actualSub ->
-                        MdnsUtils.equalsIgnoreDnsCase(
+                        DnsUtils.equalsIgnoreDnsCase(
                                 MdnsConstants.SUBTYPE_PREFIX + requiredSub, actualSub)));
 
         return matchesInstanceName && matchesSubtype;
@@ -535,7 +544,8 @@
             final long minRemainingTtl = getMinRemainingTtl(now);
             MdnsQueryScheduler.ScheduledQueryTaskArgs args =
                     mdnsQueryScheduler.maybeRescheduleCurrentRun(now, minRemainingTtl,
-                            lastSentTime, currentSessionId + 1);
+                            lastSentTime, currentSessionId + 1,
+                            searchOptions.numOfQueriesBeforeBackoff());
             if (args != null) {
                 removeScheduledTask();
                 dependencies.sendMessageDelayed(
@@ -561,7 +571,7 @@
             if (response.getServiceInstanceName() != null) {
                 listeners.valueAt(i).unsetServiceDiscovered(response.getServiceInstanceName());
                 final MdnsServiceInfo serviceInfo = buildMdnsServiceInfoFromResponse(
-                        response, serviceTypeLabels);
+                        response, serviceTypeLabels, clock.elapsedRealtime());
                 if (response.isComplete()) {
                     sharedLog.log(message + ". onServiceRemoved: " + serviceInfo);
                     listener.onServiceRemoved(serviceInfo);
@@ -605,8 +615,8 @@
                         + " %b, responseIsComplete: %b",
                 serviceInstanceName, newInCache, serviceBecomesComplete,
                 response.isComplete()));
-        MdnsServiceInfo serviceInfo =
-                buildMdnsServiceInfoFromResponse(response, serviceTypeLabels);
+        final MdnsServiceInfo serviceInfo = buildMdnsServiceInfoFromResponse(
+                response, serviceTypeLabels, clock.elapsedRealtime());
 
         for (int i = 0; i < listeners.size(); i++) {
             // If a service stops matching the options (currently can only happen if it loses a
@@ -658,7 +668,7 @@
                 continue;
             }
             if (CollectionUtils.any(resolveResponses,
-                    r -> MdnsUtils.equalsIgnoreDnsCase(resolveName, r.getServiceInstanceName()))) {
+                    r -> DnsUtils.equalsIgnoreDnsCase(resolveName, r.getServiceInstanceName()))) {
                 continue;
             }
             MdnsResponse knownResponse =
@@ -723,15 +733,21 @@
         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,
                 @NonNull Collection<MdnsResponse> servicesToResolve,
                 @NonNull Collection<String> subtypes, boolean sendDiscoveryQueries,
-                @NonNull Collection<MdnsResponse> existingServices) {
+                @NonNull Collection<MdnsResponse> existingServices,
+                boolean onlyUseIpv6OnIpv6OnlyNetworks,
+                @NonNull SocketKey socketKey) {
             this.taskArgs = taskArgs;
             this.servicesToResolve.addAll(servicesToResolve);
             this.subtypes.addAll(subtypes);
             this.sendDiscoveryQueries = sendDiscoveryQueries;
             this.existingServices.addAll(existingServices);
+            this.onlyUseIpv6OnIpv6OnlyNetworks = onlyUseIpv6OnIpv6OnlyNetworks;
+            this.socketKey = socketKey;
         }
 
         @Override
@@ -745,8 +761,8 @@
                                 subtypes,
                                 taskArgs.config.expectUnicastResponse,
                                 taskArgs.config.transactionId,
-                                taskArgs.config.socketKey,
-                                taskArgs.config.onlyUseIpv6OnIpv6OnlyNetworks,
+                                socketKey,
+                                onlyUseIpv6OnIpv6OnlyNetworks,
                                 sendDiscoveryQueries,
                                 servicesToResolve,
                                 clock,
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsSocketClient.java b/service-t/src/com/android/server/connectivity/mdns/MdnsSocketClient.java
index 9cfcba1..17e5b31 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsSocketClient.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsSocketClient.java
@@ -28,7 +28,9 @@
 import android.util.Log;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.net.module.util.CollectionUtils;
 import com.android.net.module.util.SharedLog;
+import com.android.server.connectivity.mdns.util.MdnsUtils;
 
 import java.io.IOException;
 import java.net.DatagramPacket;
@@ -249,6 +251,12 @@
             Log.wtf(TAG, "No mDns packets to send");
             return;
         }
+        // Check all packets with the same address
+        if (!MdnsUtils.checkAllPacketsWithSameAddress(packets)) {
+            Log.wtf(TAG, "Some mDNS packets have a different target address. addresses="
+                    + CollectionUtils.map(packets, DatagramPacket::getSocketAddress));
+            return;
+        }
 
         final boolean isIpv4 = ((InetSocketAddress) packets.get(0).getSocketAddress())
                 .getAddress() instanceof Inet4Address;
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsTextRecord.java b/service-t/src/com/android/server/connectivity/mdns/MdnsTextRecord.java
index 92cf324..77d1d7a 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsTextRecord.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsTextRecord.java
@@ -89,6 +89,13 @@
         }
     }
 
+    private boolean isEmpty() {
+        return entries == null || entries.size() == 0
+                // RFC6763 6.1 indicates that a TXT record with a single zero byte is equivalent to
+                // an empty record.
+                || (entries.size() == 1 && entries.get(0).isEmpty());
+    }
+
     @Override
     public String toString() {
         StringBuilder sb = new StringBuilder();
@@ -105,7 +112,7 @@
 
     @Override
     public int hashCode() {
-        return (super.hashCode() * 31) + Objects.hash(entries);
+        return (super.hashCode() * 31) + (isEmpty() ? 0 : Objects.hash(entries));
     }
 
     @Override
@@ -116,7 +123,19 @@
         if (!(other instanceof MdnsTextRecord)) {
             return false;
         }
-
-        return super.equals(other) && Objects.equals(entries, ((MdnsTextRecord) other).entries);
+        if (!super.equals(other)) {
+            return false;
+        }
+        // As per RFC6763 6.1: DNS-SD clients MUST treat the following as equivalent:
+        // - A TXT record containing a single zero byte.
+        // - An empty (zero-length) TXT record. (This is not strictly legal, but should one be
+        //   received, it should be interpreted as the same as a single empty string.)
+        // - No TXT record
+        // Ensure that empty TXT records are considered equal, so that they are not considered
+        // conflicting for example.
+        if (isEmpty() && ((MdnsTextRecord) other).isEmpty()) {
+            return true;
+        }
+        return Objects.equals(entries, ((MdnsTextRecord) other).entries);
     }
 }
\ No newline at end of file
diff --git a/service-t/src/com/android/server/connectivity/mdns/QueryTaskConfig.java b/service-t/src/com/android/server/connectivity/mdns/QueryTaskConfig.java
index 0894166..d2cd463 100644
--- a/service-t/src/com/android/server/connectivity/mdns/QueryTaskConfig.java
+++ b/service-t/src/com/android/server/connectivity/mdns/QueryTaskConfig.java
@@ -19,9 +19,6 @@
 import static com.android.server.connectivity.mdns.MdnsSearchOptions.AGGRESSIVE_QUERY_MODE;
 import static com.android.server.connectivity.mdns.MdnsSearchOptions.PASSIVE_QUERY_MODE;
 
-import android.annotation.NonNull;
-import android.annotation.Nullable;
-
 import com.android.internal.annotations.VisibleForTesting;
 
 /**
@@ -51,9 +48,6 @@
     static final int MAX_TIME_BETWEEN_AGGRESSIVE_BURSTS_MS = 60000;
     private final boolean alwaysAskForUnicastResponse =
             MdnsConfigs.alwaysAskForUnicastResponseInEachBurst();
-    private final int queryMode;
-    final boolean onlyUseIpv6OnIpv6OnlyNetworks;
-    private final int numOfQueriesBeforeBackoff;
     @VisibleForTesting
     final int transactionId;
     @VisibleForTesting
@@ -64,16 +58,11 @@
     final long delayUntilNextTaskWithoutBackoffMs;
     private final boolean isFirstBurst;
     private final long queryCount;
-    @NonNull
-    final SocketKey socketKey;
 
-    QueryTaskConfig(@NonNull QueryTaskConfig other, long queryCount, int transactionId,
+    QueryTaskConfig(long queryCount, int transactionId,
             boolean expectUnicastResponse, boolean isFirstBurst, int burstCounter,
             int queriesPerBurst, int timeBetweenBurstsInMs,
             long delayUntilNextTaskWithoutBackoffMs) {
-        this.queryMode = other.queryMode;
-        this.onlyUseIpv6OnIpv6OnlyNetworks = other.onlyUseIpv6OnIpv6OnlyNetworks;
-        this.numOfQueriesBeforeBackoff = other.numOfQueriesBeforeBackoff;
         this.transactionId = transactionId;
         this.expectUnicastResponse = expectUnicastResponse;
         this.queriesPerBurst = queriesPerBurst;
@@ -82,27 +71,20 @@
         this.delayUntilNextTaskWithoutBackoffMs = delayUntilNextTaskWithoutBackoffMs;
         this.isFirstBurst = isFirstBurst;
         this.queryCount = queryCount;
-        this.socketKey = other.socketKey;
     }
 
-    QueryTaskConfig(int queryMode,
-            boolean onlyUseIpv6OnIpv6OnlyNetworks,
-            int numOfQueriesBeforeBackoff,
-            @Nullable SocketKey socketKey) {
-        this.queryMode = queryMode;
-        this.onlyUseIpv6OnIpv6OnlyNetworks = onlyUseIpv6OnIpv6OnlyNetworks;
-        this.numOfQueriesBeforeBackoff = numOfQueriesBeforeBackoff;
+    QueryTaskConfig(int queryMode) {
         this.queriesPerBurst = QUERIES_PER_BURST;
         this.burstCounter = 0;
         this.transactionId = 1;
         this.expectUnicastResponse = true;
         this.isFirstBurst = true;
         // Config the scan frequency based on the scan mode.
-        if (this.queryMode == AGGRESSIVE_QUERY_MODE) {
+        if (queryMode == AGGRESSIVE_QUERY_MODE) {
             this.timeBetweenBurstsInMs = INITIAL_AGGRESSIVE_TIME_BETWEEN_BURSTS_MS;
             this.delayUntilNextTaskWithoutBackoffMs =
                     TIME_BETWEEN_RETRANSMISSION_QUERIES_IN_BURST_MS;
-        } else if (this.queryMode == PASSIVE_QUERY_MODE) {
+        } else if (queryMode == PASSIVE_QUERY_MODE) {
             // In passive scan mode, sends a single burst of QUERIES_PER_BURST queries, and then
             // in each TIME_BETWEEN_BURSTS interval, sends QUERIES_PER_BURST_PASSIVE_MODE
             // queries.
@@ -116,12 +98,11 @@
             this.timeBetweenBurstsInMs = INITIAL_TIME_BETWEEN_BURSTS_MS;
             this.delayUntilNextTaskWithoutBackoffMs = TIME_BETWEEN_QUERIES_IN_BURST_MS;
         }
-        this.socketKey = socketKey;
         this.queryCount = 0;
     }
 
     long getDelayUntilNextTaskWithoutBackoff(boolean isFirstQueryInBurst,
-            boolean isLastQueryInBurst) {
+            boolean isLastQueryInBurst, int queryMode) {
         if (isFirstQueryInBurst && queryMode == AGGRESSIVE_QUERY_MODE) {
             return 0;
         }
@@ -133,7 +114,7 @@
                 : TIME_BETWEEN_QUERIES_IN_BURST_MS;
     }
 
-    boolean getNextExpectUnicastResponse(boolean isLastQueryInBurst) {
+    boolean getNextExpectUnicastResponse(boolean isLastQueryInBurst, int queryMode) {
         if (!isLastQueryInBurst) {
             return false;
         }
@@ -143,7 +124,7 @@
         return alwaysAskForUnicastResponse;
     }
 
-    int getNextTimeBetweenBurstsMs(boolean isLastQueryInBurst) {
+    int getNextTimeBetweenBurstsMs(boolean isLastQueryInBurst, int queryMode) {
         if (!isLastQueryInBurst) {
             return timeBetweenBurstsInMs;
         }
@@ -155,7 +136,7 @@
     /**
      * Get new QueryTaskConfig for next run.
      */
-    public QueryTaskConfig getConfigForNextRun() {
+    public QueryTaskConfig getConfigForNextRun(int queryMode) {
         long newQueryCount = queryCount + 1;
         int newTransactionId = transactionId + 1;
         if (newTransactionId > UNSIGNED_SHORT_MAX_VALUE) {
@@ -177,16 +158,18 @@
             }
         }
 
-        return new QueryTaskConfig(this, newQueryCount, newTransactionId,
-                getNextExpectUnicastResponse(isLastQueryInBurst), newIsFirstBurst, newBurstCounter,
-                newQueriesPerBurst, getNextTimeBetweenBurstsMs(isLastQueryInBurst),
-                getDelayUntilNextTaskWithoutBackoff(isFirstQueryInBurst, isLastQueryInBurst));
+        return new QueryTaskConfig(newQueryCount, newTransactionId,
+                getNextExpectUnicastResponse(isLastQueryInBurst, queryMode), newIsFirstBurst,
+                newBurstCounter, newQueriesPerBurst,
+                getNextTimeBetweenBurstsMs(isLastQueryInBurst, queryMode),
+                getDelayUntilNextTaskWithoutBackoff(
+                        isFirstQueryInBurst, isLastQueryInBurst, queryMode));
     }
 
     /**
      * Determine if the query backoff should be used.
      */
-    public boolean shouldUseQueryBackoff() {
+    public boolean shouldUseQueryBackoff(int numOfQueriesBeforeBackoff) {
         // Don't enable backoff mode during the burst or in the first burst
         if (burstCounter != 0 || isFirstBurst) {
             return false;
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 3c11a24..8745941 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
@@ -16,6 +16,8 @@
 
 package com.android.server.connectivity.mdns.util;
 
+import static com.android.net.module.util.DnsUtils.equalsDnsLabelIgnoreDnsCase;
+import static com.android.net.module.util.DnsUtils.equalsIgnoreDnsCase;
 import static com.android.server.connectivity.mdns.MdnsConstants.FLAG_TRUNCATED;
 
 import android.annotation.NonNull;
@@ -34,6 +36,7 @@
 
 import java.io.IOException;
 import java.net.DatagramPacket;
+import java.net.InetAddress;
 import java.net.InetSocketAddress;
 import java.nio.ByteBuffer;
 import java.nio.CharBuffer;
@@ -55,18 +58,17 @@
     private MdnsUtils() { }
 
     /**
-     * Convert the string to DNS case-insensitive lowercase
+     * Compare labels a equals b or a is suffix of b.
      *
-     * Per rfc6762#page-46, accented characters are not defined to be automatically equivalent to
-     * their unaccented counterparts. So the "DNS lowercase" should be if character is A-Z then they
-     * transform into a-z. Otherwise, they are kept as-is.
+     * @param a the type or subtype.
+     * @param b the base type
      */
-    public static String toDnsLowerCase(@NonNull String string) {
-        final char[] outChars = new char[string.length()];
-        for (int i = 0; i < string.length(); i++) {
-            outChars[i] = toDnsLowerCase(string.charAt(i));
-        }
-        return new String(outChars);
+    public static boolean typeEqualsOrIsSubtype(@NonNull String[] a,
+            @NonNull String[] b) {
+        return equalsDnsLabelIgnoreDnsCase(a, b)
+                || ((b.length == (a.length + 2))
+                && equalsIgnoreDnsCase(b[1], MdnsConstants.SUBTYPE_LABEL)
+                && MdnsRecord.labelsAreSuffix(a, b));
     }
 
     /**
@@ -80,70 +82,6 @@
         }
     }
 
-    /**
-     * Convert the array of labels to DNS case-insensitive lowercase.
-     */
-    public static String[] toDnsLabelsLowerCase(@NonNull String[] labels) {
-        final String[] outStrings = new String[labels.length];
-        for (int i = 0; i < labels.length; ++i) {
-            outStrings[i] = toDnsLowerCase(labels[i]);
-        }
-        return outStrings;
-    }
-
-    /**
-     * Compare two strings by DNS case-insensitive lowercase.
-     */
-    public static boolean equalsIgnoreDnsCase(@Nullable String a, @Nullable String b) {
-        if (a == null || b == null) {
-            return a == null && b == null;
-        }
-        if (a.length() != b.length()) return false;
-        for (int i = 0; i < a.length(); i++) {
-            if (toDnsLowerCase(a.charAt(i)) != toDnsLowerCase(b.charAt(i))) {
-                return false;
-            }
-        }
-        return true;
-    }
-
-    /**
-     * Compare two set of DNS labels by DNS case-insensitive lowercase.
-     */
-    public static boolean equalsDnsLabelIgnoreDnsCase(@NonNull String[] a, @NonNull String[] b) {
-        if (a == b) {
-            return true;
-        }
-        int length = a.length;
-        if (b.length != length) {
-            return false;
-        }
-        for (int i = 0; i < length; i++) {
-            if (!equalsIgnoreDnsCase(a[i], b[i])) {
-                return false;
-            }
-        }
-        return true;
-    }
-
-    /**
-     * Compare labels a equals b or a is suffix of b.
-     *
-     * @param a the type or subtype.
-     * @param b the base type
-     */
-    public static boolean typeEqualsOrIsSubtype(@NonNull String[] a,
-            @NonNull String[] b) {
-        return MdnsUtils.equalsDnsLabelIgnoreDnsCase(a, b)
-                || ((b.length == (a.length + 2))
-                && MdnsUtils.equalsIgnoreDnsCase(b[1], MdnsConstants.SUBTYPE_LABEL)
-                && MdnsRecord.labelsAreSuffix(a, b));
-    }
-
-    private static char toDnsLowerCase(char a) {
-        return a >= 'A' && a <= 'Z' ? (char) (a + ('a' - 'A')) : a;
-    }
-
     /*** Ensure that current running thread is same as given handler thread */
     public static void ensureRunningOnHandlerThread(@NonNull Handler handler) {
         if (!isRunningOnHandlerThread(handler)) {
@@ -361,4 +299,23 @@
             return SystemClock.elapsedRealtime();
         }
     }
+
+    /**
+     * Check all DatagramPackets with the same destination address.
+     */
+    public static boolean checkAllPacketsWithSameAddress(List<DatagramPacket> packets) {
+        // No packet for address check
+        if (packets.isEmpty()) {
+            return true;
+        }
+
+        final InetAddress address =
+                ((InetSocketAddress) packets.get(0).getSocketAddress()).getAddress();
+        for (DatagramPacket packet : packets) {
+            if (!address.equals(((InetSocketAddress) packet.getSocketAddress()).getAddress())) {
+                return false;
+            }
+        }
+        return true;
+    }
 }
\ No newline at end of file
diff --git a/service-t/src/com/android/server/net/NetworkStatsService.java b/service-t/src/com/android/server/net/NetworkStatsService.java
index 114cf2e..11343d2 100644
--- a/service-t/src/com/android/server/net/NetworkStatsService.java
+++ b/service-t/src/com/android/server/net/NetworkStatsService.java
@@ -74,12 +74,12 @@
 import static com.android.net.module.util.DeviceConfigUtils.getDeviceConfigPropertyInt;
 import static com.android.net.module.util.NetworkCapabilitiesUtils.getDisplayTransport;
 import static com.android.net.module.util.NetworkStatsUtils.LIMIT_GLOBAL_ALERT;
-import static com.android.server.net.NetworkStatsEventLogger.POLL_REASON_PERIODIC;
 import static com.android.server.net.NetworkStatsEventLogger.POLL_REASON_DUMPSYS;
 import static com.android.server.net.NetworkStatsEventLogger.POLL_REASON_FORCE_UPDATE;
 import static com.android.server.net.NetworkStatsEventLogger.POLL_REASON_GLOBAL_ALERT;
 import static com.android.server.net.NetworkStatsEventLogger.POLL_REASON_NETWORK_STATUS_CHANGED;
 import static com.android.server.net.NetworkStatsEventLogger.POLL_REASON_OPEN_SESSION;
+import static com.android.server.net.NetworkStatsEventLogger.POLL_REASON_PERIODIC;
 import static com.android.server.net.NetworkStatsEventLogger.POLL_REASON_RAT_CHANGED;
 import static com.android.server.net.NetworkStatsEventLogger.POLL_REASON_REG_CALLBACK;
 import static com.android.server.net.NetworkStatsEventLogger.POLL_REASON_REMOVE_UIDS;
@@ -242,13 +242,11 @@
     // A message for broadcasting ACTION_NETWORK_STATS_UPDATED in handler thread to prevent
     // deadlock.
     private static final int MSG_BROADCAST_NETWORK_STATS_UPDATED = 4;
-
     /** Flags to control detail level of poll event. */
     private static final int FLAG_PERSIST_NETWORK = 0x1;
     private static final int FLAG_PERSIST_UID = 0x2;
     private static final int FLAG_PERSIST_ALL = FLAG_PERSIST_NETWORK | FLAG_PERSIST_UID;
     private static final int FLAG_PERSIST_FORCE = 0x100;
-
     /**
      * When global alert quota is high, wait for this delay before processing each polling,
      * and do not schedule further polls once there is already one queued.
@@ -313,6 +311,12 @@
     static final String TRAFFIC_STATS_CACHE_MAX_ENTRIES_NAME = "trafficstats_cache_max_entries";
     static final int DEFAULT_TRAFFIC_STATS_CACHE_EXPIRY_DURATION_MS = 1000;
     static final int DEFAULT_TRAFFIC_STATS_CACHE_MAX_ENTRIES = 400;
+    /**
+     * The delay time between to network stats update intents.
+     * Added to fix intent spams (b/3115462)
+     */
+    @VisibleForTesting(visibility = PRIVATE)
+    static final int BROADCAST_NETWORK_STATS_UPDATED_DELAY_MS = 1000;
 
     private final Context mContext;
     private final NetworkStatsFactory mStatsFactory;
@@ -385,6 +389,7 @@
         long getXtPersistBytes(long def);
         long getUidPersistBytes(long def);
         long getUidTagPersistBytes(long def);
+        long getBroadcastNetworkStatsUpdateDelayMs();
     }
 
     private final Object mStatsLock = new Object();
@@ -469,15 +474,36 @@
 
     private long mLastStatsSessionPoll;
 
+    /**
+     * The timestamp of the most recent network stats broadcast.
+     *
+     * Note that this time could be in the past for completed broadcasts,
+     * or in the future for scheduled broadcasts.
+     *
+     * It is initialized to {@code Long.MIN_VALUE} to ensure that the first broadcast request
+     * is fulfilled immediately, regardless of the delay time.
+     *
+     * This value is used to enforce rate limiting on intents, preventing intent spam.
+     */
+    @GuardedBy("mStatsLock")
+    private long mLatestNetworkStatsUpdatedBroadcastScheduledTime = Long.MIN_VALUE;
+
+
     private final TrafficStatsRateLimitCache mTrafficStatsTotalCache;
     private final TrafficStatsRateLimitCache mTrafficStatsIfaceCache;
     private final TrafficStatsRateLimitCache mTrafficStatsUidCache;
     static final String TRAFFICSTATS_RATE_LIMIT_CACHE_ENABLED_FLAG =
             "trafficstats_rate_limit_cache_enabled_flag";
+    static final String BROADCAST_NETWORK_STATS_UPDATED_RATE_LIMIT_ENABLED_FLAG =
+            "broadcast_network_stats_updated_rate_limit_enabled_flag";
     private final boolean mAlwaysUseTrafficStatsRateLimitCache;
     private final int mTrafficStatsRateLimitCacheExpiryDuration;
     private final int mTrafficStatsRateLimitCacheMaxEntries;
 
+    private final boolean mBroadcastNetworkStatsUpdatedRateLimitEnabled;
+
+
+
     private final Object mOpenSessionCallsLock = new Object();
 
     /**
@@ -669,6 +695,8 @@
 
         mAlwaysUseTrafficStatsRateLimitCache =
                 mDeps.alwaysUseTrafficStatsRateLimitCache(mContext);
+        mBroadcastNetworkStatsUpdatedRateLimitEnabled =
+                mDeps.enabledBroadcastNetworkStatsUpdatedRateLimiting(mContext);
         mTrafficStatsRateLimitCacheExpiryDuration =
                 mDeps.getTrafficStatsRateLimitCacheExpiryDuration();
         mTrafficStatsRateLimitCacheMaxEntries =
@@ -696,6 +724,15 @@
     @VisibleForTesting
     public static class Dependencies {
         /**
+         * Get broadcast network stats updated delay time in ms
+         * @return
+         */
+        @NonNull
+        public long getBroadcastNetworkStatsUpdateDelayMs() {
+            return BROADCAST_NETWORK_STATS_UPDATED_DELAY_MS;
+        }
+
+        /**
          * Get legacy platform stats directory.
          */
         @NonNull
@@ -927,6 +964,17 @@
         }
 
         /**
+         * Get whether broadcast network stats update rate limiting is enabled.
+         *
+         * This method should only be called once in the constructor,
+         * to ensure that the code does not need to deal with flag values changing at runtime.
+         */
+        public boolean enabledBroadcastNetworkStatsUpdatedRateLimiting(Context ctx) {
+            return DeviceConfigUtils.isTetheringFeatureNotChickenedOut(
+                    ctx, BROADCAST_NETWORK_STATS_UPDATED_RATE_LIMIT_ENABLED_FLAG);
+        }
+
+        /**
          * Get whether TrafficStats rate-limit cache is always applied.
          *
          * This method should only be called once in the constructor,
@@ -2645,8 +2693,22 @@
             performSampleLocked();
         }
 
-        // finally, dispatch updated event to any listeners
-        mHandler.sendMessage(mHandler.obtainMessage(MSG_BROADCAST_NETWORK_STATS_UPDATED));
+        // Dispatch updated event to listeners, preventing intent spamming
+        // (b/343844995) possibly from abnormal modem RAT changes or misbehaving
+        // app calls (see NetworkStatsEventLogger#POLL_REASON_* for possible reasons).
+        // If no broadcasts are scheduled, use the time of the last broadcast
+        // to schedule the next one ASAP.
+        if (!mBroadcastNetworkStatsUpdatedRateLimitEnabled) {
+            mHandler.sendMessage(mHandler.obtainMessage(MSG_BROADCAST_NETWORK_STATS_UPDATED));
+        } else if (mLatestNetworkStatsUpdatedBroadcastScheduledTime < SystemClock.uptimeMillis()) {
+            mLatestNetworkStatsUpdatedBroadcastScheduledTime = Math.max(
+                    mLatestNetworkStatsUpdatedBroadcastScheduledTime
+                            + mSettings.getBroadcastNetworkStatsUpdateDelayMs(),
+                    SystemClock.uptimeMillis()
+            );
+            mHandler.sendMessageAtTime(mHandler.obtainMessage(MSG_BROADCAST_NETWORK_STATS_UPDATED),
+                    mLatestNetworkStatsUpdatedBroadcastScheduledTime);
+        }
 
         Trace.traceEnd(TRACE_TAG_NETWORK);
     }
@@ -3605,6 +3667,11 @@
         public long getUidTagPersistBytes(long def) {
             return def;
         }
+
+        @Override
+        public long getBroadcastNetworkStatsUpdateDelayMs() {
+            return BROADCAST_NETWORK_STATS_UPDATED_DELAY_MS;
+        }
     }
 
     // TODO: Read stats by using BpfNetMapsReader.
diff --git a/service/Android.bp b/service/Android.bp
index 1a0e045..c68f0b8 100644
--- a/service/Android.bp
+++ b/service/Android.bp
@@ -206,6 +206,7 @@
     },
     visibility: [
         "//packages/modules/Connectivity/service-t",
+        "//packages/modules/Connectivity/networksecurity:__subpackages__",
         "//packages/modules/Connectivity/tests:__subpackages__",
         "//packages/modules/Connectivity/thread/service:__subpackages__",
         "//packages/modules/Connectivity/thread/tests:__subpackages__",
@@ -247,6 +248,7 @@
         "service-connectivity-pre-jarjar",
         "service-connectivity-tiramisu-pre-jarjar",
         "service-nearby-pre-jarjar",
+        "service-networksecurity-pre-jarjar",
         service_remoteauth_pre_jarjar_lib,
         "service-thread-pre-jarjar",
     ],
@@ -315,6 +317,7 @@
         ":framework-connectivity-jarjar-rules",
         ":service-connectivity-jarjar-gen",
         ":service-nearby-jarjar-gen",
+        ":service-networksecurity-jarjar-gen",
         ":service-remoteauth-jarjar-gen",
         ":service-thread-jarjar-gen",
     ],
@@ -404,6 +407,24 @@
     visibility: ["//visibility:private"],
 }
 
+java_genrule {
+    name: "service-networksecurity-jarjar-gen",
+    tool_files: [
+        ":service-networksecurity-pre-jarjar{.jar}",
+        "jarjar-excludes.txt",
+    ],
+    tools: [
+        "jarjar-rules-generator",
+    ],
+    out: ["service_ct_jarjar_rules.txt"],
+    cmd: "$(location jarjar-rules-generator) " +
+        "$(location :service-networksecurity-pre-jarjar{.jar}) " +
+        "--prefix com.android.server.net.ct " +
+        "--excludes $(location jarjar-excludes.txt) " +
+        "--output $(out)",
+    visibility: ["//visibility:private"],
+}
+
 genrule {
     name: "statslog-connectivity-java-gen",
     tools: ["stats-log-api-gen"],
diff --git a/service/ServiceConnectivityResources/res/values/config_thread.xml b/service/ServiceConnectivityResources/res/values/config_thread.xml
index 02a9ce6..4027038 100644
--- a/service/ServiceConnectivityResources/res/values/config_thread.xml
+++ b/service/ServiceConnectivityResources/res/values/config_thread.xml
@@ -54,9 +54,21 @@
     -->
     <string translatable="false" name="config_thread_model_name">Thread Border Router</string>
 
-    <!-- Whether the Thread network will be managed by the Google Home ecosystem. When this value
-    is set, a TXT entry "vgh=0" or "vgh=1" will be added to the "_mehscop._udp" mDNS service
-    respectively (The TXT value is a string).
+    <!-- Specifies vendor-specific mDNS TXT entries which will be included in the "_meshcop._udp"
+    service. The TXT entries list MUST conform to the format requirement in RFC 6763 section 6. For
+    example, the key and value of each TXT entry MUST be separated with "=". If the value length is
+    0, the trailing "=" may be omitted. Additionally, the TXT keys MUST start with "v" and be at
+    least 2 characters.
+
+    Note, do not include credentials in any of the TXT entries - they will be advertised on Wi-Fi
+    or Ethernet link.
+
+    An example config can be:
+      <string-array name="config_thread_mdns_vendor_specific_txts">
+        <item>vab=123</item>
+        <item>vcd</item>
+      </string-array>
     -->
-    <bool name="config_thread_managed_by_google_home">false</bool>
+    <string-array name="config_thread_mdns_vendor_specific_txts">
+    </string-array>
 </resources>
diff --git a/service/ServiceConnectivityResources/res/values/overlayable.xml b/service/ServiceConnectivityResources/res/values/overlayable.xml
index 158b0c8..fbaae05 100644
--- a/service/ServiceConnectivityResources/res/values/overlayable.xml
+++ b/service/ServiceConnectivityResources/res/values/overlayable.xml
@@ -51,6 +51,7 @@
             <item type="string" name="config_thread_vendor_name" />
             <item type="string" name="config_thread_vendor_oui" />
             <item type="string" name="config_thread_model_name" />
+            <item type="array" name="config_thread_mdns_vendor_specific_txts" />
         </policy>
     </overlayable>
 </resources>
diff --git a/service/src/com/android/server/CallbackQueue.java b/service/src/com/android/server/CallbackQueue.java
new file mode 100644
index 0000000..4e068ea
--- /dev/null
+++ b/service/src/com/android/server/CallbackQueue.java
@@ -0,0 +1,125 @@
+/*
+ * 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.annotation.NonNull;
+import android.net.ConnectivityManager;
+import android.net.ConnectivityManager.NetworkCallback;
+
+import com.android.net.module.util.GrowingIntArray;
+
+/**
+ * A utility class to add/remove {@link NetworkCallback}s from a queue.
+ *
+ * <p>This is intended to be used as a temporary builder to create/modify callbacks stored in an int
+ * array for memory efficiency.
+ *
+ * <p>Intended usage:
+ * <pre>
+ *     final CallbackQueue queue = new CallbackQueue(storedCallbacks);
+ *     queue.forEach(netId, callbackId -> { [...] });
+ *     queue.addCallback(netId, callbackId);
+ *     [...]
+ *     storedCallbacks = queue.getMinimizedBackingArray();
+ * </pre>
+ *
+ * <p>This class is not thread-safe.
+ */
+public class CallbackQueue extends GrowingIntArray {
+    public CallbackQueue(int[] initialCallbacks) {
+        super(initialCallbacks);
+    }
+
+    /**
+     * Get a callback int from netId and callbackId.
+     *
+     * <p>The first 16 bits of each int is the netId; the last 16 bits are the callback index.
+     */
+    private static int getCallbackInt(int netId, int callbackId) {
+        return (netId << 16) | (callbackId & 0xffff);
+    }
+
+    private static int getNetId(int callbackInt) {
+        return callbackInt >>> 16;
+    }
+
+    private static int getCallbackId(int callbackInt) {
+        return callbackInt & 0xffff;
+    }
+
+    /**
+     * A consumer interface for {@link #forEach(CallbackConsumer)}.
+     *
+     * <p>This is similar to a BiConsumer&lt;Integer, Integer&gt;, but avoids the boxing cost.
+     */
+    public interface CallbackConsumer {
+        /**
+         * Method called on each callback in the queue.
+         */
+        void accept(int netId, int callbackId);
+    }
+
+    /**
+     * Iterate over all callbacks in the queue.
+     */
+    public void forEach(@NonNull CallbackConsumer consumer) {
+        forEach(value -> {
+            final int netId = getNetId(value);
+            final int callbackId = getCallbackId(value);
+            consumer.accept(netId, callbackId);
+        });
+    }
+
+    /**
+     * Indicates whether the queue contains a callback for the given (netId, callbackId).
+     */
+    public boolean hasCallback(int netId, int callbackId) {
+        return contains(getCallbackInt(netId, callbackId));
+    }
+
+    /**
+     * Remove all callbacks for the given netId.
+     *
+     * @return true if at least one callback was removed.
+     */
+    public boolean removeCallbacksForNetId(int netId) {
+        return removeValues(cb -> getNetId(cb) == netId);
+    }
+
+    /**
+     * Remove all callbacks for the given netId and callbackId.
+     * @return true if at least one callback was removed.
+     */
+    public boolean removeCallbacks(int netId, int callbackId) {
+        final int cbInt = getCallbackInt(netId, callbackId);
+        return removeValues(cb -> cb == cbInt);
+    }
+
+    /**
+     * Add a callback at the end of the queue.
+     */
+    public void addCallback(int netId, int callbackId) {
+        add(getCallbackInt(netId, callbackId));
+    }
+
+    @Override
+    protected String valueToString(int item) {
+        final int callbackId = getCallbackId(item);
+        final int netId = getNetId(item);
+        return ConnectivityManager.getCallbackName(callbackId) + "(" + netId + ")";
+    }
+}
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index f015742..53b1eb2 100755
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -38,8 +38,8 @@
 import static android.net.ConnectivityManager.BLOCKED_METERED_REASON_MASK;
 import static android.net.ConnectivityManager.BLOCKED_REASON_APP_BACKGROUND;
 import static android.net.ConnectivityManager.BLOCKED_REASON_LOCKDOWN_VPN;
-import static android.net.ConnectivityManager.BLOCKED_REASON_NONE;
 import static android.net.ConnectivityManager.BLOCKED_REASON_NETWORK_RESTRICTED;
+import static android.net.ConnectivityManager.BLOCKED_REASON_NONE;
 import static android.net.ConnectivityManager.CALLBACK_AVAILABLE;
 import static android.net.ConnectivityManager.CALLBACK_BLK_CHANGED;
 import static android.net.ConnectivityManager.CALLBACK_CAP_CHANGED;
@@ -56,6 +56,8 @@
 import static android.net.ConnectivityManager.FIREWALL_RULE_ALLOW;
 import static android.net.ConnectivityManager.FIREWALL_RULE_DEFAULT;
 import static android.net.ConnectivityManager.FIREWALL_RULE_DENY;
+import static android.net.ConnectivityManager.NETID_UNSET;
+import static android.net.ConnectivityManager.NetworkCallback.DECLARED_METHODS_ALL;
 import static android.net.ConnectivityManager.NetworkCallback.DECLARED_METHODS_NONE;
 import static android.net.ConnectivityManager.TYPE_BLUETOOTH;
 import static android.net.ConnectivityManager.TYPE_ETHERNET;
@@ -76,7 +78,6 @@
 import static android.net.ConnectivityManager.TYPE_WIFI_P2P;
 import static android.net.ConnectivityManager.getNetworkTypeName;
 import static android.net.ConnectivityManager.isNetworkTypeValid;
-import static android.net.ConnectivityManager.NetworkCallback.DECLARED_METHODS_ALL;
 import static android.net.ConnectivitySettingsManager.PRIVATE_DNS_MODE_OPPORTUNISTIC;
 import static android.net.INetd.PERMISSION_INTERNET;
 import static android.net.INetworkMonitor.NETWORK_VALIDATION_PROBE_PRIVDNS;
@@ -145,8 +146,9 @@
 import static com.android.net.module.util.PermissionUtils.hasAnyPermissionOf;
 import static com.android.server.ConnectivityStatsLog.CONNECTIVITY_STATE_SAMPLE;
 import static com.android.server.connectivity.ConnectivityFlags.DELAY_DESTROY_SOCKETS;
-import static com.android.server.connectivity.ConnectivityFlags.REQUEST_RESTRICTED_WIFI;
 import static com.android.server.connectivity.ConnectivityFlags.INGRESS_TO_VPN_ADDRESS_FILTERING;
+import static com.android.server.connectivity.ConnectivityFlags.QUEUE_CALLBACKS_FOR_FROZEN_APPS;
+import static com.android.server.connectivity.ConnectivityFlags.REQUEST_RESTRICTED_WIFI;
 
 import android.Manifest;
 import android.annotation.CheckResult;
@@ -329,9 +331,9 @@
 import com.android.net.module.util.LinkPropertiesUtils.CompareOrUpdateResult;
 import com.android.net.module.util.LinkPropertiesUtils.CompareResult;
 import com.android.net.module.util.LocationPermissionChecker;
-import com.android.net.module.util.RoutingCoordinatorService;
 import com.android.net.module.util.PerUidCounter;
 import com.android.net.module.util.PermissionUtils;
+import com.android.net.module.util.RoutingCoordinatorService;
 import com.android.net.module.util.TcUtils;
 import com.android.net.module.util.netlink.InetDiagMessage;
 import com.android.networkstack.apishim.BroadcastOptionsShimImpl;
@@ -510,6 +512,9 @@
 
     private final boolean mUseDeclaredMethodsForCallbacksEnabled;
 
+    // Flag to delay callbacks for frozen apps, suppressing duplicate and stale callbacks.
+    private final boolean mQueueCallbacksForFrozenApps;
+
     /**
      * Uids ConnectivityService tracks blocked status of to send blocked status callbacks.
      * Key is uid based on mAsUid of registered networkRequestInfo
@@ -1873,6 +1878,9 @@
                 context, ConnectivityFlags.BACKGROUND_FIREWALL_CHAIN);
         mUseDeclaredMethodsForCallbacksEnabled = mDeps.isFeatureEnabled(context,
                 ConnectivityFlags.USE_DECLARED_METHODS_FOR_CALLBACKS);
+        // registerUidFrozenStateChangedCallback is only available on U+
+        mQueueCallbacksForFrozenApps = mDeps.isAtLeastU()
+                && mDeps.isFeatureEnabled(context, QUEUE_CALLBACKS_FOR_FROZEN_APPS);
         mCarrierPrivilegeAuthenticator = mDeps.makeCarrierPrivilegeAuthenticator(
                 mContext, mTelephonyManager, mRequestRestrictedWifiEnabled,
                 this::handleUidCarrierPrivilegesLost, mHandler);
@@ -2028,7 +2036,7 @@
         mDelayDestroySockets = mDeps.isFeatureNotChickenedOut(context, DELAY_DESTROY_SOCKETS);
         mAllowSysUiConnectivityReports = mDeps.isFeatureNotChickenedOut(
                 mContext, ALLOW_SYSUI_CONNECTIVITY_REPORTS);
-        if (mDestroyFrozenSockets) {
+        if (mDestroyFrozenSockets || mQueueCallbacksForFrozenApps) {
             final UidFrozenStateChangedCallback frozenStateChangedCallback =
                     new UidFrozenStateChangedCallback() {
                 @Override
@@ -2140,7 +2148,7 @@
     }
 
     @VisibleForTesting
-    void updateMobileDataPreferredUids() {
+    public void updateMobileDataPreferredUids() {
         mHandler.sendEmptyMessage(EVENT_MOBILE_DATA_PREFERRED_UIDS_CHANGED);
     }
 
@@ -3396,7 +3404,7 @@
     }
 
     @VisibleForTesting
-    void handleBlockedReasonsChanged(List<Pair<Integer, Integer>> reasonsList) {
+    public void handleBlockedReasonsChanged(List<Pair<Integer, Integer>> reasonsList) {
         for (Pair<Integer, Integer> reasons: reasonsList) {
             final int uid = reasons.first;
             final int blockedReasons = reasons.second;
@@ -3464,6 +3472,14 @@
 
     private void handleFrozenUids(int[] uids, int[] frozenStates) {
         ensureRunningOnConnectivityServiceThread();
+        handleDestroyFrozenSockets(uids, frozenStates);
+        handleFreezeNetworkCallbacks(uids, frozenStates);
+    }
+
+    private void handleDestroyFrozenSockets(int[] uids, int[] frozenStates) {
+        if (!mDestroyFrozenSockets) {
+            return;
+        }
         for (int i = 0; i < uids.length; i++) {
             final int uid = uids[i];
             final boolean addReason = frozenStates[i] == UID_FROZEN_STATE_FROZEN;
@@ -3475,6 +3491,73 @@
         }
     }
 
+    private void handleFreezeNetworkCallbacks(int[] uids, int[] frozenStates) {
+        if (!mQueueCallbacksForFrozenApps) {
+            return;
+        }
+        for (int i = 0; i < uids.length; i++) {
+            final int uid = uids[i];
+            // These counters may be modified on different threads, but using them here is fine
+            // because this is only an optimization where wrong behavior would only happen if they
+            // are zero even though there is a request registered. This is not possible as they are
+            // always incremented before posting messages to register, and decremented on the
+            // handler thread when unregistering.
+            if (mSystemNetworkRequestCounter.get(uid) == 0
+                    && mNetworkRequestCounter.get(uid) == 0) {
+                // Avoid iterating requests if there isn't any. The counters only track app requests
+                // and not internal requests (for example always-on requests which do not have a
+                // mMessenger), so it does not completely match the content of mRequests. This is OK
+                // as only app requests need to be frozen.
+                continue;
+            }
+
+            if (frozenStates[i] == UID_FROZEN_STATE_FROZEN) {
+                freezeNetworkCallbacksForUid(uid);
+            } else {
+                unfreezeNetworkCallbacksForUid(uid);
+            }
+        }
+    }
+
+    /**
+     * Suspend callbacks for a UID that was just frozen.
+     *
+     * <p>Note that it is not possible for a process to be frozen during a blocking binder call
+     * (see CachedAppOptimizer.freezeBinder), and IConnectivityManager callback registrations are
+     * blocking binder calls, so no callback can be registered while the UID is frozen. This means
+     * it is not necessary to check frozen state on new callback registrations, and calling this
+     * method when a UID is newly frozen is sufficient.
+     *
+     * <p>If it ever becomes possible for a process to be frozen during a blocking binder call,
+     * ConnectivityService will need to handle freezing callbacks that reach ConnectivityService
+     * after the app was frozen when being registered.
+     */
+    private void freezeNetworkCallbacksForUid(int uid) {
+        if (DDBG) Log.d(TAG, "Freezing callbacks for UID " + uid);
+        for (NetworkRequestInfo nri : mNetworkRequests.values()) {
+            if (nri.mUid != uid) continue;
+            // mNetworkRequests can have duplicate values for multilayer requests, but calling
+            // onFrozen multiple times is fine.
+            // If freezeNetworkCallbacksForUid was called multiple times in a raw for a frozen UID
+            // (which would be incorrect), this would also handle it gracefully.
+            nri.onFrozen();
+        }
+    }
+
+    private void unfreezeNetworkCallbacksForUid(int uid) {
+        // This sends all callbacks for one NetworkRequest at a time, which may not be the
+        // same order they were queued in, but different network requests use different
+        // binder objects, so the relative order of their callbacks is not guaranteed.
+        // If callbacks are not queued, callbacks from different binder objects may be
+        // posted on different threads when the process is unfrozen, so even if they were
+        // called a long time apart while the process was frozen, they may still appear in
+        // different order when unfreezing it.
+        for (NetworkRequestInfo nri : mNetworkRequests.values()) {
+            if (nri.mUid != uid) continue;
+            nri.sendQueuedCallbacks();
+        }
+    }
+
     private void handleUpdateFirewallDestroySocketReasons(
             List<Pair<Integer, Integer>> reasonsList) {
         if (!shouldTrackFirewallDestroySocketReasons()) {
@@ -5919,7 +6002,7 @@
             // TODO : The only way out of this is to diff old defaults and new defaults, and only
             // remove ranges for those requests that won't have a replacement
             final NetworkAgentInfo satisfier = nri.getSatisfier();
-            if (null != satisfier) {
+            if (null != satisfier && !satisfier.isDestroyed()) {
                 try {
                     mNetd.networkRemoveUidRangesParcel(new NativeUidRangeConfig(
                             satisfier.network.getNetId(),
@@ -7529,6 +7612,29 @@
         // single NetworkRequest in mRequests.
         final List<NetworkRequest> mRequests;
 
+        /**
+         * List of callbacks that are queued for sending later when the requesting app is unfrozen.
+         *
+         * <p>There may typically be hundreds of NetworkRequestInfo, so a memory-efficient structure
+         * (just an int[]) is used to keep queued callbacks. This reduces the number of object
+         * references.
+         *
+         * <p>This is intended to be used with {@link CallbackQueue} which defines the internal
+         * format.
+         */
+        @NonNull
+        private int[] mQueuedCallbacks = new int[0];
+
+        private static final int MATCHED_NETID_NOT_FROZEN = -1;
+
+        /**
+         * If this request was already satisfied by a network when the requesting UID was frozen,
+         * the netId that was matched at that time. Otherwise, NETID_UNSET if no network was
+         * satisfying this request when frozen (including if this is a listen and not a request),
+         * and MATCHED_NETID_NOT_FROZEN if not frozen.
+         */
+        private int mMatchedNetIdWhenFrozen = MATCHED_NETID_NOT_FROZEN;
+
         // mSatisfier and mActiveRequest rely on one another therefore set them together.
         void setSatisfier(
                 @Nullable final NetworkAgentInfo satisfier,
@@ -7700,6 +7806,8 @@
                 }
                 setSatisfier(satisfier, activeRequest);
             }
+            mMatchedNetIdWhenFrozen = nri.mMatchedNetIdWhenFrozen;
+            mQueuedCallbacks = nri.mQueuedCallbacks;
             mMessenger = nri.mMessenger;
             mBinder = nri.mBinder;
             mPid = nri.mPid;
@@ -7764,6 +7872,190 @@
             }
         }
 
+        /**
+         * Called when this NRI is being frozen.
+         *
+         * <p>Calling this method multiple times when the NRI is frozen is fine. This may happen
+         * if iterating through the NetworkRequest -> NRI map since there are duplicates in the
+         * NRI values for multilayer requests. It may also happen if an app is frozen, killed,
+         * restarted and refrozen since there is no callback sent when processes are killed, but in
+         * that case the callbacks to the killed app do not matter.
+         */
+        void onFrozen() {
+            if (mMatchedNetIdWhenFrozen != MATCHED_NETID_NOT_FROZEN) {
+                // Already frozen
+                return;
+            }
+            if (mSatisfier != null) {
+                mMatchedNetIdWhenFrozen = mSatisfier.network.netId;
+            } else {
+                mMatchedNetIdWhenFrozen = NETID_UNSET;
+            }
+        }
+
+        boolean maybeQueueCallback(@NonNull NetworkAgentInfo nai, int callbackId) {
+            if (mMatchedNetIdWhenFrozen == MATCHED_NETID_NOT_FROZEN) {
+                return false;
+            }
+
+            boolean ignoreThisCallback = false;
+            final int netId = nai.network.netId;
+            final CallbackQueue queue = new CallbackQueue(mQueuedCallbacks);
+            // Based on the new callback, clear previous callbacks that are no longer necessary.
+            // For example, if the network is lost, there is no need to send intermediate callbacks.
+            switch (callbackId) {
+                // PRECHECK is not an API and not very meaningful, do not deliver it for frozen apps
+                // Networks are likely to already be lost when the app is unfrozen, also skip LOSING
+                case CALLBACK_PRECHECK:
+                case CALLBACK_LOSING:
+                    ignoreThisCallback = true;
+                    break;
+                case CALLBACK_LOST:
+                    // All callbacks for this netId before onLost are unnecessary. And onLost itself
+                    // is also unnecessary if onAvailable was previously queued for this netId: the
+                    // Network just appeared and disappeared while the app was frozen.
+                    ignoreThisCallback = queue.hasCallback(netId, CALLBACK_AVAILABLE);
+                    queue.removeCallbacksForNetId(netId);
+                    break;
+                case CALLBACK_AVAILABLE:
+                    if (mSatisfier != null) {
+                        // For requests that are satisfied by individual networks (not LISTEN), when
+                        // AVAILABLE is received, the request is matching a new Network, so previous
+                        // callbacks (for other Networks) are unnecessary.
+                        queue.clear();
+                    }
+                    break;
+                case CALLBACK_SUSPENDED:
+                case CALLBACK_RESUMED:
+                    if (queue.hasCallback(netId, CALLBACK_AVAILABLE)) {
+                        // AVAILABLE will already send the latest suspended status
+                        ignoreThisCallback = true;
+                        break;
+                    }
+                    // If SUSPENDED was queued, just remove it from the queue instead of sending
+                    // RESUMED; and vice-versa.
+                    final int otherCb = callbackId == CALLBACK_SUSPENDED
+                            ? CALLBACK_RESUMED
+                            : CALLBACK_SUSPENDED;
+                    ignoreThisCallback = queue.removeCallbacks(netId, otherCb);
+                    break;
+                case CALLBACK_CAP_CHANGED:
+                case CALLBACK_IP_CHANGED:
+                case CALLBACK_LOCAL_NETWORK_INFO_CHANGED:
+                case CALLBACK_BLK_CHANGED:
+                    ignoreThisCallback = queue.hasCallback(netId, CALLBACK_AVAILABLE);
+                    break;
+                default:
+                    Log.wtf(TAG, "Unexpected callback type: "
+                            + ConnectivityManager.getCallbackName(callbackId));
+                    return false;
+            }
+
+            if (!ignoreThisCallback) {
+                // For non-listen (matching) callbacks, AVAILABLE can appear in the queue twice in a
+                // row for the same network if the new AVAILABLE suppressed intermediate AVAILABLEs
+                // for other networks. Example:
+                // A is matched, app is frozen, B is matched, A is matched again (removes callbacks
+                // for B), app is unfrozen.
+                // In that case call AVAILABLE sub-callbacks to update state, but not AVAILABLE
+                // itself.
+                if (callbackId == CALLBACK_AVAILABLE && netId == mMatchedNetIdWhenFrozen) {
+                    // The queue should have been cleared here, since this is AVAILABLE on a
+                    // non-listen callback (mMatchedNetIdWhenFrozen is set).
+                    addAvailableSubCallbacks(nai, queue);
+                } else {
+                    // When unfreezing, no need to send a callback multiple times for the same netId
+                    queue.removeCallbacks(netId, callbackId);
+                    // TODO: this code always adds the callback for simplicity. It would save
+                    // some CPU/memory if the code instead only added to the queue callbacks where
+                    // isCallbackOverridden=true, or which need to be in the queue because they
+                    // affect other callbacks that are overridden.
+                    queue.addCallback(netId, callbackId);
+                }
+            }
+            // Instead of shrinking the queue, possibly reallocating, the NRI could keep the array
+            // and length in memory for future adds, but this saves memory by avoiding the cost
+            // of an extra member and of unused array length (there are often hundreds of NRIs).
+            mQueuedCallbacks = queue.getMinimizedBackingArray();
+            return true;
+        }
+
+        /**
+         * Called when this NRI is being unfrozen to stop queueing, and send queued callbacks.
+         *
+         * <p>Calling this method multiple times when the NRI is unfrozen (for example iterating
+         * through the NetworkRequest -> NRI map where there are duplicate values for multilayer
+         * requests) is fine.
+         */
+        void sendQueuedCallbacks() {
+            mMatchedNetIdWhenFrozen = MATCHED_NETID_NOT_FROZEN;
+            if (mQueuedCallbacks.length == 0) {
+                return;
+            }
+            new CallbackQueue(mQueuedCallbacks).forEach((netId, callbackId) -> {
+                // For CALLBACK_LOST only, there will not be a NAI for the netId. Build and send the
+                // callback directly.
+                if (callbackId == CALLBACK_LOST) {
+                    if (isCallbackOverridden(CALLBACK_LOST)) {
+                        final Bundle cbBundle = makeCommonBundleForCallback(this,
+                                new Network(netId));
+                        callCallbackForRequest(this, CALLBACK_LOST, cbBundle, 0 /* arg1 */);
+                    }
+                    return; // Next item in forEach
+                }
+
+                // Other callbacks should always have a NAI, because if a Network disconnects
+                // LOST will be called, unless the request is no longer satisfied by that Network in
+                // which case AVAILABLE will have been called for another Network. In both cases
+                // previous callbacks are cleared.
+                final NetworkAgentInfo nai = getNetworkAgentInfoForNetId(netId);
+                if (nai == null) {
+                    Log.wtf(TAG, "Missing NetworkAgentInfo for net " + netId
+                            + " for callback " + callbackId);
+                    return; // Next item in forEach
+                }
+
+                final int arg1 =
+                        callbackId == CALLBACK_AVAILABLE || callbackId == CALLBACK_BLK_CHANGED
+                                ? getBlockedState(nai, mAsUid)
+                                : 0;
+                callCallbackForRequest(this, nai, callbackId, arg1);
+            });
+            mQueuedCallbacks = new int[0];
+        }
+
+        boolean isCallbackOverridden(int callbackId) {
+            return !mUseDeclaredMethodsForCallbacksEnabled
+                    || (mDeclaredMethodsFlags & (1 << callbackId)) != 0;
+        }
+
+        /**
+         * Queue all callbacks that are called by AVAILABLE, except onAvailable.
+         *
+         * <p>AVAILABLE may call SUSPENDED, CAP_CHANGED, IP_CHANGED, LOCAL_NETWORK_INFO_CHANGED,
+         * and BLK_CHANGED, in this order.
+         */
+        private void addAvailableSubCallbacks(
+                @NonNull NetworkAgentInfo nai, @NonNull CallbackQueue queue) {
+            final boolean callSuspended =
+                    !nai.networkCapabilities.hasCapability(NET_CAPABILITY_NOT_SUSPENDED);
+            final boolean callLocalInfoChanged = nai.isLocalNetwork();
+
+            final int cbCount = 3 + (callSuspended ? 1 : 0) + (callLocalInfoChanged ? 1 : 0);
+            // Avoid unnecessary re-allocations by reserving enough space for all callbacks to add.
+            queue.ensureHasCapacity(cbCount);
+            final int netId = nai.network.netId;
+            if (callSuspended) {
+                queue.addCallback(netId, CALLBACK_SUSPENDED);
+            }
+            queue.addCallback(netId, CALLBACK_CAP_CHANGED);
+            queue.addCallback(netId, CALLBACK_IP_CHANGED);
+            if (callLocalInfoChanged) {
+                queue.addCallback(netId, CALLBACK_LOCAL_NETWORK_INFO_CHANGED);
+            }
+            queue.addCallback(netId, CALLBACK_BLK_CHANGED);
+        }
+
         boolean hasHigherOrderThan(@NonNull final NetworkRequestInfo target) {
             // Compare two preference orders.
             return mPreferenceOrder < target.mPreferenceOrder;
@@ -9380,8 +9672,10 @@
      * interfaces.
      * Ingress discard rule is added to the address iff
      *   1. The address is not a link local address
-     *   2. The address is used by a single VPN interface and not used by any other
-     *      interfaces even non-VPN ones
+     *   2. The address is used by a single interface of VPN whose VPN type is not TYPE_VPN_LEGACY
+     *      or TYPE_VPN_OEM and the address is not used by any other interfaces even non-VPN ones
+     * Ingress discard rule is not be added to TYPE_VPN_LEGACY or TYPE_VPN_OEM VPN since these VPNs
+     * might need to receive packet to VPN address via non-VPN interface.
      * This method can be called during network disconnects, when nai has already been removed from
      * mNetworkAgentInfos.
      *
@@ -9416,7 +9710,10 @@
         // for different network.
         final Set<Pair<InetAddress, String>> ingressDiscardRules = new ArraySet<>();
         for (final NetworkAgentInfo agent : nais) {
-            if (!agent.isVPN() || agent.isDestroyed()) {
+            final int vpnType = getVpnType(agent);
+            if (!agent.isVPN() || agent.isDestroyed()
+                    || vpnType == VpnManager.TYPE_VPN_LEGACY
+                    || vpnType == VpnManager.TYPE_VPN_OEM) {
                 continue;
             }
             final LinkProperties agentLp = (nai == agent) ? lp : agent.linkProperties;
@@ -10233,6 +10530,18 @@
         return new LocalNetworkInfo.Builder().setUpstreamNetwork(upstream).build();
     }
 
+    private Bundle makeCommonBundleForCallback(@NonNull final NetworkRequestInfo nri,
+            @Nullable Network network) {
+        final Bundle bundle = new Bundle();
+        // TODO b/177608132: make sure callbacks are indexed by NRIs and not NetworkRequest objects.
+        // TODO: check if defensive copies of data is needed.
+        putParcelable(bundle, nri.getNetworkRequestForCallback());
+        if (network != null) {
+            putParcelable(bundle, network);
+        }
+        return bundle;
+    }
+
     // networkAgent is only allowed to be null if notificationType is
     // CALLBACK_UNAVAIL. This is because UNAVAIL is about no network being
     // available, while all other cases are about some particular network.
@@ -10245,22 +10554,22 @@
             // are Type.LISTEN, but should not have NetworkCallbacks invoked.
             return;
         }
-        if (mUseDeclaredMethodsForCallbacksEnabled
-                && (nri.mDeclaredMethodsFlags & (1 << notificationType)) == 0) {
+        // Even if a callback ends up not being sent, it may affect other callbacks in the queue, so
+        // queue callbacks before checking the declared methods flags.
+        if (networkAgent != null && nri.maybeQueueCallback(networkAgent, notificationType)) {
+            return;
+        }
+        if (!nri.isCallbackOverridden(notificationType)) {
             // No need to send the notification as the recipient method is not overridden
             return;
         }
-        final Bundle bundle = new Bundle();
-        // TODO b/177608132: make sure callbacks are indexed by NRIs and not NetworkRequest objects.
-        // TODO: check if defensive copies of data is needed.
-        final NetworkRequest nrForCallback = nri.getNetworkRequestForCallback();
-        putParcelable(bundle, nrForCallback);
-        Message msg = Message.obtain();
-        if (notificationType != CALLBACK_UNAVAIL) {
-            putParcelable(bundle, networkAgent.network);
-        }
+        final Network bundleNetwork = notificationType == CALLBACK_UNAVAIL
+                ? null
+                : networkAgent.network;
+        final Bundle bundle = makeCommonBundleForCallback(nri, bundleNetwork);
         final boolean includeLocationSensitiveInfo =
                 (nri.mCallbackFlags & NetworkCallback.FLAG_INCLUDE_LOCATION_INFO) != 0;
+        final NetworkRequest nrForCallback = nri.getNetworkRequestForCallback();
         switch (notificationType) {
             case CALLBACK_AVAILABLE: {
                 final NetworkCapabilities nc =
@@ -10277,12 +10586,6 @@
                 // method here.
                 bundle.putParcelable(LocalNetworkInfo.class.getSimpleName(),
                         localNetworkInfoForNai(networkAgent));
-                // For this notification, arg1 contains the blocked status.
-                msg.arg1 = arg1;
-                break;
-            }
-            case CALLBACK_LOSING: {
-                msg.arg1 = arg1;
                 break;
             }
             case CALLBACK_CAP_CHANGED: {
@@ -10305,7 +10608,6 @@
             }
             case CALLBACK_BLK_CHANGED: {
                 maybeLogBlockedStatusChanged(nri, networkAgent.network, arg1);
-                msg.arg1 = arg1;
                 break;
             }
             case CALLBACK_LOCAL_NETWORK_INFO_CHANGED: {
@@ -10317,17 +10619,26 @@
                 break;
             }
         }
+        callCallbackForRequest(nri, notificationType, bundle, arg1);
+    }
+
+    private void callCallbackForRequest(@NonNull final NetworkRequestInfo nri, int notificationType,
+            Bundle bundle, int arg1) {
+        Message msg = Message.obtain();
+        msg.arg1 = arg1;
         msg.what = notificationType;
         msg.setData(bundle);
         try {
             if (VDBG) {
                 String notification = ConnectivityManager.getCallbackName(notificationType);
-                log("sending notification " + notification + " for " + nrForCallback);
+                log("sending notification " + notification + " for "
+                        + nri.getNetworkRequestForCallback());
             }
             nri.mMessenger.send(msg);
         } catch (RemoteException e) {
             // may occur naturally in the race of binder death.
-            loge("RemoteException caught trying to send a callback msg for " + nrForCallback);
+            loge("RemoteException caught trying to send a callback msg for "
+                    + nri.getNetworkRequestForCallback());
         }
     }
 
@@ -11416,11 +11727,7 @@
             return;
         }
 
-        final int blockedReasons = mUidBlockedReasons.get(nri.mAsUid, BLOCKED_REASON_NONE);
-        final boolean metered = nai.networkCapabilities.isMetered();
-        final boolean vpnBlocked = isUidBlockedByVpn(nri.mAsUid, mVpnBlockedUidRanges);
-        callCallbackForRequest(nri, nai, CALLBACK_AVAILABLE,
-                getBlockedState(nri.mAsUid, blockedReasons, metered, vpnBlocked));
+        callCallbackForRequest(nri, nai, CALLBACK_AVAILABLE, getBlockedState(nai, nri.mAsUid));
     }
 
     // Notify the requests on this NAI that the network is now lingered.
@@ -11450,6 +11757,13 @@
                 : reasons & ~BLOCKED_REASON_LOCKDOWN_VPN;
     }
 
+    private int getBlockedState(@NonNull NetworkAgentInfo nai, int uid) {
+        final boolean metered = nai.networkCapabilities.isMetered();
+        final boolean vpnBlocked = isUidBlockedByVpn(uid, mVpnBlockedUidRanges);
+        final int blockedReasons = mUidBlockedReasons.get(uid, BLOCKED_REASON_NONE);
+        return getBlockedState(uid, blockedReasons, metered, vpnBlocked);
+    }
+
     private void setUidBlockedReasons(int uid, @BlockedReason int blockedReasons) {
         if (blockedReasons == BLOCKED_REASON_NONE) {
             mUidBlockedReasons.delete(uid);
diff --git a/service/src/com/android/server/connectivity/ConnectivityFlags.java b/service/src/com/android/server/connectivity/ConnectivityFlags.java
index 1ee1ed7..df87316 100644
--- a/service/src/com/android/server/connectivity/ConnectivityFlags.java
+++ b/service/src/com/android/server/connectivity/ConnectivityFlags.java
@@ -49,6 +49,9 @@
     public static final String USE_DECLARED_METHODS_FOR_CALLBACKS =
             "use_declared_methods_for_callbacks";
 
+    public static final String QUEUE_CALLBACKS_FOR_FROZEN_APPS =
+            "queue_callbacks_for_frozen_apps";
+
     private boolean mNoRematchAllRequestsOnRegister;
 
     /**
diff --git a/service/src/com/android/server/connectivity/ConnectivityNativeService.java b/service/src/com/android/server/connectivity/ConnectivityNativeService.java
index cf6127f..917ad4d 100644
--- a/service/src/com/android/server/connectivity/ConnectivityNativeService.java
+++ b/service/src/com/android/server/connectivity/ConnectivityNativeService.java
@@ -23,6 +23,7 @@
 import android.os.Binder;
 import android.os.Process;
 import android.os.ServiceSpecificException;
+import android.os.UserHandle;
 import android.system.ErrnoException;
 import android.util.Log;
 
@@ -67,8 +68,8 @@
     }
 
     private void enforceBlockPortPermission() {
-        final int uid = Binder.getCallingUid();
-        if (uid == Process.ROOT_UID || uid == Process.PHONE_UID) return;
+        final int appId = UserHandle.getAppId(Binder.getCallingUid());
+        if (appId == Process.ROOT_UID || appId == Process.PHONE_UID) return;
         PermissionUtils.enforceNetworkStackPermission(mContext);
     }
 
diff --git a/service/src/com/android/server/connectivity/DnsManager.java b/service/src/com/android/server/connectivity/DnsManager.java
index ac02229..b95e3b1 100644
--- a/service/src/com/android/server/connectivity/DnsManager.java
+++ b/service/src/com/android/server/connectivity/DnsManager.java
@@ -41,6 +41,7 @@
 import android.net.NetworkCapabilities;
 import android.net.ResolverParamsParcel;
 import android.net.Uri;
+import android.net.resolv.aidl.DohParamsParcel;
 import android.net.shared.PrivateDnsConfig;
 import android.os.Binder;
 import android.os.RemoteException;
@@ -52,16 +53,17 @@
 import android.util.Pair;
 
 import java.net.InetAddress;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Iterator;
+import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
-import java.util.stream.Collectors;
 
 /**
  * Encapsulate the management of DNS settings for networks.
@@ -382,15 +384,14 @@
         paramsParcel.domains = getDomainStrings(lp.getDomains());
         paramsParcel.tlsName = strictMode ? privateDnsCfg.hostname : "";
         paramsParcel.tlsServers =
-                strictMode ? makeStrings(
-                        Arrays.stream(privateDnsCfg.ips)
-                              .filter((ip) -> lp.isReachable(ip))
-                              .collect(Collectors.toList()))
+                strictMode ? makeStrings(getReachableAddressList(privateDnsCfg.ips, lp))
                 : useTls ? paramsParcel.servers  // Opportunistic
                 : new String[0];            // Off
         paramsParcel.transportTypes = nc.getTransportTypes();
         paramsParcel.meteredNetwork = nc.isMetered();
         paramsParcel.interfaceNames = lp.getAllInterfaceNames().toArray(new String[0]);
+        paramsParcel.dohParams = makeDohParamsParcel(privateDnsCfg, lp);
+
         // Prepare to track the validation status of the DNS servers in the
         // resolver config when private DNS is in opportunistic or strict mode.
         if (useTls) {
@@ -404,15 +405,16 @@
         }
 
         Log.d(TAG, String.format("sendDnsConfigurationForNetwork(%d, %s, %s, %d, %d, %d, %d, "
-                + "%d, %d, %s, %s, %s, %b, %s)", paramsParcel.netId,
+                + "%d, %d, %s, %s, %s, %b, %s, %s, %s, %s, %d)", paramsParcel.netId,
                 Arrays.toString(paramsParcel.servers), Arrays.toString(paramsParcel.domains),
                 paramsParcel.sampleValiditySeconds, paramsParcel.successThreshold,
                 paramsParcel.minSamples, paramsParcel.maxSamples, paramsParcel.baseTimeoutMsec,
                 paramsParcel.retryCount, paramsParcel.tlsName,
                 Arrays.toString(paramsParcel.tlsServers),
                 Arrays.toString(paramsParcel.transportTypes), paramsParcel.meteredNetwork,
-                Arrays.toString(paramsParcel.interfaceNames)));
-
+                Arrays.toString(paramsParcel.interfaceNames),
+                paramsParcel.dohParams.name, Arrays.toString(paramsParcel.dohParams.ips),
+                paramsParcel.dohParams.dohpath, paramsParcel.dohParams.port));
         try {
             mDnsResolver.setResolverConfiguration(paramsParcel);
         } catch (RemoteException | ServiceSpecificException e) {
@@ -498,4 +500,26 @@
     private static String[] getDomainStrings(String domains) {
         return (TextUtils.isEmpty(domains)) ? new String[0] : domains.split(" ");
     }
+
+    @NonNull
+    private List<InetAddress> getReachableAddressList(@NonNull InetAddress[] ips,
+            @NonNull LinkProperties lp) {
+        final ArrayList<InetAddress> out = new ArrayList<InetAddress>(Arrays.asList(ips));
+        out.removeIf(ip -> !lp.isReachable(ip));
+        return out;
+    }
+
+    @NonNull
+    private DohParamsParcel makeDohParamsParcel(@NonNull PrivateDnsConfig cfg,
+            @NonNull LinkProperties lp) {
+        if (cfg.mode == PRIVATE_DNS_MODE_OFF) {
+            return new DohParamsParcel.Builder().build();
+        }
+        return new DohParamsParcel.Builder()
+                .setName(cfg.dohName)
+                .setIps(makeStrings(getReachableAddressList(cfg.dohIps, lp)))
+                .setDohpath(cfg.dohPath)
+                .setPort(cfg.dohPort)
+                .build();
+    }
 }
diff --git a/service/src/com/android/server/connectivity/NetworkDiagnostics.java b/service/src/com/android/server/connectivity/NetworkDiagnostics.java
index 3db37e5..8a2e72c 100644
--- a/service/src/com/android/server/connectivity/NetworkDiagnostics.java
+++ b/service/src/com/android/server/connectivity/NetworkDiagnostics.java
@@ -567,7 +567,9 @@
 
         @Override
         public void close() {
-            IoUtils.closeQuietly(mFileDescriptor);
+            if (mFileDescriptor != null) {
+                IoUtils.closeQuietly(mFileDescriptor);
+            }
         }
     }
 
@@ -611,6 +613,7 @@
                 setupSocket(SOCK_DGRAM, mProtocol, TIMEOUT_SEND, TIMEOUT_RECV, 0);
             } catch (ErrnoException | IOException e) {
                 mMeasurement.recordFailure(e.toString());
+                close();
                 return;
             }
             mMeasurement.description += " src{" + socketAddressToString(mSocketAddress) + "}";
@@ -695,6 +698,7 @@
                         NetworkConstants.DNS_SERVER_PORT);
             } catch (ErrnoException | IOException e) {
                 mMeasurement.recordFailure(e.toString());
+                close();
                 return;
             }
 
diff --git a/staticlibs/Android.bp b/staticlibs/Android.bp
index 2c3a558..f47a23a 100644
--- a/staticlibs/Android.bp
+++ b/staticlibs/Android.bp
@@ -58,13 +58,8 @@
         "//apex_available:platform",
     ],
     visibility: [
-        "//frameworks/base/packages/Tethering",
         "//packages/modules/Connectivity:__subpackages__",
         "//packages/modules/Connectivity/framework:__subpackages__",
-        "//frameworks/opt/net/ike",
-        "//frameworks/opt/net/wifi/service",
-        "//packages/modules/Wifi/service",
-        "//frameworks/opt/net/telephony",
         "//packages/modules/NetworkStack:__subpackages__",
         "//packages/modules/CaptivePortalLogin",
     ],
@@ -345,12 +340,10 @@
     ],
     sdk_version: "module_current",
     min_sdk_version: "30",
-    static_libs: [
-        "modules-utils-build_system",
-    ],
     libs: [
         "framework-annotations-lib",
         "framework-connectivity",
+        "modules-utils-build_system",
     ],
     // TODO: remove "apex_available:platform".
     apex_available: [
@@ -445,6 +438,7 @@
         "device/com/android/net/module/util/SharedLog.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",
         "framework/com/android/net/module/util/HexDump.java",
         "framework/com/android/net/module/util/LinkPropertiesUtils.java",
     ],
@@ -456,6 +450,7 @@
     visibility: ["//packages/modules/Connectivity/service-t"],
 }
 
+// net-utils-framework-connectivity is only for framework-connectivity.
 java_library {
     name: "net-utils-framework-connectivity",
     srcs: [
@@ -468,8 +463,7 @@
         "//apex_available:platform",
     ],
     visibility: [
-        "//packages/modules/Connectivity:__subpackages__",
-        "//packages/modules/NetworkStack:__subpackages__",
+        "//packages/modules/Connectivity/framework",
     ],
     libs: [
         "androidx.annotation_annotation",
@@ -487,7 +481,6 @@
     name: "net-utils-non-bootclasspath-defaults",
     sdk_version: "module_current",
     min_sdk_version: "30",
-    jarjar_rules: "jarjar-rules-shared.txt",
     libs: [
         "androidx.annotation_annotation",
         "framework-annotations-lib",
@@ -509,9 +502,6 @@
         "com.android.tethering",
         "//apex_available:platform",
     ],
-    visibility: [
-        "//packages/modules/Connectivity:__subpackages__",
-    ],
     defaults_visibility: [
         "//visibility:private",
     ],
@@ -521,6 +511,7 @@
     },
 }
 
+// net-utils-service-connectivity is only for service-connectivity.
 java_library {
     name: "net-utils-service-connectivity",
     srcs: [
@@ -533,15 +524,38 @@
         "net-utils-framework-connectivity",
     ],
     defaults: ["net-utils-non-bootclasspath-defaults"],
+    jarjar_rules: "jarjar-rules-shared.txt",
+    visibility: [
+        "//packages/modules/Connectivity/service",
+        "//packages/modules/Connectivity/staticlibs/tests/unit",
+    ],
 }
 
+// net-utils-connectivity-apks is only for NetworkStack, CaptivePortalLogin and
+// Tethering.apk
+// It includes all the static libraries in this directory, which is safe because
+// these APKs use R8 to strip out unused code, and they do not depend on
+// bootclasspath jars that may have duplicate copies of the included classes
+// with the same jarjaring.
+// Tethering.apk does depend on a bootclasspath jar (framework-tethering.jar),
+// however it does not use any of the static libraries. If it did, Tethering.apk
+// would need to use another variant that excludes classes that are already
+// included in framework-tethering.jar (similarly to how framework-connectivity
+// and service-connectivity do it). Otherwise, static libs included in
+// framework-tethering and Tethering.apk and jarjared the same way would
+// conflict.
 java_library {
-    name: "net-utils-tethering",
+    name: "net-utils-connectivity-apks",
     srcs: [
         ":net-utils-all-srcs",
         ":framework-connectivity-shared-srcs",
     ],
     defaults: ["net-utils-non-bootclasspath-defaults"],
+    jarjar_rules: "jarjar-rules-shared.txt",
+    visibility: [
+        "//packages/modules/CaptivePortalLogin:__subpackages__",
+        "//packages/modules/Connectivity/Tethering",
+    ],
 }
 
 aidl_interface {
@@ -556,6 +570,8 @@
             min_sdk_version: "30",
             apex_available: [
                 "com.android.tethering",
+                "com.android.wifi",
+                "//apex_available:platform",
             ],
         },
         cpp: {
@@ -593,8 +609,7 @@
     ],
 }
 
-// Use a filegroup and not a library for wifi sources, as this needs corresponding jar-jar
-// rules on the wifi side.
+// Filegroup to build lib used by Wifi framework
 // Any class here *must* have a corresponding jarjar rule in the wifi build rules.
 filegroup {
     name: "net-utils-framework-wifi-common-srcs",
@@ -606,26 +621,7 @@
         "framework/com/android/net/module/util/NetUtils.java",
     ],
     path: "framework",
-    visibility: [
-        "//frameworks/base",
-    ],
-}
-
-// Use a filegroup and not a library for wifi sources, as this needs corresponding jar-jar
-// rules on the wifi side.
-// Any class here *must* have a corresponding jarjar rule in the wifi build rules.
-filegroup {
-    name: "net-utils-wifi-service-common-srcs",
-    srcs: [
-        "device/android/net/NetworkFactory.java",
-        "device/android/net/NetworkFactoryImpl.java",
-        "device/android/net/NetworkFactoryLegacyImpl.java",
-        "device/android/net/NetworkFactoryShim.java",
-    ],
-    visibility: [
-        "//frameworks/opt/net/wifi/service",
-        "//packages/modules/Wifi/service",
-    ],
+    visibility: ["//visibility:private"],
 }
 
 // Use a file group containing classes necessary for framework-connectivity. The file group should
@@ -665,3 +661,38 @@
     path: "device",
     visibility: ["//visibility:private"],
 }
+
+java_library {
+    name: "net-utils-service-wifi",
+    srcs: [
+        ":net-utils-all-srcs",
+    ],
+    exclude_srcs: [":net-utils-framework-wifi-common-srcs"],
+    libs: [
+        "net-utils-framework-wifi",
+    ],
+    defaults: ["net-utils-non-bootclasspath-defaults"],
+
+    visibility: [
+        "//packages/modules/Wifi/service",
+    ],
+    apex_available: [
+        "com.android.wifi",
+    ],
+}
+
+java_library {
+    name: "net-utils-framework-wifi",
+    sdk_version: "module_current",
+    min_sdk_version: "30",
+    srcs: [":net-utils-framework-wifi-common-srcs"],
+    libs: [
+        "framework-annotations-lib",
+        "framework-connectivity.stubs.module_lib",
+        "unsupportedappusage",
+    ],
+    visibility: [
+        "//packages/modules/Wifi/framework",
+    ],
+    apex_available: ["com.android.wifi"],
+}
diff --git a/staticlibs/device/com/android/net/module/util/GrowingIntArray.java b/staticlibs/device/com/android/net/module/util/GrowingIntArray.java
new file mode 100644
index 0000000..d47738b
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/GrowingIntArray.java
@@ -0,0 +1,190 @@
+/*
+ * 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.annotation.NonNull;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.Arrays;
+import java.util.StringJoiner;
+import java.util.function.IntConsumer;
+import java.util.function.IntPredicate;
+
+/**
+ * A growing array of primitive ints.
+ *
+ * <p>This is similar to ArrayList&lt;Integer&gt;, but avoids the cost of boxing (each Integer costs
+ * 16 bytes) and creation / garbage collection of individual Integer objects.
+ *
+ * <p>This class does not use any heuristic for growing capacity, so every call to
+ * {@link #add(int)} may reallocate the backing array. Callers should use
+ * {@link #ensureHasCapacity(int)} to minimize this behavior when they plan to add several values.
+ */
+public class GrowingIntArray {
+    private int[] mValues;
+    private int mLength;
+
+    /**
+     * Create an empty GrowingIntArray with the given capacity.
+     */
+    public GrowingIntArray(int initialCapacity) {
+        mValues = new int[initialCapacity];
+        mLength = 0;
+    }
+
+    /**
+     * Create a GrowingIntArray with an initial array of values.
+     *
+     * <p>The array will be used as-is and may be modified, so callers must stop using it after
+     * calling this constructor.
+     */
+    protected GrowingIntArray(int[] initialValues) {
+        mValues = initialValues;
+        mLength = initialValues.length;
+    }
+
+    /**
+     * Add a value to the array.
+     */
+    public void add(int value) {
+        ensureHasCapacity(1);
+        mValues[mLength] = value;
+        mLength++;
+    }
+
+    /**
+     * Get the current number of values in the array.
+     */
+    public int length() {
+        return mLength;
+    }
+
+    /**
+     * Get the value at a given index.
+     *
+     * @throws ArrayIndexOutOfBoundsException if the index is out of bounds.
+     */
+    public int get(int index) {
+        if (index < 0 || index >= mLength) {
+            throw new ArrayIndexOutOfBoundsException(index);
+        }
+        return mValues[index];
+    }
+
+    /**
+     * Iterate over all values in the array.
+     */
+    public void forEach(@NonNull IntConsumer consumer) {
+        for (int i = 0; i < mLength; i++) {
+            consumer.accept(mValues[i]);
+        }
+    }
+
+    /**
+     * Remove all values matching a predicate.
+     *
+     * @return true if at least one value was removed.
+     */
+    public boolean removeValues(@NonNull IntPredicate predicate) {
+        int newQueueLength = 0;
+        for (int i = 0; i < mLength; i++) {
+            final int cb = mValues[i];
+            if (!predicate.test(cb)) {
+                mValues[newQueueLength] = cb;
+                newQueueLength++;
+            }
+        }
+        if (mLength != newQueueLength) {
+            mLength = newQueueLength;
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Indicates whether the array contains the given value.
+     */
+    public boolean contains(int value) {
+        for (int i = 0; i < mLength; i++) {
+            if (mValues[i] == value) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Remove all values from the array.
+     */
+    public void clear() {
+        mLength = 0;
+    }
+
+    /**
+     * Ensure at least the given number of values can be added to the array without reallocating.
+     *
+     * @param capacity The minimum number of additional values the array must be able to hold.
+     */
+    public void ensureHasCapacity(int capacity) {
+        if (mValues.length >= mLength + capacity) {
+            return;
+        }
+        mValues = Arrays.copyOf(mValues, mLength + capacity);
+    }
+
+    @VisibleForTesting
+    int getBackingArrayLength() {
+        return mValues.length;
+    }
+
+    /**
+     * Shrink the array backing this class to the minimum required length.
+     */
+    public void shrinkToLength() {
+        if (mValues.length != mLength) {
+            mValues = Arrays.copyOf(mValues, mLength);
+        }
+    }
+
+    /**
+     * Get values as array by shrinking the internal array to length and returning it.
+     *
+     * <p>This avoids reallocations if the array is already the correct length, but callers should
+     * stop using this instance of {@link GrowingIntArray} if they use the array returned by this
+     * method.
+     */
+    public int[] getMinimizedBackingArray() {
+        shrinkToLength();
+        return mValues;
+    }
+
+    /**
+     * Get the String representation of an item in the array, for use by {@link #toString()}.
+     */
+    protected String valueToString(int item) {
+        return String.valueOf(item);
+    }
+
+    @NonNull
+    @Override
+    public String toString() {
+        final StringJoiner joiner = new StringJoiner(",", "[", "]");
+        forEach(item -> joiner.add(valueToString(item)));
+        return joiner.toString();
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/RoutingCoordinatorManager.java b/staticlibs/device/com/android/net/module/util/RoutingCoordinatorManager.java
index e37061c..02e3643 100644
--- a/staticlibs/device/com/android/net/module/util/RoutingCoordinatorManager.java
+++ b/staticlibs/device/com/android/net/module/util/RoutingCoordinatorManager.java
@@ -18,12 +18,10 @@
 
 import android.content.Context;
 import android.net.RouteInfo;
-import android.os.Build;
 import android.os.IBinder;
 import android.os.RemoteException;
 
 import androidx.annotation.NonNull;
-import androidx.annotation.RequiresApi;
 
 /**
  * A manager class for talking to the routing coordinator service.
@@ -32,7 +30,6 @@
  * by the build rules. Do not change build rules to gain access to this class from elsewhere.
  * @hide
  */
-@RequiresApi(Build.VERSION_CODES.S)
 public class RoutingCoordinatorManager {
     @NonNull final Context mContext;
     @NonNull final IRoutingCoordinator mService;
diff --git a/staticlibs/device/com/android/net/module/util/SingleWriterBpfMap.java b/staticlibs/device/com/android/net/module/util/SingleWriterBpfMap.java
index cd6bfec..a638cc4 100644
--- a/staticlibs/device/com/android/net/module/util/SingleWriterBpfMap.java
+++ b/staticlibs/device/com/android/net/module/util/SingleWriterBpfMap.java
@@ -19,6 +19,7 @@
 import android.system.ErrnoException;
 import android.util.Pair;
 
+import androidx.annotation.GuardedBy;
 import androidx.annotation.NonNull;
 import androidx.annotation.RequiresApi;
 
@@ -60,6 +61,7 @@
 public class SingleWriterBpfMap<K extends Struct, V extends Struct> extends BpfMap<K, V> {
     // HashMap instead of ArrayMap because it performs better on larger maps, and many maps used in
     // our code can contain hundreds of items.
+    @GuardedBy("this")
     private final HashMap<K, V> mCache = new HashMap<>();
 
     // This should only ever be called (hence private) once for a given 'path'.
@@ -72,10 +74,12 @@
         super(path, BPF_F_RDWR_EXCLUSIVE, key, value);
 
         // Populate cache with the current map contents.
-        K currentKey = super.getFirstKey();
-        while (currentKey != null) {
-            mCache.put(currentKey, super.getValue(currentKey));
-            currentKey = super.getNextKey(currentKey);
+        synchronized (this) {
+            K currentKey = super.getFirstKey();
+            while (currentKey != null) {
+                mCache.put(currentKey, super.getValue(currentKey));
+                currentKey = super.getNextKey(currentKey);
+            }
         }
     }
 
diff --git a/staticlibs/framework/com/android/net/module/util/DnsUtils.java b/staticlibs/framework/com/android/net/module/util/DnsUtils.java
new file mode 100644
index 0000000..19ffd72
--- /dev/null
+++ b/staticlibs/framework/com/android/net/module/util/DnsUtils.java
@@ -0,0 +1,95 @@
+/*
+ * 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.annotation.NonNull;
+import android.annotation.Nullable;
+
+/**
+ * Dns common utility functions.
+ *
+ * @hide
+ */
+public class DnsUtils {
+
+    private DnsUtils() { }
+
+    /**
+     * Convert the string to DNS case-insensitive uppercase.
+     *
+     * Per rfc6762#page-46, accented characters are not defined to be automatically equivalent to
+     * their unaccented counterparts. So the "DNS uppercase" should be if character is a-z then they
+     * transform into A-Z. Otherwise, they are kept as-is.
+     */
+    public static String toDnsUpperCase(@NonNull String string) {
+        final char[] outChars = new char[string.length()];
+        for (int i = 0; i < string.length(); i++) {
+            outChars[i] = toDnsUpperCase(string.charAt(i));
+        }
+        return new String(outChars);
+    }
+
+    /**
+     * Convert the array of labels to DNS case-insensitive uppercase.
+     */
+    public static String[] toDnsLabelsUpperCase(@NonNull String[] labels) {
+        final String[] outStrings = new String[labels.length];
+        for (int i = 0; i < labels.length; ++i) {
+            outStrings[i] = toDnsUpperCase(labels[i]);
+        }
+        return outStrings;
+    }
+
+    /**
+     * Compare two strings by DNS case-insensitive uppercase.
+     */
+    public static boolean equalsIgnoreDnsCase(@Nullable String a, @Nullable String b) {
+        if (a == null || b == null) {
+            return a == null && b == null;
+        }
+        if (a.length() != b.length()) return false;
+        for (int i = 0; i < a.length(); i++) {
+            if (toDnsUpperCase(a.charAt(i)) != toDnsUpperCase(b.charAt(i))) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Compare two set of DNS labels by DNS case-insensitive uppercase.
+     */
+    public static boolean equalsDnsLabelIgnoreDnsCase(@NonNull String[] a, @NonNull String[] b) {
+        if (a == b) {
+            return true;
+        }
+        int length = a.length;
+        if (b.length != length) {
+            return false;
+        }
+        for (int i = 0; i < length; i++) {
+            if (!equalsIgnoreDnsCase(a[i], b[i])) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    private static char toDnsUpperCase(char a) {
+        return a >= 'a' && a <= 'z' ? (char) (a - ('a' - 'A')) : a;
+    }
+}
diff --git a/staticlibs/framework/com/android/net/module/util/NetworkStackConstants.java b/staticlibs/framework/com/android/net/module/util/NetworkStackConstants.java
index a8c50d8..f1ff2e4 100644
--- a/staticlibs/framework/com/android/net/module/util/NetworkStackConstants.java
+++ b/staticlibs/framework/com/android/net/module/util/NetworkStackConstants.java
@@ -260,6 +260,18 @@
     public static final short DNS_OVER_TLS_PORT = 853;
 
     /**
+     * Dns query type constants.
+     *
+     * See also:
+     *    - https://datatracker.ietf.org/doc/html/rfc1035#section-3.2.2
+     */
+    public static final int TYPE_A = 1;
+    public static final int TYPE_PTR = 12;
+    public static final int TYPE_TXT = 16;
+    public static final int TYPE_AAAA = 28;
+    public static final int TYPE_SRV = 33;
+
+    /**
      * IEEE802.11 standard constants.
      *
      * See also:
diff --git a/staticlibs/framework/com/android/net/module/util/NetworkStatsUtils.java b/staticlibs/framework/com/android/net/module/util/NetworkStatsUtils.java
index 41a9428..28ff770 100644
--- a/staticlibs/framework/com/android/net/module/util/NetworkStatsUtils.java
+++ b/staticlibs/framework/com/android/net/module/util/NetworkStatsUtils.java
@@ -16,23 +16,12 @@
 
 package com.android.net.module.util;
 
-import android.app.usage.NetworkStats;
-
-import com.android.internal.annotations.VisibleForTesting;
-
 /**
  * Various utilities used for NetworkStats related code.
  *
  * @hide
  */
 public class NetworkStatsUtils {
-    // These constants must be synced with the definition in android.net.NetworkStats.
-    // TODO: update to formal APIs once all downstreams have these APIs.
-    private static final int SET_ALL = -1;
-    private static final int METERED_ALL = -1;
-    private static final int ROAMING_ALL = -1;
-    private static final int DEFAULT_NETWORK_ALL = -1;
-
     /**
      * Safely multiple a value by a rational.
      * <p>
@@ -99,77 +88,4 @@
         if (low > high) throw new IllegalArgumentException("low(" + low + ") > high(" + high + ")");
         return amount < low ? low : (amount > high ? high : amount);
     }
-
-    /**
-     * Convert structure from android.app.usage.NetworkStats to android.net.NetworkStats.
-     */
-    public static android.net.NetworkStats fromPublicNetworkStats(
-            NetworkStats publiceNetworkStats) {
-        android.net.NetworkStats stats = new android.net.NetworkStats(0L, 0);
-        while (publiceNetworkStats.hasNextBucket()) {
-            NetworkStats.Bucket bucket = new NetworkStats.Bucket();
-            publiceNetworkStats.getNextBucket(bucket);
-            final android.net.NetworkStats.Entry entry = fromBucket(bucket);
-            stats = stats.addEntry(entry);
-        }
-        return stats;
-    }
-
-    @VisibleForTesting
-    public static android.net.NetworkStats.Entry fromBucket(NetworkStats.Bucket bucket) {
-        return new android.net.NetworkStats.Entry(
-                null /* IFACE_ALL */, bucket.getUid(), convertBucketState(bucket.getState()),
-                convertBucketTag(bucket.getTag()), convertBucketMetered(bucket.getMetered()),
-                convertBucketRoaming(bucket.getRoaming()),
-                convertBucketDefaultNetworkStatus(bucket.getDefaultNetworkStatus()),
-                bucket.getRxBytes(), bucket.getRxPackets(),
-                bucket.getTxBytes(), bucket.getTxPackets(), 0 /* operations */);
-    }
-
-    private static int convertBucketState(int networkStatsSet) {
-        switch (networkStatsSet) {
-            case NetworkStats.Bucket.STATE_ALL: return SET_ALL;
-            case NetworkStats.Bucket.STATE_DEFAULT: return android.net.NetworkStats.SET_DEFAULT;
-            case NetworkStats.Bucket.STATE_FOREGROUND:
-                return android.net.NetworkStats.SET_FOREGROUND;
-        }
-        return 0;
-    }
-
-    private static int convertBucketTag(int tag) {
-        switch (tag) {
-            case NetworkStats.Bucket.TAG_NONE: return android.net.NetworkStats.TAG_NONE;
-        }
-        return tag;
-    }
-
-    private static int convertBucketMetered(int metered) {
-        switch (metered) {
-            case NetworkStats.Bucket.METERED_ALL: return METERED_ALL;
-            case NetworkStats.Bucket.METERED_NO: return android.net.NetworkStats.METERED_NO;
-            case NetworkStats.Bucket.METERED_YES: return android.net.NetworkStats.METERED_YES;
-        }
-        return 0;
-    }
-
-    private static int convertBucketRoaming(int roaming) {
-        switch (roaming) {
-            case NetworkStats.Bucket.ROAMING_ALL: return ROAMING_ALL;
-            case NetworkStats.Bucket.ROAMING_NO: return android.net.NetworkStats.ROAMING_NO;
-            case NetworkStats.Bucket.ROAMING_YES: return android.net.NetworkStats.ROAMING_YES;
-        }
-        return 0;
-    }
-
-    private static int convertBucketDefaultNetworkStatus(int defaultNetworkStatus) {
-        switch (defaultNetworkStatus) {
-            case NetworkStats.Bucket.DEFAULT_NETWORK_ALL:
-                return DEFAULT_NETWORK_ALL;
-            case NetworkStats.Bucket.DEFAULT_NETWORK_NO:
-                return android.net.NetworkStats.DEFAULT_NETWORK_NO;
-            case NetworkStats.Bucket.DEFAULT_NETWORK_YES:
-                return android.net.NetworkStats.DEFAULT_NETWORK_YES;
-        }
-        return 0;
-    }
 }
diff --git a/staticlibs/framework/com/android/net/module/util/PerUidCounter.java b/staticlibs/framework/com/android/net/module/util/PerUidCounter.java
index 463b0c4..98d91a5 100644
--- a/staticlibs/framework/com/android/net/module/util/PerUidCounter.java
+++ b/staticlibs/framework/com/android/net/module/util/PerUidCounter.java
@@ -87,7 +87,9 @@
         }
     }
 
-    @VisibleForTesting
+    /**
+     * Get the current counter value for the given uid.
+     */
     public synchronized int get(int uid) {
         return mUidToCount.get(uid, 0);
     }
diff --git a/staticlibs/native/bpf_headers/include/bpf/BpfRingbuf.h b/staticlibs/native/bpf_headers/include/bpf/BpfRingbuf.h
index cd51004..4bcd259 100644
--- a/staticlibs/native/bpf_headers/include/bpf/BpfRingbuf.h
+++ b/staticlibs/native/bpf_headers/include/bpf/BpfRingbuf.h
@@ -20,6 +20,7 @@
 #include <android-base/unique_fd.h>
 #include <linux/bpf.h>
 #include <poll.h>
+#include <sys/epoll.h>
 #include <sys/mman.h>
 #include <utils/Log.h>
 
@@ -33,7 +34,7 @@
 // BpfRingbufBase contains the non-templated functionality of BPF ring buffers.
 class BpfRingbufBase {
  public:
-  ~BpfRingbufBase() {
+  virtual ~BpfRingbufBase() {
     if (mConsumerPos) munmap(mConsumerPos, mConsumerSize);
     if (mProducerPos) munmap(mProducerPos, mProducerSize);
     mConsumerPos = nullptr;
@@ -139,12 +140,24 @@
   static base::Result<std::unique_ptr<BpfRingbuf<Value>>> Create(
       const char* path);
 
+  int epoll_ctl_add(int epfd, struct epoll_event *event) {
+    return epoll_ctl(epfd, EPOLL_CTL_ADD, mRingFd.get(), event);
+  }
+
+  int epoll_ctl_mod(int epfd, struct epoll_event *event) {
+    return epoll_ctl(epfd, EPOLL_CTL_MOD, mRingFd.get(), event);
+  }
+
+  int epoll_ctl_del(int epfd) {
+    return epoll_ctl(epfd, EPOLL_CTL_DEL, mRingFd.get(), NULL);
+  }
+
   // Consumes all messages from the ring buffer, passing them to the callback.
   // Returns the number of messages consumed or a non-ok result on error. If the
   // ring buffer has no pending messages an OK result with count 0 is returned.
   base::Result<int> ConsumeAll(const MessageCallback& callback);
 
- private:
+ protected:
   // Empty ctor for use by Create.
   BpfRingbuf() : BpfRingbufBase(sizeof(Value)) {}
 };
diff --git a/staticlibs/native/bpf_headers/include/bpf/bpf_helpers.h b/staticlibs/native/bpf_headers/include/bpf/bpf_helpers.h
index 4ddec8b..c94f1d8 100644
--- a/staticlibs/native/bpf_headers/include/bpf/bpf_helpers.h
+++ b/staticlibs/native/bpf_headers/include/bpf/bpf_helpers.h
@@ -7,7 +7,7 @@
 #include "bpf_map_def.h"
 
 /******************************************************************************
- * WARNING: CHANGES TO THIS FILE OUTSIDE OF AOSP/MASTER ARE LIKELY TO BREAK   *
+ * WARNING: CHANGES TO THIS FILE OUTSIDE OF AOSP/MAIN ARE LIKELY TO BREAK     *
  * DEVICE COMPATIBILITY WITH MAINLINE MODULES SHIPPING EBPF CODE.             *
  *                                                                            *
  * THIS WILL LIKELY RESULT IN BRICKED DEVICES AT SOME ARBITRARY FUTURE TIME   *
@@ -71,11 +71,11 @@
  * In which case it's just best to use the default.
  */
 #ifndef BPFLOADER_MIN_VER
-#define BPFLOADER_MIN_VER BPFLOADER_PLATFORM_VERSION
+#define BPFLOADER_MIN_VER BPFLOADER_PLATFORM_VERSION  // inclusive, ie. >=
 #endif
 
 #ifndef BPFLOADER_MAX_VER
-#define BPFLOADER_MAX_VER DEFAULT_BPFLOADER_MAX_VER
+#define BPFLOADER_MAX_VER 0x10000u  // exclusive, ie. < v1.0
 #endif
 
 /* place things in different elf sections */
@@ -99,23 +99,19 @@
  *
  * If missing, bpfloader_{min/max}_ver default to 0/0x10000 ie. [v0.0, v1.0),
  * while size_of_bpf_{map/prog}_def default to 32/20 which are the v0.0 sizes.
- */
-#define LICENSE(NAME)                                                                           \
-    unsigned int _bpfloader_min_ver SECTION("bpfloader_min_ver") = BPFLOADER_MIN_VER;           \
-    unsigned int _bpfloader_max_ver SECTION("bpfloader_max_ver") = BPFLOADER_MAX_VER;           \
-    size_t _size_of_bpf_map_def SECTION("size_of_bpf_map_def") = sizeof(struct bpf_map_def);    \
-    size_t _size_of_bpf_prog_def SECTION("size_of_bpf_prog_def") = sizeof(struct bpf_prog_def); \
-    char _license[] SECTION("license") = (NAME)
-
-/* This macro disables loading BTF map debug information on Android <=U *and* all user builds.
  *
- * Note: Bpfloader v0.39+ honours 'btf_user_min_bpfloader_ver' on user builds,
- * and 'btf_min_bpfloader_ver' on non-user builds.
- * Older BTF capable versions unconditionally honour 'btf_min_bpfloader_ver'
+ * This macro also disables loading BTF map debug information, as versions
+ * of the platform bpfloader that support BTF require fork-exec of btfloader
+ * which causes a regression in boot time.
  */
-#define DISABLE_BTF_ON_USER_BUILDS() \
-    unsigned _btf_min_bpfloader_ver SECTION("btf_min_bpfloader_ver") = 39u; \
-    unsigned _btf_user_min_bpfloader_ver SECTION("btf_user_min_bpfloader_ver") = 0xFFFFFFFFu
+#define LICENSE(NAME)                                                                              \
+    unsigned int _bpfloader_min_ver SECTION("bpfloader_min_ver") = BPFLOADER_MIN_VER;              \
+    unsigned int _bpfloader_max_ver SECTION("bpfloader_max_ver") = BPFLOADER_MAX_VER;              \
+    size_t _size_of_bpf_map_def SECTION("size_of_bpf_map_def") = sizeof(struct bpf_map_def);       \
+    size_t _size_of_bpf_prog_def SECTION("size_of_bpf_prog_def") = sizeof(struct bpf_prog_def);    \
+    unsigned _btf_min_bpfloader_ver SECTION("btf_min_bpfloader_ver") = BPFLOADER_MAINLINE_VERSION; \
+    unsigned _btf_user_min_bpfloader_ver SECTION("btf_user_min_bpfloader_ver") = 0xFFFFFFFFu;      \
+    char _license[] SECTION("license") = (NAME)
 
 /* flag the resulting bpf .o file as critical to system functionality,
  * loading all kernel version appropriate programs in it must succeed
@@ -414,19 +410,10 @@
     SECTION(SECTION_NAME)                                                                \
     int the_prog
 
-#ifndef DEFAULT_BPF_PROG_SELINUX_CONTEXT
-#define DEFAULT_BPF_PROG_SELINUX_CONTEXT ""
-#endif
-
-#ifndef DEFAULT_BPF_PROG_PIN_SUBDIR
-#define DEFAULT_BPF_PROG_PIN_SUBDIR ""
-#endif
-
 #define DEFINE_BPF_PROG_KVER_RANGE_OPT(SECTION_NAME, prog_uid, prog_gid, the_prog, min_kv, max_kv, \
                                        opt)                                                        \
     DEFINE_BPF_PROG_EXT(SECTION_NAME, prog_uid, prog_gid, the_prog, min_kv, max_kv,                \
-                        BPFLOADER_MIN_VER, BPFLOADER_MAX_VER, opt,                                 \
-                        DEFAULT_BPF_PROG_SELINUX_CONTEXT, DEFAULT_BPF_PROG_PIN_SUBDIR,             \
+                        BPFLOADER_MIN_VER, BPFLOADER_MAX_VER, opt, "", "",                         \
                         LOAD_ON_ENG, LOAD_ON_USER, LOAD_ON_USERDEBUG)
 
 // Programs (here used in the sense of functions/sections) marked optional are allowed to fail
diff --git a/staticlibs/native/bpf_headers/include/bpf/bpf_map_def.h b/staticlibs/native/bpf_headers/include/bpf/bpf_map_def.h
index 00ef91a..2d6736c 100644
--- a/staticlibs/native/bpf_headers/include/bpf/bpf_map_def.h
+++ b/staticlibs/native/bpf_headers/include/bpf/bpf_map_def.h
@@ -29,7 +29,7 @@
  *                                                                            *
  *                          ! ! ! W A R N I N G ! ! !                         *
  *                                                                            *
- * CHANGES TO THESE STRUCTURE DEFINITIONS OUTSIDE OF AOSP/MASTER *WILL* BREAK *
+ * CHANGES TO THESE STRUCTURE DEFINITIONS OUTSIDE OF AOSP/MAIN *WILL* BREAK   *
  * MAINLINE MODULE COMPATIBILITY                                              *
  *                                                                            *
  * AND THUS MAY RESULT IN YOUR DEVICE BRICKING AT SOME ARBITRARY POINT IN     *
@@ -42,12 +42,6 @@
  *                                                                            *
  ******************************************************************************/
 
-// These are the values used if these fields are missing
-#define DEFAULT_BPFLOADER_MIN_VER 0u        // v0.0 (this is inclusive ie. >= v0.0)
-#define DEFAULT_BPFLOADER_MAX_VER 0x10000u  // v1.0 (this is exclusive ie. < v1.0)
-#define DEFAULT_SIZEOF_BPF_MAP_DEF 32       // v0.0 struct: enum (uint sized) + 7 uint
-#define DEFAULT_SIZEOF_BPF_PROG_DEF 20      // v0.0 struct: 4 uint + bool + 3 byte alignment pad
-
 /*
  * The bpf_{map,prog}_def structures are compiled for different architectures.
  * Once by the BPF compiler for the BPF architecture, and once by a C++
diff --git a/staticlibs/netd/libnetdutils/include/netdutils/Log.h b/staticlibs/netd/libnetdutils/include/netdutils/Log.h
index d266cbc..2de5ed7 100644
--- a/staticlibs/netd/libnetdutils/include/netdutils/Log.h
+++ b/staticlibs/netd/libnetdutils/include/netdutils/Log.h
@@ -203,7 +203,7 @@
     void record(Level lvl, const std::string& entry);
 
     mutable std::shared_mutex mLock;
-    std::deque<const std::string> mEntries;  // GUARDED_BY(mLock), when supported
+    std::deque<std::string> mEntries;  // GUARDED_BY(mLock), when supported
 };
 
 }  // namespace netdutils
diff --git a/staticlibs/tests/unit/host/python/apf_utils_test.py b/staticlibs/tests/unit/host/python/apf_utils_test.py
index 8b390e3..b5a941b 100644
--- a/staticlibs/tests/unit/host/python/apf_utils_test.py
+++ b/staticlibs/tests/unit/host/python/apf_utils_test.py
@@ -13,23 +13,29 @@
 #  limitations under the License.
 
 from unittest.mock import MagicMock, patch
+from absl.testing import parameterized
 from mobly import asserts
 from mobly import base_test
 from mobly import config_parser
 from mobly.controllers.android_device_lib.adb import AdbError
 from net_tests_utils.host.python.apf_utils import (
+    ApfCapabilities,
     PatternNotFoundException,
     UnsupportedOperationException,
+    get_apf_capabilities,
     get_apf_counter,
     get_apf_counters_from_dumpsys,
     get_hardware_address,
-    send_broadcast_empty_ethercat_packet,
+    is_send_raw_packet_downstream_supported,
     send_raw_packet_downstream,
 )
 from net_tests_utils.host.python.assert_utils import UnexpectedBehaviorError
 
+TEST_IFACE_NAME = "eth0"
+TEST_PACKET_IN_HEX = "AABBCCDDEEFF"
 
-class TestApfUtils(base_test.BaseTestClass):
+
+class TestApfUtils(base_test.BaseTestClass, parameterized.TestCase):
 
   def __init__(self, configs: config_parser.TestRunConfig):
     super().__init__(configs)
@@ -105,30 +111,18 @@
     with asserts.assert_raises(PatternNotFoundException):
       get_hardware_address(self.mock_ad, "wlan0")
 
-  @patch("net_tests_utils.host.python.apf_utils.get_hardware_address")
-  @patch("net_tests_utils.host.python.apf_utils.send_raw_packet_downstream")
-  def test_send_broadcast_empty_ethercat_packet(
-      self,
-      mock_send_raw_packet_downstream: MagicMock,
-      mock_get_hardware_address: MagicMock,
-  ) -> None:
-    mock_get_hardware_address.return_value = "12:34:56:78:90:AB"
-    send_broadcast_empty_ethercat_packet(self.mock_ad, "eth0")
-    # Assuming you'll mock the packet construction part, verify calls to send_raw_packet_downstream.
-    mock_send_raw_packet_downstream.assert_called_once()
-
   @patch("net_tests_utils.host.python.adb_utils.adb_shell")
   def test_send_raw_packet_downstream_success(
       self, mock_adb_shell: MagicMock
   ) -> None:
     mock_adb_shell.return_value = ""  # Successful command output
-    iface_name = "eth0"
-    packet_in_hex = "AABBCCDDEEFF"
-    send_raw_packet_downstream(self.mock_ad, iface_name, packet_in_hex)
+    send_raw_packet_downstream(
+        self.mock_ad, TEST_IFACE_NAME, TEST_PACKET_IN_HEX
+    )
     mock_adb_shell.assert_called_once_with(
         self.mock_ad,
         "cmd network_stack send-raw-packet-downstream"
-        f" {iface_name} {packet_in_hex}",
+        f" {TEST_IFACE_NAME} {TEST_PACKET_IN_HEX}",
     )
 
   @patch("net_tests_utils.host.python.adb_utils.adb_shell")
@@ -139,7 +133,13 @@
         "Any Unexpected Output"
     )
     with asserts.assert_raises(UnexpectedBehaviorError):
-      send_raw_packet_downstream(self.mock_ad, "eth0", "AABBCCDDEEFF")
+      send_raw_packet_downstream(
+          self.mock_ad, TEST_IFACE_NAME, TEST_PACKET_IN_HEX
+      )
+    asserts.assert_true(
+        is_send_raw_packet_downstream_supported(self.mock_ad),
+        "Send raw packet should be supported.",
+    )
 
   @patch("net_tests_utils.host.python.adb_utils.adb_shell")
   def test_send_raw_packet_downstream_unsupported(
@@ -149,4 +149,30 @@
         cmd="", stdout="Unknown command", stderr="", ret_code=3
     )
     with asserts.assert_raises(UnsupportedOperationException):
-      send_raw_packet_downstream(self.mock_ad, "eth0", "AABBCCDDEEFF")
+      send_raw_packet_downstream(
+          self.mock_ad, TEST_IFACE_NAME, TEST_PACKET_IN_HEX
+      )
+    asserts.assert_false(
+        is_send_raw_packet_downstream_supported(self.mock_ad),
+        "Send raw packet should not be supported.",
+    )
+
+  @parameterized.parameters(
+      ("2,2048,1", ApfCapabilities(2, 2048, 1)),  # Valid input
+      ("3,1024,0", ApfCapabilities(3, 1024, 0)),  # Valid input
+      ("invalid,output", ApfCapabilities(0, 0, 0)),  # Invalid input
+      ("", ApfCapabilities(0, 0, 0)),  # Empty input
+  )
+  @patch("net_tests_utils.host.python.adb_utils.adb_shell")
+  def test_get_apf_capabilities(
+      self, mock_output, expected_result, mock_adb_shell
+  ):
+    """Tests the get_apf_capabilities function with various inputs and expected results."""
+    # Configure the mock adb_shell to return the specified output
+    mock_adb_shell.return_value = mock_output
+
+    # Call the function under test
+    result = get_apf_capabilities(self.mock_ad, "wlan0")
+
+    # Assert that the result matches the expected result
+    asserts.assert_equal(result, expected_result)
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/DnsUtilsTest.kt b/staticlibs/tests/unit/src/com/android/net/module/util/DnsUtilsTest.kt
new file mode 100644
index 0000000..7b1f08a
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/DnsUtilsTest.kt
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License")
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util
+
+import com.android.net.module.util.DnsUtils.equalsDnsLabelIgnoreDnsCase
+import com.android.net.module.util.DnsUtils.equalsIgnoreDnsCase
+import com.android.net.module.util.DnsUtils.toDnsLabelsUpperCase
+import com.android.net.module.util.DnsUtils.toDnsUpperCase
+import com.android.testutils.DevSdkIgnoreRunner
+import org.junit.Assert.assertArrayEquals
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(DevSdkIgnoreRunner::class)
+class DnsUtilsTest {
+    @Test
+    fun testToDnsUpperCase() {
+        assertEquals("TEST", toDnsUpperCase("TEST"))
+        assertEquals("TEST", toDnsUpperCase("TeSt"))
+        assertEquals("TEST", toDnsUpperCase("test"))
+        assertEquals("TÉST", toDnsUpperCase("TÉST"))
+        assertEquals("ţéST", toDnsUpperCase("ţést"))
+        // Unicode characters 0x10000 (𐀀), 0x10001 (𐀁), 0x10041 (𐁁)
+        // Note the last 2 bytes of 0x10041 are identical to 'A', but it should remain unchanged.
+        assertEquals(
+            "TEST: -->\ud800\udc00 \ud800\udc01 \ud800\udc41<-- ",
+                toDnsUpperCase("Test: -->\ud800\udc00 \ud800\udc01 \ud800\udc41<-- ")
+        )
+        // Also test some characters where the first surrogate is not \ud800
+        assertEquals(
+            "TEST: >\ud83c\udff4\udb40\udc67\udb40\udc62\udb40" +
+                "\udc77\udb40\udc6c\udb40\udc73\udb40\udc7f<",
+                toDnsUpperCase(
+                    "Test: >\ud83c\udff4\udb40\udc67\udb40\udc62\udb40" +
+                        "\udc77\udb40\udc6c\udb40\udc73\udb40\udc7f<"
+                )
+        )
+    }
+
+    @Test
+    fun testToDnsLabelsUpperCase() {
+        assertArrayEquals(
+            arrayOf("TEST", "TÉST", "ţéST"),
+            toDnsLabelsUpperCase(arrayOf("TeSt", "TÉST", "ţést"))
+        )
+    }
+
+    @Test
+    fun testEqualsIgnoreDnsCase() {
+        assertTrue(equalsIgnoreDnsCase("TEST", "Test"))
+        assertTrue(equalsIgnoreDnsCase("TEST", "test"))
+        assertTrue(equalsIgnoreDnsCase("test", "TeSt"))
+        assertTrue(equalsIgnoreDnsCase("Tést", "tést"))
+        assertFalse(equalsIgnoreDnsCase("ŢÉST", "ţést"))
+        // Unicode characters 0x10000 (𐀀), 0x10001 (𐀁), 0x10041 (𐁁)
+        // Note the last 2 bytes of 0x10041 are identical to 'A', but it should remain unchanged.
+        assertTrue(equalsIgnoreDnsCase(
+                "test: -->\ud800\udc00 \ud800\udc01 \ud800\udc41<-- ",
+                "Test: -->\ud800\udc00 \ud800\udc01 \ud800\udc41<-- "
+        ))
+        // Also test some characters where the first surrogate is not \ud800
+        assertTrue(equalsIgnoreDnsCase(
+                "test: >\ud83c\udff4\udb40\udc67\udb40\udc62\udb40" +
+                        "\udc77\udb40\udc6c\udb40\udc73\udb40\udc7f<",
+                "Test: >\ud83c\udff4\udb40\udc67\udb40\udc62\udb40" +
+                        "\udc77\udb40\udc6c\udb40\udc73\udb40\udc7f<"
+        ))
+    }
+
+    @Test
+    fun testEqualsLabelIgnoreDnsCase() {
+        assertTrue(equalsDnsLabelIgnoreDnsCase(arrayOf("TEST", "Test"), arrayOf("test", "test")))
+        assertFalse(equalsDnsLabelIgnoreDnsCase(arrayOf("TEST", "Test"), arrayOf("test")))
+        assertFalse(equalsDnsLabelIgnoreDnsCase(arrayOf("Test"), arrayOf("test", "test")))
+        assertFalse(equalsDnsLabelIgnoreDnsCase(arrayOf("TEST", "Test"), arrayOf("test", "tést")))
+    }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/GrowingIntArrayTest.kt b/staticlibs/tests/unit/src/com/android/net/module/util/GrowingIntArrayTest.kt
new file mode 100644
index 0000000..4b740e3
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/GrowingIntArrayTest.kt
@@ -0,0 +1,129 @@
+/*
+ * 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 androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import kotlin.test.assertContentEquals
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+import kotlin.test.fail
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class GrowingIntArrayTest {
+    @Test
+    fun testAddAndGet() {
+        val array = GrowingIntArray(1)
+        array.add(-1)
+        array.add(0)
+        array.add(2)
+
+        assertEquals(-1, array.get(0))
+        assertEquals(0, array.get(1))
+        assertEquals(2, array.get(2))
+        assertEquals(3, array.length())
+    }
+
+    @Test
+    fun testForEach() {
+        val array = GrowingIntArray(10)
+        array.add(-1)
+        array.add(0)
+        array.add(2)
+
+        val actual = mutableListOf<Int>()
+        array.forEach { actual.add(it) }
+
+        val expected = listOf(-1, 0, 2)
+        assertEquals(expected, actual)
+    }
+
+    @Test
+    fun testForEach_EmptyArray() {
+        val array = GrowingIntArray(10)
+        array.forEach {
+            fail("This should not be called")
+        }
+    }
+
+    @Test
+    fun testRemoveValues() {
+        val array = GrowingIntArray(10)
+        array.add(-1)
+        array.add(0)
+        array.add(2)
+
+        array.removeValues { it <= 0 }
+        assertEquals(1, array.length())
+        assertEquals(2, array.get(0))
+    }
+
+    @Test
+    fun testContains() {
+        val array = GrowingIntArray(10)
+        array.add(-1)
+        array.add(2)
+
+        assertTrue(array.contains(-1))
+        assertTrue(array.contains(2))
+
+        assertFalse(array.contains(0))
+        assertFalse(array.contains(3))
+    }
+
+    @Test
+    fun testClear() {
+        val array = GrowingIntArray(10)
+        array.add(-1)
+        array.add(2)
+        array.clear()
+
+        assertEquals(0, array.length())
+    }
+
+    @Test
+    fun testEnsureHasCapacity() {
+        val array = GrowingIntArray(0)
+        array.add(42)
+        array.ensureHasCapacity(2)
+
+        assertEquals(3, array.backingArrayLength)
+    }
+
+    @Test
+    fun testGetMinimizedBackingArray() {
+        val array = GrowingIntArray(10)
+        array.add(-1)
+        array.add(2)
+
+        assertContentEquals(intArrayOf(-1, 2), array.minimizedBackingArray)
+    }
+
+    @Test
+    fun testToString() {
+        assertEquals("[]", GrowingIntArray(10).toString())
+        assertEquals("[1,2,3]", GrowingIntArray(3).apply {
+            add(1)
+            add(2)
+            add(3)
+        }.toString())
+    }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/NetworkStatsUtilsTest.kt b/staticlibs/tests/unit/src/com/android/net/module/util/NetworkStatsUtilsTest.kt
index 2785ea9..9981b6a 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/NetworkStatsUtilsTest.kt
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/NetworkStatsUtilsTest.kt
@@ -16,16 +16,12 @@
 
 package com.android.net.module.util
 
-import android.net.NetworkStats
-import android.text.TextUtils
 import androidx.test.filters.SmallTest
 import androidx.test.runner.AndroidJUnit4
-import org.junit.Test
-import org.junit.runner.RunWith
 import kotlin.test.assertEquals
 import kotlin.test.assertFailsWith
-import org.mockito.Mockito.doReturn
-import org.mockito.Mockito.mock
+import org.junit.Test
+import org.junit.runner.RunWith
 
 @RunWith(AndroidJUnit4::class)
 @SmallTest
@@ -75,68 +71,4 @@
         assertEquals(11, NetworkStatsUtils.constrain(11, 11, 11))
         assertEquals(11, NetworkStatsUtils.constrain(1, 11, 11))
     }
-
-    @Test
-    fun testBucketToEntry() {
-        val bucket = makeMockBucket(android.app.usage.NetworkStats.Bucket.UID_ALL,
-                android.app.usage.NetworkStats.Bucket.TAG_NONE,
-                android.app.usage.NetworkStats.Bucket.STATE_DEFAULT,
-                android.app.usage.NetworkStats.Bucket.METERED_YES,
-                android.app.usage.NetworkStats.Bucket.ROAMING_NO,
-                android.app.usage.NetworkStats.Bucket.DEFAULT_NETWORK_ALL, 1024, 8, 2048, 12)
-        val entry = NetworkStatsUtils.fromBucket(bucket)
-        val expectedEntry = NetworkStats.Entry(null /* IFACE_ALL */, NetworkStats.UID_ALL,
-            NetworkStats.SET_DEFAULT, NetworkStats.TAG_NONE, NetworkStats.METERED_YES,
-            NetworkStats.ROAMING_NO, NetworkStats.DEFAULT_NETWORK_ALL, 1024, 8, 2048, 12,
-            0 /* operations */)
-
-        // TODO: Use assertEquals once all downstreams accept null iface in
-        // NetworkStats.Entry#equals.
-        assertEntryEquals(expectedEntry, entry)
-    }
-
-    private fun makeMockBucket(
-        uid: Int,
-        tag: Int,
-        state: Int,
-        metered: Int,
-        roaming: Int,
-        defaultNetwork: Int,
-        rxBytes: Long,
-        rxPackets: Long,
-        txBytes: Long,
-        txPackets: Long
-    ): android.app.usage.NetworkStats.Bucket {
-        val ret: android.app.usage.NetworkStats.Bucket =
-                mock(android.app.usage.NetworkStats.Bucket::class.java)
-        doReturn(uid).`when`(ret).getUid()
-        doReturn(tag).`when`(ret).getTag()
-        doReturn(state).`when`(ret).getState()
-        doReturn(metered).`when`(ret).getMetered()
-        doReturn(roaming).`when`(ret).getRoaming()
-        doReturn(defaultNetwork).`when`(ret).getDefaultNetworkStatus()
-        doReturn(rxBytes).`when`(ret).getRxBytes()
-        doReturn(rxPackets).`when`(ret).getRxPackets()
-        doReturn(txBytes).`when`(ret).getTxBytes()
-        doReturn(txPackets).`when`(ret).getTxPackets()
-        return ret
-    }
-
-    /**
-     * Assert that the two {@link NetworkStats.Entry} are equals.
-     */
-    private fun assertEntryEquals(left: NetworkStats.Entry, right: NetworkStats.Entry) {
-        TextUtils.equals(left.iface, right.iface)
-        assertEquals(left.uid, right.uid)
-        assertEquals(left.set, right.set)
-        assertEquals(left.tag, right.tag)
-        assertEquals(left.metered, right.metered)
-        assertEquals(left.roaming, right.roaming)
-        assertEquals(left.defaultNetwork, right.defaultNetwork)
-        assertEquals(left.rxBytes, right.rxBytes)
-        assertEquals(left.rxPackets, right.rxPackets)
-        assertEquals(left.txBytes, right.txBytes)
-        assertEquals(left.txPackets, right.txPackets)
-        assertEquals(left.operations, right.operations)
-    }
 }
\ No newline at end of file
diff --git a/staticlibs/tests/unit/src/com/android/testutils/DefaultNetworkRestoreMonitorTest.kt b/staticlibs/tests/unit/src/com/android/testutils/DefaultNetworkRestoreMonitorTest.kt
new file mode 100644
index 0000000..7e508fb
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/testutils/DefaultNetworkRestoreMonitorTest.kt
@@ -0,0 +1,167 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.testutils
+
+import android.content.Context
+import android.content.pm.PackageManager
+import android.net.ConnectivityManager
+import android.net.ConnectivityManager.NetworkCallback
+import android.net.Network
+import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.TRANSPORT_CELLULAR
+import android.net.NetworkCapabilities.TRANSPORT_WIFI
+import org.junit.Test
+import org.junit.runner.Description
+import org.junit.runner.notification.RunListener
+import org.junit.runner.notification.RunNotifier
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.anyString
+import org.mockito.ArgumentMatchers.argThat
+import org.mockito.Mockito.doAnswer
+import org.mockito.Mockito.doNothing
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.inOrder
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+
+class DefaultNetworkRestoreMonitorTest {
+    private val restoreDefaultNetworkDesc =
+            Description.createSuiteDescription("RestoreDefaultNetwork")
+    private val testDesc = Description.createTestDescription("testClass", "testMethod")
+    private val wifiCap = NetworkCapabilities.Builder()
+            .addTransportType(TRANSPORT_WIFI)
+            .addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
+            .build()
+    private val cellCap = NetworkCapabilities.Builder()
+            .addTransportType(TRANSPORT_CELLULAR)
+            .addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
+            .build()
+    private val cm = mock(ConnectivityManager::class.java)
+    private val pm = mock(PackageManager::class.java).also {
+        doReturn(true).`when`(it).hasSystemFeature(anyString())
+    }
+    private val ctx = mock(Context::class.java).also {
+        doReturn(cm).`when`(it).getSystemService(ConnectivityManager::class.java)
+        doReturn(pm).`when`(it).getPackageManager()
+    }
+    private val notifier = mock(RunNotifier::class.java)
+    private val defaultNetworkMonitor = DefaultNetworkRestoreMonitor(
+        ctx,
+        notifier,
+        timeoutMs = 0
+    )
+
+    private fun getRunListener(): RunListener {
+        val captor = ArgumentCaptor.forClass(RunListener::class.java)
+        verify(notifier).addListener(captor.capture())
+        return captor.value
+    }
+
+    private fun mockDefaultNetworkCapabilities(cap: NetworkCapabilities?) {
+        if (cap == null) {
+            doNothing().`when`(cm).registerDefaultNetworkCallback(any())
+            return
+        }
+        doAnswer {
+            val callback = it.getArgument(0) as NetworkCallback
+            callback.onCapabilitiesChanged(Network(100), cap)
+        }.`when`(cm).registerDefaultNetworkCallback(any())
+    }
+
+    @Test
+    fun testDefaultNetworkRestoreMonitor_defaultNetworkRestored() {
+        mockDefaultNetworkCapabilities(wifiCap)
+        defaultNetworkMonitor.init(mock(ConnectUtil::class.java))
+
+        val listener = getRunListener()
+        listener.testFinished(testDesc)
+
+        defaultNetworkMonitor.reportResultAndCleanUp(restoreDefaultNetworkDesc)
+        val inOrder = inOrder(notifier)
+        inOrder.verify(notifier).fireTestStarted(restoreDefaultNetworkDesc)
+        inOrder.verify(notifier, never()).fireTestFailure(any())
+        inOrder.verify(notifier).fireTestFinished(restoreDefaultNetworkDesc)
+        inOrder.verify(notifier).removeListener(listener)
+    }
+
+    @Test
+    fun testDefaultNetworkRestoreMonitor_testStartWithoutDefaultNetwork() {
+        // There is no default network when the tests start
+        mockDefaultNetworkCapabilities(null)
+        defaultNetworkMonitor.init(mock(ConnectUtil::class.java))
+
+        mockDefaultNetworkCapabilities(wifiCap)
+        val listener = getRunListener()
+        listener.testFinished(testDesc)
+
+        defaultNetworkMonitor.reportResultAndCleanUp(restoreDefaultNetworkDesc)
+        val inOrder = inOrder(notifier)
+        inOrder.verify(notifier).fireTestStarted(restoreDefaultNetworkDesc)
+        // fireTestFailure is called
+        inOrder.verify(notifier).fireTestFailure(any())
+        inOrder.verify(notifier).fireTestFinished(restoreDefaultNetworkDesc)
+        inOrder.verify(notifier).removeListener(listener)
+    }
+
+    @Test
+    fun testDefaultNetworkRestoreMonitor_testEndWithoutDefaultNetwork() {
+        mockDefaultNetworkCapabilities(wifiCap)
+        defaultNetworkMonitor.init(mock(ConnectUtil::class.java))
+
+        // There is no default network after the test
+        mockDefaultNetworkCapabilities(null)
+        val listener = getRunListener()
+        listener.testFinished(testDesc)
+
+        defaultNetworkMonitor.reportResultAndCleanUp(restoreDefaultNetworkDesc)
+        val inOrder = inOrder(notifier)
+        inOrder.verify(notifier).fireTestStarted(restoreDefaultNetworkDesc)
+        // fireTestFailure is called with method name
+        inOrder.verify(
+                notifier
+        ).fireTestFailure(
+                argThat{failure -> failure.exception.message?.contains("testMethod") ?: false}
+        )
+        inOrder.verify(notifier).fireTestFinished(restoreDefaultNetworkDesc)
+        inOrder.verify(notifier).removeListener(listener)
+    }
+
+    @Test
+    fun testDefaultNetworkRestoreMonitor_testChangeDefaultNetwork() {
+        mockDefaultNetworkCapabilities(wifiCap)
+        defaultNetworkMonitor.init(mock(ConnectUtil::class.java))
+
+        // The default network transport types change after the test
+        mockDefaultNetworkCapabilities(cellCap)
+        val listener = getRunListener()
+        listener.testFinished(testDesc)
+
+        defaultNetworkMonitor.reportResultAndCleanUp(restoreDefaultNetworkDesc)
+        val inOrder = inOrder(notifier)
+        inOrder.verify(notifier).fireTestStarted(restoreDefaultNetworkDesc)
+        // fireTestFailure is called with method name
+        inOrder.verify(
+                notifier
+        ).fireTestFailure(
+                argThat{failure -> failure.exception.message?.contains("testMethod") ?: false}
+        )
+        inOrder.verify(notifier).fireTestFinished(restoreDefaultNetworkDesc)
+        inOrder.verify(notifier).removeListener(listener)
+    }
+}
diff --git a/staticlibs/tests/unit/src/com/android/testutils/NetworkStatsUtilsTest.kt b/staticlibs/tests/unit/src/com/android/testutils/NetworkStatsUtilsTest.kt
new file mode 100644
index 0000000..57920fc
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/testutils/NetworkStatsUtilsTest.kt
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.testutils
+
+import android.net.NetworkStats
+import android.net.NetworkStats.DEFAULT_NETWORK_NO
+import android.net.NetworkStats.METERED_NO
+import android.net.NetworkStats.ROAMING_NO
+import android.net.NetworkStats.SET_DEFAULT
+import android.net.NetworkStats.TAG_NONE
+import android.os.Build
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+private const val TEST_IFACE = "test0"
+private val TEST_IFACE2: String? = null
+private const val TEST_START = 1194220800000L
+
+@RunWith(JUnit4::class)
+class NetworkStatsUtilsTest {
+    // This is a unit test for a test utility that uses R APIs
+    @Rule @JvmField
+    val ignoreRule = DevSdkIgnoreRule(ignoreClassUpTo = Build.VERSION_CODES.Q)
+
+    @Test
+    fun testOrderInsensitiveEquals() {
+        val testEntry = arrayOf(
+                NetworkStats.Entry(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+                        DEFAULT_NETWORK_NO, 128L, 8L, 0L, 2L, 20L),
+                NetworkStats.Entry(TEST_IFACE2, 100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+                        DEFAULT_NETWORK_NO, 512L, 32L, 0L, 0L, 0L)
+        )
+
+        // Verify equals of empty stats regardless of initial capacity.
+        val red = NetworkStats(TEST_START, 0)
+        val blue = NetworkStats(TEST_START, 1)
+        assertTrue(orderInsensitiveEquals(red, blue))
+        assertTrue(orderInsensitiveEquals(blue, red))
+
+        // Verify not equal.
+        red.combineValues(testEntry[1])
+        blue.combineValues(testEntry[0]).combineValues(testEntry[1])
+        assertFalse(orderInsensitiveEquals(red, blue))
+        assertFalse(orderInsensitiveEquals(blue, red))
+
+        // Verify equals even if the order of entries are not the same.
+        red.combineValues(testEntry[0])
+        assertTrue(orderInsensitiveEquals(red, blue))
+        assertTrue(orderInsensitiveEquals(blue, red))
+    }
+}
diff --git a/staticlibs/testutils/Android.bp b/staticlibs/testutils/Android.bp
index 4749e75..8c71a91 100644
--- a/staticlibs/testutils/Android.bp
+++ b/staticlibs/testutils/Android.bp
@@ -42,7 +42,6 @@
         "net-utils-device-common-struct",
         "net-utils-device-common-struct-base",
         "net-utils-device-common-wear",
-        "net-utils-framework-connectivity",
         "modules-utils-build_system",
     ],
     lint: {
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/AutoCloseTestInterfaceRule.kt b/staticlibs/testutils/devicetests/com/android/testutils/AutoCloseTestInterfaceRule.kt
new file mode 100644
index 0000000..89de0b3
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/AutoCloseTestInterfaceRule.kt
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.testutils
+
+import android.Manifest.permission.MANAGE_TEST_NETWORKS
+import android.content.Context
+import android.net.TestNetworkInterface
+import android.net.TestNetworkManager
+import org.junit.rules.TestRule
+import org.junit.runner.Description
+import org.junit.runners.model.Statement
+
+class AutoCloseTestInterfaceRule(
+        private val context: Context,
+    ) : TestRule {
+    private val tnm = runAsShell(MANAGE_TEST_NETWORKS) {
+        context.getSystemService(TestNetworkManager::class.java)!!
+    }
+    private val ifaces = ArrayList<TestNetworkInterface>()
+
+    fun createTapInterface(): TestNetworkInterface {
+        return runAsShell(MANAGE_TEST_NETWORKS) {
+            tnm.createTapInterface()
+        }.also {
+            ifaces.add(it)
+        }
+    }
+
+    private fun closeAllInterfaces() {
+        // TODO: wait on RTM_DELLINK before proceeding.
+        for (iface in ifaces) {
+            // ParcelFileDescriptor prevents the fd from being double closed.
+            iface.getFileDescriptor().close()
+        }
+    }
+
+    private inner class AutoCloseTestInterfaceRuleStatement(
+        private val base: Statement,
+        private val description: Description
+    ) : Statement() {
+        override fun evaluate() {
+            tryTest {
+                base.evaluate()
+            } cleanup {
+                closeAllInterfaces()
+            }
+        }
+    }
+
+    override fun apply(base: Statement, description: Description): Statement {
+        return AutoCloseTestInterfaceRuleStatement(base, description)
+    }
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/DefaultNetworkRestoreMonitor.kt b/staticlibs/testutils/devicetests/com/android/testutils/DefaultNetworkRestoreMonitor.kt
new file mode 100644
index 0000000..dd52d0b
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/DefaultNetworkRestoreMonitor.kt
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.testutils
+
+import android.content.Context
+import android.content.pm.PackageManager
+import android.net.ConnectivityManager
+import android.net.Network
+import android.net.NetworkCapabilities
+import com.android.internal.annotations.VisibleForTesting
+import com.android.net.module.util.BitUtils
+import java.util.concurrent.CompletableFuture
+import java.util.concurrent.TimeUnit
+import org.junit.runner.Description
+import org.junit.runner.notification.Failure
+import org.junit.runner.notification.RunListener
+import org.junit.runner.notification.RunNotifier
+
+@VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
+class DefaultNetworkRestoreMonitor(
+        ctx: Context,
+        private val notifier: RunNotifier,
+        private val timeoutMs: Long = 30_000
+) {
+    var firstFailure: Exception? = null
+    var initialTransports = 0L
+    val cm = ctx.getSystemService(ConnectivityManager::class.java)!!
+    val pm = ctx.packageManager
+    val listener = object : RunListener() {
+        override fun testFinished(desc: Description) {
+            // Only the first method that does not restore the default network should be blamed.
+            if (firstFailure != null) {
+                return
+            }
+            val cb = TestableNetworkCallback()
+            cm.registerDefaultNetworkCallback(cb)
+            try {
+                cb.eventuallyExpect<RecorderCallback.CallbackEntry.CapabilitiesChanged>(
+                    timeoutMs = timeoutMs
+                ) {
+                    BitUtils.packBits(it.caps.transportTypes) == initialTransports &&
+                            it.caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
+                }
+            } catch (e: AssertionError) {
+                firstFailure = IllegalStateException(desc.methodName + " does not restore the" +
+                        "default network, initialTransports = $initialTransports", e)
+            } finally {
+                cm.unregisterNetworkCallback(cb)
+            }
+        }
+    }
+
+    fun init(connectUtil: ConnectUtil) {
+        // Ensure Wi-Fi and cellular connection before running test to avoid starting test
+        // with unexpected default network.
+        // ConnectivityTestTargetPreparer does the same thing, but it's possible that previous tests
+        // don't enable DefaultNetworkRestoreMonitor and the default network is not restored.
+        // This can be removed if all tests enable DefaultNetworkRestoreMonitor
+        if (pm.hasSystemFeature(PackageManager.FEATURE_WIFI)) {
+            connectUtil.ensureWifiValidated()
+        }
+        if (pm.hasSystemFeature(PackageManager.FEATURE_TELEPHONY)) {
+            connectUtil.ensureCellularValidated()
+        }
+
+        val capFuture = CompletableFuture<NetworkCapabilities>()
+        val cb = object : ConnectivityManager.NetworkCallback() {
+            override fun onCapabilitiesChanged(
+                    network: Network,
+                    cap: NetworkCapabilities
+            ) {
+                capFuture.complete(cap)
+            }
+        }
+        cm.registerDefaultNetworkCallback(cb)
+        try {
+            val cap = capFuture.get(10_000, TimeUnit.MILLISECONDS)
+            initialTransports = BitUtils.packBits(cap.transportTypes)
+        } catch (e: Exception) {
+            firstFailure = IllegalStateException(
+                    "Failed to get default network status before starting tests", e
+            )
+        } finally {
+            cm.unregisterNetworkCallback(cb)
+        }
+        notifier.addListener(listener)
+    }
+
+    fun reportResultAndCleanUp(desc: Description) {
+        notifier.fireTestStarted(desc)
+        if (firstFailure != null) {
+            notifier.fireTestFailure(
+                    Failure(desc, firstFailure)
+            )
+        }
+        notifier.fireTestFinished(desc)
+        notifier.removeListener(listener)
+    }
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/DevSdkIgnoreRunner.kt b/staticlibs/testutils/devicetests/com/android/testutils/DevSdkIgnoreRunner.kt
index 8687ac7..d00ae52 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/DevSdkIgnoreRunner.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/DevSdkIgnoreRunner.kt
@@ -16,6 +16,8 @@
 
 package com.android.testutils
 
+import android.content.Context
+import androidx.test.core.app.ApplicationProvider
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import com.android.net.module.util.LinkPropertiesUtils.CompareOrUpdateResult
 import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter
@@ -57,6 +59,12 @@
 class DevSdkIgnoreRunner(private val klass: Class<*>) : Runner(), Filterable, Sortable {
     private val leakMonitorDesc = Description.createTestDescription(klass, "ThreadLeakMonitor")
     private val shouldThreadLeakFailTest = klass.isAnnotationPresent(MonitorThreadLeak::class.java)
+    private val restoreDefaultNetworkDesc =
+            Description.createTestDescription(klass, "RestoreDefaultNetwork")
+    val ctx = ApplicationProvider.getApplicationContext<Context>()
+    private val restoreDefaultNetwork =
+            klass.isAnnotationPresent(RestoreDefaultNetwork::class.java) &&
+            !ctx.applicationInfo.isInstantApp()
 
     // Inference correctly infers Runner & Filterable & Sortable for |baseRunner|, but the
     // Java bytecode doesn't have a way to express this. Give this type a name by wrapping it.
@@ -71,6 +79,10 @@
     // TODO(b/307693729): Remove this annotation and monitor thread leak by default.
     annotation class MonitorThreadLeak
 
+    // Annotation for test classes to indicate the test runner should verify the default network is
+    // restored after each test.
+    annotation class RestoreDefaultNetwork
+
     private val baseRunner: RunnerWrapper<*>? = klass.let {
         val ignoreAfter = it.getAnnotation(IgnoreAfter::class.java)
         val ignoreUpTo = it.getAnnotation(IgnoreUpTo::class.java)
@@ -125,6 +137,14 @@
             )
             return
         }
+
+        val networkRestoreMonitor = if (restoreDefaultNetwork) {
+            DefaultNetworkRestoreMonitor(ctx, notifier).apply{
+                init(ConnectUtil(ctx))
+            }
+        } else {
+            null
+        }
         val threadCountsBeforeTest = if (shouldThreadLeakFailTest) {
             // Dump threads as a baseline to monitor thread leaks.
             getAllThreadNameCounts()
@@ -137,6 +157,7 @@
         if (threadCountsBeforeTest != null) {
             checkThreadLeak(notifier, threadCountsBeforeTest)
         }
+        networkRestoreMonitor?.reportResultAndCleanUp(restoreDefaultNetworkDesc)
         // Clears up internal state of all inline mocks.
         // TODO: Call clearInlineMocks() at the end of each test.
         Mockito.framework().clearInlineMocks()
@@ -163,6 +184,9 @@
             if (shouldThreadLeakFailTest) {
                 it.addChild(leakMonitorDesc)
             }
+            if (restoreDefaultNetwork) {
+                it.addChild(restoreDefaultNetworkDesc)
+            }
         }
     }
 
@@ -173,7 +197,14 @@
         // When ignoring the tests, a skipped placeholder test is reported, so test count is 1.
         if (baseRunner == null) return 1
 
-        return baseRunner.testCount() + if (shouldThreadLeakFailTest) 1 else 0
+        var testCount = baseRunner.testCount()
+        if (shouldThreadLeakFailTest) {
+            testCount += 1
+        }
+        if (restoreDefaultNetwork) {
+            testCount += 1
+        }
+        return testCount
     }
 
     @Throws(NoTestsRemainException::class)
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/NetworkStatsUtils.kt b/staticlibs/testutils/devicetests/com/android/testutils/NetworkStatsUtils.kt
index 8324b25..26bdb49 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/NetworkStatsUtils.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/NetworkStatsUtils.kt
@@ -16,8 +16,21 @@
 
 package com.android.testutils
 
+import android.app.usage.NetworkStatsManager
+import android.content.Context
+import android.net.INetworkStatsService
+import android.net.INetworkStatsSession
 import android.net.NetworkStats
+import android.net.NetworkTemplate
+import android.net.NetworkTemplate.MATCH_MOBILE
+import android.text.TextUtils
+import com.android.modules.utils.build.SdkLevel
 import kotlin.test.assertTrue
+import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.anyLong
+import org.mockito.Mockito
 
 @JvmOverloads
 fun orderInsensitiveEquals(
@@ -26,7 +39,7 @@
     compareTime: Boolean = false
 ): Boolean {
     if (leftStats == rightStats) return true
-    if (compareTime && leftStats.getElapsedRealtime() != rightStats.getElapsedRealtime()) {
+    if (compareTime && leftStats.elapsedRealtime != rightStats.elapsedRealtime) {
         return false
     }
 
@@ -47,12 +60,41 @@
                 left.metered, left.roaming, left.defaultNetwork, i)
         if (j == -1) return false
         rightTrimmedEmpty.getValues(j, right)
-        if (left != right) return false
+        if (SdkLevel.isAtLeastT()) {
+            if (left != right) return false
+        } else {
+            if (!checkEntryEquals(left, right)) return false
+        }
     }
     return true
 }
 
 /**
+ * Assert that the two {@link NetworkStats.Entry} are equals.
+ */
+fun assertEntryEquals(left: NetworkStats.Entry, right: NetworkStats.Entry) {
+    assertTrue(checkEntryEquals(left, right))
+}
+
+// TODO: Make all callers use NetworkStats.Entry#equals once S- downstreams
+//  are no longer supported. Because NetworkStats is mainlined on T+ and
+//  NetworkStats.Entry#equals in S- does not support null iface.
+fun checkEntryEquals(left: NetworkStats.Entry, right: NetworkStats.Entry): Boolean {
+    return TextUtils.equals(left.iface, right.iface) &&
+            left.uid == right.uid &&
+            left.set == right.set &&
+            left.tag == right.tag &&
+            left.metered == right.metered &&
+            left.roaming == right.roaming &&
+            left.defaultNetwork == right.defaultNetwork &&
+            left.rxBytes == right.rxBytes &&
+            left.rxPackets == right.rxPackets &&
+            left.txBytes == right.txBytes &&
+            left.txPackets == right.txPackets &&
+            left.operations == right.operations
+}
+
+/**
  * Assert that two {@link NetworkStats} are equals, assuming the order of the records are not
  * necessarily the same.
  *
@@ -66,7 +108,7 @@
     compareTime: Boolean = false
 ) {
     assertTrue(orderInsensitiveEquals(expected, actual, compareTime),
-            "expected: " + expected + " but was: " + actual)
+            "expected: $expected but was: $actual")
 }
 
 /**
@@ -74,5 +116,34 @@
  * object.
  */
 fun assertParcelingIsLossless(stats: NetworkStats) {
-    assertParcelingIsLossless(stats, { a, b -> orderInsensitiveEquals(a, b) })
+    assertParcelingIsLossless(stats) { a, b -> orderInsensitiveEquals(a, b) }
+}
+
+/**
+ * Make a {@link android.app.usage.NetworkStats} instance from
+ * a {@link android.net.NetworkStats} instance.
+ */
+// It's not possible to directly create a mocked `NetworkStats` instance
+// because of limitations with `NetworkStats#getNextBucket`.
+// As a workaround for testing, create a mock by controlling the return values
+// from the mocked service that provides the `NetworkStats` data.
+// Notes:
+//   1. The order of records in the final `NetworkStats` object might change or
+//      some records might be merged if there are items with duplicate keys.
+//   2. The interface and operations fields will be empty since there is
+//      no such field in the {@link android.app.usage.NetworkStats}.
+fun makePublicStatsFromAndroidNetStats(androidNetStats: NetworkStats):
+        android.app.usage.NetworkStats {
+    val mockService = Mockito.mock(INetworkStatsService::class.java)
+    val manager = NetworkStatsManager(Mockito.mock(Context::class.java), mockService)
+    val mockStatsSession = Mockito.mock(INetworkStatsSession::class.java)
+
+    Mockito.doReturn(mockStatsSession).`when`(mockService)
+            .openSessionForUsageStats(anyInt(), any())
+    Mockito.doReturn(androidNetStats).`when`(mockStatsSession).getSummaryForAllUid(
+            any(NetworkTemplate::class.java), anyLong(), anyLong(), anyBoolean())
+    return manager.querySummary(
+            NetworkTemplate.Builder(MATCH_MOBILE).build(),
+            Long.MIN_VALUE, Long.MAX_VALUE
+    )
 }
diff --git a/staticlibs/testutils/host/python/apf_test_base.py b/staticlibs/testutils/host/python/apf_test_base.py
new file mode 100644
index 0000000..7203265
--- /dev/null
+++ b/staticlibs/testutils/host/python/apf_test_base.py
@@ -0,0 +1,80 @@
+#  Copyright (C) 2024 The Android Open Source Project
+#
+#  Licensed under the Apache License, Version 2.0 (the "License");
+#  you may not use this file except in compliance with the License.
+#  You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+
+from mobly import asserts
+from net_tests_utils.host.python import adb_utils, apf_utils, assert_utils, multi_devices_test_base, tether_utils
+from net_tests_utils.host.python.tether_utils import UpstreamType
+
+
+class ApfTestBase(multi_devices_test_base.MultiDevicesTestBase):
+
+  def setup_class(self):
+    super().setup_class()
+
+    # Check test preconditions.
+    tether_utils.assume_hotspot_test_preconditions(
+        self.serverDevice, self.clientDevice, UpstreamType.NONE
+    )
+    asserts.abort_class_if(
+        not apf_utils.is_send_raw_packet_downstream_supported(
+            self.serverDevice
+        ),
+        "NetworkStack is too old to support send raw packet, skip test.",
+    )
+
+    # Fetch device properties and storing them locally for later use.
+    client = self.clientDevice.connectivity_multi_devices_snippet
+    self.server_iface_name, client_network = (
+        tether_utils.setup_hotspot_and_client_for_upstream_type(
+            self.serverDevice, self.clientDevice, UpstreamType.NONE
+        )
+    )
+    self.client_iface_name = client.getInterfaceNameFromNetworkHandle(
+        client_network
+    )
+    self.server_mac_address = apf_utils.get_hardware_address(
+        self.serverDevice, self.server_iface_name
+    )
+
+    # Enable doze mode to activate APF.
+    adb_utils.set_doze_mode(self.clientDevice, True)
+
+  def teardown_class(self):
+    adb_utils.set_doze_mode(self.clientDevice, False)
+    tether_utils.cleanup_tethering_for_upstream_type(
+        self.serverDevice, UpstreamType.NONE
+    )
+
+  def send_packet_and_expect_counter_increased(
+      self, packet: str, counter_name: str
+  ) -> None:
+    count_before_test = apf_utils.get_apf_counter(
+        self.clientDevice,
+        self.client_iface_name,
+        counter_name,
+    )
+    apf_utils.send_raw_packet_downstream(
+        self.serverDevice, self.server_iface_name, packet
+    )
+
+    assert_utils.expect_with_retry(
+        lambda: apf_utils.get_apf_counter(
+            self.clientDevice,
+            self.client_iface_name,
+            counter_name,
+        )
+        > count_before_test
+    )
+
+    # TODO: Verify the packet is not actually received.
diff --git a/staticlibs/testutils/host/python/apf_utils.py b/staticlibs/testutils/host/python/apf_utils.py
index f71464c..a3ec6e9 100644
--- a/staticlibs/testutils/host/python/apf_utils.py
+++ b/staticlibs/testutils/host/python/apf_utils.py
@@ -12,17 +12,14 @@
 #  See the License for the specific language governing permissions and
 #  limitations under the License.
 
+from dataclasses import dataclass
 import re
+from mobly import asserts
 from mobly.controllers import android_device
 from mobly.controllers.android_device_lib.adb import AdbError
 from net_tests_utils.host.python import adb_utils, assert_utils
 
 
-# Constants.
-ETHER_BROADCAST = "FFFFFFFFFFFF"
-ETH_P_ETHERCAT = "88A4"
-
-
 class PatternNotFoundException(Exception):
   """Raised when the given pattern cannot be found."""
 
@@ -119,25 +116,17 @@
     )
 
 
-def send_broadcast_empty_ethercat_packet(
-    ad: android_device.AndroidDevice, iface_name: str
-) -> None:
-  """Transmits a broadcast empty EtherCat packet on the specified interface."""
-
-  # Get the interface's MAC address.
-  mac_address = get_hardware_address(ad, iface_name)
-
-  # TODO: Build packet by using scapy library.
-  # Ethernet header (14 bytes).
-  packet = ETHER_BROADCAST  # Destination MAC (broadcast)
-  packet += mac_address.replace(":", "")  # Source MAC
-  packet += ETH_P_ETHERCAT  # EtherType (EtherCAT)
-
-  # EtherCAT header (2 bytes) + 44 bytes of zero padding.
-  packet += "00" * 46
-
-  # Send the packet using a raw socket.
-  send_raw_packet_downstream(ad, iface_name, packet)
+def is_send_raw_packet_downstream_supported(
+    ad: android_device.AndroidDevice,
+) -> bool:
+  try:
+    # Invoke the shell command with empty argument and see how NetworkStack respond.
+    # If supported, an IllegalArgumentException with help page will be printed.
+    send_raw_packet_downstream(ad, "", "")
+  except assert_utils.UnexpectedBehaviorError:
+    return True
+  except UnsupportedOperationException:
+    return False
 
 
 def send_raw_packet_downstream(
@@ -190,3 +179,65 @@
     raise assert_utils.UnexpectedBehaviorError(
         f"Got unexpected output: {output} for command: {cmd}."
     )
+
+
+@dataclass
+class ApfCapabilities:
+  """APF program support capabilities.
+
+  See android.net.apf.ApfCapabilities.
+
+  Attributes:
+      apf_version_supported (int): Version of APF instruction set supported for
+        packet filtering. 0 indicates no support for packet filtering using APF
+        programs.
+      apf_ram_size (int): Size of APF ram.
+      apf_packet_format (int): Format of packets passed to APF filter. Should be
+        one of ARPHRD_*
+  """
+
+  apf_version_supported: int
+  apf_ram_size: int
+  apf_packet_format: int
+
+  def __init__(
+      self,
+      apf_version_supported: int,
+      apf_ram_size: int,
+      apf_packet_format: int,
+  ):
+    self.apf_version_supported = apf_version_supported
+    self.apf_ram_size = apf_ram_size
+    self.apf_packet_format = apf_packet_format
+
+  def __str__(self):
+    """Returns a user-friendly string representation of the APF capabilities."""
+    return (
+        f"APF Version: {self.apf_version_supported}\n"
+        f"Ram Size: {self.apf_ram_size} bytes\n"
+        f"Packet Format: {self.apf_packet_format}"
+    )
+
+
+def get_apf_capabilities(
+    ad: android_device.AndroidDevice, iface_name: str
+) -> ApfCapabilities:
+  output = adb_utils.adb_shell(
+      ad, f"cmd network_stack apf {iface_name} capabilities"
+  )
+  try:
+    values = [int(value_str) for value_str in output.split(",")]
+  except ValueError:
+    return ApfCapabilities(0, 0, 0)  # Conversion to integer failed
+  return ApfCapabilities(values[0], values[1], values[2])
+
+
+def assume_apf_version_support_at_least(
+    ad: android_device.AndroidDevice, iface_name: str, expected_version: int
+) -> None:
+  caps = get_apf_capabilities(ad, iface_name)
+  asserts.skip_if(
+      caps.apf_version_supported < expected_version,
+      f"Supported apf version {caps.apf_version_supported} < expected version"
+      f" {expected_version}",
+  )
diff --git a/staticlibs/testutils/host/python/mdns_utils.py b/staticlibs/testutils/host/python/mdns_utils.py
index ec1fea0..1234e54 100644
--- a/staticlibs/testutils/host/python/mdns_utils.py
+++ b/staticlibs/testutils/host/python/mdns_utils.py
@@ -12,9 +12,24 @@
 #  See the License for the specific language governing permissions and
 #  limitations under the License.
 
+from mobly import asserts
 from mobly.controllers import android_device
 
 
+def assume_mdns_test_preconditions(
+    advertising_device: android_device, discovery_device: android_device
+) -> None:
+  advertising = advertising_device.connectivity_multi_devices_snippet
+  discovery = discovery_device.connectivity_multi_devices_snippet
+
+  asserts.skip_if(
+      not advertising.isAtLeastT(), "Advertising device SDK is lower than T."
+  )
+  asserts.skip_if(
+      not discovery.isAtLeastT(), "Discovery device SDK is lower than T."
+  )
+
+
 def register_mdns_service_and_discover_resolve(
     advertising_device: android_device, discovery_device: android_device
 ) -> None:
diff --git a/staticlibs/testutils/host/python/multi_devices_test_base.py b/staticlibs/testutils/host/python/multi_devices_test_base.py
new file mode 100644
index 0000000..f8a92f3
--- /dev/null
+++ b/staticlibs/testutils/host/python/multi_devices_test_base.py
@@ -0,0 +1,54 @@
+#  Copyright (C) 2024 The Android Open Source Project
+#
+#  Licensed under the Apache License, Version 2.0 (the "License");
+#  you may not use this file except in compliance with the License.
+#  You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+#
+#  Licensed under the Apache License, Version 2.0 (the "License");
+#  you may not use this file except in compliance with the License.
+#  You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+
+from mobly import base_test
+from mobly import utils
+from mobly.controllers import android_device
+
+CONNECTIVITY_MULTI_DEVICES_SNIPPET_PACKAGE = "com.google.snippet.connectivity"
+
+
+class MultiDevicesTestBase(base_test.BaseTestClass):
+
+  def setup_class(self):
+    # Declare that two Android devices are needed.
+    self.clientDevice, self.serverDevice = self.register_controller(
+        android_device, min_number=2
+    )
+
+    def setup_device(device):
+      device.load_snippet(
+          "connectivity_multi_devices_snippet",
+          CONNECTIVITY_MULTI_DEVICES_SNIPPET_PACKAGE,
+      )
+
+    # Set up devices in parallel to save time.
+    utils.concurrent_exec(
+        setup_device,
+        ((self.clientDevice,), (self.serverDevice,)),
+        max_workers=2,
+        raise_on_exception=True,
+    )
diff --git a/staticlibs/testutils/host/python/wifip2p_utils.py b/staticlibs/testutils/host/python/wifip2p_utils.py
new file mode 100644
index 0000000..ef6af75
--- /dev/null
+++ b/staticlibs/testutils/host/python/wifip2p_utils.py
@@ -0,0 +1,93 @@
+#  Copyright (C) 2024 The Android Open Source Project
+#
+#  Licensed under the Apache License, Version 2.0 (the "License");
+#  you may not use this file except in compliance with the License.
+#  You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+
+from mobly import asserts
+from mobly.controllers import android_device
+from net_tests_utils.host.python import tether_utils
+
+
+def assume_wifi_p2p_test_preconditions(
+    server_device: android_device, client_device: android_device
+) -> None:
+  """Preconditions check for running Wi-Fi P2P test."""
+  server = server_device.connectivity_multi_devices_snippet
+  client = client_device.connectivity_multi_devices_snippet
+
+  # Assert pre-conditions
+  asserts.skip_if(not server.hasWifiFeature(), "Server requires Wifi feature")
+  asserts.skip_if(not client.hasWifiFeature(), "Client requires Wifi feature")
+  asserts.skip_if(
+      not server.isP2pSupported(), "Server requires Wi-fi P2P feature"
+  )
+  asserts.skip_if(
+      not client.isP2pSupported(), "Client requires Wi-fi P2P feature"
+  )
+
+
+def setup_wifi_p2p_server_and_client(
+    server_device: android_device, client_device: android_device
+) -> None:
+  """Set up the Wi-Fi P2P server and client, then connect them to establish a Wi-Fi P2P connection."""
+  server = server_device.connectivity_multi_devices_snippet
+  client = client_device.connectivity_multi_devices_snippet
+
+  # Start Wi-Fi P2P on both server and client.
+  server.startWifiP2p()
+  client.startWifiP2p()
+
+  # Get the current device name
+  server_name = server.getDeviceName()
+  client_name = client.getDeviceName()
+
+  # Generate Wi-Fi P2P group passphrase with random characters.
+  group_name = "DIRECT-" + tether_utils.generate_uuid32_base64()
+  group_passphrase = tether_utils.generate_uuid32_base64()
+
+  # Server creates a Wi-Fi P2P group
+  server.createGroup(group_name, group_passphrase)
+
+  # Start Wi-Fi P2p peers discovery on both devices
+  server.startPeersDiscovery()
+  client.startPeersDiscovery()
+
+  # Ensure the target device has been discovered
+  server_address = client.ensureDeviceDiscovered(server_name)
+  client_address = server.ensureDeviceDiscovered(client_name)
+
+  # Server invites the device to the group
+  server.inviteDeviceToGroup(group_name, group_passphrase, client_address)
+
+  # Wait for a p2p connection changed intent to ensure the invitation has been
+  # received.
+  client.waitForP2pConnectionChanged(True, group_name)
+  # Accept the group invitation
+  client.acceptGroupInvitation(server_address)
+
+  # Server waits for connection request from client and accept joining
+  server.waitForPeerConnectionRequestAndAcceptJoining(client_address)
+
+  # Wait for a p2p connection changed intent to ensure joining the group
+  client.waitForP2pConnectionChanged(False, group_name)
+
+  # Ensure Wi-Fi P2P connected on both devices
+  client.ensureDeviceConnected(server_name)
+  server.ensureDeviceConnected(client_name)
+
+
+def cleanup_wifi_p2p(
+    server_device: android_device, client_device: android_device
+) -> None:
+  # Stop Wi-Fi P2P
+  server_device.connectivity_multi_devices_snippet.stopWifiP2p()
+  client_device.connectivity_multi_devices_snippet.stopWifiP2p()
diff --git a/tests/common/java/android/net/netstats/NetworkStatsApiTest.kt b/tests/common/java/android/net/netstats/NetworkStatsApiTest.kt
index 8cef6aa..17f5e96 100644
--- a/tests/common/java/android/net/netstats/NetworkStatsApiTest.kt
+++ b/tests/common/java/android/net/netstats/NetworkStatsApiTest.kt
@@ -28,18 +28,26 @@
 import android.net.NetworkStats.SET_DEFAULT
 import android.net.NetworkStats.SET_FOREGROUND
 import android.net.NetworkStats.TAG_NONE
+import android.os.Build
 import androidx.test.filters.SmallTest
+import com.android.testutils.ConnectivityModuleTest
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.DevSdkIgnoreRunner
 import com.android.testutils.assertNetworkStatsEquals
 import com.android.testutils.assertParcelingIsLossless
 import kotlin.test.assertEquals
 import org.junit.Before
+import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
 
-@RunWith(JUnit4::class)
+@DevSdkIgnoreRunner.MonitorThreadLeak
+@RunWith(DevSdkIgnoreRunner::class)
 @SmallTest
 class NetworkStatsApiTest {
+    @get:Rule
+    val ignoreRule = DevSdkIgnoreRule()
     private val testStatsEmpty = NetworkStats(0L, 0)
 
     // Note that these variables need to be initialized outside of constructor, initialize
@@ -49,6 +57,7 @@
     // be merged if performing add on these 2 stats.
     private lateinit var testStats1: NetworkStats
     private lateinit var testStats2: NetworkStats
+    private lateinit var expectedEntriesInStats2: List<Entry>
 
     // This is a result of adding stats1 and stats2, while the merging of common key items is
     // subject to test later, this should not be initialized with for a loop to add stats1
@@ -84,19 +93,23 @@
                         METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 3, 1, 6, 2, 0))
         assertEquals(8, testStats1.size())
 
-        testStats2 = NetworkStats(0L, 0)
-                // Entries which are common for set1 and set2.
-                .addEntry(Entry(TEST_IFACE, TEST_UID1, SET_DEFAULT, 0x80,
-                        METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 3, 15, 2, 31, 1))
-                .addEntry(Entry(TEST_IFACE, TEST_UID1, SET_FOREGROUND, TAG_NONE,
-                        METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 13, 61, 10, 1, 45))
-                .addEntry(Entry(TEST_IFACE, TEST_UID2, SET_DEFAULT, TAG_NONE,
-                        METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 11, 2, 3, 4, 7))
-                .addEntry(Entry(IFACE_VT, TEST_UID1, SET_DEFAULT, TAG_NONE,
-                        METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 4, 3, 2, 1, 0))
-                // Entry which only appears in set2.
-                .addEntry(Entry(IFACE_VT, TEST_UID2, SET_DEFAULT, TAG_NONE,
-                        METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 2, 3, 7, 8, 0))
+        expectedEntriesInStats2 = listOf(
+            // Entries which are common for set1 and set2.
+            Entry(TEST_IFACE, TEST_UID1, SET_DEFAULT, 0x80,
+                  METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 3, 15, 2, 31, 1),
+            Entry(TEST_IFACE, TEST_UID1, SET_FOREGROUND, TAG_NONE,
+                  METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 13, 61, 10, 1, 45),
+            Entry(TEST_IFACE, TEST_UID2, SET_DEFAULT, TAG_NONE,
+                  METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 11, 2, 3, 4, 7),
+            Entry(IFACE_VT, TEST_UID1, SET_DEFAULT, TAG_NONE,
+                  METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 4, 3, 2, 1, 0),
+            // Entry which only appears in set2.
+            Entry(IFACE_VT, TEST_UID2, SET_DEFAULT, TAG_NONE,
+                  METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 2, 3, 7, 8, 0))
+        testStats2 = NetworkStats(0L, 5)
+        for (entry in expectedEntriesInStats2) {
+            testStats2 = testStats2.addEntry(entry)
+        }
         assertEquals(5, testStats2.size())
 
         testStats3 = NetworkStats(0L, 9)
@@ -125,18 +138,6 @@
 
     @Test
     fun testAddEntry() {
-        val expectedEntriesInStats2 = arrayOf(
-                Entry(TEST_IFACE, TEST_UID1, SET_DEFAULT, 0x80,
-                        METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 3, 15, 2, 31, 1),
-                Entry(TEST_IFACE, TEST_UID1, SET_FOREGROUND, TAG_NONE,
-                        METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 13, 61, 10, 1, 45),
-                Entry(TEST_IFACE, TEST_UID2, SET_DEFAULT, TAG_NONE,
-                        METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 11, 2, 3, 4, 7),
-                Entry(IFACE_VT, TEST_UID1, SET_DEFAULT, TAG_NONE,
-                        METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 4, 3, 2, 1, 0),
-                Entry(IFACE_VT, TEST_UID2, SET_DEFAULT, TAG_NONE,
-                        METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 2, 3, 7, 8, 0))
-
         // While testStats* are already initialized with addEntry, verify content added
         // matches expectation.
         for (i in expectedEntriesInStats2.indices) {
@@ -150,6 +151,27 @@
         assertEquals(Entry(IFACE_VT, TEST_UID1, SET_DEFAULT, TAG_NONE,
                 METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 16, -2, 9, 1, 9),
                 stats.getValues(3, null))
+
+        // Verify the original ststs object is not altered.
+        for (i in expectedEntriesInStats2.indices) {
+            val entry = testStats2.getValues(i, null)
+            assertEquals(expectedEntriesInStats2[i], entry)
+        }
+    }
+
+    @ConnectivityModuleTest
+    @IgnoreUpTo(Build.VERSION_CODES.S_V2) // Mainlined NetworkStats only runs on T+
+    @Test
+    fun testAddEntries() {
+        val baseStats = NetworkStats(0L, 1)
+                .addEntry(Entry(IFACE_VT, TEST_UID1, SET_DEFAULT, TAG_NONE,
+                        METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 12, -5, 7, 0, 9))
+        val statsUnderTest = baseStats.addEntries(expectedEntriesInStats2)
+        // Assume the correctness of addEntry is verified in other tests.
+        val expectedStats = testStats2
+                .addEntry(Entry(IFACE_VT, TEST_UID1, SET_DEFAULT, TAG_NONE,
+                        METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 12, -5, 7, 0, 9))
+        assertNetworkStatsEquals(expectedStats, statsUnderTest)
     }
 
     @Test
diff --git a/tests/cts/hostside-network-policy/Android.bp b/tests/cts/hostside-network-policy/Android.bp
deleted file mode 100644
index c3ce0b9..0000000
--- a/tests/cts/hostside-network-policy/Android.bp
+++ /dev/null
@@ -1,46 +0,0 @@
-// Copyright (C) 2024 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package {
-    default_team: "trendy_team_framework_backstage_power",
-    default_applicable_licenses: ["Android-Apache-2.0"],
-}
-
-java_test_host {
-    name: "CtsHostsideNetworkPolicyTests",
-    defaults: ["cts_defaults"],
-    // Only compile source java files in this apk.
-    srcs: [
-        "src/**/*.java",
-        ":ArgumentConstants",
-    ],
-    libs: [
-        "cts-tradefed",
-        "tradefed",
-    ],
-    static_libs: [
-        "modules-utils-build-testing",
-    ],
-    // Tag this module as a cts test artifact
-    test_suites: [
-        "cts",
-        "general-tests",
-        "sts",
-    ],
-    data: [
-        ":CtsHostsideNetworkPolicyTestsApp",
-        ":CtsHostsideNetworkPolicyTestsApp2",
-    ],
-    per_testcase_directory: true,
-}
diff --git a/tests/cts/hostside-network-policy/AndroidTest.xml b/tests/cts/hostside-network-policy/AndroidTest.xml
deleted file mode 100644
index 44f77f8..0000000
--- a/tests/cts/hostside-network-policy/AndroidTest.xml
+++ /dev/null
@@ -1,51 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- 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.
--->
-<configuration description="Config for CTS network policy host test cases">
-    <option name="test-suite-tag" value="cts" />
-    <option name="config-descriptor:metadata" key="component" value="networking" />
-    <option name="config-descriptor:metadata" key="token" value="SIM_CARD" />
-    <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" />
-
-    <target_preparer class="com.android.compatibility.common.tradefed.targetprep.LocationCheck" />
-    <target_preparer class="com.android.cts.netpolicy.NetworkPolicyTestsPreparer" />
-
-    <!-- Enabling change id ALLOW_TEST_API_ACCESS allows that package to access @TestApi methods -->
-    <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
-        <option name="run-command" value="am compat enable ALLOW_TEST_API_ACCESS com.android.cts.netpolicy.hostside.app2" />
-        <option name="teardown-command" value="am compat reset ALLOW_TEST_API_ACCESS com.android.cts.netpolicy.hostside.app2" />
-        <option name="teardown-command" value="cmd power set-mode 0" />
-        <option name="teardown-command" value="cmd battery reset" />
-        <option name="teardown-command" value="cmd netpolicy stop-watching" />
-    </target_preparer>
-
-    <target_preparer class="com.android.tradefed.targetprep.DeviceSetup">
-        <option name="force-skip-system-props" value="true" />
-        <option name="set-global-setting" key="verifier_verify_adb_installs" value="0" />
-        <option name="set-global-setting" key="low_power_standby_enabled" value="0" />
-    </target_preparer>
-
-    <test class="com.android.compatibility.common.tradefed.testtype.JarHostTest" >
-        <option name="jar" value="CtsHostsideNetworkPolicyTests.jar" />
-        <option name="runtime-hint" value="3m56s" />
-    </test>
-
-    <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector">
-        <option name="directory-keys" value="/sdcard/CtsHostsideNetworkPolicyTests" />
-        <option name="collect-on-run-ended-only" value="true" />
-    </metrics_collector>
-</configuration>
diff --git a/tests/cts/hostside-network-policy/OWNERS b/tests/cts/hostside-network-policy/OWNERS
deleted file mode 100644
index ea83e61..0000000
--- a/tests/cts/hostside-network-policy/OWNERS
+++ /dev/null
@@ -1,3 +0,0 @@
-# Bug component: 61373
-# Inherits parent owners
-include platform/frameworks/base:/services/core/java/com/android/server/net/OWNERS
diff --git a/tests/cts/hostside-network-policy/TEST_MAPPING b/tests/cts/hostside-network-policy/TEST_MAPPING
deleted file mode 100644
index 57ac4f7..0000000
--- a/tests/cts/hostside-network-policy/TEST_MAPPING
+++ /dev/null
@@ -1,27 +0,0 @@
-{
-  "presubmit-large": [
-    {
-      "name": "CtsHostsideNetworkPolicyTests",
-      "options": [
-        {
-          "exclude-annotation": "android.platform.test.annotations.FlakyTest"
-        },
-        {
-          "exclude-annotation": "android.platform.test.annotations.RequiresDevice"
-        }
-      ]
-    }
-  ],
-  "postsubmit": [
-    {
-      // Postsubmit on virtual devices to monitor flakiness of all tests that don't require a
-      // physical device
-      "name": "CtsHostsideNetworkPolicyTests",
-      "options": [
-        {
-          "exclude-annotation": "androidx.test.filters.RequiresDevice"
-        }
-      ]
-    }
-  ]
-}
diff --git a/tests/cts/hostside-network-policy/aidl/com/android/cts/netpolicy/hostside/IMyService.aidl b/tests/cts/hostside-network-policy/aidl/com/android/cts/netpolicy/hostside/IMyService.aidl
deleted file mode 100644
index 068d9d8..0000000
--- a/tests/cts/hostside-network-policy/aidl/com/android/cts/netpolicy/hostside/IMyService.aidl
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
- * Copyright (C) 2016 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.cts.netpolicy.hostside;
-
-import android.app.job.JobInfo;
-
-import com.android.cts.netpolicy.hostside.INetworkCallback;
-import com.android.cts.netpolicy.hostside.NetworkCheckResult;
-
-interface IMyService {
-    void registerBroadcastReceiver();
-    int getCounters(String receiverName, String action);
-    NetworkCheckResult checkNetworkStatus(String customUrl);
-    String getRestrictBackgroundStatus();
-    void sendNotification(int notificationId, String notificationType);
-    void registerNetworkCallback(in NetworkRequest request, in INetworkCallback cb);
-    void unregisterNetworkCallback();
-    int scheduleJob(in JobInfo jobInfo);
-}
diff --git a/tests/cts/hostside-network-policy/aidl/com/android/cts/netpolicy/hostside/INetworkCallback.aidl b/tests/cts/hostside-network-policy/aidl/com/android/cts/netpolicy/hostside/INetworkCallback.aidl
deleted file mode 100644
index 38efc7b..0000000
--- a/tests/cts/hostside-network-policy/aidl/com/android/cts/netpolicy/hostside/INetworkCallback.aidl
+++ /dev/null
@@ -1,27 +0,0 @@
-/*
- * Copyright (C) 2019 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.cts.netpolicy.hostside;
-
-import android.net.Network;
-import android.net.NetworkCapabilities;
-
-interface INetworkCallback {
-    void onBlockedStatusChanged(in Network network, boolean blocked);
-    void onAvailable(in Network network);
-    void onLost(in Network network);
-    void onCapabilitiesChanged(in Network network, in NetworkCapabilities cap);
-}
diff --git a/tests/cts/hostside-network-policy/aidl/com/android/cts/netpolicy/hostside/INetworkStateObserver.aidl b/tests/cts/hostside-network-policy/aidl/com/android/cts/netpolicy/hostside/INetworkStateObserver.aidl
deleted file mode 100644
index c6b7a1c..0000000
--- a/tests/cts/hostside-network-policy/aidl/com/android/cts/netpolicy/hostside/INetworkStateObserver.aidl
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * Copyright (C) 2016 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.cts.netpolicy.hostside;
-
-import android.net.NetworkInfo;
-
-import com.android.cts.netpolicy.hostside.NetworkCheckResult;
-
-interface INetworkStateObserver {
-    void onNetworkStateChecked(int resultCode, in NetworkCheckResult networkCheckResult);
-
-    const int RESULT_SUCCESS_NETWORK_STATE_CHECKED = 0;
-    const int RESULT_ERROR_UNEXPECTED_PROC_STATE = 1;
-    const int RESULT_ERROR_UNEXPECTED_CAPABILITIES = 2;
-    const int RESULT_ERROR_OTHER = 3;
-}
\ No newline at end of file
diff --git a/tests/cts/hostside-network-policy/app/Android.bp b/tests/cts/hostside-network-policy/app/Android.bp
deleted file mode 100644
index a31c843..0000000
--- a/tests/cts/hostside-network-policy/app/Android.bp
+++ /dev/null
@@ -1,57 +0,0 @@
-//
-// Copyright (C) 2024 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-
-package {
-    default_team: "trendy_team_framework_backstage_power",
-    default_applicable_licenses: ["Android-Apache-2.0"],
-}
-
-java_defaults {
-    name: "CtsHostsideNetworkPolicyTestsAppDefaults",
-    platform_apis: true,
-    static_libs: [
-        "CtsHostsideNetworkPolicyTestsAidl",
-        "androidx.test.ext.junit",
-        "androidx.test.rules",
-        "androidx.test.uiautomator_uiautomator",
-        "compatibility-device-util-axt",
-        "cts-net-utils",
-        "ctstestrunner-axt",
-        "modules-utils-build",
-    ],
-    libs: [
-        "android.test.runner",
-        "android.test.base",
-    ],
-    srcs: [
-        "src/**/*.java",
-        ":ArgumentConstants",
-    ],
-    // Tag this module as a cts test artifact
-    test_suites: [
-        "general-tests",
-        "sts",
-    ],
-}
-
-android_test_helper_app {
-    name: "CtsHostsideNetworkPolicyTestsApp",
-    defaults: [
-        "cts_support_defaults",
-        "framework-connectivity-test-defaults",
-        "CtsHostsideNetworkPolicyTestsAppDefaults",
-    ],
-}
diff --git a/tests/cts/hostside-network-policy/app/AndroidManifest.xml b/tests/cts/hostside-network-policy/app/AndroidManifest.xml
deleted file mode 100644
index f19e35f..0000000
--- a/tests/cts/hostside-network-policy/app/AndroidManifest.xml
+++ /dev/null
@@ -1,50 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- 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.
--->
-
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
-     package="com.android.cts.netpolicy.hostside">
-
-    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
-    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
-    <uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>
-    <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
-    <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
-    <uses-permission android:name="android.permission.INTERNET"/>
-    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
-    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
-    <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/>
-    <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"/>
-    <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS"/>
-    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
-    <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
-    <uses-permission android:name="android.permission.WAKE_LOCK" />
-
-    <application android:requestLegacyExternalStorage="true">
-        <uses-library android:name="android.test.runner"/>
-        <service android:name=".MyNotificationListenerService"
-             android:label="MyNotificationListenerService"
-             android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE"
-             android:exported="true">
-            <intent-filter>
-                <action android:name="android.service.notification.NotificationListenerService"/>
-            </intent-filter>
-        </service>
-    </application>
-
-    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-         android:targetPackage="com.android.cts.netpolicy.hostside"/>
-
-</manifest>
diff --git a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/AbstractAppIdleTestCase.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/AbstractAppIdleTestCase.java
deleted file mode 100644
index 19e4364..0000000
--- a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/AbstractAppIdleTestCase.java
+++ /dev/null
@@ -1,201 +0,0 @@
-/*
- * Copyright (C) 2016 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.cts.netpolicy.hostside;
-
-import static com.android.cts.netpolicy.hostside.Property.APP_STANDBY_MODE;
-import static com.android.cts.netpolicy.hostside.Property.BATTERY_SAVER_MODE;
-
-import static org.junit.Assert.assertEquals;
-
-import android.os.SystemClock;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-
-/**
- * Base class for metered and non-metered tests on idle apps.
- */
-@RequiredProperties({APP_STANDBY_MODE})
-abstract class AbstractAppIdleTestCase extends AbstractRestrictBackgroundNetworkTestCase {
-
-    @Before
-    public final void setUp() throws Exception {
-        super.setUp();
-
-        // Set initial state.
-        removePowerSaveModeWhitelist(TEST_APP2_PKG);
-        removePowerSaveModeExceptIdleWhitelist(TEST_APP2_PKG);
-        setAppIdle(false);
-        turnBatteryOn();
-
-        registerBroadcastReceiver();
-    }
-
-    @After
-    public final void tearDown() throws Exception {
-        super.tearDown();
-
-        resetBatteryState();
-        setAppIdle(false);
-    }
-
-    @Test
-    public void testBackgroundNetworkAccess_enabled() throws Exception {
-        setAppIdle(true);
-        assertBackgroundNetworkAccess(false);
-
-        assertsForegroundAlwaysHasNetworkAccess();
-        setAppIdle(true);
-        assertBackgroundNetworkAccess(false);
-
-        // Make sure foreground app doesn't lose access upon enabling it.
-        setAppIdle(true);
-        launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_ACTIVTIY);
-        finishActivity();
-        assertAppIdle(false); // verify - not idle anymore, since activity was launched...
-        assertBackgroundNetworkAccess(true);
-        setAppIdle(true);
-        assertBackgroundNetworkAccess(false);
-
-        // Same for foreground service.
-        setAppIdle(true);
-        launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_FOREGROUND_SERVICE);
-        stopForegroundService();
-        assertAppIdle(true);
-        assertBackgroundNetworkAccess(false);
-
-        // Set Idle after foreground service start.
-        launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_FOREGROUND_SERVICE);
-        setAppIdle(true);
-        addPowerSaveModeWhitelist(TEST_PKG);
-        removePowerSaveModeWhitelist(TEST_PKG);
-        assertForegroundServiceNetworkAccess();
-        stopForegroundService();
-        assertAppIdle(true);
-        assertBackgroundNetworkAccess(false);
-
-    }
-
-    @Test
-    public void testBackgroundNetworkAccess_whitelisted() throws Exception {
-        setAppIdle(true);
-        assertBackgroundNetworkAccess(false);
-
-        addPowerSaveModeWhitelist(TEST_APP2_PKG);
-        assertAppIdle(false); // verify - not idle anymore, since whitelisted
-        assertBackgroundNetworkAccess(true);
-
-        setAppIdleNoAssert(true);
-        assertAppIdle(false); // app is still whitelisted
-        removePowerSaveModeWhitelist(TEST_APP2_PKG);
-        assertAppIdle(true); // verify - idle again, once whitelisted was removed
-        assertBackgroundNetworkAccess(false);
-
-        setAppIdle(true);
-        addPowerSaveModeExceptIdleWhitelist(TEST_APP2_PKG);
-        assertAppIdle(false); // verify - not idle anymore, since whitelisted
-        assertBackgroundNetworkAccess(true);
-
-        setAppIdleNoAssert(true);
-        assertAppIdle(false); // app is still whitelisted
-        removePowerSaveModeExceptIdleWhitelist(TEST_APP2_PKG);
-        assertAppIdle(true); // verify - idle again, once whitelisted was removed
-        assertBackgroundNetworkAccess(false);
-
-        assertsForegroundAlwaysHasNetworkAccess();
-
-        // verify - no whitelist, no access!
-        setAppIdle(true);
-        assertBackgroundNetworkAccess(false);
-    }
-
-    @Test
-    public void testBackgroundNetworkAccess_tempWhitelisted() throws Exception {
-        setAppIdle(true);
-        assertBackgroundNetworkAccess(false);
-
-        addTempPowerSaveModeWhitelist(TEST_APP2_PKG, TEMP_POWERSAVE_WHITELIST_DURATION_MS);
-        assertBackgroundNetworkAccess(true);
-        // Wait until the whitelist duration is expired.
-        SystemClock.sleep(TEMP_POWERSAVE_WHITELIST_DURATION_MS);
-        assertBackgroundNetworkAccess(false);
-    }
-
-    @Test
-    public void testBackgroundNetworkAccess_disabled() throws Exception {
-        assertBackgroundNetworkAccess(true);
-
-        assertsForegroundAlwaysHasNetworkAccess();
-        assertBackgroundNetworkAccess(true);
-    }
-
-    @RequiredProperties({BATTERY_SAVER_MODE})
-    @Test
-    public void testAppIdleNetworkAccess_whenCharging() throws Exception {
-        // Check that app is paroled when charging
-        setAppIdle(true);
-        assertBackgroundNetworkAccess(false);
-        turnBatteryOff();
-        assertBackgroundNetworkAccess(true);
-        turnBatteryOn();
-        assertBackgroundNetworkAccess(false);
-
-        // Check that app is restricted when not idle but power-save is on
-        setAppIdle(false);
-        assertBackgroundNetworkAccess(true);
-        setBatterySaverMode(true);
-        assertBackgroundNetworkAccess(false);
-        // Use setBatterySaverMode API to leave power-save mode instead of plugging in charger
-        setBatterySaverMode(false);
-        turnBatteryOff();
-        assertBackgroundNetworkAccess(true);
-
-        // And when no longer charging, it still has network access, since it's not idle
-        turnBatteryOn();
-        assertBackgroundNetworkAccess(true);
-    }
-
-    @Test
-    public void testAppIdleNetworkAccess_idleWhitelisted() throws Exception {
-        setAppIdle(true);
-        assertAppIdle(true);
-        assertBackgroundNetworkAccess(false);
-
-        addAppIdleWhitelist(mUid);
-        assertBackgroundNetworkAccess(true);
-
-        removeAppIdleWhitelist(mUid);
-        assertBackgroundNetworkAccess(false);
-
-        // Make sure whitelisting a random app doesn't affect the tested app.
-        addAppIdleWhitelist(mUid + 1);
-        assertBackgroundNetworkAccess(false);
-        removeAppIdleWhitelist(mUid + 1);
-    }
-
-    @Test
-    public void testAppIdle_toast() throws Exception {
-        setAppIdle(true);
-        assertAppIdle(true);
-        assertEquals("Shown", showToast());
-        assertAppIdle(true);
-        // Wait for a couple of seconds for the toast to actually be shown
-        SystemClock.sleep(2000);
-        assertAppIdle(true);
-    }
-}
diff --git a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/AbstractBatterySaverModeTestCase.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/AbstractBatterySaverModeTestCase.java
deleted file mode 100644
index ae226e2..0000000
--- a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/AbstractBatterySaverModeTestCase.java
+++ /dev/null
@@ -1,111 +0,0 @@
-/*
- * Copyright (C) 2016 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.cts.netpolicy.hostside;
-
-import static com.android.cts.netpolicy.hostside.Property.BATTERY_SAVER_MODE;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-
-/**
- * Base class for metered and non-metered Battery Saver Mode tests.
- */
-@RequiredProperties({BATTERY_SAVER_MODE})
-abstract class AbstractBatterySaverModeTestCase extends AbstractRestrictBackgroundNetworkTestCase {
-
-    @Before
-    public final void setUp() throws Exception {
-        super.setUp();
-
-        // Set initial state.
-        removePowerSaveModeWhitelist(TEST_APP2_PKG);
-        removePowerSaveModeExceptIdleWhitelist(TEST_APP2_PKG);
-        setBatterySaverMode(false);
-
-        registerBroadcastReceiver();
-    }
-
-    @After
-    public final void tearDown() throws Exception {
-        super.tearDown();
-
-        setBatterySaverMode(false);
-    }
-
-    @Test
-    public void testBackgroundNetworkAccess_enabled() throws Exception {
-        setBatterySaverMode(true);
-        assertBackgroundNetworkAccess(false);
-
-        assertsForegroundAlwaysHasNetworkAccess();
-        assertBackgroundNetworkAccess(false);
-
-        // Make sure foreground app doesn't lose access upon Battery Saver.
-        setBatterySaverMode(false);
-        launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_ACTIVTIY);
-        setBatterySaverMode(true);
-        assertTopNetworkAccess(true);
-
-        // Although it should not have access while the screen is off.
-        turnScreenOff();
-        assertBackgroundNetworkAccess(false);
-        turnScreenOn();
-        assertTopNetworkAccess(true);
-
-        // Goes back to background state.
-        finishActivity();
-        assertBackgroundNetworkAccess(false);
-
-        // Make sure foreground service doesn't lose access upon enabling Battery Saver.
-        setBatterySaverMode(false);
-        launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_FOREGROUND_SERVICE);
-        setBatterySaverMode(true);
-        assertForegroundServiceNetworkAccess();
-        stopForegroundService();
-        assertBackgroundNetworkAccess(false);
-    }
-
-    @Test
-    public void testBackgroundNetworkAccess_whitelisted() throws Exception {
-        setBatterySaverMode(true);
-        assertBackgroundNetworkAccess(false);
-
-        addPowerSaveModeWhitelist(TEST_APP2_PKG);
-        assertBackgroundNetworkAccess(true);
-
-        removePowerSaveModeWhitelist(TEST_APP2_PKG);
-        assertBackgroundNetworkAccess(false);
-
-        addPowerSaveModeExceptIdleWhitelist(TEST_APP2_PKG);
-        assertBackgroundNetworkAccess(true);
-
-        removePowerSaveModeExceptIdleWhitelist(TEST_APP2_PKG);
-        assertBackgroundNetworkAccess(false);
-
-        assertsForegroundAlwaysHasNetworkAccess();
-        assertBackgroundNetworkAccess(false);
-    }
-
-    @Test
-    public void testBackgroundNetworkAccess_disabled() throws Exception {
-        assertBackgroundNetworkAccess(true);
-
-        assertsForegroundAlwaysHasNetworkAccess();
-        assertBackgroundNetworkAccess(true);
-    }
-}
diff --git a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/AbstractDefaultRestrictionsTest.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/AbstractDefaultRestrictionsTest.java
deleted file mode 100644
index 00f67f4..0000000
--- a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/AbstractDefaultRestrictionsTest.java
+++ /dev/null
@@ -1,93 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.cts.netpolicy.hostside;
-
-import static android.app.ActivityManager.PROCESS_STATE_LAST_ACTIVITY;
-
-import static org.junit.Assume.assumeTrue;
-
-import android.os.SystemClock;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-
-/**
- * Base class for default, always-on network restrictions.
- */
-abstract class AbstractDefaultRestrictionsTest extends AbstractRestrictBackgroundNetworkTestCase {
-
-    @Before
-    public final void setUp() throws Exception {
-        super.setUp();
-
-        removePowerSaveModeWhitelist(TEST_APP2_PKG);
-        removePowerSaveModeExceptIdleWhitelist(TEST_APP2_PKG);
-
-        registerBroadcastReceiver();
-        assumeTrue("Feature not enabled", isNetworkBlockedForTopSleepingAndAbove());
-    }
-
-    @After
-    public final void tearDown() throws Exception {
-        super.tearDown();
-
-        stopApp();
-        removePowerSaveModeWhitelist(TEST_APP2_PKG);
-        removePowerSaveModeExceptIdleWhitelist(TEST_APP2_PKG);
-    }
-
-    @Test
-    public void testFgsNetworkAccess() throws Exception {
-        assertProcessStateBelow(PROCESS_STATE_LAST_ACTIVITY);
-        SystemClock.sleep(mProcessStateTransitionShortDelayMs);
-        assertNetworkAccess(false, null);
-
-        launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_FOREGROUND_SERVICE);
-    }
-
-    @Test
-    public void testActivityNetworkAccess() throws Exception {
-        assertProcessStateBelow(PROCESS_STATE_LAST_ACTIVITY);
-        SystemClock.sleep(mProcessStateTransitionShortDelayMs);
-        assertNetworkAccess(false, null);
-
-        launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_ACTIVTIY);
-    }
-
-    @Test
-    public void testBackgroundNetworkAccess_inFullAllowlist() throws Exception {
-        assertProcessStateBelow(PROCESS_STATE_LAST_ACTIVITY);
-        SystemClock.sleep(mProcessStateTransitionShortDelayMs);
-        assertNetworkAccess(false, null);
-
-        addPowerSaveModeWhitelist(TEST_APP2_PKG);
-        assertProcessStateBelow(PROCESS_STATE_LAST_ACTIVITY);
-        assertNetworkAccess(true, null);
-    }
-
-    @Test
-    public void testBackgroundNetworkAccess_inExceptIdleAllowlist() throws Exception {
-        assertProcessStateBelow(PROCESS_STATE_LAST_ACTIVITY);
-        SystemClock.sleep(mProcessStateTransitionShortDelayMs);
-        assertNetworkAccess(false, null);
-
-        addPowerSaveModeExceptIdleWhitelist(TEST_APP2_PKG);
-        assertProcessStateBelow(PROCESS_STATE_LAST_ACTIVITY);
-        assertNetworkAccess(true, null);
-    }
-}
diff --git a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/AbstractDozeModeTestCase.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/AbstractDozeModeTestCase.java
deleted file mode 100644
index 0c8cb70..0000000
--- a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/AbstractDozeModeTestCase.java
+++ /dev/null
@@ -1,143 +0,0 @@
-/*
- * Copyright (C) 2016 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.cts.netpolicy.hostside;
-
-import static android.app.ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE;
-
-import static com.android.cts.netpolicy.hostside.Property.DOZE_MODE;
-import static com.android.cts.netpolicy.hostside.Property.NOT_LOW_RAM_DEVICE;
-
-import android.os.SystemClock;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-
-/**
- * Base class for metered and non-metered Doze Mode tests.
- */
-@RequiredProperties({DOZE_MODE})
-abstract class AbstractDozeModeTestCase extends AbstractRestrictBackgroundNetworkTestCase {
-
-    @Before
-    public final void setUp() throws Exception {
-        super.setUp();
-
-        // Set initial state.
-        removePowerSaveModeWhitelist(TEST_APP2_PKG);
-        removePowerSaveModeExceptIdleWhitelist(TEST_APP2_PKG);
-        setDozeMode(false);
-
-        registerBroadcastReceiver();
-    }
-
-    @After
-    public final void tearDown() throws Exception {
-        super.tearDown();
-
-        setDozeMode(false);
-    }
-
-    @Test
-    public void testBackgroundNetworkAccess_enabled() throws Exception {
-        setDozeMode(true);
-        assertBackgroundNetworkAccess(false);
-
-        assertsForegroundAlwaysHasNetworkAccess();
-        assertBackgroundNetworkAccess(false);
-
-        // Make sure foreground service doesn't lose network access upon enabling doze.
-        setDozeMode(false);
-        launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_FOREGROUND_SERVICE);
-        setDozeMode(true);
-        assertForegroundServiceNetworkAccess();
-        stopForegroundService();
-        assertProcessStateBelow(PROCESS_STATE_BOUND_FOREGROUND_SERVICE);
-        assertBackgroundNetworkAccess(false);
-    }
-
-    @Test
-    public void testBackgroundNetworkAccess_whitelisted() throws Exception {
-        setDozeMode(true);
-        assertBackgroundNetworkAccess(false);
-
-        addPowerSaveModeWhitelist(TEST_APP2_PKG);
-        assertBackgroundNetworkAccess(true);
-
-        removePowerSaveModeWhitelist(TEST_APP2_PKG);
-        assertBackgroundNetworkAccess(false);
-
-        addPowerSaveModeExceptIdleWhitelist(TEST_APP2_PKG);
-        assertBackgroundNetworkAccess(false);
-
-        removePowerSaveModeExceptIdleWhitelist(TEST_APP2_PKG);
-        assertBackgroundNetworkAccess(false);
-
-        assertsForegroundAlwaysHasNetworkAccess();
-        assertBackgroundNetworkAccess(false);
-    }
-
-    @Test
-    public void testBackgroundNetworkAccess_disabled() throws Exception {
-        assertBackgroundNetworkAccess(true);
-
-        assertsForegroundAlwaysHasNetworkAccess();
-        assertBackgroundNetworkAccess(true);
-    }
-
-    @RequiredProperties({NOT_LOW_RAM_DEVICE})
-    @Test
-    public void testBackgroundNetworkAccess_enabledButWhitelistedOnNotificationAction()
-            throws Exception {
-        setPendingIntentAllowlistDuration(NETWORK_TIMEOUT_MS);
-        try {
-            registerNotificationListenerService();
-            setDozeMode(true);
-            assertBackgroundNetworkAccess(false);
-
-            testNotification(4, NOTIFICATION_TYPE_CONTENT);
-            testNotification(8, NOTIFICATION_TYPE_DELETE);
-            testNotification(15, NOTIFICATION_TYPE_FULL_SCREEN);
-            testNotification(16, NOTIFICATION_TYPE_BUNDLE);
-            testNotification(23, NOTIFICATION_TYPE_ACTION);
-            testNotification(42, NOTIFICATION_TYPE_ACTION_BUNDLE);
-            testNotification(108, NOTIFICATION_TYPE_ACTION_REMOTE_INPUT);
-        } finally {
-            resetDeviceIdleSettings();
-        }
-    }
-
-    private void testNotification(int id, String type) throws Exception {
-        sendNotification(id, type);
-        assertBackgroundNetworkAccess(true);
-        if (type.equals(NOTIFICATION_TYPE_ACTION)) {
-            // Make sure access is disabled after it expires. Since this check considerably slows
-            // downs the CTS tests, do it just once.
-            SystemClock.sleep(NETWORK_TIMEOUT_MS);
-            assertBackgroundNetworkAccess(false);
-        }
-    }
-
-    // Must override so it only tests foreground service - once an app goes to foreground, device
-    // leaves Doze Mode.
-    @Override
-    protected void assertsForegroundAlwaysHasNetworkAccess() throws Exception {
-        launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_FOREGROUND_SERVICE);
-        stopForegroundService();
-        assertProcessStateBelow(PROCESS_STATE_BOUND_FOREGROUND_SERVICE);
-    }
-}
diff --git a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/AbstractExpeditedJobTest.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/AbstractExpeditedJobTest.java
deleted file mode 100644
index 5435920..0000000
--- a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/AbstractExpeditedJobTest.java
+++ /dev/null
@@ -1,135 +0,0 @@
-/*
- * Copyright (C) 2021 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.cts.netpolicy.hostside;
-
-import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.setRestrictBackground;
-import static com.android.cts.netpolicy.hostside.Property.APP_STANDBY_MODE;
-import static com.android.cts.netpolicy.hostside.Property.BATTERY_SAVER_MODE;
-import static com.android.cts.netpolicy.hostside.Property.DATA_SAVER_MODE;
-import static com.android.cts.netpolicy.hostside.Property.DOZE_MODE;
-import static com.android.cts.netpolicy.hostside.Property.METERED_NETWORK;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-
-public class AbstractExpeditedJobTest extends AbstractRestrictBackgroundNetworkTestCase {
-    @Before
-    public final void setUp() throws Exception {
-        super.setUp();
-        resetDeviceState();
-    }
-
-    @After
-    public final void tearDown() throws Exception {
-        super.tearDown();
-        resetDeviceState();
-    }
-
-    private void resetDeviceState() throws Exception {
-        resetBatteryState();
-        setBatterySaverMode(false);
-        setRestrictBackground(false);
-        setAppIdle(false);
-        setDozeMode(false);
-    }
-
-    @Test
-    @RequiredProperties({BATTERY_SAVER_MODE})
-    public void testNetworkAccess_batterySaverMode() throws Exception {
-        assertBackgroundNetworkAccess(true);
-        assertExpeditedJobHasNetworkAccess();
-
-        setBatterySaverMode(true);
-        assertBackgroundNetworkAccess(false);
-        assertExpeditedJobHasNetworkAccess();
-    }
-
-    @Test
-    @RequiredProperties({DATA_SAVER_MODE, METERED_NETWORK})
-    public void testNetworkAccess_dataSaverMode() throws Exception {
-        assertBackgroundNetworkAccess(true);
-        assertExpeditedJobHasNetworkAccess();
-
-        setRestrictBackground(true);
-        assertBackgroundNetworkAccess(false);
-        assertExpeditedJobHasNoNetworkAccess();
-    }
-
-    @Test
-    @RequiredProperties({APP_STANDBY_MODE})
-    public void testNetworkAccess_appIdleState() throws Exception {
-        turnBatteryOn();
-        setAppIdle(false);
-        assertBackgroundNetworkAccess(true);
-        assertExpeditedJobHasNetworkAccess();
-
-        setAppIdle(true);
-        assertBackgroundNetworkAccess(false);
-        assertExpeditedJobHasNetworkAccess();
-    }
-
-    @Test
-    @RequiredProperties({DOZE_MODE})
-    public void testNetworkAccess_dozeMode() throws Exception {
-        assertBackgroundNetworkAccess(true);
-        assertExpeditedJobHasNetworkAccess();
-
-        setDozeMode(true);
-        assertBackgroundNetworkAccess(false);
-        assertExpeditedJobHasNetworkAccess();
-    }
-
-    @Test
-    @RequiredProperties({DATA_SAVER_MODE, BATTERY_SAVER_MODE, METERED_NETWORK})
-    public void testNetworkAccess_dataAndBatterySaverMode() throws Exception {
-        assertBackgroundNetworkAccess(true);
-        assertExpeditedJobHasNetworkAccess();
-
-        setRestrictBackground(true);
-        setBatterySaverMode(true);
-        assertBackgroundNetworkAccess(false);
-        assertExpeditedJobHasNoNetworkAccess();
-    }
-
-    @Test
-    @RequiredProperties({DOZE_MODE, DATA_SAVER_MODE, METERED_NETWORK})
-    public void testNetworkAccess_dozeAndDataSaverMode() throws Exception {
-        assertBackgroundNetworkAccess(true);
-        assertExpeditedJobHasNetworkAccess();
-
-        setRestrictBackground(true);
-        setDozeMode(true);
-        assertBackgroundNetworkAccess(false);
-        assertExpeditedJobHasNoNetworkAccess();
-    }
-
-    @Test
-    @RequiredProperties({DATA_SAVER_MODE, BATTERY_SAVER_MODE, METERED_NETWORK, DOZE_MODE,
-            APP_STANDBY_MODE})
-    public void testNetworkAccess_allRestrictionsEnabled() throws Exception {
-        assertBackgroundNetworkAccess(true);
-        assertExpeditedJobHasNetworkAccess();
-
-        setRestrictBackground(true);
-        setBatterySaverMode(true);
-        setAppIdle(true);
-        setDozeMode(true);
-        assertBackgroundNetworkAccess(false);
-        assertExpeditedJobHasNoNetworkAccess();
-    }
-}
diff --git a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/AbstractRestrictBackgroundNetworkTestCase.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/AbstractRestrictBackgroundNetworkTestCase.java
deleted file mode 100644
index 0f5f58c..0000000
--- a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/AbstractRestrictBackgroundNetworkTestCase.java
+++ /dev/null
@@ -1,1165 +0,0 @@
-/*
- * Copyright (C) 2016 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.cts.netpolicy.hostside;
-
-import static android.app.ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE;
-import static android.app.ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE;
-import static android.app.ActivityManager.PROCESS_STATE_TOP;
-import static android.app.job.JobScheduler.RESULT_SUCCESS;
-import static android.net.ConnectivityManager.ACTION_RESTRICT_BACKGROUND_CHANGED;
-import static android.os.BatteryManager.BATTERY_PLUGGED_ANY;
-
-import static com.android.cts.netpolicy.arguments.InstrumentationArguments.ARG_CONNECTION_CHECK_CUSTOM_URL;
-import static com.android.cts.netpolicy.arguments.InstrumentationArguments.ARG_WAIVE_BIND_PRIORITY;
-import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.executeShellCommand;
-import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.forceRunJob;
-import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.getConnectivityManager;
-import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.getContext;
-import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.getInstrumentation;
-import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.isAppStandbySupported;
-import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.isBatterySaverSupported;
-import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.isDozeModeSupported;
-import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.restrictBackgroundValueToString;
-import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.setRestrictBackgroundInternal;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
-
-import android.annotation.NonNull;
-import android.app.Instrumentation;
-import android.app.NotificationManager;
-import android.app.job.JobInfo;
-import android.content.BroadcastReceiver;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.net.ConnectivityManager;
-import android.net.NetworkInfo;
-import android.net.NetworkInfo.DetailedState;
-import android.net.NetworkInfo.State;
-import android.net.NetworkRequest;
-import android.os.BatteryManager;
-import android.os.Binder;
-import android.os.Bundle;
-import android.os.PowerManager;
-import android.os.RemoteCallback;
-import android.os.SystemClock;
-import android.provider.DeviceConfig;
-import android.service.notification.NotificationListenerService;
-import android.util.Log;
-import android.util.Pair;
-
-import androidx.annotation.Nullable;
-import androidx.test.platform.app.InstrumentationRegistry;
-
-import com.android.compatibility.common.util.AmUtils;
-import com.android.compatibility.common.util.BatteryUtils;
-import com.android.compatibility.common.util.DeviceConfigStateHelper;
-import com.android.compatibility.common.util.ThrowingRunnable;
-import com.android.modules.utils.build.SdkLevel;
-
-import org.junit.Rule;
-import org.junit.rules.RuleChain;
-import org.junit.runner.RunWith;
-
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.LinkedBlockingQueue;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicReference;
-import java.util.function.Predicate;
-
-/**
- * Superclass for tests related to background network restrictions.
- */
-@RunWith(NetworkPolicyTestRunner.class)
-public abstract class AbstractRestrictBackgroundNetworkTestCase {
-    public static final String TAG = "RestrictBackgroundNetworkTests";
-
-    protected static final String TEST_PKG = "com.android.cts.netpolicy.hostside";
-    protected static final String TEST_APP2_PKG = "com.android.cts.netpolicy.hostside.app2";
-    // TODO(b/321797685): Configure it via device-config once it is available.
-    protected final long mProcessStateTransitionLongDelayMs =
-            useDifferentDelaysForBackgroundChain() ? TimeUnit.SECONDS.toMillis(20)
-                    : TimeUnit.SECONDS.toMillis(5);
-    protected final long mProcessStateTransitionShortDelayMs =
-            useDifferentDelaysForBackgroundChain() ? TimeUnit.SECONDS.toMillis(2)
-                    : TimeUnit.SECONDS.toMillis(5);
-
-    private static final String TEST_APP2_ACTIVITY_CLASS = TEST_APP2_PKG + ".MyActivity";
-    private static final String TEST_APP2_SERVICE_CLASS = TEST_APP2_PKG + ".MyForegroundService";
-    private static final String TEST_APP2_JOB_SERVICE_CLASS = TEST_APP2_PKG + ".MyJobService";
-
-    private static final ComponentName TEST_JOB_COMPONENT = new ComponentName(
-            TEST_APP2_PKG, TEST_APP2_JOB_SERVICE_CLASS);
-    private static final int TEST_JOB_ID = 7357437;
-
-    private static final int SLEEP_TIME_SEC = 1;
-
-    // Constants below must match values defined on app2's Common.java
-    private static final String MANIFEST_RECEIVER = "ManifestReceiver";
-    private static final String DYNAMIC_RECEIVER = "DynamicReceiver";
-    private static final String ACTION_FINISH_ACTIVITY =
-            "com.android.cts.netpolicy.hostside.app2.action.FINISH_ACTIVITY";
-    private static final String ACTION_FINISH_JOB =
-            "com.android.cts.netpolicy.hostside.app2.action.FINISH_JOB";
-    // Copied from com.android.server.net.NetworkPolicyManagerService class
-    private static final String ACTION_SNOOZE_WARNING =
-            "com.android.server.net.action.SNOOZE_WARNING";
-
-    private static final String ACTION_RECEIVER_READY =
-            "com.android.cts.netpolicy.hostside.app2.action.RECEIVER_READY";
-    static final String ACTION_SHOW_TOAST =
-            "com.android.cts.netpolicy.hostside.app2.action.SHOW_TOAST";
-
-    protected static final String NOTIFICATION_TYPE_CONTENT = "CONTENT";
-    protected static final String NOTIFICATION_TYPE_DELETE = "DELETE";
-    protected static final String NOTIFICATION_TYPE_FULL_SCREEN = "FULL_SCREEN";
-    protected static final String NOTIFICATION_TYPE_BUNDLE = "BUNDLE";
-    protected static final String NOTIFICATION_TYPE_ACTION = "ACTION";
-    protected static final String NOTIFICATION_TYPE_ACTION_BUNDLE = "ACTION_BUNDLE";
-    protected static final String NOTIFICATION_TYPE_ACTION_REMOTE_INPUT = "ACTION_REMOTE_INPUT";
-
-    private static final String NETWORK_STATUS_SEPARATOR = "\\|";
-    private static final int SECOND_IN_MS = 1000;
-    static final int NETWORK_TIMEOUT_MS = 15 * SECOND_IN_MS;
-
-    private static final String KEY_NETWORK_STATE_OBSERVER = TEST_PKG + ".observer";
-    private static final String KEY_SKIP_VALIDATION_CHECKS = TEST_PKG + ".skip_validation_checks";
-    private static final String KEY_CUSTOM_URL = TEST_PKG + ".custom_url";
-
-    private static final String EMPTY_STRING = "";
-
-    protected static final int TYPE_COMPONENT_ACTIVTIY = 0;
-    protected static final int TYPE_COMPONENT_FOREGROUND_SERVICE = 1;
-    protected static final int TYPE_EXPEDITED_JOB = 2;
-
-    private static final int BATTERY_STATE_TIMEOUT_MS = 5000;
-    private static final int BATTERY_STATE_CHECK_INTERVAL_MS = 500;
-
-    private static final int ACTIVITY_NETWORK_STATE_TIMEOUT_MS = 10_000;
-    private static final int JOB_NETWORK_STATE_TIMEOUT_MS = 10_000;
-    private static final int LAUNCH_ACTIVITY_TIMEOUT_MS = 10_000;
-
-    // Must be higher than NETWORK_TIMEOUT_MS
-    private static final int ORDERED_BROADCAST_TIMEOUT_MS = NETWORK_TIMEOUT_MS * 4;
-
-    private static final IntentFilter BATTERY_CHANGED_FILTER =
-            new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
-
-    protected static final long TEMP_POWERSAVE_WHITELIST_DURATION_MS = 20_000; // 20 sec
-
-    private static final long BROADCAST_TIMEOUT_MS = 5_000;
-
-    protected Context mContext;
-    protected Instrumentation mInstrumentation;
-    protected ConnectivityManager mCm;
-    protected int mUid;
-    private int mMyUid;
-    private @Nullable String mCustomUrl;
-    private MyServiceClient mServiceClient;
-    private DeviceConfigStateHelper mDeviceIdleDeviceConfigStateHelper;
-    private PowerManager mPowerManager;
-    private PowerManager.WakeLock mLock;
-
-    @Rule
-    public final RuleChain mRuleChain = RuleChain.outerRule(new RequiredPropertiesRule())
-            .around(new MeterednessConfigurationRule());
-
-    protected void setUp() throws Exception {
-        mInstrumentation = getInstrumentation();
-        mContext = getContext();
-        mCm = getConnectivityManager();
-        mDeviceIdleDeviceConfigStateHelper =
-                new DeviceConfigStateHelper(DeviceConfig.NAMESPACE_DEVICE_IDLE);
-        mUid = getUid(TEST_APP2_PKG);
-        mMyUid = getUid(mContext.getPackageName());
-        mServiceClient = new MyServiceClient(mContext);
-
-        final Bundle args = InstrumentationRegistry.getArguments();
-        mCustomUrl = args.getString(ARG_CONNECTION_CHECK_CUSTOM_URL);
-        if (mCustomUrl != null) {
-            Log.d(TAG, "Using custom URL " + mCustomUrl + " for network checks");
-        }
-
-        final int bindPriorityFlags;
-        if (Boolean.valueOf(args.getString(ARG_WAIVE_BIND_PRIORITY, "false"))) {
-            bindPriorityFlags = Context.BIND_WAIVE_PRIORITY;
-        } else {
-            bindPriorityFlags = Context.BIND_NOT_FOREGROUND;
-        }
-        mServiceClient.bind(bindPriorityFlags);
-
-        mPowerManager = mContext.getSystemService(PowerManager.class);
-        executeShellCommand("cmd netpolicy start-watching " + mUid);
-        // Some of the test cases assume that Data saver mode is initially disabled, which might not
-        // always be the case. Therefore, explicitly disable it before running the tests.
-        // Invoke setRestrictBackgroundInternal() directly instead of going through
-        // setRestrictBackground(), as some devices do not fully support the Data saver mode but
-        // still have certain parts of it enabled by default.
-        setRestrictBackgroundInternal(false);
-        setAppIdle(false);
-        mLock = mPowerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
-
-        Log.i(TAG, "Apps status:\n"
-                + "\ttest app: uid=" + mMyUid + ", state=" + getProcessStateByUid(mMyUid) + "\n"
-                + "\tapp2: uid=" + mUid + ", state=" + getProcessStateByUid(mUid));
-    }
-
-    protected void tearDown() throws Exception {
-        executeShellCommand("cmd netpolicy stop-watching");
-        mServiceClient.unbind();
-        final PowerManager.WakeLock lock = mLock;
-        if (null != lock && lock.isHeld()) lock.release();
-    }
-
-    /**
-     * Check if the feature blocking network for top_sleeping and lower priority proc-states is
-     * enabled. This is a manual check because the feature flag infrastructure may not be available
-     * in all the branches that will get this code.
-     * TODO: b/322115994 - Use @RequiresFlagsEnabled with
-     * Flags.FLAG_NETWORK_BLOCKED_FOR_TOP_SLEEPING_AND_ABOVE once the tests are moved to cts.
-     */
-    protected boolean isNetworkBlockedForTopSleepingAndAbove() {
-        if (!SdkLevel.isAtLeastV()) {
-            return false;
-        }
-        final String output = executeShellCommand("device_config get backstage_power"
-                + " com.android.server.net.network_blocked_for_top_sleeping_and_above");
-        return Boolean.parseBoolean(output);
-    }
-
-    /**
-     * Check if the flag to use different delays for sensitive proc-states is enabled.
-     * This is a manual check because the feature flag infrastructure may not be available
-     * in all the branches that will get this code.
-     * TODO: b/322115994 - Use @RequiresFlagsEnabled with
-     * Flags.FLAG_USE_DIFFERENT_DELAYS_FOR_BACKGROUND_CHAIN once the tests are moved to cts.
-     */
-    private boolean useDifferentDelaysForBackgroundChain() {
-        if (!SdkLevel.isAtLeastV()) {
-            return false;
-        }
-        final String output = executeShellCommand("device_config get backstage_power"
-                + " com.android.server.net.use_different_delays_for_background_chain");
-        return Boolean.parseBoolean(output);
-    }
-
-    protected int getUid(String packageName) throws Exception {
-        return mContext.getPackageManager().getPackageUid(packageName, 0);
-    }
-
-    protected void assertRestrictBackgroundChangedReceived(int expectedCount) throws Exception {
-        assertRestrictBackgroundChangedReceived(DYNAMIC_RECEIVER, expectedCount);
-        assertRestrictBackgroundChangedReceived(MANIFEST_RECEIVER, 0);
-    }
-
-    protected void assertRestrictBackgroundChangedReceived(String receiverName, int expectedCount)
-            throws Exception {
-        int attempts = 0;
-        int count = 0;
-        final int maxAttempts = 5;
-        do {
-            attempts++;
-            count = getNumberBroadcastsReceived(receiverName, ACTION_RESTRICT_BACKGROUND_CHANGED);
-            assertFalse("Expected count " + expectedCount + " but actual is " + count,
-                    count > expectedCount);
-            if (count == expectedCount) {
-                break;
-            }
-            Log.d(TAG, "Expecting count " + expectedCount + " but actual is " + count + " after "
-                    + attempts + " attempts; sleeping "
-                    + SLEEP_TIME_SEC + " seconds before trying again");
-            // No sleep after the last turn
-            if (attempts <= maxAttempts) {
-                SystemClock.sleep(SLEEP_TIME_SEC * SECOND_IN_MS);
-            }
-        } while (attempts <= maxAttempts);
-        assertEquals("Number of expected broadcasts for " + receiverName + " not reached after "
-                + maxAttempts * SLEEP_TIME_SEC + " seconds", expectedCount, count);
-    }
-
-    protected void assertSnoozeWarningNotReceived() throws Exception {
-        // Wait for a while to take broadcast queue delays into account
-        SystemClock.sleep(BROADCAST_TIMEOUT_MS);
-        assertEquals(0, getNumberBroadcastsReceived(DYNAMIC_RECEIVER, ACTION_SNOOZE_WARNING));
-    }
-
-    protected String sendOrderedBroadcast(Intent intent) throws Exception {
-        return sendOrderedBroadcast(intent, ORDERED_BROADCAST_TIMEOUT_MS);
-    }
-
-    protected String sendOrderedBroadcast(Intent intent, int timeoutMs) throws Exception {
-        final LinkedBlockingQueue<String> result = new LinkedBlockingQueue<>(1);
-        Log.d(TAG, "Sending ordered broadcast: " + intent);
-        mContext.sendOrderedBroadcast(intent, null, new BroadcastReceiver() {
-
-            @Override
-            public void onReceive(Context context, Intent intent) {
-                final String resultData = getResultData();
-                if (resultData == null) {
-                    Log.e(TAG, "Received null data from ordered intent");
-                    // Offer an empty string so that the code waiting for the result can return.
-                    result.offer(EMPTY_STRING);
-                    return;
-                }
-                result.offer(resultData);
-            }
-        }, null, 0, null, null);
-
-        final String resultData = result.poll(timeoutMs, TimeUnit.MILLISECONDS);
-        Log.d(TAG, "Ordered broadcast response after " + timeoutMs + "ms: " + resultData );
-        return resultData;
-    }
-
-    protected int getNumberBroadcastsReceived(String receiverName, String action) throws Exception {
-        return mServiceClient.getCounters(receiverName, action);
-    }
-
-    protected void assertRestrictBackgroundStatus(int expectedStatus) throws Exception {
-        final String status = mServiceClient.getRestrictBackgroundStatus();
-        assertNotNull("didn't get API status from app2", status);
-        assertEquals(restrictBackgroundValueToString(expectedStatus),
-                restrictBackgroundValueToString(Integer.parseInt(status)));
-    }
-
-    /**
-     * @deprecated The definition of "background" can be ambiguous. Use separate calls to
-     * {@link #assertProcessStateBelow(int)} with
-     * {@link #assertNetworkAccess(boolean, boolean, String)} to be explicit, instead.
-     */
-    @Deprecated
-    protected void assertBackgroundNetworkAccess(boolean expectAllowed) throws Exception {
-        assertProcessStateBelow(PROCESS_STATE_BOUND_FOREGROUND_SERVICE);
-        assertNetworkAccess(expectAllowed, false, null);
-    }
-
-    protected void assertTopNetworkAccess(boolean expectAllowed) throws Exception {
-        assertTopState();
-        assertNetworkAccess(expectAllowed, true /* needScreenOn */);
-    }
-
-    protected void assertForegroundServiceNetworkAccess() throws Exception {
-        assertForegroundServiceState();
-        assertNetworkAccess(true /* expectAvailable */, false /* needScreenOn */);
-    }
-
-    /**
-     * Asserts that an app always have access while on foreground or running a foreground service.
-     *
-     * <p>This method will launch an activity, a foreground service to make
-     * the assertion, but will finish the activity / stop the service afterwards.
-     */
-    protected void assertsForegroundAlwaysHasNetworkAccess() throws Exception{
-        // Checks foreground first.
-        launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_ACTIVTIY);
-        finishActivity();
-
-        // Then foreground service
-        launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_FOREGROUND_SERVICE);
-        stopForegroundService();
-    }
-
-    protected void assertExpeditedJobHasNetworkAccess() throws Exception {
-        launchComponentAndAssertNetworkAccess(TYPE_EXPEDITED_JOB);
-        finishExpeditedJob();
-    }
-
-    protected void assertExpeditedJobHasNoNetworkAccess() throws Exception {
-        launchComponentAndAssertNetworkAccess(TYPE_EXPEDITED_JOB, false);
-        finishExpeditedJob();
-    }
-
-    /**
-     * Asserts that the process state of the test app is below, in priority, to the given
-     * {@link android.app.ActivityManager.ProcessState}.
-     */
-    protected final void assertProcessStateBelow(int processState) throws Exception {
-        assertProcessState(ps -> ps.state > processState, null);
-    }
-
-    protected final void assertTopState() throws Exception {
-        assertProcessState(ps -> ps.state == PROCESS_STATE_TOP, () -> turnScreenOn());
-    }
-
-    protected final void assertForegroundServiceState() throws Exception {
-        assertProcessState(ps -> ps.state == PROCESS_STATE_FOREGROUND_SERVICE, null);
-    }
-
-    private void assertProcessState(Predicate<ProcessState> statePredicate,
-            ThrowingRunnable onRetry) throws Exception {
-        final int maxTries = 30;
-        ProcessState state = null;
-        for (int i = 1; i <= maxTries; i++) {
-            if (onRetry != null) {
-                onRetry.run();
-            }
-            state = getProcessStateByUid(mUid);
-            Log.v(TAG, "assertProcessState(): status for app2 (" + mUid + ") on attempt #" + i
-                    + ": " + state);
-            if (statePredicate.test(state)) {
-                return;
-            }
-            Log.i(TAG, "App not in desired process state on attempt #" + i
-                    + "; sleeping 1s before trying again");
-            if (i < maxTries) {
-                SystemClock.sleep(SECOND_IN_MS);
-            }
-        }
-        fail("App2 (" + mUid + ") is not in the desired process state after " + maxTries
-                + " attempts: " + state);
-    }
-
-    /**
-     * Asserts whether the active network is available or not. If the network is unavailable, also
-     * checks whether it is blocked by the expected error.
-     *
-     * @param expectAllowed expect background network access to be allowed or not.
-     * @param expectedUnavailableError the expected error when {@code expectAllowed} is false. It's
-     *                                 meaningful only when the {@code expectAllowed} is 'false'.
-     *                                 Throws an IllegalArgumentException when {@code expectAllowed}
-     *                                 is true and this parameter is not null. When the
-     *                                 {@code expectAllowed} is 'false' and this parameter is null,
-     *                                 this function does not compare error type of the networking
-     *                                 access failure.
-     */
-    protected void assertNetworkAccess(boolean expectAllowed, String expectedUnavailableError)
-            throws Exception {
-        if (expectAllowed && expectedUnavailableError != null) {
-            throw new IllegalArgumentException("expectedUnavailableError is not null");
-        }
-        assertNetworkAccess(expectAllowed, false, expectedUnavailableError);
-    }
-
-    /**
-     * Asserts whether the active network is available or not.
-     */
-    private void assertNetworkAccess(boolean expectAvailable, boolean needScreenOn)
-            throws Exception {
-        assertNetworkAccess(expectAvailable, needScreenOn, null);
-    }
-
-    private void assertNetworkAccess(boolean expectAvailable, boolean needScreenOn,
-            @Nullable final String expectedUnavailableError) throws Exception {
-        final int maxTries = 5;
-        String error = null;
-        int timeoutMs = 500;
-
-        for (int i = 1; i <= maxTries; i++) {
-            error = checkNetworkAccess(expectAvailable, expectedUnavailableError);
-
-            if (error == null) return;
-
-            // TODO: ideally, it should retry only when it cannot connect to an external site,
-            // or no retry at all! But, currently, the initial change fails almost always on
-            // battery saver tests because the netd changes are made asynchronously.
-            // Once b/27803922 is fixed, this retry mechanism should be revisited.
-
-            Log.w(TAG, "Network status didn't match for expectAvailable=" + expectAvailable
-                    + " on attempt #" + i + ": " + error + "\n"
-                    + "Sleeping " + timeoutMs + "ms before trying again");
-            if (needScreenOn) {
-                turnScreenOn();
-            }
-            // No sleep after the last turn
-            if (i < maxTries) {
-                SystemClock.sleep(timeoutMs);
-            }
-            // Exponential back-off.
-            timeoutMs = Math.min(timeoutMs*2, NETWORK_TIMEOUT_MS);
-        }
-        fail("Invalid state for " + mUid + "; expectAvailable=" + expectAvailable + " after "
-                + maxTries + " attempts.\nLast error: " + error);
-    }
-
-    /**
-     * Asserts whether the network is blocked by accessing bpf maps if command-line tool supports.
-     */
-    void assertNetworkAccessBlockedByBpf(boolean expectBlocked, int uid, boolean metered) {
-        final String result;
-        try {
-            result = executeShellCommand(
-                    "cmd network_stack is-uid-networking-blocked " + uid + " " + metered);
-        } catch (AssertionError e) {
-            // If NetworkStack is too old to support this command, ignore and continue
-            // this test to verify other parts.
-            if (e.getMessage().contains("No shell command implementation.")) {
-                return;
-            }
-            throw e;
-        }
-
-        // Tethering module is too old.
-        if (result.contains("API is unsupported")) {
-            return;
-        }
-
-        assertEquals(expectBlocked, parseBooleanOrThrow(result.trim()));
-    }
-
-    /**
-     * Similar to {@link Boolean#parseBoolean} but throws when the input
-     * is unexpected instead of returning false.
-     */
-    private static boolean parseBooleanOrThrow(@NonNull String s) {
-        // Don't use Boolean.parseBoolean
-        if ("true".equalsIgnoreCase(s)) return true;
-        if ("false".equalsIgnoreCase(s)) return false;
-        throw new IllegalArgumentException("Unexpected: " + s);
-    }
-
-    /**
-     * Checks whether the network is available as expected.
-     *
-     * @return error message with the mismatch (or empty if assertion passed).
-     */
-    private String checkNetworkAccess(boolean expectAvailable,
-            @Nullable final String expectedUnavailableError) throws Exception {
-        final NetworkCheckResult checkResult = mServiceClient.checkNetworkStatus(mCustomUrl);
-        return checkForAvailabilityInNetworkCheckResult(checkResult, expectAvailable,
-                expectedUnavailableError);
-    }
-
-    private String checkForAvailabilityInNetworkCheckResult(NetworkCheckResult networkCheckResult,
-            boolean expectAvailable, @Nullable final String expectedUnavailableError) {
-        assertNotNull("NetworkCheckResult from app2 is null", networkCheckResult);
-
-        final NetworkInfo networkInfo = networkCheckResult.networkInfo;
-        assertNotNull("NetworkInfo from app2 is null", networkInfo);
-
-        final State state = networkInfo.getState();
-        final DetailedState detailedState = networkInfo.getDetailedState();
-
-        final boolean connected = networkCheckResult.connected;
-        final String connectionCheckDetails = networkCheckResult.details;
-
-        final StringBuilder errors = new StringBuilder();
-        final State expectedState;
-        final DetailedState expectedDetailedState;
-        if (expectAvailable) {
-            expectedState = State.CONNECTED;
-            expectedDetailedState = DetailedState.CONNECTED;
-        } else {
-            expectedState = State.DISCONNECTED;
-            expectedDetailedState = DetailedState.BLOCKED;
-        }
-
-        if (expectAvailable != connected) {
-            errors.append(String.format("External site connection failed: expected %s, got %s\n",
-                    expectAvailable, connected));
-        }
-        if (expectedState != state || expectedDetailedState != detailedState) {
-            errors.append(String.format("Connection state mismatch: expected %s/%s, got %s/%s\n",
-                    expectedState, expectedDetailedState, state, detailedState));
-        } else if (!expectAvailable && (expectedUnavailableError != null)
-                 && !connectionCheckDetails.contains(expectedUnavailableError)) {
-            errors.append("Connection unavailable reason mismatch: expected "
-                     + expectedUnavailableError + "\n");
-        }
-
-        if (errors.length() > 0) {
-            errors.append("\tnetworkInfo: " + networkInfo + "\n");
-            errors.append("\tconnectionCheckDetails: " + connectionCheckDetails + "\n");
-        }
-        return errors.length() == 0 ? null : errors.toString();
-    }
-
-    /**
-     * Runs a Shell command which is not expected to generate output.
-     */
-    protected void executeSilentShellCommand(String command) {
-        final String result = executeShellCommand(command);
-        assertTrue("Command '" + command + "' failed: " + result, result.trim().isEmpty());
-    }
-
-    /**
-     * Asserts the result of a command, wait and re-running it a couple times if necessary.
-     */
-    protected void assertDelayedShellCommand(String command, final String expectedResult)
-            throws Exception {
-        assertDelayedShellCommand(command, 5, 1, expectedResult);
-    }
-
-    protected void assertDelayedShellCommand(String command, int maxTries, int napTimeSeconds,
-            final String expectedResult) throws Exception {
-        assertDelayedShellCommand(command, maxTries, napTimeSeconds, new ExpectResultChecker() {
-
-            @Override
-            public boolean isExpected(String result) {
-                return expectedResult.equals(result);
-            }
-
-            @Override
-            public String getExpected() {
-                return expectedResult;
-            }
-        });
-    }
-
-    protected void assertDelayedShellCommand(String command, int maxTries, int napTimeSeconds,
-            ExpectResultChecker checker) throws Exception {
-        String result = "";
-        for (int i = 1; i <= maxTries; i++) {
-            result = executeShellCommand(command).trim();
-            if (checker.isExpected(result)) return;
-            Log.v(TAG, "Command '" + command + "' returned '" + result + " instead of '"
-                    + checker.getExpected() + "' on attempt #" + i
-                    + "; sleeping " + napTimeSeconds + "s before trying again");
-            // No sleep after the last turn
-            if (i < maxTries) {
-                SystemClock.sleep(napTimeSeconds * SECOND_IN_MS);
-            }
-        }
-        fail("Command '" + command + "' did not return '" + checker.getExpected() + "' after "
-                + maxTries
-                + " attempts. Last result: '" + result + "'");
-    }
-
-    protected void addRestrictBackgroundWhitelist(int uid) throws Exception {
-        executeShellCommand("cmd netpolicy add restrict-background-whitelist " + uid);
-        assertRestrictBackgroundWhitelist(uid, true);
-        // UID policies live by the Highlander rule: "There can be only one".
-        // Hence, if app is whitelisted, it should not be blacklisted.
-        assertRestrictBackgroundBlacklist(uid, false);
-    }
-
-    protected void removeRestrictBackgroundWhitelist(int uid) throws Exception {
-        executeShellCommand("cmd netpolicy remove restrict-background-whitelist " + uid);
-        assertRestrictBackgroundWhitelist(uid, false);
-    }
-
-    protected void assertRestrictBackgroundWhitelist(int uid, boolean expected) throws Exception {
-        assertRestrictBackground("restrict-background-whitelist", uid, expected);
-    }
-
-    protected void addRestrictBackgroundBlacklist(int uid) throws Exception {
-        executeShellCommand("cmd netpolicy add restrict-background-blacklist " + uid);
-        assertRestrictBackgroundBlacklist(uid, true);
-        // UID policies live by the Highlander rule: "There can be only one".
-        // Hence, if app is blacklisted, it should not be whitelisted.
-        assertRestrictBackgroundWhitelist(uid, false);
-    }
-
-    protected void removeRestrictBackgroundBlacklist(int uid) throws Exception {
-        executeShellCommand("cmd netpolicy remove restrict-background-blacklist " + uid);
-        assertRestrictBackgroundBlacklist(uid, false);
-    }
-
-    protected void assertRestrictBackgroundBlacklist(int uid, boolean expected) throws Exception {
-        assertRestrictBackground("restrict-background-blacklist", uid, expected);
-    }
-
-    protected void addAppIdleWhitelist(int uid) throws Exception {
-        executeShellCommand("cmd netpolicy add app-idle-whitelist " + uid);
-        assertAppIdleWhitelist(uid, true);
-    }
-
-    protected void removeAppIdleWhitelist(int uid) throws Exception {
-        executeShellCommand("cmd netpolicy remove app-idle-whitelist " + uid);
-        assertAppIdleWhitelist(uid, false);
-    }
-
-    protected void assertAppIdleWhitelist(int uid, boolean expected) throws Exception {
-        assertRestrictBackground("app-idle-whitelist", uid, expected);
-    }
-
-    private void assertRestrictBackground(String list, int uid, boolean expected) throws Exception {
-        final int maxTries = 5;
-        boolean actual = false;
-        final String expectedUid = Integer.toString(uid);
-        String uids = "";
-        for (int i = 1; i <= maxTries; i++) {
-            final String output =
-                    executeShellCommand("cmd netpolicy list " + list);
-            uids = output.split(":")[1];
-            for (String candidate : uids.split(" ")) {
-                actual = candidate.trim().equals(expectedUid);
-                if (expected == actual) {
-                    return;
-                }
-            }
-            Log.v(TAG, list + " check for uid " + uid + " doesn't match yet (expected "
-                    + expected + ", got " + actual + "); sleeping 1s before polling again");
-            // No sleep after the last turn
-            if (i < maxTries) {
-                SystemClock.sleep(SECOND_IN_MS);
-            }
-        }
-        fail(list + " check for uid " + uid + " failed: expected " + expected + ", got " + actual
-                + ". Full list: " + uids);
-    }
-
-    protected void addTempPowerSaveModeWhitelist(String packageName, long duration)
-            throws Exception {
-        Log.i(TAG, "Adding pkg " + packageName + " to temp-power-save-mode whitelist");
-        executeShellCommand("dumpsys deviceidle tempwhitelist -d " + duration + " " + packageName);
-    }
-
-    protected void assertPowerSaveModeWhitelist(String packageName, boolean expected)
-            throws Exception {
-        // TODO: currently the power-save mode is behaving like idle, but once it changes, we'll
-        // need to use netpolicy for whitelisting
-        assertDelayedShellCommand("dumpsys deviceidle whitelist =" + packageName,
-                Boolean.toString(expected));
-    }
-
-    protected void addPowerSaveModeWhitelist(String packageName) throws Exception {
-        Log.i(TAG, "Adding package " + packageName + " to power-save-mode whitelist");
-        // TODO: currently the power-save mode is behaving like idle, but once it changes, we'll
-        // need to use netpolicy for whitelisting
-        executeShellCommand("dumpsys deviceidle whitelist +" + packageName);
-        assertPowerSaveModeWhitelist(packageName, true);
-    }
-
-    protected void removePowerSaveModeWhitelist(String packageName) throws Exception {
-        Log.i(TAG, "Removing package " + packageName + " from power-save-mode whitelist");
-        // TODO: currently the power-save mode is behaving like idle, but once it changes, we'll
-        // need to use netpolicy for whitelisting
-        executeShellCommand("dumpsys deviceidle whitelist -" + packageName);
-        assertPowerSaveModeWhitelist(packageName, false);
-    }
-
-    protected void assertPowerSaveModeExceptIdleWhitelist(String packageName, boolean expected)
-            throws Exception {
-        // TODO: currently the power-save mode is behaving like idle, but once it changes, we'll
-        // need to use netpolicy for whitelisting
-        assertDelayedShellCommand("dumpsys deviceidle except-idle-whitelist =" + packageName,
-                Boolean.toString(expected));
-    }
-
-    protected void addPowerSaveModeExceptIdleWhitelist(String packageName) throws Exception {
-        Log.i(TAG, "Adding package " + packageName + " to power-save-mode-except-idle whitelist");
-        // TODO: currently the power-save mode is behaving like idle, but once it changes, we'll
-        // need to use netpolicy for whitelisting
-        executeShellCommand("dumpsys deviceidle except-idle-whitelist +" + packageName);
-        assertPowerSaveModeExceptIdleWhitelist(packageName, true);
-    }
-
-    protected void removePowerSaveModeExceptIdleWhitelist(String packageName) throws Exception {
-        Log.i(TAG, "Removing package " + packageName
-                + " from power-save-mode-except-idle whitelist");
-        // TODO: currently the power-save mode is behaving like idle, but once it changes, we'll
-        // need to use netpolicy for whitelisting
-        executeShellCommand("dumpsys deviceidle except-idle-whitelist reset");
-        assertPowerSaveModeExceptIdleWhitelist(packageName, false);
-    }
-
-    protected void turnBatteryOn() throws Exception {
-        executeSilentShellCommand("cmd battery unplug");
-        executeSilentShellCommand("cmd battery set status "
-                + BatteryManager.BATTERY_STATUS_DISCHARGING);
-        assertBatteryState(false);
-    }
-
-    protected void turnBatteryOff() throws Exception {
-        executeSilentShellCommand("cmd battery set ac " + BATTERY_PLUGGED_ANY);
-        executeSilentShellCommand("cmd battery set level 100");
-        executeSilentShellCommand("cmd battery set status "
-                + BatteryManager.BATTERY_STATUS_CHARGING);
-        assertBatteryState(true);
-    }
-
-    protected void resetBatteryState() {
-        BatteryUtils.runDumpsysBatteryReset();
-    }
-
-    private void assertBatteryState(boolean pluggedIn) throws Exception {
-        final long endTime = SystemClock.elapsedRealtime() + BATTERY_STATE_TIMEOUT_MS;
-        while (isDevicePluggedIn() != pluggedIn && SystemClock.elapsedRealtime() <= endTime) {
-            Thread.sleep(BATTERY_STATE_CHECK_INTERVAL_MS);
-        }
-        if (isDevicePluggedIn() != pluggedIn) {
-            fail("Timed out waiting for the plugged-in state to change,"
-                    + " expected pluggedIn: " + pluggedIn);
-        }
-    }
-
-    private boolean isDevicePluggedIn() {
-        final Intent batteryIntent = mContext.registerReceiver(null, BATTERY_CHANGED_FILTER);
-        return batteryIntent.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1) > 0;
-    }
-
-    protected void turnScreenOff() throws Exception {
-        if (!mLock.isHeld()) mLock.acquire();
-        executeSilentShellCommand("input keyevent KEYCODE_SLEEP");
-    }
-
-    protected void turnScreenOn() throws Exception {
-        executeSilentShellCommand("input keyevent KEYCODE_WAKEUP");
-        if (mLock.isHeld()) mLock.release();
-        executeSilentShellCommand("wm dismiss-keyguard");
-    }
-
-    protected void setBatterySaverMode(boolean enabled) throws Exception {
-        if (!isBatterySaverSupported()) {
-            return;
-        }
-        Log.i(TAG, "Setting Battery Saver Mode to " + enabled);
-        if (enabled) {
-            turnBatteryOn();
-            AmUtils.waitForBroadcastBarrier();
-            executeSilentShellCommand("cmd power set-mode 1");
-        } else {
-            executeSilentShellCommand("cmd power set-mode 0");
-            turnBatteryOff();
-            AmUtils.waitForBroadcastBarrier();
-        }
-    }
-
-    protected void setDozeMode(boolean enabled) throws Exception {
-        if (!isDozeModeSupported()) {
-            return;
-        }
-
-        Log.i(TAG, "Setting Doze Mode to " + enabled);
-        if (enabled) {
-            turnBatteryOn();
-            turnScreenOff();
-            executeShellCommand("dumpsys deviceidle force-idle deep");
-        } else {
-            turnScreenOn();
-            turnBatteryOff();
-            executeShellCommand("dumpsys deviceidle unforce");
-        }
-        assertDozeMode(enabled);
-    }
-
-    protected void assertDozeMode(boolean enabled) throws Exception {
-        assertDelayedShellCommand("dumpsys deviceidle get deep", enabled ? "IDLE" : "ACTIVE");
-    }
-
-    protected void stopApp() {
-        executeSilentShellCommand("am stop-app " + TEST_APP2_PKG);
-    }
-
-    protected void setAppIdle(boolean isIdle) throws Exception {
-        setAppIdleNoAssert(isIdle);
-        assertAppIdle(isIdle);
-    }
-
-    protected void setAppIdleNoAssert(boolean isIdle) throws Exception {
-        if (!isAppStandbySupported()) {
-            return;
-        }
-        Log.i(TAG, "Setting app idle to " + isIdle);
-        final String bucketName = isIdle ? "rare" : "active";
-        executeSilentShellCommand("am set-standby-bucket " + TEST_APP2_PKG + " " + bucketName);
-    }
-
-    protected void assertAppIdle(boolean isIdle) throws Exception {
-        try {
-            assertDelayedShellCommand("am get-inactive " + TEST_APP2_PKG,
-                    30 /* maxTries */, 1 /* napTimeSeconds */, "Idle=" + isIdle);
-        } catch (Throwable e) {
-            throw e;
-        }
-    }
-
-    /**
-     * Starts a service that will register a broadcast receiver to receive
-     * {@code RESTRICT_BACKGROUND_CHANGE} intents.
-     * <p>
-     * The service must run in a separate app because otherwise it would be killed every time
-     * {@link #runDeviceTests(String, String)} is executed.
-     */
-    protected void registerBroadcastReceiver() throws Exception {
-        mServiceClient.registerBroadcastReceiver();
-
-        final Intent intent = new Intent(ACTION_RECEIVER_READY)
-                .addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
-        // Wait until receiver is ready.
-        final int maxTries = 10;
-        for (int i = 1; i <= maxTries; i++) {
-            final String message = sendOrderedBroadcast(intent, SECOND_IN_MS * 4);
-            Log.d(TAG, "app2 receiver acked: " + message);
-            if (message != null) {
-                return;
-            }
-            Log.v(TAG, "app2 receiver is not ready yet; sleeping 1s before polling again");
-            // No sleep after the last turn
-            if (i < maxTries) {
-                SystemClock.sleep(SECOND_IN_MS);
-            }
-        }
-        fail("app2 receiver is not ready in " + mUid);
-    }
-
-    protected void registerNetworkCallback(final NetworkRequest request, INetworkCallback cb)
-            throws Exception {
-        Log.i(TAG, "Registering network callback for request: " + request);
-        mServiceClient.registerNetworkCallback(request, cb);
-    }
-
-    protected void unregisterNetworkCallback() throws Exception {
-        mServiceClient.unregisterNetworkCallback();
-    }
-
-    /**
-     * Registers a {@link NotificationListenerService} implementation that will execute the
-     * notification actions right after the notification is sent.
-     */
-    protected void registerNotificationListenerService() throws Exception {
-        executeShellCommand("cmd notification allow_listener "
-                + MyNotificationListenerService.getId());
-        final NotificationManager nm = mContext.getSystemService(NotificationManager.class);
-        final ComponentName listenerComponent = MyNotificationListenerService.getComponentName();
-        assertTrue(listenerComponent + " has not been granted access",
-                nm.isNotificationListenerAccessGranted(listenerComponent));
-    }
-
-    protected void setPendingIntentAllowlistDuration(long durationMs) {
-        mDeviceIdleDeviceConfigStateHelper.set("notification_allowlist_duration_ms",
-                String.valueOf(durationMs));
-    }
-
-    protected void resetDeviceIdleSettings() {
-        mDeviceIdleDeviceConfigStateHelper.restoreOriginalValues();
-    }
-
-    protected void launchActivity() throws Exception {
-        turnScreenOn();
-        final CountDownLatch latch = new CountDownLatch(1);
-        final Intent launchIntent = getIntentForComponent(TYPE_COMPONENT_ACTIVTIY);
-        final RemoteCallback callback = new RemoteCallback(result -> latch.countDown());
-        launchIntent.putExtra(Intent.EXTRA_REMOTE_CALLBACK, callback);
-        mContext.startActivity(launchIntent);
-        // There might be a race when app2 is launched but ACTION_FINISH_ACTIVITY has not registered
-        // before test calls finishActivity(). When the issue is happened, there is no way to fix
-        // it, so have a callback design to make sure that the app is launched completely and
-        // ACTION_FINISH_ACTIVITY will be registered before leaving this method.
-        if (!latch.await(LAUNCH_ACTIVITY_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
-            fail("Timed out waiting for launching activity");
-        }
-    }
-
-    protected void launchComponentAndAssertNetworkAccess(int type) throws Exception {
-        launchComponentAndAssertNetworkAccess(type, true);
-    }
-
-    protected void launchComponentAndAssertNetworkAccess(int type, boolean expectAvailable)
-            throws Exception {
-        if (type == TYPE_COMPONENT_FOREGROUND_SERVICE) {
-            startForegroundService();
-            assertForegroundServiceNetworkAccess();
-        } else if (type == TYPE_COMPONENT_ACTIVTIY) {
-            turnScreenOn();
-            final CountDownLatch latch = new CountDownLatch(1);
-            final Intent launchIntent = getIntentForComponent(type);
-            final Bundle extras = new Bundle();
-            final AtomicReference<Pair<Integer, NetworkCheckResult>> result =
-                    new AtomicReference<>();
-            extras.putBinder(KEY_NETWORK_STATE_OBSERVER, getNewNetworkStateObserver(latch, result));
-            extras.putBoolean(KEY_SKIP_VALIDATION_CHECKS, !expectAvailable);
-            extras.putString(KEY_CUSTOM_URL, mCustomUrl);
-            launchIntent.putExtras(extras);
-            mContext.startActivity(launchIntent);
-            if (latch.await(ACTIVITY_NETWORK_STATE_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
-                final int resultCode = result.get().first;
-                final NetworkCheckResult networkCheckResult = result.get().second;
-                if (resultCode == INetworkStateObserver.RESULT_SUCCESS_NETWORK_STATE_CHECKED) {
-                    final String error = checkForAvailabilityInNetworkCheckResult(
-                            networkCheckResult, expectAvailable,
-                            null /* expectedUnavailableError */);
-                    if (error != null) {
-                        fail("Network is not available for activity in app2 (" + mUid + "): "
-                                + error);
-                    }
-                } else if (resultCode == INetworkStateObserver.RESULT_ERROR_UNEXPECTED_PROC_STATE) {
-                    Log.d(TAG, networkCheckResult.details);
-                    // App didn't come to foreground when the activity is started, so try again.
-                    assertTopNetworkAccess(true);
-                } else {
-                    fail("Unexpected resultCode=" + resultCode
-                            + "; networkCheckResult=[" + networkCheckResult + "]");
-                }
-            } else {
-                fail("Timed out waiting for network availability status from app2's activity ("
-                        + mUid + ")");
-            }
-        } else if (type == TYPE_EXPEDITED_JOB) {
-            final Bundle extras = new Bundle();
-            final AtomicReference<Pair<Integer, NetworkCheckResult>> result =
-                    new AtomicReference<>();
-            final CountDownLatch latch = new CountDownLatch(1);
-            extras.putBinder(KEY_NETWORK_STATE_OBSERVER, getNewNetworkStateObserver(latch, result));
-            extras.putBoolean(KEY_SKIP_VALIDATION_CHECKS, !expectAvailable);
-            extras.putString(KEY_CUSTOM_URL, mCustomUrl);
-            final JobInfo jobInfo = new JobInfo.Builder(TEST_JOB_ID, TEST_JOB_COMPONENT)
-                    .setExpedited(true)
-                    .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
-                    .setTransientExtras(extras)
-                    .build();
-            assertEquals("Error scheduling " + jobInfo,
-                    RESULT_SUCCESS, mServiceClient.scheduleJob(jobInfo));
-            forceRunJob(TEST_APP2_PKG, TEST_JOB_ID);
-            if (latch.await(JOB_NETWORK_STATE_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
-                final int resultCode = result.get().first;
-                final NetworkCheckResult networkCheckResult = result.get().second;
-                if (resultCode == INetworkStateObserver.RESULT_SUCCESS_NETWORK_STATE_CHECKED) {
-                    final String error = checkForAvailabilityInNetworkCheckResult(
-                            networkCheckResult, expectAvailable,
-                            null /* expectedUnavailableError */);
-                    if (error != null) {
-                        Log.d(TAG, "Network state is unexpected, checking again. " + error);
-                        // Right now we could end up in an unexpected state if expedited job
-                        // doesn't have network access immediately after starting, so check again.
-                        assertNetworkAccess(expectAvailable, false /* needScreenOn */);
-                    }
-                } else {
-                    fail("Unexpected resultCode=" + resultCode
-                            + "; networkCheckResult=[" + networkCheckResult + "]");
-                }
-            } else {
-                fail("Timed out waiting for network availability status from app2's expedited job ("
-                        + mUid + ")");
-            }
-        } else {
-            throw new IllegalArgumentException("Unknown type: " + type);
-        }
-    }
-
-    protected void startActivity() throws Exception {
-        final Intent launchIntent = getIntentForComponent(TYPE_COMPONENT_ACTIVTIY);
-        mContext.startActivity(launchIntent);
-    }
-
-    private void startForegroundService() throws Exception {
-        final Intent launchIntent = getIntentForComponent(TYPE_COMPONENT_FOREGROUND_SERVICE);
-        mContext.startForegroundService(launchIntent);
-        assertForegroundServiceState();
-    }
-
-    private Intent getIntentForComponent(int type) {
-        final Intent intent = new Intent();
-        if (type == TYPE_COMPONENT_ACTIVTIY) {
-            intent.setComponent(new ComponentName(TEST_APP2_PKG, TEST_APP2_ACTIVITY_CLASS))
-                    .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP);
-        } else if (type == TYPE_COMPONENT_FOREGROUND_SERVICE) {
-            intent.setComponent(new ComponentName(TEST_APP2_PKG, TEST_APP2_SERVICE_CLASS))
-                    .setFlags(1);
-        } else {
-            fail("Unknown type: " + type);
-        }
-        return intent;
-    }
-
-    protected void stopForegroundService() throws Exception {
-        executeShellCommand(String.format("am startservice -f 2 %s/%s",
-                TEST_APP2_PKG, TEST_APP2_SERVICE_CLASS));
-        // NOTE: cannot assert state because it depends on whether activity was on top before.
-    }
-
-    private Binder getNewNetworkStateObserver(final CountDownLatch latch,
-            final AtomicReference<Pair<Integer, NetworkCheckResult>> result) {
-        return new INetworkStateObserver.Stub() {
-            @Override
-            public void onNetworkStateChecked(int resultCode,
-                    NetworkCheckResult networkCheckResult) {
-                result.set(Pair.create(resultCode, networkCheckResult));
-                latch.countDown();
-            }
-        };
-    }
-
-    /**
-     * Finishes an activity on app2 so its process is demoted from foreground status.
-     */
-    protected void finishActivity() throws Exception {
-        final Intent intent = new Intent(ACTION_FINISH_ACTIVITY)
-                .setPackage(TEST_APP2_PKG)
-                .setFlags(Intent.FLAG_RECEIVER_FOREGROUND | Intent.FLAG_RECEIVER_REGISTERED_ONLY);
-        sendOrderedBroadcast(intent);
-    }
-
-    /**
-     * Finishes the expedited job on app2 so its process is demoted from foreground status.
-     */
-    private void finishExpeditedJob() throws Exception {
-        final Intent intent = new Intent(ACTION_FINISH_JOB)
-                .setPackage(TEST_APP2_PKG)
-                .setFlags(Intent.FLAG_RECEIVER_FOREGROUND | Intent.FLAG_RECEIVER_REGISTERED_ONLY);
-        sendOrderedBroadcast(intent);
-    }
-
-    protected void sendNotification(int notificationId, String notificationType) throws Exception {
-        Log.d(TAG, "Sending notification broadcast (id=" + notificationId
-                + ", type=" + notificationType);
-        mServiceClient.sendNotification(notificationId, notificationType);
-    }
-
-    protected String showToast() {
-        final Intent intent = new Intent(ACTION_SHOW_TOAST);
-        intent.setPackage(TEST_APP2_PKG);
-        Log.d(TAG, "Sending request to show toast");
-        try {
-            return sendOrderedBroadcast(intent, 3 * SECOND_IN_MS);
-        } catch (Exception e) {
-            return "";
-        }
-    }
-
-    private ProcessState getProcessStateByUid(int uid) throws Exception {
-        return new ProcessState(executeShellCommand("cmd activity get-uid-state " + uid));
-    }
-
-    private static class ProcessState {
-        private final String fullState;
-        final int state;
-
-        ProcessState(String fullState) {
-            this.fullState = fullState;
-            try {
-                this.state = Integer.parseInt(fullState.split(" ")[0]);
-            } catch (Exception e) {
-                throw new IllegalArgumentException("Could not parse " + fullState);
-            }
-        }
-
-        @Override
-        public String toString() {
-            return fullState;
-        }
-    }
-
-    /**
-     * Helper class used to assert the result of a Shell command.
-     */
-    protected static interface ExpectResultChecker {
-        /**
-         * Checkes whether the result of the command matched the expectation.
-         */
-        boolean isExpected(String result);
-        /**
-         * Gets the expected result so it's displayed on log and failure messages.
-         */
-        String getExpected();
-    }
-
-    protected void setRestrictedNetworkingMode(boolean enabled) throws Exception {
-        executeSilentShellCommand(
-                "settings put global restricted_networking_mode " + (enabled ? 1 : 0));
-        assertRestrictedNetworkingModeState(enabled);
-    }
-
-    protected void assertRestrictedNetworkingModeState(boolean enabled) throws Exception {
-        assertDelayedShellCommand("cmd netpolicy get restricted-mode",
-                "Restricted mode status: " + (enabled ? "enabled" : "disabled"));
-    }
-}
diff --git a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/AppIdleMeteredTest.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/AppIdleMeteredTest.java
deleted file mode 100644
index 6b802f6..0000000
--- a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/AppIdleMeteredTest.java
+++ /dev/null
@@ -1,23 +0,0 @@
-/*
- * Copyright (C) 2016 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.cts.netpolicy.hostside;
-
-import static com.android.cts.netpolicy.hostside.Property.METERED_NETWORK;
-
-@RequiredProperties({METERED_NETWORK})
-public class AppIdleMeteredTest extends AbstractAppIdleTestCase {
-}
diff --git a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/AppIdleNonMeteredTest.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/AppIdleNonMeteredTest.java
deleted file mode 100644
index 2e725ae..0000000
--- a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/AppIdleNonMeteredTest.java
+++ /dev/null
@@ -1,23 +0,0 @@
-/*
- * Copyright (C) 2016 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.cts.netpolicy.hostside;
-
-import static com.android.cts.netpolicy.hostside.Property.NON_METERED_NETWORK;
-
-@RequiredProperties({NON_METERED_NETWORK})
-public class AppIdleNonMeteredTest extends AbstractAppIdleTestCase {
-}
diff --git a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/BatterySaverModeMeteredTest.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/BatterySaverModeMeteredTest.java
deleted file mode 100644
index 2e421f6..0000000
--- a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/BatterySaverModeMeteredTest.java
+++ /dev/null
@@ -1,23 +0,0 @@
-/*
- * Copyright (C) 2016 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.cts.netpolicy.hostside;
-
-import static com.android.cts.netpolicy.hostside.Property.METERED_NETWORK;
-
-@RequiredProperties({METERED_NETWORK})
-public class BatterySaverModeMeteredTest extends AbstractBatterySaverModeTestCase {
-}
diff --git a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/BatterySaverModeNonMeteredTest.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/BatterySaverModeNonMeteredTest.java
deleted file mode 100644
index 0be5644..0000000
--- a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/BatterySaverModeNonMeteredTest.java
+++ /dev/null
@@ -1,24 +0,0 @@
-/*
- * Copyright (C) 2016 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.cts.netpolicy.hostside;
-
-
-import static com.android.cts.netpolicy.hostside.Property.NON_METERED_NETWORK;
-
-@RequiredProperties({NON_METERED_NETWORK})
-public class BatterySaverModeNonMeteredTest extends AbstractBatterySaverModeTestCase {
-}
diff --git a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/ConnOnActivityStartTest.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/ConnOnActivityStartTest.java
deleted file mode 100644
index bfccce9..0000000
--- a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/ConnOnActivityStartTest.java
+++ /dev/null
@@ -1,129 +0,0 @@
-/*
- * Copyright (C) 2022 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.cts.netpolicy.hostside;
-
-
-import static android.app.ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE;
-import static android.app.ActivityManager.PROCESS_STATE_TOP_SLEEPING;
-
-import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.getUiDevice;
-import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.setRestrictBackground;
-import static com.android.cts.netpolicy.hostside.Property.APP_STANDBY_MODE;
-import static com.android.cts.netpolicy.hostside.Property.BATTERY_SAVER_MODE;
-import static com.android.cts.netpolicy.hostside.Property.DATA_SAVER_MODE;
-import static com.android.cts.netpolicy.hostside.Property.DOZE_MODE;
-import static com.android.cts.netpolicy.hostside.Property.METERED_NETWORK;
-import static com.android.cts.netpolicy.hostside.Property.NON_METERED_NETWORK;
-
-import static org.junit.Assume.assumeTrue;
-
-import android.os.SystemClock;
-import android.util.Log;
-
-import com.android.compatibility.common.util.ThrowingRunnable;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-
-@RequiredProperties({NON_METERED_NETWORK})
-public class ConnOnActivityStartTest extends AbstractRestrictBackgroundNetworkTestCase {
-    private static final int TEST_ITERATION_COUNT = 5;
-
-    @Before
-    public final void setUp() throws Exception {
-        super.setUp();
-        resetDeviceState();
-    }
-
-    @After
-    public final void tearDown() throws Exception {
-        super.tearDown();
-        stopApp();
-        resetDeviceState();
-    }
-
-    private void resetDeviceState() throws Exception {
-        resetBatteryState();
-        setBatterySaverMode(false);
-        setRestrictBackground(false);
-        setAppIdle(false);
-        setDozeMode(false);
-    }
-
-
-    @Test
-    @RequiredProperties({BATTERY_SAVER_MODE})
-    public void testStartActivity_batterySaver() throws Exception {
-        setBatterySaverMode(true);
-        assertNetworkAccess(false, null);
-        assertLaunchedActivityHasNetworkAccess("testStartActivity_batterySaver", null);
-    }
-
-    @Test
-    @RequiredProperties({DATA_SAVER_MODE, METERED_NETWORK})
-    public void testStartActivity_dataSaver() throws Exception {
-        setRestrictBackground(true);
-        assertNetworkAccess(false, null);
-        assertLaunchedActivityHasNetworkAccess("testStartActivity_dataSaver", null);
-    }
-
-    @Test
-    @RequiredProperties({DOZE_MODE})
-    public void testStartActivity_doze() throws Exception {
-        setDozeMode(true);
-        assertNetworkAccess(false, null);
-        // TODO (235284115): We need to turn on Doze every time before starting
-        // the activity.
-        assertLaunchedActivityHasNetworkAccess("testStartActivity_doze", null);
-    }
-
-    @Test
-    @RequiredProperties({APP_STANDBY_MODE})
-    public void testStartActivity_appStandby() throws Exception {
-        turnBatteryOn();
-        setAppIdle(true);
-        assertNetworkAccess(false, null);
-        // TODO (235284115): We need to put the app into app standby mode every
-        // time before starting the activity.
-        assertLaunchedActivityHasNetworkAccess("testStartActivity_appStandby", null);
-    }
-
-    @Test
-    public void testStartActivity_default() throws Exception {
-        assumeTrue("Feature not enabled", isNetworkBlockedForTopSleepingAndAbove());
-        assertLaunchedActivityHasNetworkAccess("testStartActivity_default", () -> {
-            assertProcessStateBelow(PROCESS_STATE_TOP_SLEEPING);
-            SystemClock.sleep(mProcessStateTransitionLongDelayMs);
-            assertNetworkAccess(false, null);
-        });
-    }
-
-    private void assertLaunchedActivityHasNetworkAccess(String testName,
-            ThrowingRunnable onBeginIteration) throws Exception {
-        for (int i = 0; i < TEST_ITERATION_COUNT; ++i) {
-            if (onBeginIteration != null) {
-                onBeginIteration.run();
-            }
-            Log.i(TAG, testName + " start #" + i);
-            launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_ACTIVTIY);
-            getUiDevice().pressHome();
-            assertProcessStateBelow(PROCESS_STATE_BOUND_FOREGROUND_SERVICE);
-            Log.i(TAG, testName + " end #" + i);
-        }
-    }
-}
diff --git a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/DataSaverModeTest.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/DataSaverModeTest.java
deleted file mode 100644
index 66e0d00..0000000
--- a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/DataSaverModeTest.java
+++ /dev/null
@@ -1,213 +0,0 @@
-/*
- * Copyright (C) 2016 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.cts.netpolicy.hostside;
-
-import static android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_DISABLED;
-import static android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED;
-import static android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_WHITELISTED;
-
-import static com.android.compatibility.common.util.FeatureUtil.isTV;
-import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.setRestrictBackground;
-import static com.android.cts.netpolicy.hostside.Property.DATA_SAVER_MODE;
-import static com.android.cts.netpolicy.hostside.Property.METERED_NETWORK;
-import static com.android.cts.netpolicy.hostside.Property.NO_DATA_SAVER_MODE;
-
-import static org.junit.Assert.fail;
-
-import androidx.test.filters.LargeTest;
-
-import com.android.compatibility.common.util.CddTest;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-
-@RequiredProperties({DATA_SAVER_MODE, METERED_NETWORK})
-@LargeTest
-public class DataSaverModeTest extends AbstractRestrictBackgroundNetworkTestCase {
-
-    private static final String[] REQUIRED_WHITELISTED_PACKAGES = {
-        "com.android.providers.downloads"
-    };
-
-    @Before
-    public void setUp() throws Exception {
-        super.setUp();
-
-        // Set initial state.
-        setRestrictBackground(false);
-        removeRestrictBackgroundWhitelist(mUid);
-        removeRestrictBackgroundBlacklist(mUid);
-
-        registerBroadcastReceiver();
-        assertRestrictBackgroundChangedReceived(0);
-   }
-
-    @After
-    public void tearDown() throws Exception {
-        super.tearDown();
-
-        setRestrictBackground(false);
-    }
-
-    @Test
-    public void testGetRestrictBackgroundStatus_disabled() throws Exception {
-        assertDataSaverStatusOnBackground(RESTRICT_BACKGROUND_STATUS_DISABLED);
-
-        // Verify status is always disabled, never whitelisted
-        addRestrictBackgroundWhitelist(mUid);
-        assertRestrictBackgroundChangedReceived(0);
-        assertDataSaverStatusOnBackground(RESTRICT_BACKGROUND_STATUS_DISABLED);
-
-        assertsForegroundAlwaysHasNetworkAccess();
-        assertDataSaverStatusOnBackground(RESTRICT_BACKGROUND_STATUS_DISABLED);
-    }
-
-    @Test
-    public void testGetRestrictBackgroundStatus_whitelisted() throws Exception {
-        setRestrictBackground(true);
-        assertRestrictBackgroundChangedReceived(1);
-        assertDataSaverStatusOnBackground(RESTRICT_BACKGROUND_STATUS_ENABLED);
-
-        addRestrictBackgroundWhitelist(mUid);
-        assertRestrictBackgroundChangedReceived(2);
-        assertDataSaverStatusOnBackground(RESTRICT_BACKGROUND_STATUS_WHITELISTED);
-
-        removeRestrictBackgroundWhitelist(mUid);
-        assertRestrictBackgroundChangedReceived(3);
-        assertDataSaverStatusOnBackground(RESTRICT_BACKGROUND_STATUS_ENABLED);
-
-        assertsForegroundAlwaysHasNetworkAccess();
-        assertDataSaverStatusOnBackground(RESTRICT_BACKGROUND_STATUS_ENABLED);
-    }
-
-    @Test
-    public void testGetRestrictBackgroundStatus_enabled() throws Exception {
-        setRestrictBackground(true);
-        assertRestrictBackgroundChangedReceived(1);
-        assertDataSaverStatusOnBackground(RESTRICT_BACKGROUND_STATUS_ENABLED);
-
-        assertsForegroundAlwaysHasNetworkAccess();
-        assertDataSaverStatusOnBackground(RESTRICT_BACKGROUND_STATUS_ENABLED);
-
-        // Make sure foreground app doesn't lose access upon enabling Data Saver.
-        setRestrictBackground(false);
-        launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_ACTIVTIY);
-        setRestrictBackground(true);
-        assertTopNetworkAccess(true);
-
-        // Although it should not have access while the screen is off.
-        turnScreenOff();
-        assertBackgroundNetworkAccess(false);
-        turnScreenOn();
-        // On some TVs, it is possible that the activity on top may change after the screen is
-        // turned off and on again, so relaunch the activity in the test app again.
-        if (isTV()) {
-            startActivity();
-        }
-        assertTopNetworkAccess(true);
-
-        // Goes back to background state.
-        finishActivity();
-        assertBackgroundNetworkAccess(false);
-
-        // Make sure foreground service doesn't lose access upon enabling Data Saver.
-        setRestrictBackground(false);
-        launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_FOREGROUND_SERVICE);
-        setRestrictBackground(true);
-        assertForegroundServiceNetworkAccess();
-        stopForegroundService();
-        assertBackgroundNetworkAccess(false);
-    }
-
-    @Test
-    public void testGetRestrictBackgroundStatus_blacklisted() throws Exception {
-        addRestrictBackgroundBlacklist(mUid);
-        assertRestrictBackgroundChangedReceived(1);
-        assertDataSaverStatusOnBackground(RESTRICT_BACKGROUND_STATUS_ENABLED);
-
-        assertsForegroundAlwaysHasNetworkAccess();
-        assertRestrictBackgroundChangedReceived(1);
-        assertDataSaverStatusOnBackground(RESTRICT_BACKGROUND_STATUS_ENABLED);
-
-        // UID policies live by the Highlander rule: "There can be only one".
-        // Hence, if app is whitelisted, it should not be blacklisted anymore.
-        setRestrictBackground(true);
-        assertRestrictBackgroundChangedReceived(2);
-        assertDataSaverStatusOnBackground(RESTRICT_BACKGROUND_STATUS_ENABLED);
-        addRestrictBackgroundWhitelist(mUid);
-        assertRestrictBackgroundChangedReceived(3);
-        assertDataSaverStatusOnBackground(RESTRICT_BACKGROUND_STATUS_WHITELISTED);
-
-        // Check status after removing blacklist.
-        // ...re-enables first
-        addRestrictBackgroundBlacklist(mUid);
-        assertRestrictBackgroundChangedReceived(4);
-        assertDataSaverStatusOnBackground(RESTRICT_BACKGROUND_STATUS_ENABLED);
-        assertsForegroundAlwaysHasNetworkAccess();
-        // ... remove blacklist - access's still rejected because Data Saver is on
-        removeRestrictBackgroundBlacklist(mUid);
-        assertRestrictBackgroundChangedReceived(4);
-        assertDataSaverStatusOnBackground(RESTRICT_BACKGROUND_STATUS_ENABLED);
-        assertsForegroundAlwaysHasNetworkAccess();
-        // ... finally, disable Data Saver
-        setRestrictBackground(false);
-        assertRestrictBackgroundChangedReceived(5);
-        assertDataSaverStatusOnBackground(RESTRICT_BACKGROUND_STATUS_DISABLED);
-        assertsForegroundAlwaysHasNetworkAccess();
-    }
-
-    @Test
-    public void testGetRestrictBackgroundStatus_requiredWhitelistedPackages() throws Exception {
-        final StringBuilder error = new StringBuilder();
-        for (String packageName : REQUIRED_WHITELISTED_PACKAGES) {
-            int uid = -1;
-            try {
-                uid = getUid(packageName);
-                assertRestrictBackgroundWhitelist(uid, true);
-            } catch (Throwable t) {
-                error.append("\nFailed for '").append(packageName).append("'");
-                if (uid > 0) {
-                    error.append(" (uid ").append(uid).append(")");
-                }
-                error.append(": ").append(t).append("\n");
-            }
-        }
-        if (error.length() > 0) {
-            fail(error.toString());
-        }
-    }
-
-    @RequiredProperties({NO_DATA_SAVER_MODE})
-    @CddTest(requirement="7.4.7/C-2-2")
-    @Test
-    public void testBroadcastNotSentOnUnsupportedDevices() throws Exception {
-        setRestrictBackground(true);
-        assertRestrictBackgroundChangedReceived(0);
-
-        setRestrictBackground(false);
-        assertRestrictBackgroundChangedReceived(0);
-
-        setRestrictBackground(true);
-        assertRestrictBackgroundChangedReceived(0);
-    }
-
-    private void assertDataSaverStatusOnBackground(int expectedStatus) throws Exception {
-        assertRestrictBackgroundStatus(expectedStatus);
-        assertBackgroundNetworkAccess(expectedStatus != RESTRICT_BACKGROUND_STATUS_ENABLED);
-    }
-}
diff --git a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/DataWarningReceiverTest.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/DataWarningReceiverTest.java
deleted file mode 100644
index 69ca206..0000000
--- a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/DataWarningReceiverTest.java
+++ /dev/null
@@ -1,108 +0,0 @@
-/*
- * Copyright (C) 2021 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.cts.netpolicy.hostside;
-
-import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.clearSnoozeTimestamps;
-
-import android.content.pm.PackageManager;
-import android.telephony.SubscriptionManager;
-import android.telephony.SubscriptionPlan;
-
-import androidx.test.platform.app.InstrumentationRegistry;
-import androidx.test.uiautomator.By;
-import androidx.test.uiautomator.Direction;
-import androidx.test.uiautomator.UiDevice;
-import androidx.test.uiautomator.UiObject2;
-import androidx.test.uiautomator.Until;
-
-import com.android.compatibility.common.util.SystemUtil;
-import com.android.compatibility.common.util.UiAutomatorUtils2;
-
-import org.junit.After;
-import org.junit.Assume;
-import org.junit.Before;
-import org.junit.Test;
-
-import java.time.Period;
-import java.time.ZonedDateTime;
-import java.util.Arrays;
-import java.util.List;
-
-public class DataWarningReceiverTest extends AbstractRestrictBackgroundNetworkTestCase {
-
-    @Before
-    public void setUp() throws Exception {
-        super.setUp();
-
-        clearSnoozeTimestamps();
-        registerBroadcastReceiver();
-        turnScreenOn();
-    }
-
-    @After
-    public void tearDown() throws Exception {
-        super.tearDown();
-    }
-
-    @Test
-    public void testSnoozeWarningNotReceived() throws Exception {
-        Assume.assumeTrue("Feature not supported: " + PackageManager.FEATURE_TELEPHONY,
-                mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELEPHONY));
-        final SubscriptionManager sm = mContext.getSystemService(SubscriptionManager.class);
-        final int subId = SubscriptionManager.getDefaultDataSubscriptionId();
-        Assume.assumeTrue("Valid subId not found",
-                subId != SubscriptionManager.INVALID_SUBSCRIPTION_ID);
-
-        setSubPlanOwner(subId, TEST_PKG);
-        final List<SubscriptionPlan> originalPlans = sm.getSubscriptionPlans(subId);
-        try {
-            // In NetworkPolicyManagerService class, we set the data warning bytes to 90% of
-            // data limit bytes. So, create the subscription plan in such a way this data warning
-            // threshold is already reached.
-            final SubscriptionPlan plan = SubscriptionPlan.Builder
-                    .createRecurring(ZonedDateTime.parse("2007-03-14T00:00:00.000Z"),
-                            Period.ofMonths(1))
-                    .setTitle("CTS")
-                    .setDataLimit(1_000_000_000, SubscriptionPlan.LIMIT_BEHAVIOR_THROTTLED)
-                    .setDataUsage(999_000_000, System.currentTimeMillis())
-                    .build();
-            sm.setSubscriptionPlans(subId, Arrays.asList(plan));
-            final UiDevice uiDevice = UiDevice.getInstance(mInstrumentation);
-            uiDevice.openNotification();
-            try {
-                final UiObject2 uiObject = UiAutomatorUtils2.waitFindObject(
-                        By.text("Data warning"));
-                Assume.assumeNotNull(uiObject);
-                uiObject.wait(Until.clickable(true), 10_000L);
-                uiObject.getParent().swipe(Direction.RIGHT, 1.0f);
-            } catch (Throwable t) {
-                Assume.assumeNoException(
-                        "Error occurred while finding and swiping the notification", t);
-            }
-            assertSnoozeWarningNotReceived();
-            uiDevice.pressHome();
-        } finally {
-            sm.setSubscriptionPlans(subId, originalPlans);
-            setSubPlanOwner(subId, "");
-        }
-    }
-
-    private static void setSubPlanOwner(int subId, String packageName) throws Exception {
-        SystemUtil.runShellCommand(InstrumentationRegistry.getInstrumentation(),
-                "cmd netpolicy set sub-plan-owner " + subId + " " + packageName);
-    }
-}
diff --git a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/DefaultRestrictionsMeteredTest.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/DefaultRestrictionsMeteredTest.java
deleted file mode 100644
index 810fd19..0000000
--- a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/DefaultRestrictionsMeteredTest.java
+++ /dev/null
@@ -1,23 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.cts.netpolicy.hostside;
-
-import static com.android.cts.netpolicy.hostside.Property.METERED_NETWORK;
-
-@RequiredProperties({METERED_NETWORK})
-public class DefaultRestrictionsMeteredTest extends AbstractDefaultRestrictionsTest {
-}
diff --git a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/DefaultRestrictionsNonMeteredTest.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/DefaultRestrictionsNonMeteredTest.java
deleted file mode 100644
index fef546c..0000000
--- a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/DefaultRestrictionsNonMeteredTest.java
+++ /dev/null
@@ -1,23 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.cts.netpolicy.hostside;
-
-import static com.android.cts.netpolicy.hostside.Property.NON_METERED_NETWORK;
-
-@RequiredProperties({NON_METERED_NETWORK})
-public class DefaultRestrictionsNonMeteredTest extends AbstractDefaultRestrictionsTest {
-}
diff --git a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/DozeModeMeteredTest.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/DozeModeMeteredTest.java
deleted file mode 100644
index 741dd7e..0000000
--- a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/DozeModeMeteredTest.java
+++ /dev/null
@@ -1,23 +0,0 @@
-/*
- * Copyright (C) 2016 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.cts.netpolicy.hostside;
-
-import static com.android.cts.netpolicy.hostside.Property.METERED_NETWORK;
-
-@RequiredProperties({METERED_NETWORK})
-public class DozeModeMeteredTest extends AbstractDozeModeTestCase {
-}
diff --git a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/DozeModeNonMeteredTest.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/DozeModeNonMeteredTest.java
deleted file mode 100644
index f343df5..0000000
--- a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/DozeModeNonMeteredTest.java
+++ /dev/null
@@ -1,23 +0,0 @@
-/*
- * Copyright (C) 2016 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.cts.netpolicy.hostside;
-
-import static com.android.cts.netpolicy.hostside.Property.NON_METERED_NETWORK;
-
-@RequiredProperties({NON_METERED_NETWORK})
-public class DozeModeNonMeteredTest extends AbstractDozeModeTestCase {
-}
diff --git a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/DumpOnFailureRule.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/DumpOnFailureRule.java
deleted file mode 100644
index 2dc6cc4..0000000
--- a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/DumpOnFailureRule.java
+++ /dev/null
@@ -1,118 +0,0 @@
-/*
- * Copyright (C) 2019 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.cts.netpolicy.hostside;
-
-import static com.android.cts.netpolicy.hostside.AbstractRestrictBackgroundNetworkTestCase.TAG;
-import static com.android.cts.netpolicy.hostside.AbstractRestrictBackgroundNetworkTestCase.TEST_APP2_PKG;
-import static com.android.cts.netpolicy.hostside.AbstractRestrictBackgroundNetworkTestCase.TEST_PKG;
-
-import android.os.Environment;
-import android.os.FileUtils;
-import android.os.ParcelFileDescriptor;
-import android.util.Log;
-
-import androidx.test.platform.app.InstrumentationRegistry;
-import androidx.test.uiautomator.UiDevice;
-
-import com.android.compatibility.common.util.OnFailureRule;
-
-import org.junit.AssumptionViolatedException;
-import org.junit.runner.Description;
-import org.junit.runners.model.Statement;
-
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.nio.charset.StandardCharsets;
-
-public class DumpOnFailureRule extends OnFailureRule {
-    private File mDumpDir = new File(Environment.getExternalStorageDirectory(),
-            "CtsHostsideNetworkPolicyTests");
-
-    @Override
-    public void onTestFailure(Statement base, Description description, Throwable throwable) {
-        if (throwable instanceof AssumptionViolatedException) {
-            final String testName = description.getClassName() + "_" + description.getMethodName();
-            Log.d(TAG, "Skipping test " + testName + ": " + throwable);
-            return;
-        }
-
-        prepareDumpRootDir();
-        final String shortenedTestName = getShortenedTestName(description);
-        final File dumpFile = new File(mDumpDir, "dump-" + shortenedTestName);
-        Log.i(TAG, "Dumping debug info for " + description + ": " + dumpFile.getPath());
-        try (FileOutputStream out = new FileOutputStream(dumpFile)) {
-            for (String cmd : new String[] {
-                    "dumpsys netpolicy",
-                    "dumpsys network_management",
-                    "dumpsys usagestats " + TEST_PKG + " " + TEST_APP2_PKG,
-                    "dumpsys usagestats appstandby",
-                    "dumpsys connectivity trafficcontroller",
-                    "dumpsys netd trafficcontroller",
-                    "dumpsys platform_compat", // TODO (b/279829773): Remove this dump
-                    "dumpsys jobscheduler " + TEST_APP2_PKG, // TODO (b/288220398): Remove this dump
-            }) {
-                dumpCommandOutput(out, cmd);
-            }
-        } catch (FileNotFoundException e) {
-            Log.e(TAG, "Error opening file: " + dumpFile, e);
-        } catch (IOException e) {
-            Log.e(TAG, "Error closing file: " + dumpFile, e);
-        }
-        final UiDevice uiDevice = UiDevice.getInstance(
-                InstrumentationRegistry.getInstrumentation());
-        final File screenshotFile = new File(mDumpDir, "sc-" + shortenedTestName + ".png");
-        uiDevice.takeScreenshot(screenshotFile);
-        final File windowHierarchyFile = new File(mDumpDir, "wh-" + shortenedTestName + ".xml");
-        try {
-            uiDevice.dumpWindowHierarchy(windowHierarchyFile);
-        } catch (IOException e) {
-            Log.e(TAG, "Error dumping window hierarchy", e);
-        }
-    }
-
-    private String getShortenedTestName(Description description) {
-        final String qualifiedClassName = description.getClassName();
-        final String className = qualifiedClassName.substring(
-                qualifiedClassName.lastIndexOf(".") + 1);
-        final String shortenedClassName = className.chars()
-                .filter(Character::isUpperCase)
-                .collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append)
-                .toString();
-        return shortenedClassName + "_" + description.getMethodName();
-    }
-
-    void dumpCommandOutput(FileOutputStream out, String cmd) {
-        final ParcelFileDescriptor pfd = InstrumentationRegistry.getInstrumentation()
-                .getUiAutomation().executeShellCommand(cmd);
-        try (FileInputStream in = new ParcelFileDescriptor.AutoCloseInputStream(pfd)) {
-            out.write(("Output of '" + cmd + "':\n").getBytes(StandardCharsets.UTF_8));
-            FileUtils.copy(in, out);
-            out.write("\n\n=================================================================\n\n"
-                    .getBytes(StandardCharsets.UTF_8));
-        } catch (IOException e) {
-            Log.e(TAG, "Error dumping '" + cmd + "'", e);
-        }
-    }
-
-    void prepareDumpRootDir() {
-        if (!mDumpDir.exists() && !mDumpDir.mkdir()) {
-            Log.e(TAG, "Error creating " + mDumpDir);
-        }
-    }
-}
diff --git a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/ExpeditedJobMeteredTest.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/ExpeditedJobMeteredTest.java
deleted file mode 100644
index d56a50b..0000000
--- a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/ExpeditedJobMeteredTest.java
+++ /dev/null
@@ -1,23 +0,0 @@
-/*
- * Copyright (C) 2021 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.cts.netpolicy.hostside;
-
-import static com.android.cts.netpolicy.hostside.Property.METERED_NETWORK;
-
-@RequiredProperties({METERED_NETWORK})
-public class ExpeditedJobMeteredTest extends AbstractExpeditedJobTest {
-}
diff --git a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/ExpeditedJobNonMeteredTest.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/ExpeditedJobNonMeteredTest.java
deleted file mode 100644
index 0a776ee..0000000
--- a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/ExpeditedJobNonMeteredTest.java
+++ /dev/null
@@ -1,23 +0,0 @@
-/*
- * Copyright (C) 2021 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.cts.netpolicy.hostside;
-
-import static com.android.cts.netpolicy.hostside.Property.NON_METERED_NETWORK;
-
-@RequiredProperties({NON_METERED_NETWORK})
-public class ExpeditedJobNonMeteredTest extends AbstractExpeditedJobTest {
-}
diff --git a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/MeterednessConfigurationRule.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/MeterednessConfigurationRule.java
deleted file mode 100644
index 4f4e68e..0000000
--- a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/MeterednessConfigurationRule.java
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * Copyright (C) 2019 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.cts.netpolicy.hostside;
-
-import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.setupActiveNetworkMeteredness;
-import static com.android.cts.netpolicy.hostside.Property.METERED_NETWORK;
-import static com.android.cts.netpolicy.hostside.Property.NON_METERED_NETWORK;
-
-import android.util.ArraySet;
-
-import com.android.compatibility.common.util.BeforeAfterRule;
-import com.android.compatibility.common.util.ThrowingRunnable;
-
-import org.junit.runner.Description;
-import org.junit.runners.model.Statement;
-
-public class MeterednessConfigurationRule extends BeforeAfterRule {
-    private ThrowingRunnable mMeterednessResetter;
-
-    @Override
-    public void onBefore(Statement base, Description description) throws Throwable {
-        final ArraySet<Property> requiredProperties
-                = RequiredPropertiesRule.getRequiredProperties();
-        if (requiredProperties.contains(METERED_NETWORK)) {
-            configureNetworkMeteredness(true);
-        } else if (requiredProperties.contains(NON_METERED_NETWORK)) {
-            configureNetworkMeteredness(false);
-        }
-    }
-
-    @Override
-    public void onAfter(Statement base, Description description) throws Throwable {
-        resetNetworkMeteredness();
-    }
-
-    public void configureNetworkMeteredness(boolean metered) throws Exception {
-        mMeterednessResetter = setupActiveNetworkMeteredness(metered);
-    }
-
-    public void resetNetworkMeteredness() throws Exception {
-        if (mMeterednessResetter != null) {
-            mMeterednessResetter.run();
-            mMeterednessResetter = null;
-        }
-    }
-}
diff --git a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/MixedModesTest.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/MixedModesTest.java
deleted file mode 100644
index b0fa106..0000000
--- a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/MixedModesTest.java
+++ /dev/null
@@ -1,370 +0,0 @@
-/*
- * Copyright (C) 2016 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.cts.netpolicy.hostside;
-
-import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.setRestrictBackground;
-import static com.android.cts.netpolicy.hostside.Property.APP_STANDBY_MODE;
-import static com.android.cts.netpolicy.hostside.Property.BATTERY_SAVER_MODE;
-import static com.android.cts.netpolicy.hostside.Property.DATA_SAVER_MODE;
-import static com.android.cts.netpolicy.hostside.Property.DOZE_MODE;
-import static com.android.cts.netpolicy.hostside.Property.METERED_NETWORK;
-import static com.android.cts.netpolicy.hostside.Property.NON_METERED_NETWORK;
-
-import android.os.SystemClock;
-import android.util.Log;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-
-/**
- * Test cases for the more complex scenarios where multiple restrictions (like Battery Saver Mode
- * and Data Saver Mode) are applied simultaneously.
- * <p>
- * <strong>NOTE: </strong>it might sound like the test methods on this class are testing too much,
- * which would make it harder to diagnose individual failures, but the assumption is that such
- * failure most likely will happen when the restriction is tested individually as well.
- */
-public class MixedModesTest extends AbstractRestrictBackgroundNetworkTestCase {
-    private static final String TAG = "MixedModesTest";
-
-    @Before
-    public void setUp() throws Exception {
-        super.setUp();
-
-        // Set initial state.
-        removeRestrictBackgroundWhitelist(mUid);
-        removeRestrictBackgroundBlacklist(mUid);
-        removePowerSaveModeWhitelist(TEST_APP2_PKG);
-        removePowerSaveModeExceptIdleWhitelist(TEST_APP2_PKG);
-
-        registerBroadcastReceiver();
-    }
-
-    @After
-    public void tearDown() throws Exception {
-        super.tearDown();
-
-        try {
-            setRestrictBackground(false);
-        } finally {
-            setBatterySaverMode(false);
-        }
-    }
-
-    /**
-     * Tests all DS ON and BS ON scenarios from network-policy-restrictions.md on metered networks.
-     */
-    @RequiredProperties({DATA_SAVER_MODE, BATTERY_SAVER_MODE, METERED_NETWORK})
-    @Test
-    public void testDataAndBatterySaverModes_meteredNetwork() throws Exception {
-        final MeterednessConfigurationRule meterednessConfiguration
-                = new MeterednessConfigurationRule();
-        meterednessConfiguration.configureNetworkMeteredness(true);
-        try {
-            setRestrictBackground(true);
-            setBatterySaverMode(true);
-
-            Log.v(TAG, "Not whitelisted for any.");
-            assertBackgroundNetworkAccess(false);
-            assertsForegroundAlwaysHasNetworkAccess();
-            assertBackgroundNetworkAccess(false);
-
-            Log.v(TAG, "Whitelisted for Data Saver but not for Battery Saver.");
-            addRestrictBackgroundWhitelist(mUid);
-            removePowerSaveModeWhitelist(TEST_APP2_PKG);
-            assertBackgroundNetworkAccess(false);
-            assertsForegroundAlwaysHasNetworkAccess();
-            assertBackgroundNetworkAccess(false);
-            removeRestrictBackgroundWhitelist(mUid);
-
-            Log.v(TAG, "Whitelisted for Battery Saver but not for Data Saver.");
-            addPowerSaveModeWhitelist(TEST_APP2_PKG);
-            removeRestrictBackgroundWhitelist(mUid);
-            assertBackgroundNetworkAccess(false);
-            assertsForegroundAlwaysHasNetworkAccess();
-            assertBackgroundNetworkAccess(false);
-            removePowerSaveModeWhitelist(TEST_APP2_PKG);
-
-            Log.v(TAG, "Whitelisted for both.");
-            addRestrictBackgroundWhitelist(mUid);
-            addPowerSaveModeWhitelist(TEST_APP2_PKG);
-            assertBackgroundNetworkAccess(true);
-            assertsForegroundAlwaysHasNetworkAccess();
-            assertBackgroundNetworkAccess(true);
-            removePowerSaveModeWhitelist(TEST_APP2_PKG);
-            assertBackgroundNetworkAccess(false);
-            removeRestrictBackgroundWhitelist(mUid);
-
-            Log.v(TAG, "Blacklisted for Data Saver, not whitelisted for Battery Saver.");
-            addRestrictBackgroundBlacklist(mUid);
-            removePowerSaveModeWhitelist(TEST_APP2_PKG);
-            assertBackgroundNetworkAccess(false);
-            assertsForegroundAlwaysHasNetworkAccess();
-            assertBackgroundNetworkAccess(false);
-            removeRestrictBackgroundBlacklist(mUid);
-
-            Log.v(TAG, "Blacklisted for Data Saver, whitelisted for Battery Saver.");
-            addRestrictBackgroundBlacklist(mUid);
-            addPowerSaveModeWhitelist(TEST_APP2_PKG);
-            assertBackgroundNetworkAccess(false);
-            assertsForegroundAlwaysHasNetworkAccess();
-            assertBackgroundNetworkAccess(false);
-            removeRestrictBackgroundBlacklist(mUid);
-            removePowerSaveModeWhitelist(TEST_APP2_PKG);
-        } finally {
-            meterednessConfiguration.resetNetworkMeteredness();
-        }
-    }
-
-    /**
-     * Tests all DS ON and BS ON scenarios from network-policy-restrictions.md on non-metered
-     * networks.
-     */
-    @RequiredProperties({DATA_SAVER_MODE, BATTERY_SAVER_MODE, NON_METERED_NETWORK})
-    @Test
-    public void testDataAndBatterySaverModes_nonMeteredNetwork() throws Exception {
-        final MeterednessConfigurationRule meterednessConfiguration
-                = new MeterednessConfigurationRule();
-        meterednessConfiguration.configureNetworkMeteredness(false);
-        try {
-            setRestrictBackground(true);
-            setBatterySaverMode(true);
-
-            Log.v(TAG, "Not whitelisted for any.");
-            assertBackgroundNetworkAccess(false);
-            assertsForegroundAlwaysHasNetworkAccess();
-            assertBackgroundNetworkAccess(false);
-
-            Log.v(TAG, "Whitelisted for Data Saver but not for Battery Saver.");
-            addRestrictBackgroundWhitelist(mUid);
-            removePowerSaveModeWhitelist(TEST_APP2_PKG);
-            assertBackgroundNetworkAccess(false);
-            assertsForegroundAlwaysHasNetworkAccess();
-            assertBackgroundNetworkAccess(false);
-            removeRestrictBackgroundWhitelist(mUid);
-
-            Log.v(TAG, "Whitelisted for Battery Saver but not for Data Saver.");
-            addPowerSaveModeWhitelist(TEST_APP2_PKG);
-            removeRestrictBackgroundWhitelist(mUid);
-            assertBackgroundNetworkAccess(true);
-            assertsForegroundAlwaysHasNetworkAccess();
-            assertBackgroundNetworkAccess(true);
-            removePowerSaveModeWhitelist(TEST_APP2_PKG);
-
-            Log.v(TAG, "Whitelisted for both.");
-            addRestrictBackgroundWhitelist(mUid);
-            addPowerSaveModeWhitelist(TEST_APP2_PKG);
-            assertBackgroundNetworkAccess(true);
-            assertsForegroundAlwaysHasNetworkAccess();
-            assertBackgroundNetworkAccess(true);
-            removePowerSaveModeWhitelist(TEST_APP2_PKG);
-            assertBackgroundNetworkAccess(false);
-            removeRestrictBackgroundWhitelist(mUid);
-
-            Log.v(TAG, "Blacklisted for Data Saver, not whitelisted for Battery Saver.");
-            addRestrictBackgroundBlacklist(mUid);
-            removePowerSaveModeWhitelist(TEST_APP2_PKG);
-            assertBackgroundNetworkAccess(false);
-            assertsForegroundAlwaysHasNetworkAccess();
-            assertBackgroundNetworkAccess(false);
-            removeRestrictBackgroundBlacklist(mUid);
-
-            Log.v(TAG, "Blacklisted for Data Saver, whitelisted for Battery Saver.");
-            addRestrictBackgroundBlacklist(mUid);
-            addPowerSaveModeWhitelist(TEST_APP2_PKG);
-            assertBackgroundNetworkAccess(true);
-            assertsForegroundAlwaysHasNetworkAccess();
-            assertBackgroundNetworkAccess(true);
-            removeRestrictBackgroundBlacklist(mUid);
-            removePowerSaveModeWhitelist(TEST_APP2_PKG);
-        } finally {
-            meterednessConfiguration.resetNetworkMeteredness();
-        }
-    }
-
-    /**
-     * Tests that powersave whitelists works as expected when doze and battery saver modes
-     * are enabled.
-     */
-    @RequiredProperties({DOZE_MODE, BATTERY_SAVER_MODE})
-    @Test
-    public void testDozeAndBatterySaverMode_powerSaveWhitelists() throws Exception {
-        setBatterySaverMode(true);
-        setDozeMode(true);
-
-        try {
-            addPowerSaveModeWhitelist(TEST_APP2_PKG);
-            assertBackgroundNetworkAccess(true);
-
-            removePowerSaveModeWhitelist(TEST_APP2_PKG);
-            assertBackgroundNetworkAccess(false);
-
-            addPowerSaveModeExceptIdleWhitelist(TEST_APP2_PKG);
-            assertBackgroundNetworkAccess(false);
-
-            removePowerSaveModeExceptIdleWhitelist(TEST_APP2_PKG);
-            assertBackgroundNetworkAccess(false);
-        } finally {
-            setBatterySaverMode(false);
-            setDozeMode(false);
-        }
-    }
-
-    /**
-     * Tests that powersave whitelists works as expected when doze and appIdle modes
-     * are enabled.
-     */
-    @RequiredProperties({DOZE_MODE, APP_STANDBY_MODE})
-    @Test
-    public void testDozeAndAppIdle_powerSaveWhitelists() throws Exception {
-        setDozeMode(true);
-        setAppIdle(true);
-
-        try {
-            addPowerSaveModeWhitelist(TEST_APP2_PKG);
-            assertBackgroundNetworkAccess(true);
-
-            removePowerSaveModeWhitelist(TEST_APP2_PKG);
-            assertBackgroundNetworkAccess(false);
-
-            addPowerSaveModeExceptIdleWhitelist(TEST_APP2_PKG);
-            assertBackgroundNetworkAccess(false);
-
-            removePowerSaveModeExceptIdleWhitelist(TEST_APP2_PKG);
-            assertBackgroundNetworkAccess(false);
-        } finally {
-            setAppIdle(false);
-            setDozeMode(false);
-        }
-    }
-
-    @RequiredProperties({APP_STANDBY_MODE, DOZE_MODE})
-    @Test
-    public void testAppIdleAndDoze_tempPowerSaveWhitelists() throws Exception {
-        setDozeMode(true);
-        setAppIdle(true);
-
-        try {
-            assertBackgroundNetworkAccess(false);
-
-            addTempPowerSaveModeWhitelist(TEST_APP2_PKG, TEMP_POWERSAVE_WHITELIST_DURATION_MS);
-            assertBackgroundNetworkAccess(true);
-
-            // Wait until the whitelist duration is expired.
-            SystemClock.sleep(TEMP_POWERSAVE_WHITELIST_DURATION_MS);
-            assertBackgroundNetworkAccess(false);
-        } finally {
-            setAppIdle(false);
-            setDozeMode(false);
-        }
-    }
-
-    @RequiredProperties({APP_STANDBY_MODE, BATTERY_SAVER_MODE})
-    @Test
-    public void testAppIdleAndBatterySaver_tempPowerSaveWhitelists() throws Exception {
-        setBatterySaverMode(true);
-        setAppIdle(true);
-
-        try {
-            assertBackgroundNetworkAccess(false);
-
-            addTempPowerSaveModeWhitelist(TEST_APP2_PKG, TEMP_POWERSAVE_WHITELIST_DURATION_MS);
-            assertBackgroundNetworkAccess(true);
-
-            // Wait until the whitelist duration is expired.
-            SystemClock.sleep(TEMP_POWERSAVE_WHITELIST_DURATION_MS);
-            assertBackgroundNetworkAccess(false);
-        } finally {
-            setAppIdle(false);
-            setBatterySaverMode(false);
-        }
-    }
-
-    /**
-     * Tests that the app idle whitelist works as expected when doze and appIdle mode are enabled.
-     */
-    @RequiredProperties({DOZE_MODE, APP_STANDBY_MODE})
-    @Test
-    public void testDozeAndAppIdle_appIdleWhitelist() throws Exception {
-        setDozeMode(true);
-        setAppIdle(true);
-
-        try {
-            assertBackgroundNetworkAccess(false);
-
-            // UID still shouldn't have access because of Doze.
-            addAppIdleWhitelist(mUid);
-            assertBackgroundNetworkAccess(false);
-
-            removeAppIdleWhitelist(mUid);
-            assertBackgroundNetworkAccess(false);
-        } finally {
-            setAppIdle(false);
-            setDozeMode(false);
-        }
-    }
-
-    @RequiredProperties({APP_STANDBY_MODE, DOZE_MODE})
-    @Test
-    public void testAppIdleAndDoze_tempPowerSaveAndAppIdleWhitelists() throws Exception {
-        setDozeMode(true);
-        setAppIdle(true);
-
-        try {
-            assertBackgroundNetworkAccess(false);
-
-            addAppIdleWhitelist(mUid);
-            assertBackgroundNetworkAccess(false);
-
-            addTempPowerSaveModeWhitelist(TEST_APP2_PKG, TEMP_POWERSAVE_WHITELIST_DURATION_MS);
-            assertBackgroundNetworkAccess(true);
-
-            // Wait until the whitelist duration is expired.
-            SystemClock.sleep(TEMP_POWERSAVE_WHITELIST_DURATION_MS);
-            assertBackgroundNetworkAccess(false);
-        } finally {
-            setAppIdle(false);
-            setDozeMode(false);
-            removeAppIdleWhitelist(mUid);
-        }
-    }
-
-    @RequiredProperties({APP_STANDBY_MODE, BATTERY_SAVER_MODE})
-    @Test
-    public void testAppIdleAndBatterySaver_tempPowerSaveAndAppIdleWhitelists() throws Exception {
-        setBatterySaverMode(true);
-        setAppIdle(true);
-
-        try {
-            assertBackgroundNetworkAccess(false);
-
-            addAppIdleWhitelist(mUid);
-            assertBackgroundNetworkAccess(false);
-
-            addTempPowerSaveModeWhitelist(TEST_APP2_PKG, TEMP_POWERSAVE_WHITELIST_DURATION_MS);
-            assertBackgroundNetworkAccess(true);
-
-            // Wait until the whitelist duration is expired.
-            SystemClock.sleep(TEMP_POWERSAVE_WHITELIST_DURATION_MS);
-            assertBackgroundNetworkAccess(false);
-        } finally {
-            setAppIdle(false);
-            setBatterySaverMode(false);
-            removeAppIdleWhitelist(mUid);
-        }
-    }
-}
diff --git a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/MyNotificationListenerService.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/MyNotificationListenerService.java
deleted file mode 100644
index 6dc9921..0000000
--- a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/MyNotificationListenerService.java
+++ /dev/null
@@ -1,123 +0,0 @@
-/*
- * Copyright (C) 2016 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.cts.netpolicy.hostside;
-
-import android.app.Notification;
-import android.app.PendingIntent;
-import android.app.PendingIntent.CanceledException;
-import android.app.RemoteInput;
-import android.content.ComponentName;
-import android.os.Bundle;
-import android.service.notification.NotificationListenerService;
-import android.service.notification.StatusBarNotification;
-import android.util.Log;
-
-/**
- * NotificationListenerService implementation that executes the notification actions once they're
- * created.
- */
-public class MyNotificationListenerService extends NotificationListenerService {
-    private static final String TAG = "MyNotificationListenerService";
-
-    @Override
-    public void onListenerConnected() {
-        Log.d(TAG, "onListenerConnected()");
-    }
-
-    @Override
-    public void onNotificationPosted(StatusBarNotification sbn) {
-        Log.d(TAG, "onNotificationPosted(): "  + sbn);
-        if (!sbn.getPackageName().startsWith(getPackageName())) {
-            Log.v(TAG, "ignoring notification from a different package");
-            return;
-        }
-        final PendingIntentSender sender = new PendingIntentSender();
-        final Notification notification = sbn.getNotification();
-        if (notification.contentIntent != null) {
-            sender.send("content", notification.contentIntent);
-        }
-        if (notification.deleteIntent != null) {
-            sender.send("delete", notification.deleteIntent);
-        }
-        if (notification.fullScreenIntent != null) {
-            sender.send("full screen", notification.fullScreenIntent);
-        }
-        if (notification.actions != null) {
-            for (Notification.Action action : notification.actions) {
-                sender.send("action", action.actionIntent);
-                sender.send("action extras", action.getExtras());
-                final RemoteInput[] remoteInputs = action.getRemoteInputs();
-                if (remoteInputs != null && remoteInputs.length > 0) {
-                    for (RemoteInput remoteInput : remoteInputs) {
-                        sender.send("remote input extras", remoteInput.getExtras());
-                    }
-                }
-            }
-        }
-        sender.send("notification extras", notification.extras);
-    }
-
-    static String getId() {
-        return String.format("%s/%s", MyNotificationListenerService.class.getPackage().getName(),
-                MyNotificationListenerService.class.getName());
-    }
-
-    static ComponentName getComponentName() {
-        return new ComponentName(MyNotificationListenerService.class.getPackage().getName(),
-                MyNotificationListenerService.class.getName());
-    }
-
-    private static final class PendingIntentSender {
-        private PendingIntent mSentIntent = null;
-        private String mReason = null;
-
-        private void send(String reason, PendingIntent pendingIntent) {
-            if (pendingIntent == null) {
-                // Could happen on action that only has extras
-                Log.v(TAG, "Not sending null pending intent for " + reason);
-                return;
-            }
-            if (mSentIntent != null || mReason != null) {
-                // Sanity check: make sure test case set up just one pending intent in the
-                // notification, otherwise it could pass because another pending intent caused the
-                // whitelisting.
-                throw new IllegalStateException("Already sent a PendingIntent (" + mSentIntent
-                        + ") for reason '" + mReason + "' when requested another for '" + reason
-                        + "' (" + pendingIntent + ")");
-            }
-            Log.i(TAG, "Sending pending intent for " + reason + ":" + pendingIntent);
-            try {
-                pendingIntent.send();
-                mSentIntent = pendingIntent;
-                mReason = reason;
-            } catch (CanceledException e) {
-                Log.w(TAG, "Pending intent " + pendingIntent + " canceled");
-            }
-        }
-
-        private void send(String reason, Bundle extras) {
-            if (extras != null) {
-                for (String key : extras.keySet()) {
-                    Object value = extras.get(key);
-                    if (value instanceof PendingIntent) {
-                        send(reason + " with key '" + key + "'", (PendingIntent) value);
-                    }
-                }
-            }
-        }
-
-    }
-}
diff --git a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/MyServiceClient.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/MyServiceClient.java
deleted file mode 100644
index 71b28f6..0000000
--- a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/MyServiceClient.java
+++ /dev/null
@@ -1,133 +0,0 @@
-/*
- * Copyright (C) 2016 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.cts.netpolicy.hostside;
-
-import android.app.job.JobInfo;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.content.ServiceConnection;
-import android.net.NetworkRequest;
-import android.os.ConditionVariable;
-import android.os.IBinder;
-import android.os.RemoteException;
-
-public class MyServiceClient {
-    private static final int TIMEOUT_MS = 20_000;
-    private static final String PACKAGE = MyServiceClient.class.getPackage().getName();
-    private static final String APP2_PACKAGE = PACKAGE + ".app2";
-    private static final String SERVICE_NAME = APP2_PACKAGE + ".MyService";
-
-    private Context mContext;
-    private ServiceConnection mServiceConnection;
-    private volatile IMyService mService;
-    private final ConditionVariable mServiceCondition = new ConditionVariable();
-
-    public MyServiceClient(Context context) {
-        mContext = context;
-    }
-
-    /**
-     * Binds to a service in the test app to communicate state.
-     * @param bindPriorityFlags Flags to influence the process-state of the bound app.
-     */
-    public void bind(int bindPriorityFlags) {
-        if (mService != null) {
-            throw new IllegalStateException("Already bound");
-        }
-        mServiceConnection = new ServiceConnection() {
-            @Override
-            public void onServiceConnected(ComponentName name, IBinder service) {
-                mService = IMyService.Stub.asInterface(service);
-                mServiceCondition.open();
-            }
-            @Override
-            public void onServiceDisconnected(ComponentName name) {
-                mServiceCondition.close();
-                mService = null;
-            }
-        };
-
-        final Intent intent = new Intent();
-        intent.setComponent(new ComponentName(APP2_PACKAGE, SERVICE_NAME));
-        // Needs to use BIND_NOT_FOREGROUND so app2 does not run in
-        // the same process state as app
-        mContext.bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE
-                | bindPriorityFlags);
-        ensureServiceConnection();
-    }
-
-    public void unbind() {
-        if (mService != null) {
-            mContext.unbindService(mServiceConnection);
-        }
-    }
-
-    private void ensureServiceConnection() {
-        if (mService != null) {
-            return;
-        }
-        mServiceCondition.block(TIMEOUT_MS);
-        if (mService == null) {
-            throw new IllegalStateException(
-                    "Could not bind to MyService service after " + TIMEOUT_MS + "ms");
-        }
-    }
-
-    public void registerBroadcastReceiver() throws RemoteException {
-        ensureServiceConnection();
-        mService.registerBroadcastReceiver();
-    }
-
-    public int getCounters(String receiverName, String action) throws RemoteException {
-        ensureServiceConnection();
-        return mService.getCounters(receiverName, action);
-    }
-
-    /** Retrieves the network state as observed from the bound test app */
-    public NetworkCheckResult checkNetworkStatus(String address) throws RemoteException {
-        ensureServiceConnection();
-        return mService.checkNetworkStatus(address);
-    }
-
-    public String getRestrictBackgroundStatus() throws RemoteException {
-        ensureServiceConnection();
-        return mService.getRestrictBackgroundStatus();
-    }
-
-    public void sendNotification(int notificationId, String notificationType)
-            throws RemoteException {
-        ensureServiceConnection();
-        mService.sendNotification(notificationId, notificationType);
-    }
-
-    public void registerNetworkCallback(final NetworkRequest request, INetworkCallback cb)
-            throws RemoteException {
-        ensureServiceConnection();
-        mService.registerNetworkCallback(request, cb);
-    }
-
-    public void unregisterNetworkCallback() throws RemoteException {
-        ensureServiceConnection();
-        mService.unregisterNetworkCallback();
-    }
-
-    public int scheduleJob(JobInfo jobInfo) throws RemoteException {
-        ensureServiceConnection();
-        return mService.scheduleJob(jobInfo);
-    }
-}
diff --git a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/NetworkCallbackTest.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/NetworkCallbackTest.java
deleted file mode 100644
index 3934cfa..0000000
--- a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/NetworkCallbackTest.java
+++ /dev/null
@@ -1,428 +0,0 @@
-/*
- * Copyright (C) 2019 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.cts.netpolicy.hostside;
-
-import static android.app.ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE;
-import static android.app.ActivityManager.PROCESS_STATE_TOP_SLEEPING;
-import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED;
-import static android.net.NetworkCapabilities.SIGNAL_STRENGTH_UNSPECIFIED;
-
-import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.canChangeActiveNetworkMeteredness;
-import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.getActiveNetworkCapabilities;
-import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.setRestrictBackground;
-import static com.android.cts.netpolicy.hostside.Property.BATTERY_SAVER_MODE;
-import static com.android.cts.netpolicy.hostside.Property.DATA_SAVER_MODE;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.fail;
-import static org.junit.Assume.assumeTrue;
-
-import android.net.Network;
-import android.net.NetworkCapabilities;
-import android.net.NetworkRequest;
-import android.net.cts.util.CtsNetUtils;
-import android.os.SystemClock;
-import android.util.Log;
-
-import com.android.modules.utils.build.SdkLevel;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-
-import java.util.ArrayList;
-import java.util.Objects;
-import java.util.concurrent.LinkedBlockingQueue;
-import java.util.concurrent.TimeUnit;
-
-public class NetworkCallbackTest extends AbstractRestrictBackgroundNetworkTestCase {
-    private Network mNetwork;
-    private final TestNetworkCallback mTestNetworkCallback = new TestNetworkCallback();
-    private CtsNetUtils mCtsNetUtils;
-    private static final String GOOGLE_PRIVATE_DNS_SERVER = "dns.google";
-
-    @Rule
-    public final MeterednessConfigurationRule mMeterednessConfiguration
-            = new MeterednessConfigurationRule();
-
-    enum CallbackState {
-        NONE,
-        AVAILABLE,
-        LOST,
-        BLOCKED_STATUS,
-        CAPABILITIES
-    }
-
-    private static class CallbackInfo {
-        public final CallbackState state;
-        public final Network network;
-        public final Object arg;
-
-        CallbackInfo(CallbackState s, Network n, Object o) {
-            state = s; network = n; arg = o;
-        }
-
-        public String toString() {
-            return String.format("%s (%s) (%s)", state, network, arg);
-        }
-
-        @Override
-        public boolean equals(Object o) {
-            if (!(o instanceof CallbackInfo)) return false;
-            // Ignore timeMs, since it's unpredictable.
-            final CallbackInfo other = (CallbackInfo) o;
-            return (state == other.state) && Objects.equals(network, other.network)
-                    && Objects.equals(arg, other.arg);
-        }
-
-        @Override
-        public int hashCode() {
-            return Objects.hash(state, network, arg);
-        }
-    }
-
-    private class TestNetworkCallback extends INetworkCallback.Stub {
-        private static final int TEST_CONNECT_TIMEOUT_MS = 30_000;
-        private static final int TEST_CALLBACK_TIMEOUT_MS = 5_000;
-
-        private final LinkedBlockingQueue<CallbackInfo> mCallbacks = new LinkedBlockingQueue<>();
-
-        protected void setLastCallback(CallbackState state, Network network, Object o) {
-            mCallbacks.offer(new CallbackInfo(state, network, o));
-        }
-
-        CallbackInfo nextCallback(int timeoutMs) {
-            CallbackInfo cb = null;
-            try {
-                cb = mCallbacks.poll(timeoutMs, TimeUnit.MILLISECONDS);
-            } catch (InterruptedException e) {
-            }
-            if (cb == null) {
-                fail("Did not receive callback after " + timeoutMs + "ms");
-            }
-            return cb;
-        }
-
-        CallbackInfo expectCallback(CallbackState state, Network expectedNetwork, Object o) {
-            final CallbackInfo expected = new CallbackInfo(state, expectedNetwork, o);
-            final CallbackInfo actual = nextCallback(TEST_CALLBACK_TIMEOUT_MS);
-            assertEquals("Unexpected callback:", expected, actual);
-            return actual;
-        }
-
-        @Override
-        public void onAvailable(Network network) {
-            setLastCallback(CallbackState.AVAILABLE, network, null);
-        }
-
-        @Override
-        public void onLost(Network network) {
-            setLastCallback(CallbackState.LOST, network, null);
-        }
-
-        @Override
-        public void onBlockedStatusChanged(Network network, boolean blocked) {
-            setLastCallback(CallbackState.BLOCKED_STATUS, network, blocked);
-        }
-
-        @Override
-        public void onCapabilitiesChanged(Network network, NetworkCapabilities cap) {
-            setLastCallback(CallbackState.CAPABILITIES, network, cap);
-        }
-
-        public Network expectAvailableCallbackAndGetNetwork() {
-            final CallbackInfo cb = nextCallback(TEST_CONNECT_TIMEOUT_MS);
-            if (cb.state != CallbackState.AVAILABLE) {
-                fail("Network is not available. Instead obtained the following callback :" + cb);
-            }
-            return cb.network;
-        }
-
-        public void drainAndWaitForIdle() {
-            try {
-                do {
-                    mCallbacks.drainTo(new ArrayList<>());
-                } while (mCallbacks.poll(TEST_CALLBACK_TIMEOUT_MS, TimeUnit.MILLISECONDS) != null);
-            } catch (InterruptedException ie) {
-                Log.e(TAG, "Interrupted while draining callback queue", ie);
-                Thread.currentThread().interrupt();
-            }
-        }
-
-        public void expectBlockedStatusCallback(Network expectedNetwork, boolean expectBlocked) {
-            expectCallback(CallbackState.BLOCKED_STATUS, expectedNetwork, expectBlocked);
-        }
-
-        public void expectBlockedStatusCallbackEventually(Network expectedNetwork,
-                boolean expectBlocked) {
-            final long deadline = System.currentTimeMillis() + TEST_CALLBACK_TIMEOUT_MS;
-            do {
-                final CallbackInfo cb = nextCallback((int) (deadline - System.currentTimeMillis()));
-                if (cb.state == CallbackState.BLOCKED_STATUS
-                        && cb.network.equals(expectedNetwork)) {
-                    assertEquals(expectBlocked, cb.arg);
-                    return;
-                }
-            } while (System.currentTimeMillis() <= deadline);
-            fail("Didn't receive onBlockedStatusChanged()");
-        }
-
-        public void expectCapabilitiesCallbackEventually(Network expectedNetwork, boolean hasCap,
-                int cap) {
-            final long deadline = System.currentTimeMillis() + TEST_CALLBACK_TIMEOUT_MS;
-            do {
-                final CallbackInfo cb = nextCallback((int) (deadline - System.currentTimeMillis()));
-                if (cb.state != CallbackState.CAPABILITIES
-                        || !expectedNetwork.equals(cb.network)
-                        || (hasCap != ((NetworkCapabilities) cb.arg).hasCapability(cap))) {
-                    Log.i("NetworkCallbackTest#expectCapabilitiesCallback",
-                            "Ignoring non-matching callback : " + cb);
-                    continue;
-                }
-                // Found a match, return
-                return;
-            } while (System.currentTimeMillis() <= deadline);
-            fail("Didn't receive the expected callback to onCapabilitiesChanged(). Check the "
-                    + "log for a list of received callbacks, if any.");
-        }
-    }
-
-    @Before
-    public void setUp() throws Exception {
-        super.setUp();
-
-        assumeTrue(canChangeActiveNetworkMeteredness());
-
-        registerBroadcastReceiver();
-
-        removeRestrictBackgroundWhitelist(mUid);
-        removeRestrictBackgroundBlacklist(mUid);
-        assertRestrictBackgroundChangedReceived(0);
-
-        // Initial state
-        setBatterySaverMode(false);
-        setRestrictBackground(false);
-        setAppIdle(false);
-
-        // Get transports of the active network, this has to be done before changing meteredness,
-        // since wifi will be disconnected when changing from non-metered to metered.
-        final NetworkCapabilities networkCapabilities = getActiveNetworkCapabilities();
-
-        // Mark network as metered.
-        mMeterednessConfiguration.configureNetworkMeteredness(true);
-
-        // Register callback, copy the capabilities from the active network to expect the "original"
-        // network before disconnecting, but null out some fields to prevent over-specified.
-        registerNetworkCallback(new NetworkRequest.Builder()
-                .setCapabilities(networkCapabilities.setTransportInfo(null))
-                .removeCapability(NET_CAPABILITY_NOT_METERED)
-                .setSignalStrength(SIGNAL_STRENGTH_UNSPECIFIED).build(), mTestNetworkCallback);
-        // Wait for onAvailable() callback to ensure network is available before the test
-        // and store the default network.
-        mNetwork = mTestNetworkCallback.expectAvailableCallbackAndGetNetwork();
-        // Check that the network is metered.
-        mTestNetworkCallback.expectCapabilitiesCallbackEventually(mNetwork,
-                false /* hasCapability */, NET_CAPABILITY_NOT_METERED);
-        mTestNetworkCallback.drainAndWaitForIdle();
-
-        // Before Android T, DNS queries over private DNS should be but are not restricted by Power
-        // Saver or Data Saver. The issue is fixed in mainline update and apps can no longer request
-        // DNS queries when its network is restricted by Power Saver. The fix takes effect backwards
-        // starting from Android T. But for Data Saver, the fix is not backward compatible since
-        // there are some platform changes involved. It is only available on devices that a specific
-        // trunk flag is enabled.
-        //
-        // This test can not only verify that the network traffic from apps is blocked at the right
-        // time, but also verify whether it is correctly blocked at the DNS stage, or at a later
-        // socket connection stage.
-        if (SdkLevel.isAtLeastT()) {
-            // Enable private DNS
-            mCtsNetUtils = new CtsNetUtils(mContext);
-            mCtsNetUtils.storePrivateDnsSetting();
-            mCtsNetUtils.setPrivateDnsStrictMode(GOOGLE_PRIVATE_DNS_SERVER);
-            mCtsNetUtils.awaitPrivateDnsSetting(
-                    "NetworkCallbackTest wait private DNS setting timeout", mNetwork,
-                    GOOGLE_PRIVATE_DNS_SERVER, true);
-        }
-    }
-
-    @After
-    public void tearDown() throws Exception {
-        super.tearDown();
-
-        setRestrictBackground(false);
-        setBatterySaverMode(false);
-        unregisterNetworkCallback();
-        stopApp();
-
-        if (SdkLevel.isAtLeastT() && (mCtsNetUtils != null)) {
-            mCtsNetUtils.restorePrivateDnsSetting();
-        }
-    }
-
-    @RequiredProperties({DATA_SAVER_MODE})
-    @Test
-    public void testOnBlockedStatusChanged_dataSaver() throws Exception {
-        try {
-            // Enable restrict background
-            setRestrictBackground(true);
-            // TODO: Verify expectedUnavailableError when aconfig support mainline.
-            // (see go/aconfig-in-mainline-problems)
-            assertBackgroundNetworkAccess(false);
-            assertNetworkAccessBlockedByBpf(true, mUid, true /* metered */);
-            mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, true);
-
-            // Add to whitelist
-            addRestrictBackgroundWhitelist(mUid);
-            assertBackgroundNetworkAccess(true);
-            assertNetworkAccessBlockedByBpf(false, mUid, true /* metered */);
-            mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, false);
-
-            // Remove from whitelist
-            removeRestrictBackgroundWhitelist(mUid);
-            // TODO: Verify expectedUnavailableError when aconfig support mainline.
-            assertBackgroundNetworkAccess(false);
-            assertNetworkAccessBlockedByBpf(true, mUid, true /* metered */);
-            mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, true);
-        } finally {
-            mMeterednessConfiguration.resetNetworkMeteredness();
-        }
-
-        // Set to non-metered network
-        mMeterednessConfiguration.configureNetworkMeteredness(false);
-        mTestNetworkCallback.expectCapabilitiesCallbackEventually(mNetwork,
-                true /* hasCapability */, NET_CAPABILITY_NOT_METERED);
-        try {
-            assertBackgroundNetworkAccess(true);
-            assertNetworkAccessBlockedByBpf(false, mUid, false /* metered */);
-            mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, false);
-
-            // Disable restrict background, should not trigger callback
-            setRestrictBackground(false);
-            assertBackgroundNetworkAccess(true);
-            assertNetworkAccessBlockedByBpf(false, mUid, false /* metered */);
-        } finally {
-            mMeterednessConfiguration.resetNetworkMeteredness();
-        }
-    }
-
-    @RequiredProperties({BATTERY_SAVER_MODE})
-    @Test
-    public void testOnBlockedStatusChanged_powerSaver() throws Exception {
-        try {
-            // Enable Power Saver
-            setBatterySaverMode(true);
-            if (SdkLevel.isAtLeastT()) {
-                assertProcessStateBelow(PROCESS_STATE_BOUND_FOREGROUND_SERVICE);
-                assertNetworkAccess(false, "java.net.UnknownHostException");
-            } else {
-                assertBackgroundNetworkAccess(false);
-            }
-            mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, true);
-            assertNetworkAccessBlockedByBpf(true, mUid, true /* metered */);
-
-            // Disable Power Saver
-            setBatterySaverMode(false);
-            assertBackgroundNetworkAccess(true);
-            mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, false);
-            assertNetworkAccessBlockedByBpf(false, mUid, true /* metered */);
-        } finally {
-            mMeterednessConfiguration.resetNetworkMeteredness();
-        }
-
-        // Set to non-metered network
-        mMeterednessConfiguration.configureNetworkMeteredness(false);
-        mTestNetworkCallback.expectCapabilitiesCallbackEventually(mNetwork,
-                true /* hasCapability */, NET_CAPABILITY_NOT_METERED);
-        try {
-            // Enable Power Saver
-            setBatterySaverMode(true);
-            if (SdkLevel.isAtLeastT()) {
-                assertProcessStateBelow(PROCESS_STATE_BOUND_FOREGROUND_SERVICE);
-                assertNetworkAccess(false, "java.net.UnknownHostException");
-            } else {
-                assertBackgroundNetworkAccess(false);
-            }
-            mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, true);
-            assertNetworkAccessBlockedByBpf(true, mUid, false /* metered */);
-
-            // Disable Power Saver
-            setBatterySaverMode(false);
-            assertBackgroundNetworkAccess(true);
-            mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, false);
-            assertNetworkAccessBlockedByBpf(false, mUid, false /* metered */);
-        } finally {
-            mMeterednessConfiguration.resetNetworkMeteredness();
-        }
-    }
-
-    @Test
-    public void testOnBlockedStatusChanged_default() throws Exception {
-        assumeTrue("Feature not enabled", isNetworkBlockedForTopSleepingAndAbove());
-
-        try {
-            assertProcessStateBelow(PROCESS_STATE_TOP_SLEEPING);
-            assertNetworkAccess(false, null);
-            assertNetworkAccessBlockedByBpf(true, mUid, true /* metered */);
-
-            launchActivity();
-            assertTopState();
-            assertNetworkAccess(true, null);
-            mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, false);
-            assertNetworkAccessBlockedByBpf(false, mUid, true /* metered */);
-
-            finishActivity();
-            assertProcessStateBelow(PROCESS_STATE_TOP_SLEEPING);
-            SystemClock.sleep(mProcessStateTransitionLongDelayMs);
-            assertNetworkAccess(false, null);
-            mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, true);
-            assertNetworkAccessBlockedByBpf(true, mUid, true /* metered */);
-
-        } finally {
-            mMeterednessConfiguration.resetNetworkMeteredness();
-        }
-
-        // Set to non-metered network
-        mMeterednessConfiguration.configureNetworkMeteredness(false);
-        mTestNetworkCallback.expectCapabilitiesCallbackEventually(mNetwork,
-                true /* hasCapability */, NET_CAPABILITY_NOT_METERED);
-        try {
-            assertProcessStateBelow(PROCESS_STATE_TOP_SLEEPING);
-            assertNetworkAccess(false, null);
-            assertNetworkAccessBlockedByBpf(true, mUid, false /* metered */);
-
-            launchActivity();
-            assertTopState();
-            assertNetworkAccess(true, null);
-            mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, false);
-            assertNetworkAccessBlockedByBpf(false, mUid, false /* metered */);
-
-            finishActivity();
-            assertProcessStateBelow(PROCESS_STATE_TOP_SLEEPING);
-            SystemClock.sleep(mProcessStateTransitionLongDelayMs);
-            assertNetworkAccess(false, null);
-            mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, true);
-            assertNetworkAccessBlockedByBpf(true, mUid, false /* metered */);
-        } finally {
-            mMeterednessConfiguration.resetNetworkMeteredness();
-        }
-    }
-
-    // TODO: 1. test against VPN lockdown.
-    //       2. test against multiple networks.
-}
diff --git a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/NetworkPolicyManagerTest.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/NetworkPolicyManagerTest.java
deleted file mode 100644
index 6c5f2ff..0000000
--- a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/NetworkPolicyManagerTest.java
+++ /dev/null
@@ -1,276 +0,0 @@
-/*
- * Copyright (C) 2021 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.cts.netpolicy.hostside;
-
-import static android.app.ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE;
-import static android.app.ActivityManager.PROCESS_STATE_LAST_ACTIVITY;
-import static android.app.ActivityManager.PROCESS_STATE_TOP_SLEEPING;
-import static android.os.Process.SYSTEM_UID;
-
-import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.assertIsUidRestrictedOnMeteredNetworks;
-import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.assertNetworkingBlockedStatusForUid;
-import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.isUidNetworkingBlocked;
-import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.setRestrictBackground;
-import static com.android.cts.netpolicy.hostside.Property.BATTERY_SAVER_MODE;
-import static com.android.cts.netpolicy.hostside.Property.DATA_SAVER_MODE;
-
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assume.assumeTrue;
-
-import android.os.SystemClock;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-
-public class NetworkPolicyManagerTest extends AbstractRestrictBackgroundNetworkTestCase {
-    private static final boolean METERED = true;
-    private static final boolean NON_METERED = false;
-
-    @Before
-    public void setUp() throws Exception {
-        super.setUp();
-
-        registerBroadcastReceiver();
-
-        removeRestrictBackgroundWhitelist(mUid);
-        removeRestrictBackgroundBlacklist(mUid);
-        assertRestrictBackgroundChangedReceived(0);
-
-        // Initial state
-        setBatterySaverMode(false);
-        setRestrictBackground(false);
-        setRestrictedNetworkingMode(false);
-    }
-
-    @After
-    public void tearDown() throws Exception {
-        super.tearDown();
-
-        setBatterySaverMode(false);
-        setRestrictBackground(false);
-        setRestrictedNetworkingMode(false);
-        unregisterNetworkCallback();
-        stopApp();
-    }
-
-    @Test
-    public void testIsUidNetworkingBlocked_withUidNotBlocked() throws Exception {
-        // Refer to NetworkPolicyManagerService#isUidNetworkingBlockedInternal(), this test is to
-        // test the cases of non-metered network and uid not matched by any rule.
-        // If mUid is not blocked by data saver mode or power saver mode, no matter the network is
-        // metered or non-metered, mUid shouldn't be blocked.
-        assertFalse(isUidNetworkingBlocked(mUid, METERED)); // Match NTWK_ALLOWED_DEFAULT
-        assertFalse(isUidNetworkingBlocked(mUid, NON_METERED)); // Match NTWK_ALLOWED_NON_METERED
-    }
-
-    @RequiredProperties({DATA_SAVER_MODE, BATTERY_SAVER_MODE})
-    @Test
-    public void testIsUidNetworkingBlocked_withSystemUid() throws Exception {
-        // Refer to NetworkPolicyManagerService#isUidNetworkingBlockedInternal(), this test is to
-        // test the case of uid is system uid.
-        // SYSTEM_UID will never be blocked.
-        assertFalse(isUidNetworkingBlocked(SYSTEM_UID, METERED)); // Match NTWK_ALLOWED_SYSTEM
-        assertFalse(isUidNetworkingBlocked(SYSTEM_UID, NON_METERED)); // Match NTWK_ALLOWED_SYSTEM
-        try {
-            setRestrictBackground(true);
-            setBatterySaverMode(true);
-            setRestrictedNetworkingMode(true);
-            assertNetworkingBlockedStatusForUid(SYSTEM_UID, METERED,
-                    false /* expectedResult */); // Match NTWK_ALLOWED_SYSTEM
-            assertFalse(
-                    isUidNetworkingBlocked(SYSTEM_UID, NON_METERED)); // Match NTWK_ALLOWED_SYSTEM
-        } finally {
-            setRestrictBackground(false);
-            setBatterySaverMode(false);
-            setRestrictedNetworkingMode(false);
-            assertNetworkingBlockedStatusForUid(mUid, METERED,
-                    false /* expectedResult */); // Match NTWK_ALLOWED_DEFAULT
-        }
-    }
-
-    @RequiredProperties({DATA_SAVER_MODE})
-    @Test
-    public void testIsUidNetworkingBlocked_withDataSaverMode() throws Exception {
-        // Refer to NetworkPolicyManagerService#isUidNetworkingBlockedInternal(), this test is to
-        // test the cases of non-metered network, uid is matched by restrict background blacklist,
-        // uid is matched by restrict background whitelist, app is in the foreground with restrict
-        // background enabled and the app is in the background with restrict background enabled.
-        try {
-            // Enable restrict background and mUid will be blocked because it's not in the
-            // foreground.
-            setRestrictBackground(true);
-            assertNetworkingBlockedStatusForUid(mUid, METERED,
-                    true /* expectedResult */); // Match NTWK_BLOCKED_BG_RESTRICT
-
-            // Although restrict background is enabled and mUid is in the background, but mUid will
-            // not be blocked if network is non-metered.
-            assertFalse(
-                    isUidNetworkingBlocked(mUid, NON_METERED)); // Match NTWK_ALLOWED_NON_METERED
-
-            // Add mUid into the restrict background blacklist.
-            addRestrictBackgroundBlacklist(mUid);
-            assertNetworkingBlockedStatusForUid(mUid, METERED,
-                    true /* expectedResult */); // Match NTWK_BLOCKED_DENYLIST
-
-            // Although mUid is in the restrict background blacklist, but mUid won't be blocked if
-            // the network is non-metered.
-            assertFalse(
-                    isUidNetworkingBlocked(mUid, NON_METERED)); // Match NTWK_ALLOWED_NON_METERED
-            removeRestrictBackgroundBlacklist(mUid);
-
-            // Add mUid into the restrict background whitelist.
-            addRestrictBackgroundWhitelist(mUid);
-            assertNetworkingBlockedStatusForUid(mUid, METERED,
-                    false /* expectedResult */); // Match NTWK_ALLOWED_ALLOWLIST
-            assertFalse(
-                    isUidNetworkingBlocked(mUid, NON_METERED)); // Match NTWK_ALLOWED_NON_METERED
-            removeRestrictBackgroundWhitelist(mUid);
-
-            // Make TEST_APP2_PKG go to foreground and mUid will be allowed temporarily.
-            launchActivity();
-            assertTopState();
-            assertNetworkingBlockedStatusForUid(mUid, METERED,
-                    false /* expectedResult */); // Match NTWK_ALLOWED_TMP_ALLOWLIST
-
-            // Back to background.
-            finishActivity();
-            assertProcessStateBelow(PROCESS_STATE_BOUND_FOREGROUND_SERVICE);
-            assertNetworkingBlockedStatusForUid(mUid, METERED,
-                    true /* expectedResult */); // Match NTWK_BLOCKED_BG_RESTRICT
-        } finally {
-            setRestrictBackground(false);
-            assertNetworkingBlockedStatusForUid(mUid, METERED,
-                    false /* expectedResult */); // Match NTWK_ALLOWED_DEFAULT
-        }
-    }
-
-    @Test
-    public void testIsUidNetworkingBlocked_withRestrictedNetworkingMode() throws Exception {
-        // Refer to NetworkPolicyManagerService#isUidNetworkingBlockedInternal(), this test is to
-        // test the cases of restricted networking mode enabled.
-        try {
-            // All apps should be blocked if restricted networking mode is enabled except for those
-            // apps who have CONNECTIVITY_USE_RESTRICTED_NETWORKS permission.
-            // This test won't test if an app who has CONNECTIVITY_USE_RESTRICTED_NETWORKS will not
-            // be blocked because CONNECTIVITY_USE_RESTRICTED_NETWORKS is a signature/privileged
-            // permission that CTS cannot acquire. Also it's not good for this test to use those
-            // privileged apps which have CONNECTIVITY_USE_RESTRICTED_NETWORKS to test because there
-            // is no guarantee that those apps won't remove this permission someday, and if it
-            // happens, then this test will fail.
-            setRestrictedNetworkingMode(true);
-            assertNetworkingBlockedStatusForUid(mUid, METERED,
-                    true /* expectedResult */); // Match NTWK_BLOCKED_RESTRICTED_MODE
-            assertTrue(isUidNetworkingBlocked(mUid,
-                    NON_METERED)); // Match NTWK_BLOCKED_RESTRICTED_MODE
-        } finally {
-            setRestrictedNetworkingMode(false);
-            assertNetworkingBlockedStatusForUid(mUid, METERED,
-                    false /* expectedResult */); // Match NTWK_ALLOWED_DEFAULT
-        }
-    }
-
-    @RequiredProperties({BATTERY_SAVER_MODE})
-    @Test
-    public void testIsUidNetworkingBlocked_withPowerSaverMode() throws Exception {
-        // Refer to NetworkPolicyManagerService#isUidNetworkingBlockedInternal(), this test is to
-        // test the cases of power saver mode enabled, uid in the power saver mode whitelist and
-        // uid in the power saver mode whitelist with non-metered network.
-        try {
-            // mUid should be blocked if power saver mode is enabled.
-            setBatterySaverMode(true);
-            assertNetworkingBlockedStatusForUid(mUid, METERED,
-                    true /* expectedResult */); // Match NTWK_BLOCKED_POWER
-            assertTrue(isUidNetworkingBlocked(mUid, NON_METERED)); // Match NTWK_BLOCKED_POWER
-
-            // Add TEST_APP2_PKG into power saver mode whitelist, its uid rule is RULE_ALLOW_ALL and
-            // it shouldn't be blocked.
-            addPowerSaveModeWhitelist(TEST_APP2_PKG);
-            assertNetworkingBlockedStatusForUid(mUid, METERED,
-                    false /* expectedResult */); // Match NTWK_ALLOWED_DEFAULT
-            assertFalse(
-                    isUidNetworkingBlocked(mUid, NON_METERED)); // Match NTWK_ALLOWED_NON_METERED
-            removePowerSaveModeWhitelist(TEST_APP2_PKG);
-        } finally {
-            setBatterySaverMode(false);
-            assertNetworkingBlockedStatusForUid(mUid, METERED,
-                    false /* expectedResult */); // Match NTWK_ALLOWED_DEFAULT
-        }
-    }
-
-    @RequiredProperties({DATA_SAVER_MODE})
-    @Test
-    public void testIsUidRestrictedOnMeteredNetworks() throws Exception {
-        try {
-            // isUidRestrictedOnMeteredNetworks() will only return true when restrict background is
-            // enabled and mUid is not in the restrict background whitelist and TEST_APP2_PKG is not
-            // in the foreground. For other cases, it will return false.
-            setRestrictBackground(true);
-            assertIsUidRestrictedOnMeteredNetworks(mUid, true /* expectedResult */);
-
-            // Make TEST_APP2_PKG go to foreground and isUidRestrictedOnMeteredNetworks() will
-            // return false.
-            launchActivity();
-            assertTopState();
-            assertIsUidRestrictedOnMeteredNetworks(mUid, false /* expectedResult */);
-            // Back to background.
-            finishActivity();
-            assertProcessStateBelow(PROCESS_STATE_BOUND_FOREGROUND_SERVICE);
-
-            // Add mUid into restrict background whitelist and isUidRestrictedOnMeteredNetworks()
-            // will return false.
-            addRestrictBackgroundWhitelist(mUid);
-            assertIsUidRestrictedOnMeteredNetworks(mUid, false /* expectedResult */);
-            removeRestrictBackgroundWhitelist(mUid);
-        } finally {
-            // Restrict background is disabled and isUidRestrictedOnMeteredNetworks() will return
-            // false.
-            setRestrictBackground(false);
-            assertIsUidRestrictedOnMeteredNetworks(mUid, false /* expectedResult */);
-        }
-    }
-
-    @Test
-    public void testIsUidNetworkingBlocked_whenInBackground() throws Exception {
-        assumeTrue("Feature not enabled", isNetworkBlockedForTopSleepingAndAbove());
-
-        try {
-            assertProcessStateBelow(PROCESS_STATE_LAST_ACTIVITY);
-            SystemClock.sleep(mProcessStateTransitionShortDelayMs);
-            assertNetworkingBlockedStatusForUid(mUid, METERED, true /* expectedResult */);
-            assertTrue(isUidNetworkingBlocked(mUid, NON_METERED));
-
-            launchActivity();
-            assertTopState();
-            assertNetworkingBlockedStatusForUid(mUid, METERED, false /* expectedResult */);
-            assertFalse(isUidNetworkingBlocked(mUid, NON_METERED));
-
-            finishActivity();
-            assertProcessStateBelow(PROCESS_STATE_TOP_SLEEPING);
-            SystemClock.sleep(mProcessStateTransitionLongDelayMs);
-            assertNetworkingBlockedStatusForUid(mUid, METERED, true /* expectedResult */);
-            assertTrue(isUidNetworkingBlocked(mUid, NON_METERED));
-
-            addPowerSaveModeWhitelist(TEST_APP2_PKG);
-            assertNetworkingBlockedStatusForUid(mUid, METERED, false /* expectedResult */);
-            assertFalse(isUidNetworkingBlocked(mUid, NON_METERED));
-        } finally {
-            removePowerSaveModeWhitelist(TEST_APP2_PKG);
-        }
-    }
-}
diff --git a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/NetworkPolicyTestRunner.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/NetworkPolicyTestRunner.java
deleted file mode 100644
index 0207b00..0000000
--- a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/NetworkPolicyTestRunner.java
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.cts.netpolicy.hostside;
-
-import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner;
-
-import org.junit.rules.RunRules;
-import org.junit.rules.TestRule;
-import org.junit.runners.model.FrameworkMethod;
-import org.junit.runners.model.InitializationError;
-import org.junit.runners.model.Statement;
-
-import java.util.List;
-
-/**
- * Custom runner to allow dumping logs after a test failure before the @After methods get to run.
- */
-public class NetworkPolicyTestRunner extends AndroidJUnit4ClassRunner {
-    private TestRule mDumpOnFailureRule = new DumpOnFailureRule();
-
-    public NetworkPolicyTestRunner(Class<?> klass) throws InitializationError {
-        super(klass);
-    }
-
-    @Override
-    public Statement methodInvoker(FrameworkMethod method, Object test) {
-        return new RunRules(super.methodInvoker(method, test), List.of(mDumpOnFailureRule),
-                describeChild(method));
-    }
-}
diff --git a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/NetworkPolicyTestUtils.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/NetworkPolicyTestUtils.java
deleted file mode 100644
index 26a88f2..0000000
--- a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/NetworkPolicyTestUtils.java
+++ /dev/null
@@ -1,486 +0,0 @@
-/*
- * Copyright (C) 2019 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.cts.netpolicy.hostside;
-
-import static android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_DISABLED;
-import static android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED;
-import static android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_WHITELISTED;
-import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED;
-import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
-import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
-import static android.net.wifi.WifiConfiguration.METERED_OVERRIDE_METERED;
-import static android.net.wifi.WifiConfiguration.METERED_OVERRIDE_NONE;
-
-import static com.android.compatibility.common.util.SystemUtil.runShellCommandOrThrow;
-import static com.android.cts.netpolicy.hostside.AbstractRestrictBackgroundNetworkTestCase.TAG;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
-
-import android.app.ActivityManager;
-import android.app.Instrumentation;
-import android.app.UiAutomation;
-import android.content.Context;
-import android.content.pm.PackageManager;
-import android.location.LocationManager;
-import android.net.ConnectivityManager;
-import android.net.ConnectivityManager.NetworkCallback;
-import android.net.Network;
-import android.net.NetworkCapabilities;
-import android.net.NetworkPolicyManager;
-import android.net.wifi.WifiConfiguration;
-import android.net.wifi.WifiManager;
-import android.net.wifi.WifiManager.ActionListener;
-import android.os.PersistableBundle;
-import android.os.Process;
-import android.os.UserHandle;
-import android.telephony.CarrierConfigManager;
-import android.telephony.SubscriptionManager;
-import android.telephony.data.ApnSetting;
-import android.util.Log;
-
-import androidx.test.platform.app.InstrumentationRegistry;
-import androidx.test.uiautomator.UiDevice;
-
-import com.android.compatibility.common.util.AppStandbyUtils;
-import com.android.compatibility.common.util.BatteryUtils;
-import com.android.compatibility.common.util.PollingCheck;
-import com.android.compatibility.common.util.ShellIdentityUtils;
-import com.android.compatibility.common.util.ThrowingRunnable;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-import java.util.concurrent.BlockingQueue;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.LinkedBlockingQueue;
-import java.util.concurrent.TimeUnit;
-
-public class NetworkPolicyTestUtils {
-
-    // android.telephony.CarrierConfigManager.KEY_CARRIER_METERED_APN_TYPES_STRINGS
-    // TODO: Expose it as a @TestApi instead of copying the constant
-    private static final String KEY_CARRIER_METERED_APN_TYPES_STRINGS =
-            "carrier_metered_apn_types_strings";
-
-    private static final int TIMEOUT_CHANGE_METEREDNESS_MS = 10_000;
-
-    private static ConnectivityManager mCm;
-    private static WifiManager mWm;
-    private static CarrierConfigManager mCarrierConfigManager;
-    private static NetworkPolicyManager sNpm;
-
-    private static Boolean mBatterySaverSupported;
-    private static Boolean mDataSaverSupported;
-    private static Boolean mDozeModeSupported;
-    private static Boolean mAppStandbySupported;
-
-    private NetworkPolicyTestUtils() {}
-
-    public static boolean isBatterySaverSupported() {
-        if (mBatterySaverSupported == null) {
-            mBatterySaverSupported = BatteryUtils.isBatterySaverSupported();
-        }
-        return mBatterySaverSupported;
-    }
-
-    private static boolean isWear() {
-        return getContext().getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH);
-    }
-
-    /**
-     * As per CDD requirements, if the device doesn't support data saver mode then
-     * ConnectivityManager.getRestrictBackgroundStatus() will always return
-     * RESTRICT_BACKGROUND_STATUS_DISABLED. So, enable the data saver mode and check if
-     * ConnectivityManager.getRestrictBackgroundStatus() for an app in background returns
-     * RESTRICT_BACKGROUND_STATUS_DISABLED or not.
-     */
-    public static boolean isDataSaverSupported() {
-        if (isWear()) {
-            return false;
-        }
-        if (mDataSaverSupported == null) {
-            setRestrictBackgroundInternal(false);
-            assertMyRestrictBackgroundStatus(RESTRICT_BACKGROUND_STATUS_DISABLED);
-            try {
-                setRestrictBackgroundInternal(true);
-                mDataSaverSupported = !isMyRestrictBackgroundStatus(
-                        RESTRICT_BACKGROUND_STATUS_DISABLED);
-            } finally {
-                setRestrictBackgroundInternal(false);
-            }
-        }
-        return mDataSaverSupported;
-    }
-
-    public static boolean isDozeModeSupported() {
-        if (mDozeModeSupported == null) {
-            final String result = executeShellCommand("cmd deviceidle enabled deep");
-            mDozeModeSupported = result.equals("1");
-        }
-        return mDozeModeSupported;
-    }
-
-    public static boolean isAppStandbySupported() {
-        if (mAppStandbySupported == null) {
-            mAppStandbySupported = AppStandbyUtils.isAppStandbyEnabled();
-        }
-        return mAppStandbySupported;
-    }
-
-    public static boolean isLowRamDevice() {
-        final ActivityManager am = (ActivityManager) getContext().getSystemService(
-                Context.ACTIVITY_SERVICE);
-        return am.isLowRamDevice();
-    }
-
-    /** Forces JobScheduler to run the job if constraints are met. */
-    public static void forceRunJob(String pkg, int jobId) {
-        executeShellCommand("cmd jobscheduler run -f -u " + UserHandle.myUserId()
-                + " " + pkg + " " + jobId);
-    }
-
-    public static boolean isLocationEnabled() {
-        final LocationManager lm = (LocationManager) getContext().getSystemService(
-                Context.LOCATION_SERVICE);
-        return lm.isLocationEnabled();
-    }
-
-    public static void setLocationEnabled(boolean enabled) {
-        final LocationManager lm = (LocationManager) getContext().getSystemService(
-                Context.LOCATION_SERVICE);
-        lm.setLocationEnabledForUser(enabled, Process.myUserHandle());
-        assertEquals("Couldn't change location enabled state", lm.isLocationEnabled(), enabled);
-        Log.d(TAG, "Changed location enabled state to " + enabled);
-    }
-
-    public static boolean isActiveNetworkMetered(boolean metered) {
-        return getConnectivityManager().isActiveNetworkMetered() == metered;
-    }
-
-    public static boolean canChangeActiveNetworkMeteredness() {
-        final NetworkCapabilities networkCapabilities = getActiveNetworkCapabilities();
-        return networkCapabilities.hasTransport(TRANSPORT_WIFI)
-                || networkCapabilities.hasTransport(TRANSPORT_CELLULAR);
-    }
-
-    /**
-     * Updates the meteredness of the active network. Right now we can only change meteredness
-     * of either Wifi or cellular network, so if the active network is not either of these, this
-     * will throw an exception.
-     *
-     * @return a {@link ThrowingRunnable} object that can used to reset the meteredness change
-     *         made by this method.
-     */
-    public static ThrowingRunnable setupActiveNetworkMeteredness(boolean metered) throws Exception {
-        if (isActiveNetworkMetered(metered)) {
-            return null;
-        }
-        final NetworkCapabilities networkCapabilities = getActiveNetworkCapabilities();
-        if (networkCapabilities.hasTransport(TRANSPORT_WIFI)) {
-            final String ssid = getWifiSsid();
-            setWifiMeteredStatus(ssid, metered);
-            return () -> setWifiMeteredStatus(ssid, !metered);
-        } else if (networkCapabilities.hasTransport(TRANSPORT_CELLULAR)) {
-            final int subId = SubscriptionManager.getActiveDataSubscriptionId();
-            setCellularMeteredStatus(subId, metered);
-            return () -> setCellularMeteredStatus(subId, !metered);
-        } else {
-            // Right now, we don't have a way to change meteredness of networks other
-            // than Wi-Fi or Cellular, so just throw an exception.
-            throw new IllegalStateException("Can't change meteredness of current active network");
-        }
-    }
-
-    private static String getWifiSsid() {
-        final UiAutomation uiAutomation = getInstrumentation().getUiAutomation();
-        try {
-            uiAutomation.adoptShellPermissionIdentity();
-            final String ssid = getWifiManager().getConnectionInfo().getSSID();
-            assertNotEquals(WifiManager.UNKNOWN_SSID, ssid);
-            return ssid;
-        } finally {
-            uiAutomation.dropShellPermissionIdentity();
-        }
-    }
-
-    static NetworkCapabilities getActiveNetworkCapabilities() {
-        final Network activeNetwork = getConnectivityManager().getActiveNetwork();
-        assertNotNull("No active network available", activeNetwork);
-        return getConnectivityManager().getNetworkCapabilities(activeNetwork);
-    }
-
-    private static void setWifiMeteredStatus(String ssid, boolean metered) throws Exception {
-        final UiAutomation uiAutomation = getInstrumentation().getUiAutomation();
-        try {
-            uiAutomation.adoptShellPermissionIdentity();
-            final WifiConfiguration currentConfig = getWifiConfiguration(ssid);
-            currentConfig.meteredOverride = metered
-                    ? METERED_OVERRIDE_METERED : METERED_OVERRIDE_NONE;
-            BlockingQueue<Integer> blockingQueue = new LinkedBlockingQueue<>();
-            getWifiManager().save(currentConfig, createActionListener(
-                    blockingQueue, Integer.MAX_VALUE));
-            Integer resultCode = blockingQueue.poll(TIMEOUT_CHANGE_METEREDNESS_MS,
-                    TimeUnit.MILLISECONDS);
-            if (resultCode == null) {
-                fail("Timed out waiting for meteredness to change; ssid=" + ssid
-                        + ", metered=" + metered);
-            } else if (resultCode != Integer.MAX_VALUE) {
-                fail("Error overriding the meteredness; ssid=" + ssid
-                        + ", metered=" + metered + ", error=" + resultCode);
-            }
-            final boolean success = assertActiveNetworkMetered(metered, false /* throwOnFailure */);
-            if (!success) {
-                Log.i(TAG, "Retry connecting to wifi; ssid=" + ssid);
-                blockingQueue = new LinkedBlockingQueue<>();
-                getWifiManager().connect(currentConfig, createActionListener(
-                        blockingQueue, Integer.MAX_VALUE));
-                resultCode = blockingQueue.poll(TIMEOUT_CHANGE_METEREDNESS_MS,
-                        TimeUnit.MILLISECONDS);
-                if (resultCode == null) {
-                    fail("Timed out waiting for wifi to connect; ssid=" + ssid);
-                } else if (resultCode != Integer.MAX_VALUE) {
-                    fail("Error connecting to wifi; ssid=" + ssid
-                            + ", error=" + resultCode);
-                }
-                assertActiveNetworkMetered(metered, true /* throwOnFailure */);
-            }
-        } finally {
-            uiAutomation.dropShellPermissionIdentity();
-        }
-    }
-
-    private static WifiConfiguration getWifiConfiguration(String ssid) {
-        final List<String> ssids = new ArrayList<>();
-        for (WifiConfiguration config : getWifiManager().getConfiguredNetworks()) {
-            if (config.SSID.equals(ssid)) {
-                return config;
-            }
-            ssids.add(config.SSID);
-        }
-        fail("Couldn't find the wifi config; ssid=" + ssid
-                + ", all=" + Arrays.toString(ssids.toArray()));
-        return null;
-    }
-
-    private static ActionListener createActionListener(BlockingQueue<Integer> blockingQueue,
-            int successCode) {
-        return new ActionListener() {
-            @Override
-            public void onSuccess() {
-                blockingQueue.offer(successCode);
-            }
-
-            @Override
-            public void onFailure(int reason) {
-                blockingQueue.offer(reason);
-            }
-        };
-    }
-
-    private static void setCellularMeteredStatus(int subId, boolean metered) throws Exception {
-        final PersistableBundle bundle = new PersistableBundle();
-        bundle.putStringArray(KEY_CARRIER_METERED_APN_TYPES_STRINGS,
-                new String[] {ApnSetting.TYPE_MMS_STRING});
-        ShellIdentityUtils.invokeMethodWithShellPermissionsNoReturn(getCarrierConfigManager(),
-                (cm) -> cm.overrideConfig(subId, metered ? null : bundle));
-        assertActiveNetworkMetered(metered, true /* throwOnFailure */);
-    }
-
-    private static boolean assertActiveNetworkMetered(boolean expectedMeteredStatus,
-            boolean throwOnFailure) throws Exception {
-        final CountDownLatch latch = new CountDownLatch(1);
-        final NetworkCallback networkCallback = new NetworkCallback() {
-            @Override
-            public void onCapabilitiesChanged(Network network, NetworkCapabilities nc) {
-                final boolean metered = !nc.hasCapability(NET_CAPABILITY_NOT_METERED);
-                if (metered == expectedMeteredStatus) {
-                    latch.countDown();
-                }
-            }
-        };
-        // Registering a callback here guarantees onCapabilitiesChanged is called immediately
-        // with the current setting. Therefore, if the setting has already been changed,
-        // this method will return right away, and if not it will wait for the setting to change.
-        getConnectivityManager().registerDefaultNetworkCallback(networkCallback);
-        try {
-            if (!latch.await(TIMEOUT_CHANGE_METEREDNESS_MS, TimeUnit.MILLISECONDS)) {
-                final String errorMsg = "Timed out waiting for active network metered status "
-                        + "to change to " + expectedMeteredStatus + "; network = "
-                        + getConnectivityManager().getActiveNetwork();
-                if (throwOnFailure) {
-                    fail(errorMsg);
-                }
-                Log.w(TAG, errorMsg);
-                return false;
-            }
-            return true;
-        } finally {
-            getConnectivityManager().unregisterNetworkCallback(networkCallback);
-        }
-    }
-
-    public static void setRestrictBackground(boolean enabled) {
-        if (!isDataSaverSupported()) {
-            return;
-        }
-        setRestrictBackgroundInternal(enabled);
-    }
-
-    static void setRestrictBackgroundInternal(boolean enabled) {
-        executeShellCommand("cmd netpolicy set restrict-background " + enabled);
-        final String output = executeShellCommand("cmd netpolicy get restrict-background");
-        final String expectedSuffix = enabled ? "enabled" : "disabled";
-        assertTrue("output '" + output + "' should end with '" + expectedSuffix + "'",
-                output.endsWith(expectedSuffix));
-    }
-
-    public static boolean isMyRestrictBackgroundStatus(int expectedStatus) {
-        final int actualStatus = getConnectivityManager().getRestrictBackgroundStatus();
-        if (expectedStatus != actualStatus) {
-            Log.d(TAG, "MyRestrictBackgroundStatus: "
-                    + "Expected: " + restrictBackgroundValueToString(expectedStatus)
-                    + "; Actual: " + restrictBackgroundValueToString(actualStatus));
-            return false;
-        }
-        return true;
-    }
-
-    // Copied from cts/tests/tests/net/src/android/net/cts/ConnectivityManagerTest.java
-    private static String unquoteSSID(String ssid) {
-        // SSID is returned surrounded by quotes if it can be decoded as UTF-8.
-        // Otherwise it's guaranteed not to start with a quote.
-        if (ssid.charAt(0) == '"') {
-            return ssid.substring(1, ssid.length() - 1);
-        } else {
-            return ssid;
-        }
-    }
-
-    public static String restrictBackgroundValueToString(int status) {
-        switch (status) {
-            case RESTRICT_BACKGROUND_STATUS_DISABLED:
-                return "DISABLED";
-            case RESTRICT_BACKGROUND_STATUS_WHITELISTED:
-                return "WHITELISTED";
-            case RESTRICT_BACKGROUND_STATUS_ENABLED:
-                return "ENABLED";
-            default:
-                return "UNKNOWN_STATUS_" + status;
-        }
-    }
-
-    public static void clearSnoozeTimestamps() {
-        executeShellCommand("dumpsys netpolicy --unsnooze");
-    }
-
-    public static String executeShellCommand(String command) {
-        final String result = runShellCommandOrThrow(command).trim();
-        Log.d(TAG, "Output of '" + command + "': '" + result + "'");
-        return result;
-    }
-
-    public static void assertMyRestrictBackgroundStatus(int expectedStatus) {
-        final int actualStatus = getConnectivityManager().getRestrictBackgroundStatus();
-        assertEquals(restrictBackgroundValueToString(expectedStatus),
-                restrictBackgroundValueToString(actualStatus));
-    }
-
-    public static ConnectivityManager getConnectivityManager() {
-        if (mCm == null) {
-            mCm = (ConnectivityManager) getContext().getSystemService(Context.CONNECTIVITY_SERVICE);
-        }
-        return mCm;
-    }
-
-    public static WifiManager getWifiManager() {
-        if (mWm == null) {
-            mWm = (WifiManager) getContext().getSystemService(Context.WIFI_SERVICE);
-        }
-        return mWm;
-    }
-
-    public static CarrierConfigManager getCarrierConfigManager() {
-        if (mCarrierConfigManager == null) {
-            mCarrierConfigManager = (CarrierConfigManager) getContext().getSystemService(
-                    Context.CARRIER_CONFIG_SERVICE);
-        }
-        return mCarrierConfigManager;
-    }
-
-    public static NetworkPolicyManager getNetworkPolicyManager() {
-        if (sNpm == null) {
-            sNpm = getContext().getSystemService(NetworkPolicyManager.class);
-        }
-        return sNpm;
-    }
-
-    public static Context getContext() {
-        return getInstrumentation().getContext();
-    }
-
-    public static Instrumentation getInstrumentation() {
-        return InstrumentationRegistry.getInstrumentation();
-    }
-
-    public static UiDevice getUiDevice() {
-        return UiDevice.getInstance(getInstrumentation());
-    }
-
-    // When power saver mode or restrict background enabled or adding any white/black list into
-    // those modes, NetworkPolicy may need to take some time to update the rules of uids. So having
-    // this function and using PollingCheck to try to make sure the uid has updated and reduce the
-    // flaky rate.
-    public static void assertNetworkingBlockedStatusForUid(int uid, boolean metered,
-            boolean expectedResult) {
-        final String errMsg = String.format("Unexpected result from isUidNetworkingBlocked; "
-                + "uid= " + uid + ", metered=" + metered + ", expected=" + expectedResult);
-        PollingCheck.waitFor(() -> (expectedResult == isUidNetworkingBlocked(uid, metered)),
-                errMsg);
-    }
-
-    public static void assertIsUidRestrictedOnMeteredNetworks(int uid, boolean expectedResult) {
-        final String errMsg = String.format(
-                "Unexpected result from isUidRestrictedOnMeteredNetworks; "
-                + "uid= " + uid + ", expected=" + expectedResult);
-        PollingCheck.waitFor(() -> (expectedResult == isUidRestrictedOnMeteredNetworks(uid)),
-                errMsg);
-    }
-
-    public static boolean isUidNetworkingBlocked(int uid, boolean meteredNetwork) {
-        final UiAutomation uiAutomation = getInstrumentation().getUiAutomation();
-        try {
-            uiAutomation.adoptShellPermissionIdentity();
-            return getNetworkPolicyManager().isUidNetworkingBlocked(uid, meteredNetwork);
-        } finally {
-            uiAutomation.dropShellPermissionIdentity();
-        }
-    }
-
-    public static boolean isUidRestrictedOnMeteredNetworks(int uid) {
-        final UiAutomation uiAutomation = getInstrumentation().getUiAutomation();
-        try {
-            uiAutomation.adoptShellPermissionIdentity();
-            return getNetworkPolicyManager().isUidRestrictedOnMeteredNetworks(uid);
-        } finally {
-            uiAutomation.dropShellPermissionIdentity();
-        }
-    }
-}
diff --git a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/Property.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/Property.java
deleted file mode 100644
index a03833f..0000000
--- a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/Property.java
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
- * Copyright (C) 2019 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.cts.netpolicy.hostside;
-
-import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.canChangeActiveNetworkMeteredness;
-import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.isActiveNetworkMetered;
-import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.isAppStandbySupported;
-import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.isBatterySaverSupported;
-import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.isDataSaverSupported;
-import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.isDozeModeSupported;
-import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.isLowRamDevice;
-
-public enum Property {
-    BATTERY_SAVER_MODE(1 << 0) {
-        public boolean isSupported() { return isBatterySaverSupported(); }
-    },
-
-    DATA_SAVER_MODE(1 << 1) {
-        public boolean isSupported() { return isDataSaverSupported(); }
-    },
-
-    NO_DATA_SAVER_MODE(~DATA_SAVER_MODE.getValue()) {
-        public boolean isSupported() { return !isDataSaverSupported(); }
-    },
-
-    DOZE_MODE(1 << 2) {
-        public boolean isSupported() { return isDozeModeSupported(); }
-    },
-
-    APP_STANDBY_MODE(1 << 3) {
-        public boolean isSupported() { return isAppStandbySupported(); }
-    },
-
-    NOT_LOW_RAM_DEVICE(1 << 4) {
-        public boolean isSupported() { return !isLowRamDevice(); }
-    },
-
-    METERED_NETWORK(1 << 5) {
-        public boolean isSupported() {
-            return isActiveNetworkMetered(true) || canChangeActiveNetworkMeteredness();
-        }
-    },
-
-    NON_METERED_NETWORK(~METERED_NETWORK.getValue()) {
-        public boolean isSupported() {
-            return isActiveNetworkMetered(false) || canChangeActiveNetworkMeteredness();
-        }
-    };
-
-    private int mValue;
-
-    Property(int value) { mValue = value; }
-
-    public int getValue() { return mValue; }
-
-    abstract boolean isSupported();
-}
diff --git a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/RequiredProperties.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/RequiredProperties.java
deleted file mode 100644
index 799a513..0000000
--- a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/RequiredProperties.java
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- * Copyright (C) 2019 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.cts.netpolicy.hostside;
-
-import static java.lang.annotation.ElementType.METHOD;
-import static java.lang.annotation.ElementType.TYPE;
-import static java.lang.annotation.RetentionPolicy.RUNTIME;
-
-import java.lang.annotation.Inherited;
-import java.lang.annotation.Retention;
-import java.lang.annotation.Target;
-
-@Retention(RUNTIME)
-@Target({METHOD, TYPE})
-@Inherited
-public @interface RequiredProperties {
-    Property[] value();
-}
diff --git a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/RequiredPropertiesRule.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/RequiredPropertiesRule.java
deleted file mode 100644
index 5dea67c..0000000
--- a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/RequiredPropertiesRule.java
+++ /dev/null
@@ -1,94 +0,0 @@
-/*
- * Copyright (C) 2019 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.cts.netpolicy.hostside;
-
-import static com.android.cts.netpolicy.hostside.AbstractRestrictBackgroundNetworkTestCase.TAG;
-
-import android.text.TextUtils;
-import android.util.ArraySet;
-import android.util.Log;
-
-import com.android.compatibility.common.util.BeforeAfterRule;
-
-import org.junit.Assume;
-import org.junit.runner.Description;
-import org.junit.runners.model.Statement;
-
-import java.util.ArrayList;
-import java.util.Collections;
-
-public class RequiredPropertiesRule extends BeforeAfterRule {
-
-    private static ArraySet<Property> mRequiredProperties;
-
-    @Override
-    public void onBefore(Statement base, Description description) {
-        mRequiredProperties = getAllRequiredProperties(description);
-
-        final String testName = description.getClassName() + "#" + description.getMethodName();
-        assertTestIsValid(testName, mRequiredProperties);
-        Log.i(TAG, "Running test " + testName + " with required properties: "
-                + propertiesToString(mRequiredProperties));
-    }
-
-    private ArraySet<Property> getAllRequiredProperties(Description description) {
-        final ArraySet<Property> allRequiredProperties = new ArraySet<>();
-        RequiredProperties requiredProperties = description.getAnnotation(RequiredProperties.class);
-        if (requiredProperties != null) {
-            Collections.addAll(allRequiredProperties, requiredProperties.value());
-        }
-
-        for (Class<?> clazz = description.getTestClass();
-                clazz != null; clazz = clazz.getSuperclass()) {
-            requiredProperties = clazz.getDeclaredAnnotation(RequiredProperties.class);
-            if (requiredProperties == null) {
-                continue;
-            }
-            for (Property requiredProperty : requiredProperties.value()) {
-                for (Property p : Property.values()) {
-                    if (p.getValue() == ~requiredProperty.getValue()
-                            && allRequiredProperties.contains(p)) {
-                        continue;
-                    }
-                }
-                allRequiredProperties.add(requiredProperty);
-            }
-        }
-        return allRequiredProperties;
-    }
-
-    private void assertTestIsValid(String testName, ArraySet<Property> requiredProperies) {
-        if (requiredProperies == null) {
-            return;
-        }
-        final ArrayList<Property> unsupportedProperties = new ArrayList<>();
-        for (Property property : requiredProperies) {
-            if (!property.isSupported()) {
-                unsupportedProperties.add(property);
-            }
-        }
-        Assume.assumeTrue("Unsupported properties: "
-                + propertiesToString(unsupportedProperties), unsupportedProperties.isEmpty());
-    }
-
-    public static ArraySet<Property> getRequiredProperties() {
-        return mRequiredProperties;
-    }
-
-    private static String propertiesToString(Iterable<Property> properties) {
-        return "[" + TextUtils.join(",", properties) + "]";
-    }
-}
diff --git a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/RestrictedModeTest.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/RestrictedModeTest.java
deleted file mode 100644
index f183f4e..0000000
--- a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/RestrictedModeTest.java
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- * Copyright (C) 2021 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.cts.netpolicy.hostside;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-
-public final class RestrictedModeTest extends AbstractRestrictBackgroundNetworkTestCase {
-    @Before
-    public void setUp() throws Exception {
-        super.setUp();
-        setRestrictedNetworkingMode(false);
-    }
-
-    @After
-    public void tearDown() throws Exception {
-        setRestrictedNetworkingMode(false);
-        super.tearDown();
-    }
-
-    @Test
-    public void testNetworkAccess() throws Exception {
-        // go to foreground state and enable restricted mode
-        launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_ACTIVTIY);
-        setRestrictedNetworkingMode(true);
-        assertTopNetworkAccess(false);
-
-        // go to background state
-        finishActivity();
-        assertBackgroundNetworkAccess(false);
-
-        // disable restricted mode and assert network access in foreground and background states
-        setRestrictedNetworkingMode(false);
-        launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_ACTIVTIY);
-        assertTopNetworkAccess(true);
-
-        // go to background state
-        finishActivity();
-        assertBackgroundNetworkAccess(true);
-    }
-
-    @Test
-    public void testNetworkAccess_withBatterySaver() throws Exception {
-        setBatterySaverMode(true);
-        try {
-            addPowerSaveModeWhitelist(TEST_APP2_PKG);
-            assertBackgroundNetworkAccess(true);
-
-            setRestrictedNetworkingMode(true);
-            // App would be denied network access since Restricted mode is on.
-            assertBackgroundNetworkAccess(false);
-            setRestrictedNetworkingMode(false);
-            // Given that Restricted mode is turned off, app should be able to access network again.
-            assertBackgroundNetworkAccess(true);
-        } finally {
-            setBatterySaverMode(false);
-        }
-    }
-}
diff --git a/tests/cts/hostside-network-policy/app2/Android.bp b/tests/cts/hostside-network-policy/app2/Android.bp
deleted file mode 100644
index 6ef0b06..0000000
--- a/tests/cts/hostside-network-policy/app2/Android.bp
+++ /dev/null
@@ -1,39 +0,0 @@
-//
-// Copyright (C) 2024 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-
-package {
-    default_team: "trendy_team_framework_backstage_power",
-    default_applicable_licenses: ["Android-Apache-2.0"],
-}
-
-android_test_helper_app {
-    name: "CtsHostsideNetworkPolicyTestsApp2",
-    defaults: ["cts_support_defaults"],
-    platform_apis: true,
-    static_libs: [
-        "androidx.annotation_annotation",
-        "CtsHostsideNetworkPolicyTestsAidl",
-        "modules-utils-build",
-    ],
-    srcs: ["src/**/*.java"],
-    // Tag this module as a cts test artifact
-    test_suites: [
-        "cts",
-        "general-tests",
-        "sts",
-    ],
-    certificate: ":cts-netpolicy-app",
-}
diff --git a/tests/cts/hostside-network-policy/app2/AndroidManifest.xml b/tests/cts/hostside-network-policy/app2/AndroidManifest.xml
deleted file mode 100644
index 668f2da..0000000
--- a/tests/cts/hostside-network-policy/app2/AndroidManifest.xml
+++ /dev/null
@@ -1,77 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-     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.
--->
-
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
-     package="com.android.cts.netpolicy.hostside.app2">
-
-    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
-    <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
-    <uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"/>
-    <uses-permission android:name="android.permission.INTERNET"/>
-    <uses-permission android:name="android.permission.PACKAGE_USAGE_STATS" />
-    <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
-
-    <!--
-     This application is used to listen to RESTRICT_BACKGROUND_CHANGED intents and store
-     them in a shared preferences which is then read by the test app. These broadcasts are
-     handled by 2 listeners, one defined the manifest and another dynamically registered by
-     a service.
-
-     The manifest-defined listener also handles ordered broadcasts used to share data with the
-     test app.
-
-     This application also provides a service, RemoteSocketFactoryService, that the test app can
-     use to open sockets to remote hosts as a different user ID.
-    -->
-    <application android:usesCleartextTraffic="true"
-            android:testOnly="true"
-            android:debuggable="true">
-
-        <activity android:name=".MyActivity"
-             android:exported="true"/>
-        <service android:name=".MyService"
-             android:exported="true"/>
-        <service android:name=".MyForegroundService"
-             android:foregroundServiceType="specialUse"
-             android:exported="true">
-            <property android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
-                      android:value="Connectivity" />
-        </service>
-        <receiver android:name=".MyBroadcastReceiver"
-             android:exported="true">
-            <intent-filter>
-                <action android:name="android.net.conn.RESTRICT_BACKGROUND_CHANGED"/>
-                <action android:name="com.android.cts.netpolicy.hostside.app2.action.GET_COUNTERS"/>
-                <action android:name="com.android.cts.netpolicy.hostside.app2.action.GET_RESTRICT_BACKGROUND_STATUS"/>
-                <action android:name="com.android.cts.netpolicy.hostside.app2.action.CHECK_NETWORK"/>
-                <action android:name="com.android.cts.netpolicy.hostside.app2.action.SEND_NOTIFICATION"/>
-                <action android:name="com.android.cts.netpolicy.hostside.app2.action.SHOW_TOAST"/>
-                </intent-filter>
-        </receiver>
-        <service android:name=".MyJobService"
-            android:permission="android.permission.BIND_JOB_SERVICE" />
-    </application>
-
-    <!--
-      Adding this to make sure that receiving the broadcast is not restricted by
-      package visibility restrictions.
-    -->
-    <queries>
-        <package android:name="android" />
-    </queries>
-
-</manifest>
diff --git a/tests/cts/hostside-network-policy/app2/res/drawable/ic_notification.png b/tests/cts/hostside-network-policy/app2/res/drawable/ic_notification.png
deleted file mode 100644
index 6ae570b..0000000
--- a/tests/cts/hostside-network-policy/app2/res/drawable/ic_notification.png
+++ /dev/null
Binary files differ
diff --git a/tests/cts/hostside-network-policy/app2/src/com/android/cts/netpolicy/hostside/app2/Common.java b/tests/cts/hostside-network-policy/app2/src/com/android/cts/netpolicy/hostside/app2/Common.java
deleted file mode 100644
index 1719f9b..0000000
--- a/tests/cts/hostside-network-policy/app2/src/com/android/cts/netpolicy/hostside/app2/Common.java
+++ /dev/null
@@ -1,234 +0,0 @@
-/*
- * Copyright (C) 2016 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.cts.netpolicy.hostside.app2;
-
-import static com.android.cts.netpolicy.hostside.INetworkStateObserver.RESULT_ERROR_OTHER;
-import static com.android.cts.netpolicy.hostside.INetworkStateObserver.RESULT_ERROR_UNEXPECTED_CAPABILITIES;
-import static com.android.cts.netpolicy.hostside.INetworkStateObserver.RESULT_ERROR_UNEXPECTED_PROC_STATE;
-
-import android.app.ActivityManager;
-import android.content.Context;
-import android.content.Intent;
-import android.content.pm.PackageManager.NameNotFoundException;
-import android.net.ConnectivityManager;
-import android.net.NetworkInfo;
-import android.os.AsyncTask;
-import android.os.Bundle;
-import android.os.Process;
-import android.os.RemoteException;
-import android.util.Log;
-
-import com.android.cts.netpolicy.hostside.INetworkStateObserver;
-import com.android.cts.netpolicy.hostside.NetworkCheckResult;
-
-import java.net.HttpURLConnection;
-import java.net.InetAddress;
-import java.net.URL;
-import java.util.concurrent.TimeUnit;
-
-public final class Common {
-
-    static final String TAG = "CtsNetApp2";
-
-    // Constants below must match values defined on app's
-    // AbstractRestrictBackgroundNetworkTestCase.java
-    static final String MANIFEST_RECEIVER = "ManifestReceiver";
-    static final String DYNAMIC_RECEIVER = "DynamicReceiver";
-
-    static final String ACTION_RECEIVER_READY =
-            "com.android.cts.netpolicy.hostside.app2.action.RECEIVER_READY";
-    static final String ACTION_FINISH_ACTIVITY =
-            "com.android.cts.netpolicy.hostside.app2.action.FINISH_ACTIVITY";
-    static final String ACTION_FINISH_JOB =
-            "com.android.cts.netpolicy.hostside.app2.action.FINISH_JOB";
-    static final String ACTION_SHOW_TOAST =
-            "com.android.cts.netpolicy.hostside.app2.action.SHOW_TOAST";
-    // Copied from com.android.server.net.NetworkPolicyManagerService class
-    static final String ACTION_SNOOZE_WARNING =
-            "com.android.server.net.action.SNOOZE_WARNING";
-
-    private static final String DEFAULT_TEST_URL =
-            "https://connectivitycheck.android.com/generate_204";
-
-    static final String NOTIFICATION_TYPE_CONTENT = "CONTENT";
-    static final String NOTIFICATION_TYPE_DELETE = "DELETE";
-    static final String NOTIFICATION_TYPE_FULL_SCREEN = "FULL_SCREEN";
-    static final String NOTIFICATION_TYPE_BUNDLE = "BUNDLE";
-    static final String NOTIFICATION_TYPE_ACTION = "ACTION";
-    static final String NOTIFICATION_TYPE_ACTION_BUNDLE = "ACTION_BUNDLE";
-    static final String NOTIFICATION_TYPE_ACTION_REMOTE_INPUT = "ACTION_REMOTE_INPUT";
-
-    static final String TEST_PKG = "com.android.cts.netpolicy.hostside";
-    static final String KEY_NETWORK_STATE_OBSERVER = TEST_PKG + ".observer";
-    static final String KEY_SKIP_VALIDATION_CHECKS = TEST_PKG + ".skip_validation_checks";
-    static final String KEY_CUSTOM_URL =  TEST_PKG + ".custom_url";
-
-    static final int TYPE_COMPONENT_ACTIVTY = 0;
-    static final int TYPE_COMPONENT_FOREGROUND_SERVICE = 1;
-    static final int TYPE_COMPONENT_EXPEDITED_JOB = 2;
-    private static final int NETWORK_TIMEOUT_MS = (int) TimeUnit.SECONDS.toMillis(10);
-
-    static int getUid(Context context) {
-        final String packageName = context.getPackageName();
-        try {
-            return context.getPackageManager().getPackageUid(packageName, 0);
-        } catch (NameNotFoundException e) {
-            throw new IllegalStateException("Could not get UID for " + packageName, e);
-        }
-    }
-
-    private static NetworkCheckResult createNetworkCheckResult(boolean connected, String details,
-            NetworkInfo networkInfo) {
-        final NetworkCheckResult checkResult = new NetworkCheckResult();
-        checkResult.connected = connected;
-        checkResult.details = details;
-        checkResult.networkInfo = networkInfo;
-        return checkResult;
-    }
-
-    private static boolean validateComponentState(Context context, int componentType,
-            INetworkStateObserver observer) throws RemoteException {
-        final ActivityManager activityManager = context.getSystemService(ActivityManager.class);
-        switch (componentType) {
-            case TYPE_COMPONENT_ACTIVTY: {
-                final int procState = activityManager.getUidProcessState(Process.myUid());
-                if (procState != ActivityManager.PROCESS_STATE_TOP) {
-                    observer.onNetworkStateChecked(RESULT_ERROR_UNEXPECTED_PROC_STATE,
-                            createNetworkCheckResult(false, "Unexpected procstate: " + procState,
-                                    null));
-                    return false;
-                }
-                return true;
-            }
-            case TYPE_COMPONENT_FOREGROUND_SERVICE: {
-                final int procState = activityManager.getUidProcessState(Process.myUid());
-                if (procState != ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE) {
-                    observer.onNetworkStateChecked(RESULT_ERROR_UNEXPECTED_PROC_STATE,
-                            createNetworkCheckResult(false, "Unexpected procstate: " + procState,
-                                    null));
-                    return false;
-                }
-                return true;
-            }
-            case TYPE_COMPONENT_EXPEDITED_JOB: {
-                final int capabilities = activityManager.getUidProcessCapabilities(Process.myUid());
-                if ((capabilities
-                        & ActivityManager.PROCESS_CAPABILITY_POWER_RESTRICTED_NETWORK) == 0) {
-                    observer.onNetworkStateChecked(RESULT_ERROR_UNEXPECTED_CAPABILITIES,
-                            createNetworkCheckResult(false,
-                                    "Unexpected capabilities: " + capabilities, null));
-                    return false;
-                }
-                return true;
-            }
-            default: {
-                observer.onNetworkStateChecked(RESULT_ERROR_OTHER,
-                        createNetworkCheckResult(false, "Unknown component type: " + componentType,
-                                null));
-                return false;
-            }
-        }
-    }
-
-    static void notifyNetworkStateObserver(Context context, Intent intent, int componentType) {
-        if (intent == null) {
-            return;
-        }
-        final Bundle extras = intent.getExtras();
-        notifyNetworkStateObserver(context, extras, componentType);
-    }
-
-    static void notifyNetworkStateObserver(Context context, Bundle extras, int componentType) {
-        if (extras == null) {
-            return;
-        }
-        final INetworkStateObserver observer = INetworkStateObserver.Stub.asInterface(
-                extras.getBinder(KEY_NETWORK_STATE_OBSERVER));
-        if (observer != null) {
-            final String customUrl = extras.getString(KEY_CUSTOM_URL);
-            try {
-                final boolean skipValidation = extras.getBoolean(KEY_SKIP_VALIDATION_CHECKS);
-                if (!skipValidation && !validateComponentState(context, componentType, observer)) {
-                    return;
-                }
-            } catch (RemoteException e) {
-                Log.e(TAG, "Error occurred while informing the validation result: " + e);
-            }
-            AsyncTask.execute(() -> {
-                try {
-                    observer.onNetworkStateChecked(
-                            INetworkStateObserver.RESULT_SUCCESS_NETWORK_STATE_CHECKED,
-                            checkNetworkStatus(context, customUrl));
-                } catch (RemoteException e) {
-                    Log.e(TAG, "Error occurred while notifying the observer: " + e);
-                }
-            });
-        }
-    }
-
-    /**
-     * Checks whether the network is available by attempting a connection to the given address
-     * and returns a {@link NetworkCheckResult} object containing all the relevant details for
-     * debugging. Uses a default address if the given address is {@code null}.
-     *
-     * <p>
-     * The returned object has the following fields:
-     *
-     * <ul>
-     * <li>{@code connected}: whether or not the connection was successful.
-     * <li>{@code networkInfo}: the {@link NetworkInfo} describing the current active network as
-     * visible to this app.
-     * <li>{@code details}: A human readable string giving useful information about the success or
-     * failure.
-     * </ul>
-     */
-    static NetworkCheckResult checkNetworkStatus(Context context, String customUrl) {
-        final String address = (customUrl == null) ? DEFAULT_TEST_URL : customUrl;
-
-        // The current Android DNS resolver returns an UnknownHostException whenever network access
-        // is blocked. This can get cached in the current process-local InetAddress cache. Clearing
-        // the cache before attempting a connection ensures we never report a failure due to a
-        // negative cache entry.
-        InetAddress.clearDnsCache();
-
-        final ConnectivityManager cm = context.getSystemService(ConnectivityManager.class);
-
-        final NetworkInfo networkInfo = cm.getActiveNetworkInfo();
-        Log.d(TAG, "Running checkNetworkStatus() on thread "
-                + Thread.currentThread().getName() + " for UID " + getUid(context)
-                + "\n\tactiveNetworkInfo: " + networkInfo + "\n\tURL: " + address);
-        boolean checkStatus = false;
-        String checkDetails = "N/A";
-        try {
-            final URL url = new URL(address);
-            final HttpURLConnection conn = (HttpURLConnection) url.openConnection();
-            conn.setReadTimeout(NETWORK_TIMEOUT_MS);
-            conn.setConnectTimeout(NETWORK_TIMEOUT_MS / 2);
-            conn.setRequestMethod("GET");
-            conn.connect();
-            final int response = conn.getResponseCode();
-            checkStatus = true;
-            checkDetails = "HTTP response for " + address + ": " + response;
-        } catch (Exception e) {
-            checkStatus = false;
-            checkDetails = "Exception getting " + address + ": " + e;
-        }
-        final NetworkCheckResult result = createNetworkCheckResult(checkStatus, checkDetails,
-                networkInfo);
-        Log.d(TAG, "Offering: " + result);
-        return result;
-    }
-}
diff --git a/tests/cts/hostside-network-policy/app2/src/com/android/cts/netpolicy/hostside/app2/MyActivity.java b/tests/cts/hostside-network-policy/app2/src/com/android/cts/netpolicy/hostside/app2/MyActivity.java
deleted file mode 100644
index d274c50..0000000
--- a/tests/cts/hostside-network-policy/app2/src/com/android/cts/netpolicy/hostside/app2/MyActivity.java
+++ /dev/null
@@ -1,102 +0,0 @@
-/*
- * Copyright (C) 2016 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.cts.netpolicy.hostside.app2;
-
-import static com.android.cts.netpolicy.hostside.app2.Common.ACTION_FINISH_ACTIVITY;
-import static com.android.cts.netpolicy.hostside.app2.Common.TAG;
-import static com.android.cts.netpolicy.hostside.app2.Common.TYPE_COMPONENT_ACTIVTY;
-
-import android.app.Activity;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.os.Bundle;
-import android.os.RemoteCallback;
-import android.util.Log;
-import android.view.WindowManager;
-
-import androidx.annotation.GuardedBy;
-
-/**
- * Activity used to bring process to foreground.
- */
-public class MyActivity extends Activity {
-
-    @GuardedBy("this")
-    private BroadcastReceiver finishCommandReceiver = null;
-
-    @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-        Log.d(TAG, "MyActivity.onCreate()");
-
-        getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
-    }
-
-    @Override
-    public void finish() {
-        synchronized (this) {
-            if (finishCommandReceiver != null) {
-                unregisterReceiver(finishCommandReceiver);
-                finishCommandReceiver = null;
-            }
-        }
-        super.finish();
-    }
-
-    @Override
-    protected void onStart() {
-        super.onStart();
-        Log.d(TAG, "MyActivity.onStart()");
-    }
-
-    @Override
-    protected void onNewIntent(Intent intent) {
-        super.onNewIntent(intent);
-        Log.d(TAG, "MyActivity.onNewIntent()");
-        setIntent(intent);
-    }
-
-    @Override
-    protected void onResume() {
-        super.onResume();
-        Log.d(TAG, "MyActivity.onResume(): " + getIntent());
-        Common.notifyNetworkStateObserver(this, getIntent(), TYPE_COMPONENT_ACTIVTY);
-        synchronized (this) {
-            finishCommandReceiver = new BroadcastReceiver() {
-                @Override
-                public void onReceive(Context context, Intent intent) {
-                    Log.d(TAG, "Finishing MyActivity");
-                    MyActivity.this.finish();
-                }
-            };
-            registerReceiver(finishCommandReceiver, new IntentFilter(ACTION_FINISH_ACTIVITY),
-                    Context.RECEIVER_EXPORTED);
-        }
-        final RemoteCallback callback = getIntent().getParcelableExtra(
-                Intent.EXTRA_REMOTE_CALLBACK);
-        if (callback != null) {
-            callback.sendResult(null);
-        }
-    }
-
-    @Override
-    protected void onDestroy() {
-        Log.d(TAG, "MyActivity.onDestroy()");
-        super.onDestroy();
-    }
-}
diff --git a/tests/cts/hostside-network-policy/app2/src/com/android/cts/netpolicy/hostside/app2/MyBroadcastReceiver.java b/tests/cts/hostside-network-policy/app2/src/com/android/cts/netpolicy/hostside/app2/MyBroadcastReceiver.java
deleted file mode 100644
index 27aec8c..0000000
--- a/tests/cts/hostside-network-policy/app2/src/com/android/cts/netpolicy/hostside/app2/MyBroadcastReceiver.java
+++ /dev/null
@@ -1,193 +0,0 @@
-/*
- * Copyright (C) 2016 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.cts.netpolicy.hostside.app2;
-
-import static android.net.ConnectivityManager.ACTION_RESTRICT_BACKGROUND_CHANGED;
-
-import static com.android.cts.netpolicy.hostside.app2.Common.ACTION_RECEIVER_READY;
-import static com.android.cts.netpolicy.hostside.app2.Common.ACTION_SHOW_TOAST;
-import static com.android.cts.netpolicy.hostside.app2.Common.ACTION_SNOOZE_WARNING;
-import static com.android.cts.netpolicy.hostside.app2.Common.MANIFEST_RECEIVER;
-import static com.android.cts.netpolicy.hostside.app2.Common.NOTIFICATION_TYPE_ACTION;
-import static com.android.cts.netpolicy.hostside.app2.Common.NOTIFICATION_TYPE_ACTION_BUNDLE;
-import static com.android.cts.netpolicy.hostside.app2.Common.NOTIFICATION_TYPE_ACTION_REMOTE_INPUT;
-import static com.android.cts.netpolicy.hostside.app2.Common.NOTIFICATION_TYPE_BUNDLE;
-import static com.android.cts.netpolicy.hostside.app2.Common.NOTIFICATION_TYPE_CONTENT;
-import static com.android.cts.netpolicy.hostside.app2.Common.NOTIFICATION_TYPE_DELETE;
-import static com.android.cts.netpolicy.hostside.app2.Common.NOTIFICATION_TYPE_FULL_SCREEN;
-import static com.android.cts.netpolicy.hostside.app2.Common.TAG;
-
-import android.app.Notification;
-import android.app.Notification.Action;
-import android.app.NotificationManager;
-import android.app.PendingIntent;
-import android.app.RemoteInput;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.SharedPreferences;
-import android.net.ConnectivityManager;
-import android.os.Bundle;
-import android.util.Log;
-import android.widget.Toast;
-
-/**
- * Receiver used to:
- * <ol>
- *   <li>Count number of {@code RESTRICT_BACKGROUND_CHANGED} broadcasts received.
- *   <li>Show a toast.
- * </ol>
- */
-public class MyBroadcastReceiver extends BroadcastReceiver {
-
-    private final String mName;
-
-    public MyBroadcastReceiver() {
-        this(MANIFEST_RECEIVER);
-    }
-
-    MyBroadcastReceiver(String name) {
-        Log.d(TAG, "Constructing MyBroadcastReceiver named " + name);
-        mName = name;
-    }
-
-    @Override
-    public void onReceive(Context context, Intent intent) {
-        Log.d(TAG, "onReceive() for " + mName + ": " + intent);
-        final String action = intent.getAction();
-        switch (action) {
-            case ACTION_SNOOZE_WARNING:
-                increaseCounter(context, action);
-                break;
-            case ACTION_RESTRICT_BACKGROUND_CHANGED:
-                increaseCounter(context, action);
-                break;
-            case ACTION_RECEIVER_READY:
-                final String message = mName + " is ready to rumble";
-                Log.d(TAG, message);
-                setResultData(message);
-                break;
-            case ACTION_SHOW_TOAST:
-                showToast(context);
-                break;
-            default:
-                Log.e(TAG, "received unexpected action: " + action);
-        }
-    }
-
-    @Override
-    public String toString() {
-        return "[MyBroadcastReceiver: mName=" + mName + "]";
-    }
-
-    private void increaseCounter(Context context, String action) {
-        final SharedPreferences prefs = context.getApplicationContext()
-                .getSharedPreferences(mName, Context.MODE_PRIVATE);
-        final int value = prefs.getInt(action, 0) + 1;
-        Log.d(TAG, "increaseCounter('" + action + "'): setting '" + mName + "' to " + value);
-        prefs.edit().putInt(action, value).apply();
-    }
-
-    static int getCounter(Context context, String action, String receiverName) {
-        final SharedPreferences prefs = context.getSharedPreferences(receiverName,
-                Context.MODE_PRIVATE);
-        final int value = prefs.getInt(action, 0);
-        Log.d(TAG, "getCounter('" + action + "', '" + receiverName + "'): " + value);
-        return value;
-    }
-
-    static String getRestrictBackgroundStatus(Context context) {
-        final ConnectivityManager cm = (ConnectivityManager) context
-                .getSystemService(Context.CONNECTIVITY_SERVICE);
-        final int apiStatus = cm.getRestrictBackgroundStatus();
-        Log.d(TAG, "getRestrictBackgroundStatus: returning " + apiStatus);
-        return String.valueOf(apiStatus);
-    }
-
-    /**
-     * Sends a system notification containing actions with pending intents to launch the app's
-     * main activitiy or service.
-     */
-    static void sendNotification(Context context, String channelId, int notificationId,
-            String notificationType ) {
-        Log.d(TAG, "sendNotification: id=" + notificationId + ", type=" + notificationType);
-        final Intent serviceIntent = new Intent(context, MyService.class);
-        final PendingIntent pendingIntent = PendingIntent.getService(context, 0, serviceIntent,
-                PendingIntent.FLAG_MUTABLE);
-        final Bundle bundle = new Bundle();
-        bundle.putCharSequence("parcelable", "I am not");
-
-        final Notification.Builder builder = new Notification.Builder(context, channelId)
-                .setSmallIcon(R.drawable.ic_notification);
-
-        Action action = null;
-        switch (notificationType) {
-            case NOTIFICATION_TYPE_CONTENT:
-                builder
-                    .setContentTitle("Light, Cameras...")
-                    .setContentIntent(pendingIntent);
-                break;
-            case NOTIFICATION_TYPE_DELETE:
-                builder.setDeleteIntent(pendingIntent);
-                break;
-            case NOTIFICATION_TYPE_FULL_SCREEN:
-                builder.setFullScreenIntent(pendingIntent, true);
-                break;
-            case NOTIFICATION_TYPE_BUNDLE:
-                bundle.putParcelable("Magnum P.I. (Pending Intent)", pendingIntent);
-                builder.setExtras(bundle);
-                break;
-            case NOTIFICATION_TYPE_ACTION:
-                action = new Action.Builder(
-                        R.drawable.ic_notification, "ACTION", pendingIntent)
-                        .build();
-                builder.addAction(action);
-                break;
-            case NOTIFICATION_TYPE_ACTION_BUNDLE:
-                bundle.putParcelable("Magnum A.P.I. (Action Pending Intent)", pendingIntent);
-                action = new Action.Builder(
-                        R.drawable.ic_notification, "ACTION WITH BUNDLE", null)
-                        .addExtras(bundle)
-                        .build();
-                builder.addAction(action);
-                break;
-            case NOTIFICATION_TYPE_ACTION_REMOTE_INPUT:
-                bundle.putParcelable("Magnum R.I. (Remote Input)", null);
-                final RemoteInput remoteInput = new RemoteInput.Builder("RI")
-                    .addExtras(bundle)
-                    .build();
-                action = new Action.Builder(
-                        R.drawable.ic_notification, "ACTION WITH REMOTE INPUT", pendingIntent)
-                        .addRemoteInput(remoteInput)
-                        .build();
-                builder.addAction(action);
-                break;
-            default:
-                Log.e(TAG, "Unknown notification type: " + notificationType);
-                return;
-        }
-
-        final Notification notification = builder.build();
-        ((NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE))
-            .notify(notificationId, notification);
-    }
-
-    private void showToast(Context context) {
-        Toast.makeText(context, "Toast from CTS test", Toast.LENGTH_SHORT).show();
-        setResultData("Shown");
-    }
-}
diff --git a/tests/cts/hostside-network-policy/app2/src/com/android/cts/netpolicy/hostside/app2/MyForegroundService.java b/tests/cts/hostside-network-policy/app2/src/com/android/cts/netpolicy/hostside/app2/MyForegroundService.java
deleted file mode 100644
index 54cee3c..0000000
--- a/tests/cts/hostside-network-policy/app2/src/com/android/cts/netpolicy/hostside/app2/MyForegroundService.java
+++ /dev/null
@@ -1,73 +0,0 @@
-/*
- * Copyright (C) 2016 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.cts.netpolicy.hostside.app2;
-
-import static com.android.cts.netpolicy.hostside.app2.Common.TAG;
-import static com.android.cts.netpolicy.hostside.app2.Common.TEST_PKG;
-import static com.android.cts.netpolicy.hostside.app2.Common.TYPE_COMPONENT_FOREGROUND_SERVICE;
-
-import android.R;
-import android.app.Notification;
-import android.app.NotificationChannel;
-import android.app.NotificationManager;
-import android.app.Service;
-import android.content.Intent;
-import android.os.AsyncTask;
-import android.os.Bundle;
-import android.os.IBinder;
-import android.os.RemoteException;
-import android.util.Log;
-
-import com.android.cts.netpolicy.hostside.INetworkStateObserver;
-
-/**
- * Service used to change app state to FOREGROUND_SERVICE.
- */
-public class MyForegroundService extends Service {
-    private static final String NOTIFICATION_CHANNEL_ID = "cts/MyForegroundService";
-    private static final int FLAG_START_FOREGROUND = 1;
-    private static final int FLAG_STOP_FOREGROUND = 2;
-
-    @Override
-    public IBinder onBind(Intent intent) {
-        return null;
-    }
-
-    @Override
-    public int onStartCommand(Intent intent, int flags, int startId) {
-        Log.v(TAG, "MyForegroundService.onStartCommand(): " + intent);
-        NotificationManager notificationManager = getSystemService(NotificationManager.class);
-        notificationManager.createNotificationChannel(new NotificationChannel(
-                NOTIFICATION_CHANNEL_ID, NOTIFICATION_CHANNEL_ID,
-                NotificationManager.IMPORTANCE_DEFAULT));
-        switch (intent.getFlags()) {
-            case FLAG_START_FOREGROUND:
-                Log.d(TAG, "Starting foreground");
-                startForeground(42, new Notification.Builder(this, NOTIFICATION_CHANNEL_ID)
-                        .setSmallIcon(R.drawable.ic_dialog_alert) // any icon is fine
-                        .build());
-                Common.notifyNetworkStateObserver(this, intent, TYPE_COMPONENT_FOREGROUND_SERVICE);
-                break;
-            case FLAG_STOP_FOREGROUND:
-                Log.d(TAG, "Stopping foreground");
-                stopForeground(true);
-                break;
-            default:
-                Log.wtf(TAG, "Invalid flag on intent " + intent);
-        }
-        return START_STICKY;
-    }
-}
diff --git a/tests/cts/hostside-network-policy/app2/src/com/android/cts/netpolicy/hostside/app2/MyJobService.java b/tests/cts/hostside-network-policy/app2/src/com/android/cts/netpolicy/hostside/app2/MyJobService.java
deleted file mode 100644
index eba55ed..0000000
--- a/tests/cts/hostside-network-policy/app2/src/com/android/cts/netpolicy/hostside/app2/MyJobService.java
+++ /dev/null
@@ -1,82 +0,0 @@
-/*
- * Copyright (C) 2021 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.cts.netpolicy.hostside.app2;
-
-import static com.android.cts.netpolicy.hostside.app2.Common.ACTION_FINISH_JOB;
-import static com.android.cts.netpolicy.hostside.app2.Common.TAG;
-import static com.android.cts.netpolicy.hostside.app2.Common.TYPE_COMPONENT_EXPEDITED_JOB;
-
-import android.app.job.JobParameters;
-import android.app.job.JobService;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.util.Log;
-
-public class MyJobService extends JobService {
-
-    private BroadcastReceiver mFinishCommandReceiver = null;
-
-    @Override
-    public void onCreate() {
-        super.onCreate();
-        Log.v(TAG, "MyJobService.onCreate()");
-    }
-
-    @Override
-    public boolean onStartJob(JobParameters params) {
-        Log.v(TAG, "MyJobService.onStartJob()");
-        Common.notifyNetworkStateObserver(this, params.getTransientExtras(),
-                TYPE_COMPONENT_EXPEDITED_JOB);
-        mFinishCommandReceiver = new BroadcastReceiver() {
-            @Override
-            public void onReceive(Context context, Intent intent) {
-                Log.v(TAG, "Finishing MyJobService");
-                try {
-                    jobFinished(params, /*wantsReschedule=*/ false);
-                } finally {
-                    if (mFinishCommandReceiver != null) {
-                        unregisterReceiver(mFinishCommandReceiver);
-                        mFinishCommandReceiver = null;
-                    }
-                }
-            }
-        };
-        registerReceiver(mFinishCommandReceiver, new IntentFilter(ACTION_FINISH_JOB),
-                Context.RECEIVER_EXPORTED);
-        return true;
-    }
-
-    @Override
-    public boolean onStopJob(JobParameters params) {
-        // If this job is stopped before it had a chance to send network status via
-        // INetworkStateObserver, the test will fail. It could happen either due to test timing out
-        // or this app moving to a lower proc_state and losing network access.
-        Log.v(TAG, "MyJobService.onStopJob()");
-        if (mFinishCommandReceiver != null) {
-            unregisterReceiver(mFinishCommandReceiver);
-            mFinishCommandReceiver = null;
-        }
-        return false;
-    }
-
-    @Override
-    public void onDestroy() {
-        super.onDestroy();
-        Log.v(TAG, "MyJobService.onDestroy()");
-    }
-}
diff --git a/tests/cts/hostside-network-policy/app2/src/com/android/cts/netpolicy/hostside/app2/MyService.java b/tests/cts/hostside-network-policy/app2/src/com/android/cts/netpolicy/hostside/app2/MyService.java
deleted file mode 100644
index 71bcead..0000000
--- a/tests/cts/hostside-network-policy/app2/src/com/android/cts/netpolicy/hostside/app2/MyService.java
+++ /dev/null
@@ -1,200 +0,0 @@
-/*
- * Copyright (C) 2016 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.cts.netpolicy.hostside.app2;
-
-import static android.net.ConnectivityManager.ACTION_RESTRICT_BACKGROUND_CHANGED;
-
-import static com.android.cts.netpolicy.hostside.app2.Common.ACTION_RECEIVER_READY;
-import static com.android.cts.netpolicy.hostside.app2.Common.ACTION_SNOOZE_WARNING;
-import static com.android.cts.netpolicy.hostside.app2.Common.DYNAMIC_RECEIVER;
-import static com.android.cts.netpolicy.hostside.app2.Common.TAG;
-
-import android.app.NotificationChannel;
-import android.app.NotificationManager;
-import android.app.Service;
-import android.app.job.JobInfo;
-import android.app.job.JobScheduler;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.net.ConnectivityManager;
-import android.net.Network;
-import android.net.NetworkCapabilities;
-import android.net.NetworkRequest;
-import android.os.IBinder;
-import android.os.RemoteException;
-import android.util.Log;
-
-import com.android.cts.netpolicy.hostside.IMyService;
-import com.android.cts.netpolicy.hostside.INetworkCallback;
-import com.android.cts.netpolicy.hostside.NetworkCheckResult;
-import com.android.modules.utils.build.SdkLevel;
-
-/**
- * Service used to dynamically register a broadcast receiver.
- */
-public class MyService extends Service {
-    private static final String NOTIFICATION_CHANNEL_ID = "MyService";
-
-    ConnectivityManager mCm;
-
-    private MyBroadcastReceiver mReceiver;
-    private ConnectivityManager.NetworkCallback mNetworkCallback;
-
-    // TODO: move MyBroadcast static functions here - they were kept there to make git diff easier.
-
-    private IMyService.Stub mBinder = new IMyService.Stub() {
-        @Override
-        public void registerBroadcastReceiver() {
-            if (mReceiver != null) {
-                Log.d(TAG, "receiver already registered: " + mReceiver);
-                return;
-            }
-            final Context context = getApplicationContext();
-            final int flags = SdkLevel.isAtLeastT() ? RECEIVER_EXPORTED : 0;
-            mReceiver = new MyBroadcastReceiver(DYNAMIC_RECEIVER);
-            context.registerReceiver(mReceiver,
-                    new IntentFilter(ACTION_RECEIVER_READY), flags);
-            context.registerReceiver(mReceiver,
-                    new IntentFilter(ACTION_RESTRICT_BACKGROUND_CHANGED), flags);
-            context.registerReceiver(mReceiver,
-                    new IntentFilter(ACTION_SNOOZE_WARNING), flags);
-            Log.d(TAG, "receiver registered");
-        }
-
-        @Override
-        public int getCounters(String receiverName, String action) {
-            return MyBroadcastReceiver.getCounter(getApplicationContext(), action, receiverName);
-        }
-
-        @Override
-        public NetworkCheckResult checkNetworkStatus(String customUrl) {
-            return Common.checkNetworkStatus(getApplicationContext(), customUrl);
-        }
-
-        @Override
-        public String getRestrictBackgroundStatus() {
-            return MyBroadcastReceiver.getRestrictBackgroundStatus(getApplicationContext());
-        }
-
-        @Override
-        public void sendNotification(int notificationId, String notificationType) {
-            MyBroadcastReceiver.sendNotification(getApplicationContext(), NOTIFICATION_CHANNEL_ID,
-                    notificationId, notificationType);
-        }
-
-        @Override
-        public void registerNetworkCallback(final NetworkRequest request, INetworkCallback cb) {
-            if (mNetworkCallback != null) {
-                Log.d(TAG, "unregister previous network callback: " + mNetworkCallback);
-                unregisterNetworkCallback();
-            }
-            Log.d(TAG, "registering network callback for " + request);
-
-            mNetworkCallback = new ConnectivityManager.NetworkCallback() {
-                @Override
-                public void onBlockedStatusChanged(Network network, boolean blocked) {
-                    try {
-                        cb.onBlockedStatusChanged(network, blocked);
-                    } catch (RemoteException e) {
-                        Log.d(TAG, "Cannot send onBlockedStatusChanged: " + e);
-                        unregisterNetworkCallback();
-                    }
-                }
-
-                @Override
-                public void onAvailable(Network network) {
-                    try {
-                        cb.onAvailable(network);
-                    } catch (RemoteException e) {
-                        Log.d(TAG, "Cannot send onAvailable: " + e);
-                        unregisterNetworkCallback();
-                    }
-                }
-
-                @Override
-                public void onLost(Network network) {
-                    try {
-                        cb.onLost(network);
-                    } catch (RemoteException e) {
-                        Log.d(TAG, "Cannot send onLost: " + e);
-                        unregisterNetworkCallback();
-                    }
-                }
-
-                @Override
-                public void onCapabilitiesChanged(Network network, NetworkCapabilities cap) {
-                    try {
-                        cb.onCapabilitiesChanged(network, cap);
-                    } catch (RemoteException e) {
-                        Log.d(TAG, "Cannot send onCapabilitiesChanged: " + e);
-                        unregisterNetworkCallback();
-                    }
-                }
-            };
-            mCm.registerNetworkCallback(request, mNetworkCallback);
-            try {
-                cb.asBinder().linkToDeath(() -> unregisterNetworkCallback(), 0);
-            } catch (RemoteException e) {
-                unregisterNetworkCallback();
-            }
-        }
-
-        @Override
-        public void unregisterNetworkCallback() {
-            Log.d(TAG, "unregistering network callback");
-            if (mNetworkCallback != null) {
-                mCm.unregisterNetworkCallback(mNetworkCallback);
-                mNetworkCallback = null;
-            }
-        }
-
-        @Override
-        public int scheduleJob(JobInfo jobInfo) {
-            final JobScheduler jobScheduler = getApplicationContext()
-                    .getSystemService(JobScheduler.class);
-            return jobScheduler.schedule(jobInfo);
-        }
-    };
-
-    @Override
-    public IBinder onBind(Intent intent) {
-        return mBinder;
-    }
-
-    @Override
-    public void onCreate() {
-        final Context context = getApplicationContext();
-        ((NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE))
-                .createNotificationChannel(new NotificationChannel(NOTIFICATION_CHANNEL_ID,
-                        NOTIFICATION_CHANNEL_ID, NotificationManager.IMPORTANCE_DEFAULT));
-        mCm = (ConnectivityManager) getApplicationContext()
-                .getSystemService(Context.CONNECTIVITY_SERVICE);
-    }
-
-    @Override
-    public void onDestroy() {
-        final Context context = getApplicationContext();
-        ((NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE))
-                .deleteNotificationChannel(NOTIFICATION_CHANNEL_ID);
-        if (mReceiver != null) {
-            Log.d(TAG, "onDestroy(): unregistering " + mReceiver);
-            getApplicationContext().unregisterReceiver(mReceiver);
-        }
-
-        super.onDestroy();
-    }
-}
diff --git a/tests/cts/hostside-network-policy/certs/Android.bp b/tests/cts/hostside-network-policy/certs/Android.bp
deleted file mode 100644
index bfbc341..0000000
--- a/tests/cts/hostside-network-policy/certs/Android.bp
+++ /dev/null
@@ -1,9 +0,0 @@
-package {
-    default_team: "trendy_team_framework_backstage_power",
-    default_applicable_licenses: ["Android-Apache-2.0"],
-}
-
-android_app_certificate {
-    name: "cts-netpolicy-app",
-    certificate: "cts-net-app",
-}
diff --git a/tests/cts/hostside-network-policy/certs/README b/tests/cts/hostside-network-policy/certs/README
deleted file mode 100644
index b660a82..0000000
--- a/tests/cts/hostside-network-policy/certs/README
+++ /dev/null
@@ -1,2 +0,0 @@
-# Generated with:
-development/tools/make_key cts-net-app '/CN=cts-net-app'
diff --git a/tests/cts/hostside-network-policy/certs/cts-net-app.pk8 b/tests/cts/hostside-network-policy/certs/cts-net-app.pk8
deleted file mode 100644
index 1703e4e..0000000
--- a/tests/cts/hostside-network-policy/certs/cts-net-app.pk8
+++ /dev/null
Binary files differ
diff --git a/tests/cts/hostside-network-policy/certs/cts-net-app.x509.pem b/tests/cts/hostside-network-policy/certs/cts-net-app.x509.pem
deleted file mode 100644
index a15ff48..0000000
--- a/tests/cts/hostside-network-policy/certs/cts-net-app.x509.pem
+++ /dev/null
@@ -1,19 +0,0 @@
------BEGIN CERTIFICATE-----
-MIIDAjCCAeqgAwIBAgIJAMhWwIIqr1r6MA0GCSqGSIb3DQEBCwUAMBYxFDASBgNV
-BAMMC2N0cy1uZXQtYXBwMB4XDTE4MDYyMDAyMjAwN1oXDTQ1MTEwNTAyMjAwN1ow
-FjEUMBIGA1UEAwwLY3RzLW5ldC1hcHAwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw
-ggEKAoIBAQDefOayWQss1E+FQIONK6IhlXhe0BEyHshIrnPOOmuCPa/Svfbnmziy
-hr1KTjaQ3ET/mGShwlt6AUti7nKx9aB71IJp5mSBuwW62A8jvN3yNOo45YV8+n1o
-TrEoMWMf7hQmoOSqaSJ+VFuVms/kPSEh99okDgHCej6rsEkEcDoh6pJajQyUYDwR
-SNAF8SrqCDhqFbZW/LWedvuikCUlNtzuv7/GrcLcsiWEfHv7UOBKpMjLo9BhD1XF
-IefnxImcBQrQGMnE9TLixBiEeX5yauLgbZuxBqD/zsI2TH1FjxTeuJan83kLbqqH
-FgyvPaUjwckAdQPyom7ZUYFnBc0LQ9xzAgMBAAGjUzBRMB0GA1UdDgQWBBRZrBEw
-tAB2WNXj8dQ7ZOuJ34kY5DAfBgNVHSMEGDAWgBRZrBEwtAB2WNXj8dQ7ZOuJ34kY
-5DAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQDeI9AnLW6l/39y
-z96w/ldxZVFPzBRiFIsJsPHVyXlD5vUHZv/ju2jFn8TZSZR5TK0bzCEoVLp34Sho
-bbS0magP82yIvCRibyoyD+TDNnZkNJwjYnikE+/oyshTSQtpkn/rDA+0Y09BUC1E
-N2I6bV9pTXLFg7oah2FmqPRPzhgeYUKENgOQkrrjUCn6y0i/k374n7aftzdniSIz
-2kCRVEeN9gws6CnoMPx0vr32v/JVuPV6zfdJYadgj/eFRyTNE4msd9kE82Wc46eU
-YiI+LuXZ3ZMUNWGY7MK2pOUUS52JsBQ3K235dA5WaU4x8OBlY/WkNYX/eLbNs5jj
-FzLmhZZ1
------END CERTIFICATE-----
diff --git a/tests/cts/hostside-network-policy/instrumentation_arguments/Android.bp b/tests/cts/hostside-network-policy/instrumentation_arguments/Android.bp
deleted file mode 100644
index cdede36..0000000
--- a/tests/cts/hostside-network-policy/instrumentation_arguments/Android.bp
+++ /dev/null
@@ -1,22 +0,0 @@
-// Copyright (C) 2024 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package {
-    default_applicable_licenses: ["Android-Apache-2.0"],
-}
-
-filegroup {
-    name: "ArgumentConstants",
-    srcs: ["src/**/*.java"],
-}
diff --git a/tests/cts/hostside-network-policy/instrumentation_arguments/src/com/android/cts/netpolicy/arguments/InstrumentationArguments.java b/tests/cts/hostside-network-policy/instrumentation_arguments/src/com/android/cts/netpolicy/arguments/InstrumentationArguments.java
deleted file mode 100644
index 0fe98e9..0000000
--- a/tests/cts/hostside-network-policy/instrumentation_arguments/src/com/android/cts/netpolicy/arguments/InstrumentationArguments.java
+++ /dev/null
@@ -1,22 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.cts.netpolicy.arguments;
-
-public interface InstrumentationArguments {
-    String ARG_WAIVE_BIND_PRIORITY = "waive_bind_priority";
-    String ARG_CONNECTION_CHECK_CUSTOM_URL = "connection_check_custom_url";
-}
diff --git a/tests/cts/hostside-network-policy/src/com/android/cts/netpolicy/HostsideConnOnActivityStartTest.java b/tests/cts/hostside-network-policy/src/com/android/cts/netpolicy/HostsideConnOnActivityStartTest.java
deleted file mode 100644
index 422231d..0000000
--- a/tests/cts/hostside-network-policy/src/com/android/cts/netpolicy/HostsideConnOnActivityStartTest.java
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- * Copyright (C) 2022 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.cts.netpolicy;
-
-import static com.android.cts.netpolicy.arguments.InstrumentationArguments.ARG_WAIVE_BIND_PRIORITY;
-
-import android.platform.test.annotations.FlakyTest;
-
-import com.android.tradefed.device.DeviceNotAvailableException;
-import com.android.tradefed.invoker.TestInformation;
-import com.android.tradefed.testtype.junit4.AfterClassWithInfo;
-import com.android.tradefed.testtype.junit4.BeforeClassWithInfo;
-
-import org.junit.Test;
-
-import java.util.Map;
-
-@FlakyTest(bugId = 288324467)
-public class HostsideConnOnActivityStartTest extends HostsideNetworkPolicyTestCase {
-    private static final String TEST_CLASS = TEST_PKG + ".ConnOnActivityStartTest";
-
-    @BeforeClassWithInfo
-    public static void setUpOnce(TestInformation testInfo) throws Exception {
-        uninstallPackage(testInfo, TEST_APP2_PKG, false);
-        installPackage(testInfo, TEST_APP2_APK);
-    }
-
-    @AfterClassWithInfo
-    public static void tearDownOnce(TestInformation testInfo) throws DeviceNotAvailableException {
-        uninstallPackage(testInfo, TEST_APP2_PKG, true);
-    }
-
-    @Test
-    public void testStartActivity_batterySaver() throws Exception {
-        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_CLASS, "testStartActivity_batterySaver");
-    }
-
-    @Test
-    public void testStartActivity_dataSaver() throws Exception {
-        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_CLASS, "testStartActivity_dataSaver");
-    }
-
-    @FlakyTest(bugId = 231440256)
-    @Test
-    public void testStartActivity_doze() throws Exception {
-        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_CLASS, "testStartActivity_doze");
-    }
-
-    @Test
-    public void testStartActivity_appStandby() throws Exception {
-        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_CLASS, "testStartActivity_appStandby");
-    }
-
-    // TODO(b/321848487): Annotate with @RequiresFlagsEnabled to mirror the device-side test.
-    @Test
-    public void testStartActivity_default() throws Exception {
-        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_CLASS, "testStartActivity_default",
-                Map.of(ARG_WAIVE_BIND_PRIORITY, "true"));
-    }
-}
diff --git a/tests/cts/hostside-network-policy/src/com/android/cts/netpolicy/HostsideDefaultNetworkRestrictionsTests.java b/tests/cts/hostside-network-policy/src/com/android/cts/netpolicy/HostsideDefaultNetworkRestrictionsTests.java
deleted file mode 100644
index 62952bb..0000000
--- a/tests/cts/hostside-network-policy/src/com/android/cts/netpolicy/HostsideDefaultNetworkRestrictionsTests.java
+++ /dev/null
@@ -1,104 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.cts.netpolicy;
-
-import static com.android.cts.netpolicy.arguments.InstrumentationArguments.ARG_WAIVE_BIND_PRIORITY;
-
-import android.platform.test.annotations.FlakyTest;
-
-import com.android.tradefed.device.DeviceNotAvailableException;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-
-import java.util.Map;
-
-// TODO(b/321848487): Annotate with @RequiresFlagsEnabled to mirror the device-side tests.
-@FlakyTest(bugId = 288324467)
-public class HostsideDefaultNetworkRestrictionsTests extends HostsideNetworkPolicyTestCase {
-    private static final String METERED_TEST_CLASS = TEST_PKG + ".DefaultRestrictionsMeteredTest";
-    private static final String NON_METERED_TEST_CLASS =
-            TEST_PKG + ".DefaultRestrictionsNonMeteredTest";
-
-    @Before
-    public void setUp() throws Exception {
-        uninstallPackage(TEST_APP2_PKG, false);
-        installPackage(TEST_APP2_APK);
-    }
-
-    @After
-    public void tearDown() throws Exception {
-        uninstallPackage(TEST_APP2_PKG, true);
-    }
-
-    private void runMeteredTest(String methodName) throws DeviceNotAvailableException {
-        runDeviceTestsWithCustomOptions(TEST_PKG, METERED_TEST_CLASS, methodName,
-                Map.of(ARG_WAIVE_BIND_PRIORITY, "true"));
-    }
-
-    private void runNonMeteredTest(String methodName) throws DeviceNotAvailableException {
-        runDeviceTestsWithCustomOptions(TEST_PKG, NON_METERED_TEST_CLASS, methodName,
-                Map.of(ARG_WAIVE_BIND_PRIORITY, "true"));
-    }
-
-    @Test
-    public void testMeteredNetworkAccess_defaultRestrictions_testActivityNetworkAccess()
-            throws Exception {
-        runMeteredTest("testActivityNetworkAccess");
-    }
-
-    @Test
-    public void testMeteredNetworkAccess_defaultRestrictions_testFgsNetworkAccess()
-            throws Exception {
-        runMeteredTest("testFgsNetworkAccess");
-    }
-
-    @Test
-    public void testMeteredNetworkAccess_defaultRestrictions_inFullAllowlist() throws Exception {
-        runMeteredTest("testBackgroundNetworkAccess_inFullAllowlist");
-    }
-
-    @Test
-    public void testMeteredNetworkAccess_defaultRestrictions_inExceptIdleAllowlist()
-            throws Exception {
-        runMeteredTest("testBackgroundNetworkAccess_inExceptIdleAllowlist");
-    }
-
-    @Test
-    public void testNonMeteredNetworkAccess_defaultRestrictions_testActivityNetworkAccess()
-            throws Exception {
-        runNonMeteredTest("testActivityNetworkAccess");
-    }
-
-    @Test
-    public void testNonMeteredNetworkAccess_defaultRestrictions_testFgsNetworkAccess()
-            throws Exception {
-        runNonMeteredTest("testFgsNetworkAccess");
-    }
-
-    @Test
-    public void testNonMeteredNetworkAccess_defaultRestrictions_inFullAllowlist() throws Exception {
-        runNonMeteredTest("testBackgroundNetworkAccess_inFullAllowlist");
-    }
-
-    @Test
-    public void testNonMeteredNetworkAccess_defaultRestrictions_inExceptIdleAllowlist()
-            throws Exception {
-        runNonMeteredTest("testBackgroundNetworkAccess_inExceptIdleAllowlist");
-    }
-}
diff --git a/tests/cts/hostside-network-policy/src/com/android/cts/netpolicy/HostsideNetworkCallbackTests.java b/tests/cts/hostside-network-policy/src/com/android/cts/netpolicy/HostsideNetworkCallbackTests.java
deleted file mode 100644
index 2c2b118..0000000
--- a/tests/cts/hostside-network-policy/src/com/android/cts/netpolicy/HostsideNetworkCallbackTests.java
+++ /dev/null
@@ -1,61 +0,0 @@
-/*
- * Copyright (C) 2019 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.cts.netpolicy;
-
-import static com.android.cts.netpolicy.arguments.InstrumentationArguments.ARG_WAIVE_BIND_PRIORITY;
-
-import android.platform.test.annotations.FlakyTest;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-
-import java.util.Map;
-
-@FlakyTest(bugId = 288324467)
-public class HostsideNetworkCallbackTests extends HostsideNetworkPolicyTestCase {
-
-    @Before
-    public void setUp() throws Exception {
-        uninstallPackage(TEST_APP2_PKG, false);
-        installPackage(TEST_APP2_APK);
-    }
-
-    @After
-    public void tearDown() throws Exception {
-        uninstallPackage(TEST_APP2_PKG, true);
-    }
-
-    @Test
-    public void testOnBlockedStatusChanged_dataSaver() throws Exception {
-        runDeviceTestsWithCustomOptions(TEST_PKG,
-                TEST_PKG + ".NetworkCallbackTest", "testOnBlockedStatusChanged_dataSaver");
-    }
-
-    @Test
-    public void testOnBlockedStatusChanged_powerSaver() throws Exception {
-        runDeviceTestsWithCustomOptions(TEST_PKG,
-                TEST_PKG + ".NetworkCallbackTest", "testOnBlockedStatusChanged_powerSaver");
-    }
-
-    // TODO(b/321848487): Annotate with @RequiresFlagsEnabled to mirror the device-side test.
-    @Test
-    public void testOnBlockedStatusChanged_default() throws Exception {
-        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".NetworkCallbackTest",
-                "testOnBlockedStatusChanged_default", Map.of(ARG_WAIVE_BIND_PRIORITY, "true"));
-    }
-}
-
diff --git a/tests/cts/hostside-network-policy/src/com/android/cts/netpolicy/HostsideNetworkPolicyManagerTests.java b/tests/cts/hostside-network-policy/src/com/android/cts/netpolicy/HostsideNetworkPolicyManagerTests.java
deleted file mode 100644
index 8ffe360..0000000
--- a/tests/cts/hostside-network-policy/src/com/android/cts/netpolicy/HostsideNetworkPolicyManagerTests.java
+++ /dev/null
@@ -1,86 +0,0 @@
-/*
- * Copyright (C) 2021 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.cts.netpolicy;
-
-import static com.android.cts.netpolicy.arguments.InstrumentationArguments.ARG_WAIVE_BIND_PRIORITY;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-
-import java.util.Map;
-
-public class HostsideNetworkPolicyManagerTests extends HostsideNetworkPolicyTestCase {
-    @Before
-    public void setUp() throws Exception {
-        uninstallPackage(TEST_APP2_PKG, false);
-        installPackage(TEST_APP2_APK);
-    }
-
-    @After
-    public void tearDown() throws Exception {
-        uninstallPackage(TEST_APP2_PKG, true);
-    }
-
-    @Test
-    public void testIsUidNetworkingBlocked_withUidNotBlocked() throws Exception {
-        runDeviceTestsWithCustomOptions(TEST_PKG,
-                TEST_PKG + ".NetworkPolicyManagerTest",
-                "testIsUidNetworkingBlocked_withUidNotBlocked");
-    }
-
-    @Test
-    public void testIsUidNetworkingBlocked_withSystemUid() throws Exception {
-        runDeviceTestsWithCustomOptions(TEST_PKG,
-                TEST_PKG + ".NetworkPolicyManagerTest", "testIsUidNetworkingBlocked_withSystemUid");
-    }
-
-    @Test
-    public void testIsUidNetworkingBlocked_withDataSaverMode() throws Exception {
-        runDeviceTestsWithCustomOptions(TEST_PKG,
-                TEST_PKG + ".NetworkPolicyManagerTest",
-                "testIsUidNetworkingBlocked_withDataSaverMode");
-    }
-
-    @Test
-    public void testIsUidNetworkingBlocked_withRestrictedNetworkingMode() throws Exception {
-        runDeviceTestsWithCustomOptions(TEST_PKG,
-                TEST_PKG + ".NetworkPolicyManagerTest",
-                "testIsUidNetworkingBlocked_withRestrictedNetworkingMode");
-    }
-
-    @Test
-    public void testIsUidNetworkingBlocked_withPowerSaverMode() throws Exception {
-        runDeviceTestsWithCustomOptions(TEST_PKG,
-                TEST_PKG + ".NetworkPolicyManagerTest",
-                "testIsUidNetworkingBlocked_withPowerSaverMode");
-    }
-
-    @Test
-    public void testIsUidRestrictedOnMeteredNetworks() throws Exception {
-        runDeviceTestsWithCustomOptions(TEST_PKG,
-                TEST_PKG + ".NetworkPolicyManagerTest", "testIsUidRestrictedOnMeteredNetworks");
-    }
-
-    // TODO(b/321848487): Annotate with @RequiresFlagsEnabled to mirror the device-side test.
-    @Test
-    public void testIsUidNetworkingBlocked_whenInBackground() throws Exception {
-        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".NetworkPolicyManagerTest",
-                "testIsUidNetworkingBlocked_whenInBackground",
-                Map.of(ARG_WAIVE_BIND_PRIORITY, "true"));
-    }
-}
diff --git a/tests/cts/hostside-network-policy/src/com/android/cts/netpolicy/HostsideNetworkPolicyTestCase.java b/tests/cts/hostside-network-policy/src/com/android/cts/netpolicy/HostsideNetworkPolicyTestCase.java
deleted file mode 100644
index 6de6b17..0000000
--- a/tests/cts/hostside-network-policy/src/com/android/cts/netpolicy/HostsideNetworkPolicyTestCase.java
+++ /dev/null
@@ -1,187 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.cts.netpolicy;
-
-import static com.android.cts.netpolicy.arguments.InstrumentationArguments.ARG_CONNECTION_CHECK_CUSTOM_URL;
-
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.fail;
-
-import com.android.ddmlib.Log;
-import com.android.tradefed.config.Option;
-import com.android.tradefed.device.DeviceNotAvailableException;
-import com.android.tradefed.invoker.TestInformation;
-import com.android.tradefed.targetprep.BuildError;
-import com.android.tradefed.targetprep.TargetSetupError;
-import com.android.tradefed.targetprep.suite.SuiteApkInstaller;
-import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
-import com.android.tradefed.testtype.junit4.AfterClassWithInfo;
-import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
-import com.android.tradefed.testtype.junit4.BeforeClassWithInfo;
-import com.android.tradefed.testtype.junit4.DeviceTestRunOptions;
-import com.android.tradefed.util.RunUtil;
-
-import org.junit.runner.RunWith;
-
-import java.util.Map;
-
-@RunWith(DeviceJUnit4ClassRunner.class)
-abstract class HostsideNetworkPolicyTestCase extends BaseHostJUnit4Test {
-    protected static final boolean DEBUG = false;
-    protected static final String TAG = "HostsideNetworkPolicyTests";
-    protected static final String TEST_PKG = "com.android.cts.netpolicy.hostside";
-    protected static final String TEST_APK = "CtsHostsideNetworkPolicyTestsApp.apk";
-    protected static final String TEST_APP2_PKG = "com.android.cts.netpolicy.hostside.app2";
-    protected static final String TEST_APP2_APK = "CtsHostsideNetworkPolicyTestsApp2.apk";
-
-    @Option(name = "custom-url", importance = Option.Importance.IF_UNSET,
-            description = "A custom url to use for testing network connections")
-    protected String mCustomUrl;
-
-    @BeforeClassWithInfo
-    public static void setUpOnceBase(TestInformation testInfo) throws Exception {
-        uninstallPackage(testInfo, TEST_PKG, false);
-        installPackage(testInfo, TEST_APK);
-    }
-
-    @AfterClassWithInfo
-    public static void tearDownOnceBase(TestInformation testInfo)
-            throws DeviceNotAvailableException {
-        uninstallPackage(testInfo, TEST_PKG, true);
-    }
-
-    // Custom static method to install the specified package, this is used to bypass auto-cleanup
-    // per test in BaseHostJUnit4.
-    protected static void installPackage(TestInformation testInfo, String apk)
-            throws DeviceNotAvailableException, TargetSetupError {
-        assertNotNull(testInfo);
-        final int userId = testInfo.getDevice().getCurrentUser();
-        final SuiteApkInstaller installer = new SuiteApkInstaller();
-        // Force the apk clean up
-        installer.setCleanApk(true);
-        installer.addTestFileName(apk);
-        installer.setUserId(userId);
-        installer.setShouldGrantPermission(true);
-        installer.addInstallArg("-t");
-        try {
-            installer.setUp(testInfo);
-        } catch (BuildError e) {
-            throw new TargetSetupError(
-                    e.getMessage(), e, testInfo.getDevice().getDeviceDescriptor(), e.getErrorId());
-        }
-    }
-
-    protected void installPackage(String apk) throws DeviceNotAvailableException, TargetSetupError {
-        installPackage(getTestInformation(), apk);
-    }
-
-    protected static void uninstallPackage(TestInformation testInfo, String packageName,
-            boolean shouldSucceed)
-            throws DeviceNotAvailableException {
-        assertNotNull(testInfo);
-        final String result = testInfo.getDevice().uninstallPackage(packageName);
-        if (shouldSucceed) {
-            assertNull("uninstallPackage(" + packageName + ") failed: " + result, result);
-        }
-    }
-
-    protected void uninstallPackage(String packageName,
-            boolean shouldSucceed)
-            throws DeviceNotAvailableException {
-        uninstallPackage(getTestInformation(), packageName, shouldSucceed);
-    }
-
-    protected void assertPackageUninstalled(String packageName) throws DeviceNotAvailableException {
-        final String command = "cmd package list packages " + packageName;
-        final int max_tries = 5;
-        for (int i = 1; i <= max_tries; i++) {
-            final String result = runCommand(command);
-            if (result.trim().isEmpty()) {
-                return;
-            }
-            // 'list packages' filters by substring, so we need to iterate with the results
-            // and check one by one, otherwise 'com.android.cts.netpolicy.hostside' could return
-            // 'com.android.cts.netpolicy.hostside.app2'
-            boolean found = false;
-            for (String line : result.split("[\\r\\n]+")) {
-                if (line.endsWith(packageName)) {
-                    found = true;
-                    break;
-                }
-            }
-            if (!found) {
-                return;
-            }
-            Log.v(TAG, "Package " + packageName + " not uninstalled yet (" + result
-                    + "); sleeping 1s before polling again");
-            RunUtil.getDefault().sleep(1000);
-        }
-        fail("Package '" + packageName + "' not uinstalled after " + max_tries + " seconds");
-    }
-
-    protected int getUid(String packageName) throws DeviceNotAvailableException {
-        final int currentUser = getDevice().getCurrentUser();
-        final String uidLines = runCommand(
-                "cmd package list packages -U --user " + currentUser + " " + packageName);
-        for (String uidLine : uidLines.split("\n")) {
-            if (uidLine.startsWith("package:" + packageName + " uid:")) {
-                final String[] uidLineParts = uidLine.split(":");
-                // 3rd entry is package uid
-                return Integer.parseInt(uidLineParts[2].trim());
-            }
-        }
-        throw new IllegalStateException("Failed to find the test app on the device; pkg="
-                + packageName + ", u=" + currentUser);
-    }
-
-    protected boolean runDeviceTestsWithCustomOptions(String packageName, String className)
-            throws DeviceNotAvailableException {
-        return runDeviceTestsWithCustomOptions(packageName, className, null);
-    }
-
-    protected boolean runDeviceTestsWithCustomOptions(String packageName, String className,
-            String methodName) throws DeviceNotAvailableException {
-        return runDeviceTestsWithCustomOptions(packageName, className, methodName, null);
-    }
-
-    protected boolean runDeviceTestsWithCustomOptions(String packageName, String className,
-            String methodName, Map<String, String> testArgs) throws DeviceNotAvailableException {
-        final DeviceTestRunOptions deviceTestRunOptions = new DeviceTestRunOptions(packageName)
-                .setTestClassName(className)
-                .setTestMethodName(methodName);
-
-        // Currently there is only one custom option that the test exposes.
-        if (mCustomUrl != null) {
-            deviceTestRunOptions.addInstrumentationArg(ARG_CONNECTION_CHECK_CUSTOM_URL, mCustomUrl);
-        }
-        // Pass over any test specific arguments.
-        if (testArgs != null) {
-            for (Map.Entry<String, String> arg : testArgs.entrySet()) {
-                deviceTestRunOptions.addInstrumentationArg(arg.getKey(), arg.getValue());
-            }
-        }
-        return runDeviceTests(deviceTestRunOptions);
-    }
-
-    protected String runCommand(String command) throws DeviceNotAvailableException {
-        Log.d(TAG, "Command: '" + command + "'");
-        final String output = getDevice().executeShellCommand(command);
-        if (DEBUG) Log.v(TAG, "Output: " + output.trim());
-        return output;
-    }
-}
diff --git a/tests/cts/hostside-network-policy/src/com/android/cts/netpolicy/HostsideRestrictBackgroundNetworkTests.java b/tests/cts/hostside-network-policy/src/com/android/cts/netpolicy/HostsideRestrictBackgroundNetworkTests.java
deleted file mode 100644
index 0261c7d..0000000
--- a/tests/cts/hostside-network-policy/src/com/android/cts/netpolicy/HostsideRestrictBackgroundNetworkTests.java
+++ /dev/null
@@ -1,466 +0,0 @@
-/*
- * Copyright (C) 2016 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.cts.netpolicy;
-
-import static org.junit.Assert.fail;
-
-import android.platform.test.annotations.FlakyTest;
-import android.platform.test.annotations.SecurityTest;
-
-import com.android.ddmlib.Log;
-import com.android.tradefed.device.DeviceNotAvailableException;
-import com.android.tradefed.util.RunUtil;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-
-@FlakyTest(bugId = 288324467)
-public class HostsideRestrictBackgroundNetworkTests extends HostsideNetworkPolicyTestCase {
-
-    @Before
-    public void setUp() throws Exception {
-        uninstallPackage(TEST_APP2_PKG, false);
-        installPackage(TEST_APP2_APK);
-    }
-
-    @After
-    public void tearDown() throws Exception {
-        uninstallPackage(TEST_APP2_PKG, true);
-    }
-
-    @SecurityTest
-    @Test
-    public void testDataWarningReceiver() throws Exception {
-        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".DataWarningReceiverTest",
-                "testSnoozeWarningNotReceived");
-    }
-
-    /**************************
-     * Data Saver Mode tests. *
-     **************************/
-
-    @Test
-    public void testDataSaverMode_disabled() throws Exception {
-        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".DataSaverModeTest",
-                "testGetRestrictBackgroundStatus_disabled");
-    }
-
-    @Test
-    public void testDataSaverMode_whitelisted() throws Exception {
-        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".DataSaverModeTest",
-                "testGetRestrictBackgroundStatus_whitelisted");
-    }
-
-    @Test
-    public void testDataSaverMode_enabled() throws Exception {
-        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".DataSaverModeTest",
-                "testGetRestrictBackgroundStatus_enabled");
-    }
-
-    @Test
-    public void testDataSaverMode_blacklisted() throws Exception {
-        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".DataSaverModeTest",
-                "testGetRestrictBackgroundStatus_blacklisted");
-    }
-
-    @Test
-    public void testDataSaverMode_reinstall() throws Exception {
-        final int oldUid = getUid(TEST_APP2_PKG);
-
-        // Make sure whitelist is revoked when package is removed
-        addRestrictBackgroundWhitelist(oldUid);
-
-        uninstallPackage(TEST_APP2_PKG, true);
-        assertPackageUninstalled(TEST_APP2_PKG);
-        assertRestrictBackgroundWhitelist(oldUid, false);
-
-        installPackage(TEST_APP2_APK);
-        final int newUid = getUid(TEST_APP2_PKG);
-        assertRestrictBackgroundWhitelist(oldUid, false);
-        assertRestrictBackgroundWhitelist(newUid, false);
-    }
-
-    @Test
-    public void testDataSaverMode_requiredWhitelistedPackages() throws Exception {
-        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".DataSaverModeTest",
-                "testGetRestrictBackgroundStatus_requiredWhitelistedPackages");
-    }
-
-    @Test
-    public void testDataSaverMode_broadcastNotSentOnUnsupportedDevices() throws Exception {
-        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".DataSaverModeTest",
-                "testBroadcastNotSentOnUnsupportedDevices");
-    }
-
-    /*****************************
-     * Battery Saver Mode tests. *
-     *****************************/
-
-    @Test
-    public void testBatterySaverModeMetered_disabled() throws Exception {
-        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".BatterySaverModeMeteredTest",
-                "testBackgroundNetworkAccess_disabled");
-    }
-
-    @Test
-    public void testBatterySaverModeMetered_whitelisted() throws Exception {
-        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".BatterySaverModeMeteredTest",
-                "testBackgroundNetworkAccess_whitelisted");
-    }
-
-    @Test
-    public void testBatterySaverModeMetered_enabled() throws Exception {
-        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".BatterySaverModeMeteredTest",
-                "testBackgroundNetworkAccess_enabled");
-    }
-
-    @Test
-    public void testBatterySaverMode_reinstall() throws Exception {
-        if (!isDozeModeEnabled()) {
-            Log.w(TAG, "testBatterySaverMode_reinstall() skipped because device does not support "
-                    + "Doze Mode");
-            return;
-        }
-
-        addPowerSaveModeWhitelist(TEST_APP2_PKG);
-
-        uninstallPackage(TEST_APP2_PKG, true);
-        assertPackageUninstalled(TEST_APP2_PKG);
-        assertPowerSaveModeWhitelist(TEST_APP2_PKG, false);
-
-        installPackage(TEST_APP2_APK);
-        assertPowerSaveModeWhitelist(TEST_APP2_PKG, false);
-    }
-
-    @Test
-    public void testBatterySaverModeNonMetered_disabled() throws Exception {
-        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".BatterySaverModeNonMeteredTest",
-                "testBackgroundNetworkAccess_disabled");
-    }
-
-    @Test
-    public void testBatterySaverModeNonMetered_whitelisted() throws Exception {
-        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".BatterySaverModeNonMeteredTest",
-                "testBackgroundNetworkAccess_whitelisted");
-    }
-
-    @Test
-    public void testBatterySaverModeNonMetered_enabled() throws Exception {
-        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".BatterySaverModeNonMeteredTest",
-                "testBackgroundNetworkAccess_enabled");
-    }
-
-    /*******************
-     * App idle tests. *
-     *******************/
-
-    @Test
-    public void testAppIdleMetered_disabled() throws Exception {
-        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".AppIdleMeteredTest",
-                "testBackgroundNetworkAccess_disabled");
-    }
-
-    @Test
-    public void testAppIdleMetered_whitelisted() throws Exception {
-        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".AppIdleMeteredTest",
-                "testBackgroundNetworkAccess_whitelisted");
-    }
-
-    @Test
-    public void testAppIdleMetered_tempWhitelisted() throws Exception {
-        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".AppIdleMeteredTest",
-                "testBackgroundNetworkAccess_tempWhitelisted");
-    }
-
-    @Test
-    public void testAppIdleMetered_enabled() throws Exception {
-        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".AppIdleMeteredTest",
-                "testBackgroundNetworkAccess_enabled");
-    }
-
-    @Test
-    public void testAppIdleMetered_idleWhitelisted() throws Exception {
-        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".AppIdleMeteredTest",
-                "testAppIdleNetworkAccess_idleWhitelisted");
-    }
-
-    // TODO: currently power-save mode and idle uses the same whitelist, so this test would be
-    // redundant (as it would be testing the same as testBatterySaverMode_reinstall())
-    //    public void testAppIdle_reinstall() throws Exception {
-    //    }
-
-    @Test
-    public void testAppIdleNonMetered_disabled() throws Exception {
-        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".AppIdleNonMeteredTest",
-                "testBackgroundNetworkAccess_disabled");
-    }
-
-
-    @Test
-    public void testAppIdleNonMetered_whitelisted() throws Exception {
-        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".AppIdleNonMeteredTest",
-                "testBackgroundNetworkAccess_whitelisted");
-    }
-
-    @Test
-    public void testAppIdleNonMetered_tempWhitelisted() throws Exception {
-        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".AppIdleNonMeteredTest",
-                "testBackgroundNetworkAccess_tempWhitelisted");
-    }
-
-    @Test
-    public void testAppIdleNonMetered_enabled() throws Exception {
-        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".AppIdleNonMeteredTest",
-                "testBackgroundNetworkAccess_enabled");
-    }
-
-    @Test
-    public void testAppIdleNonMetered_idleWhitelisted() throws Exception {
-        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".AppIdleNonMeteredTest",
-                "testAppIdleNetworkAccess_idleWhitelisted");
-    }
-
-    @Test
-    public void testAppIdleNonMetered_whenCharging() throws Exception {
-        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".AppIdleNonMeteredTest",
-                "testAppIdleNetworkAccess_whenCharging");
-    }
-
-    @Test
-    public void testAppIdleMetered_whenCharging() throws Exception {
-        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".AppIdleMeteredTest",
-                "testAppIdleNetworkAccess_whenCharging");
-    }
-
-    @Test
-    public void testAppIdle_toast() throws Exception {
-        // Check that showing a toast doesn't bring an app out of standby
-        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".AppIdleNonMeteredTest",
-                "testAppIdle_toast");
-    }
-
-    /********************
-     * Doze Mode tests. *
-     ********************/
-
-    @Test
-    public void testDozeModeMetered_disabled() throws Exception {
-        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".DozeModeMeteredTest",
-                "testBackgroundNetworkAccess_disabled");
-    }
-
-    @Test
-    public void testDozeModeMetered_whitelisted() throws Exception {
-        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".DozeModeMeteredTest",
-                "testBackgroundNetworkAccess_whitelisted");
-    }
-
-    @Test
-    public void testDozeModeMetered_enabled() throws Exception {
-        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".DozeModeMeteredTest",
-                "testBackgroundNetworkAccess_enabled");
-    }
-
-    @Test
-    public void testDozeModeMetered_enabledButWhitelistedOnNotificationAction() throws Exception {
-        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".DozeModeMeteredTest",
-                "testBackgroundNetworkAccess_enabledButWhitelistedOnNotificationAction");
-    }
-
-    // TODO: currently power-save mode and idle uses the same whitelist, so this test would be
-    // redundant (as it would be testing the same as testBatterySaverMode_reinstall())
-    //    public void testDozeMode_reinstall() throws Exception {
-    //    }
-
-    @Test
-    public void testDozeModeNonMetered_disabled() throws Exception {
-        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".DozeModeNonMeteredTest",
-                "testBackgroundNetworkAccess_disabled");
-    }
-
-    @Test
-    public void testDozeModeNonMetered_whitelisted() throws Exception {
-        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".DozeModeNonMeteredTest",
-                "testBackgroundNetworkAccess_whitelisted");
-    }
-
-    @Test
-    public void testDozeModeNonMetered_enabled() throws Exception {
-        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".DozeModeNonMeteredTest",
-                "testBackgroundNetworkAccess_enabled");
-    }
-
-    @Test
-    public void testDozeModeNonMetered_enabledButWhitelistedOnNotificationAction()
-            throws Exception {
-        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".DozeModeNonMeteredTest",
-                "testBackgroundNetworkAccess_enabledButWhitelistedOnNotificationAction");
-    }
-
-    /**********************
-     * Mixed modes tests. *
-     **********************/
-
-    @Test
-    public void testDataAndBatterySaverModes_meteredNetwork() throws Exception {
-        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".MixedModesTest",
-                "testDataAndBatterySaverModes_meteredNetwork");
-    }
-
-    @Test
-    public void testDataAndBatterySaverModes_nonMeteredNetwork() throws Exception {
-        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".MixedModesTest",
-                "testDataAndBatterySaverModes_nonMeteredNetwork");
-    }
-
-    @Test
-    public void testDozeAndBatterySaverMode_powerSaveWhitelists() throws Exception {
-        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".MixedModesTest",
-                "testDozeAndBatterySaverMode_powerSaveWhitelists");
-    }
-
-    @Test
-    public void testDozeAndAppIdle_powerSaveWhitelists() throws Exception {
-        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".MixedModesTest",
-                "testDozeAndAppIdle_powerSaveWhitelists");
-    }
-
-    @Test
-    public void testAppIdleAndDoze_tempPowerSaveWhitelists() throws Exception {
-        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".MixedModesTest",
-                "testAppIdleAndDoze_tempPowerSaveWhitelists");
-    }
-
-    @Test
-    public void testAppIdleAndBatterySaver_tempPowerSaveWhitelists() throws Exception {
-        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".MixedModesTest",
-                "testAppIdleAndBatterySaver_tempPowerSaveWhitelists");
-    }
-
-    @Test
-    public void testDozeAndAppIdle_appIdleWhitelist() throws Exception {
-        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".MixedModesTest",
-                "testDozeAndAppIdle_appIdleWhitelist");
-    }
-
-    @Test
-    public void testAppIdleAndDoze_tempPowerSaveAndAppIdleWhitelists() throws Exception {
-        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".MixedModesTest",
-                "testAppIdleAndDoze_tempPowerSaveAndAppIdleWhitelists");
-    }
-
-    @Test
-    public void testAppIdleAndBatterySaver_tempPowerSaveAndAppIdleWhitelists() throws Exception {
-        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".MixedModesTest",
-                "testAppIdleAndBatterySaver_tempPowerSaveAndAppIdleWhitelists");
-    }
-
-    /**************************
-     * Restricted mode tests. *
-     **************************/
-
-    @Test
-    public void testNetworkAccess_restrictedMode() throws Exception {
-        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".RestrictedModeTest",
-                "testNetworkAccess");
-    }
-
-    @Test
-    public void testNetworkAccess_restrictedMode_withBatterySaver() throws Exception {
-        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".RestrictedModeTest",
-                "testNetworkAccess_withBatterySaver");
-    }
-
-    /************************
-     * Expedited job tests. *
-     ************************/
-
-    @Test
-    public void testMeteredNetworkAccess_expeditedJob() throws Exception {
-        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".ExpeditedJobMeteredTest");
-    }
-
-    @Test
-    public void testNonMeteredNetworkAccess_expeditedJob() throws Exception {
-        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".ExpeditedJobNonMeteredTest");
-    }
-
-    /*******************
-     * Helper methods. *
-     *******************/
-
-    private void assertRestrictBackgroundWhitelist(int uid, boolean expected) throws Exception {
-        final int max_tries = 5;
-        boolean actual = false;
-        for (int i = 1; i <= max_tries; i++) {
-            final String output = runCommand("cmd netpolicy list restrict-background-whitelist ");
-            actual = output.contains(Integer.toString(uid));
-            if (expected == actual) {
-                return;
-            }
-            Log.v(TAG, "whitelist check for uid " + uid + " doesn't match yet (expected "
-                    + expected + ", got " + actual + "); sleeping 1s before polling again");
-            RunUtil.getDefault().sleep(1000);
-        }
-        fail("whitelist check for uid " + uid + " failed: expected "
-                + expected + ", got " + actual);
-    }
-
-    private void assertPowerSaveModeWhitelist(String packageName, boolean expected)
-            throws Exception {
-        // TODO: currently the power-save mode is behaving like idle, but once it changes, we'll
-        // need to use netpolicy for whitelisting
-        assertDelayedCommand("dumpsys deviceidle whitelist =" + packageName,
-                Boolean.toString(expected));
-    }
-
-    /**
-     * Asserts the result of a command, wait and re-running it a couple times if necessary.
-     */
-    private void assertDelayedCommand(String command, String expectedResult)
-            throws InterruptedException, DeviceNotAvailableException {
-        final int maxTries = 5;
-        for (int i = 1; i <= maxTries; i++) {
-            final String result = runCommand(command).trim();
-            if (result.equals(expectedResult)) return;
-            Log.v(TAG, "Command '" + command + "' returned '" + result + " instead of '"
-                    + expectedResult + "' on attempt #; sleeping 1s before polling again");
-            RunUtil.getDefault().sleep(1000);
-        }
-        fail("Command '" + command + "' did not return '" + expectedResult + "' after " + maxTries
-                + " attempts");
-    }
-
-    protected void addRestrictBackgroundWhitelist(int uid) throws Exception {
-        runCommand("cmd netpolicy add restrict-background-whitelist " + uid);
-        assertRestrictBackgroundWhitelist(uid, true);
-    }
-
-    private void addPowerSaveModeWhitelist(String packageName) throws Exception {
-        Log.i(TAG, "Adding package " + packageName + " to power-save-mode whitelist");
-        // TODO: currently the power-save mode is behaving like idle, but once it changes, we'll
-        // need to use netpolicy for whitelisting
-        runCommand("dumpsys deviceidle whitelist +" + packageName);
-        assertPowerSaveModeWhitelist(packageName, true);
-    }
-
-    protected boolean isDozeModeEnabled() throws Exception {
-        final String result = runCommand("cmd deviceidle enabled deep").trim();
-        return result.equals("1");
-    }
-}
diff --git a/tests/cts/hostside-network-policy/src/com/android/cts/netpolicy/NetworkPolicyTestsPreparer.java b/tests/cts/hostside-network-policy/src/com/android/cts/netpolicy/NetworkPolicyTestsPreparer.java
deleted file mode 100644
index cbf2f4d..0000000
--- a/tests/cts/hostside-network-policy/src/com/android/cts/netpolicy/NetworkPolicyTestsPreparer.java
+++ /dev/null
@@ -1,92 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.cts.netpolicy;
-
-import com.android.tradefed.device.DeviceNotAvailableException;
-import com.android.tradefed.device.ITestDevice;
-import com.android.tradefed.invoker.TestInformation;
-import com.android.tradefed.log.LogUtil;
-import com.android.tradefed.targetprep.ITargetPreparer;
-
-public class NetworkPolicyTestsPreparer implements ITargetPreparer {
-    private ITestDevice mDevice;
-    private boolean mOriginalAirplaneModeEnabled;
-    private String mOriginalAppStandbyEnabled;
-    private String mOriginalBatteryStatsConstants;
-    private final static String KEY_STABLE_CHARGING_DELAY_MS = "battery_charged_delay_ms";
-    private final static int DESIRED_STABLE_CHARGING_DELAY_MS = 0;
-
-    @Override
-    public void setUp(TestInformation testInformation) throws DeviceNotAvailableException {
-        mDevice = testInformation.getDevice();
-        mOriginalAppStandbyEnabled = getAppStandbyEnabled();
-        setAppStandbyEnabled("1");
-        LogUtil.CLog.d("Original app_standby_enabled: " + mOriginalAppStandbyEnabled);
-
-        mOriginalBatteryStatsConstants = getBatteryStatsConstants();
-        setBatteryStatsConstants(
-                KEY_STABLE_CHARGING_DELAY_MS + "=" + DESIRED_STABLE_CHARGING_DELAY_MS);
-        LogUtil.CLog.d("Original battery_saver_constants: " + mOriginalBatteryStatsConstants);
-
-        mOriginalAirplaneModeEnabled = getAirplaneModeEnabled();
-        // Turn off airplane mode in case another test left the device in that state.
-        setAirplaneModeEnabled(false);
-        LogUtil.CLog.d("Original airplane mode state: " + mOriginalAirplaneModeEnabled);
-    }
-
-    @Override
-    public void tearDown(TestInformation testInformation, Throwable e)
-            throws DeviceNotAvailableException {
-        setAirplaneModeEnabled(mOriginalAirplaneModeEnabled);
-        setAppStandbyEnabled(mOriginalAppStandbyEnabled);
-        setBatteryStatsConstants(mOriginalBatteryStatsConstants);
-    }
-
-    private void setAirplaneModeEnabled(boolean enable) throws DeviceNotAvailableException {
-        executeCmd("cmd connectivity airplane-mode " + (enable ? "enable" : "disable"));
-    }
-
-    private boolean getAirplaneModeEnabled() throws DeviceNotAvailableException {
-        return "enabled".equals(executeCmd("cmd connectivity airplane-mode").trim());
-    }
-
-    private void setAppStandbyEnabled(String appStandbyEnabled) throws DeviceNotAvailableException {
-        if ("null".equals(appStandbyEnabled)) {
-            executeCmd("settings delete global app_standby_enabled");
-        } else {
-            executeCmd("settings put global app_standby_enabled " + appStandbyEnabled);
-        }
-    }
-
-    private String getAppStandbyEnabled() throws DeviceNotAvailableException {
-        return executeCmd("settings get global app_standby_enabled").trim();
-    }
-
-    private void setBatteryStatsConstants(String batteryStatsConstants)
-            throws DeviceNotAvailableException {
-        executeCmd("settings put global battery_stats_constants \"" + batteryStatsConstants + "\"");
-    }
-
-    private String getBatteryStatsConstants() throws DeviceNotAvailableException {
-        return executeCmd("settings get global battery_stats_constants");
-    }
-
-    private String executeCmd(String cmd) throws DeviceNotAvailableException {
-        final String output = mDevice.executeShellCommand(cmd).trim();
-        LogUtil.CLog.d("Output for '%s': %s", cmd, output);
-        return output;
-    }
-}
diff --git a/tests/cts/multidevices/Android.bp b/tests/cts/multidevices/Android.bp
index dc90adb..5f062f1 100644
--- a/tests/cts/multidevices/Android.bp
+++ b/tests/cts/multidevices/Android.bp
@@ -19,9 +19,11 @@
 
 python_test_host {
     name: "CtsConnectivityMultiDevicesTestCases",
-    main: "connectivity_multi_devices_test.py",
+    main: "run_tests.py",
     srcs: [
+        "apfv4_test.py",
         "connectivity_multi_devices_test.py",
+        "run_tests.py",
     ],
     libs: [
         "mobly",
diff --git a/tests/cts/multidevices/apfv4_test.py b/tests/cts/multidevices/apfv4_test.py
new file mode 100644
index 0000000..4633d37
--- /dev/null
+++ b/tests/cts/multidevices/apfv4_test.py
@@ -0,0 +1,35 @@
+#  Copyright (C) 2024 The Android Open Source Project
+#
+#  Licensed under the Apache License, Version 2.0 (the "License");
+#  you may not use this file except in compliance with the License.
+#  You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+
+from net_tests_utils.host.python import apf_test_base
+
+# Constants.
+COUNTER_DROPPED_ETHERTYPE_NOT_ALLOWED = "DROPPED_ETHERTYPE_NOT_ALLOWED"
+ETHER_BROADCAST_ADDR = "FFFFFFFFFFFF"
+ETH_P_ETHERCAT = "88A4"
+
+
+class ApfV4Test(apf_test_base.ApfTestBase):
+
+  def test_apf_drop_ethercat(self):
+    # Ethernet header (14 bytes).
+    packet = ETHER_BROADCAST_ADDR  # Destination MAC (broadcast)
+    packet += self.server_mac_address.replace(":", "")  # Source MAC
+    packet += ETH_P_ETHERCAT  # EtherType (EtherCAT)
+
+    # EtherCAT header (2 bytes) + 44 bytes of zero padding.
+    packet += "00" * 46
+    self.send_packet_and_expect_counter_increased(
+        packet, COUNTER_DROPPED_ETHERTYPE_NOT_ALLOWED
+    )
diff --git a/tests/cts/multidevices/connectivity_multi_devices_test.py b/tests/cts/multidevices/connectivity_multi_devices_test.py
index 0cfc361..416a2e8 100644
--- a/tests/cts/multidevices/connectivity_multi_devices_test.py
+++ b/tests/cts/multidevices/connectivity_multi_devices_test.py
@@ -1,40 +1,25 @@
-# Lint as: python3
-"""Connectivity multi devices tests."""
-import sys
+#  Copyright (C) 2024 The Android Open Source Project
+#
+#  Licensed under the Apache License, Version 2.0 (the "License");
+#  you may not use this file except in compliance with the License.
+#  You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
 
-from mobly import asserts
-from mobly import base_test
-from mobly import test_runner
-from mobly import utils
-from mobly.controllers import android_device
-from net_tests_utils.host.python import adb_utils, apf_utils, assert_utils, mdns_utils, tether_utils
+from net_tests_utils.host.python import mdns_utils, multi_devices_test_base, tether_utils
+from net_tests_utils.host.python import wifip2p_utils
 from net_tests_utils.host.python.tether_utils import UpstreamType
 
-CONNECTIVITY_MULTI_DEVICES_SNIPPET_PACKAGE = "com.google.snippet.connectivity"
-COUNTER_DROPPED_ETHERTYPE_NOT_ALLOWED = "DROPPED_ETHERTYPE_NOT_ALLOWED"
 
-
-class ConnectivityMultiDevicesTest(base_test.BaseTestClass):
-
-  def setup_class(self):
-    # Declare that two Android devices are needed.
-    self.clientDevice, self.serverDevice = self.register_controller(
-        android_device, min_number=2
-    )
-
-    def setup_device(device):
-      device.load_snippet(
-          "connectivity_multi_devices_snippet",
-          CONNECTIVITY_MULTI_DEVICES_SNIPPET_PACKAGE,
-      )
-
-    # Set up devices in parallel to save time.
-    utils.concurrent_exec(
-        setup_device,
-        ((self.clientDevice,), (self.serverDevice,)),
-        max_workers=2,
-        raise_on_exception=True,
-    )
+class ConnectivityMultiDevicesTest(
+    multi_devices_test_base.MultiDevicesTestBase
+):
 
   def test_hotspot_upstream_wifi(self):
     tether_utils.assume_hotspot_test_preconditions(
@@ -68,6 +53,9 @@
     tether_utils.assume_hotspot_test_preconditions(
         self.serverDevice, self.clientDevice, UpstreamType.NONE
     )
+    mdns_utils.assume_mdns_test_preconditions(
+        self.clientDevice, self.serverDevice
+    )
     try:
       # Connectivity of the client verified by asserting the validated capability.
       tether_utils.setup_hotspot_and_client_for_upstream_type(
@@ -82,53 +70,20 @@
           self.serverDevice, UpstreamType.NONE
       )
 
-  def test_apf_drop_ethercat(self):
-    tether_utils.assume_hotspot_test_preconditions(
-        self.serverDevice, self.clientDevice, UpstreamType.NONE
+  def test_mdns_via_wifip2p(self):
+    wifip2p_utils.assume_wifi_p2p_test_preconditions(
+        self.serverDevice, self.clientDevice
     )
-    client = self.clientDevice.connectivity_multi_devices_snippet
+    mdns_utils.assume_mdns_test_preconditions(
+        self.clientDevice, self.serverDevice
+    )
     try:
-      server_iface_name, client_network = (
-          tether_utils.setup_hotspot_and_client_for_upstream_type(
-              self.serverDevice, self.clientDevice, UpstreamType.NONE
-          )
+      wifip2p_utils.setup_wifi_p2p_server_and_client(
+          self.serverDevice, self.clientDevice
       )
-      client_iface_name = client.getInterfaceNameFromNetworkHandle(client_network)
-
-      adb_utils.set_doze_mode(self.clientDevice, True)
-
-      count_before_test = apf_utils.get_apf_counter(
-          self.clientDevice,
-          client_iface_name,
-          COUNTER_DROPPED_ETHERTYPE_NOT_ALLOWED,
-      )
-      try:
-        apf_utils.send_broadcast_empty_ethercat_packet(
-            self.serverDevice, server_iface_name
-        )
-      except apf_utils.UnsupportedOperationException:
-        asserts.skip(
-            "NetworkStack is too old to support send raw packet, skip test."
-        )
-
-      assert_utils.expect_with_retry(
-          lambda: apf_utils.get_apf_counter(
-              self.clientDevice,
-              client_iface_name,
-              COUNTER_DROPPED_ETHERTYPE_NOT_ALLOWED,
-          )
-          > count_before_test
+      mdns_utils.register_mdns_service_and_discover_resolve(
+          self.clientDevice, self.serverDevice
       )
     finally:
-      adb_utils.set_doze_mode(self.clientDevice, False)
-      tether_utils.cleanup_tethering_for_upstream_type(
-          self.serverDevice, UpstreamType.NONE
-      )
-
-
-if __name__ == "__main__":
-  # Take test args
-  if "--" in sys.argv:
-    index = sys.argv.index("--")
-    sys.argv = sys.argv[:1] + sys.argv[index + 1 :]
-  test_runner.main()
+      mdns_utils.cleanup_mdns_service(self.clientDevice, self.serverDevice)
+      wifip2p_utils.cleanup_wifi_p2p(self.serverDevice, self.clientDevice)
diff --git a/tests/cts/multidevices/run_tests.py b/tests/cts/multidevices/run_tests.py
new file mode 100644
index 0000000..1391d13
--- /dev/null
+++ b/tests/cts/multidevices/run_tests.py
@@ -0,0 +1,38 @@
+#  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.
+
+"""Main entrypoint for all of test cases."""
+
+import sys
+from apfv4_test import ApfV4Test
+from connectivity_multi_devices_test import ConnectivityMultiDevicesTest
+from mobly import suite_runner
+
+
+if __name__ == "__main__":
+  # For MoblyBinaryHostTest, this entry point will be called twice:
+  # 1. List tests.
+  #   <mobly-par-file-name> -- --list_tests
+  # 2. Run tests.
+  #   <mobly-par-file-name> -- --config=<yaml-path> \
+  #      --device_serial=<device-serial> --log_path=<log-path>
+  # Strip the "--" since suite runner doesn't recognize it.
+  # While the parameters before "--" is for the infrastructure,
+  # ignore them if any. Also, do not alter parameters if there
+  # is no "--", in case the binary is invoked manually.
+  if "--" in sys.argv:
+    index = sys.argv.index("--")
+    sys.argv = sys.argv[:1] + sys.argv[index + 1 :]
+  # TODO: make the tests can be executed without manually list classes.
+  suite_runner.run_suite([ConnectivityMultiDevicesTest, ApfV4Test], sys.argv)
diff --git a/tests/cts/multidevices/snippet/Android.bp b/tests/cts/multidevices/snippet/Android.bp
index b0b32c2..c94087e 100644
--- a/tests/cts/multidevices/snippet/Android.bp
+++ b/tests/cts/multidevices/snippet/Android.bp
@@ -26,6 +26,7 @@
     srcs: [
         "ConnectivityMultiDevicesSnippet.kt",
         "MdnsMultiDevicesSnippet.kt",
+        "Wifip2pMultiDevicesSnippet.kt",
     ],
     manifest: "AndroidManifest.xml",
     static_libs: [
diff --git a/tests/cts/multidevices/snippet/AndroidManifest.xml b/tests/cts/multidevices/snippet/AndroidManifest.xml
index 967e581..4637497 100644
--- a/tests/cts/multidevices/snippet/AndroidManifest.xml
+++ b/tests/cts/multidevices/snippet/AndroidManifest.xml
@@ -21,6 +21,8 @@
   <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
   <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
   <uses-permission android:name="android.permission.INTERNET" />
+  <uses-permission android:name="android.permission.NEARBY_WIFI_DEVICES"
+                   android:usesPermissionFlags="neverForLocation" />
   <application>
     <!-- Add any classes that implement the Snippet interface as meta-data, whose
          value is a comma-separated string, each section being the package path
@@ -28,7 +30,8 @@
     <meta-data
         android:name="mobly-snippets"
         android:value="com.google.snippet.connectivity.ConnectivityMultiDevicesSnippet,
-                       com.google.snippet.connectivity.MdnsMultiDevicesSnippet" />
+                       com.google.snippet.connectivity.MdnsMultiDevicesSnippet,
+                       com.google.snippet.connectivity.Wifip2pMultiDevicesSnippet" />
   </application>
   <!-- Add an instrumentation tag so that the app can be launched through an
        instrument command. The runner `com.google.android.mobly.snippet.SnippetRunner`
diff --git a/tests/cts/multidevices/snippet/ConnectivityMultiDevicesSnippet.kt b/tests/cts/multidevices/snippet/ConnectivityMultiDevicesSnippet.kt
index 9bdf4a3..7368669 100644
--- a/tests/cts/multidevices/snippet/ConnectivityMultiDevicesSnippet.kt
+++ b/tests/cts/multidevices/snippet/ConnectivityMultiDevicesSnippet.kt
@@ -36,6 +36,7 @@
 import android.net.wifi.WifiNetworkSpecifier
 import android.net.wifi.WifiSsid
 import androidx.test.platform.app.InstrumentationRegistry
+import com.android.modules.utils.build.SdkLevel
 import com.android.testutils.AutoReleaseNetworkCallbackRule
 import com.android.testutils.ConnectUtil
 import com.android.testutils.NetworkCallbackHelper
@@ -71,6 +72,9 @@
     @Rpc(description = "Check whether the device supporters AP + STA concurrency.")
     fun isStaApConcurrencySupported() = wifiManager.isStaApConcurrencySupported()
 
+    @Rpc(description = "Check whether the device SDK is as least T")
+    fun isAtLeastT() = SdkLevel.isAtLeastT()
+
     @Rpc(description = "Request cellular connection and ensure it is the default network.")
     fun requestCellularAndEnsureDefault() {
         ctsNetUtils.disableWifi()
diff --git a/tests/cts/multidevices/snippet/Wifip2pMultiDevicesSnippet.kt b/tests/cts/multidevices/snippet/Wifip2pMultiDevicesSnippet.kt
new file mode 100644
index 0000000..f8c9351
--- /dev/null
+++ b/tests/cts/multidevices/snippet/Wifip2pMultiDevicesSnippet.kt
@@ -0,0 +1,312 @@
+/*
+ * 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.google.snippet.connectivity
+
+import android.Manifest.permission.MANAGE_WIFI_NETWORK_SELECTION
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.net.MacAddress
+import android.net.wifi.WifiManager
+import android.net.wifi.p2p.WifiP2pConfig
+import android.net.wifi.p2p.WifiP2pDevice
+import android.net.wifi.p2p.WifiP2pDeviceList
+import android.net.wifi.p2p.WifiP2pGroup
+import android.net.wifi.p2p.WifiP2pManager
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.net.module.util.ArrayTrackRecord
+import com.android.testutils.runAsShell
+import com.google.android.mobly.snippet.Snippet
+import com.google.android.mobly.snippet.rpc.Rpc
+import com.google.snippet.connectivity.Wifip2pMultiDevicesSnippet.Wifip2pIntentReceiver.IntentReceivedEvent.ConnectionChanged
+import com.google.snippet.connectivity.Wifip2pMultiDevicesSnippet.Wifip2pIntentReceiver.IntentReceivedEvent.PeersChanged
+import java.util.concurrent.CompletableFuture
+import java.util.concurrent.TimeUnit
+import kotlin.test.assertNotNull
+import kotlin.test.fail
+
+private const val TIMEOUT_MS = 60000L
+
+class Wifip2pMultiDevicesSnippet : Snippet {
+    private val context by lazy { InstrumentationRegistry.getInstrumentation().getTargetContext() }
+    private val wifiManager by lazy {
+        context.getSystemService(WifiManager::class.java)
+                ?: fail("Could not get WifiManager service")
+    }
+    private val wifip2pManager by lazy {
+        context.getSystemService(WifiP2pManager::class.java)
+                ?: fail("Could not get WifiP2pManager service")
+    }
+    private lateinit var wifip2pChannel: WifiP2pManager.Channel
+    private val wifip2pIntentReceiver = Wifip2pIntentReceiver()
+
+    private class Wifip2pIntentReceiver : BroadcastReceiver() {
+        val history = ArrayTrackRecord<IntentReceivedEvent>().newReadHead()
+
+        sealed class IntentReceivedEvent {
+            abstract val intent: Intent
+            data class ConnectionChanged(override val intent: Intent) : IntentReceivedEvent()
+            data class PeersChanged(override val intent: Intent) : IntentReceivedEvent()
+        }
+
+        override fun onReceive(context: Context, intent: Intent) {
+            when (intent.action) {
+                WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION -> {
+                    history.add(ConnectionChanged(intent))
+                }
+                WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION -> {
+                    history.add(PeersChanged(intent))
+                }
+            }
+        }
+
+        inline fun <reified T : IntentReceivedEvent> eventuallyExpectedIntent(
+                timeoutMs: Long = TIMEOUT_MS,
+                crossinline predicate: (T) -> Boolean = { true }
+        ): T = history.poll(timeoutMs) { it is T && predicate(it) }.also {
+            assertNotNull(it, "Intent ${T::class} not received within ${timeoutMs}ms.")
+        } as T
+    }
+
+    @Rpc(description = "Check whether the device supports Wi-Fi P2P.")
+    fun isP2pSupported() = wifiManager.isP2pSupported()
+
+    @Rpc(description = "Start Wi-Fi P2P")
+    fun startWifiP2p() {
+        // Initialize Wi-Fi P2P
+        wifip2pChannel = wifip2pManager.initialize(context, context.mainLooper, null)
+
+        // Ensure the Wi-Fi P2P channel is available
+        val p2pStateEnabledFuture = CompletableFuture<Boolean>()
+        wifip2pManager.requestP2pState(wifip2pChannel) { state ->
+            if (state == WifiP2pManager.WIFI_P2P_STATE_ENABLED) {
+                p2pStateEnabledFuture.complete(true)
+            }
+        }
+        p2pStateEnabledFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
+        // Register an intent filter to receive Wi-Fi P2P intents
+        val filter = IntentFilter(WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION)
+        filter.addAction(WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION)
+        context.registerReceiver(wifip2pIntentReceiver, filter)
+    }
+
+    @Rpc(description = "Stop Wi-Fi P2P")
+    fun stopWifiP2p() {
+        if (this::wifip2pChannel.isInitialized) {
+            wifip2pManager.cancelConnect(wifip2pChannel, null)
+            wifip2pManager.removeGroup(wifip2pChannel, null)
+        }
+        // Unregister the intent filter
+        context.unregisterReceiver(wifip2pIntentReceiver)
+    }
+
+    @Rpc(description = "Get the current device name")
+    fun getDeviceName(): String {
+        // Retrieve current device info
+        val deviceFuture = CompletableFuture<String>()
+        wifip2pManager.requestDeviceInfo(wifip2pChannel) { wifiP2pDevice ->
+            if (wifiP2pDevice != null) {
+                deviceFuture.complete(wifiP2pDevice.deviceName)
+            }
+        }
+        // Return current device name
+        return deviceFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
+    }
+
+    @Rpc(description = "Wait for a p2p connection changed intent and check the group")
+    @Suppress("DEPRECATION")
+    fun waitForP2pConnectionChanged(ignoreGroupCheck: Boolean, groupName: String) {
+        wifip2pIntentReceiver.eventuallyExpectedIntent<ConnectionChanged>() {
+            val p2pGroup: WifiP2pGroup? =
+                    it.intent.getParcelableExtra(WifiP2pManager.EXTRA_WIFI_P2P_GROUP)
+            val groupMatched = p2pGroup?.networkName == groupName
+            return@eventuallyExpectedIntent ignoreGroupCheck || groupMatched
+        }
+    }
+
+    @Rpc(description = "Create a Wi-Fi P2P group")
+    fun createGroup(groupName: String, groupPassphrase: String) {
+        // Create a Wi-Fi P2P group
+        val wifip2pConfig = WifiP2pConfig.Builder()
+                .setNetworkName(groupName)
+                .setPassphrase(groupPassphrase)
+                .build()
+        val createGroupFuture = CompletableFuture<Boolean>()
+        wifip2pManager.createGroup(
+                wifip2pChannel,
+                wifip2pConfig,
+                object : WifiP2pManager.ActionListener {
+                    override fun onFailure(reason: Int) = Unit
+                    override fun onSuccess() { createGroupFuture.complete(true) }
+                }
+        )
+        createGroupFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
+
+        // Ensure the Wi-Fi P2P group is created.
+        waitForP2pConnectionChanged(false, groupName)
+    }
+
+    @Rpc(description = "Start Wi-Fi P2P peers discovery")
+    fun startPeersDiscovery() {
+        // Start discovery Wi-Fi P2P peers
+        wifip2pManager.discoverPeers(wifip2pChannel, null)
+
+        // Ensure the discovery is started
+        val p2pDiscoveryStartedFuture = CompletableFuture<Boolean>()
+        wifip2pManager.requestDiscoveryState(wifip2pChannel) { state ->
+            if (state == WifiP2pManager.WIFI_P2P_DISCOVERY_STARTED) {
+                p2pDiscoveryStartedFuture.complete(true)
+            }
+        }
+        p2pDiscoveryStartedFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
+    }
+
+    /**
+     * Get the device address from the given intent that matches the given device name.
+     *
+     * @param peersChangedIntent the intent to get the device address from
+     * @param deviceName the target device name
+     * @return the address of the target device or null if no devices match.
+     */
+    @Suppress("DEPRECATION")
+    private fun getDeviceAddress(peersChangedIntent: Intent, deviceName: String): String? {
+        val peers: WifiP2pDeviceList? =
+                peersChangedIntent.getParcelableExtra(WifiP2pManager.EXTRA_P2P_DEVICE_LIST)
+        return peers?.deviceList?.firstOrNull { it.deviceName == deviceName }?.deviceAddress
+    }
+
+    /**
+     * Ensure the given device has been discovered and returns the associated device address for
+     * connection.
+     *
+     * @param deviceName the target device name
+     * @return the address of the target device.
+     */
+    @Rpc(description = "Ensure the target Wi-Fi P2P device is discovered")
+    fun ensureDeviceDiscovered(deviceName: String): String {
+        val changedEvent = wifip2pIntentReceiver.eventuallyExpectedIntent<PeersChanged>() {
+            return@eventuallyExpectedIntent getDeviceAddress(it.intent, deviceName) != null
+        }
+        return getDeviceAddress(changedEvent.intent, deviceName)
+                ?: fail("Missing device in filtered intent")
+    }
+
+    @Rpc(description = "Invite a Wi-Fi P2P device to the group")
+    fun inviteDeviceToGroup(groupName: String, groupPassphrase: String, deviceAddress: String) {
+        // Connect to the device to send invitation
+        val wifip2pConfig = WifiP2pConfig.Builder()
+                .setNetworkName(groupName)
+                .setPassphrase(groupPassphrase)
+                .setDeviceAddress(MacAddress.fromString(deviceAddress))
+                .build()
+        val connectedFuture = CompletableFuture<Boolean>()
+        wifip2pManager.connect(
+                wifip2pChannel,
+                wifip2pConfig,
+                object : WifiP2pManager.ActionListener {
+                    override fun onFailure(reason: Int) = Unit
+                    override fun onSuccess() {
+                        connectedFuture.complete(true)
+                    }
+                }
+        )
+        connectedFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
+    }
+
+    private fun runExternalApproverForGroupProcess(
+            deviceAddress: String,
+            isGroupInvitation: Boolean
+    ) {
+        val peer = MacAddress.fromString(deviceAddress)
+        runAsShell(MANAGE_WIFI_NETWORK_SELECTION) {
+            val connectionRequestFuture = CompletableFuture<Boolean>()
+            val attachedFuture = CompletableFuture<Boolean>()
+            wifip2pManager.addExternalApprover(
+                    wifip2pChannel,
+                    peer,
+                    object : WifiP2pManager.ExternalApproverRequestListener {
+                        override fun onAttached(deviceAddress: MacAddress) {
+                            attachedFuture.complete(true)
+                        }
+                        override fun onDetached(deviceAddress: MacAddress, reason: Int) = Unit
+                        override fun onConnectionRequested(
+                                requestType: Int,
+                                config: WifiP2pConfig,
+                                device: WifiP2pDevice
+                        ) {
+                            connectionRequestFuture.complete(true)
+                        }
+                        override fun onPinGenerated(deviceAddress: MacAddress, pin: String) = Unit
+                    }
+            )
+            if (isGroupInvitation) attachedFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS) else
+                connectionRequestFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
+
+            val resultFuture = CompletableFuture<Boolean>()
+            wifip2pManager.setConnectionRequestResult(
+                    wifip2pChannel,
+                    peer,
+                    WifiP2pManager.CONNECTION_REQUEST_ACCEPT,
+                    object : WifiP2pManager.ActionListener {
+                        override fun onFailure(reason: Int) = Unit
+                        override fun onSuccess() {
+                            resultFuture.complete(true)
+                        }
+                    }
+            )
+            resultFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
+
+            val removeFuture = CompletableFuture<Boolean>()
+            wifip2pManager.removeExternalApprover(
+                    wifip2pChannel,
+                    peer,
+                    object : WifiP2pManager.ActionListener {
+                        override fun onFailure(reason: Int) = Unit
+                        override fun onSuccess() {
+                            removeFuture.complete(true)
+                        }
+                    }
+            )
+            removeFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
+        }
+    }
+
+    @Rpc(description = "Accept P2P group invitation from device")
+    fun acceptGroupInvitation(deviceAddress: String) {
+        // Accept the Wi-Fi P2P group invitation
+        runExternalApproverForGroupProcess(deviceAddress, true /* isGroupInvitation */)
+    }
+
+    @Rpc(description = "Wait for connection request from the peer and accept joining")
+    fun waitForPeerConnectionRequestAndAcceptJoining(deviceAddress: String) {
+        // Wait for connection request from the peer and accept joining
+        runExternalApproverForGroupProcess(deviceAddress, false /* isGroupInvitation */)
+    }
+
+    @Rpc(description = "Ensure the target device is connected")
+    fun ensureDeviceConnected(deviceName: String) {
+        // Retrieve peers and ensure the target device is connected
+        val connectedFuture = CompletableFuture<Boolean>()
+        wifip2pManager.requestPeers(wifip2pChannel) { peers -> peers?.deviceList?.any {
+            it.deviceName == deviceName && it.status == WifiP2pDevice.CONNECTED }.let {
+                connectedFuture.complete(true)
+            }
+        }
+        connectedFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
+    }
+}
diff --git a/tests/cts/net/AndroidTestTemplate.xml b/tests/cts/net/AndroidTestTemplate.xml
index 024d3bf..24431a6 100644
--- a/tests/cts/net/AndroidTestTemplate.xml
+++ b/tests/cts/net/AndroidTestTemplate.xml
@@ -25,6 +25,7 @@
     <option name="config-descriptor:metadata" key="mainline-param" value="com.google.android.tethering.apex" />
     <option name="not-shardable" value="true" />
     <target_preparer class="com.android.compatibility.common.tradefed.targetprep.DynamicConfigPusher">
+        <option name="has-server-side-config" value="false" />
         <option name="target" value="device" />
         <option name="config-filename" value="{MODULE}" />
         <option name="version" value="1.0" />
diff --git a/tests/cts/net/src/android/net/cts/ApfIntegrationTest.kt b/tests/cts/net/src/android/net/cts/ApfIntegrationTest.kt
index 5662fca..9ac2c67 100644
--- a/tests/cts/net/src/android/net/cts/ApfIntegrationTest.kt
+++ b/tests/cts/net/src/android/net/cts/ApfIntegrationTest.kt
@@ -410,6 +410,12 @@
         }
     }
 
+    private fun installAndVerifyProgram(program: ByteArray) {
+        installProgram(program)
+        val readResult = readProgram().take(program.size).toByteArray()
+        assertThat(readResult).isEqualTo(program)
+    }
+
     fun ApfV4GeneratorBase<*>.addPassIfNotIcmpv6EchoReply() {
         // If not IPv6 -> PASS
         addLoad16(R0, ETH_ETHERTYPE_OFFSET)
@@ -436,12 +442,7 @@
         assumeApfVersionSupportAtLeast(4)
 
         // clear any active APF filter
-        var gen = ApfV4Generator(
-                caps.apfVersionSupported,
-                caps.maximumApfProgramSize,
-                caps.maximumApfProgramSize
-        ).addPass()
-        installProgram(gen.generate())
+        clearApfMemory()
         readProgram() // wait for install completion
 
         // Assert that initial ping does not get filtered.
@@ -455,7 +456,7 @@
         assertThat(packetReader.expectPingReply()).isEqualTo(data)
 
         // Generate an APF program that drops the next ping
-        gen = ApfV4Generator(
+        val gen = ApfV4Generator(
                 caps.apfVersionSupported,
                 caps.maximumApfProgramSize,
                 caps.maximumApfProgramSize
@@ -472,8 +473,7 @@
         gen.addJump(BaseApfGenerator.DROP_LABEL)
 
         val program = gen.generate()
-        installProgram(program)
-        readProgram() // wait for install completion
+        installAndVerifyProgram(program)
 
         packetReader.sendPing(data, payloadSize)
         packetReader.expectPingDropped()
@@ -518,8 +518,7 @@
 
         val program = gen.generate()
         assertThat(program.size).isLessThan(counterRegion)
-        installProgram(program)
-        readProgram() // wait for install completion
+        installAndVerifyProgram(program)
 
         // Trigger the program by sending a ping and waiting on the reply.
         val payloadSize = if (getFirstApiLevel() >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
@@ -567,8 +566,7 @@
         gen.addLoadFromMemory(R0, MemorySlot.FILTER_AGE_SECONDS)
         gen.addStoreData(R0, 0)
 
-        installProgram(gen.generate())
-        readProgram() // wait for install completion
+        installAndVerifyProgram(gen.generate())
 
         val payloadSize = 56
         val data = ByteArray(payloadSize).also { Random.nextBytes(it) }
@@ -608,8 +606,7 @@
         gen.addLoadFromMemory(R0, MemorySlot.FILTER_AGE_16384THS)
         gen.addStoreCounter(FILTER_AGE_16384THS, R0)
 
-        installProgram(gen.generate())
-        readProgram() // wait for install completion
+        installAndVerifyProgram(gen.generate())
 
         val payloadSize = 56
         val data = ByteArray(payloadSize).also { Random.nextBytes(it) }
@@ -717,8 +714,7 @@
                 .addPass()
                 .generate()
 
-        installProgram(program)
-        readProgram() // Ensure installation is complete
+        installAndVerifyProgram(program)
 
         packetReader.sendPing(payload, payloadSize)
 
diff --git a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
index 21eb90f..88c2d5a 100644
--- a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
@@ -195,7 +195,6 @@
 
 import androidx.test.filters.RequiresDevice;
 import androidx.test.platform.app.InstrumentationRegistry;
-import androidx.test.runner.AndroidJUnit4;
 
 import com.android.compatibility.common.util.DynamicConfigDeviceSide;
 import com.android.internal.util.ArrayUtils;
@@ -207,10 +206,12 @@
 import com.android.networkstack.apishim.common.ConnectivityManagerShim;
 import com.android.testutils.AutoReleaseNetworkCallbackRule;
 import com.android.testutils.CompatUtil;
+import com.android.testutils.ConnectUtil;
 import com.android.testutils.ConnectivityModuleTest;
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
+import com.android.testutils.DevSdkIgnoreRunner;
 import com.android.testutils.DeviceConfigRule;
 import com.android.testutils.DeviceInfoUtils;
 import com.android.testutils.DumpTestUtils;
@@ -274,7 +275,8 @@
 import fi.iki.elonen.NanoHTTPD.Response.IStatus;
 import fi.iki.elonen.NanoHTTPD.Response.Status;
 
-@RunWith(AndroidJUnit4.class)
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRunner.RestoreDefaultNetwork
 public class ConnectivityManagerTest {
     @Rule(order = 1)
     public final DevSdkIgnoreRule ignoreRule = new DevSdkIgnoreRule();
@@ -2941,20 +2943,10 @@
         // This may also apply to wifi in principle, but in practice methods that mock validation
         // URL all disconnect wifi forcefully anyway, so don't wait for wifi to validate.
         if (mPackageManager.hasSystemFeature(FEATURE_TELEPHONY)) {
-            ensureValidatedNetwork(makeCellNetworkRequest());
+            new ConnectUtil(mContext).ensureCellularValidated();
         }
     }
 
-    private void ensureValidatedNetwork(NetworkRequest request) {
-        final TestableNetworkCallback cb = new TestableNetworkCallback();
-        mCm.registerNetworkCallback(request, cb);
-        cb.eventuallyExpect(CallbackEntry.NETWORK_CAPS_UPDATED,
-                NETWORK_CALLBACK_TIMEOUT_MS,
-                entry -> ((CallbackEntry.CapabilitiesChanged) entry).getCaps()
-                        .hasCapability(NET_CAPABILITY_VALIDATED));
-        mCm.unregisterNetworkCallback(cb);
-    }
-
     @AppModeFull(reason = "WRITE_DEVICE_CONFIG permission can't be granted to instant apps")
     @Test
     public void testAcceptPartialConnectivity_validatedNetwork() throws Exception {
diff --git a/tests/cts/net/src/android/net/cts/DnsResolverTest.java b/tests/cts/net/src/android/net/cts/DnsResolverTest.java
index 752891f..fa44ae9 100644
--- a/tests/cts/net/src/android/net/cts/DnsResolverTest.java
+++ b/tests/cts/net/src/android/net/cts/DnsResolverTest.java
@@ -852,13 +852,14 @@
     }
 
     public void doTestContinuousQueries(Executor executor) throws InterruptedException {
-        final String msg = "Test continuous " + QUERY_TIMES + " queries " + TEST_DOMAIN;
         for (Network network : getTestableNetworks()) {
             for (int i = 0; i < QUERY_TIMES ; ++i) {
-                final VerifyCancelInetAddressCallback callback =
-                        new VerifyCancelInetAddressCallback(msg, null);
                 // query v6/v4 in turn
                 boolean queryV6 = (i % 2 == 0);
+                final String msg = "Test continuous " + QUERY_TIMES + " queries " + TEST_DOMAIN
+                        + " on " + network + ", queryV6=" + queryV6;
+                final VerifyCancelInetAddressCallback callback =
+                        new VerifyCancelInetAddressCallback(msg, null);
                 mDns.query(network, TEST_DOMAIN, queryV6 ? TYPE_AAAA : TYPE_A,
                         FLAG_NO_CACHE_LOOKUP, executor, null, callback);
 
diff --git a/tests/cts/net/src/android/net/cts/DnsTest.java b/tests/cts/net/src/android/net/cts/DnsTest.java
index fb63a19..b1e5680 100644
--- a/tests/cts/net/src/android/net/cts/DnsTest.java
+++ b/tests/cts/net/src/android/net/cts/DnsTest.java
@@ -16,6 +16,10 @@
 
 package android.net.cts;
 
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import android.content.Context;
 import android.content.pm.PackageManager;
 import android.net.ConnectivityManager;
 import android.net.ConnectivityManager.NetworkCallback;
@@ -23,10 +27,16 @@
 import android.net.Network;
 import android.net.NetworkInfo;
 import android.os.SystemClock;
-import android.test.AndroidTestCase;
 import android.util.Log;
 
 import androidx.test.filters.RequiresDevice;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.testutils.DevSdkIgnoreRunner;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
 
 import java.net.Inet4Address;
 import java.net.Inet6Address;
@@ -36,7 +46,9 @@
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
 
-public class DnsTest extends AndroidTestCase {
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRunner.RestoreDefaultNetwork
+public class DnsTest {
 
     static {
         System.loadLibrary("nativedns_jni");
@@ -46,10 +58,13 @@
     private static final String TAG = "DnsTest";
     private static final String PROXY_NETWORK_TYPE = "PROXY";
 
+    private Context mContext;
     private ConnectivityManager mCm;
 
+    @Before
     public void setUp() {
-        mCm = getContext().getSystemService(ConnectivityManager.class);
+        mContext = InstrumentationRegistry.getInstrumentation().getContext();
+        mCm = mContext.getSystemService(ConnectivityManager.class);
     }
 
     /**
@@ -69,6 +84,7 @@
      * Perf - measure size of first and second tier caches and their effect
      * Assert requires network permission
      */
+    @Test
     @RequiresDevice // IPv6 support may be missing on presubmit virtual hardware
     public void testDnsWorks() throws Exception {
         ensureIpv6Connectivity();
@@ -91,7 +107,7 @@
 
         // Skip the rest of the test if the active network for watch is PROXY.
         // TODO: Check NetworkInfo type in addition to type name once ag/601257 is merged.
-        if (getContext().getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH)
+        if (mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH)
                 && activeNetworkInfoIsProxy()) {
             Log.i(TAG, "Skipping test because the active network type name is PROXY.");
             return;
diff --git a/tests/cts/net/src/android/net/cts/IpSecManagerTest.java b/tests/cts/net/src/android/net/cts/IpSecManagerTest.java
index b5f43d3..e94d94f 100644
--- a/tests/cts/net/src/android/net/cts/IpSecManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/IpSecManagerTest.java
@@ -451,9 +451,10 @@
             long uidTxDelta = 0;
             long uidRxDelta = 0;
             for (int i = 0; i < 100; i++) {
-                // Clear TrafficStats cache is needed to avoid rate-limit caching for
-                // TrafficStats API results on V+ devices.
-                if (SdkLevel.isAtLeastV()) {
+                // Since the target SDK of this test should always equal or be larger than V,
+                // TrafficStats caching is always enabled. Clearing the cache is needed to
+                // avoid rate-limiting on devices with a mainlined (T+) NetworkStatsService.
+                if (SdkLevel.isAtLeastT()) {
                     runAsShell(NETWORK_SETTINGS, () -> TrafficStats.clearRateLimitCaches());
                 }
                 uidTxDelta = TrafficStats.getUidTxPackets(Os.getuid()) - uidTxPackets;
@@ -530,9 +531,10 @@
         }
 
         private static void initStatsChecker() throws Exception {
-            // Clear TrafficStats cache is needed to avoid rate-limit caching for
-            // TrafficStats API results on V+ devices.
-            if (SdkLevel.isAtLeastV()) {
+            // Since the target SDK of this test should always equal or be larger than V,
+            // TrafficStats caching is always enabled. Clearing the cache is needed to
+            // avoid rate-limiting on devices with a mainlined (T+) NetworkStatsService.
+            if (SdkLevel.isAtLeastT()) {
                 runAsShell(NETWORK_SETTINGS, () -> TrafficStats.clearRateLimitCaches());
             }
             uidTxBytes = TrafficStats.getUidTxBytes(Os.getuid());
diff --git a/tests/cts/net/src/android/net/cts/MultinetworkApiTest.java b/tests/cts/net/src/android/net/cts/MultinetworkApiTest.java
index 06a827b..2c7d5c6 100644
--- a/tests/cts/net/src/android/net/cts/MultinetworkApiTest.java
+++ b/tests/cts/net/src/android/net/cts/MultinetworkApiTest.java
@@ -40,10 +40,10 @@
 import android.system.OsConstants;
 import android.util.ArraySet;
 
-import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.platform.app.InstrumentationRegistry;
 
 import com.android.testutils.AutoReleaseNetworkCallbackRule;
+import com.android.testutils.DevSdkIgnoreRunner;
 import com.android.testutils.DeviceConfigRule;
 
 import org.junit.Before;
@@ -53,7 +53,8 @@
 
 import java.util.Set;
 
-@RunWith(AndroidJUnit4.class)
+@DevSdkIgnoreRunner.RestoreDefaultNetwork
+@RunWith(DevSdkIgnoreRunner.class)
 public class MultinetworkApiTest {
     @Rule(order = 1)
     public final DeviceConfigRule mDeviceConfigRule = new DeviceConfigRule();
diff --git a/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt b/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
index beb9274..60081d4 100644
--- a/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
+++ b/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
@@ -194,15 +194,16 @@
 // TODO : enable this in a Mainline update or in V.
 private const val SHOULD_CREATE_NETWORKS_IMMEDIATELY = false
 
-@RunWith(DevSdkIgnoreRunner::class)
-// NetworkAgent is not updatable in R-, so this test does not need to be compatible with older
-// versions. NetworkAgent was also based on AsyncChannel before S so cannot be tested the same way.
-@IgnoreUpTo(Build.VERSION_CODES.R)
+@AppModeFull(reason = "Instant apps can't use NetworkAgent because it needs NETWORK_FACTORY'.")
 // NetworkAgent is updated as part of the connectivity module, and running NetworkAgent tests in MTS
 // for modules other than Connectivity does not provide much value. Only run them in connectivity
 // module MTS, so the tests only need to cover the case of an updated NetworkAgent.
 @ConnectivityModuleTest
-@AppModeFull(reason = "Instant apps can't use NetworkAgent because it needs NETWORK_FACTORY'.")
+@DevSdkIgnoreRunner.RestoreDefaultNetwork
+// NetworkAgent is not updatable in R-, so this test does not need to be compatible with older
+// versions. NetworkAgent was also based on AsyncChannel before S so cannot be tested the same way.
+@IgnoreUpTo(Build.VERSION_CODES.R)
+@RunWith(DevSdkIgnoreRunner::class)
 class NetworkAgentTest {
     private val LOCAL_IPV4_ADDRESS = InetAddresses.parseNumericAddress("192.0.2.1")
     private val REMOTE_IPV4_ADDRESS = InetAddresses.parseNumericAddress("192.0.2.2")
diff --git a/tests/cts/net/src/android/net/cts/NetworkValidationTest.kt b/tests/cts/net/src/android/net/cts/NetworkValidationTest.kt
index 621af23..f9acb66 100644
--- a/tests/cts/net/src/android/net/cts/NetworkValidationTest.kt
+++ b/tests/cts/net/src/android/net/cts/NetworkValidationTest.kt
@@ -19,7 +19,6 @@
 import android.Manifest.permission.MANAGE_TEST_NETWORKS
 import android.Manifest.permission.NETWORK_SETTINGS
 import android.content.Context
-import android.content.pm.PackageManager
 import android.net.ConnectivityManager
 import android.net.EthernetManager
 import android.net.InetAddresses
@@ -36,7 +35,6 @@
 import android.net.dhcp.DhcpPacket.DHCP_MESSAGE_TYPE_DISCOVER
 import android.net.dhcp.DhcpPacket.DHCP_MESSAGE_TYPE_REQUEST
 import android.net.dhcp.DhcpRequestPacket
-import android.os.Build
 import android.os.HandlerThread
 import android.platform.test.annotations.AppModeFull
 import androidx.test.platform.app.InstrumentationRegistry
@@ -44,7 +42,7 @@
 import com.android.net.module.util.Inet4AddressUtils.getBroadcastAddress
 import com.android.net.module.util.Inet4AddressUtils.getPrefixMaskAsInet4Address
 import com.android.net.module.util.NetworkStackConstants.IPV4_ADDR_ANY
-import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.AutoCloseTestInterfaceRule
 import com.android.testutils.DhcpClientPacketFilter
 import com.android.testutils.DhcpOptionFilter
 import com.android.testutils.RecorderCallback.CallbackEntry
@@ -79,10 +77,6 @@
 @AppModeFull(reason = "Instant apps cannot create test networks")
 @RunWith(AndroidJUnit4::class)
 class NetworkValidationTest {
-    @JvmField
-    @Rule
-    val ignoreRule = DevSdkIgnoreRule(ignoreClassUpTo = Build.VERSION_CODES.Q)
-
     private val context by lazy { InstrumentationRegistry.getInstrumentation().context }
     private val tnm by lazy { context.assertHasService(TestNetworkManager::class.java) }
     private val eth by lazy { context.assertHasService(EthernetManager::class.java) }
@@ -104,23 +98,24 @@
 
     private var testSkipped = false
 
+    @get:Rule
+    val testInterfaceRule = AutoCloseTestInterfaceRule(context)
+
     @Before
     fun setUp() {
         // This test requires using a tap interface as an ethernet interface.
-        val pm = context.getPackageManager()
-        testSkipped = !pm.hasSystemFeature(PackageManager.FEATURE_ETHERNET) &&
-                context.getSystemService(EthernetManager::class.java) == null
+        testSkipped = context.getSystemService(EthernetManager::class.java) == null
         assumeFalse(testSkipped)
 
         // Register a request so the network does not get torn down
         cm.requestNetwork(ethRequest, ethRequestCb)
         runAsShell(NETWORK_SETTINGS, MANAGE_TEST_NETWORKS) {
             eth.setIncludeTestInterfaces(true)
-            // Keeping a reference to the test interface also makes sure the ParcelFileDescriptor
-            // does not go out of scope, which would cause it to close the underlying FileDescriptor
-            // in its finalizer.
-            iface = tnm.createTapInterface()
         }
+        // Keeping a reference to the test interface also makes sure the ParcelFileDescriptor
+        // does not go out of scope, which would cause it to close the underlying FileDescriptor
+        // in its finalizer.
+        iface = testInterfaceRule.createTapInterface()
 
         handlerThread.start()
         reader = TapPacketReader(
@@ -147,8 +142,6 @@
         handlerThread.threadHandler.post { reader.stop() }
         handlerThread.quitSafely()
         handlerThread.join()
-
-        iface.fileDescriptor.close()
     }
 
     @Test
diff --git a/tests/cts/net/src/android/net/cts/NsdManagerDownstreamTetheringTest.kt b/tests/cts/net/src/android/net/cts/NsdManagerDownstreamTetheringTest.kt
index f45f881..24af42b 100644
--- a/tests/cts/net/src/android/net/cts/NsdManagerDownstreamTetheringTest.kt
+++ b/tests/cts/net/src/android/net/cts/NsdManagerDownstreamTetheringTest.kt
@@ -17,19 +17,21 @@
 
 import android.net.EthernetTetheringTestBase
 import android.net.LinkAddress
-import android.net.TestNetworkInterface
 import android.net.TetheringManager.CONNECTIVITY_SCOPE_LOCAL
 import android.net.TetheringManager.TETHERING_ETHERNET
 import android.net.TetheringManager.TetheringRequest
+import android.net.cts.util.EthernetTestInterface
 import android.net.nsd.NsdManager
 import android.os.Build
+import android.os.Handler
+import android.os.HandlerThread
 import android.platform.test.annotations.AppModeFull
 import androidx.test.filters.SmallTest
+import com.android.testutils.AutoCloseTestInterfaceRule
 import com.android.testutils.ConnectivityModuleTest
 import com.android.testutils.DevSdkIgnoreRule
 import com.android.testutils.DevSdkIgnoreRunner
 import com.android.testutils.NsdDiscoveryRecord
-import com.android.testutils.TapPacketReader
 import com.android.testutils.pollForQuery
 import com.android.testutils.tryTest
 import java.util.Random
@@ -38,9 +40,12 @@
 import org.junit.After
 import org.junit.Assume.assumeFalse
 import org.junit.Before
+import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 
+private const val TAG = "NsdManagerDownstreamTetheringTest"
+
 @RunWith(DevSdkIgnoreRunner::class)
 @SmallTest
 @ConnectivityModuleTest
@@ -50,14 +55,29 @@
     private val nsdManager by lazy { context.getSystemService(NsdManager::class.java)!! }
     private val serviceType = "_nmt%09d._tcp".format(Random().nextInt(1_000_000_000))
 
+    private val handlerThread = HandlerThread("$TAG thread").apply { start() }
+    private val handler = Handler(handlerThread.looper)
+    private lateinit var downstreamIface: EthernetTestInterface
+    private var tetheringEventCallback: MyTetheringEventCallback? = null
+
+    @get:Rule
+    val testInterfaceRule = AutoCloseTestInterfaceRule(context)
+
     @Before
     override fun setUp() {
         super.setUp()
-        setIncludeTestInterfaces(true)
+        val iface = testInterfaceRule.createTapInterface()
+        downstreamIface = EthernetTestInterface(context, handler, iface)
     }
 
     @After
     override fun tearDown() {
+        if (::downstreamIface.isInitialized) {
+            downstreamIface.destroy()
+        }
+        handlerThread.quitSafely()
+        handlerThread.join()
+        maybeUnregisterTetheringEventCallback(tetheringEventCallback)
         super.tearDown()
     }
 
@@ -65,16 +85,11 @@
     fun testMdnsDiscoveryCanSendPacketOnLocalOnlyDownstreamTetheringInterface() {
         assumeFalse(isInterfaceForTetheringAvailable())
 
-        var downstreamIface: TestNetworkInterface? = null
-        var tetheringEventCallback: MyTetheringEventCallback? = null
-        var downstreamReader: TapPacketReader? = null
-
         val discoveryRecord = NsdDiscoveryRecord()
 
         tryTest {
-            downstreamIface = createTestInterface()
             val iface = mTetheredInterfaceRequester.getInterface()
-            assertEquals(iface, downstreamIface?.interfaceName)
+            assertEquals(downstreamIface.name, iface)
             val request = TetheringRequest.Builder(TETHERING_ETHERNET)
                 .setConnectivityScope(CONNECTIVITY_SCOPE_LOCAL).build()
             tetheringEventCallback = enableEthernetTethering(
@@ -83,23 +98,15 @@
             ).apply {
                 awaitInterfaceLocalOnly()
             }
-            // This shouldn't be flaky because the TAP interface will buffer all packets even
-            // before the reader is started.
-            downstreamReader = makePacketReader(downstreamIface)
+            val downstreamReader = downstreamIface.packetReader
             waitForRouterAdvertisement(downstreamReader, iface, WAIT_RA_TIMEOUT_MS)
 
             nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD, discoveryRecord)
             discoveryRecord.expectCallback<NsdDiscoveryRecord.DiscoveryEvent.DiscoveryStarted>()
-            assertNotNull(downstreamReader?.pollForQuery("$serviceType.local", 12 /* type PTR */))
+            assertNotNull(downstreamReader.pollForQuery("$serviceType.local", 12 /* type PTR */))
         } cleanupStep {
             nsdManager.stopServiceDiscovery(discoveryRecord)
             discoveryRecord.expectCallback<NsdDiscoveryRecord.DiscoveryEvent.DiscoveryStopped>()
-        } cleanupStep {
-            maybeStopTapPacketReader(downstreamReader)
-        } cleanupStep {
-            maybeCloseTestInterface(downstreamIface)
-        } cleanup {
-            maybeUnregisterTetheringEventCallback(tetheringEventCallback)
         }
     }
 
@@ -107,16 +114,11 @@
     fun testMdnsDiscoveryWorkOnTetheringInterface() {
         assumeFalse(isInterfaceForTetheringAvailable())
 
-        var downstreamIface: TestNetworkInterface? = null
-        var tetheringEventCallback: MyTetheringEventCallback? = null
-        var downstreamReader: TapPacketReader? = null
-
         val discoveryRecord = NsdDiscoveryRecord()
 
         tryTest {
-            downstreamIface = createTestInterface()
             val iface = mTetheredInterfaceRequester.getInterface()
-            assertEquals(iface, downstreamIface?.interfaceName)
+            assertEquals(downstreamIface.name, iface)
 
             val localAddr = LinkAddress("192.0.2.3/28")
             val clientAddr = LinkAddress("192.0.2.2/28")
@@ -130,23 +132,14 @@
                 awaitInterfaceTethered()
             }
 
-            val fd = downstreamIface?.fileDescriptor?.fileDescriptor
-            assertNotNull(fd)
-            downstreamReader = makePacketReader(fd, getMTU(downstreamIface))
-
             nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD, discoveryRecord)
             discoveryRecord.expectCallback<NsdDiscoveryRecord.DiscoveryEvent.DiscoveryStarted>()
-            assertNotNull(downstreamReader?.pollForQuery("$serviceType.local", 12 /* type PTR */))
+            val downstreamReader = downstreamIface.packetReader
+            assertNotNull(downstreamReader.pollForQuery("$serviceType.local", 12 /* type PTR */))
             // TODO: Add another test to check packet reply can trigger serviceFound.
         } cleanupStep {
             nsdManager.stopServiceDiscovery(discoveryRecord)
             discoveryRecord.expectCallback<NsdDiscoveryRecord.DiscoveryEvent.DiscoveryStopped>()
-        } cleanupStep {
-            maybeStopTapPacketReader(downstreamReader)
-        } cleanupStep {
-            maybeCloseTestInterface(downstreamIface)
-        } cleanup {
-            maybeUnregisterTetheringEventCallback(tetheringEventCallback)
         }
     }
 }
diff --git a/tests/cts/net/src/android/net/cts/NsdManagerTest.kt b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
index be80787..c71d925 100644
--- a/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
+++ b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
@@ -105,7 +105,6 @@
 import com.android.testutils.TestableNetworkAgent
 import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnNetworkCreated
 import com.android.testutils.TestableNetworkCallback
-import com.android.testutils.assertContainsExactly
 import com.android.testutils.assertEmpty
 import com.android.testutils.filters.CtsNetTestCasesMaxTargetSdk30
 import com.android.testutils.filters.CtsNetTestCasesMaxTargetSdk33
@@ -128,8 +127,10 @@
 import java.util.Random
 import java.util.concurrent.Executor
 import kotlin.math.min
+import kotlin.test.assertContentEquals
 import kotlin.test.assertEquals
 import kotlin.test.assertFailsWith
+import kotlin.test.assertNotEquals
 import kotlin.test.assertNotNull
 import kotlin.test.assertNull
 import kotlin.test.fail
@@ -142,10 +143,10 @@
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
-import kotlin.test.assertNotEquals
 
 private const val TAG = "NsdManagerTest"
 private const val TIMEOUT_MS = 2000L
+
 // Registration may take a long time if there are devices with the same hostname on the network,
 // as the device needs to try another name and probe again. This is especially true since when using
 // mdnsresponder the usual hostname is "Android", and on conflict "Android-2", "Android-3", ... are
@@ -187,11 +188,12 @@
     private val customHostname = "NsdTestHost%09d".format(Random().nextInt(1_000_000_000))
     private val customHostname2 = "NsdTestHost%09d".format(Random().nextInt(1_000_000_000))
     private val publicKey = hexStringToByteArray(
-            "0201030dc141d0637960b98cbc12cfca"
-                    + "221d2879dac26ee5b460e9007c992e19"
-                    + "02d897c391b03764d448f7d0c772fdb0"
-                    + "3b1d9d6d52ff8886769e8e2362513565"
-                    + "270962d3")
+            "0201030dc141d0637960b98cbc12cfca" +
+                    "221d2879dac26ee5b460e9007c992e19" +
+                    "02d897c391b03764d448f7d0c772fdb0" +
+                    "3b1d9d6d52ff8886769e8e2362513565" +
+                    "270962d3"
+    )
     private val handlerThread = HandlerThread(NsdManagerTest::class.java.simpleName)
     private val ctsNetUtils by lazy{ CtsNetUtils(context) }
 
@@ -243,11 +245,14 @@
         val iface = tnm.createTapInterface()
         val cb = TestableNetworkCallback()
         val testNetworkSpecifier = TestNetworkSpecifier(iface.interfaceName)
-        cm.requestNetwork(NetworkRequest.Builder()
+        cm.requestNetwork(
+            NetworkRequest.Builder()
                 .removeCapability(NET_CAPABILITY_TRUSTED)
                 .addTransportType(TRANSPORT_TEST)
                 .setNetworkSpecifier(testNetworkSpecifier)
-                .build(), cb)
+                .build(),
+            cb
+        )
         val agent = registerTestNetworkAgent(iface.interfaceName)
         val network = agent.network ?: fail("Registered agent should have a network")
 
@@ -267,12 +272,17 @@
         val lp = LinkProperties().apply {
             interfaceName = ifaceName
         }
-        val agent = TestableNetworkAgent(context, handlerThread.looper,
+        val agent = TestableNetworkAgent(
+            context,
+            handlerThread.looper,
                 NetworkCapabilities().apply {
                     removeCapability(NET_CAPABILITY_TRUSTED)
                     addTransportType(TRANSPORT_TEST)
                     setNetworkSpecifier(TestNetworkSpecifier(ifaceName))
-                }, lp, NetworkAgentConfig.Builder().build())
+                },
+            lp,
+            NetworkAgentConfig.Builder().build()
+        )
         val network = agent.register()
         agent.markConnected()
         agent.expectCallback<OnNetworkCreated>()
@@ -346,15 +356,19 @@
                 Triple(null, null, "null key"),
                 Triple("", null, "empty key"),
                 Triple(string256, null, "key with 256 characters"),
-                Triple("key", string256.substring(3),
-                        "key+value combination with more than 255 characters"),
+                Triple(
+                    "key",
+                    string256.substring(3),
+                        "key+value combination with more than 255 characters"
+                ),
                 Triple("key", string256.substring(4), "key+value combination with 255 characters"),
                 Triple("\u0019", null, "key with invalid character"),
                 Triple("=", null, "key with invalid character"),
                 Triple("\u007f", null, "key with invalid character")
         ).forEach {
             assertFailsWith<IllegalArgumentException>(
-                    "Setting invalid ${it.third} unexpectedly succeeded") {
+                    "Setting invalid ${it.third} unexpectedly succeeded"
+            ) {
                 si.setAttribute(it.first, it.second)
             }
         }
@@ -377,7 +391,8 @@
         // Test registering without an Executor
         nsdManager.registerService(si, NsdManager.PROTOCOL_DNS_SD, registrationRecord)
         val registeredInfo = registrationRecord.expectCallback<ServiceRegistered>(
-                REGISTRATION_TIMEOUT_MS).serviceInfo
+                REGISTRATION_TIMEOUT_MS
+        ).serviceInfo
 
         // Only service name is included in ServiceRegistered callbacks
         assertNull(registeredInfo.serviceType)
@@ -392,7 +407,9 @@
 
         // Expect a service record to be discovered
         val foundInfo = discoveryRecord.waitForServiceDiscovered(
-                registeredInfo.serviceName, serviceType)
+                registeredInfo.serviceName,
+            serviceType
+        )
 
         // Test resolving without an Executor
         val resolveRecord = NsdResolveRecord()
@@ -407,8 +424,10 @@
         assertNull(resolvedService.attributes["booleanAttr"])
         assertEquals("value", resolvedService.attributes["keyValueAttr"].utf8ToString())
         assertEquals("=", resolvedService.attributes["keyEqualsAttr"].utf8ToString())
-        assertEquals(" value ",
-                resolvedService.attributes[" whiteSpaceKeyValueAttr "].utf8ToString())
+        assertEquals(
+            " value ",
+                resolvedService.attributes[" whiteSpaceKeyValueAttr "].utf8ToString()
+        )
         assertEquals(string256.substring(9), resolvedService.attributes["longkey"].utf8ToString())
         assertArrayEquals(testByteArray, resolvedService.attributes["binaryDataAttr"])
         assertTrue(resolvedService.attributes.containsKey("nullBinaryDataAttr"))
@@ -442,12 +461,15 @@
         val registrationRecord2 = NsdRegistrationRecord()
         nsdManager.registerService(si2, NsdManager.PROTOCOL_DNS_SD, registrationRecord2)
         val registeredInfo2 = registrationRecord2.expectCallback<ServiceRegistered>(
-                REGISTRATION_TIMEOUT_MS).serviceInfo
+                REGISTRATION_TIMEOUT_MS
+        ).serviceInfo
 
         // Expect a service record to be discovered (and filter the ones
         // that are unrelated to this test)
         val foundInfo2 = discoveryRecord.waitForServiceDiscovered(
-                registeredInfo2.serviceName, serviceType)
+                registeredInfo2.serviceName,
+            serviceType
+        )
 
         // Resolve the service
         val resolveRecord2 = NsdResolveRecord()
@@ -950,7 +972,8 @@
         // when the compat change is disabled.
         // Note that before T the compat constant had a different int value.
         assertFalse(CompatChanges.isChangeEnabled(
-                ConnectivityCompatChanges.RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER))
+                ConnectivityCompatChanges.RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER
+        ))
     }
 
     @Test
@@ -1023,7 +1046,9 @@
         // onStopResolutionFailed on the record directly then verify it is received.
         val resolveRecord = NsdResolveRecord()
         resolveRecord.onStopResolutionFailed(
-                NsdServiceInfo(), NsdManager.FAILURE_OPERATION_NOT_RUNNING)
+                NsdServiceInfo(),
+            NsdManager.FAILURE_OPERATION_NOT_RUNNING
+        )
         val failedCb = resolveRecord.expectCallback<StopResolutionFailed>()
         assertEquals(NsdManager.FAILURE_OPERATION_NOT_RUNNING, failedCb.errorCode)
     }
@@ -1274,15 +1299,22 @@
 
         val si = makeTestServiceInfo(testNetwork1.network)
 
-        val packetReader = TapPacketReader(Handler(handlerThread.looper),
-                testNetwork1.iface.fileDescriptor.fileDescriptor, 1500 /* maxPacketSize */)
+        val packetReader = TapPacketReader(
+            Handler(handlerThread.looper),
+                testNetwork1.iface.fileDescriptor.fileDescriptor,
+            1500 /* maxPacketSize */
+        )
         packetReader.startAsyncForTest()
         handlerThread.waitForIdle(TIMEOUT_MS)
 
         // Register service on testNetwork1
         val registrationRecord = NsdRegistrationRecord()
-        nsdManager.registerService(si, NsdManager.PROTOCOL_DNS_SD, { it.run() },
-                registrationRecord)
+        nsdManager.registerService(
+            si,
+            NsdManager.PROTOCOL_DNS_SD,
+            { it.run() },
+                registrationRecord
+        )
 
         tryTest {
             assertNotNull(packetReader.pollForProbe(serviceName, serviceType),
@@ -1313,15 +1345,22 @@
                     parseNumericAddress("2001:db8::3"))
         }
 
-        val packetReader = TapPacketReader(Handler(handlerThread.looper),
-                testNetwork1.iface.fileDescriptor.fileDescriptor, 1500 /* maxPacketSize */)
+        val packetReader = TapPacketReader(
+            Handler(handlerThread.looper),
+                testNetwork1.iface.fileDescriptor.fileDescriptor,
+            1500 /* maxPacketSize */
+        )
         packetReader.startAsyncForTest()
         handlerThread.waitForIdle(TIMEOUT_MS)
 
         // Register service on testNetwork1
         val registrationRecord = NsdRegistrationRecord()
-        nsdManager.registerService(si, NsdManager.PROTOCOL_DNS_SD, { it.run() },
-                registrationRecord)
+        nsdManager.registerService(
+            si,
+            NsdManager.PROTOCOL_DNS_SD,
+            { it.run() },
+                registrationRecord
+        )
 
         tryTest {
             assertNotNull(packetReader.pollForProbe(serviceName, serviceType),
@@ -1352,15 +1391,22 @@
             hostname = customHostname
         }
 
-        val packetReader = TapPacketReader(Handler(handlerThread.looper),
-                testNetwork1.iface.fileDescriptor.fileDescriptor, 1500 /* maxPacketSize */)
+        val packetReader = TapPacketReader(
+            Handler(handlerThread.looper),
+                testNetwork1.iface.fileDescriptor.fileDescriptor,
+            1500 /* maxPacketSize */
+        )
         packetReader.startAsyncForTest()
         handlerThread.waitForIdle(TIMEOUT_MS)
 
         // Register service on testNetwork1
         val registrationRecord = NsdRegistrationRecord()
-        nsdManager.registerService(si, NsdManager.PROTOCOL_DNS_SD, { it.run() },
-                registrationRecord)
+        nsdManager.registerService(
+            si,
+            NsdManager.PROTOCOL_DNS_SD,
+            { it.run() },
+                registrationRecord
+        )
 
         tryTest {
             assertNotNull(packetReader.pollForProbe(serviceName, serviceType),
@@ -1392,8 +1438,11 @@
         val registrationRecord = NsdRegistrationRecord()
         val discoveryRecord = NsdDiscoveryRecord()
         val registeredService = registerService(registrationRecord, si)
-        val packetReader = TapPacketReader(Handler(handlerThread.looper),
-                testNetwork1.iface.fileDescriptor.fileDescriptor, 1500 /* maxPacketSize */)
+        val packetReader = TapPacketReader(
+            Handler(handlerThread.looper),
+                testNetwork1.iface.fileDescriptor.fileDescriptor,
+            1500 /* maxPacketSize */
+        )
         packetReader.startAsyncForTest()
         handlerThread.waitForIdle(TIMEOUT_MS)
 
@@ -1471,7 +1520,9 @@
         val registeredService = registerService(registrationRecord, si)
         val packetReader = TapPacketReader(
                 Handler(handlerThread.looper),
-                testNetwork1.iface.fileDescriptor.fileDescriptor, 1500 /* maxPacketSize */)
+                testNetwork1.iface.fileDescriptor.fileDescriptor,
+            1500 /* maxPacketSize */
+        )
         packetReader.startAsyncForTest()
         handlerThread.waitForIdle(TIMEOUT_MS)
 
@@ -1501,11 +1552,11 @@
             val newRegistration =
                     registrationRecord
                             .expectCallbackEventually<ServiceRegistered>(REGISTRATION_TIMEOUT_MS) {
-                                it.serviceInfo.serviceName == serviceName
-                                        && it.serviceInfo.hostname.let { hostname ->
-                                    hostname != null
-                                            && hostname.startsWith(customHostname)
-                                            && hostname != customHostname
+                                it.serviceInfo.serviceName == serviceName &&
+                                        it.serviceInfo.hostname.let { hostname ->
+                                    hostname != null &&
+                                            hostname.startsWith(customHostname) &&
+                                            hostname != customHostname
                                 }
                             }
 
@@ -1536,8 +1587,11 @@
         val registrationRecord = NsdRegistrationRecord()
         val discoveryRecord = NsdDiscoveryRecord()
         val registeredService = registerService(registrationRecord, si)
-        val packetReader = TapPacketReader(Handler(handlerThread.looper),
-                testNetwork1.iface.fileDescriptor.fileDescriptor, 1500 /* maxPacketSize */)
+        val packetReader = TapPacketReader(
+            Handler(handlerThread.looper),
+                testNetwork1.iface.fileDescriptor.fileDescriptor,
+            1500 /* maxPacketSize */
+        )
         packetReader.startAsyncForTest()
         handlerThread.waitForIdle(TIMEOUT_MS)
 
@@ -1576,13 +1630,21 @@
     fun testDiscoveryWithPtrOnlyResponse_ServiceIsFound() {
         // Register service on testNetwork1
         val discoveryRecord = NsdDiscoveryRecord()
-        val packetReader = TapPacketReader(Handler(handlerThread.looper),
-                testNetwork1.iface.fileDescriptor.fileDescriptor, 1500 /* maxPacketSize */)
+        val packetReader = TapPacketReader(
+            Handler(handlerThread.looper),
+                testNetwork1.iface.fileDescriptor.fileDescriptor,
+            1500 /* maxPacketSize */
+        )
         packetReader.startAsyncForTest()
         handlerThread.waitForIdle(TIMEOUT_MS)
 
-        nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD,
-                testNetwork1.network, { it.run() }, discoveryRecord)
+        nsdManager.discoverServices(
+            serviceType,
+            NsdManager.PROTOCOL_DNS_SD,
+                testNetwork1.network,
+            { it.run() },
+            discoveryRecord
+        )
 
         tryTest {
             discoveryRecord.expectCallback<DiscoveryStarted>()
@@ -1626,8 +1688,10 @@
     fun testResolveWhenServerSendsNoAdditionalRecord() {
         // Resolve service on testNetwork1
         val resolveRecord = NsdResolveRecord()
-        val packetReader = TapPacketReader(Handler(handlerThread.looper),
-                testNetwork1.iface.fileDescriptor.fileDescriptor, 1500 /* maxPacketSize */
+        val packetReader = TapPacketReader(
+            Handler(handlerThread.looper),
+                testNetwork1.iface.fileDescriptor.fileDescriptor,
+            1500 /* maxPacketSize */
         )
         packetReader.startAsyncForTest()
         handlerThread.waitForIdle(TIMEOUT_MS)
@@ -1651,16 +1715,21 @@
                 rdata='testkey=testvalue')
         ))).hex()
          */
-        val srvTxtResponsePayload = HexDump.hexStringToByteArray("000084000000000200000000104" +
+        val srvTxtResponsePayload = HexDump.hexStringToByteArray(
+            "000084000000000200000000104" +
                 "e7364546573743132333435363738390d5f6e6d74313233343536373839045f746370056c6f6" +
                 "3616c0000218001000000780011000000007a020874657374686f7374c030c00c00100001000" +
-                "00078001211746573746b65793d7465737476616c7565")
+                "00078001211746573746b65793d7465737476616c7565"
+        )
         replaceServiceNameAndTypeWithTestSuffix(srvTxtResponsePayload)
         packetReader.sendResponse(buildMdnsPacket(srvTxtResponsePayload))
 
         val testHostname = "testhost.local"
-        val addressQuery = packetReader.pollForQuery(testHostname,
-            DnsResolver.TYPE_A, DnsResolver.TYPE_AAAA)
+        val addressQuery = packetReader.pollForQuery(
+            testHostname,
+            DnsResolver.TYPE_A,
+            DnsResolver.TYPE_AAAA
+        )
         assertNotNull(addressQuery)
 
         /*
@@ -1672,9 +1741,11 @@
                 rdata='2001:db8::123')
         ))).hex()
          */
-        val addressPayload = HexDump.hexStringToByteArray("0000840000000002000000000874657374" +
+        val addressPayload = HexDump.hexStringToByteArray(
+            "0000840000000002000000000874657374" +
                 "686f7374056c6f63616c0000010001000000780004c000027bc00c001c000100000078001020" +
-                "010db8000000000000000000000123")
+                "010db8000000000000000000000123"
+        )
         packetReader.sendResponse(buildMdnsPacket(addressPayload))
 
         val serviceResolved = resolveRecord.expectCallback<ServiceResolved>()
@@ -1688,7 +1759,8 @@
         }
         assertEquals(
                 setOf(parseNumericAddress("192.0.2.123"), parseNumericAddress("2001:db8::123")),
-                serviceResolved.serviceInfo.hostAddresses.toSet())
+                serviceResolved.serviceInfo.hostAddresses.toSet()
+        )
     }
 
     @Test
@@ -1715,7 +1787,7 @@
             scapy.raw(scapy.DNS(rd=0, qr=0, aa=0, qd =
                     scapy.DNSQR(qname='_nmt123456789._tcp.local', qtype='PTR', qclass=0x8001)
             )).hex()
-            */
+             */
             val mdnsPayload = HexDump.hexStringToByteArray("0000000000010000000000000d5f6e6d74313" +
                     "233343536373839045f746370056c6f63616c00000c8001")
             replaceServiceNameAndTypeWithTestSuffix(mdnsPayload)
@@ -1771,7 +1843,7 @@
                     an = scapy.DNSRR(rrname='_nmt123456789._tcp.local', type='PTR', ttl=4500,
                             rdata='NsdTest123456789._nmt123456789._tcp.local')
             )).hex()
-            */
+             */
             val query = HexDump.hexStringToByteArray("0000000000020001000000000d5f6e6d74313233343" +
                     "536373839045f746370056c6f63616c00000c8001104e7364546573743132333435363738390" +
                     "d5f6e6d74313233343536373839045f746370056c6f63616c00001080010d5f6e6d743132333" +
@@ -1806,7 +1878,7 @@
                     an = scapy.DNSRR(rrname='_nmt123456789._tcp.local', type='PTR', ttl=2150,
                             rdata='NsdTest123456789._nmt123456789._tcp.local')
             )).hex()
-            */
+             */
             val query2 = HexDump.hexStringToByteArray("0000000000020001000000000d5f6e6d7431323334" +
                     "3536373839045f746370056c6f63616c00000c8001104e736454657374313233343536373839" +
                     "0d5f6e6d74313233343536373839045f746370056c6f63616c00001080010d5f6e6d74313233" +
@@ -1858,7 +1930,7 @@
                     scapy.DNSQR(qname='NsdTest123456789._nmt123456789._tcp.local', qtype='TXT',
                             qclass=0x8001)
             )).hex()
-            */
+             */
             val query = HexDump.hexStringToByteArray("0000020000020000000000000d5f6e6d74313233343" +
                     "536373839045f746370056c6f63616c00000c8001104e7364546573743132333435363738390" +
                     "d5f6e6d74313233343536373839045f746370056c6f63616c0000108001")
@@ -1870,7 +1942,7 @@
                     an = scapy.DNSRR(rrname='_test._tcp.local', type='PTR', ttl=4500,
                             rdata='NsdTest._test._tcp.local')
             )).hex()
-            */
+             */
             val knownAnswer1 = HexDump.hexStringToByteArray("000002000000000100000000055f74657374" +
                     "045f746370056c6f63616c00000c000100001194001a074e736454657374055f74657374045f" +
                     "746370056c6f63616c00")
@@ -1882,7 +1954,7 @@
                     an = scapy.DNSRR(rrname='_nmt123456789._tcp.local', type='PTR', ttl=4500,
                             rdata='NsdTest123456789._nmt123456789._tcp.local')
             )).hex()
-            */
+             */
             val knownAnswer2 = HexDump.hexStringToByteArray("0000000000000001000000000d5f6e6d7431" +
                     "3233343536373839045f746370056c6f63616c00000c000100001194002b104e736454657374" +
                     "3132333435363738390d5f6e6d74313233343536373839045f746370056c6f63616c00")
@@ -1919,13 +1991,21 @@
 
         // Register service on testNetwork1
         val discoveryRecord = NsdDiscoveryRecord()
-        val packetReader = TapPacketReader(Handler(handlerThread.looper),
-                testNetwork1.iface.fileDescriptor.fileDescriptor, 1500 /* maxPacketSize */)
+        val packetReader = TapPacketReader(
+            Handler(handlerThread.looper),
+                testNetwork1.iface.fileDescriptor.fileDescriptor,
+            1500 /* maxPacketSize */
+        )
         packetReader.startAsyncForTest()
         handlerThread.waitForIdle(TIMEOUT_MS)
 
-        nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD,
-                testNetwork1.network, { it.run() }, discoveryRecord)
+        nsdManager.discoverServices(
+            serviceType,
+            NsdManager.PROTOCOL_DNS_SD,
+                testNetwork1.network,
+            { it.run() },
+            discoveryRecord
+        )
 
         tryTest {
             discoveryRecord.expectCallback<DiscoveryStarted>()
@@ -1987,10 +2067,12 @@
         val hostAddresses1 = listOf(
                 parseNumericAddress("192.0.2.23"),
                 parseNumericAddress("2001:db8::1"),
-                parseNumericAddress("2001:db8::2"))
+                parseNumericAddress("2001:db8::2")
+        )
         val hostAddresses2 = listOf(
                 parseNumericAddress("192.0.2.24"),
-                parseNumericAddress("2001:db8::3"))
+                parseNumericAddress("2001:db8::3")
+        )
         val si1 = NsdServiceInfo().also {
             it.network = testNetwork1.network
             it.serviceName = serviceName
@@ -2054,10 +2136,12 @@
         val hostAddresses1 = listOf(
                 parseNumericAddress("192.0.2.23"),
                 parseNumericAddress("2001:db8::1"),
-                parseNumericAddress("2001:db8::2"))
+                parseNumericAddress("2001:db8::2")
+        )
         val hostAddresses2 = listOf(
                 parseNumericAddress("192.0.2.24"),
-                parseNumericAddress("2001:db8::3"))
+                parseNumericAddress("2001:db8::3")
+        )
         val si1 = NsdServiceInfo().also {
             it.network = testNetwork1.network
             it.hostname = customHostname
@@ -2105,7 +2189,8 @@
         val hostAddresses = listOf(
                 parseNumericAddress("192.0.2.23"),
                 parseNumericAddress("2001:db8::1"),
-                parseNumericAddress("2001:db8::2"))
+                parseNumericAddress("2001:db8::2")
+        )
         val si1 = NsdServiceInfo().also {
             it.network = testNetwork1.network
             it.serviceType = serviceType
@@ -2270,8 +2355,11 @@
             it.port = TEST_PORT
             it.publicKey = publicKey
         }
-        val packetReader = TapPacketReader(Handler(handlerThread.looper),
-                testNetwork1.iface.fileDescriptor.fileDescriptor, 1500 /* maxPacketSize */)
+        val packetReader = TapPacketReader(
+            Handler(handlerThread.looper),
+                testNetwork1.iface.fileDescriptor.fileDescriptor,
+            1500 /* maxPacketSize */
+        )
         packetReader.startAsyncForTest()
         handlerThread.waitForIdle(TIMEOUT_MS)
 
@@ -2322,8 +2410,11 @@
                     parseNumericAddress("2001:db8::2"))
             it.publicKey = publicKey
         }
-        val packetReader = TapPacketReader(Handler(handlerThread.looper),
-                testNetwork1.iface.fileDescriptor.fileDescriptor, 1500 /* maxPacketSize */)
+        val packetReader = TapPacketReader(
+            Handler(handlerThread.looper),
+                testNetwork1.iface.fileDescriptor.fileDescriptor,
+            1500 /* maxPacketSize */
+        )
         packetReader.startAsyncForTest()
         handlerThread.waitForIdle(TIMEOUT_MS)
 
@@ -2376,8 +2467,11 @@
             it.hostAddresses = listOf()
             it.publicKey = publicKey
         }
-        val packetReader = TapPacketReader(Handler(handlerThread.looper),
-            testNetwork1.iface.fileDescriptor.fileDescriptor, 1500 /* maxPacketSize */)
+        val packetReader = TapPacketReader(
+            Handler(handlerThread.looper),
+            testNetwork1.iface.fileDescriptor.fileDescriptor,
+            1500 /* maxPacketSize */
+        )
         packetReader.startAsyncForTest()
         handlerThread.waitForIdle(TIMEOUT_MS)
 
@@ -2430,10 +2524,20 @@
         val discoveryRecord1 = NsdDiscoveryRecord()
         val discoveryRecord2 = NsdDiscoveryRecord()
         val discoveryRecord3 = NsdDiscoveryRecord()
-        nsdManager.discoverServices("_test1._tcp", NsdManager.PROTOCOL_DNS_SD,
-                testNetwork1.network, { it.run() }, discoveryRecord1)
-        nsdManager.discoverServices("_test2._tcp", NsdManager.PROTOCOL_DNS_SD,
-                testNetwork1.network, { it.run() }, discoveryRecord2)
+        nsdManager.discoverServices(
+            "_test1._tcp",
+            NsdManager.PROTOCOL_DNS_SD,
+                testNetwork1.network,
+            { it.run() },
+            discoveryRecord1
+        )
+        nsdManager.discoverServices(
+            "_test2._tcp",
+            NsdManager.PROTOCOL_DNS_SD,
+                testNetwork1.network,
+            { it.run() },
+            discoveryRecord2
+        )
         nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD, discoveryRecord3)
 
         tryTest {
@@ -2471,6 +2575,44 @@
         }
     }
 
+    @Test
+    fun testAdvertiseServiceWithNoAttributes_TxtRecordIstNotEmpty() {
+        deviceConfigRule.setConfig(
+            NAMESPACE_TETHERING,
+            "test_nsd_avoid_advertising_empty_txt_records",
+            "1"
+        )
+        val packetReader = TapPacketReader(
+            Handler(handlerThread.looper),
+            testNetwork1.iface.fileDescriptor.fileDescriptor,
+            1500 /* maxPacketSize */
+        )
+        packetReader.startAsyncForTest()
+        handlerThread.waitForIdle(TIMEOUT_MS)
+
+        // Test behavior described in RFC6763 6.1: empty TXT records are not allowed, but TXT
+        // records with a zero length string are equivalent.
+        val si = makeTestServiceInfo(testNetwork1.network)
+        // Register service on testNetwork1
+        val registrationRecord = NsdRegistrationRecord()
+        registerService(registrationRecord, si)
+
+        tryTest {
+            val announcement =
+                packetReader.pollForReply("$serviceName.$serviceType.local", DnsResolver.TYPE_TXT)
+            assertNotNull(announcement)
+            val txtRecords = announcement.records[ANSECTION].filter {
+                it.nsType == DnsResolver.TYPE_TXT
+            }
+            assertEquals(1, txtRecords.size)
+            // The TXT record should contain as single zero
+            assertContentEquals(byteArrayOf(0), txtRecords[0].rr)
+        } cleanup {
+            nsdManager.unregisterService(registrationRecord)
+            registrationRecord.expectCallback<ServiceUnregistered>()
+        }
+    }
+
     private fun hasServiceTypeClientsForNetwork(clients: List<String>, network: Network): Boolean {
         return clients.any { client -> client.substring(
                 client.indexOf("network=") + "network=".length,
@@ -2489,11 +2631,16 @@
      */
     private fun getServiceTypeClients(): List<String> {
         return SystemUtil.runShellCommand(
-                InstrumentationRegistry.getInstrumentation(), "dumpsys servicediscovery")
+                InstrumentationRegistry.getInstrumentation(),
+            "dumpsys servicediscovery"
+        )
                 .split("\n").mapNotNull { line ->
                     line.indexOf("ServiceTypeClient:").let { idx ->
-                        if (idx == -1) null
-                        else line.substring(idx)
+                        if (idx == -1) {
+                            null
+                        } else {
+                            line.substring(idx)
+                        }
                     }
                 }
     }
@@ -2506,9 +2653,11 @@
                     rclass=0x8001, port=31234, target='conflict.local', ttl=120)
         )).hex()
          */
-        val mdnsPayload = HexDump.hexStringToByteArray("000084000000000100000000104e736454657" +
+        val mdnsPayload = HexDump.hexStringToByteArray(
+            "000084000000000100000000104e736454657" +
                 "3743132333435363738390d5f6e6d74313233343536373839045f746370056c6f63616c00002" +
-                "18001000000780016000000007a0208636f6e666c696374056c6f63616c00")
+                "18001000000780016000000007a0208636f6e666c696374056c6f63616c00"
+        )
         replaceServiceNameAndTypeWithTestSuffix(mdnsPayload)
 
         return buildMdnsPacket(mdnsPayload)
@@ -2522,9 +2671,11 @@
                     rdata='2001:db8::321')
         )).hex()
          */
-        val mdnsPayload = HexDump.hexStringToByteArray("000084000000000100000000144e7364" +
+        val mdnsPayload = HexDump.hexStringToByteArray(
+            "000084000000000100000000144e7364" +
                 "54657374486f7374313233343536373839056c6f63616c00001c000100000078001020010db80000" +
-                "00000000000000000321")
+                "00000000000000000321"
+        )
         replaceCustomHostnameWithTestSuffix(mdnsPayload)
 
         return buildMdnsPacket(mdnsPayload)
@@ -2575,22 +2726,29 @@
         mdnsPayload: ByteArray,
         srcAddr: Inet6Address = testSrcAddr
     ): ByteBuffer {
-        val packetBuffer = PacketBuilder.allocate(true /* hasEther */, IPPROTO_IPV6,
-                IPPROTO_UDP, mdnsPayload.size)
+        val packetBuffer = PacketBuilder.allocate(
+            true /* hasEther */,
+            IPPROTO_IPV6,
+                IPPROTO_UDP,
+            mdnsPayload.size
+        )
         val packetBuilder = PacketBuilder(packetBuffer)
         // Multicast ethernet address for IPv6 to ff02::fb
         val multicastEthAddr = MacAddress.fromBytes(
-                byteArrayOf(0x33, 0x33, 0, 0, 0, 0xfb.toByte()))
+                byteArrayOf(0x33, 0x33, 0, 0, 0, 0xfb.toByte())
+        )
         packetBuilder.writeL2Header(
                 MacAddress.fromBytes(byteArrayOf(1, 2, 3, 4, 5, 6)) /* srcMac */,
                 multicastEthAddr,
-                ETH_P_IPV6.toShort())
+                ETH_P_IPV6.toShort()
+        )
         packetBuilder.writeIpv6Header(
                 0x60000000, // version=6, traffic class=0x0, flowlabel=0x0
                 IPPROTO_UDP.toByte(),
                 64 /* hop limit */,
                 srcAddr,
-                multicastIpv6Addr /* dstIp */)
+                multicastIpv6Addr /* dstIp */
+        )
         packetBuilder.writeUdpHeader(MDNS_PORT /* srcPort */, MDNS_PORT /* dstPort */)
         packetBuffer.put(mdnsPayload)
         return packetBuilder.finalizePacket()
diff --git a/tests/cts/net/util/java/android/net/cts/util/EthernetTestInterface.kt b/tests/cts/net/util/java/android/net/cts/util/EthernetTestInterface.kt
new file mode 100644
index 0000000..32d6899
--- /dev/null
+++ b/tests/cts/net/util/java/android/net/cts/util/EthernetTestInterface.kt
@@ -0,0 +1,128 @@
+/*
+ * 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.cts.util
+
+import android.Manifest.permission.NETWORK_SETTINGS
+import android.content.Context
+import android.net.EthernetManager
+import android.net.EthernetManager.InterfaceStateListener
+import android.net.EthernetManager.STATE_ABSENT
+import android.net.EthernetManager.STATE_LINK_UP
+import android.net.IpConfiguration
+import android.net.TestNetworkInterface
+import android.net.cts.util.EthernetTestInterface.EthernetStateListener.CallbackEntry.InterfaceStateChanged
+import android.os.Handler
+import android.util.Log
+import com.android.net.module.util.ArrayTrackRecord
+import com.android.testutils.TapPacketReader
+import com.android.testutils.runAsShell
+import com.android.testutils.waitForIdle
+import java.net.NetworkInterface
+import kotlin.concurrent.Volatile
+import kotlin.test.assertNotNull
+
+private const val TAG = "EthernetTestInterface"
+private const val TIMEOUT_MS = 5_000L
+
+/**
+ * A class to manage the lifecycle of an ethernet interface.
+ *
+ * This class encapsulates creating new tun/tap interfaces and registering them with ethernet
+ * service.
+ */
+class EthernetTestInterface(
+    private val context: Context,
+    private val handler: Handler,
+    val testIface: TestNetworkInterface
+) {
+    private class EthernetStateListener(private val trackedIface: String) : InterfaceStateListener {
+        val events = ArrayTrackRecord<CallbackEntry>().newReadHead()
+
+        sealed class CallbackEntry {
+            data class InterfaceStateChanged(
+                val iface: String,
+                val state: Int,
+                val role: Int,
+                val cfg: IpConfiguration?
+            ) : CallbackEntry()
+        }
+
+        override fun onInterfaceStateChanged(
+            iface: String,
+            state: Int,
+            role: Int,
+            cfg: IpConfiguration?
+        ) {
+            // filter out callbacks for other interfaces
+            if (iface != trackedIface) return
+            events.add(InterfaceStateChanged(iface, state, role, cfg))
+        }
+
+        fun eventuallyExpect(state: Int) {
+            val cb = events.poll(TIMEOUT_MS) { it is InterfaceStateChanged && it.state == state }
+            assertNotNull(cb, "Never received state $state. Got: ${events.backtrace()}")
+        }
+    }
+
+    val name get() = testIface.interfaceName
+    val mtu: Int
+        get() {
+            val nif = NetworkInterface.getByName(name)
+            assertNotNull(nif)
+            return nif.mtu
+        }
+    val packetReader = TapPacketReader(handler, testIface.fileDescriptor.fileDescriptor, mtu)
+    private val listener = EthernetStateListener(name)
+    private val em = context.getSystemService(EthernetManager::class.java)!!
+    @Volatile private var cleanedUp = false
+
+    init{
+        em.addInterfaceStateListener(handler::post, listener)
+        runAsShell(NETWORK_SETTINGS) {
+            em.setIncludeTestInterfaces(true)
+        }
+        // Wait for link up to be processed in EthernetManager before returning.
+        listener.eventuallyExpect(STATE_LINK_UP)
+        handler.post { packetReader.start() }
+        handler.waitForIdle(TIMEOUT_MS)
+    }
+
+    fun destroy() {
+        // packetReader.stop() closes the test interface.
+        handler.post { packetReader.stop() }
+        handler.waitForIdle(TIMEOUT_MS)
+        listener.eventuallyExpect(STATE_ABSENT)
+
+        // setIncludeTestInterfaces() posts on the handler and does not run synchronously. However,
+        // there should be no need for a synchronization mechanism here. If the next test is
+        // bringing up its interface, a RTM_NEWLINK will be put on the back of the handler and is
+        // guaranteed to be in order with (i.e. after) this call, so there is no race here.
+        runAsShell(NETWORK_SETTINGS) {
+            em.setIncludeTestInterfaces(false)
+        }
+        em.removeInterfaceStateListener(listener)
+
+        cleanedUp = true
+    }
+
+    protected fun finalize() {
+        if (!cleanedUp) {
+            Log.wtf(TAG, "destroy() was not called for interface $name.")
+            destroy()
+        }
+    }
+}
diff --git a/tests/mts/Android.bp b/tests/mts/Android.bp
index c118d0a..9d158fd 100644
--- a/tests/mts/Android.bp
+++ b/tests/mts/Android.bp
@@ -40,6 +40,6 @@
     srcs: [
         "bpf_existence_test.cpp",
     ],
-    compile_multilib: "first",
+    compile_multilib: "both",
     min_sdk_version: "30", // Ensure test runs on R and above.
 }
diff --git a/tests/native/connectivity_native_test/Android.bp b/tests/native/connectivity_native_test/Android.bp
index c5088c6..39a08fa 100644
--- a/tests/native/connectivity_native_test/Android.bp
+++ b/tests/native/connectivity_native_test/Android.bp
@@ -21,6 +21,7 @@
     stl: "libc++_static",
     shared_libs: [
         "libbinder_ndk",
+        "libcom.android.tethering.connectivity_native",
         "liblog",
         "libnetutils",
         "libprocessgroup",
@@ -32,7 +33,7 @@
         "libmodules-utils-build",
         "libutils",
     ],
-    compile_multilib: "first",
+    compile_multilib: "both",
 }
 
 filegroup {
diff --git a/tests/native/connectivity_native_test/AndroidTestTemplate.xml b/tests/native/connectivity_native_test/AndroidTestTemplate.xml
index 44e35a9..4ea114c 100644
--- a/tests/native/connectivity_native_test/AndroidTestTemplate.xml
+++ b/tests/native/connectivity_native_test/AndroidTestTemplate.xml
@@ -15,8 +15,10 @@
 <configuration description="Configuration for connectivity {MODULE} tests">
     <option name="test-suite-tag" value="mts" />
     <option name="config-descriptor:metadata" key="mainline-param" value="CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex" />
-    <!-- The tested code is only part of a SDK 30+ module (Tethering) -->
-    <object type="module_controller" class="com.android.tradefed.testtype.suite.module.Sdk30ModuleController" />
+    <!-- The tested library is only usable on U+ -->
+    <object type="module_controller" class="com.android.tradefed.testtype.suite.module.Sdk34ModuleController" />
+    <!-- The tested library is only available with the primary abi -->
+    <option name="config-descriptor:metadata" key="parameter" value="not_multi_abi" />
 
     <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer"/>
     <target_preparer class="com.android.compatibility.common.tradefed.targetprep.FilePusher">
diff --git a/tests/native/connectivity_native_test/connectivity_native_test.cpp b/tests/native/connectivity_native_test/connectivity_native_test.cpp
index f62a30b..faaf150 100644
--- a/tests/native/connectivity_native_test/connectivity_native_test.cpp
+++ b/tests/native/connectivity_native_test/connectivity_native_test.cpp
@@ -14,25 +14,17 @@
  * limitations under the License.
  */
 
+#include <android-modules-utils/sdk_level.h>
 #include <android/binder_manager.h>
 #include <android/binder_process.h>
-#include <android-modules-utils/sdk_level.h>
-#include <cutils/misc.h>  // FIRST_APPLICATION_UID
+#include <connectivity_native.h>
+#include <cutils/misc.h> // FIRST_APPLICATION_UID
 #include <dlfcn.h>
 #include <gtest/gtest.h>
 #include <netinet/in.h>
 
 #include "bpf/BpfUtils.h"
 
-typedef int (*GetPortsBlockedForBind)(in_port_t*, size_t*);
-GetPortsBlockedForBind getPortsBlockedForBind;
-typedef int (*BlockPortForBind)(in_port_t);
-BlockPortForBind blockPortForBind;
-typedef int (*UnblockPortForBind)(in_port_t);
-UnblockPortForBind unblockPortForBind;
-typedef int (*UnblockAllPortsForBind)();
-UnblockAllPortsForBind unblockAllPortsForBind;
-
 class ConnectivityNativeBinderTest : public ::testing::Test {
   public:
     in_port_t mActualBlockedPorts[65535];
@@ -50,29 +42,12 @@
         if (!android::bpf::isAtLeastKernelVersion(5, 4, 0))
             GTEST_SKIP() << "Kernel should be at least 5.4.";
 
-        // Necessary to use dlopen/dlsym since the lib is only available on U and there
-        // is no Sdk34ModuleController in tradefed yet.
-        // TODO: link against the library directly and add Sdk34ModuleController to
-        // AndroidTest.txml when available.
-        void* nativeLib = dlopen("libcom.android.tethering.connectivity_native.so", RTLD_NOW);
-        ASSERT_NE(nullptr, nativeLib);
-        getPortsBlockedForBind = reinterpret_cast<GetPortsBlockedForBind>(
-                dlsym(nativeLib, "AConnectivityNative_getPortsBlockedForBind"));
-        ASSERT_NE(nullptr, getPortsBlockedForBind);
-        blockPortForBind = reinterpret_cast<BlockPortForBind>(
-                dlsym(nativeLib, "AConnectivityNative_blockPortForBind"));
-        ASSERT_NE(nullptr, blockPortForBind);
-        unblockPortForBind = reinterpret_cast<UnblockPortForBind>(
-                dlsym(nativeLib, "AConnectivityNative_unblockPortForBind"));
-        ASSERT_NE(nullptr, unblockPortForBind);
-        unblockAllPortsForBind = reinterpret_cast<UnblockAllPortsForBind>(
-                dlsym(nativeLib, "AConnectivityNative_unblockAllPortsForBind"));
-        ASSERT_NE(nullptr, unblockAllPortsForBind);
-
-        // If there are already ports being blocked on device unblockAllPortsForBind() store
-        // the currently blocked ports and add them back at the end of the test. Do this for
-        // every test case so additional test cases do not forget to add ports back.
-        int err = getPortsBlockedForBind(mActualBlockedPorts, &mActualBlockedPortsCount);
+        // If there are already ports being blocked on device store the
+        // currently blocked ports and add them back at the end of the test. Do
+        // this for every test case so additional test cases do not forget to
+        // add ports back.
+        int err = AConnectivityNative_getPortsBlockedForBind(
+            mActualBlockedPorts, &mActualBlockedPortsCount);
         EXPECT_EQ(err, 0);
         restoreBlockedPorts = true;
     }
@@ -81,8 +56,9 @@
         int err;
         if (mActualBlockedPortsCount > 0 && restoreBlockedPorts) {
             for (int i=0; i < mActualBlockedPortsCount; i++) {
-                err = blockPortForBind(mActualBlockedPorts[i]);
-                EXPECT_EQ(err, 0);
+              err =
+                  AConnectivityNative_blockPortForBind(mActualBlockedPorts[i]);
+              EXPECT_EQ(err, 0);
             }
         }
     }
@@ -99,7 +75,7 @@
         int blockedPort = 0;
         if (blockPort) {
             blockedPort = ntohs(port);
-            err = blockPortForBind(blockedPort);
+            err = AConnectivityNative_blockPortForBind(blockedPort);
             EXPECT_EQ(err, 0);
         }
 
@@ -107,7 +83,7 @@
 
         if (blockPort) {
             EXPECT_EQ(-1, sock3);
-            err = unblockPortForBind(blockedPort);
+            err = AConnectivityNative_unblockPortForBind(blockedPort);
             EXPECT_EQ(err, 0);
         } else {
             EXPECT_NE(-1, sock3);
@@ -197,11 +173,11 @@
 }
 
 TEST_F(ConnectivityNativeBinderTest, BlockPortTwice) {
-    int err = blockPortForBind(5555);
+    int err = AConnectivityNative_blockPortForBind(5555);
     EXPECT_EQ(err, 0);
-    err = blockPortForBind(5555);
+    err = AConnectivityNative_blockPortForBind(5555);
     EXPECT_EQ(err, 0);
-    err = unblockPortForBind(5555);
+    err = AConnectivityNative_unblockPortForBind(5555);
     EXPECT_EQ(err, 0);
 }
 
@@ -210,16 +186,17 @@
     in_port_t blockedPorts[8] = {1, 100, 1220, 1333, 2700, 5555, 5600, 65000};
 
     if (mActualBlockedPortsCount > 0) {
-        err = unblockAllPortsForBind();
+        err = AConnectivityNative_unblockAllPortsForBind();
     }
 
     for (int i : blockedPorts) {
-        err = blockPortForBind(i);
+        err = AConnectivityNative_blockPortForBind(i);
         EXPECT_EQ(err, 0);
     }
     size_t actualBlockedPortsCount = 8;
     in_port_t actualBlockedPorts[actualBlockedPortsCount];
-    err = getPortsBlockedForBind((in_port_t*) actualBlockedPorts, &actualBlockedPortsCount);
+    err = AConnectivityNative_getPortsBlockedForBind(
+        (in_port_t *)actualBlockedPorts, &actualBlockedPortsCount);
     EXPECT_EQ(err, 0);
     EXPECT_NE(actualBlockedPortsCount, 0);
     for (int i=0; i < actualBlockedPortsCount; i++) {
@@ -227,9 +204,10 @@
     }
 
     // Remove the ports we added.
-    err = unblockAllPortsForBind();
+    err = AConnectivityNative_unblockAllPortsForBind();
     EXPECT_EQ(err, 0);
-    err = getPortsBlockedForBind(actualBlockedPorts, &actualBlockedPortsCount);
+    err = AConnectivityNative_getPortsBlockedForBind(actualBlockedPorts,
+                                                     &actualBlockedPortsCount);
     EXPECT_EQ(err, 0);
     EXPECT_EQ(actualBlockedPortsCount, 0);
 }
@@ -239,23 +217,25 @@
     in_port_t blockedPorts[8] = {1, 100, 1220, 1333, 2700, 5555, 5600, 65000};
 
     if (mActualBlockedPortsCount > 0) {
-        err = unblockAllPortsForBind();
+        err = AConnectivityNative_unblockAllPortsForBind();
     }
 
     for (int i : blockedPorts) {
-        err = blockPortForBind(i);
+        err = AConnectivityNative_blockPortForBind(i);
         EXPECT_EQ(err, 0);
     }
 
     size_t actualBlockedPortsCount = 8;
     in_port_t actualBlockedPorts[actualBlockedPortsCount];
-    err = getPortsBlockedForBind((in_port_t*) actualBlockedPorts, &actualBlockedPortsCount);
+    err = AConnectivityNative_getPortsBlockedForBind(
+        (in_port_t *)actualBlockedPorts, &actualBlockedPortsCount);
     EXPECT_EQ(err, 0);
     EXPECT_EQ(actualBlockedPortsCount, 8);
 
-    err = unblockAllPortsForBind();
+    err = AConnectivityNative_unblockAllPortsForBind();
     EXPECT_EQ(err, 0);
-    err = getPortsBlockedForBind((in_port_t*) actualBlockedPorts, &actualBlockedPortsCount);
+    err = AConnectivityNative_getPortsBlockedForBind(
+        (in_port_t *)actualBlockedPorts, &actualBlockedPortsCount);
     EXPECT_EQ(err, 0);
     EXPECT_EQ(actualBlockedPortsCount, 0);
     // If mActualBlockedPorts is not empty, ports will be added back in teardown.
@@ -264,7 +244,7 @@
 TEST_F(ConnectivityNativeBinderTest, CheckPermission) {
     int curUid = getuid();
     EXPECT_EQ(0, seteuid(FIRST_APPLICATION_UID + 2000)) << "seteuid failed: " << strerror(errno);
-    int err = blockPortForBind((in_port_t) 5555);
+    int err = AConnectivityNative_blockPortForBind((in_port_t)5555);
     EXPECT_EQ(EPERM, err);
     EXPECT_EQ(0, seteuid(curUid)) << "seteuid failed: " << strerror(errno);
 }
diff --git a/tests/unit/Android.bp b/tests/unit/Android.bp
index 2f88c41..ef3ebb0 100644
--- a/tests/unit/Android.bp
+++ b/tests/unit/Android.bp
@@ -92,6 +92,7 @@
         "frameworks-net-integration-testutils",
         "framework-protos",
         "mockito-target-minus-junit4",
+        "modules-utils-build",
         "net-tests-utils",
         "net-utils-services-common",
         "platform-compat-test-rules",
diff --git a/tests/unit/java/com/android/server/BpfNetMapsTest.java b/tests/unit/java/com/android/server/BpfNetMapsTest.java
index 859c54a..c1c15ca 100644
--- a/tests/unit/java/com/android/server/BpfNetMapsTest.java
+++ b/tests/unit/java/com/android/server/BpfNetMapsTest.java
@@ -41,6 +41,7 @@
 import static android.net.ConnectivityManager.BLOCKED_METERED_REASON_ADMIN_DISABLED;
 import static android.net.ConnectivityManager.BLOCKED_METERED_REASON_DATA_SAVER;
 import static android.net.ConnectivityManager.BLOCKED_METERED_REASON_USER_RESTRICTED;
+import static android.net.ConnectivityManager.BLOCKED_REASON_APP_BACKGROUND;
 import static android.net.ConnectivityManager.BLOCKED_REASON_APP_STANDBY;
 import static android.net.ConnectivityManager.BLOCKED_REASON_BATTERY_SAVER;
 import static android.net.ConnectivityManager.BLOCKED_REASON_DOZE;
@@ -136,6 +137,12 @@
 
     private static final int TEST_UID = 10086;
     private static final int[] TEST_UIDS = {10002, 10003};
+    private static final int[] CORE_AIDS = {
+            Process.ROOT_UID,
+            Process.SYSTEM_UID,
+            Process.FIRST_APPLICATION_UID - 10,
+            Process.FIRST_APPLICATION_UID - 1,
+    };
     private static final String TEST_IF_NAME = "wlan0";
     private static final int TEST_IF_INDEX = 7;
     private static final int NO_IIF = 0;
@@ -1261,15 +1268,9 @@
         assertTrue(BpfNetMapsUtils.isUidNetworkingBlocked(TEST_UID, false, mConfigurationMap,
                 mUidOwnerMap, mDataSaverEnabledMap));
 
-        final int[] coreAids = new int[] {
-                Process.ROOT_UID,
-                Process.SYSTEM_UID,
-                Process.FIRST_APPLICATION_UID - 10,
-                Process.FIRST_APPLICATION_UID - 1,
-        };
         // Core appIds are not on the chain but should still be allowed on any user.
         for (int userId = 0; userId < 20; userId++) {
-            for (final int aid : coreAids) {
+            for (final int aid : CORE_AIDS) {
                 final int uid = UserHandle.getUid(userId, aid);
                 assertFalse(BpfNetMapsUtils.isUidNetworkingBlocked(uid, false, mConfigurationMap,
                         mUidOwnerMap, mDataSaverEnabledMap));
@@ -1277,6 +1278,26 @@
         }
     }
 
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+    public void testGetUidNetworkingBlockedReasonsForCoreUids() throws Exception {
+        // Enable BACKGROUND_MATCH that is an allowlist match.
+        mConfigurationMap.updateEntry(UID_RULES_CONFIGURATION_KEY, new U32(BACKGROUND_MATCH));
+
+        // Non-core uid that is not on this chain is blocked by BLOCKED_REASON_APP_BACKGROUND.
+        assertEquals(BLOCKED_REASON_APP_BACKGROUND, BpfNetMapsUtils.getUidNetworkingBlockedReasons(
+                TEST_UID, mConfigurationMap, mUidOwnerMap, mDataSaverEnabledMap));
+
+        // Core appIds are not on the chain but should not be blocked on any users.
+        for (int userId = 0; userId < 20; userId++) {
+            for (final int aid : CORE_AIDS) {
+                final int uid = UserHandle.getUid(userId, aid);
+                assertEquals(BLOCKED_REASON_NONE, BpfNetMapsUtils.getUidNetworkingBlockedReasons(
+                        uid, mConfigurationMap, mUidOwnerMap, mDataSaverEnabledMap));
+            }
+        }
+    }
+
     private void doTestIsUidRestrictedOnMeteredNetworks(
             final long enabledMatches,
             final long uidRules,
diff --git a/tests/unit/java/com/android/server/CallbackQueueTest.kt b/tests/unit/java/com/android/server/CallbackQueueTest.kt
new file mode 100644
index 0000000..d8d35c1
--- /dev/null
+++ b/tests/unit/java/com/android/server/CallbackQueueTest.kt
@@ -0,0 +1,181 @@
+/*
+ * 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.CALLBACK_AVAILABLE
+import android.net.ConnectivityManager.CALLBACK_CAP_CHANGED
+import android.net.ConnectivityManager.CALLBACK_IP_CHANGED
+import android.os.Build
+import androidx.test.filters.SmallTest
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRunner
+import java.lang.reflect.Modifier
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+
+private const val TEST_NETID_1 = 123
+
+// Maximum 16 bits unsigned value
+private const val TEST_NETID_2 = 0xffff
+
+@RunWith(DevSdkIgnoreRunner::class)
+@SmallTest
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+class CallbackQueueTest {
+    @Test
+    fun testAddCallback() {
+        val cbs = listOf(
+            TEST_NETID_1 to CALLBACK_AVAILABLE,
+            TEST_NETID_2 to CALLBACK_AVAILABLE,
+            TEST_NETID_1 to CALLBACK_CAP_CHANGED,
+            TEST_NETID_1 to CALLBACK_CAP_CHANGED
+        )
+        val queue = CallbackQueue(intArrayOf()).apply {
+            cbs.forEach { addCallback(it.first, it.second) }
+        }
+
+        assertQueueEquals(cbs, queue)
+    }
+
+    @Test
+    fun testHasCallback() {
+        val queue = CallbackQueue(intArrayOf()).apply {
+            addCallback(TEST_NETID_1, CALLBACK_AVAILABLE)
+            addCallback(TEST_NETID_2, CALLBACK_AVAILABLE)
+            addCallback(TEST_NETID_1, CALLBACK_CAP_CHANGED)
+            addCallback(TEST_NETID_1, CALLBACK_CAP_CHANGED)
+        }
+
+        assertTrue(queue.hasCallback(TEST_NETID_1, CALLBACK_AVAILABLE))
+        assertTrue(queue.hasCallback(TEST_NETID_2, CALLBACK_AVAILABLE))
+        assertTrue(queue.hasCallback(TEST_NETID_1, CALLBACK_CAP_CHANGED))
+
+        assertFalse(queue.hasCallback(TEST_NETID_2, CALLBACK_CAP_CHANGED))
+        assertFalse(queue.hasCallback(1234, CALLBACK_AVAILABLE))
+        assertFalse(queue.hasCallback(TEST_NETID_1, 5678))
+        assertFalse(queue.hasCallback(1234, 5678))
+    }
+
+    @Test
+    fun testRemoveCallbacks() {
+        val queue = CallbackQueue(intArrayOf()).apply {
+            assertFalse(removeCallbacks(TEST_NETID_1, CALLBACK_AVAILABLE))
+            addCallback(TEST_NETID_1, CALLBACK_AVAILABLE)
+            addCallback(TEST_NETID_1, CALLBACK_CAP_CHANGED)
+            addCallback(TEST_NETID_2, CALLBACK_AVAILABLE)
+            addCallback(TEST_NETID_1, CALLBACK_AVAILABLE)
+            assertTrue(removeCallbacks(TEST_NETID_1, CALLBACK_AVAILABLE))
+        }
+        assertQueueEquals(listOf(
+            TEST_NETID_1 to CALLBACK_CAP_CHANGED,
+            TEST_NETID_2 to CALLBACK_AVAILABLE
+        ), queue)
+    }
+
+    @Test
+    fun testRemoveCallbacksForNetId() {
+        val queue = CallbackQueue(intArrayOf()).apply {
+            assertFalse(removeCallbacksForNetId(TEST_NETID_2))
+            addCallback(TEST_NETID_2, CALLBACK_AVAILABLE)
+            assertFalse(removeCallbacksForNetId(TEST_NETID_1))
+            addCallback(TEST_NETID_1, CALLBACK_AVAILABLE)
+            addCallback(TEST_NETID_1, CALLBACK_CAP_CHANGED)
+            addCallback(TEST_NETID_2, CALLBACK_CAP_CHANGED)
+            addCallback(TEST_NETID_1, CALLBACK_AVAILABLE)
+            addCallback(TEST_NETID_2, CALLBACK_IP_CHANGED)
+            assertTrue(removeCallbacksForNetId(TEST_NETID_2))
+        }
+        assertQueueEquals(listOf(
+            TEST_NETID_1 to CALLBACK_AVAILABLE,
+            TEST_NETID_1 to CALLBACK_CAP_CHANGED,
+            TEST_NETID_1 to CALLBACK_AVAILABLE,
+        ), queue)
+    }
+
+    @Test
+    fun testConstructorFromExistingArray() {
+        val queue1 = CallbackQueue(intArrayOf()).apply {
+            addCallback(TEST_NETID_1, CALLBACK_AVAILABLE)
+            addCallback(TEST_NETID_2, CALLBACK_AVAILABLE)
+        }
+        val queue2 = CallbackQueue(queue1.minimizedBackingArray)
+        assertQueueEquals(listOf(
+            TEST_NETID_1 to CALLBACK_AVAILABLE,
+            TEST_NETID_2 to CALLBACK_AVAILABLE
+        ), queue2)
+    }
+
+    @Test
+    fun testToString() {
+        assertEquals("[]", CallbackQueue(intArrayOf()).toString())
+        assertEquals(
+            "[CALLBACK_AVAILABLE($TEST_NETID_1)]",
+            CallbackQueue(intArrayOf()).apply {
+                addCallback(TEST_NETID_1, CALLBACK_AVAILABLE)
+            }.toString()
+        )
+        assertEquals(
+            "[CALLBACK_AVAILABLE($TEST_NETID_1),CALLBACK_CAP_CHANGED($TEST_NETID_2)]",
+            CallbackQueue(intArrayOf()).apply {
+                addCallback(TEST_NETID_1, CALLBACK_AVAILABLE)
+                addCallback(TEST_NETID_2, CALLBACK_CAP_CHANGED)
+            }.toString()
+        )
+    }
+
+    @Test
+    fun testMaxNetId() {
+        // CallbackQueue assumes netIds are at most 16 bits
+        assertTrue(NetIdManager.MAX_NET_ID <= 0xffff)
+    }
+
+    @Test
+    fun testMaxCallbackId() {
+        // CallbackQueue assumes callback IDs are at most 16 bits.
+        val constants = ConnectivityManager::class.java.declaredFields.filter {
+            Modifier.isStatic(it.modifiers) && Modifier.isFinal(it.modifiers) &&
+                    it.name.startsWith("CALLBACK_")
+        }
+        constants.forEach {
+            it.isAccessible = true
+            assertTrue(it.get(null) as Int <= 0xffff)
+        }
+    }
+}
+
+private fun assertQueueEquals(expected: List<Pair<Int, Int>>, actual: CallbackQueue) {
+    assertEquals(
+        expected.size,
+        actual.length(),
+        "Size mismatch between expected: $expected and actual: $actual"
+    )
+
+    var nextIndex = 0
+    actual.forEach { netId, cbId ->
+        val (expNetId, expCbId) = expected[nextIndex]
+        val msg = "$actual does not match $expected at index $nextIndex"
+        assertEquals(expNetId, netId, msg)
+        assertEquals(expCbId, cbId, msg)
+        nextIndex++
+    }
+    // Ensure forEach iterations and size are consistent
+    assertEquals(expected.size, nextIndex)
+}
diff --git a/tests/unit/java/com/android/server/ConnectivityServiceTest.java b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
index be7f2a3..999d17d 100755
--- a/tests/unit/java/com/android/server/ConnectivityServiceTest.java
+++ b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
@@ -2180,6 +2180,7 @@
                 case ConnectivityFlags.CARRIER_SERVICE_CHANGED_USE_CALLBACK:
                 case ConnectivityFlags.REQUEST_RESTRICTED_WIFI:
                 case ConnectivityFlags.USE_DECLARED_METHODS_FOR_CALLBACKS:
+                case ConnectivityFlags.QUEUE_CALLBACKS_FOR_FROZEN_APPS:
                 case KEY_DESTROY_FROZEN_SOCKETS_VERSION:
                     return true;
                 default:
diff --git a/tests/unit/java/com/android/server/connectivity/DnsManagerTest.java b/tests/unit/java/com/android/server/connectivity/DnsManagerTest.java
index ea3d2dd..b47b97d 100644
--- a/tests/unit/java/com/android/server/connectivity/DnsManagerTest.java
+++ b/tests/unit/java/com/android/server/connectivity/DnsManagerTest.java
@@ -54,6 +54,7 @@
 import android.net.ResolverOptionsParcel;
 import android.net.ResolverParamsParcel;
 import android.net.RouteInfo;
+import android.net.resolv.aidl.DohParamsParcel;
 import android.net.shared.PrivateDnsConfig;
 import android.os.Build;
 import android.provider.Settings;
@@ -327,8 +328,16 @@
     @Test
     public void testSendDnsConfiguration() throws Exception {
         reset(mMockDnsResolver);
-        mDnsManager.updatePrivateDns(new Network(TEST_NETID),
-                mDnsManager.getPrivateDnsConfig());
+        final PrivateDnsConfig cfg = new PrivateDnsConfig(
+                PRIVATE_DNS_MODE_OPPORTUNISTIC /* mode */,
+                null /* hostname */,
+                null /* ips */,
+                "doh.com" /* dohName */,
+                null /* dohIps */,
+                "/some-path{?dns}" /* dohPath */,
+                5353 /* dohPort */);
+
+        mDnsManager.updatePrivateDns(new Network(TEST_NETID), cfg);
         final LinkProperties lp = new LinkProperties();
         lp.setInterfaceName(TEST_IFACENAME);
         lp.addDnsServer(InetAddress.getByName("3.3.3.3"));
@@ -352,7 +361,11 @@
         expectedParams.transportTypes = TEST_TRANSPORT_TYPES;
         expectedParams.resolverOptions = null;
         expectedParams.meteredNetwork = true;
-        expectedParams.dohParams = null;
+        expectedParams.dohParams = new DohParamsParcel.Builder()
+                .setName("doh.com")
+                .setDohpath("/some-path{?dns}")
+                .setPort(5353)
+                .build();
         expectedParams.interfaceNames = new String[]{TEST_IFACENAME};
         verify(mMockDnsResolver, times(1)).setResolverConfiguration(eq(expectedParams));
     }
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClientTest.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClientTest.java
index fb3d183..4c71991 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClientTest.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClientTest.java
@@ -18,8 +18,10 @@
 
 import static com.android.server.connectivity.mdns.MdnsSocketProvider.SocketCallback;
 import static com.android.server.connectivity.mdns.MulticastPacketReader.PacketHandler;
+import static com.android.testutils.Cleanup.testAndCleanup;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.eq;
@@ -35,6 +37,7 @@
 import android.os.Build;
 import android.os.Handler;
 import android.os.HandlerThread;
+import android.util.Log;
 
 import com.android.net.module.util.HexDump;
 import com.android.net.module.util.SharedLog;
@@ -59,6 +62,7 @@
 import java.net.SocketException;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
 
 @RunWith(DevSdkIgnoreRunner.class)
 @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
@@ -437,4 +441,34 @@
             inOrder.verify(mSocket).send(packets.get(i));
         }
     }
+
+    @Test
+    public void testSendPacketWithMultiplePacketsWithDifferentAddresses() throws IOException {
+        final SocketCallback callback = expectSocketCallback();
+        final DatagramPacket ipv4Packet = new DatagramPacket(BUFFER, 0 /* offset */, BUFFER.length,
+                InetAddresses.parseNumericAddress("192.0.2.1"), 0 /* port */);
+        final DatagramPacket ipv6Packet = new DatagramPacket(BUFFER, 0 /* offset */, BUFFER.length,
+                InetAddresses.parseNumericAddress("2001:db8::"), 0 /* port */);
+        doReturn(true).when(mSocket).hasJoinedIpv4();
+        doReturn(true).when(mSocket).hasJoinedIpv6();
+        doReturn(createEmptyNetworkInterface()).when(mSocket).getInterface();
+
+        // Notify socket created
+        callback.onSocketCreated(mSocketKey, mSocket, List.of());
+        verify(mSocketCreationCallback).onSocketCreated(mSocketKey);
+
+        // Send packets with IPv4 and IPv6 then verify wtf logs and sending has never been called.
+        // Override the default TerribleFailureHandler, as that handler might terminate the process
+        // (if we're on an eng build).
+        final AtomicBoolean hasFailed = new AtomicBoolean(false);
+        final Log.TerribleFailureHandler originalHandler =
+                Log.setWtfHandler((tag, what, system) -> hasFailed.set(true));
+        testAndCleanup(() -> {
+            mSocketClient.sendPacketRequestingMulticastResponse(List.of(ipv4Packet, ipv6Packet),
+                    mSocketKey, false /* onlyUseIpv6OnIpv6OnlyNetworks */);
+            HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT);
+            assertTrue(hasFailed.get());
+            verify(mSocket, never()).send(any());
+        }, () -> Log.setWtfHandler(originalHandler));
+    }
 }
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt
index 2cb97c9..9674da3 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt
@@ -123,18 +123,18 @@
 }
 
 private val TEST_PUBLIC_KEY = hexStringToByteArray(
-        "0201030dc141d0637960b98cbc12cfca"
-                + "221d2879dac26ee5b460e9007c992e19"
-                + "02d897c391b03764d448f7d0c772fdb0"
-                + "3b1d9d6d52ff8886769e8e2362513565"
-                + "270962d3")
+        "0201030dc141d0637960b98cbc12cfca" +
+                "221d2879dac26ee5b460e9007c992e19" +
+                "02d897c391b03764d448f7d0c772fdb0" +
+                "3b1d9d6d52ff8886769e8e2362513565" +
+                "270962d3")
 
 private val TEST_PUBLIC_KEY_2 = hexStringToByteArray(
-        "0201030dc141d0637960b98cbc12cfca"
-                + "221d2879dac26ee5b460e9007c992e19"
-                + "02d897c391b03764d448f7d0c772fdb0"
-                + "3b1d9d6d52ff8886769e8e2362513565"
-                + "270962d4")
+        "0201030dc141d0637960b98cbc12cfca" +
+                "221d2879dac26ee5b460e9007c992e19" +
+                "02d897c391b03764d448f7d0c772fdb0" +
+                "3b1d9d6d52ff8886769e8e2362513565" +
+                "270962d4")
 
 @RunWith(DevSdkIgnoreRunner::class)
 @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
@@ -159,7 +159,7 @@
 
     @Before
     fun setUp() {
-        deps.resetElapsedRealTime();
+        deps.resetElapsedRealTime()
         thread.start()
     }
 
@@ -172,11 +172,13 @@
     private fun makeFlags(
         includeInetAddressesInProbing: Boolean = false,
         isKnownAnswerSuppressionEnabled: Boolean = false,
-        unicastReplyEnabled: Boolean = true
+        unicastReplyEnabled: Boolean = true,
+        avoidAdvertisingEmptyTxtRecords: Boolean = true
     ) = MdnsFeatureFlags.Builder()
         .setIncludeInetAddressRecordsInProbing(includeInetAddressesInProbing)
         .setIsKnownAnswerSuppressionEnabled(isKnownAnswerSuppressionEnabled)
         .setIsUnicastReplyEnabled(unicastReplyEnabled)
+        .setAvoidAdvertisingEmptyTxtRecords(avoidAdvertisingEmptyTxtRecords)
         .build()
 
     @Test
@@ -1721,6 +1723,30 @@
     }
 
     @Test
+    fun testGetConflictingServices_ZeroLengthTxtRecord_NoConflict() {
+        val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+        repository.addServiceAndFinishProbing(TEST_SERVICE_ID_1, TEST_SERVICE_1)
+
+        val packet = MdnsPacket(
+            0 /* flags */,
+            emptyList() /* questions */,
+            listOf(
+                    MdnsTextRecord(
+                        arrayOf("MyOtherTestService", "_testservice", "_tcp", "local"),
+                        0L /* receiptTimeMillis */,
+                        true /* cacheFlush */,
+                        SHORT_TTL,
+                        listOf(TextEntry("", null as ByteArray?))
+                    ),
+            ) /* answers */,
+            emptyList() /* authorityRecords */,
+            emptyList() /* additionalRecords */
+        )
+
+        assertEquals(emptyMap(), repository.getConflictingServices(packet))
+    }
+
+    @Test
     fun testGetServiceRepliedRequestsCount() {
         val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
         repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
@@ -2168,6 +2194,46 @@
         assertEquals(knownAnswers, reply.knownAnswers)
     }
 
+    private fun doAddServiceWithEmptyTxtRecordTest(flags: MdnsFeatureFlags): MdnsTextRecord {
+        val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
+        repository.addServiceAndFinishProbing(TEST_SERVICE_ID_1, TEST_SERVICE_1)
+
+        val questions = listOf(MdnsTextRecord(
+            arrayOf("MyTestService", "_testservice", "_tcp", "local"),
+            true /* isUnicast */
+        ))
+        val query = MdnsPacket(
+            0 /* flags */,
+            questions,
+            emptyList() /* answers */,
+            emptyList() /* authorityRecords */,
+            emptyList() /* additionalRecords */
+        )
+        val src = InetSocketAddress(parseNumericAddress("192.0.2.123"), 5353)
+        val reply = repository.getReply(query, src)
+
+        assertNotNull(reply)
+        assertEquals(1, reply.answers.size)
+        assertTrue(reply.answers[0] is MdnsTextRecord)
+        return reply.answers[0] as MdnsTextRecord
+    }
+
+    @Test
+    fun testAddService_AvoidEmptyTxtRecords_HasTxtRecordWithEmptyString() {
+        val answerRecord = doAddServiceWithEmptyTxtRecordTest(makeFlags())
+        assertEquals(1, answerRecord.entries.size)
+        assertEquals(0, answerRecord.entries[0].key.length)
+        assertNull(answerRecord.entries[0].value)
+    }
+
+    @Test
+    fun testAddService_UsesEmptyTxtRecords_HasEmptyTxtRecord() {
+        val answerRecord = doAddServiceWithEmptyTxtRecordTest(makeFlags(
+            avoidAdvertisingEmptyTxtRecords = false
+        ))
+        assertEquals(0, answerRecord.entries.size)
+    }
+
     @Test
     fun testRestartProbingForHostname() {
         val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordTests.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordTests.java
index 63548c1..784c502 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordTests.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordTests.java
@@ -28,6 +28,8 @@
 import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
 
+import static java.util.Collections.emptyList;
+
 import android.util.Log;
 
 import com.android.net.module.util.HexDump;
@@ -372,6 +374,30 @@
         assertEquals(dataInText, dataOutText);
     }
 
+    private static MdnsTextRecord makeTextRecordWithEntries(List<TextEntry> entries) {
+        return new MdnsTextRecord(new String[] { "test", "record" }, 0L /* receiptTimeMillis */,
+                true /* cacheFlush */, 120_000L /* ttlMillis */, entries);
+    }
+
+    @Test
+    public void testTextRecord_EmptyRecordsAreEquivalent() {
+        final MdnsTextRecord record1 = makeTextRecordWithEntries(emptyList());
+        final MdnsTextRecord record2 = makeTextRecordWithEntries(
+                List.of(new TextEntry("", (byte[]) null)));
+        final MdnsTextRecord record3 = makeTextRecordWithEntries(
+                List.of(new TextEntry(null, (byte[]) null)));
+        final MdnsTextRecord nonEmptyRecord = makeTextRecordWithEntries(
+                List.of(new TextEntry("a", (byte[]) null)));
+
+        assertEquals(record1, record1);
+        assertEquals(record1, record2);
+        assertEquals(record1, record3);
+
+        assertNotEquals(nonEmptyRecord, record1);
+        assertNotEquals(nonEmptyRecord, record2);
+        assertNotEquals(nonEmptyRecord, record3);
+    }
+
     private static String toHex(MdnsRecord record) throws IOException {
         MdnsPacketWriter writer = new MdnsPacketWriter(MAX_PACKET_SIZE);
         record.write(writer, record.getReceiptTime());
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceCacheTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceCacheTest.kt
index b040ab6..0a8f108 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceCacheTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceCacheTest.kt
@@ -154,6 +154,11 @@
         serviceCache.registerServiceExpiredCallback(cacheKey, callback)
     }
 
+    private fun removeServices(
+            serviceCache: MdnsServiceCache,
+            cacheKey: CacheKey
+    ): Unit = runningOnHandlerAndReturn { serviceCache.removeServices(cacheKey) }
+
     @Test
     fun testAddAndRemoveService() {
         val serviceCache = MdnsServiceCache(thread.looper, makeFlags(), clock)
@@ -291,6 +296,37 @@
         assertEquals(response4, responses[3])
     }
 
+    @Test
+    fun testRemoveServices() {
+        val serviceCache = MdnsServiceCache(thread.looper, makeFlags(), clock)
+        addOrUpdateService(serviceCache, cacheKey1, createResponse(SERVICE_NAME_1, SERVICE_TYPE_1))
+        addOrUpdateService(serviceCache, cacheKey1, createResponse(SERVICE_NAME_2, SERVICE_TYPE_1))
+        addOrUpdateService(serviceCache, cacheKey2, createResponse(SERVICE_NAME_1, SERVICE_TYPE_2))
+        val responses1 = getServices(serviceCache, cacheKey1)
+        assertEquals(2, responses1.size)
+        assertTrue(responses1.stream().anyMatch { response ->
+            response.serviceInstanceName == SERVICE_NAME_1
+        })
+        assertTrue(responses1.any { response ->
+            response.serviceInstanceName == SERVICE_NAME_2
+        })
+        val responses2 = getServices(serviceCache, cacheKey2)
+        assertEquals(1, responses2.size)
+        assertTrue(responses2.stream().anyMatch { response ->
+            response.serviceInstanceName == SERVICE_NAME_1
+        })
+
+        removeServices(serviceCache, cacheKey1)
+        val responses3 = getServices(serviceCache, cacheKey1)
+        assertEquals(0, responses3.size)
+        val responses4 = getServices(serviceCache, cacheKey2)
+        assertEquals(1, responses4.size)
+
+        removeServices(serviceCache, cacheKey2)
+        val responses5 = getServices(serviceCache, cacheKey2)
+        assertEquals(0, responses5.size)
+    }
+
     private fun createResponse(
             serviceInstanceName: String,
             serviceType: String,
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 44fa55c..da0bc88 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java
@@ -562,10 +562,7 @@
         //MdnsConfigsFlagsImpl.alwaysAskForUnicastResponseInEachBurst.override(true);
         MdnsSearchOptions searchOptions = MdnsSearchOptions.newBuilder()
                 .addSubtype(SUBTYPE).setQueryMode(ACTIVE_QUERY_MODE).build();
-        QueryTaskConfig config = new QueryTaskConfig(
-                searchOptions.getQueryMode(),
-                false /* onlyUseIpv6OnIpv6OnlyNetworks */, 3 /* numOfQueriesBeforeBackoff */,
-                socketKey);
+        QueryTaskConfig config = new QueryTaskConfig(searchOptions.getQueryMode());
 
         // This is the first query. We will ask for unicast response.
         assertTrue(config.expectUnicastResponse);
@@ -574,14 +571,14 @@
         // For the rest of queries in this burst, we will NOT ask for unicast response.
         for (int i = 1; i < MdnsConfigs.queriesPerBurst(); i++) {
             int oldTransactionId = config.transactionId;
-            config = config.getConfigForNextRun();
+            config = config.getConfigForNextRun(ACTIVE_QUERY_MODE);
             assertFalse(config.expectUnicastResponse);
             assertEquals(config.transactionId, oldTransactionId + 1);
         }
 
         // This is the first query of a new burst. We will ask for unicast response.
         int oldTransactionId = config.transactionId;
-        config = config.getConfigForNextRun();
+        config = config.getConfigForNextRun(ACTIVE_QUERY_MODE);
         assertTrue(config.expectUnicastResponse);
         assertEquals(config.transactionId, oldTransactionId + 1);
     }
@@ -590,10 +587,7 @@
     public void testQueryTaskConfig_askForUnicastInFirstQuery() {
         MdnsSearchOptions searchOptions = MdnsSearchOptions.newBuilder()
                 .addSubtype(SUBTYPE).setQueryMode(ACTIVE_QUERY_MODE).build();
-        QueryTaskConfig config = new QueryTaskConfig(
-                searchOptions.getQueryMode(),
-                false /* onlyUseIpv6OnIpv6OnlyNetworks */, 3 /* numOfQueriesBeforeBackoff */,
-                socketKey);
+        QueryTaskConfig config = new QueryTaskConfig(searchOptions.getQueryMode());
 
         // This is the first query. We will ask for unicast response.
         assertTrue(config.expectUnicastResponse);
@@ -602,14 +596,14 @@
         // For the rest of queries in this burst, we will NOT ask for unicast response.
         for (int i = 1; i < MdnsConfigs.queriesPerBurst(); i++) {
             int oldTransactionId = config.transactionId;
-            config = config.getConfigForNextRun();
+            config = config.getConfigForNextRun(ACTIVE_QUERY_MODE);
             assertFalse(config.expectUnicastResponse);
             assertEquals(config.transactionId, oldTransactionId + 1);
         }
 
         // This is the first query of a new burst. We will NOT ask for unicast response.
         int oldTransactionId = config.transactionId;
-        config = config.getConfigForNextRun();
+        config = config.getConfigForNextRun(ACTIVE_QUERY_MODE);
         assertFalse(config.expectUnicastResponse);
         assertEquals(config.transactionId, oldTransactionId + 1);
     }
@@ -2062,6 +2056,64 @@
         assertTrue(hasAnswer(knownAnswersQueryPacket, MdnsRecord.TYPE_PTR, subtypeLabels));
     }
 
+    @Test
+    public void sendQueries_AggressiveQueryMode_ServiceInCache() {
+        final int numOfQueriesBeforeBackoff = 11;
+        final MdnsSearchOptions searchOptions = MdnsSearchOptions.newBuilder()
+                .setQueryMode(AGGRESSIVE_QUERY_MODE)
+                .setNumOfQueriesBeforeBackoff(numOfQueriesBeforeBackoff)
+                .build();
+        startSendAndReceive(mockListenerOne, searchOptions);
+        verify(mockDeps, times(1)).removeMessages(any(), eq(EVENT_START_QUERYTASK));
+
+        int burstCounter = 0;
+        int betweenBurstTime = 0;
+        for (int i = 0; i < numOfQueriesBeforeBackoff; i += 3) {
+            verifyAndSendQuery(i, betweenBurstTime, /* expectsUnicastResponse= */ true);
+            verifyAndSendQuery(i + 1, /* timeInMs= */ 0, /* expectsUnicastResponse= */ false);
+            verifyAndSendQuery(i + 2, TIME_BETWEEN_RETRANSMISSION_QUERIES_IN_BURST_MS,
+                    /* expectsUnicastResponse= */ false);
+            betweenBurstTime = Math.min(
+                    INITIAL_AGGRESSIVE_TIME_BETWEEN_BURSTS_MS * (int) Math.pow(2, burstCounter),
+                    MAX_TIME_BETWEEN_AGGRESSIVE_BURSTS_MS);
+            burstCounter++;
+        }
+        // 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(mockDeps).hasMessages(any(), eq(EVENT_START_QUERYTASK));
+        processResponse(createResponse(
+                "service-instance-1", "192.0.2.123", 5353,
+                SERVICE_TYPE_LABELS,
+                Collections.emptyMap(), TEST_TTL), socketKey);
+        verify(mockDeps, times(2)).removeMessages(any(), eq(EVENT_START_QUERYTASK));
+        assertNotNull(delayMessage);
+        assertEquals((long) (TEST_TTL / 2 * 0.8), latestDelayMs);
+
+        // Register another listener. There is a service in cache, the query time should be
+        // rescheduled with previous run.
+        currentTime += (long) ((TEST_TTL / 2 * 0.8) - 500L);
+        doReturn(currentTime).when(mockDecoderClock).elapsedRealtime();
+        startSendAndReceive(mockListenerTwo, searchOptions);
+        verify(mockDeps, times(3)).removeMessages(any(), eq(EVENT_START_QUERYTASK));
+        assertNotNull(delayMessage);
+        assertEquals(500L, latestDelayMs);
+
+        // Stop all listeners
+        stopSendAndReceive(mockListenerOne);
+        stopSendAndReceive(mockListenerTwo);
+        verify(mockDeps, times(4)).removeMessages(any(), eq(EVENT_START_QUERYTASK));
+
+        // Register a new listener. There is a service in cache, the query time should be
+        // rescheduled with remaining ttl.
+        currentTime += 400L;
+        doReturn(currentTime).when(mockDecoderClock).elapsedRealtime();
+        startSendAndReceive(mockListenerOne, searchOptions);
+        assertNotNull(delayMessage);
+        assertEquals(9680L, latestDelayMs);
+    }
+
     private static MdnsServiceInfo matchServiceName(String name) {
         return argThat(info -> info.getServiceInstanceName().equals(name));
     }
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsSocketClientTests.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsSocketClientTests.java
index 1989ed3..ab70e38 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsSocketClientTests.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsSocketClientTests.java
@@ -16,6 +16,7 @@
 
 package com.android.server.connectivity.mdns;
 
+import static com.android.testutils.Cleanup.testAndCleanup;
 import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
 
 import static org.junit.Assert.assertFalse;
@@ -38,9 +39,11 @@
 import android.annotation.RequiresPermission;
 import android.content.Context;
 import android.net.ConnectivityManager;
+import android.net.InetAddresses;
 import android.net.wifi.WifiManager;
 import android.net.wifi.WifiManager.MulticastLock;
 import android.text.format.DateUtils;
+import android.util.Log;
 
 import com.android.net.module.util.HexDump;
 import com.android.net.module.util.SharedLog;
@@ -594,6 +597,29 @@
         }
     }
 
+    @Test
+    public void testSendPacketWithMultiplePacketsWithDifferentAddresses() throws IOException {
+        mdnsClient.startDiscovery();
+        final byte[] buffer = new byte[10];
+        final DatagramPacket ipv4Packet = new DatagramPacket(buffer, 0 /* offset */, buffer.length,
+                InetAddresses.parseNumericAddress("192.0.2.1"), 0 /* port */);
+        final DatagramPacket ipv6Packet = new DatagramPacket(buffer, 0 /* offset */, buffer.length,
+                InetAddresses.parseNumericAddress("2001:db8::"), 0 /* port */);
+
+        // Send packets with IPv4 and IPv6 then verify wtf logs and sending has never been called.
+        // Override the default TerribleFailureHandler, as that handler might terminate the process
+        // (if we're on an eng build).
+        final AtomicBoolean hasFailed = new AtomicBoolean(false);
+        final Log.TerribleFailureHandler originalHandler =
+                Log.setWtfHandler((tag, what, system) -> hasFailed.set(true));
+        testAndCleanup(() -> {
+            mdnsClient.sendPacketRequestingMulticastResponse(List.of(ipv4Packet, ipv6Packet),
+                    false /* onlyUseIpv6OnIpv6OnlyNetworks */);
+            assertTrue(hasFailed.get());
+            verify(mockMulticastSocket, never()).send(any());
+        }, () -> Log.setWtfHandler(originalHandler));
+    }
+
     private DatagramPacket getTestDatagramPacket() {
         return new DatagramPacket(buf, 0, 5,
                 new InetSocketAddress(MdnsConstants.getMdnsIPv4Address(), 5353 /* port */));
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/util/MdnsUtilsTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/util/MdnsUtilsTest.kt
index 009205e..5c3ad22 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/util/MdnsUtilsTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/util/MdnsUtilsTest.kt
@@ -16,24 +16,22 @@
 
 package com.android.server.connectivity.mdns.util
 
+import android.net.InetAddresses
 import android.os.Build
 import com.android.server.connectivity.mdns.MdnsConstants
 import com.android.server.connectivity.mdns.MdnsConstants.FLAG_TRUNCATED
+import com.android.server.connectivity.mdns.MdnsConstants.IPV4_SOCKET_ADDR
+import com.android.server.connectivity.mdns.MdnsConstants.IPV6_SOCKET_ADDR
 import com.android.server.connectivity.mdns.MdnsPacket
 import com.android.server.connectivity.mdns.MdnsPacketReader
 import com.android.server.connectivity.mdns.MdnsPointerRecord
 import com.android.server.connectivity.mdns.MdnsRecord
 import com.android.server.connectivity.mdns.util.MdnsUtils.createQueryDatagramPackets
-import com.android.server.connectivity.mdns.util.MdnsUtils.equalsDnsLabelIgnoreDnsCase
-import com.android.server.connectivity.mdns.util.MdnsUtils.equalsIgnoreDnsCase
-import com.android.server.connectivity.mdns.util.MdnsUtils.toDnsLabelsLowerCase
-import com.android.server.connectivity.mdns.util.MdnsUtils.toDnsLowerCase
 import com.android.server.connectivity.mdns.util.MdnsUtils.truncateServiceName
 import com.android.testutils.DevSdkIgnoreRule
 import com.android.testutils.DevSdkIgnoreRunner
 import java.net.DatagramPacket
 import kotlin.test.assertContentEquals
-import org.junit.Assert.assertArrayEquals
 import org.junit.Assert.assertEquals
 import org.junit.Assert.assertFalse
 import org.junit.Assert.assertTrue
@@ -43,59 +41,6 @@
 @RunWith(DevSdkIgnoreRunner::class)
 @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
 class MdnsUtilsTest {
-    @Test
-    fun testToDnsLowerCase() {
-        assertEquals("test", toDnsLowerCase("TEST"))
-        assertEquals("test", toDnsLowerCase("TeSt"))
-        assertEquals("test", toDnsLowerCase("test"))
-        assertEquals("tÉst", toDnsLowerCase("TÉST"))
-        assertEquals("ţést", toDnsLowerCase("ţést"))
-        // Unicode characters 0x10000 (𐀀), 0x10001 (𐀁), 0x10041 (𐁁)
-        // Note the last 2 bytes of 0x10041 are identical to 'A', but it should remain unchanged.
-        assertEquals(
-            "test: -->\ud800\udc00 \ud800\udc01 \ud800\udc41<-- ",
-                toDnsLowerCase("Test: -->\ud800\udc00 \ud800\udc01 \ud800\udc41<-- ")
-        )
-        // Also test some characters where the first surrogate is not \ud800
-        assertEquals(
-            "test: >\ud83c\udff4\udb40\udc67\udb40\udc62\udb40" +
-                "\udc77\udb40\udc6c\udb40\udc73\udb40\udc7f<",
-                toDnsLowerCase(
-                    "Test: >\ud83c\udff4\udb40\udc67\udb40\udc62\udb40" +
-                        "\udc77\udb40\udc6c\udb40\udc73\udb40\udc7f<"
-                )
-        )
-    }
-
-    @Test
-    fun testToDnsLabelsLowerCase() {
-        assertArrayEquals(
-            arrayOf("test", "tÉst", "ţést"),
-            toDnsLabelsLowerCase(arrayOf("TeSt", "TÉST", "ţést"))
-        )
-    }
-
-    @Test
-    fun testEqualsIgnoreDnsCase() {
-        assertTrue(equalsIgnoreDnsCase("TEST", "Test"))
-        assertTrue(equalsIgnoreDnsCase("TEST", "test"))
-        assertTrue(equalsIgnoreDnsCase("test", "TeSt"))
-        assertTrue(equalsIgnoreDnsCase("Tést", "tést"))
-        assertFalse(equalsIgnoreDnsCase("ŢÉST", "ţést"))
-        // Unicode characters 0x10000 (𐀀), 0x10001 (𐀁), 0x10041 (𐁁)
-        // Note the last 2 bytes of 0x10041 are identical to 'A', but it should remain unchanged.
-        assertTrue(equalsIgnoreDnsCase(
-            "test: -->\ud800\udc00 \ud800\udc01 \ud800\udc41<-- ",
-                "Test: -->\ud800\udc00 \ud800\udc01 \ud800\udc41<-- "
-        ))
-        // Also test some characters where the first surrogate is not \ud800
-        assertTrue(equalsIgnoreDnsCase(
-            "test: >\ud83c\udff4\udb40\udc67\udb40\udc62\udb40" +
-                "\udc77\udb40\udc6c\udb40\udc73\udb40\udc7f<",
-                "Test: >\ud83c\udff4\udb40\udc67\udb40\udc62\udb40" +
-                        "\udc77\udb40\udc6c\udb40\udc73\udb40\udc7f<"
-        ))
-    }
 
     @Test
     fun testTruncateServiceName() {
@@ -104,14 +49,6 @@
     }
 
     @Test
-    fun testEqualsLabelIgnoreDnsCase() {
-        assertTrue(equalsDnsLabelIgnoreDnsCase(arrayOf("TEST", "Test"), arrayOf("test", "test")))
-        assertFalse(equalsDnsLabelIgnoreDnsCase(arrayOf("TEST", "Test"), arrayOf("test")))
-        assertFalse(equalsDnsLabelIgnoreDnsCase(arrayOf("Test"), arrayOf("test", "test")))
-        assertFalse(equalsDnsLabelIgnoreDnsCase(arrayOf("TEST", "Test"), arrayOf("test", "tést")))
-    }
-
-    @Test
     fun testTypeEqualsOrIsSubtype() {
         assertTrue(MdnsUtils.typeEqualsOrIsSubtype(
             arrayOf("_type", "_tcp", "local"),
@@ -193,4 +130,31 @@
         }
         return MdnsPacket(flags, questions, answers, emptyList(), emptyList())
     }
+
+    @Test
+    fun testCheckAllPacketsWithSameAddress() {
+        val buffer = ByteArray(10)
+        val v4Packet = DatagramPacket(buffer, buffer.size, IPV4_SOCKET_ADDR)
+        val otherV4Packet = DatagramPacket(
+            buffer,
+            buffer.size,
+            InetAddresses.parseNumericAddress("192.0.2.1"),
+            1234
+        )
+        val v6Packet = DatagramPacket(ByteArray(10), 10, IPV6_SOCKET_ADDR)
+        val otherV6Packet = DatagramPacket(
+            buffer,
+            buffer.size,
+            InetAddresses.parseNumericAddress("2001:db8::"),
+            1234
+        )
+        assertTrue(MdnsUtils.checkAllPacketsWithSameAddress(listOf()))
+        assertTrue(MdnsUtils.checkAllPacketsWithSameAddress(listOf(v4Packet)))
+        assertTrue(MdnsUtils.checkAllPacketsWithSameAddress(listOf(v4Packet, v4Packet)))
+        assertFalse(MdnsUtils.checkAllPacketsWithSameAddress(listOf(v4Packet, otherV4Packet)))
+        assertTrue(MdnsUtils.checkAllPacketsWithSameAddress(listOf(v6Packet)))
+        assertTrue(MdnsUtils.checkAllPacketsWithSameAddress(listOf(v6Packet, v6Packet)))
+        assertFalse(MdnsUtils.checkAllPacketsWithSameAddress(listOf(v6Packet, otherV6Packet)))
+        assertFalse(MdnsUtils.checkAllPacketsWithSameAddress(listOf(v4Packet, v6Packet)))
+    }
 }
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSIngressDiscardRuleTests.kt b/tests/unit/java/com/android/server/connectivityservice/CSIngressDiscardRuleTests.kt
index 93f6e81..77b06b2 100644
--- a/tests/unit/java/com/android/server/connectivityservice/CSIngressDiscardRuleTests.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/CSIngressDiscardRuleTests.kt
@@ -26,7 +26,9 @@
 import android.net.NetworkCapabilities.TRANSPORT_VPN
 import android.net.NetworkCapabilities.TRANSPORT_WIFI
 import android.net.NetworkRequest
+import android.net.VpnManager.TYPE_VPN_OEM
 import android.net.VpnManager.TYPE_VPN_SERVICE
+import android.net.VpnManager.TYPE_VPN_LEGACY
 import android.net.VpnTransportInfo
 import android.os.Build
 import androidx.test.filters.SmallTest
@@ -49,19 +51,19 @@
 private const val TIMEOUT_MS = 1_000L
 private const val LONG_TIMEOUT_MS = 5_000
 
-private fun vpnNc() = NetworkCapabilities.Builder()
-        .addTransportType(TRANSPORT_VPN)
-        .removeCapability(NET_CAPABILITY_NOT_VPN)
-        .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
-        .setTransportInfo(
-                VpnTransportInfo(
-                        TYPE_VPN_SERVICE,
-                        "MySession12345",
-                        false /* bypassable */,
-                        false /* longLivedTcpConnectionsExpensive */
-                )
-        )
-        .build()
+private fun vpnNc(vpnType: Int = TYPE_VPN_SERVICE) = NetworkCapabilities.Builder().apply {
+    addTransportType(TRANSPORT_VPN)
+    removeCapability(NET_CAPABILITY_NOT_VPN)
+    addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+    setTransportInfo(
+            VpnTransportInfo(
+                    vpnType,
+                    "MySession12345",
+                    false /* bypassable */,
+                    false /* longLivedTcpConnectionsExpensive */
+            )
+    )
+}.build()
 
 private fun wifiNc() = NetworkCapabilities.Builder()
         .addTransportType(TRANSPORT_WIFI)
@@ -310,4 +312,38 @@
         // IngressDiscardRule should not be added since feature is disabled
         verify(bpfNetMaps, never()).setIngressDiscardRule(any(), any())
     }
+
+    fun doTestVpnIngressDiscardRule_VpnType(vpnType: Int, expectAddRule: Boolean) {
+        val nr = nr(TRANSPORT_VPN)
+        val cb = TestableNetworkCallback()
+        cm.registerNetworkCallback(nr, cb)
+        val nc = vpnNc(vpnType)
+        val lp = lp(VPN_IFNAME, IPV6_LINK_ADDRESS, LOCAL_IPV6_LINK_ADDRRESS)
+        val agent = Agent(nc = nc, lp = lp)
+        agent.connect()
+        cb.expectAvailableCallbacks(agent.network, validated = false)
+
+        if (expectAddRule) {
+            verify(bpfNetMaps).setIngressDiscardRule(IPV6_ADDRESS, VPN_IFNAME)
+        } else {
+            verify(bpfNetMaps, never()).setIngressDiscardRule(any(), any())
+        }
+    }
+
+    @Test
+    fun testVpnIngressDiscardRule_ServiceVpn() {
+        doTestVpnIngressDiscardRule_VpnType(TYPE_VPN_SERVICE, expectAddRule = true)
+    }
+
+    @Test
+    fun testVpnIngressDiscardRule_LegacyVpn() {
+        // IngressDiscardRule should not be added to Legacy VPN
+        doTestVpnIngressDiscardRule_VpnType(TYPE_VPN_LEGACY, expectAddRule = false)
+    }
+
+    @Test
+    fun testVpnIngressDiscardRule_OemVpn() {
+        // IngressDiscardRule should not be added to OEM VPN
+        doTestVpnIngressDiscardRule_VpnType(TYPE_VPN_OEM, expectAddRule = false)
+    }
 }
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSQueuedCallbacksTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSQueuedCallbacksTest.kt
new file mode 100644
index 0000000..fc2a06c
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivityservice/CSQueuedCallbacksTest.kt
@@ -0,0 +1,636 @@
+/*
+ * 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.connectivityservice
+
+import android.app.ActivityManager.UidFrozenStateChangedCallback
+import android.app.ActivityManager.UidFrozenStateChangedCallback.UID_FROZEN_STATE_FROZEN
+import android.app.ActivityManager.UidFrozenStateChangedCallback.UID_FROZEN_STATE_UNFROZEN
+import android.net.ConnectivityManager.BLOCKED_METERED_REASON_DATA_SAVER
+import android.net.ConnectivityManager.BLOCKED_REASON_NONE
+import android.net.ConnectivitySettingsManager
+import android.net.INetworkMonitor.NETWORK_VALIDATION_PROBE_DNS
+import android.net.INetworkMonitor.NETWORK_VALIDATION_PROBE_HTTPS
+import android.net.INetworkMonitor.NETWORK_VALIDATION_RESULT_VALID
+import android.net.LinkAddress
+import android.net.LinkProperties
+import android.net.LocalNetworkConfig
+import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.NET_CAPABILITY_FOREGROUND
+import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
+import android.net.NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED
+import android.net.NetworkCapabilities.NET_CAPABILITY_TEMPORARILY_NOT_METERED
+import android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED
+import android.net.NetworkCapabilities.TRANSPORT_CELLULAR
+import android.net.NetworkCapabilities.TRANSPORT_ETHERNET
+import android.net.NetworkCapabilities.TRANSPORT_WIFI
+import android.net.NetworkPolicyManager.NetworkPolicyCallback
+import android.net.NetworkRequest
+import android.os.Build
+import android.os.Process
+import com.android.server.CALLING_UID_UNMOCKED
+import com.android.server.CSAgentWrapper
+import com.android.server.CSTest
+import com.android.server.FromS
+import com.android.server.HANDLER_TIMEOUT_MS
+import com.android.server.connectivity.ConnectivityFlags.QUEUE_CALLBACKS_FOR_FROZEN_APPS
+import com.android.server.defaultLp
+import com.android.server.defaultNc
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.RecorderCallback.CallbackEntry.BlockedStatus
+import com.android.testutils.RecorderCallback.CallbackEntry.CapabilitiesChanged
+import com.android.testutils.RecorderCallback.CallbackEntry.LinkPropertiesChanged
+import com.android.testutils.RecorderCallback.CallbackEntry.LocalInfoChanged
+import com.android.testutils.RecorderCallback.CallbackEntry.Lost
+import com.android.testutils.RecorderCallback.CallbackEntry.Resumed
+import com.android.testutils.RecorderCallback.CallbackEntry.Suspended
+import com.android.testutils.TestableNetworkCallback
+import com.android.testutils.visibleOnHandlerThread
+import com.android.testutils.waitForIdleSerialExecutor
+import java.util.Collections
+import kotlin.test.fail
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+import org.mockito.ArgumentCaptor
+import org.mockito.Mockito.any
+import org.mockito.Mockito.verify
+
+private const val TEST_UID = 42
+
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
+@DevSdkIgnoreRunner.MonitorThreadLeak
+@RunWith(DevSdkIgnoreRunner::class)
+class CSQueuedCallbacksTest(freezingBehavior: FreezingBehavior) : CSTest() {
+    companion object {
+        enum class FreezingBehavior {
+            UID_FROZEN,
+            UID_NOT_FROZEN,
+            UID_FROZEN_FEATURE_DISABLED
+        }
+
+        // Use a parameterized test with / without freezing to make it easy to compare and make sure
+        // freezing behavior (which callbacks are sent in which order) stays close to what happens
+        // without freezing.
+        @JvmStatic
+        @Parameterized.Parameters(name = "freezingBehavior={0}")
+        fun freezingBehavior() = listOf(
+            FreezingBehavior.UID_FROZEN,
+            FreezingBehavior.UID_NOT_FROZEN,
+            FreezingBehavior.UID_FROZEN_FEATURE_DISABLED
+        )
+
+        private val TAG = CSQueuedCallbacksTest::class.simpleName
+            ?: fail("Could not get test class name")
+    }
+
+    @get:Rule
+    val ignoreRule = DevSdkIgnoreRule()
+
+    private val mockedBlockedReasonsPerUid = Collections.synchronizedMap(mutableMapOf(
+        Process.myUid() to BLOCKED_REASON_NONE,
+        TEST_UID to BLOCKED_REASON_NONE
+    ))
+
+    private val freezeUids = freezingBehavior != FreezingBehavior.UID_NOT_FROZEN
+    private val expectAllCallbacks = freezingBehavior == FreezingBehavior.UID_NOT_FROZEN ||
+            freezingBehavior == FreezingBehavior.UID_FROZEN_FEATURE_DISABLED
+    init {
+        setFeatureEnabled(
+            QUEUE_CALLBACKS_FOR_FROZEN_APPS,
+            freezingBehavior != FreezingBehavior.UID_FROZEN_FEATURE_DISABLED
+        )
+    }
+
+    @Before
+    fun subclassSetUp() {
+        // Ensure cellular stays up. CS is recreated for each test so no cleanup is necessary.
+//        cm.requestNetwork(
+//            NetworkRequest.Builder().addTransportType(TRANSPORT_CELLULAR).build(),
+//            TestableNetworkCallback()
+//        )
+    }
+
+    @Test
+    fun testFrozenWhileNetworkConnects_UpdatesAreReceived() {
+        val cb = TestableNetworkCallback(logTag = TAG)
+        withCallingUid(TEST_UID) {
+            cm.registerNetworkCallback(NetworkRequest.Builder().build(), cb)
+        }
+        val agent = Agent(TRANSPORT_CELLULAR).apply { connect() }
+        val lpChangeOnConnect = agent.sendLpChange { setLinkAddresses("fe80:db8::123/64") }
+        val ncChangeOnConnect = agent.sendNcChange {
+            addCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED)
+        }
+
+        maybeSetUidsFrozen(true, TEST_UID)
+
+        val lpChange1WhileFrozen = agent.sendLpChange {
+            setLinkAddresses("fe80:db8::126/64")
+        }
+        val ncChange1WhileFrozen = agent.sendNcChange {
+            removeCapability(NET_CAPABILITY_NOT_ROAMING)
+        }
+        val ncChange2WhileFrozen = agent.sendNcChange {
+            addCapability(NET_CAPABILITY_NOT_ROAMING)
+            addCapability(NET_CAPABILITY_NOT_CONGESTED)
+        }
+        val lpChange2WhileFrozen = agent.sendLpChange {
+            setLinkAddresses("fe80:db8::125/64")
+        }
+        maybeSetUidsFrozen(false, TEST_UID)
+
+        // Verify callbacks that are sent before freezing
+        cb.expectAvailableCallbacks(agent.network, validated = false)
+        cb.expectLpWith(agent, lpChangeOnConnect)
+        cb.expectNcWith(agent, ncChangeOnConnect)
+
+        // Below callbacks should be skipped if the processes were frozen, since a single callback
+        // will be sent with the latest state after unfreezing
+        if (expectAllCallbacks) {
+            cb.expectLpWith(agent, lpChange1WhileFrozen)
+            cb.expectNcWith(agent, ncChange1WhileFrozen)
+        }
+
+        cb.expectNcWith(agent, ncChange2WhileFrozen)
+        cb.expectLpWith(agent, lpChange2WhileFrozen)
+
+        cb.assertNoCallback(timeoutMs = 0L)
+    }
+
+    @Test
+    fun testFrozenWhileNetworkConnects_SuspendedUnsuspendedWhileFrozen() {
+        val cb = TestableNetworkCallback(logTag = TAG)
+        withCallingUid(TEST_UID) {
+            cm.registerNetworkCallback(NetworkRequest.Builder().build(), cb)
+        }
+
+        val agent = Agent(TRANSPORT_CELLULAR).apply { connect() }
+        maybeSetUidsFrozen(true, TEST_UID)
+        val rmCap = agent.sendNcChange { removeCapability(NET_CAPABILITY_NOT_SUSPENDED) }
+        val addCap = agent.sendNcChange { addCapability(NET_CAPABILITY_NOT_SUSPENDED) }
+
+        maybeSetUidsFrozen(false, TEST_UID)
+
+        cb.expectAvailableCallbacks(agent.network, validated = false)
+        if (expectAllCallbacks) {
+            cb.expectNcWith(agent, rmCap)
+            cb.expect<Suspended>(agent)
+            cb.expectNcWith(agent, addCap)
+            cb.expect<Resumed>(agent)
+        } else {
+            // When frozen, a single NetworkCapabilitiesChange will be sent at unfreezing time,
+            // with nc actually identical to the original ones. This is because NetworkCapabilities
+            // callbacks were sent, but CS does not keep initial NetworkCapabilities in memory, so
+            // it cannot detect A->B->A.
+            cb.expect<CapabilitiesChanged>(agent) {
+                it.caps.hasCapability(NET_CAPABILITY_NOT_SUSPENDED)
+            }
+        }
+        cb.assertNoCallback(timeoutMs = 0L)
+    }
+
+    @Test
+    fun testFrozenWhileNetworkConnects_UnsuspendedWhileFrozen_GetResumedCallbackWhenUnfrozen() {
+        val cb = TestableNetworkCallback(logTag = TAG)
+        withCallingUid(TEST_UID) {
+            cm.registerNetworkCallback(NetworkRequest.Builder().build(), cb)
+        }
+
+        val agent = Agent(TRANSPORT_CELLULAR).apply { connect() }
+        val rmCap = agent.sendNcChange { removeCapability(NET_CAPABILITY_NOT_SUSPENDED) }
+        maybeSetUidsFrozen(true, TEST_UID)
+        val addCap = agent.sendNcChange { addCapability(NET_CAPABILITY_NOT_SUSPENDED) }
+        maybeSetUidsFrozen(false, TEST_UID)
+
+        cb.expectAvailableCallbacks(agent.network, validated = false)
+        cb.expectNcWith(agent, rmCap)
+        cb.expect<Suspended>(agent)
+        cb.expectNcWith(agent, addCap)
+        cb.expect<Resumed>(agent)
+        cb.assertNoCallback(timeoutMs = 0L)
+    }
+
+    @Test
+    fun testFrozenWhileNetworkConnects_BlockedUnblockedWhileFrozen_SingleCallbackIfFrozen() {
+        val cb = TestableNetworkCallback(logTag = TAG)
+        withCallingUid(TEST_UID) {
+            cm.registerNetworkCallback(NetworkRequest.Builder().build(), cb)
+        }
+        val agent = Agent(TRANSPORT_CELLULAR).apply { connect() }
+
+        maybeSetUidsFrozen(true, TEST_UID)
+        setUidsBlockedForDataSaver(true, TEST_UID)
+        setUidsBlockedForDataSaver(false, TEST_UID)
+        maybeSetUidsFrozen(false, TEST_UID)
+
+        cb.expectAvailableCallbacks(agent.network, validated = false)
+        if (expectAllCallbacks) {
+            cb.expect<BlockedStatus>(agent) { it.blocked }
+        }
+        // The unblocked callback is sent in any case (with the latest blocked reason), as the
+        // blocked reason may have changed, and ConnectivityService cannot know that it is the same
+        // as the original reason as it does not keep pre-freeze blocked reasons in memory.
+        cb.expect<BlockedStatus>(agent) { !it.blocked }
+        cb.assertNoCallback(timeoutMs = 0L)
+    }
+
+    @Test
+    fun testFrozenWhileNetworkConnects_BlockedWhileFrozen_GetLastBlockedCallbackOnlyIfFrozen() {
+        val cb = TestableNetworkCallback(logTag = TAG)
+        withCallingUid(TEST_UID) {
+            cm.registerNetworkCallback(NetworkRequest.Builder().build(), cb)
+        }
+        val agent = Agent(TRANSPORT_CELLULAR).apply { connect() }
+
+        maybeSetUidsFrozen(true, TEST_UID)
+        setUidsBlockedForDataSaver(true, TEST_UID)
+        setUidsBlockedForDataSaver(false, TEST_UID)
+        setUidsBlockedForDataSaver(true, TEST_UID)
+        maybeSetUidsFrozen(false, TEST_UID)
+
+        cb.expectAvailableCallbacks(agent.network, validated = false)
+        if (expectAllCallbacks) {
+            cb.expect<BlockedStatus>(agent) { it.blocked }
+            cb.expect<BlockedStatus>(agent) { !it.blocked }
+        }
+        cb.expect<BlockedStatus>(agent) { it.blocked }
+        cb.assertNoCallback(timeoutMs = 0L)
+    }
+
+    @Test
+    fun testNetworkCallback_NetworkToggledWhileFrozen_NotSeen() {
+        val cb = TestableNetworkCallback(logTag = TAG)
+        withCallingUid(TEST_UID) {
+            cm.registerNetworkCallback(NetworkRequest.Builder().build(), cb)
+        }
+        val cellAgent = Agent(TRANSPORT_CELLULAR).apply { connect() }
+        maybeSetUidsFrozen(true, TEST_UID)
+        val wifiAgent = Agent(TRANSPORT_WIFI).apply { connect() }
+        wifiAgent.disconnect()
+        maybeSetUidsFrozen(false, TEST_UID)
+
+        cb.expectAvailableCallbacks(cellAgent.network, validated = false)
+        if (expectAllCallbacks) {
+            cb.expectAvailableCallbacks(wifiAgent.network, validated = false)
+            cb.expect<Lost>(wifiAgent)
+        }
+        cb.assertNoCallback(timeoutMs = 0L)
+    }
+
+    @Test
+    fun testNetworkCallback_NetworkAppearedWhileFrozen_ReceiveLatestInfoInOnAvailable() {
+        val cb = TestableNetworkCallback(logTag = TAG)
+        withCallingUid(TEST_UID) {
+            cm.registerNetworkCallback(NetworkRequest.Builder().build(), cb)
+        }
+        maybeSetUidsFrozen(true, TEST_UID)
+        val agent = Agent(TRANSPORT_CELLULAR).apply { connect() }
+        waitForIdle()
+        agent.makeValidationSuccess()
+        val lpChange = agent.sendLpChange {
+            setLinkAddresses("fe80:db8::123/64")
+        }
+        val suspendedChange = agent.sendNcChange {
+            removeCapability(NET_CAPABILITY_NOT_SUSPENDED)
+        }
+        setUidsBlockedForDataSaver(true, TEST_UID)
+
+        maybeSetUidsFrozen(false, TEST_UID)
+
+        val expectLatestStatusInOnAvailable = !expectAllCallbacks
+        cb.expectAvailableCallbacks(
+            agent.network,
+            suspended = expectLatestStatusInOnAvailable,
+            validated = expectLatestStatusInOnAvailable,
+            blocked = expectLatestStatusInOnAvailable
+        )
+        if (expectAllCallbacks) {
+            cb.expectNcWith(agent) { addCapability(NET_CAPABILITY_VALIDATED) }
+            cb.expectLpWith(agent, lpChange)
+            cb.expectNcWith(agent, suspendedChange)
+            cb.expect<Suspended>(agent)
+            cb.expect<BlockedStatus>(agent) { it.blocked }
+        }
+        cb.assertNoCallback(timeoutMs = 0L)
+    }
+
+    @Test
+    @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    fun testNetworkCallback_LocalNetworkAppearedWhileFrozen_ReceiveLatestInfoInOnAvailable() {
+        val cb = TestableNetworkCallback(logTag = TAG)
+        withCallingUid(TEST_UID) {
+            cm.registerNetworkCallback(
+                NetworkRequest.Builder().addCapability(NET_CAPABILITY_LOCAL_NETWORK).build(),
+                cb
+            )
+        }
+        val upstreamAgent = Agent(
+            nc = defaultNc()
+                .addTransportType(TRANSPORT_WIFI)
+                .addCapability(NET_CAPABILITY_INTERNET),
+            lp = defaultLp().apply { interfaceName = "wlan0" }
+        ).apply { connect() }
+        maybeSetUidsFrozen(true, TEST_UID)
+
+        val lnc = LocalNetworkConfig.Builder().build()
+        val localAgent = Agent(
+            nc = defaultNc()
+                .addCapability(NET_CAPABILITY_LOCAL_NETWORK)
+                .removeCapability(NET_CAPABILITY_INTERNET),
+            lp = defaultLp().apply { interfaceName = "local42" },
+            lnc = FromS(lnc)
+        ).apply { connect() }
+        localAgent.sendLocalNetworkConfig(
+            LocalNetworkConfig.Builder()
+                .setUpstreamSelector(
+                    NetworkRequest.Builder()
+                        .addCapability(NET_CAPABILITY_INTERNET)
+                        .build()
+                )
+                .build()
+        )
+
+        maybeSetUidsFrozen(false, TEST_UID)
+
+        cb.expectAvailableCallbacks(
+            localAgent.network,
+            validated = false,
+            upstream = if (expectAllCallbacks) null else upstreamAgent.network
+        )
+        if (expectAllCallbacks) {
+            cb.expect<LocalInfoChanged>(localAgent) {
+                it.info.upstreamNetwork == upstreamAgent.network
+            }
+        }
+        cb.assertNoCallback(timeoutMs = 0L)
+    }
+
+    @Test
+    fun testNetworkRequest_NetworkSwitchesWhileFrozen_ReceiveLastNetworkUpdatesOnly() {
+        val cb = TestableNetworkCallback(logTag = TAG)
+        withCallingUid(TEST_UID) {
+            cm.requestNetwork(NetworkRequest.Builder().build(), cb)
+        }
+        val cellAgent = Agent(TRANSPORT_CELLULAR).apply { connect() }
+        maybeSetUidsFrozen(true, TEST_UID)
+        val wifiAgent = Agent(TRANSPORT_WIFI).apply { connect() }
+        val ethAgent = Agent(TRANSPORT_ETHERNET).apply { connect() }
+        waitForIdle()
+        ethAgent.makeValidationSuccess()
+        maybeSetUidsFrozen(false, TEST_UID)
+
+        cb.expectAvailableCallbacks(cellAgent.network, validated = false)
+        if (expectAllCallbacks) {
+            cb.expectAvailableCallbacks(wifiAgent.network, validated = false)
+            cb.expectAvailableCallbacks(ethAgent.network, validated = false)
+            cb.expectNcWith(ethAgent) { addCapability(NET_CAPABILITY_VALIDATED) }
+        } else {
+            cb.expectAvailableCallbacks(ethAgent.network, validated = true)
+        }
+        cb.assertNoCallback(timeoutMs = 0L)
+    }
+
+    @Test
+    fun testNetworkRequest_NetworkSwitchesBackWhileFrozen_ReceiveNoAvailableCallback() {
+        val cb = TestableNetworkCallback(logTag = TAG)
+        withCallingUid(TEST_UID) {
+            cm.requestNetwork(NetworkRequest.Builder().build(), cb)
+        }
+        val cellAgent = Agent(TRANSPORT_CELLULAR).apply { connect() }
+        maybeSetUidsFrozen(true, TEST_UID)
+        val wifiAgent = Agent(TRANSPORT_WIFI).apply { connect() }
+        waitForIdle()
+
+        // CS switches back to validated cell over non-validated Wi-Fi
+        cellAgent.makeValidationSuccess()
+        val cellLpChange = cellAgent.sendLpChange {
+            setLinkAddresses("fe80:db8::123/64")
+        }
+        setUidsBlockedForDataSaver(true, TEST_UID)
+        maybeSetUidsFrozen(false, TEST_UID)
+
+        cb.expectAvailableCallbacks(cellAgent.network, validated = false)
+        if (expectAllCallbacks) {
+            cb.expectAvailableCallbacks(wifiAgent.network, validated = false)
+            // There is an extra "double validated" CapabilitiesChange callback (b/245893397), so
+            // callbacks are (AVAIL, NC, LP), extra NC, then further updates (LP and BLK here).
+            cb.expectAvailableDoubleValidatedCallbacks(cellAgent.network)
+            cb.expectLpWith(cellAgent, cellLpChange)
+            cb.expect<BlockedStatus>(cellAgent) { it.blocked }
+        } else {
+            cb.expectNcWith(cellAgent) {
+                addCapability(NET_CAPABILITY_VALIDATED)
+            }
+            cb.expectLpWith(cellAgent, cellLpChange)
+            cb.expect<BlockedStatus>(cellAgent) { it.blocked }
+        }
+        cb.assertNoCallback(timeoutMs = 0L)
+    }
+
+    @Test
+    fun testTrackDefaultRequest_AppFrozenWhilePerAppDefaultRequestFiled_ReceiveChangeCallbacks() {
+        val cellAgent = Agent(TRANSPORT_CELLULAR, baseNc = makeInternetNc()).apply { connect() }
+        waitForIdle()
+
+        val cb = TestableNetworkCallback(logTag = TAG)
+        withCallingUid(TEST_UID) {
+            cm.registerDefaultNetworkCallback(cb)
+        }
+        maybeSetUidsFrozen(true, TEST_UID)
+
+        // Change LinkProperties twice before the per-app network request is applied
+        val lpChange1 = cellAgent.sendLpChange {
+            setLinkAddresses("fe80:db8::123/64")
+        }
+        val lpChange2 = cellAgent.sendLpChange {
+            setLinkAddresses("fe80:db8::124/64")
+        }
+        setMobileDataPreferredUids(setOf(TEST_UID))
+
+        // Change NetworkCapabilities after the per-app network request is applied
+        val ncChange = cellAgent.sendNcChange {
+            addCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED)
+        }
+        maybeSetUidsFrozen(false, TEST_UID)
+
+        // Even if a per-app network request was filed to replace the default network request for
+        // the app, all network change callbacks are received
+        cb.expectAvailableCallbacks(cellAgent.network, validated = false)
+        if (expectAllCallbacks) {
+            cb.expectLpWith(cellAgent, lpChange1)
+        }
+        cb.expectLpWith(cellAgent, lpChange2)
+        cb.expectNcWith(cellAgent, ncChange)
+        cb.assertNoCallback(timeoutMs = 0L)
+    }
+
+    @Test
+    fun testTrackDefaultRequest_AppFrozenWhilePerAppDefaultToggled_GetStatusUpdateCallbacksOnly() {
+        // Add validated Wi-Fi and non-validated cell, expect Wi-Fi is preferred by default
+        val wifiAgent = Agent(TRANSPORT_WIFI, baseNc = makeInternetNc()).apply { connect() }
+        wifiAgent.makeValidationSuccess()
+        val cellAgent = Agent(TRANSPORT_CELLULAR, baseNc = makeInternetNc()).apply { connect() }
+        waitForIdle()
+
+        val cb = TestableNetworkCallback(logTag = TAG)
+        withCallingUid(TEST_UID) {
+            cm.registerDefaultNetworkCallback(cb)
+        }
+        maybeSetUidsFrozen(true, TEST_UID)
+
+        // LP change on the original Wi-Fi network
+        val lpChange = wifiAgent.sendLpChange {
+            setLinkAddresses("fe80:db8::123/64")
+        }
+        // Set per-app default to cell, then unset it
+        setMobileDataPreferredUids(setOf(TEST_UID))
+        setMobileDataPreferredUids(emptySet())
+
+        maybeSetUidsFrozen(false, TEST_UID)
+
+        cb.expectAvailableCallbacks(wifiAgent.network)
+        if (expectAllCallbacks) {
+            cb.expectLpWith(wifiAgent, lpChange)
+            cb.expectAvailableCallbacks(cellAgent.network, validated = false)
+            // Cellular stops being foreground since it is now matched for this app
+            cb.expect<CapabilitiesChanged> { it.caps.hasCapability(NET_CAPABILITY_FOREGROUND) }
+            cb.expectAvailableCallbacks(wifiAgent.network)
+        } else {
+            // After switching to cell and back while frozen, only network attribute update
+            // callbacks (and not AVAILABLE) for the original Wi-Fi network should be sent
+            cb.expect<CapabilitiesChanged>(wifiAgent)
+            cb.expectLpWith(wifiAgent, lpChange)
+            cb.expect<BlockedStatus> { !it.blocked }
+        }
+        cb.assertNoCallback(timeoutMs = 0L)
+    }
+
+    private fun setUidsBlockedForDataSaver(blocked: Boolean, vararg uid: Int) {
+        val reason = if (blocked) {
+            BLOCKED_METERED_REASON_DATA_SAVER
+        } else {
+            BLOCKED_REASON_NONE
+        }
+        if (deps.isAtLeastV) {
+            visibleOnHandlerThread(csHandler) {
+                service.handleBlockedReasonsChanged(uid.map { android.util.Pair(it, reason) })
+            }
+        } else {
+            notifyLegacyBlockedReasonChanged(reason, uid)
+            waitForIdle()
+        }
+    }
+
+    @Suppress("DEPRECATION")
+    private fun notifyLegacyBlockedReasonChanged(reason: Int, uids: IntArray) {
+        val cbCaptor = ArgumentCaptor.forClass(NetworkPolicyCallback::class.java)
+        verify(context.networkPolicyManager).registerNetworkPolicyCallback(
+            any(),
+            cbCaptor.capture()
+        )
+        uids.forEach {
+            cbCaptor.value.onUidBlockedReasonChanged(it, reason)
+        }
+    }
+
+    private fun withCallingUid(uid: Int, action: () -> Unit) {
+        deps.callingUid = uid
+        action()
+        deps.callingUid = CALLING_UID_UNMOCKED
+    }
+
+    private fun getUidFrozenStateChangedCallback(): UidFrozenStateChangedCallback {
+        val captor = ArgumentCaptor.forClass(UidFrozenStateChangedCallback::class.java)
+        verify(activityManager).registerUidFrozenStateChangedCallback(any(), captor.capture())
+        return captor.value
+    }
+
+    private fun maybeSetUidsFrozen(frozen: Boolean, vararg uids: Int) {
+        if (!freezeUids) return
+        val state = if (frozen) UID_FROZEN_STATE_FROZEN else UID_FROZEN_STATE_UNFROZEN
+        getUidFrozenStateChangedCallback()
+            .onUidFrozenStateChanged(uids, IntArray(uids.size) { state })
+        waitForIdle()
+    }
+
+    private fun CSAgentWrapper.makeValidationSuccess() {
+        setValidationResult(
+            NETWORK_VALIDATION_RESULT_VALID,
+            probesCompleted = NETWORK_VALIDATION_PROBE_DNS or NETWORK_VALIDATION_PROBE_HTTPS,
+            probesSucceeded = NETWORK_VALIDATION_PROBE_DNS or NETWORK_VALIDATION_PROBE_HTTPS
+        )
+        cm.reportNetworkConnectivity(network, true)
+        // Ensure validation is scheduled
+        waitForIdle()
+        // Ensure validation completes on mock executor
+        waitForIdleSerialExecutor(CSTestExecutor, HANDLER_TIMEOUT_MS)
+        // Ensure validation results are processed
+        waitForIdle()
+    }
+
+    private fun setMobileDataPreferredUids(uids: Set<Int>) {
+        ConnectivitySettingsManager.setMobileDataPreferredUids(context, uids)
+        service.updateMobileDataPreferredUids()
+        waitForIdle()
+    }
+}
+
+private fun makeInternetNc() = NetworkCapabilities.Builder(defaultNc())
+    .addCapability(NET_CAPABILITY_INTERNET)
+    .build()
+
+private fun CSAgentWrapper.sendLpChange(
+    mutator: LinkProperties.() -> Unit
+): LinkProperties.() -> Unit {
+    lp.mutator()
+    sendLinkProperties(lp)
+    return mutator
+}
+
+private fun CSAgentWrapper.sendNcChange(
+    mutator: NetworkCapabilities.() -> Unit
+): NetworkCapabilities.() -> Unit {
+    nc.mutator()
+    sendNetworkCapabilities(nc)
+    return mutator
+}
+
+private fun TestableNetworkCallback.expectLpWith(
+    agent: CSAgentWrapper,
+    change: LinkProperties.() -> Unit
+) = expect<LinkPropertiesChanged>(agent) {
+    // This test uses changes that are no-op when already applied (idempotent): verify that the
+    // change is already applied.
+    it.lp == LinkProperties(it.lp).apply(change)
+}
+
+private fun TestableNetworkCallback.expectNcWith(
+    agent: CSAgentWrapper,
+    change: NetworkCapabilities.() -> Unit
+) = expect<CapabilitiesChanged>(agent) {
+    it.caps == NetworkCapabilities(it.caps).apply(change)
+}
+
+private fun LinkProperties.setLinkAddresses(vararg addrs: String) {
+    setLinkAddresses(addrs.map { LinkAddress(it) })
+}
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSSatelliteNetworkTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSSatelliteNetworkTest.kt
index 88c2738..5ca7fcc 100644
--- a/tests/unit/java/com/android/server/connectivityservice/CSSatelliteNetworkTest.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/CSSatelliteNetworkTest.kt
@@ -54,6 +54,7 @@
 import org.mockito.ArgumentMatchers.any
 import org.mockito.Mockito.inOrder
 import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
 import kotlin.test.assertEquals
 import kotlin.test.assertTrue
 
@@ -162,6 +163,43 @@
         doTestSatelliteNeverBecomeDefaultNetwork(restricted = false)
     }
 
+    private fun doTestUnregisterAfterReplacementSatisfier(destroyed: Boolean) {
+        val satelliteAgent = createSatelliteAgent("satellite0")
+        satelliteAgent.connect()
+
+        val uids = setOf(TEST_PACKAGE_UID)
+        updateSatelliteNetworkFallbackUids(uids)
+
+        if (destroyed) {
+            satelliteAgent.unregisterAfterReplacement(timeoutMs = 5000)
+        }
+
+        updateSatelliteNetworkFallbackUids(setOf())
+        if (destroyed) {
+            // If the network is already destroyed, networkRemoveUidRangesParcel should not be
+            // called.
+            verify(netd, never()).networkRemoveUidRangesParcel(any())
+        } else {
+            verify(netd).networkRemoveUidRangesParcel(
+                    NativeUidRangeConfig(
+                            satelliteAgent.network.netId,
+                            toUidRangeStableParcels(uidRangesForUids(uids)),
+                            PREFERENCE_ORDER_SATELLITE_FALLBACK
+                    )
+            )
+        }
+    }
+
+    @Test
+    fun testUnregisterAfterReplacementSatisfier_destroyed() {
+        doTestUnregisterAfterReplacementSatisfier(destroyed = true)
+    }
+
+    @Test
+    fun testUnregisterAfterReplacementSatisfier_notDestroyed() {
+        doTestUnregisterAfterReplacementSatisfier(destroyed = false)
+    }
+
     private fun assertCreateMultiLayerNrisFromSatelliteNetworkPreferredUids(uids: Set<Int>) {
         val nris: Set<ConnectivityService.NetworkRequestInfo> =
             service.createMultiLayerNrisFromSatelliteNetworkFallbackUids(uids)
diff --git a/tests/unit/java/com/android/server/connectivityservice/base/CSAgentWrapper.kt b/tests/unit/java/com/android/server/connectivityservice/base/CSAgentWrapper.kt
index 13c5cbc..1f5ee32 100644
--- a/tests/unit/java/com/android/server/connectivityservice/base/CSAgentWrapper.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/base/CSAgentWrapper.kt
@@ -192,7 +192,8 @@
         connect()
     }
 
-    fun setProbesStatus(probesCompleted: Int, probesSucceeded: Int) {
+    fun setValidationResult(result: Int, probesCompleted: Int, probesSucceeded: Int) {
+        nmValidationResult = result
         nmProbesCompleted = probesCompleted
         nmProbesSucceeded = probesSucceeded
     }
@@ -204,8 +205,10 @@
         // in the beginning. Because NETWORK_VALIDATION_PROBE_HTTP is the decisive probe for captive
         // portal, considering the NETWORK_VALIDATION_PROBE_HTTPS hasn't probed yet and set only
         // DNS and HTTP probes completed.
-        setProbesStatus(
-            NETWORK_VALIDATION_PROBE_DNS or NETWORK_VALIDATION_PROBE_HTTP /* probesCompleted */,
-            VALIDATION_RESULT_INVALID /* probesSucceeded */)
+        setValidationResult(
+            VALIDATION_RESULT_INVALID,
+            probesCompleted = NETWORK_VALIDATION_PROBE_DNS or NETWORK_VALIDATION_PROBE_HTTP,
+            probesSucceeded = NO_PROBE_RESULT
+        )
     }
 }
diff --git a/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt b/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt
index ed72fd2..46c25d2 100644
--- a/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt
@@ -165,6 +165,7 @@
         it[ConnectivityFlags.BACKGROUND_FIREWALL_CHAIN] = true
         it[ConnectivityFlags.DELAY_DESTROY_SOCKETS] = true
         it[ConnectivityFlags.USE_DECLARED_METHODS_FOR_CALLBACKS] = true
+        it[ConnectivityFlags.QUEUE_CALLBACKS_FOR_FROZEN_APPS] = true
     }
     fun setFeatureEnabled(flag: String, enabled: Boolean) = enabledFeatures.set(flag, enabled)
 
@@ -537,8 +538,12 @@
             provider: NetworkProvider? = null
     ) = CSAgentWrapper(context, deps, csHandlerThread, networkStack,
             nac, nc, lp, lnc, score, provider)
-    fun Agent(vararg transports: Int, lp: LinkProperties = defaultLp()): CSAgentWrapper {
-        val nc = NetworkCapabilities.Builder().apply {
+    fun Agent(
+        vararg transports: Int,
+        baseNc: NetworkCapabilities = defaultNc(),
+        lp: LinkProperties = defaultLp()
+    ): CSAgentWrapper {
+        val nc = NetworkCapabilities.Builder(baseNc).apply {
             transports.forEach {
                 addTransportType(it)
             }
diff --git a/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java b/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
index 7e0a225..3d2f389 100644
--- a/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
+++ b/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
@@ -69,6 +69,8 @@
 import static com.android.server.net.NetworkStatsEventLogger.POLL_REASON_RAT_CHANGED;
 import static com.android.server.net.NetworkStatsEventLogger.PollEvent.pollReasonNameOf;
 import static com.android.server.net.NetworkStatsService.ACTION_NETWORK_STATS_POLL;
+import static com.android.server.net.NetworkStatsService.ACTION_NETWORK_STATS_UPDATED;
+import static com.android.server.net.NetworkStatsService.BROADCAST_NETWORK_STATS_UPDATED_RATE_LIMIT_ENABLED_FLAG;
 import static com.android.server.net.NetworkStatsService.DEFAULT_TRAFFIC_STATS_CACHE_EXPIRY_DURATION_MS;
 import static com.android.server.net.NetworkStatsService.DEFAULT_TRAFFIC_STATS_CACHE_MAX_ENTRIES;
 import static com.android.server.net.NetworkStatsService.NETSTATS_FASTDATAINPUT_FALLBACKS_COUNTER_NAME;
@@ -82,6 +84,7 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.AdditionalMatchers.aryEq;
@@ -101,8 +104,10 @@
 
 import android.annotation.NonNull;
 import android.app.AlarmManager;
+import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
+import android.content.IntentFilter;
 import android.content.pm.PackageManager;
 import android.content.res.Resources;
 import android.database.ContentObserver;
@@ -138,6 +143,7 @@
 import android.provider.Settings;
 import android.system.ErrnoException;
 import android.telephony.TelephonyManager;
+import android.testing.TestableLooper;
 import android.text.TextUtils;
 import android.util.ArrayMap;
 import android.util.IndentingPrintWriter;
@@ -150,6 +156,7 @@
 import com.android.connectivity.resources.R;
 import com.android.internal.util.FileRotator;
 import com.android.internal.util.test.BroadcastInterceptingContext;
+import com.android.net.module.util.ArrayTrackRecord;
 import com.android.net.module.util.BpfDump;
 import com.android.net.module.util.IBpfMap;
 import com.android.net.module.util.LocationPermissionChecker;
@@ -618,6 +625,12 @@
         }
 
         @Override
+        public boolean enabledBroadcastNetworkStatsUpdatedRateLimiting(Context ctx) {
+            return mFeatureFlags.getOrDefault(
+                    BROADCAST_NETWORK_STATS_UPDATED_RATE_LIMIT_ENABLED_FLAG, true);
+        }
+
+        @Override
         public int getTrafficStatsRateLimitCacheExpiryDuration() {
             return DEFAULT_TRAFFIC_STATS_CACHE_EXPIRY_DURATION_MS;
         }
@@ -2617,6 +2630,8 @@
 
     private void mockDefaultSettings() throws Exception {
         mockSettings(HOUR_IN_MILLIS, WEEK_IN_MILLIS);
+        mSettings.setBroadcastNetworkStatsUpdateDelayMs(
+                NetworkStatsService.BROADCAST_NETWORK_STATS_UPDATED_DELAY_MS);
     }
 
     private void mockSettings(long bucketDuration, long deleteAge) {
@@ -2631,6 +2646,8 @@
         @NonNull
         private volatile Config mConfig;
         private final AtomicBoolean mCombineSubtypeEnabled = new AtomicBoolean();
+        private long mBroadcastNetworkStatsUpdateDelayMs =
+                NetworkStatsService.BROADCAST_NETWORK_STATS_UPDATED_DELAY_MS;
 
         TestNetworkStatsSettings(long bucketDuration, long deleteAge) {
             mConfig = new Config(bucketDuration, deleteAge, deleteAge);
@@ -2693,6 +2710,15 @@
         public boolean getAugmentEnabled() {
             return false;
         }
+
+        @Override
+        public long getBroadcastNetworkStatsUpdateDelayMs() {
+            return mBroadcastNetworkStatsUpdateDelayMs;
+        }
+
+        public void setBroadcastNetworkStatsUpdateDelayMs(long broadcastDelay) {
+            mBroadcastNetworkStatsUpdateDelayMs = broadcastDelay;
+        }
     }
 
     private void assertStatsFilesExist(boolean exist) {
@@ -3064,4 +3090,91 @@
         final String dump = getDump();
         assertDumpContains(dump, "Log for testing");
     }
+
+    private static class TestNetworkStatsUpdatedReceiver extends BroadcastReceiver {
+        private final ArrayTrackRecord<Intent>.ReadHead mHistory;
+
+        TestNetworkStatsUpdatedReceiver() {
+            mHistory = (new ArrayTrackRecord<Intent>()).newReadHead();
+        }
+
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            mHistory.add(intent);
+        }
+
+        /**
+         * Assert no broadcast intent is received in blocking manner
+         */
+        public void assertNoBroadcastIntentReceived()  {
+            assertNull(mHistory.peek());
+        }
+
+        /**
+         * Assert an intent is received and remove it from queue
+         */
+        public void assertBroadcastIntentReceived() {
+            assertNotNull(mHistory.poll(WAIT_TIMEOUT, number -> true));
+        }
+    }
+
+    @FeatureFlag(name = BROADCAST_NETWORK_STATS_UPDATED_RATE_LIMIT_ENABLED_FLAG)
+    @Test
+    public void testNetworkStatsUpdatedIntentSpam_rateLimitOn() throws Exception {
+        // Set the update delay long enough that messages won't be processed before unblocked
+        // Set a short time to test the behavior before reaching delay.
+        // Constraint: test running time < toleranceMs < update delay time
+        mSettings.setBroadcastNetworkStatsUpdateDelayMs(100_000L);
+        final long toleranceMs = 5000;
+
+        final TestableLooper mTestableLooper = new TestableLooper(mHandlerThread.getLooper());
+        final TestNetworkStatsUpdatedReceiver receiver = new TestNetworkStatsUpdatedReceiver();
+        mServiceContext.registerReceiver(receiver, new IntentFilter(ACTION_NETWORK_STATS_UPDATED));
+
+        try {
+            // Test that before anything, the intent is delivered immediately
+            mService.forceUpdate();
+            mTestableLooper.processAllMessages();
+            receiver.assertBroadcastIntentReceived();
+            receiver.assertNoBroadcastIntentReceived();
+
+            // Test that the next two intents results in exactly one intent delivered
+            for (int i = 0; i < 2; i++) {
+                mService.forceUpdate();
+            }
+            // Test that the delay depends on our set value
+            mTestableLooper.moveTimeForward(mSettings.getBroadcastNetworkStatsUpdateDelayMs()
+                    - toleranceMs);
+            mTestableLooper.processAllMessages();
+            receiver.assertNoBroadcastIntentReceived();
+
+            // Unblock messages and test that the second and third update
+            // is broadcasted right after the delay
+            mTestableLooper.moveTimeForward(toleranceMs);
+            mTestableLooper.processAllMessages();
+            receiver.assertBroadcastIntentReceived();
+            receiver.assertNoBroadcastIntentReceived();
+
+        } finally {
+            mTestableLooper.destroy();
+        }
+    }
+
+    @FeatureFlag(name = BROADCAST_NETWORK_STATS_UPDATED_RATE_LIMIT_ENABLED_FLAG, enabled = false)
+    @Test
+    public void testNetworkStatsUpdatedIntentSpam_rateLimitOff() throws Exception {
+        // Set the update delay long enough to ensure that messages are processed
+        // despite the rate limit.
+        mSettings.setBroadcastNetworkStatsUpdateDelayMs(100_000L);
+
+        final TestNetworkStatsUpdatedReceiver receiver = new TestNetworkStatsUpdatedReceiver();
+        mServiceContext.registerReceiver(receiver, new IntentFilter(ACTION_NETWORK_STATS_UPDATED));
+
+        for (int i = 0; i < 2; i++) {
+            mService.forceUpdate();
+            waitForIdle();
+            receiver.assertBroadcastIntentReceived();
+        }
+        receiver.assertNoBroadcastIntentReceived();
+    }
 }
diff --git a/thread/framework/java/android/net/thread/ActiveOperationalDataset.java b/thread/framework/java/android/net/thread/ActiveOperationalDataset.java
index 22457f5..1b50ba7 100644
--- a/thread/framework/java/android/net/thread/ActiveOperationalDataset.java
+++ b/thread/framework/java/android/net/thread/ActiveOperationalDataset.java
@@ -35,6 +35,7 @@
 import android.util.SparseArray;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.net.thread.flags.Flags;
 
 import java.io.ByteArrayOutputStream;
 import java.net.Inet6Address;
@@ -69,7 +70,7 @@
  *
  * @hide
  */
-@FlaggedApi(ThreadNetworkFlags.FLAG_THREAD_ENABLED)
+@FlaggedApi(Flags.FLAG_THREAD_ENABLED)
 @SystemApi
 public final class ActiveOperationalDataset implements Parcelable {
     /** The maximum length of the Active Operational Dataset TLV array in bytes. */
diff --git a/thread/framework/java/android/net/thread/ChannelMaxPower.aidl b/thread/framework/java/android/net/thread/ChannelMaxPower.aidl
index bcda8a8..abc00b9 100644
--- a/thread/framework/java/android/net/thread/ChannelMaxPower.aidl
+++ b/thread/framework/java/android/net/thread/ChannelMaxPower.aidl
@@ -16,11 +16,11 @@
 
 package android.net.thread;
 
- /**
-  * Mapping from a channel to its max power.
-  *
-  * {@hide}
-  */
+/**
+ * Mapping from a channel to its max power.
+ *
+ * {@hide}
+ */
 parcelable ChannelMaxPower {
     int channel; // The Thread radio channel.
     int maxPower; // The max power in the unit of 0.01dBm. Passing INT16_MAX(32767) will
diff --git a/thread/framework/java/android/net/thread/IOperationalDatasetCallback.aidl b/thread/framework/java/android/net/thread/IOperationalDatasetCallback.aidl
index b576b33..3fece65 100644
--- a/thread/framework/java/android/net/thread/IOperationalDatasetCallback.aidl
+++ b/thread/framework/java/android/net/thread/IOperationalDatasetCallback.aidl
@@ -23,6 +23,8 @@
  * @hide
  */
 oneway interface IOperationalDatasetCallback {
-    void onActiveOperationalDatasetChanged(in @nullable ActiveOperationalDataset activeOpDataset);
-    void onPendingOperationalDatasetChanged(in @nullable PendingOperationalDataset pendingOpDataset);
+    void onActiveOperationalDatasetChanged(
+            in @nullable ActiveOperationalDataset activeOpDataset);
+    void onPendingOperationalDatasetChanged(
+            in @nullable PendingOperationalDataset pendingOpDataset);
 }
diff --git a/thread/framework/java/android/net/thread/IThreadNetworkController.aidl b/thread/framework/java/android/net/thread/IThreadNetworkController.aidl
index f50de74..b7f68c9 100644
--- a/thread/framework/java/android/net/thread/IThreadNetworkController.aidl
+++ b/thread/framework/java/android/net/thread/IThreadNetworkController.aidl
@@ -28,9 +28,9 @@
 import android.net.thread.ThreadConfiguration;
 
 /**
-* Interface for communicating with ThreadNetworkControllerService.
-* @hide
-*/
+ * Interface for communicating with ThreadNetworkControllerService.
+ * @hide
+ */
 interface IThreadNetworkController {
     void registerStateCallback(in IStateCallback callback);
     void unregisterStateCallback(in IStateCallback callback);
@@ -38,10 +38,12 @@
     void unregisterOperationalDatasetCallback(in IOperationalDatasetCallback callback);
 
     void join(in ActiveOperationalDataset activeOpDataset, in IOperationReceiver receiver);
-    void scheduleMigration(in PendingOperationalDataset pendingOpDataset, in IOperationReceiver receiver);
+    void scheduleMigration(
+            in PendingOperationalDataset pendingOpDataset, in IOperationReceiver receiver);
     void leave(in IOperationReceiver receiver);
 
-    void setTestNetworkAsUpstream(in String testNetworkInterfaceName, in IOperationReceiver receiver);
+    void setTestNetworkAsUpstream(
+            in String testNetworkInterfaceName, in IOperationReceiver receiver);
     void setChannelMaxPowers(in ChannelMaxPower[] channelMaxPowers, in IOperationReceiver receiver);
 
     int getThreadVersion();
diff --git a/thread/framework/java/android/net/thread/IThreadNetworkManager.aidl b/thread/framework/java/android/net/thread/IThreadNetworkManager.aidl
index 0e394b1..b63cd72 100644
--- a/thread/framework/java/android/net/thread/IThreadNetworkManager.aidl
+++ b/thread/framework/java/android/net/thread/IThreadNetworkManager.aidl
@@ -19,9 +19,9 @@
 import android.net.thread.IThreadNetworkController;
 
 /**
-* Interface for communicating with ThreadNetworkService.
-* @hide
-*/
+ * Interface for communicating with ThreadNetworkService.
+ * @hide
+ */
 interface IThreadNetworkManager {
     List<IThreadNetworkController> getAllThreadNetworkControllers();
 }
diff --git a/thread/framework/java/android/net/thread/OperationalDatasetTimestamp.java b/thread/framework/java/android/net/thread/OperationalDatasetTimestamp.java
index cecb4e9..489f941 100644
--- a/thread/framework/java/android/net/thread/OperationalDatasetTimestamp.java
+++ b/thread/framework/java/android/net/thread/OperationalDatasetTimestamp.java
@@ -26,6 +26,8 @@
 import android.annotation.Nullable;
 import android.annotation.SystemApi;
 
+import com.android.net.thread.flags.Flags;
+
 import java.nio.ByteBuffer;
 import java.time.Instant;
 import java.util.Objects;
@@ -37,7 +39,7 @@
  * @see PendingOperationalDataset
  * @hide
  */
-@FlaggedApi(ThreadNetworkFlags.FLAG_THREAD_ENABLED)
+@FlaggedApi(Flags.FLAG_THREAD_ENABLED)
 @SystemApi
 public final class OperationalDatasetTimestamp {
     /** @hide */
diff --git a/thread/framework/java/android/net/thread/PendingOperationalDataset.java b/thread/framework/java/android/net/thread/PendingOperationalDataset.java
index c1351af..235e563 100644
--- a/thread/framework/java/android/net/thread/PendingOperationalDataset.java
+++ b/thread/framework/java/android/net/thread/PendingOperationalDataset.java
@@ -27,6 +27,8 @@
 import android.os.Parcelable;
 import android.util.SparseArray;
 
+import com.android.net.thread.flags.Flags;
+
 import java.io.ByteArrayOutputStream;
 import java.nio.ByteBuffer;
 import java.time.Duration;
@@ -42,7 +44,7 @@
  * @see ThreadNetworkController#scheduleMigration
  * @hide
  */
-@FlaggedApi(ThreadNetworkFlags.FLAG_THREAD_ENABLED)
+@FlaggedApi(Flags.FLAG_THREAD_ENABLED)
 @SystemApi
 public final class PendingOperationalDataset implements Parcelable {
     // Value defined in Thread spec 8.10.1.16
diff --git a/thread/framework/java/android/net/thread/ThreadConfiguration.java b/thread/framework/java/android/net/thread/ThreadConfiguration.java
index e09b3a6..1c25535 100644
--- a/thread/framework/java/android/net/thread/ThreadConfiguration.java
+++ b/thread/framework/java/android/net/thread/ThreadConfiguration.java
@@ -15,10 +15,14 @@
  */
 package android.net.thread;
 
+import android.annotation.FlaggedApi;
 import android.annotation.NonNull;
+import android.annotation.SystemApi;
 import android.os.Parcel;
 import android.os.Parcelable;
 
+import com.android.net.thread.flags.Flags;
+
 import java.util.Objects;
 
 /**
@@ -37,19 +41,19 @@
  * @see ThreadNetworkController#unregisterConfigurationCallback
  * @hide
  */
-// @FlaggedApi(ThreadNetworkFlags.FLAG_CONFIGURATION_ENABLED)
-// @SystemApi
+@FlaggedApi(Flags.FLAG_CONFIGURATION_ENABLED)
+@SystemApi
 public final class ThreadConfiguration implements Parcelable {
     private final boolean mNat64Enabled;
-    private final boolean mDhcp6PdEnabled;
+    private final boolean mDhcpv6PdEnabled;
 
     private ThreadConfiguration(Builder builder) {
-        this(builder.mNat64Enabled, builder.mDhcp6PdEnabled);
+        this(builder.mNat64Enabled, builder.mDhcpv6PdEnabled);
     }
 
-    private ThreadConfiguration(boolean nat64Enabled, boolean dhcp6PdEnabled) {
+    private ThreadConfiguration(boolean nat64Enabled, boolean dhcpv6PdEnabled) {
         this.mNat64Enabled = nat64Enabled;
-        this.mDhcp6PdEnabled = dhcp6PdEnabled;
+        this.mDhcpv6PdEnabled = dhcpv6PdEnabled;
     }
 
     /** Returns {@code true} if NAT64 is enabled. */
@@ -58,8 +62,8 @@
     }
 
     /** Returns {@code true} if DHCPv6 Prefix Delegation is enabled. */
-    public boolean isDhcp6PdEnabled() {
-        return mDhcp6PdEnabled;
+    public boolean isDhcpv6PdEnabled() {
+        return mDhcpv6PdEnabled;
     }
 
     @Override
@@ -71,13 +75,13 @@
         } else {
             ThreadConfiguration otherConfig = (ThreadConfiguration) other;
             return mNat64Enabled == otherConfig.mNat64Enabled
-                    && mDhcp6PdEnabled == otherConfig.mDhcp6PdEnabled;
+                    && mDhcpv6PdEnabled == otherConfig.mDhcpv6PdEnabled;
         }
     }
 
     @Override
     public int hashCode() {
-        return Objects.hash(mNat64Enabled, mDhcp6PdEnabled);
+        return Objects.hash(mNat64Enabled, mDhcpv6PdEnabled);
     }
 
     @Override
@@ -85,7 +89,7 @@
         StringBuilder sb = new StringBuilder();
         sb.append('{');
         sb.append("Nat64Enabled=").append(mNat64Enabled);
-        sb.append(", Dhcp6PdEnabled=").append(mDhcp6PdEnabled);
+        sb.append(", Dhcpv6PdEnabled=").append(mDhcpv6PdEnabled);
         sb.append('}');
         return sb.toString();
     }
@@ -98,7 +102,7 @@
     @Override
     public void writeToParcel(@NonNull Parcel dest, int flags) {
         dest.writeBoolean(mNat64Enabled);
-        dest.writeBoolean(mDhcp6PdEnabled);
+        dest.writeBoolean(mDhcpv6PdEnabled);
     }
 
     public static final @NonNull Creator<ThreadConfiguration> CREATOR =
@@ -107,7 +111,7 @@
                 public ThreadConfiguration createFromParcel(Parcel in) {
                     ThreadConfiguration.Builder builder = new ThreadConfiguration.Builder();
                     builder.setNat64Enabled(in.readBoolean());
-                    builder.setDhcp6PdEnabled(in.readBoolean());
+                    builder.setDhcpv6PdEnabled(in.readBoolean());
                     return builder.build();
                 }
 
@@ -117,10 +121,14 @@
                 }
             };
 
-    /** The builder for creating {@link ThreadConfiguration} objects. */
+    /**
+     * The builder for creating {@link ThreadConfiguration} objects.
+     *
+     * @hide
+     */
     public static final class Builder {
         private boolean mNat64Enabled = false;
-        private boolean mDhcp6PdEnabled = false;
+        private boolean mDhcpv6PdEnabled = false;
 
         /** Creates a new {@link Builder} object with all features disabled. */
         public Builder() {}
@@ -134,7 +142,7 @@
             Objects.requireNonNull(config);
 
             mNat64Enabled = config.mNat64Enabled;
-            mDhcp6PdEnabled = config.mDhcp6PdEnabled;
+            mDhcpv6PdEnabled = config.mDhcpv6PdEnabled;
         }
 
         /**
@@ -156,8 +164,8 @@
          * IPv6.
          */
         @NonNull
-        public Builder setDhcp6PdEnabled(boolean enabled) {
-            this.mDhcp6PdEnabled = enabled;
+        public Builder setDhcpv6PdEnabled(boolean enabled) {
+            this.mDhcpv6PdEnabled = enabled;
             return this;
         }
 
diff --git a/thread/framework/java/android/net/thread/ThreadNetworkController.java b/thread/framework/java/android/net/thread/ThreadNetworkController.java
index 30b3d6a..551b98f 100644
--- a/thread/framework/java/android/net/thread/ThreadNetworkController.java
+++ b/thread/framework/java/android/net/thread/ThreadNetworkController.java
@@ -34,6 +34,7 @@
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.net.thread.flags.Flags;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -51,7 +52,7 @@
  *
  * @hide
  */
-@FlaggedApi(ThreadNetworkFlags.FLAG_THREAD_ENABLED)
+@FlaggedApi(Flags.FLAG_THREAD_ENABLED)
 @SystemApi
 public final class ThreadNetworkController {
     private static final String TAG = "ThreadNetworkController";
@@ -619,16 +620,15 @@
     /**
      * Registers a callback to be called when the configuration is changed.
      *
-     * <p>Upon return of this method, {@code callback} will be invoked immediately with the new
+     * <p>Upon return of this method, {@code callback} will be invoked immediately with the current
      * {@link ThreadConfiguration}.
      *
      * @param executor the executor to execute the {@code callback}
      * @param callback the callback to receive Thread configuration changes
      * @throws IllegalArgumentException if {@code callback} has already been registered
-     * @hide
      */
-    // @FlaggedApi(ThreadNetworkFlags.FLAG_CONFIGURATION_ENABLED)
-    // @RequiresPermission(permission.THREAD_NETWORK_PRIVILEGED)
+    @FlaggedApi(Flags.FLAG_CONFIGURATION_ENABLED)
+    @RequiresPermission(permission.THREAD_NETWORK_PRIVILEGED)
     public void registerConfigurationCallback(
             @NonNull @CallbackExecutor Executor executor,
             @NonNull Consumer<ThreadConfiguration> callback) {
@@ -656,10 +656,9 @@
      * @param callback the callback which has been registered with {@link
      *     #registerConfigurationCallback}
      * @throws IllegalArgumentException if {@code callback} hasn't been registered
-     * @hide
      */
-    // @FlaggedApi(ThreadNetworkFlags.FLAG_CONFIGURATION_ENABLED)
-    // @RequiresPermission(permission.THREAD_NETWORK_PRIVILEGED)
+    @FlaggedApi(Flags.FLAG_CONFIGURATION_ENABLED)
+    @RequiresPermission(permission.THREAD_NETWORK_PRIVILEGED)
     public void unregisterConfigurationCallback(@NonNull Consumer<ThreadConfiguration> callback) {
         requireNonNull(callback, "callback cannot be null");
         synchronized (mConfigurationCallbackMapLock) {
diff --git a/thread/framework/java/android/net/thread/ThreadNetworkException.java b/thread/framework/java/android/net/thread/ThreadNetworkException.java
index f699c30..b6973f8 100644
--- a/thread/framework/java/android/net/thread/ThreadNetworkException.java
+++ b/thread/framework/java/android/net/thread/ThreadNetworkException.java
@@ -23,6 +23,8 @@
 import android.annotation.NonNull;
 import android.annotation.SystemApi;
 
+import com.android.net.thread.flags.Flags;
+
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 
@@ -31,7 +33,7 @@
  *
  * @hide
  */
-@FlaggedApi(ThreadNetworkFlags.FLAG_THREAD_ENABLED)
+@FlaggedApi(Flags.FLAG_THREAD_ENABLED)
 @SystemApi
 public class ThreadNetworkException extends Exception {
     /** @hide */
diff --git a/thread/framework/java/android/net/thread/ThreadNetworkFlags.java b/thread/framework/java/android/net/thread/ThreadNetworkFlags.java
deleted file mode 100644
index 691bbf5..0000000
--- a/thread/framework/java/android/net/thread/ThreadNetworkFlags.java
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
- * Copyright (C) 2023 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.thread;
-
-/**
- * Container for flag constants defined in the "thread_network" namespace.
- *
- * @hide
- */
-// TODO: replace this class with auto-generated "com.android.net.thread.flags.Flags" once the
-// flagging infra is fully supported for mainline modules.
-public final class ThreadNetworkFlags {
-    /** @hide */
-    public static final String FLAG_THREAD_ENABLED = "com.android.net.thread.flags.thread_enabled";
-
-    /** @hide */
-    public static final String FLAG_CONFIGURATION_ENABLED =
-            "com.android.net.thread.flags.configuration_enabled";
-
-    private ThreadNetworkFlags() {}
-}
diff --git a/thread/framework/java/android/net/thread/ThreadNetworkManager.java b/thread/framework/java/android/net/thread/ThreadNetworkManager.java
index 150b759..bca8b6e 100644
--- a/thread/framework/java/android/net/thread/ThreadNetworkManager.java
+++ b/thread/framework/java/android/net/thread/ThreadNetworkManager.java
@@ -26,6 +26,7 @@
 import android.os.RemoteException;
 
 import com.android.net.module.util.CollectionUtils;
+import com.android.net.thread.flags.Flags;
 
 import java.util.Collections;
 import java.util.List;
@@ -35,7 +36,7 @@
  *
  * @hide
  */
-@FlaggedApi(ThreadNetworkFlags.FLAG_THREAD_ENABLED)
+@FlaggedApi(Flags.FLAG_THREAD_ENABLED)
 @SystemApi
 @SystemService(ThreadNetworkManager.SERVICE_NAME)
 public final class ThreadNetworkManager {
diff --git a/thread/scripts/make-pretty.sh b/thread/scripts/make-pretty.sh
index c176bfa..e012d41 100755
--- a/thread/scripts/make-pretty.sh
+++ b/thread/scripts/make-pretty.sh
@@ -1,9 +1,35 @@
 #!/usr/bin/env bash
 
-SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
+ANDROID_ROOT_DIR=$(
+    while [ ! -d ".repo" ] && [ "$PWD" != "/" ]; do cd ..; done
+    pwd
+)
 
-GOOGLE_JAVA_FORMAT=$SCRIPT_DIR/../../../../../prebuilts/tools/common/google-java-format/google-java-format
-ANDROID_BP_FORMAT=$SCRIPT_DIR/../../../../../prebuilts/build-tools/linux-x86/bin/bpfmt
+if [ ! -d "$ANDROID_ROOT_DIR/.repo" ]; then
+    echo "Error: The script has to run in an Android repo checkout"
+    exit 1
+fi
 
-$GOOGLE_JAVA_FORMAT --aosp -i $(find $SCRIPT_DIR/../ -name "*.java")
-$ANDROID_BP_FORMAT -w $(find $SCRIPT_DIR/../ -name "*.bp")
+GOOGLE_JAVA_FORMAT=$ANDROID_ROOT_DIR/prebuilts/tools/common/google-java-format/google-java-format
+ANDROID_BP_FORMAT=$ANDROID_ROOT_DIR/prebuilts/build-tools/linux-x86/bin/bpfmt
+AIDL_FORMAT=$ANDROID_ROOT_DIR/system/tools/aidl/aidl-format.sh
+
+CONNECTIVITY_DIR=$ANDROID_ROOT_DIR/packages/modules/Connectivity
+OPENTHREAD_DIR=$ANDROID_ROOT_DIR/external/openthread
+OTBR_POSIX_DIR=$ANDROID_ROOT_DIR/external/ot-br-posix
+
+ALLOWED_CODE_DIRS=($CONNECTIVITY_DIR $OPENTHREAD_DIR $OTBR_POSIX_DIR)
+CODE_DIR=$(git rev-parse --show-toplevel)
+
+if [[ ! " ${ALLOWED_CODE_DIRS[@]} " =~ " ${CODE_DIR} " ]]; then
+    echo "Error: The script has to run in the Git project Connectivity, openthread or ot-br-posix"
+    exit 1
+fi
+
+if [[ $CODE_DIR == $CONNECTIVITY_DIR ]]; then
+    CODE_DIR=$CODE_DIR"/thread"
+fi
+
+$GOOGLE_JAVA_FORMAT --aosp -i $(find $CODE_DIR -name "*.java")
+$ANDROID_BP_FORMAT -w $(find $CODE_DIR -name "*.bp")
+$AIDL_FORMAT -w $(find $CODE_DIR -name "*.aidl")
diff --git a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
index 2f60d9a..b621a6a 100644
--- a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
+++ b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
@@ -108,10 +108,10 @@
 import android.os.HandlerThread;
 import android.os.IBinder;
 import android.os.Looper;
+import android.os.ParcelFileDescriptor;
 import android.os.RemoteException;
 import android.os.SystemClock;
 import android.os.UserManager;
-import android.provider.Settings;
 import android.util.Log;
 import android.util.SparseArray;
 
@@ -120,7 +120,7 @@
 import com.android.server.ServiceManagerWrapper;
 import com.android.server.connectivity.ConnectivityResources;
 import com.android.server.thread.openthread.BackboneRouterState;
-import com.android.server.thread.openthread.BorderRouterConfigurationParcel;
+import com.android.server.thread.openthread.BorderRouterConfiguration;
 import com.android.server.thread.openthread.DnsTxtAttribute;
 import com.android.server.thread.openthread.IChannelMasksReceiver;
 import com.android.server.thread.openthread.IOtDaemon;
@@ -139,6 +139,7 @@
 import java.time.Clock;
 import java.time.DateTimeException;
 import java.time.Instant;
+import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -210,10 +211,9 @@
     private final ThreadPersistentSettings mPersistentSettings;
     private final UserManager mUserManager;
     private boolean mUserRestricted;
-    private boolean mAirplaneModeOn;
     private boolean mForceStopOtDaemonEnabled;
 
-    private BorderRouterConfigurationParcel mBorderRouterConfig;
+    private BorderRouterConfiguration mBorderRouterConfig;
 
     @VisibleForTesting
     ThreadNetworkControllerService(
@@ -238,7 +238,11 @@
         mInfraIfController = infraIfController;
         mUpstreamNetworkRequest = newUpstreamNetworkRequest();
         mNetworkToInterface = new HashMap<Network, String>();
-        mBorderRouterConfig = new BorderRouterConfigurationParcel();
+        mBorderRouterConfig =
+                new BorderRouterConfiguration.Builder()
+                        .setIsBorderRoutingEnabled(true)
+                        .setInfraInterfaceName(null)
+                        .build();
         mPersistentSettings = persistentSettings;
         mNsdPublisher = nsdPublisher;
         mUserManager = userManager;
@@ -272,10 +276,12 @@
     }
 
     private NetworkRequest newUpstreamNetworkRequest() {
-        NetworkRequest.Builder builder = new NetworkRequest.Builder().clearCapabilities();
+        NetworkRequest.Builder builder = new NetworkRequest.Builder();
 
         if (mUpstreamTestNetworkSpecifier != null) {
-            return builder.addTransportType(NetworkCapabilities.TRANSPORT_TEST)
+            // Test networks don't have NET_CAPABILITY_TRUSTED
+            return builder.removeCapability(NetworkCapabilities.NET_CAPABILITY_TRUSTED)
+                    .addTransportType(NetworkCapabilities.TRANSPORT_TEST)
                     .setNetworkSpecifier(mUpstreamTestNetworkSpecifier)
                     .build();
         }
@@ -344,8 +350,8 @@
         final String modelName = resources.getString(R.string.config_thread_model_name);
         final String vendorName = resources.getString(R.string.config_thread_vendor_name);
         final String vendorOui = resources.getString(R.string.config_thread_vendor_oui);
-        final boolean managedByGoogle =
-                resources.getBoolean(R.bool.config_thread_managed_by_google_home);
+        final String[] vendorSpecificTxts =
+                resources.getStringArray(R.array.config_thread_mdns_vendor_specific_txts);
 
         if (!modelName.isEmpty()) {
             if (modelName.getBytes(UTF_8).length > MAX_MODEL_NAME_UTF8_BYTES) {
@@ -375,19 +381,44 @@
         meshcopTxts.modelName = modelName;
         meshcopTxts.vendorName = vendorName;
         meshcopTxts.vendorOui = HexEncoding.decode(vendorOui.replace("-", "").replace(":", ""));
-        meshcopTxts.nonStandardTxtEntries = List.of(makeManagedByGoogleTxtAttr(managedByGoogle));
+        meshcopTxts.nonStandardTxtEntries = makeVendorSpecificTxtAttrs(vendorSpecificTxts);
 
         return meshcopTxts;
     }
 
     /**
-     * Creates a DNS-SD TXT entry for indicating whether Thread on this device is managed by Google.
+     * Parses vendor-specific TXT entries from "=" separated strings into list of {@link
+     * DnsTxtAttribute}.
      *
-     * @return TXT entry "vgh=1" if {@code managedByGoogle} is {@code true}; otherwise, "vgh=0"
+     * @throws IllegalArgumentsException if invalid TXT entries are found in {@code vendorTxts}
      */
-    private static DnsTxtAttribute makeManagedByGoogleTxtAttr(boolean managedByGoogle) {
-        final byte[] value = (managedByGoogle ? "1" : "0").getBytes(UTF_8);
-        return new DnsTxtAttribute("vgh", value);
+    @VisibleForTesting
+    static List<DnsTxtAttribute> makeVendorSpecificTxtAttrs(String[] vendorTxts) {
+        List<DnsTxtAttribute> txts = new ArrayList<>();
+        for (String txt : vendorTxts) {
+            String[] kv = txt.split("=", 2 /* limit */); // Split with only the first '='
+            if (kv.length < 1) {
+                throw new IllegalArgumentException(
+                        "Invalid vendor-specific TXT is found in resources: " + txt);
+            }
+
+            if (kv[0].length() < 2) {
+                throw new IllegalArgumentException(
+                        "Invalid vendor-specific TXT key \""
+                                + kv[0]
+                                + "\": it must contain at least 2 characters");
+            }
+
+            if (!kv[0].startsWith("v")) {
+                throw new IllegalArgumentException(
+                        "Invalid vendor-specific TXT key \""
+                                + kv[0]
+                                + "\": it doesn't start with \"v\"");
+            }
+
+            txts.add(new DnsTxtAttribute(kv[0], (kv.length >= 2 ? kv[1] : "").getBytes(UTF_8)));
+        }
+        return txts;
     }
 
     private void onOtDaemonDied() {
@@ -420,8 +451,6 @@
                     requestThreadNetwork();
                     mUserRestricted = isThreadUserRestricted();
                     registerUserRestrictionsReceiver();
-                    mAirplaneModeOn = isAirplaneModeOn();
-                    registerAirplaneModeReceiver();
                     maybeInitializeOtDaemon();
                 });
     }
@@ -503,15 +532,6 @@
             // the otDaemon set enabled state operation succeeded or not, so that it can recover
             // to the desired value after reboot.
             mPersistentSettings.put(ThreadPersistentSettings.THREAD_ENABLED.key, isEnabled);
-
-            // Remember whether the user wanted to keep Thread enabled in airplane mode. If once
-            // the user disabled Thread again in airplane mode, the persistent settings state is
-            // reset (so that Thread will be auto-disabled again when airplane mode is turned on).
-            // This behavior is consistent with Wi-Fi and bluetooth.
-            if (mAirplaneModeOn) {
-                mPersistentSettings.put(
-                        ThreadPersistentSettings.THREAD_ENABLED_IN_AIRPLANE_MODE.key, isEnabled);
-            }
         }
 
         try {
@@ -598,10 +618,12 @@
                 new BroadcastReceiver() {
                     @Override
                     public void onReceive(Context context, Intent intent) {
-                        mHandler.post(() -> onUserRestrictionsChanged(isThreadUserRestricted()));
+                        onUserRestrictionsChanged(isThreadUserRestricted());
                     }
                 },
-                new IntentFilter(UserManager.ACTION_USER_RESTRICTIONS_CHANGED));
+                new IntentFilter(UserManager.ACTION_USER_RESTRICTIONS_CHANGED),
+                null /* broadcastPermission */,
+                mHandler);
     }
 
     private void onUserRestrictionsChanged(boolean newUserRestrictedState) {
@@ -648,72 +670,13 @@
         return mUserManager.hasUserRestriction(DISALLOW_THREAD_NETWORK);
     }
 
-    private void registerAirplaneModeReceiver() {
-        mContext.registerReceiver(
-                new BroadcastReceiver() {
-                    @Override
-                    public void onReceive(Context context, Intent intent) {
-                        mHandler.post(() -> onAirplaneModeChanged(isAirplaneModeOn()));
-                    }
-                },
-                new IntentFilter(Intent.ACTION_AIRPLANE_MODE_CHANGED));
-    }
-
-    private void onAirplaneModeChanged(boolean newAirplaneModeOn) {
-        checkOnHandlerThread();
-        if (mAirplaneModeOn == newAirplaneModeOn) {
-            return;
-        }
-        Log.i(TAG, "Airplane mode changed: " + mAirplaneModeOn + " -> " + newAirplaneModeOn);
-        mAirplaneModeOn = newAirplaneModeOn;
-
-        final boolean shouldEnableThread = shouldEnableThread();
-        final IOperationReceiver receiver =
-                new IOperationReceiver.Stub() {
-                    @Override
-                    public void onSuccess() {
-                        Log.d(
-                                TAG,
-                                (shouldEnableThread ? "Enabled" : "Disabled")
-                                        + " Thread due to airplane mode change");
-                    }
-
-                    @Override
-                    public void onError(int errorCode, String errorMessage) {
-                        Log.e(
-                                TAG,
-                                "Failed to "
-                                        + (shouldEnableThread ? "enable" : "disable")
-                                        + " Thread for airplane mode change");
-                    }
-                };
-        // Do not save the user restriction state to persistent settings so that the user
-        // configuration won't be overwritten
-        setEnabledInternal(
-                shouldEnableThread, false /* persist */, new OperationReceiverWrapper(receiver));
-    }
-
-    /** Returns {@code true} if Airplane mode has been turned on. */
-    private boolean isAirplaneModeOn() {
-        return Settings.Global.getInt(
-                        mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 0)
-                == 1;
-    }
-
     /**
      * Returns {@code true} if Thread should be enabled based on current settings, runtime user
-     * restriction and airplane mode state.
+     * restriction state.
      */
     private boolean shouldEnableThread() {
-        final boolean enabledInAirplaneMode =
-                mPersistentSettings.get(ThreadPersistentSettings.THREAD_ENABLED_IN_AIRPLANE_MODE);
-
         return !mForceStopOtDaemonEnabled
                 && !mUserRestricted
-                // FIXME(b/340744397): Note that here we need to call `isAirplaneModeOn()` to get
-                // the latest state of airplane mode but can't use `mIsAirplaneMode`. This is for
-                // avoiding the race conditions described in b/340744397
-                && (!isAirplaneModeOn() || enabledInAirplaneMode)
                 && mPersistentSettings.get(ThreadPersistentSettings.THREAD_ENABLED);
     }
 
@@ -1269,38 +1232,54 @@
         }
     }
 
-    private void enableBorderRouting(String infraIfName) {
-        if (mBorderRouterConfig.isBorderRoutingEnabled
-                && infraIfName.equals(mBorderRouterConfig.infraInterfaceName)) {
+    private void configureBorderRouter(BorderRouterConfiguration borderRouterConfig) {
+        if (mBorderRouterConfig.equals(borderRouterConfig)) {
             return;
         }
-        Log.i(TAG, "Enable border routing on AIL: " + infraIfName);
+        Log.i(
+                TAG,
+                "Configuring Border Router: " + mBorderRouterConfig + " -> " + borderRouterConfig);
+        mBorderRouterConfig = borderRouterConfig;
+        ParcelFileDescriptor infraIcmp6Socket = null;
+        if (mBorderRouterConfig.infraInterfaceName != null) {
+            try {
+                infraIcmp6Socket =
+                        mInfraIfController.createIcmp6Socket(
+                                mBorderRouterConfig.infraInterfaceName);
+            } catch (IOException e) {
+                Log.i(TAG, "Failed to create ICMPv6 socket on infra network interface", e);
+            }
+        }
         try {
-            mBorderRouterConfig.infraInterfaceName = infraIfName;
-            mBorderRouterConfig.infraInterfaceIcmp6Socket =
-                    mInfraIfController.createIcmp6Socket(infraIfName);
-            mBorderRouterConfig.isBorderRoutingEnabled = true;
-
             getOtDaemon()
                     .configureBorderRouter(
-                            mBorderRouterConfig, new ConfigureBorderRouterStatusReceiver());
-        } catch (RemoteException | IOException | ThreadNetworkException e) {
-            Log.w(TAG, "Failed to enable border routing", e);
+                            mBorderRouterConfig,
+                            infraIcmp6Socket,
+                            new ConfigureBorderRouterStatusReceiver());
+        } catch (RemoteException | ThreadNetworkException e) {
+            Log.w(TAG, "Failed to configure border router " + mBorderRouterConfig, e);
         }
     }
 
+    private void enableBorderRouting(String infraIfName) {
+        BorderRouterConfiguration borderRouterConfig =
+                newBorderRouterConfigBuilder(mBorderRouterConfig)
+                        .setIsBorderRoutingEnabled(true)
+                        .setInfraInterfaceName(infraIfName)
+                        .build();
+        Log.i(TAG, "Enable border routing on AIL: " + infraIfName);
+        configureBorderRouter(borderRouterConfig);
+    }
+
     private void disableBorderRouting() {
         mUpstreamNetwork = null;
-        mBorderRouterConfig.infraInterfaceName = null;
-        mBorderRouterConfig.infraInterfaceIcmp6Socket = null;
-        mBorderRouterConfig.isBorderRoutingEnabled = false;
-        try {
-            getOtDaemon()
-                    .configureBorderRouter(
-                            mBorderRouterConfig, new ConfigureBorderRouterStatusReceiver());
-        } catch (RemoteException | ThreadNetworkException e) {
-            Log.w(TAG, "Failed to disable border routing", e);
-        }
+        BorderRouterConfiguration borderRouterConfig =
+                newBorderRouterConfigBuilder(mBorderRouterConfig)
+                        .setIsBorderRoutingEnabled(false)
+                        .setInfraInterfaceName(null)
+                        .build();
+        Log.i(TAG, "Disabling border routing");
+        configureBorderRouter(borderRouterConfig);
     }
 
     private void handleThreadInterfaceStateChanged(boolean isUp) {
@@ -1401,6 +1380,13 @@
         return builder.build();
     }
 
+    private static BorderRouterConfiguration.Builder newBorderRouterConfigBuilder(
+            BorderRouterConfiguration brConfig) {
+        return new BorderRouterConfiguration.Builder()
+                .setIsBorderRoutingEnabled(brConfig.isBorderRoutingEnabled)
+                .setInfraInterfaceName(brConfig.infraInterfaceName);
+    }
+
     private static final class CallbackMetadata {
         private static long gId = 0;
 
diff --git a/thread/service/java/com/android/server/thread/ThreadPersistentSettings.java b/thread/service/java/com/android/server/thread/ThreadPersistentSettings.java
index 747cc96..7c4c72d 100644
--- a/thread/service/java/com/android/server/thread/ThreadPersistentSettings.java
+++ b/thread/service/java/com/android/server/thread/ThreadPersistentSettings.java
@@ -197,7 +197,7 @@
             return false;
         }
         putObject(CONFIG_NAT64_ENABLED.key, configuration.isNat64Enabled());
-        putObject(CONFIG_DHCP6_PD_ENABLED.key, configuration.isDhcp6PdEnabled());
+        putObject(CONFIG_DHCP6_PD_ENABLED.key, configuration.isDhcpv6PdEnabled());
         writeToStoreFile();
         return true;
     }
@@ -206,7 +206,7 @@
     public ThreadConfiguration getConfiguration() {
         return new ThreadConfiguration.Builder()
                 .setNat64Enabled(get(CONFIG_NAT64_ENABLED))
-                .setDhcp6PdEnabled(get(CONFIG_DHCP6_PD_ENABLED))
+                .setDhcpv6PdEnabled(get(CONFIG_DHCP6_PD_ENABLED))
                 .build();
     }
 
diff --git a/thread/tests/cts/Android.bp b/thread/tests/cts/Android.bp
index c1cf0a0..6db7c9c 100644
--- a/thread/tests/cts/Android.bp
+++ b/thread/tests/cts/Android.bp
@@ -21,9 +21,11 @@
 
 android_test {
     name: "CtsThreadNetworkTestCases",
-    defaults: ["cts_defaults"],
+    defaults: [
+        "cts_defaults",
+        "framework-connectivity-test-defaults",
+    ],
     min_sdk_version: "33",
-    sdk_version: "test_current",
     manifest: "AndroidManifest.xml",
     test_config: "AndroidTest.xml",
     srcs: [
diff --git a/thread/tests/cts/src/android/net/thread/cts/ThreadConfigurationTest.java b/thread/tests/cts/src/android/net/thread/cts/ThreadConfigurationTest.java
new file mode 100644
index 0000000..386412e
--- /dev/null
+++ b/thread/tests/cts/src/android/net/thread/cts/ThreadConfigurationTest.java
@@ -0,0 +1,95 @@
+/*
+ * 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.thread.cts;
+
+import static com.android.testutils.ParcelUtils.assertParcelingIsLossless;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.net.thread.ThreadConfiguration;
+import android.net.thread.utils.ThreadFeatureCheckerRule;
+import android.net.thread.utils.ThreadFeatureCheckerRule.RequiresThreadFeature;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.util.Arrays;
+import java.util.Collection;
+
+/** Tests for {@link ThreadConfiguration}. */
+@SmallTest
+@RequiresThreadFeature
+@RunWith(Parameterized.class)
+public final class ThreadConfigurationTest {
+    @Rule public final ThreadFeatureCheckerRule mThreadRule = new ThreadFeatureCheckerRule();
+
+    public final boolean mIsNat64Enabled;
+    public final boolean mIsDhcpv6PdEnabled;
+
+    @Parameterized.Parameters
+    public static Collection configArguments() {
+        return Arrays.asList(
+                new Object[][] {
+                    {false, false}, // All disabled
+                    {true, false}, // NAT64 enabled
+                    {false, true}, // DHCP6-PD enabled
+                    {true, true}, // All enabled
+                });
+    }
+
+    public ThreadConfigurationTest(boolean isNat64Enabled, boolean isDhcpv6PdEnabled) {
+        mIsNat64Enabled = isNat64Enabled;
+        mIsDhcpv6PdEnabled = isDhcpv6PdEnabled;
+    }
+
+    @Test
+    public void parcelable_parcelingIsLossLess() {
+        ThreadConfiguration config =
+                new ThreadConfiguration.Builder()
+                        .setNat64Enabled(mIsNat64Enabled)
+                        .setDhcpv6PdEnabled(mIsDhcpv6PdEnabled)
+                        .build();
+        assertParcelingIsLossless(config);
+    }
+
+    @Test
+    public void builder_correctValuesAreSet() {
+        ThreadConfiguration config =
+                new ThreadConfiguration.Builder()
+                        .setNat64Enabled(mIsNat64Enabled)
+                        .setDhcpv6PdEnabled(mIsDhcpv6PdEnabled)
+                        .build();
+
+        assertThat(config.isNat64Enabled()).isEqualTo(mIsNat64Enabled);
+        assertThat(config.isDhcpv6PdEnabled()).isEqualTo(mIsDhcpv6PdEnabled);
+    }
+
+    @Test
+    public void builderConstructor_configsAreEqual() {
+        ThreadConfiguration config1 =
+                new ThreadConfiguration.Builder()
+                        .setNat64Enabled(mIsNat64Enabled)
+                        .setDhcpv6PdEnabled(mIsDhcpv6PdEnabled)
+                        .build();
+        ThreadConfiguration config2 = new ThreadConfiguration.Builder(config1).build();
+        assertThat(config1).isEqualTo(config2);
+    }
+}
diff --git a/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java b/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java
index 41f34ff..1a101b6 100644
--- a/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java
+++ b/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java
@@ -58,6 +58,7 @@
 import android.net.thread.ActiveOperationalDataset;
 import android.net.thread.OperationalDatasetTimestamp;
 import android.net.thread.PendingOperationalDataset;
+import android.net.thread.ThreadConfiguration;
 import android.net.thread.ThreadNetworkController;
 import android.net.thread.ThreadNetworkController.OperationalDatasetCallback;
 import android.net.thread.ThreadNetworkController.StateCallback;
@@ -95,9 +96,11 @@
 import java.util.Set;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executor;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 import java.util.concurrent.TimeoutException;
+import java.util.function.Consumer;
 import java.util.function.Predicate;
 
 /** CTS tests for {@link ThreadNetworkController}. */
@@ -110,11 +113,14 @@
     private static final int NETWORK_CALLBACK_TIMEOUT_MILLIS = 10 * 1000;
     private static final int CALLBACK_TIMEOUT_MILLIS = 1_000;
     private static final int ENABLED_TIMEOUT_MILLIS = 2_000;
+    private static final int SET_CONFIGURATION_TIMEOUT_MILLIS = 1_000;
     private static final int SERVICE_DISCOVERY_TIMEOUT_MILLIS = 30_000;
     private static final int SERVICE_LOST_TIMEOUT_MILLIS = 20_000;
     private static final String MESHCOP_SERVICE_TYPE = "_meshcop._udp";
     private static final String THREAD_NETWORK_PRIVILEGED =
             "android.permission.THREAD_NETWORK_PRIVILEGED";
+    private static final ThreadConfiguration DEFAULT_CONFIG =
+            new ThreadConfiguration.Builder().build();
 
     @Rule public final ThreadFeatureCheckerRule mThreadRule = new ThreadFeatureCheckerRule();
 
@@ -127,6 +133,9 @@
     private HandlerThread mHandlerThread;
     private TapTestNetworkTracker mTestNetworkTracker;
 
+    private final List<Consumer<ThreadConfiguration>> mConfigurationCallbacksToCleanUp =
+            new ArrayList<>();
+
     @Before
     public void setUp() throws Exception {
         mController =
@@ -141,6 +150,7 @@
         mHandlerThread.start();
 
         setEnabledAndWait(mController, true);
+        setConfigurationAndWait(mController, DEFAULT_CONFIG);
     }
 
     @After
@@ -148,6 +158,18 @@
         dropAllPermissions();
         leaveAndWait(mController);
         tearDownTestNetwork();
+        setConfigurationAndWait(mController, DEFAULT_CONFIG);
+        for (Consumer<ThreadConfiguration> configurationCallback :
+                mConfigurationCallbacksToCleanUp) {
+            try {
+                runAsShell(
+                        THREAD_NETWORK_PRIVILEGED,
+                        () -> mController.unregisterConfigurationCallback(configurationCallback));
+            } catch (IllegalArgumentException e) {
+                // Ignore the exception when the callback is not registered.
+            }
+        }
+        mConfigurationCallbacksToCleanUp.clear();
     }
 
     @Test
@@ -832,6 +854,152 @@
                         NET_CAPABILITY_TRUSTED);
     }
 
+    @Test
+    public void setConfiguration_null_throwsNullPointerException() throws Exception {
+        CompletableFuture<Void> setConfigFuture = new CompletableFuture<>();
+        assertThrows(
+                NullPointerException.class,
+                () ->
+                        mController.setConfiguration(
+                                null, mExecutor, newOutcomeReceiver(setConfigFuture)));
+    }
+
+    @Test
+    public void setConfiguration_noPermissions_throwsSecurityException() throws Exception {
+        ThreadConfiguration configuration =
+                new ThreadConfiguration.Builder().setNat64Enabled(true).build();
+        CompletableFuture<Void> setConfigFuture = new CompletableFuture<>();
+        assertThrows(
+                SecurityException.class,
+                () -> {
+                    mController.setConfiguration(
+                            configuration, mExecutor, newOutcomeReceiver(setConfigFuture));
+                });
+    }
+
+    @Test
+    public void registerConfigurationCallback_permissionsGranted_returnsCurrentStatus()
+            throws Exception {
+        CompletableFuture<ThreadConfiguration> getConfigFuture = new CompletableFuture<>();
+        Consumer<ThreadConfiguration> callback = getConfigFuture::complete;
+
+        runAsShell(
+                THREAD_NETWORK_PRIVILEGED,
+                () -> registerConfigurationCallback(mController, mExecutor, callback));
+        assertThat(getConfigFuture.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS))
+                .isEqualTo(DEFAULT_CONFIG);
+    }
+
+    @Test
+    public void registerConfigurationCallback_noPermissions_throwsSecurityException()
+            throws Exception {
+        dropAllPermissions();
+
+        assertThrows(
+                SecurityException.class,
+                () -> registerConfigurationCallback(mController, mExecutor, config -> {}));
+    }
+
+    @Test
+    public void registerConfigurationCallback_returnsUpdatedConfigurations() throws Exception {
+        CompletableFuture<Void> setFuture1 = new CompletableFuture<>();
+        CompletableFuture<Void> setFuture2 = new CompletableFuture<>();
+        ConfigurationListener listener = new ConfigurationListener(mController);
+        ThreadConfiguration config1 =
+                new ThreadConfiguration.Builder()
+                        .setNat64Enabled(true)
+                        .setDhcpv6PdEnabled(true)
+                        .build();
+        ThreadConfiguration config2 =
+                new ThreadConfiguration.Builder()
+                        .setNat64Enabled(false)
+                        .setDhcpv6PdEnabled(true)
+                        .build();
+
+        try {
+            runAsShell(
+                    THREAD_NETWORK_PRIVILEGED,
+                    () ->
+                            mController.setConfiguration(
+                                    config1, mExecutor, newOutcomeReceiver(setFuture1)));
+            runAsShell(
+                    THREAD_NETWORK_PRIVILEGED,
+                    () ->
+                            mController.setConfiguration(
+                                    config2, mExecutor, newOutcomeReceiver(setFuture2)));
+            setFuture1.get(ENABLED_TIMEOUT_MILLIS, MILLISECONDS);
+            setFuture2.get(ENABLED_TIMEOUT_MILLIS, MILLISECONDS);
+
+            listener.expectConfiguration(DEFAULT_CONFIG);
+            listener.expectConfiguration(config1);
+            listener.expectConfiguration(config2);
+            listener.expectNoMoreConfiguration();
+        } finally {
+            listener.unregisterConfigurationCallback();
+        }
+    }
+
+    @Test
+    public void registerConfigurationCallback_alreadyRegistered_throwsIllegalArgumentException()
+            throws Exception {
+        grantPermissions(THREAD_NETWORK_PRIVILEGED);
+
+        Consumer<ThreadConfiguration> callback = config -> {};
+        registerConfigurationCallback(mController, mExecutor, callback);
+
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> registerConfigurationCallback(mController, mExecutor, callback));
+    }
+
+    @Test
+    public void unregisterConfigurationCallback_noPermissions_throwsSecurityException()
+            throws Exception {
+        Consumer<ThreadConfiguration> callback = config -> {};
+        runAsShell(
+                THREAD_NETWORK_PRIVILEGED,
+                () -> registerConfigurationCallback(mController, mExecutor, callback));
+
+        assertThrows(
+                SecurityException.class,
+                () -> mController.unregisterConfigurationCallback(callback));
+    }
+
+    @Test
+    public void unregisterConfigurationCallback_callbackRegistered_success() throws Exception {
+        Consumer<ThreadConfiguration> callback = config -> {};
+        runAsShell(
+                THREAD_NETWORK_PRIVILEGED,
+                () -> {
+                    registerConfigurationCallback(mController, mExecutor, callback);
+                    mController.unregisterConfigurationCallback(callback);
+                });
+    }
+
+    @Test
+    public void
+            unregisterConfigurationCallback_callbackNotRegistered_throwsIllegalArgumentException()
+                    throws Exception {
+        Consumer<ThreadConfiguration> callback = config -> {};
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> mController.unregisterConfigurationCallback(callback));
+    }
+
+    @Test
+    public void unregisterConfigurationCallback_alreadyUnregistered_throwsIllegalArgumentException()
+            throws Exception {
+        grantPermissions(THREAD_NETWORK_PRIVILEGED);
+
+        Consumer<ThreadConfiguration> callback = config -> {};
+        registerConfigurationCallback(mController, mExecutor, callback);
+        mController.unregisterConfigurationCallback(callback);
+
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> mController.unregisterConfigurationCallback(callback));
+    }
+
     private void grantPermissions(String... permissions) {
         for (String permission : permissions) {
             mGrantedPermissions.add(permission);
@@ -859,7 +1027,6 @@
         assertThat(txtMap.get("rv")).isNotNull();
         assertThat(txtMap.get("tv")).isNotNull();
         assertThat(txtMap.get("sb")).isNotNull();
-        assertThat(new String(txtMap.get("vgh"))).isIn(List.of("0", "1"));
     }
 
     @Test
@@ -886,7 +1053,6 @@
         assertThat(txtMap.get("tv")).isNotNull();
         assertThat(txtMap.get("sb")).isNotNull();
         assertThat(txtMap.get("id").length).isEqualTo(16);
-        assertThat(new String(txtMap.get("vgh"))).isIn(List.of("0", "1"));
     }
 
     @Test
@@ -1040,6 +1206,35 @@
         }
     }
 
+    private class ConfigurationListener {
+        private ArrayTrackRecord<ThreadConfiguration> mConfigurations = new ArrayTrackRecord<>();
+        private final ArrayTrackRecord<ThreadConfiguration>.ReadHead mReadHead =
+                mConfigurations.newReadHead();
+        ThreadNetworkController mController;
+        Consumer<ThreadConfiguration> mCallback = (config) -> mConfigurations.add(config);
+
+        ConfigurationListener(ThreadNetworkController controller) {
+            this.mController = controller;
+            runAsShell(
+                    THREAD_NETWORK_PRIVILEGED,
+                    () -> controller.registerConfigurationCallback(mExecutor, mCallback));
+        }
+
+        public void expectConfiguration(ThreadConfiguration config) {
+            assertThat(mReadHead.poll(CALLBACK_TIMEOUT_MILLIS, c -> c.equals(config))).isNotNull();
+        }
+
+        public void expectNoMoreConfiguration() {
+            assertThat(mReadHead.poll(CALLBACK_TIMEOUT_MILLIS, c -> true)).isNull();
+        }
+
+        public void unregisterConfigurationCallback() {
+            runAsShell(
+                    THREAD_NETWORK_PRIVILEGED,
+                    () -> mController.unregisterConfigurationCallback(mCallback));
+        }
+    }
+
     private int booleanToEnabledState(boolean enabled) {
         return enabled ? STATE_ENABLED : STATE_DISABLED;
     }
@@ -1054,6 +1249,18 @@
         waitForEnabledState(controller, booleanToEnabledState(enabled));
     }
 
+    private void setConfigurationAndWait(
+            ThreadNetworkController controller, ThreadConfiguration configuration)
+            throws Exception {
+        CompletableFuture<Void> setFuture = new CompletableFuture<>();
+        runAsShell(
+                THREAD_NETWORK_PRIVILEGED,
+                () ->
+                        controller.setConfiguration(
+                                configuration, mExecutor, newOutcomeReceiver(setFuture)));
+        setFuture.get(SET_CONFIGURATION_TIMEOUT_MILLIS, MILLISECONDS);
+    }
+
     private CompletableFuture joinRandomizedDataset(
             ThreadNetworkController controller, String networkName) throws Exception {
         ActiveOperationalDataset activeDataset = newRandomizedDataset(networkName, controller);
@@ -1120,6 +1327,14 @@
         };
     }
 
+    private void registerConfigurationCallback(
+            ThreadNetworkController controller,
+            Executor executor,
+            Consumer<ThreadConfiguration> callback) {
+        controller.registerConfigurationCallback(executor, callback);
+        mConfigurationCallbacksToCleanUp.add(callback);
+    }
+
     private static void assertDoesNotThrow(ThrowingRunnable runnable) {
         try {
             runnable.run();
diff --git a/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java b/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java
index 8c63d37..9e8dc3a 100644
--- a/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java
+++ b/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java
@@ -17,23 +17,26 @@
 package android.net.thread;
 
 import static android.Manifest.permission.MANAGE_TEST_NETWORKS;
-import static android.net.thread.utils.IntegrationTestUtils.JOIN_TIMEOUT;
+import static android.net.InetAddresses.parseNumericAddress;
+import static android.net.thread.utils.IntegrationTestUtils.DEFAULT_DATASET;
 import static android.net.thread.utils.IntegrationTestUtils.getIpv6LinkAddresses;
+import static android.net.thread.utils.IntegrationTestUtils.isExpectedIcmpv4Packet;
 import static android.net.thread.utils.IntegrationTestUtils.isExpectedIcmpv6Packet;
-import static android.net.thread.utils.IntegrationTestUtils.isFromIpv6Source;
+import static android.net.thread.utils.IntegrationTestUtils.isFrom;
 import static android.net.thread.utils.IntegrationTestUtils.isInMulticastGroup;
-import static android.net.thread.utils.IntegrationTestUtils.isToIpv6Destination;
+import static android.net.thread.utils.IntegrationTestUtils.isTo;
+import static android.net.thread.utils.IntegrationTestUtils.joinNetworkAndWaitForOmr;
 import static android.net.thread.utils.IntegrationTestUtils.newPacketReader;
 import static android.net.thread.utils.IntegrationTestUtils.pollForPacket;
 import static android.net.thread.utils.IntegrationTestUtils.sendUdpMessage;
 import static android.net.thread.utils.IntegrationTestUtils.waitFor;
+import static android.system.OsConstants.ICMP_ECHO;
 
 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.TestNetworkTrackerKt.initTestNetwork;
 import static com.android.testutils.TestPermissionUtil.runAsShell;
 
-import static com.google.common.io.BaseEncoding.base16;
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.junit.Assert.assertEquals;
@@ -44,11 +47,11 @@
 import static java.util.Objects.requireNonNull;
 
 import android.content.Context;
-import android.net.InetAddresses;
 import android.net.IpPrefix;
 import android.net.LinkAddress;
 import android.net.LinkProperties;
 import android.net.MacAddress;
+import android.net.RouteInfo;
 import android.net.thread.utils.FullThreadDevice;
 import android.net.thread.utils.InfraNetworkDevice;
 import android.net.thread.utils.OtDaemonController;
@@ -74,7 +77,9 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.net.Inet4Address;
 import java.net.Inet6Address;
+import java.net.InetAddress;
 import java.time.Duration;
 import java.util.ArrayList;
 import java.util.List;
@@ -89,22 +94,14 @@
     private static final String TAG = BorderRoutingTest.class.getSimpleName();
     private static final int NUM_FTD = 2;
     private static final Inet6Address GROUP_ADDR_SCOPE_5 =
-            (Inet6Address) InetAddresses.parseNumericAddress("ff05::1234");
+            (Inet6Address) parseNumericAddress("ff05::1234");
     private static final Inet6Address GROUP_ADDR_SCOPE_4 =
-            (Inet6Address) InetAddresses.parseNumericAddress("ff04::1234");
+            (Inet6Address) parseNumericAddress("ff04::1234");
     private static final Inet6Address GROUP_ADDR_SCOPE_3 =
-            (Inet6Address) InetAddresses.parseNumericAddress("ff03::1234");
-
-    // A valid Thread Active Operational Dataset generated from OpenThread CLI "dataset init new".
-    private static final byte[] DEFAULT_DATASET_TLVS =
-            base16().decode(
-                            "0E080000000000010000000300001335060004001FFFE002"
-                                    + "08ACC214689BC40BDF0708FD64DB1225F47E0B0510F26B31"
-                                    + "53760F519A63BAFDDFFC80D2AF030F4F70656E5468726561"
-                                    + "642D643961300102D9A00410A245479C836D551B9CA557F7"
-                                    + "B9D351B40C0402A0FFF8");
-    private static final ActiveOperationalDataset DEFAULT_DATASET =
-            ActiveOperationalDataset.fromThreadTlvs(DEFAULT_DATASET_TLVS);
+            (Inet6Address) parseNumericAddress("ff03::1234");
+    private static final Inet4Address IPV4_SERVER_ADDR =
+            (Inet4Address) parseNumericAddress("8.8.8.8");
+    private static final String NAT64_CIDR = "192.168.255.0/24";
 
     @Rule public final ThreadFeatureCheckerRule mThreadRule = new ThreadFeatureCheckerRule();
 
@@ -171,12 +168,12 @@
          */
 
         FullThreadDevice ftd = mFtds.get(0);
-        startFtdChild(ftd);
+        joinNetworkAndWaitForOmr(ftd, DEFAULT_DATASET);
 
         mInfraDevice.sendEchoRequest(ftd.getOmrAddress());
 
         // Infra device receives an echo reply sent by FTD.
-        assertNotNull(pollForPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd.getOmrAddress()));
+        assertNotNull(pollForIcmpPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd.getOmrAddress()));
     }
 
     @Test
@@ -193,11 +190,11 @@
 
         startInfraDeviceAndWaitForOnLinkAddr();
         FullThreadDevice ftd = mFtds.get(0);
-        startFtdChild(ftd);
+        joinNetworkAndWaitForOmr(ftd, DEFAULT_DATASET);
 
         mInfraDevice.sendEchoRequest(ftd.getOmrAddress());
 
-        assertNotNull(pollForPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd.getOmrAddress()));
+        assertNotNull(pollForIcmpPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd.getOmrAddress()));
     }
 
     @Test
@@ -213,7 +210,7 @@
          */
 
         FullThreadDevice ftd = mFtds.get(0);
-        startFtdChild(ftd);
+        joinNetworkAndWaitForOmr(ftd, DEFAULT_DATASET);
         Inet6Address ftdOmr = ftd.getOmrAddress();
         // Create a new infra network and let Thread prefer it
         TestNetworkTracker oldInfraNetworkTracker = mInfraNetworkTracker;
@@ -224,7 +221,7 @@
 
             mInfraDevice.sendEchoRequest(ftd.getOmrAddress());
 
-            assertNotNull(pollForPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftdOmr));
+            assertNotNull(pollForIcmpPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftdOmr));
         } finally {
             runAsShell(MANAGE_TEST_NETWORKS, () -> oldInfraNetworkTracker.teardown());
         }
@@ -243,7 +240,7 @@
          */
 
         FullThreadDevice ftd = mFtds.get(0);
-        startFtdChild(ftd);
+        joinNetworkAndWaitForOmr(ftd, DEFAULT_DATASET);
         Inet6Address ftdOmr = requireNonNull(ftd.getOmrAddress());
         Inet6Address ftdMlEid = requireNonNull(ftd.getMlEid());
 
@@ -285,7 +282,7 @@
          */
 
         FullThreadDevice ftd = mFtds.get(0);
-        startFtdChild(ftd);
+        joinNetworkAndWaitForOmr(ftd, DEFAULT_DATASET);
 
         ftd.subscribeMulticastAddress(GROUP_ADDR_SCOPE_5);
 
@@ -307,7 +304,7 @@
          */
 
         FullThreadDevice ftd = mFtds.get(0);
-        startFtdChild(ftd);
+        joinNetworkAndWaitForOmr(ftd, DEFAULT_DATASET);
 
         ftd.subscribeMulticastAddress(GROUP_ADDR_SCOPE_3);
 
@@ -328,12 +325,12 @@
          */
 
         FullThreadDevice ftd = mFtds.get(0);
-        startFtdChild(ftd);
+        joinNetworkAndWaitForOmr(ftd, DEFAULT_DATASET);
         subscribeMulticastAddressAndWait(ftd, GROUP_ADDR_SCOPE_5);
 
         mInfraDevice.sendEchoRequest(GROUP_ADDR_SCOPE_5);
 
-        assertNotNull(pollForPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd.getOmrAddress()));
+        assertNotNull(pollForIcmpPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd.getOmrAddress()));
     }
 
     @Test
@@ -360,12 +357,12 @@
          */
 
         FullThreadDevice ftd = mFtds.get(0);
-        startFtdChild(ftd);
+        joinNetworkAndWaitForOmr(ftd, DEFAULT_DATASET);
         ftd.subscribeMulticastAddress(GROUP_ADDR_SCOPE_3);
 
         mInfraDevice.sendEchoRequest(GROUP_ADDR_SCOPE_3);
 
-        assertNull(pollForPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd.getOmrAddress()));
+        assertNull(pollForIcmpPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd.getOmrAddress()));
     }
 
     @Test
@@ -382,11 +379,11 @@
          */
 
         FullThreadDevice ftd = mFtds.get(0);
-        startFtdChild(ftd);
+        joinNetworkAndWaitForOmr(ftd, DEFAULT_DATASET);
 
         mInfraDevice.sendEchoRequest(GROUP_ADDR_SCOPE_4);
 
-        assertNull(pollForPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd.getOmrAddress()));
+        assertNull(pollForIcmpPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd.getOmrAddress()));
     }
 
     @Test
@@ -407,22 +404,24 @@
          */
 
         FullThreadDevice ftd1 = mFtds.get(0);
-        startFtdChild(ftd1);
+        joinNetworkAndWaitForOmr(ftd1, DEFAULT_DATASET);
         subscribeMulticastAddressAndWait(ftd1, GROUP_ADDR_SCOPE_5);
 
         FullThreadDevice ftd2 = mFtds.get(1);
-        startFtdChild(ftd2);
+        joinNetworkAndWaitForOmr(ftd2, DEFAULT_DATASET);
         subscribeMulticastAddressAndWait(ftd2, GROUP_ADDR_SCOPE_4);
 
         mInfraDevice.sendEchoRequest(GROUP_ADDR_SCOPE_5);
 
-        assertNotNull(pollForPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd1.getOmrAddress()));
+        assertNotNull(
+                pollForIcmpPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd1.getOmrAddress()));
 
         // Verify ping reply from ftd1 and ftd2 separately as the order of replies can't be
         // predicted.
         mInfraDevice.sendEchoRequest(GROUP_ADDR_SCOPE_4);
 
-        assertNotNull(pollForPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd2.getOmrAddress()));
+        assertNotNull(
+                pollForIcmpPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd2.getOmrAddress()));
     }
 
     @Test
@@ -443,21 +442,23 @@
          */
 
         FullThreadDevice ftd1 = mFtds.get(0);
-        startFtdChild(ftd1);
+        joinNetworkAndWaitForOmr(ftd1, DEFAULT_DATASET);
         subscribeMulticastAddressAndWait(ftd1, GROUP_ADDR_SCOPE_5);
 
         FullThreadDevice ftd2 = mFtds.get(1);
-        startFtdChild(ftd2);
+        joinNetworkAndWaitForOmr(ftd2, DEFAULT_DATASET);
         subscribeMulticastAddressAndWait(ftd2, GROUP_ADDR_SCOPE_5);
 
         mInfraDevice.sendEchoRequest(GROUP_ADDR_SCOPE_5);
 
-        assertNotNull(pollForPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd1.getOmrAddress()));
+        assertNotNull(
+                pollForIcmpPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd1.getOmrAddress()));
 
         // Send the request twice as the order of replies from ftd1 and ftd2 are not guaranteed
         mInfraDevice.sendEchoRequest(GROUP_ADDR_SCOPE_5);
 
-        assertNotNull(pollForPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd2.getOmrAddress()));
+        assertNotNull(
+                pollForIcmpPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd2.getOmrAddress()));
     }
 
     @Test
@@ -473,16 +474,18 @@
          */
 
         FullThreadDevice ftd = mFtds.get(0);
-        startFtdChild(ftd);
+        joinNetworkAndWaitForOmr(ftd, DEFAULT_DATASET);
         Inet6Address ftdOmr = ftd.getOmrAddress();
 
         ftd.ping(GROUP_ADDR_SCOPE_5);
         ftd.ping(GROUP_ADDR_SCOPE_4);
 
         assertNotNull(
-                pollForPacketOnInfraNetwork(ICMPV6_ECHO_REQUEST_TYPE, ftdOmr, GROUP_ADDR_SCOPE_5));
+                pollForIcmpPacketOnInfraNetwork(
+                        ICMPV6_ECHO_REQUEST_TYPE, ftdOmr, GROUP_ADDR_SCOPE_5));
         assertNotNull(
-                pollForPacketOnInfraNetwork(ICMPV6_ECHO_REQUEST_TYPE, ftdOmr, GROUP_ADDR_SCOPE_4));
+                pollForIcmpPacketOnInfraNetwork(
+                        ICMPV6_ECHO_REQUEST_TYPE, ftdOmr, GROUP_ADDR_SCOPE_4));
     }
 
     @Test
@@ -499,12 +502,12 @@
          */
 
         FullThreadDevice ftd = mFtds.get(0);
-        startFtdChild(ftd);
+        joinNetworkAndWaitForOmr(ftd, DEFAULT_DATASET);
 
         ftd.ping(GROUP_ADDR_SCOPE_3);
 
         assertNull(
-                pollForPacketOnInfraNetwork(
+                pollForIcmpPacketOnInfraNetwork(
                         ICMPV6_ECHO_REQUEST_TYPE, ftd.getOmrAddress(), GROUP_ADDR_SCOPE_3));
     }
 
@@ -521,14 +524,15 @@
          */
 
         FullThreadDevice ftd = mFtds.get(0);
-        startFtdChild(ftd);
+        joinNetworkAndWaitForOmr(ftd, DEFAULT_DATASET);
         Inet6Address ftdLla = ftd.getLinkLocalAddress();
         assertNotNull(ftdLla);
 
         ftd.ping(GROUP_ADDR_SCOPE_4, ftdLla);
 
         assertNull(
-                pollForPacketOnInfraNetwork(ICMPV6_ECHO_REQUEST_TYPE, ftdLla, GROUP_ADDR_SCOPE_4));
+                pollForIcmpPacketOnInfraNetwork(
+                        ICMPV6_ECHO_REQUEST_TYPE, ftdLla, GROUP_ADDR_SCOPE_4));
     }
 
     @Test
@@ -544,7 +548,7 @@
          */
 
         FullThreadDevice ftd = mFtds.get(0);
-        startFtdChild(ftd);
+        joinNetworkAndWaitForOmr(ftd, DEFAULT_DATASET);
         List<Inet6Address> ftdMlas = ftd.getMeshLocalAddresses();
         assertFalse(ftdMlas.isEmpty());
 
@@ -552,7 +556,7 @@
             ftd.ping(GROUP_ADDR_SCOPE_4, ftdMla);
 
             assertNull(
-                    pollForPacketOnInfraNetwork(
+                    pollForIcmpPacketOnInfraNetwork(
                             ICMPV6_ECHO_REQUEST_TYPE, ftdMla, GROUP_ADDR_SCOPE_4));
         }
     }
@@ -571,7 +575,7 @@
          */
 
         FullThreadDevice ftd = mFtds.get(0);
-        startFtdChild(ftd);
+        joinNetworkAndWaitForOmr(ftd, DEFAULT_DATASET);
         subscribeMulticastAddressAndWait(ftd, GROUP_ADDR_SCOPE_5);
         Inet6Address ftdOmr = ftd.getOmrAddress();
 
@@ -583,7 +587,7 @@
 
         mInfraDevice.sendEchoRequest(GROUP_ADDR_SCOPE_5);
 
-        assertNotNull(pollForPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftdOmr));
+        assertNotNull(pollForIcmpPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftdOmr));
     }
 
     @Test
@@ -599,7 +603,7 @@
          */
 
         FullThreadDevice ftd = mFtds.get(0);
-        startFtdChild(ftd);
+        joinNetworkAndWaitForOmr(ftd, DEFAULT_DATASET);
         Inet6Address ftdOmr = ftd.getOmrAddress();
 
         // Destroy infra link and re-create
@@ -611,33 +615,46 @@
         ftd.ping(GROUP_ADDR_SCOPE_4);
 
         assertNotNull(
-                pollForPacketOnInfraNetwork(ICMPV6_ECHO_REQUEST_TYPE, ftdOmr, GROUP_ADDR_SCOPE_4));
+                pollForIcmpPacketOnInfraNetwork(
+                        ICMPV6_ECHO_REQUEST_TYPE, ftdOmr, GROUP_ADDR_SCOPE_4));
+    }
+
+    @Test
+    public void nat64_threadDevicePingIpv4InfraDevice_outboundPacketIsForwarded() throws Exception {
+        FullThreadDevice ftd = mFtds.get(0);
+        joinNetworkAndWaitForOmr(ftd, DEFAULT_DATASET);
+        // TODO: enable NAT64 via ThreadNetworkController API instead of ot-ctl
+        mOtCtl.setNat64Cidr(NAT64_CIDR);
+        mOtCtl.setNat64Enabled(true);
+        waitFor(() -> mOtCtl.hasNat64PrefixInNetdata(), Duration.ofSeconds(10));
+
+        ftd.ping(IPV4_SERVER_ADDR);
+
+        assertNotNull(pollForIcmpPacketOnInfraNetwork(ICMP_ECHO, null, IPV4_SERVER_ADDR));
     }
 
     private void setUpInfraNetwork() throws Exception {
+        LinkProperties lp = new LinkProperties();
+        // NAT64 feature requires the infra network to have an IPv4 default route.
+        lp.addRoute(
+                new RouteInfo(
+                        new IpPrefix("0.0.0.0/0") /* destination */,
+                        null /* gateway */,
+                        null,
+                        RouteInfo.RTN_UNICAST,
+                        1500 /* mtu */));
         mInfraNetworkTracker =
                 runAsShell(
                         MANAGE_TEST_NETWORKS,
-                        () ->
-                                initTestNetwork(
-                                        mContext, new LinkProperties(), 5000 /* timeoutMs */));
-        mController.setTestNetworkAsUpstreamAndWait(
-                mInfraNetworkTracker.getTestIface().getInterfaceName());
+                        () -> initTestNetwork(mContext, lp, 5000 /* timeoutMs */));
+        String infraNetworkName = mInfraNetworkTracker.getTestIface().getInterfaceName();
+        mController.setTestNetworkAsUpstreamAndWait(infraNetworkName);
     }
 
     private void tearDownInfraNetwork() {
         runAsShell(MANAGE_TEST_NETWORKS, () -> mInfraNetworkTracker.teardown());
     }
 
-    private void startFtdChild(FullThreadDevice ftd) throws Exception {
-        ftd.factoryReset();
-        ftd.joinNetwork(DEFAULT_DATASET);
-        ftd.waitForStateAnyOf(List.of("router", "child"), JOIN_TIMEOUT);
-        waitFor(() -> ftd.getOmrAddress() != null, Duration.ofSeconds(60));
-        Inet6Address ftdOmr = ftd.getOmrAddress();
-        assertNotNull(ftdOmr);
-    }
-
     private void startInfraDeviceAndWaitForOnLinkAddr() throws Exception {
         mInfraDevice =
                 new InfraNetworkDevice(MacAddress.fromString("1:2:3:4:5:6"), mInfraNetworkReader);
@@ -668,20 +685,28 @@
         assertInfraLinkMemberOfGroup(address);
     }
 
-    private byte[] pollForPacketOnInfraNetwork(int type, Inet6Address srcAddress) {
-        return pollForPacketOnInfraNetwork(type, srcAddress, null);
+    private byte[] pollForIcmpPacketOnInfraNetwork(int type, InetAddress srcAddress) {
+        return pollForIcmpPacketOnInfraNetwork(type, srcAddress, null /* destAddress */);
     }
 
-    private byte[] pollForPacketOnInfraNetwork(
-            int type, Inet6Address srcAddress, Inet6Address destAddress) {
-        Predicate<byte[]> filter;
-        filter =
+    private byte[] pollForIcmpPacketOnInfraNetwork(
+            int type, InetAddress srcAddress, InetAddress destAddress) {
+        if (srcAddress == null && destAddress == null) {
+            throw new IllegalArgumentException("srcAddress and destAddress cannot be both null");
+        }
+        if (srcAddress != null && destAddress != null) {
+            if ((srcAddress instanceof Inet4Address) != (destAddress instanceof Inet4Address)) {
+                throw new IllegalArgumentException(
+                        "srcAddress and destAddress must be both IPv4 or both IPv6");
+            }
+        }
+        boolean isIpv4 =
+                (srcAddress instanceof Inet4Address) || (destAddress instanceof Inet4Address);
+        final Predicate<byte[]> filter =
                 p ->
-                        (isExpectedIcmpv6Packet(p, type)
-                                && (srcAddress == null ? true : isFromIpv6Source(p, srcAddress))
-                                && (destAddress == null
-                                        ? true
-                                        : isToIpv6Destination(p, destAddress)));
+                        (isIpv4 ? isExpectedIcmpv4Packet(p, type) : isExpectedIcmpv6Packet(p, type))
+                                && (srcAddress == null || isFrom(p, srcAddress))
+                                && (destAddress == null || isTo(p, destAddress));
         return pollForPacket(mInfraNetworkReader, filter);
     }
 }
diff --git a/thread/tests/integration/src/android/net/thread/ServiceDiscoveryTest.java b/thread/tests/integration/src/android/net/thread/ServiceDiscoveryTest.java
index e10f134..2afca5f 100644
--- a/thread/tests/integration/src/android/net/thread/ServiceDiscoveryTest.java
+++ b/thread/tests/integration/src/android/net/thread/ServiceDiscoveryTest.java
@@ -18,10 +18,10 @@
 
 import static android.net.InetAddresses.parseNumericAddress;
 import static android.net.nsd.NsdManager.PROTOCOL_DNS_SD;
-import static android.net.thread.utils.IntegrationTestUtils.JOIN_TIMEOUT;
 import static android.net.thread.utils.IntegrationTestUtils.SERVICE_DISCOVERY_TIMEOUT;
 import static android.net.thread.utils.IntegrationTestUtils.discoverForServiceLost;
 import static android.net.thread.utils.IntegrationTestUtils.discoverService;
+import static android.net.thread.utils.IntegrationTestUtils.joinNetworkAndWaitForOmr;
 import static android.net.thread.utils.IntegrationTestUtils.resolveService;
 import static android.net.thread.utils.IntegrationTestUtils.resolveServiceUntil;
 import static android.net.thread.utils.IntegrationTestUtils.waitFor;
@@ -168,8 +168,7 @@
 
         // Creates Full Thread Devices (FTD) and let them join the network.
         for (FullThreadDevice ftd : mFtds) {
-            ftd.joinNetwork(DEFAULT_DATASET);
-            ftd.waitForStateAnyOf(List.of("router", "child"), JOIN_TIMEOUT);
+            joinNetworkAndWaitForOmr(ftd, DEFAULT_DATASET);
         }
 
         int randomId = new Random().nextInt(10_000);
@@ -223,8 +222,7 @@
 
         // Creates a Full Thread Devices (FTD) and let it join the network.
         FullThreadDevice ftd = mFtds.get(0);
-        ftd.joinNetwork(DEFAULT_DATASET);
-        ftd.waitForStateAnyOf(List.of("router", "child"), JOIN_TIMEOUT);
+        joinNetworkAndWaitForOmr(ftd, DEFAULT_DATASET);
         ftd.setSrpHostname("my-host");
         ftd.setSrpHostAddresses(List.of((Inet6Address) parseNumericAddress("2001:db8::1")));
         ftd.addSrpService(
@@ -279,8 +277,7 @@
 
         // Creates a Full Thread Devices (FTD) and let it join the network.
         FullThreadDevice ftd = mFtds.get(0);
-        ftd.joinNetwork(DEFAULT_DATASET);
-        ftd.waitForStateAnyOf(List.of("router", "child"), JOIN_TIMEOUT);
+        joinNetworkAndWaitForOmr(ftd, DEFAULT_DATASET);
         ftd.setSrpHostname("my-host");
         ftd.setSrpHostAddresses(
                 List.of(
@@ -346,8 +343,7 @@
         mRegistrationListeners.add(listener);
         for (int i = 0; i < NUM_FTD; ++i) {
             FullThreadDevice ftd = mFtds.get(i);
-            ftd.joinNetwork(DEFAULT_DATASET);
-            ftd.waitForStateAnyOf(List.of("router", "child"), JOIN_TIMEOUT);
+            joinNetworkAndWaitForOmr(ftd, DEFAULT_DATASET);
             ftd.setDnsServerAddress(mOtCtl.getMlEid().getHostAddress());
         }
         final ArrayList<NsdServiceInfo> browsedServices = new ArrayList<>();
@@ -409,8 +405,7 @@
          * </pre>
          */
         FullThreadDevice srpClient = mFtds.get(0);
-        srpClient.joinNetwork(DEFAULT_DATASET);
-        srpClient.waitForStateAnyOf(List.of("router", "child"), JOIN_TIMEOUT);
+        joinNetworkAndWaitForOmr(srpClient, DEFAULT_DATASET);
         srpClient.setSrpHostname("my-host");
         srpClient.setSrpHostAddresses(List.of((Inet6Address) parseNumericAddress("2001::1")));
         srpClient.addSrpService(
@@ -421,8 +416,7 @@
                 Map.of("key1", bytes(0x01, 0x02), "key2", bytes(0x03)));
 
         FullThreadDevice dnsClient = mFtds.get(1);
-        dnsClient.joinNetwork(DEFAULT_DATASET);
-        dnsClient.waitForStateAnyOf(List.of("router", "child"), JOIN_TIMEOUT);
+        joinNetworkAndWaitForOmr(dnsClient, DEFAULT_DATASET);
         dnsClient.setDnsServerAddress(mOtCtl.getMlEid().getHostAddress());
 
         NsdServiceInfo browsedService = dnsClient.browseService("_test._udp.default.service.arpa.");
diff --git a/thread/tests/integration/src/android/net/thread/utils/FullThreadDevice.java b/thread/tests/integration/src/android/net/thread/utils/FullThreadDevice.java
index c0a8eea..083a841 100644
--- a/thread/tests/integration/src/android/net/thread/utils/FullThreadDevice.java
+++ b/thread/tests/integration/src/android/net/thread/utils/FullThreadDevice.java
@@ -282,6 +282,7 @@
         for (String subtype : subtypes) {
             fullServiceType.append(",").append(subtype);
         }
+        waitForSrpServer();
         executeCommand(
                 "srp client service add %s %s %d %d %d %s",
                 serviceName,
@@ -416,7 +417,7 @@
         executeCommand("ipmaddr add " + address.getHostAddress());
     }
 
-    public void ping(Inet6Address address, Inet6Address source) {
+    public void ping(InetAddress address, Inet6Address source) {
         ping(
                 address,
                 source,
@@ -427,7 +428,7 @@
                 PING_TIMEOUT_0_1_SECOND);
     }
 
-    public void ping(Inet6Address address) {
+    public void ping(InetAddress address) {
         ping(
                 address,
                 null,
@@ -439,7 +440,7 @@
     }
 
     /** Returns the number of ping reply packets received. */
-    public int ping(Inet6Address address, int count) {
+    public int ping(InetAddress address, int count) {
         List<String> output =
                 ping(
                         address,
@@ -453,7 +454,7 @@
     }
 
     private List<String> ping(
-            Inet6Address address,
+            InetAddress address,
             Inet6Address source,
             int size,
             int count,
@@ -492,6 +493,22 @@
         return -1;
     }
 
+    /** Waits for an SRP server to be present in Network Data */
+    private void waitForSrpServer() throws TimeoutException {
+        // CLI output:
+        // > srp client server
+        // [fd64:db12:25f4:7e0b:1bfc:6344:25ac:2dd7]:53538
+        // Done
+        waitFor(
+                () -> {
+                    final String serverAddr = executeCommand("srp client server").get(0);
+                    final int lastColonIndex = serverAddr.lastIndexOf(':');
+                    final int port = Integer.parseInt(serverAddr.substring(lastColonIndex + 1));
+                    return port > 0;
+                },
+                SERVICE_DISCOVERY_TIMEOUT);
+    }
+
     @FormatMethod
     private List<String> executeCommand(String commandFormat, Object... args) {
         return executeCommand(String.format(commandFormat, args));
diff --git a/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.java b/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.java
index ada46c8..82e9332 100644
--- a/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.java
+++ b/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.java
@@ -16,14 +16,18 @@
 package android.net.thread.utils;
 
 import static android.net.NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK;
+import static android.system.OsConstants.IPPROTO_ICMP;
 import static android.system.OsConstants.IPPROTO_ICMPV6;
 
 import static com.android.compatibility.common.util.SystemUtil.runShellCommandOrThrow;
 import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ND_OPTION_PIO;
 import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ROUTER_ADVERTISEMENT;
 
+import static com.google.common.io.BaseEncoding.base16;
 import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
 
+import static org.junit.Assert.assertNotNull;
+
 import static java.util.concurrent.TimeUnit.MILLISECONDS;
 import static java.util.concurrent.TimeUnit.SECONDS;
 
@@ -36,6 +40,7 @@
 import android.net.TestNetworkInterface;
 import android.net.nsd.NsdManager;
 import android.net.nsd.NsdServiceInfo;
+import android.net.thread.ActiveOperationalDataset;
 import android.net.thread.ThreadNetworkController;
 import android.os.Build;
 import android.os.Handler;
@@ -45,7 +50,9 @@
 import androidx.test.core.app.ApplicationProvider;
 
 import com.android.net.module.util.Struct;
+import com.android.net.module.util.structs.Icmpv4Header;
 import com.android.net.module.util.structs.Icmpv6Header;
+import com.android.net.module.util.structs.Ipv4Header;
 import com.android.net.module.util.structs.Ipv6Header;
 import com.android.net.module.util.structs.PrefixInformationOption;
 import com.android.net.module.util.structs.RaHeader;
@@ -58,6 +65,7 @@
 import java.io.IOException;
 import java.net.DatagramPacket;
 import java.net.DatagramSocket;
+import java.net.Inet4Address;
 import java.net.Inet6Address;
 import java.net.InetAddress;
 import java.net.InetSocketAddress;
@@ -84,6 +92,17 @@
     public static final Duration CALLBACK_TIMEOUT = Duration.ofSeconds(1);
     public static final Duration SERVICE_DISCOVERY_TIMEOUT = Duration.ofSeconds(20);
 
+    // A valid Thread Active Operational Dataset generated from OpenThread CLI "dataset init new".
+    private static final byte[] DEFAULT_DATASET_TLVS =
+            base16().decode(
+                            "0E080000000000010000000300001335060004001FFFE002"
+                                    + "08ACC214689BC40BDF0708FD64DB1225F47E0B0510F26B31"
+                                    + "53760F519A63BAFDDFFC80D2AF030F4F70656E5468726561"
+                                    + "642D643961300102D9A00410A245479C836D551B9CA557F7"
+                                    + "B9D351B40C0402A0FFF8");
+    public static final ActiveOperationalDataset DEFAULT_DATASET =
+            ActiveOperationalDataset.fromThreadTlvs(DEFAULT_DATASET_TLVS);
+
     private IntegrationTestUtils() {}
 
     /**
@@ -177,16 +196,36 @@
         return null;
     }
 
-    /** Returns {@code true} if {@code packet} is an ICMPv6 packet of given {@code type}. */
-    public static boolean isExpectedIcmpv6Packet(byte[] packet, int type) {
-        if (packet == null) {
+    /** Returns {@code true} if {@code packet} is an ICMPv4 packet of given {@code type}. */
+    public static boolean isExpectedIcmpv4Packet(byte[] packet, int type) {
+        ByteBuffer buf = makeByteBuffer(packet);
+        Ipv4Header header = extractIpv4Header(buf);
+        if (header == null) {
             return false;
         }
-        ByteBuffer buf = ByteBuffer.wrap(packet);
+        if (header.protocol != (byte) IPPROTO_ICMP) {
+            return false;
+        }
         try {
-            if (Struct.parse(Ipv6Header.class, buf).nextHeader != (byte) IPPROTO_ICMPV6) {
-                return false;
-            }
+            return Struct.parse(Icmpv4Header.class, buf).type == (short) type;
+        } catch (IllegalArgumentException ignored) {
+            // It's fine that the passed in packet is malformed because it's could be sent
+            // by anybody.
+        }
+        return false;
+    }
+
+    /** Returns {@code true} if {@code packet} is an ICMPv6 packet of given {@code type}. */
+    public static boolean isExpectedIcmpv6Packet(byte[] packet, int type) {
+        ByteBuffer buf = makeByteBuffer(packet);
+        Ipv6Header header = extractIpv6Header(buf);
+        if (header == null) {
+            return false;
+        }
+        if (header.nextHeader != (byte) IPPROTO_ICMPV6) {
+            return false;
+        }
+        try {
             return Struct.parse(Icmpv6Header.class, buf).type == (short) type;
         } catch (IllegalArgumentException ignored) {
             // It's fine that the passed in packet is malformed because it's could be sent
@@ -195,32 +234,66 @@
         return false;
     }
 
-    public static boolean isFromIpv6Source(byte[] packet, Inet6Address src) {
-        if (packet == null) {
-            return false;
-        }
-        ByteBuffer buf = ByteBuffer.wrap(packet);
-        try {
-            return Struct.parse(Ipv6Header.class, buf).srcIp.equals(src);
-        } catch (IllegalArgumentException ignored) {
-            // It's fine that the passed in packet is malformed because it's could be sent
-            // by anybody.
+    public static boolean isFrom(byte[] packet, InetAddress src) {
+        if (src instanceof Inet4Address) {
+            return isFromIpv4Source(packet, (Inet4Address) src);
+        } else if (src instanceof Inet6Address) {
+            return isFromIpv6Source(packet, (Inet6Address) src);
         }
         return false;
     }
 
-    public static boolean isToIpv6Destination(byte[] packet, Inet6Address dest) {
-        if (packet == null) {
-            return false;
+    public static boolean isTo(byte[] packet, InetAddress dest) {
+        if (dest instanceof Inet4Address) {
+            return isToIpv4Destination(packet, (Inet4Address) dest);
+        } else if (dest instanceof Inet6Address) {
+            return isToIpv6Destination(packet, (Inet6Address) dest);
         }
-        ByteBuffer buf = ByteBuffer.wrap(packet);
+        return false;
+    }
+
+    private static boolean isFromIpv4Source(byte[] packet, Inet4Address src) {
+        Ipv4Header header = extractIpv4Header(makeByteBuffer(packet));
+        return header != null && header.srcIp.equals(src);
+    }
+
+    private static boolean isFromIpv6Source(byte[] packet, Inet6Address src) {
+        Ipv6Header header = extractIpv6Header(makeByteBuffer(packet));
+        return header != null && header.srcIp.equals(src);
+    }
+
+    private static boolean isToIpv4Destination(byte[] packet, Inet4Address dest) {
+        Ipv4Header header = extractIpv4Header(makeByteBuffer(packet));
+        return header != null && header.dstIp.equals(dest);
+    }
+
+    private static boolean isToIpv6Destination(byte[] packet, Inet6Address dest) {
+        Ipv6Header header = extractIpv6Header(makeByteBuffer(packet));
+        return header != null && header.dstIp.equals(dest);
+    }
+
+    private static ByteBuffer makeByteBuffer(byte[] packet) {
+        return packet == null ? null : ByteBuffer.wrap(packet);
+    }
+
+    private static Ipv4Header extractIpv4Header(ByteBuffer buf) {
         try {
-            return Struct.parse(Ipv6Header.class, buf).dstIp.equals(dest);
+            return Struct.parse(Ipv4Header.class, buf);
         } catch (IllegalArgumentException ignored) {
             // It's fine that the passed in packet is malformed because it's could be sent
             // by anybody.
         }
-        return false;
+        return null;
+    }
+
+    private static Ipv6Header extractIpv6Header(ByteBuffer buf) {
+        try {
+            return Struct.parse(Ipv6Header.class, buf);
+        } catch (IllegalArgumentException ignored) {
+            // It's fine that the passed in packet is malformed because it's could be sent
+            // by anybody.
+        }
+        return null;
     }
 
     /** Returns the Prefix Information Options (PIO) extracted from an ICMPv6 RA message. */
@@ -413,6 +486,22 @@
         return networkFuture.get(timeout.toSeconds(), SECONDS);
     }
 
+    /**
+     * Let the FTD join the specified Thread network and wait for border routing to be available.
+     *
+     * @return the OMR address
+     */
+    public static Inet6Address joinNetworkAndWaitForOmr(
+            FullThreadDevice ftd, ActiveOperationalDataset dataset) throws Exception {
+        ftd.factoryReset();
+        ftd.joinNetwork(dataset);
+        ftd.waitForStateAnyOf(List.of("router", "child"), JOIN_TIMEOUT);
+        waitFor(() -> ftd.getOmrAddress() != null, Duration.ofSeconds(60));
+        Inet6Address ftdOmr = ftd.getOmrAddress();
+        assertNotNull(ftdOmr);
+        return ftdOmr;
+    }
+
     private static class DefaultDiscoveryListener implements NsdManager.DiscoveryListener {
         @Override
         public void onStartDiscoveryFailed(String serviceType, int errorCode) {}
diff --git a/thread/tests/integration/src/android/net/thread/utils/OtDaemonController.java b/thread/tests/integration/src/android/net/thread/utils/OtDaemonController.java
index b3175fd..15a3f5c 100644
--- a/thread/tests/integration/src/android/net/thread/utils/OtDaemonController.java
+++ b/thread/tests/integration/src/android/net/thread/utils/OtDaemonController.java
@@ -105,6 +105,29 @@
         return prefixes.isEmpty() ? null : prefixes.get(0);
     }
 
+    /** Enables/Disables NAT64 feature. */
+    public void setNat64Enabled(boolean enabled) {
+        executeCommand("nat64 " + (enabled ? "enable" : "disable"));
+    }
+
+    /** Sets the NAT64 CIDR. */
+    public void setNat64Cidr(String cidr) {
+        executeCommand("nat64 cidr " + cidr);
+    }
+
+    /** Returns whether there's a NAT64 prefix in network data */
+    public boolean hasNat64PrefixInNetdata() {
+        // Example (in the 'Routes' section):
+        // fdb2:bae3:5b59:2:0:0::/96 sn low c000
+        List<String> outputLines = executeCommandAndParse("netdata show");
+        for (String line : outputLines) {
+            if (line.contains(" sn")) {
+                return true;
+            }
+        }
+        return false;
+    }
+
     public String executeCommand(String cmd) {
         return SystemUtil.runShellCommand(OT_CTL + " " + cmd);
     }
diff --git a/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java
index 6e2369f..be32764 100644
--- a/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java
+++ b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java
@@ -16,6 +16,12 @@
 
 package com.android.server.thread;
 
+import static android.Manifest.permission.NETWORK_SETTINGS;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VPN;
+import static android.net.NetworkCapabilities.TRANSPORT_ETHERNET;
+import static android.net.NetworkCapabilities.TRANSPORT_THREAD;
+import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
 import static android.net.thread.ActiveOperationalDataset.CHANNEL_PAGE_24_GHZ;
 import static android.net.thread.ThreadNetworkController.STATE_DISABLED;
 import static android.net.thread.ThreadNetworkController.STATE_ENABLED;
@@ -26,7 +32,6 @@
 import static android.net.thread.ThreadNetworkManager.PERMISSION_THREAD_NETWORK_PRIVILEGED;
 
 import static com.android.server.thread.ThreadNetworkCountryCode.DEFAULT_COUNTRY_CODE;
-import static com.android.server.thread.ThreadPersistentSettings.THREAD_ENABLED_IN_AIRPLANE_MODE;
 import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_INVALID_STATE;
 
 import static com.google.common.io.BaseEncoding.base16;
@@ -39,6 +44,8 @@
 import static org.mockito.ArgumentMatchers.argThat;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.clearInvocations;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.doThrow;
@@ -49,6 +56,8 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
+
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
@@ -56,6 +65,7 @@
 import android.net.ConnectivityManager;
 import android.net.NetworkAgent;
 import android.net.NetworkProvider;
+import android.net.NetworkRequest;
 import android.net.thread.ActiveOperationalDataset;
 import android.net.thread.IActiveOperationalDatasetReceiver;
 import android.net.thread.IOperationReceiver;
@@ -68,7 +78,6 @@
 import android.os.SystemClock;
 import android.os.UserManager;
 import android.os.test.TestLooper;
-import android.provider.Settings;
 import android.util.AtomicFile;
 
 import androidx.test.annotation.UiThreadTest;
@@ -96,11 +105,11 @@
 import org.mockito.MockitoAnnotations;
 import org.mockito.MockitoSession;
 
-import java.nio.charset.StandardCharsets;
 import java.time.Clock;
 import java.time.DateTimeException;
 import java.time.Instant;
 import java.time.ZoneId;
+import java.util.List;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.atomic.AtomicReference;
@@ -150,7 +159,6 @@
     private static final byte[] TEST_VENDOR_OUI_BYTES = new byte[] {(byte) 0xAC, (byte) 0xDE, 0x48};
     private static final String TEST_VENDOR_NAME = "test vendor";
     private static final String TEST_MODEL_NAME = "test model";
-    private static final boolean TEST_VGH_VALUE = false;
 
     @Mock private ConnectivityManager mMockConnectivityManager;
     @Mock private NetworkAgent mMockNetworkAgent;
@@ -182,6 +190,9 @@
                 .when(mContext)
                 .enforceCallingOrSelfPermission(
                         eq(PERMISSION_THREAD_NETWORK_PRIVILEGED), anyString());
+        doNothing()
+                .when(mContext)
+                .enforceCallingOrSelfPermission(eq(NETWORK_SETTINGS), anyString());
 
         mTestLooper = new TestLooper();
         final Handler handler = new Handler(mTestLooper.getLooper());
@@ -193,8 +204,6 @@
 
         when(mMockUserManager.hasUserRestriction(eq(DISALLOW_THREAD_NETWORK))).thenReturn(false);
 
-        Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 0);
-
         when(mConnectivityResources.get()).thenReturn(mResources);
         when(mResources.getBoolean(eq(R.bool.config_thread_default_enabled))).thenReturn(true);
         when(mResources.getString(eq(R.string.config_thread_vendor_name)))
@@ -203,8 +212,8 @@
                 .thenReturn(TEST_VENDOR_OUI);
         when(mResources.getString(eq(R.string.config_thread_model_name)))
                 .thenReturn(TEST_MODEL_NAME);
-        when(mResources.getBoolean(eq(R.bool.config_thread_managed_by_google_home)))
-                .thenReturn(TEST_VGH_VALUE);
+        when(mResources.getStringArray(eq(R.array.config_thread_mdns_vendor_specific_txts)))
+                .thenReturn(new String[] {});
 
         final AtomicFile storageFile = new AtomicFile(tempFolder.newFile("thread_settings.xml"));
         mPersistentSettings = new ThreadPersistentSettings(storageFile, mConnectivityResources);
@@ -247,8 +256,8 @@
                 .thenReturn(TEST_VENDOR_OUI);
         when(mResources.getString(eq(R.string.config_thread_model_name)))
                 .thenReturn(TEST_MODEL_NAME);
-        when(mResources.getBoolean(eq(R.bool.config_thread_managed_by_google_home)))
-                .thenReturn(true);
+        when(mResources.getStringArray(eq(R.array.config_thread_mdns_vendor_specific_txts)))
+                .thenReturn(new String[] {"vt=test"});
 
         mService.initialize();
         mTestLooper.dispatchAll();
@@ -258,19 +267,7 @@
         assertThat(meshcopTxts.vendorOui).isEqualTo(TEST_VENDOR_OUI_BYTES);
         assertThat(meshcopTxts.modelName).isEqualTo(TEST_MODEL_NAME);
         assertThat(meshcopTxts.nonStandardTxtEntries)
-                .containsExactly(new DnsTxtAttribute("vgh", "1".getBytes(StandardCharsets.UTF_8)));
-    }
-
-    @Test
-    public void getMeshcopTxtAttributes_managedByGoogleIsFalse_vghIsZero() {
-        when(mResources.getBoolean(eq(R.bool.config_thread_managed_by_google_home)))
-                .thenReturn(false);
-
-        MeshcopTxtAttributes meshcopTxts =
-                ThreadNetworkControllerService.getMeshcopTxtAttributes(mResources);
-
-        assertThat(meshcopTxts.nonStandardTxtEntries)
-                .containsExactly(new DnsTxtAttribute("vgh", "0".getBytes(StandardCharsets.UTF_8)));
+                .containsExactly(new DnsTxtAttribute("vt", "test".getBytes(UTF_8)));
     }
 
     @Test
@@ -343,6 +340,61 @@
     }
 
     @Test
+    public void makeVendorSpecificTxtAttrs_validTxts_returnsParsedTxtAttrs() {
+        String[] txts = new String[] {"va=123", "vb=", "vc"};
+
+        List<DnsTxtAttribute> attrs = mService.makeVendorSpecificTxtAttrs(txts);
+
+        assertThat(attrs)
+                .containsExactly(
+                        new DnsTxtAttribute("va", "123".getBytes(UTF_8)),
+                        new DnsTxtAttribute("vb", new byte[] {}),
+                        new DnsTxtAttribute("vc", new byte[] {}));
+    }
+
+    @Test
+    public void makeVendorSpecificTxtAttrs_txtKeyNotStartWithV_throwsIllegalArgument() {
+        String[] txts = new String[] {"abc=123"};
+
+        assertThrows(
+                IllegalArgumentException.class, () -> mService.makeVendorSpecificTxtAttrs(txts));
+    }
+
+    @Test
+    public void makeVendorSpecificTxtAttrs_txtIsTooShort_throwsIllegalArgument() {
+        String[] txtEmptyKey = new String[] {"=123"};
+        String[] txtSingleCharKey = new String[] {"v=456"};
+
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> mService.makeVendorSpecificTxtAttrs(txtEmptyKey));
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> mService.makeVendorSpecificTxtAttrs(txtSingleCharKey));
+    }
+
+    @Test
+    public void makeVendorSpecificTxtAttrs_txtValueIsEmpty_parseSuccess() {
+        String[] txts = new String[] {"va=", "vb"};
+
+        List<DnsTxtAttribute> attrs = mService.makeVendorSpecificTxtAttrs(txts);
+
+        assertThat(attrs)
+                .containsExactly(
+                        new DnsTxtAttribute("va", new byte[] {}),
+                        new DnsTxtAttribute("vb", new byte[] {}));
+    }
+
+    @Test
+    public void makeVendorSpecificTxtAttrs_multipleEquals_splittedByTheFirstEqual() {
+        String[] txts = new String[] {"va=abc=def=123"};
+
+        List<DnsTxtAttribute> attrs = mService.makeVendorSpecificTxtAttrs(txts);
+
+        assertThat(attrs).containsExactly(new DnsTxtAttribute("va", "abc=def=123".getBytes(UTF_8)));
+    }
+
+    @Test
     public void join_otDaemonRemoteFailure_returnsInternalError() throws Exception {
         mService.initialize();
         final IOperationReceiver mockReceiver = mock(IOperationReceiver.class);
@@ -437,100 +489,6 @@
         assertThat(failure.getErrorCode()).isEqualTo(ERROR_FAILED_PRECONDITION);
     }
 
-    @Test
-    public void airplaneMode_initWithAirplaneModeOn_otDaemonNotStarted() {
-        Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 1);
-
-        mService.initialize();
-        mTestLooper.dispatchAll();
-
-        assertThat(mFakeOtDaemon.isInitialized()).isFalse();
-    }
-
-    @Test
-    public void airplaneMode_initWithAirplaneModeOff_threadIsEnabled() {
-        Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 0);
-
-        mService.initialize();
-        mTestLooper.dispatchAll();
-
-        assertThat(mFakeOtDaemon.getEnabledState()).isEqualTo(STATE_ENABLED);
-    }
-
-    @Test
-    public void airplaneMode_changesFromOffToOn_stateIsDisabled() {
-        Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 0);
-        AtomicReference<BroadcastReceiver> receiverRef =
-                captureBroadcastReceiver(Intent.ACTION_AIRPLANE_MODE_CHANGED);
-        mService.initialize();
-        mTestLooper.dispatchAll();
-
-        Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 1);
-        receiverRef.get().onReceive(mContext, new Intent());
-        mTestLooper.dispatchAll();
-
-        assertThat(mFakeOtDaemon.getEnabledState()).isEqualTo(STATE_DISABLED);
-    }
-
-    @Test
-    public void airplaneMode_changesFromOnToOff_stateIsEnabled() {
-        Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 1);
-        AtomicReference<BroadcastReceiver> receiverRef =
-                captureBroadcastReceiver(Intent.ACTION_AIRPLANE_MODE_CHANGED);
-        mService.initialize();
-        mTestLooper.dispatchAll();
-
-        Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 0);
-        receiverRef.get().onReceive(mContext, new Intent());
-        mTestLooper.dispatchAll();
-
-        assertThat(mFakeOtDaemon.getEnabledState()).isEqualTo(STATE_ENABLED);
-    }
-
-    @Test
-    public void airplaneMode_setEnabledWhenAirplaneModeIsOn_WillNotAutoDisableSecondTime() {
-        Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 1);
-        AtomicReference<BroadcastReceiver> receiverRef =
-                captureBroadcastReceiver(Intent.ACTION_AIRPLANE_MODE_CHANGED);
-        CompletableFuture<Void> setEnabledFuture = new CompletableFuture<>();
-        mService.initialize();
-
-        mService.setEnabled(true, newOperationReceiver(setEnabledFuture));
-        mTestLooper.dispatchAll();
-        Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 0);
-        receiverRef.get().onReceive(mContext, new Intent());
-        mTestLooper.dispatchAll();
-        Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 1);
-        receiverRef.get().onReceive(mContext, new Intent());
-        mTestLooper.dispatchAll();
-
-        assertThat(mFakeOtDaemon.getEnabledState()).isEqualTo(STATE_ENABLED);
-        assertThat(mPersistentSettings.get(THREAD_ENABLED_IN_AIRPLANE_MODE)).isTrue();
-    }
-
-    @Test
-    public void airplaneMode_setDisabledWhenAirplaneModeIsOn_WillAutoDisableSecondTime() {
-        Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 1);
-        AtomicReference<BroadcastReceiver> receiverRef =
-                captureBroadcastReceiver(Intent.ACTION_AIRPLANE_MODE_CHANGED);
-        CompletableFuture<Void> setEnabledFuture = new CompletableFuture<>();
-        mService.initialize();
-        mService.setEnabled(true, newOperationReceiver(setEnabledFuture));
-        mTestLooper.dispatchAll();
-
-        mService.setEnabled(false, newOperationReceiver(setEnabledFuture));
-        mTestLooper.dispatchAll();
-        Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 0);
-        receiverRef.get().onReceive(mContext, new Intent());
-        mTestLooper.dispatchAll();
-        Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 1);
-        receiverRef.get().onReceive(mContext, new Intent());
-        mTestLooper.dispatchAll();
-
-        assertThat(mFakeOtDaemon.getEnabledState()).isEqualTo(STATE_DISABLED);
-        assertThat(mPersistentSettings.get(THREAD_ENABLED_IN_AIRPLANE_MODE)).isFalse();
-    }
-
     private AtomicReference<BroadcastReceiver> captureBroadcastReceiver(String action) {
         AtomicReference<BroadcastReceiver> receiverRef = new AtomicReference<>();
 
@@ -542,7 +500,9 @@
                 .when(mContext)
                 .registerReceiver(
                         any(BroadcastReceiver.class),
-                        argThat(actualIntentFilter -> actualIntentFilter.hasAction(action)));
+                        argThat(actualIntentFilter -> actualIntentFilter.hasAction(action)),
+                        any(),
+                        any());
 
         return receiverRef;
     }
@@ -768,12 +728,12 @@
         ThreadConfiguration config1 =
                 new ThreadConfiguration.Builder()
                         .setNat64Enabled(false)
-                        .setDhcp6PdEnabled(false)
+                        .setDhcpv6PdEnabled(false)
                         .build();
         ThreadConfiguration config2 =
                 new ThreadConfiguration.Builder()
                         .setNat64Enabled(true)
-                        .setDhcp6PdEnabled(true)
+                        .setDhcpv6PdEnabled(true)
                         .build();
         ThreadConfiguration config3 =
                 new ThreadConfiguration.Builder(config2).build(); // Same as config2
@@ -789,4 +749,56 @@
         inOrder.verify(mockReceiver2).onSuccess();
         inOrder.verify(mockReceiver3).onSuccess();
     }
+
+    @Test
+    public void initialize_upstreamNetworkRequestHasCertainTransportTypesAndCapabilities() {
+        mService.initialize();
+        mTestLooper.dispatchAll();
+
+        ArgumentCaptor<NetworkRequest> networkRequestCaptor =
+                ArgumentCaptor.forClass(NetworkRequest.class);
+        verify(mMockConnectivityManager, atLeastOnce())
+                .registerNetworkCallback(
+                        networkRequestCaptor.capture(),
+                        any(ConnectivityManager.NetworkCallback.class),
+                        any(Handler.class));
+        List<NetworkRequest> upstreamNetworkRequests =
+                networkRequestCaptor.getAllValues().stream()
+                        .filter(nr -> !nr.hasTransport(TRANSPORT_THREAD))
+                        .toList();
+        assertThat(upstreamNetworkRequests.size()).isEqualTo(1);
+        NetworkRequest upstreamNetworkRequest = upstreamNetworkRequests.get(0);
+        assertThat(upstreamNetworkRequest.hasTransport(TRANSPORT_WIFI)).isTrue();
+        assertThat(upstreamNetworkRequest.hasTransport(TRANSPORT_ETHERNET)).isTrue();
+        assertThat(upstreamNetworkRequest.hasCapability(NET_CAPABILITY_NOT_VPN)).isTrue();
+        assertThat(upstreamNetworkRequest.hasCapability(NET_CAPABILITY_INTERNET)).isTrue();
+    }
+
+    @Test
+    public void setTestNetworkAsUpstream_upstreamNetworkRequestAlwaysDisallowsVpn() {
+        mService.initialize();
+        mTestLooper.dispatchAll();
+        clearInvocations(mMockConnectivityManager);
+
+        final IOperationReceiver mockReceiver1 = mock(IOperationReceiver.class);
+        final IOperationReceiver mockReceiver2 = mock(IOperationReceiver.class);
+        mService.setTestNetworkAsUpstream("test-network", mockReceiver1);
+        mService.setTestNetworkAsUpstream(null, mockReceiver2);
+        mTestLooper.dispatchAll();
+
+        ArgumentCaptor<NetworkRequest> networkRequestCaptor =
+                ArgumentCaptor.forClass(NetworkRequest.class);
+        verify(mMockConnectivityManager, times(2))
+                .registerNetworkCallback(
+                        networkRequestCaptor.capture(),
+                        any(ConnectivityManager.NetworkCallback.class),
+                        any(Handler.class));
+        assertThat(networkRequestCaptor.getAllValues().size()).isEqualTo(2);
+        NetworkRequest networkRequest1 = networkRequestCaptor.getAllValues().get(0);
+        NetworkRequest networkRequest2 = networkRequestCaptor.getAllValues().get(1);
+        assertThat(networkRequest1.getNetworkSpecifier()).isNotNull();
+        assertThat(networkRequest1.hasCapability(NET_CAPABILITY_NOT_VPN)).isTrue();
+        assertThat(networkRequest2.getNetworkSpecifier()).isNull();
+        assertThat(networkRequest2.hasCapability(NET_CAPABILITY_NOT_VPN)).isTrue();
+    }
 }
diff --git a/thread/tests/unit/src/com/android/server/thread/ThreadPersistentSettingsTest.java b/thread/tests/unit/src/com/android/server/thread/ThreadPersistentSettingsTest.java
index c932ac8..ba489d9 100644
--- a/thread/tests/unit/src/com/android/server/thread/ThreadPersistentSettingsTest.java
+++ b/thread/tests/unit/src/com/android/server/thread/ThreadPersistentSettingsTest.java
@@ -152,7 +152,7 @@
         ThreadConfiguration configuration =
                 new ThreadConfiguration.Builder()
                         .setNat64Enabled(true)
-                        .setDhcp6PdEnabled(true)
+                        .setDhcpv6PdEnabled(true)
                         .build();
         mThreadPersistentSettings.putConfiguration(configuration);
 
@@ -164,13 +164,13 @@
         ThreadConfiguration configuration1 =
                 new ThreadConfiguration.Builder()
                         .setNat64Enabled(false)
-                        .setDhcp6PdEnabled(false)
+                        .setDhcpv6PdEnabled(false)
                         .build();
         mThreadPersistentSettings.putConfiguration(configuration1);
         ThreadConfiguration configuration2 =
                 new ThreadConfiguration.Builder()
                         .setNat64Enabled(true)
-                        .setDhcp6PdEnabled(true)
+                        .setDhcpv6PdEnabled(true)
                         .build();
 
         assertThat(mThreadPersistentSettings.putConfiguration(configuration2)).isTrue();
@@ -188,9 +188,9 @@
     }
 
     @Test
-    public void putConfiguration_dhcp6PdEnabled_valuesUpdatedAndPersisted() throws Exception {
+    public void putConfiguration_dhcpv6PdEnabled_valuesUpdatedAndPersisted() throws Exception {
         ThreadConfiguration configuration =
-                new ThreadConfiguration.Builder().setDhcp6PdEnabled(true).build();
+                new ThreadConfiguration.Builder().setDhcpv6PdEnabled(true).build();
         mThreadPersistentSettings.putConfiguration(configuration);
 
         assertThat(mThreadPersistentSettings.getConfiguration()).isEqualTo(configuration);