Merge "Check whether feature is enabled by compare with apex version" into mainline-prod am: 2b5ca309f9

Original change: https://googleplex-android-review.googlesource.com/c/platform/packages/modules/Connectivity/+/14608089

Change-Id: If82b0f147e3f26813746e7f3c4df11f605bf4a94
diff --git a/TEST_MAPPING b/TEST_MAPPING
index ef96d88..389453a 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -15,7 +15,8 @@
   ],
   "mainline-presubmit": [
     {
-      "name": "CtsNetTestCasesLatestSdk[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex]",
+      // TODO: add back the tethering modules when updatable in this branch
+      "name": "CtsNetTestCasesLatestSdk[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex]",
       "options": [
         {
           "exclude-annotation": "com.android.testutils.SkipPresubmit"
@@ -26,7 +27,8 @@
   // Tests on physical devices with SIM cards: postsubmit only for capacity constraints
   "mainline-postsubmit": [
     {
-      "name": "CtsNetTestCasesLatestSdk[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex]",
+      // TODO: add back the tethering module when updatable in this branch
+      "name": "CtsNetTestCasesLatestSdk[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex]",
       "keywords": ["sim"]
     }
   ]
diff --git a/Tethering/Android.bp b/Tethering/Android.bp
index 7993c20..f469d89 100644
--- a/Tethering/Android.bp
+++ b/Tethering/Android.bp
@@ -25,7 +25,7 @@
     srcs: [
         "apishim/**/*.java",
         "src/**/*.java",
-        ":framework-tethering-shared-srcs",
+        ":framework-connectivity-shared-srcs",
         ":tethering-module-utils-srcs",
         ":services-tethering-shared-srcs",
     ],
@@ -37,11 +37,14 @@
         "networkstack-client",
         "android.hardware.tetheroffload.config-V1.0-java",
         "android.hardware.tetheroffload.control-V1.0-java",
+        "android.hardware.tetheroffload.control-V1.1-java",
         "net-utils-framework-common",
         "net-utils-device-common",
         "netd-client",
+        "NetworkStackApiCurrentShims",
     ],
     libs: [
+        "framework-connectivity",
         "framework-statsd.stubs.module_lib",
         "framework-tethering.impl",
         "framework-wifi",
@@ -145,7 +148,10 @@
     manifest: "AndroidManifest.xml",
     use_embedded_native_libs: true,
     // The permission configuration *must* be included to ensure security of the device
-    required: ["NetworkPermissionConfig"],
+    required: [
+        "NetworkPermissionConfig",
+        "privapp_whitelist_com.android.networkstack.tethering",
+    ],
     apex_available: ["com.android.tethering"],
     min_sdk_version: "30",
 }
diff --git a/Tethering/apex/Android.bp b/Tethering/apex/Android.bp
index 6703e46..5be3933 100644
--- a/Tethering/apex/Android.bp
+++ b/Tethering/apex/Android.bp
@@ -20,14 +20,30 @@
 
 apex {
     name: "com.android.tethering",
+    compile_multilib: "both",
     updatable: true,
     min_sdk_version: "30",
-    java_libs: ["framework-tethering"],
+    java_libs: [
+        "framework-connectivity",
+        "framework-tethering",
+        "service-connectivity",
+    ],
+    multilib: {
+        first: {
+            jni_libs: ["libservice-connectivity"]
+        },
+        both: {
+            jni_libs: ["libframework-connectivity-jni"],
+        }
+    },
     bpfs: [
         "offload.o",
         "test.o",
     ],
-    apps: ["Tethering"],
+    apps: [
+        "ServiceConnectivityResources",
+        "Tethering",
+    ],
     manifest: "manifest.json",
     key: "com.android.tethering.key",
 
@@ -50,6 +66,7 @@
     base: "com.android.tethering",
     package_name: "com.android.tethering.inprocess",
     apps: [
+        "ServiceConnectivityResources",
         "InProcessTethering",
     ],
 }
diff --git a/Tethering/common/TetheringLib/Android.bp b/Tethering/common/TetheringLib/Android.bp
index 2631d08..6bff616 100644
--- a/Tethering/common/TetheringLib/Android.bp
+++ b/Tethering/common/TetheringLib/Android.bp
@@ -26,6 +26,13 @@
     ],
 
     srcs: [":framework-tethering-srcs"],
+    libs: ["framework-connectivity.stubs.module_lib"],
+    stub_only_libs: ["framework-connectivity.stubs.module_lib"],
+    aidl: {
+        include_dirs: [
+            "frameworks/base/packages/Connectivity/framework/aidl-export",
+        ],
+    },
 
     jarjar_rules: "jarjar-rules.txt",
     installable: true,
diff --git a/Tethering/jarjar-rules.txt b/Tethering/jarjar-rules.txt
index d1ad569..5de4b97 100644
--- a/Tethering/jarjar-rules.txt
+++ b/Tethering/jarjar-rules.txt
@@ -1,5 +1,5 @@
-# These must be kept in sync with the framework-tethering-shared-srcs filegroup.
-# Classes from the framework-tethering-shared-srcs filegroup.
+# These must be kept in sync with the framework-connectivity-shared-srcs filegroup.
+# Classes from the framework-connectivity-shared-srcs filegroup.
 # If there are files in that filegroup that are not covered below, the classes in the
 # module will be overwritten by the ones in the framework.
 rule com.android.internal.util.** com.android.networkstack.tethering.util.@1
diff --git a/Tethering/src/com/android/networkstack/tethering/OffloadController.java b/Tethering/src/com/android/networkstack/tethering/OffloadController.java
index 88c77b0..beb1821 100644
--- a/Tethering/src/com/android/networkstack/tethering/OffloadController.java
+++ b/Tethering/src/com/android/networkstack/tethering/OffloadController.java
@@ -26,6 +26,9 @@
 import static android.net.netstats.provider.NetworkStatsProvider.QUOTA_UNLIMITED;
 import static android.provider.Settings.Global.TETHER_OFFLOAD_DISABLED;
 
+import static com.android.networkstack.tethering.OffloadHardwareInterface.OFFLOAD_HAL_VERSION_1_0;
+import static com.android.networkstack.tethering.OffloadHardwareInterface.OFFLOAD_HAL_VERSION_1_1;
+import static com.android.networkstack.tethering.OffloadHardwareInterface.OFFLOAD_HAL_VERSION_NONE;
 import static com.android.networkstack.tethering.TetheringConfiguration.DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS;
 
 import android.annotation.NonNull;
@@ -96,7 +99,8 @@
     private final SharedLog mLog;
     private final HashMap<String, LinkProperties> mDownstreams;
     private boolean mConfigInitialized;
-    private boolean mControlInitialized;
+    @OffloadHardwareInterface.OffloadHalVersion
+    private int mControlHalVersion;
     private LinkProperties mUpstreamLinkProperties;
     // The complete set of offload-exempt prefixes passed in via Tethering from
     // all upstream and downstream sources.
@@ -112,11 +116,42 @@
     private ConcurrentHashMap<String, ForwardedStats> mForwardedStats =
             new ConcurrentHashMap<>(16, 0.75F, 1);
 
+    private static class InterfaceQuota {
+        public final long warningBytes;
+        public final long limitBytes;
+
+        public static InterfaceQuota MAX_VALUE = new InterfaceQuota(Long.MAX_VALUE, Long.MAX_VALUE);
+
+        InterfaceQuota(long warningBytes, long limitBytes) {
+            this.warningBytes = warningBytes;
+            this.limitBytes = limitBytes;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) return true;
+            if (!(o instanceof InterfaceQuota)) return false;
+            InterfaceQuota that = (InterfaceQuota) o;
+            return warningBytes == that.warningBytes
+                    && limitBytes == that.limitBytes;
+        }
+
+        @Override
+        public int hashCode() {
+            return (int) (warningBytes * 3 + limitBytes * 5);
+        }
+
+        @Override
+        public String toString() {
+            return "InterfaceQuota{" + "warning=" + warningBytes + ", limit=" + limitBytes + '}';
+        }
+    }
+
     // Maps upstream interface names to interface quotas.
     // Always contains the latest value received from the framework for each interface, regardless
     // of whether offload is currently running (or is even supported) on that interface. Only
     // includes upstream interfaces that have a quota set.
-    private HashMap<String, Long> mInterfaceQuotas = new HashMap<>();
+    private HashMap<String, InterfaceQuota> mInterfaceQuotas = new HashMap<>();
 
     // Tracking remaining alert quota. Unlike limit quota is subject to interface, the alert
     // quota is interface independent and global for tether offload. Note that this is only
@@ -179,7 +214,7 @@
             }
         }
 
-        mControlInitialized = mHwInterface.initOffloadControl(
+        mControlHalVersion = mHwInterface.initOffloadControl(
                 // OffloadHardwareInterface guarantees that these callback
                 // methods are called on the handler passed to it, which is the
                 // same as mHandler, as coordinated by the setup in Tethering.
@@ -248,6 +283,18 @@
                     }
 
                     @Override
+                    public void onWarningReached() {
+                        if (!started()) return;
+                        mLog.log("onWarningReached");
+
+                        updateStatsForCurrentUpstream();
+                        if (mStatsProvider != null) {
+                            mStatsProvider.pushTetherStats();
+                            mStatsProvider.notifyWarningReached();
+                        }
+                    }
+
+                    @Override
                     public void onNatTimeoutUpdate(int proto,
                                                    String srcAddr, int srcPort,
                                                    String dstAddr, int dstPort) {
@@ -261,7 +308,8 @@
             mLog.i("tethering offload control not supported");
             stop();
         } else {
-            mLog.log("tethering offload started");
+            mLog.log("tethering offload started, version: "
+                    + OffloadHardwareInterface.halVerToString(mControlHalVersion));
             mNatUpdateCallbacksReceived = 0;
             mNatUpdateNetlinkErrors = 0;
             maybeSchedulePollingStats();
@@ -278,7 +326,7 @@
         updateStatsForCurrentUpstream();
         mUpstreamLinkProperties = null;
         mHwInterface.stopOffloadControl();
-        mControlInitialized = false;
+        mControlHalVersion = OFFLOAD_HAL_VERSION_NONE;
         mConfigInitialized = false;
         if (mHandler.hasCallbacks(mScheduledPollingTask)) {
             mHandler.removeCallbacks(mScheduledPollingTask);
@@ -287,7 +335,7 @@
     }
 
     private boolean started() {
-        return mConfigInitialized && mControlInitialized;
+        return mConfigInitialized && mControlHalVersion != OFFLOAD_HAL_VERSION_NONE;
     }
 
     @VisibleForTesting
@@ -320,24 +368,35 @@
 
         @Override
         public void onSetLimit(String iface, long quotaBytes) {
+            onSetWarningAndLimit(iface, QUOTA_UNLIMITED, quotaBytes);
+        }
+
+        @Override
+        public void onSetWarningAndLimit(@NonNull String iface,
+                long warningBytes, long limitBytes) {
             // Listen for all iface is necessary since upstream might be changed after limit
             // is set.
             mHandler.post(() -> {
-                final Long curIfaceQuota = mInterfaceQuotas.get(iface);
+                final InterfaceQuota curIfaceQuota = mInterfaceQuotas.get(iface);
+                final InterfaceQuota newIfaceQuota = new InterfaceQuota(
+                        warningBytes == QUOTA_UNLIMITED ? Long.MAX_VALUE : warningBytes,
+                        limitBytes == QUOTA_UNLIMITED ? Long.MAX_VALUE : limitBytes);
 
                 // If the quota is set to unlimited, the value set to HAL is Long.MAX_VALUE,
                 // which is ~8.4 x 10^6 TiB, no one can actually reach it. Thus, it is not
                 // useful to set it multiple times.
                 // Otherwise, the quota needs to be updated to tell HAL to re-count from now even
                 // if the quota is the same as the existing one.
-                if (null == curIfaceQuota && QUOTA_UNLIMITED == quotaBytes) return;
+                if (null == curIfaceQuota && InterfaceQuota.MAX_VALUE.equals(newIfaceQuota)) {
+                    return;
+                }
 
-                if (quotaBytes == QUOTA_UNLIMITED) {
+                if (InterfaceQuota.MAX_VALUE.equals(newIfaceQuota)) {
                     mInterfaceQuotas.remove(iface);
                 } else {
-                    mInterfaceQuotas.put(iface, quotaBytes);
+                    mInterfaceQuotas.put(iface, newIfaceQuota);
                 }
-                maybeUpdateDataLimit(iface);
+                maybeUpdateDataWarningAndLimit(iface);
             });
         }
 
@@ -372,7 +431,11 @@
 
         @Override
         public void onSetAlert(long quotaBytes) {
-            // TODO: Ask offload HAL to notify alert without stopping traffic.
+            // Ignore set alert calls from HAL V1.1 since the hardware supports set warning now.
+            // Thus, the software polling mechanism is not needed.
+            if (!useStatsPolling()) {
+                return;
+            }
             // Post it to handler thread since it access remaining quota bytes.
             mHandler.post(() -> {
                 updateAlertQuota(quotaBytes);
@@ -457,24 +520,32 @@
 
     private boolean isPollingStatsNeeded() {
         return started() && mRemainingAlertQuota > 0
+                && useStatsPolling()
                 && !TextUtils.isEmpty(currentUpstreamInterface())
                 && mDeps.getTetherConfig() != null
                 && mDeps.getTetherConfig().getOffloadPollInterval()
                 >= DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS;
     }
 
-    private boolean maybeUpdateDataLimit(String iface) {
-        // setDataLimit may only be called while offload is occurring on this upstream.
+    private boolean useStatsPolling() {
+        return mControlHalVersion == OFFLOAD_HAL_VERSION_1_0;
+    }
+
+    private boolean maybeUpdateDataWarningAndLimit(String iface) {
+        // setDataLimit or setDataWarningAndLimit may only be called while offload is occurring
+        // on this upstream.
         if (!started() || !TextUtils.equals(iface, currentUpstreamInterface())) {
             return true;
         }
 
-        Long limit = mInterfaceQuotas.get(iface);
-        if (limit == null) {
-            limit = Long.MAX_VALUE;
+        final InterfaceQuota quota = mInterfaceQuotas.getOrDefault(iface, InterfaceQuota.MAX_VALUE);
+        final boolean ret;
+        if (mControlHalVersion >= OFFLOAD_HAL_VERSION_1_1) {
+            ret = mHwInterface.setDataWarningAndLimit(iface, quota.warningBytes, quota.limitBytes);
+        } else {
+            ret = mHwInterface.setDataLimit(iface, quota.limitBytes);
         }
-
-        return mHwInterface.setDataLimit(iface, limit);
+        return ret;
     }
 
     private void updateStatsForCurrentUpstream() {
@@ -628,7 +699,7 @@
         maybeUpdateStats(prevUpstream);
 
         // Data limits can only be set once offload is running on the upstream.
-        success = maybeUpdateDataLimit(iface);
+        success = maybeUpdateDataWarningAndLimit(iface);
         if (!success) {
             // If we failed to set a data limit, don't use this upstream, because we don't want to
             // blow through the data limit that we were told to apply.
@@ -696,6 +767,8 @@
         }
         final boolean isStarted = started();
         pw.println("Offload HALs " + (isStarted ? "started" : "not started"));
+        pw.println("Offload Control HAL version: "
+                + OffloadHardwareInterface.halVerToString(mControlHalVersion));
         LinkProperties lp = mUpstreamLinkProperties;
         String upstream = (lp != null) ? lp.getInterfaceName() : null;
         pw.println("Current upstream: " + upstream);
diff --git a/Tethering/src/com/android/networkstack/tethering/OffloadHardwareInterface.java b/Tethering/src/com/android/networkstack/tethering/OffloadHardwareInterface.java
index da5f25b..e3ac660 100644
--- a/Tethering/src/com/android/networkstack/tethering/OffloadHardwareInterface.java
+++ b/Tethering/src/com/android/networkstack/tethering/OffloadHardwareInterface.java
@@ -20,13 +20,14 @@
 import static android.net.netlink.StructNlMsgHdr.NLM_F_REQUEST;
 import static android.net.util.TetheringUtils.uint16;
 
+import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.hardware.tetheroffload.config.V1_0.IOffloadConfig;
 import android.hardware.tetheroffload.control.V1_0.IOffloadControl;
-import android.hardware.tetheroffload.control.V1_0.ITetheringOffloadCallback;
 import android.hardware.tetheroffload.control.V1_0.NatTimeoutUpdate;
 import android.hardware.tetheroffload.control.V1_0.NetworkProtocol;
 import android.hardware.tetheroffload.control.V1_0.OffloadCallbackEvent;
+import android.hardware.tetheroffload.control.V1_1.ITetheringOffloadCallback;
 import android.net.netlink.NetlinkSocket;
 import android.net.netlink.StructNfGenMsg;
 import android.net.netlink.StructNlMsgHdr;
@@ -38,12 +39,16 @@
 import android.system.ErrnoException;
 import android.system.Os;
 import android.system.OsConstants;
+import android.util.Log;
+import android.util.Pair;
 
 import com.android.internal.annotations.VisibleForTesting;
 
 import java.io.FileDescriptor;
 import java.io.IOException;
 import java.io.InterruptedIOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
 import java.net.SocketAddress;
 import java.net.SocketException;
 import java.nio.ByteBuffer;
@@ -82,6 +87,37 @@
     private final SharedLog mLog;
     private final Dependencies mDeps;
     private IOffloadControl mOffloadControl;
+
+    // TODO: Use major-minor version control to prevent from defining new constants.
+    static final int OFFLOAD_HAL_VERSION_NONE = 0;
+    static final int OFFLOAD_HAL_VERSION_1_0 = 1;
+    static final int OFFLOAD_HAL_VERSION_1_1 = 2;
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(prefix = "OFFLOAD_HAL_VERSION_", value = {
+            OFFLOAD_HAL_VERSION_NONE,
+            OFFLOAD_HAL_VERSION_1_0,
+            OFFLOAD_HAL_VERSION_1_1
+    })
+    public @interface OffloadHalVersion {}
+    @OffloadHalVersion
+    private int mOffloadControlVersion = OFFLOAD_HAL_VERSION_NONE;
+
+    @NonNull
+    static String halVerToString(int version) {
+        switch(version) {
+            case OFFLOAD_HAL_VERSION_1_0:
+                return "1.0";
+            case OFFLOAD_HAL_VERSION_1_1:
+                return "1.1";
+            case OFFLOAD_HAL_VERSION_NONE:
+                return "None";
+            default:
+                throw new IllegalArgumentException("Unsupported version int " + version);
+        }
+
+    }
+
     private TetheringOffloadCallback mTetheringOffloadCallback;
     private ControlCallback mControlCallback;
 
@@ -105,6 +141,8 @@
         public void onSupportAvailable() {}
         /** Offload stopped because of usage limit reached. */
         public void onStoppedLimitReached() {}
+        /** Indicate that data warning quota is reached. */
+        public void onWarningReached() {}
 
         /** Indicate to update NAT timeout. */
         public void onNatTimeoutUpdate(int proto,
@@ -167,13 +205,30 @@
             }
         }
 
-        public IOffloadControl getOffloadControl() {
+        @NonNull
+        public Pair<IOffloadControl, Integer> getOffloadControl() {
+            IOffloadControl hal = null;
+            int version = OFFLOAD_HAL_VERSION_NONE;
             try {
-                return IOffloadControl.getService(true /*retry*/);
-            } catch (RemoteException | NoSuchElementException e) {
-                mLog.e("tethering offload control not supported: " + e);
-                return null;
+                hal = android.hardware.tetheroffload.control
+                        .V1_1.IOffloadControl.getService(true /*retry*/);
+                version = OFFLOAD_HAL_VERSION_1_1;
+            } catch (NoSuchElementException e) {
+                // Unsupported by device.
+            } catch (RemoteException e) {
+                mLog.e("Unable to get offload control " + OFFLOAD_HAL_VERSION_1_1);
             }
+            if (hal == null) {
+                try {
+                    hal = IOffloadControl.getService(true /*retry*/);
+                    version = OFFLOAD_HAL_VERSION_1_0;
+                } catch (NoSuchElementException e) {
+                    // Unsupported by device.
+                } catch (RemoteException e) {
+                    mLog.e("Unable to get offload control " + OFFLOAD_HAL_VERSION_1_0);
+                }
+            }
+            return new Pair<IOffloadControl, Integer>(hal, version);
         }
 
         public NativeHandle createConntrackSocket(final int groups) {
@@ -304,23 +359,33 @@
         }
     }
 
-    /** Initialize the tethering offload HAL. */
-    public boolean initOffloadControl(ControlCallback controlCb) {
+    /**
+     * Initialize the tethering offload HAL.
+     *
+     * @return one of {@code OFFLOAD_HAL_VERSION_*} represents the HAL version, or
+     *         {@link #OFFLOAD_HAL_VERSION_NONE} if failed.
+     */
+    public int initOffloadControl(ControlCallback controlCb) {
         mControlCallback = controlCb;
 
         if (mOffloadControl == null) {
-            mOffloadControl = mDeps.getOffloadControl();
+            final Pair<IOffloadControl, Integer> halAndVersion = mDeps.getOffloadControl();
+            mOffloadControl = halAndVersion.first;
+            mOffloadControlVersion = halAndVersion.second;
             if (mOffloadControl == null) {
                 mLog.e("tethering IOffloadControl.getService() returned null");
-                return false;
+                return OFFLOAD_HAL_VERSION_NONE;
             }
+            mLog.i("tethering offload control version "
+                    + halVerToString(mOffloadControlVersion) + " is supported.");
         }
 
         final String logmsg = String.format("initOffloadControl(%s)",
                 (controlCb == null) ? "null"
                         : "0x" + Integer.toHexString(System.identityHashCode(controlCb)));
 
-        mTetheringOffloadCallback = new TetheringOffloadCallback(mHandler, mControlCallback, mLog);
+        mTetheringOffloadCallback = new TetheringOffloadCallback(
+                mHandler, mControlCallback, mLog, mOffloadControlVersion);
         final CbResults results = new CbResults();
         try {
             mOffloadControl.initOffload(
@@ -331,11 +396,11 @@
                     });
         } catch (RemoteException e) {
             record(logmsg, e);
-            return false;
+            return OFFLOAD_HAL_VERSION_NONE;
         }
 
         record(logmsg, results);
-        return results.mSuccess;
+        return results.mSuccess ? mOffloadControlVersion : OFFLOAD_HAL_VERSION_NONE;
     }
 
     /** Stop IOffloadControl. */
@@ -419,6 +484,33 @@
         return results.mSuccess;
     }
 
+    /** Set data warning and limit value to offload management process. */
+    public boolean setDataWarningAndLimit(String iface, long warning, long limit) {
+        if (mOffloadControlVersion < OFFLOAD_HAL_VERSION_1_1) {
+            throw new IllegalArgumentException(
+                    "setDataWarningAndLimit is not supported below HAL V1.1");
+        }
+        final String logmsg =
+                String.format("setDataWarningAndLimit(%s, %d, %d)", iface, warning, limit);
+
+        final CbResults results = new CbResults();
+        try {
+            ((android.hardware.tetheroffload.control.V1_1.IOffloadControl) mOffloadControl)
+                    .setDataWarningAndLimit(
+                            iface, warning, limit,
+                            (boolean success, String errMsg) -> {
+                                results.mSuccess = success;
+                                results.mErrMsg = errMsg;
+                            });
+        } catch (RemoteException e) {
+            record(logmsg, e);
+            return false;
+        }
+
+        record(logmsg, results);
+        return results.mSuccess;
+    }
+
     /** Set upstream parameters to offload management process. */
     public boolean setUpstreamParameters(
             String iface, String v4addr, String v4gateway, ArrayList<String> v6gws) {
@@ -504,35 +596,64 @@
         public final Handler handler;
         public final ControlCallback controlCb;
         public final SharedLog log;
+        private final int mOffloadControlVersion;
 
-        TetheringOffloadCallback(Handler h, ControlCallback cb, SharedLog sharedLog) {
+        TetheringOffloadCallback(
+                Handler h, ControlCallback cb, SharedLog sharedLog, int offloadControlVersion) {
             handler = h;
             controlCb = cb;
             log = sharedLog;
+            this.mOffloadControlVersion = offloadControlVersion;
+        }
+
+        private void handleOnEvent(int event) {
+            switch (event) {
+                case OffloadCallbackEvent.OFFLOAD_STARTED:
+                    controlCb.onStarted();
+                    break;
+                case OffloadCallbackEvent.OFFLOAD_STOPPED_ERROR:
+                    controlCb.onStoppedError();
+                    break;
+                case OffloadCallbackEvent.OFFLOAD_STOPPED_UNSUPPORTED:
+                    controlCb.onStoppedUnsupported();
+                    break;
+                case OffloadCallbackEvent.OFFLOAD_SUPPORT_AVAILABLE:
+                    controlCb.onSupportAvailable();
+                    break;
+                case OffloadCallbackEvent.OFFLOAD_STOPPED_LIMIT_REACHED:
+                    controlCb.onStoppedLimitReached();
+                    break;
+                case android.hardware.tetheroffload.control
+                        .V1_1.OffloadCallbackEvent.OFFLOAD_WARNING_REACHED:
+                    controlCb.onWarningReached();
+                    break;
+                default:
+                    log.e("Unsupported OffloadCallbackEvent: " + event);
+            }
         }
 
         @Override
         public void onEvent(int event) {
+            // The implementation should never call onEvent()) if the event is already reported
+            // through newer callback.
+            if (mOffloadControlVersion > OFFLOAD_HAL_VERSION_1_0) {
+                Log.wtf(TAG, "onEvent(" + event + ") fired on HAL "
+                        + halVerToString(mOffloadControlVersion));
+            }
             handler.post(() -> {
-                switch (event) {
-                    case OffloadCallbackEvent.OFFLOAD_STARTED:
-                        controlCb.onStarted();
-                        break;
-                    case OffloadCallbackEvent.OFFLOAD_STOPPED_ERROR:
-                        controlCb.onStoppedError();
-                        break;
-                    case OffloadCallbackEvent.OFFLOAD_STOPPED_UNSUPPORTED:
-                        controlCb.onStoppedUnsupported();
-                        break;
-                    case OffloadCallbackEvent.OFFLOAD_SUPPORT_AVAILABLE:
-                        controlCb.onSupportAvailable();
-                        break;
-                    case OffloadCallbackEvent.OFFLOAD_STOPPED_LIMIT_REACHED:
-                        controlCb.onStoppedLimitReached();
-                        break;
-                    default:
-                        log.e("Unsupported OffloadCallbackEvent: " + event);
-                }
+                handleOnEvent(event);
+            });
+        }
+
+        @Override
+        public void onEvent_1_1(int event) {
+            if (mOffloadControlVersion < OFFLOAD_HAL_VERSION_1_1) {
+                Log.wtf(TAG, "onEvent_1_1(" + event + ") fired on HAL "
+                        + halVerToString(mOffloadControlVersion));
+                return;
+            }
+            handler.post(() -> {
+                handleOnEvent(event);
             });
         }
 
diff --git a/Tethering/src/com/android/networkstack/tethering/TetheringService.java b/Tethering/src/com/android/networkstack/tethering/TetheringService.java
index 723b1db..722ec8f 100644
--- a/Tethering/src/com/android/networkstack/tethering/TetheringService.java
+++ b/Tethering/src/com/android/networkstack/tethering/TetheringService.java
@@ -46,13 +46,14 @@
 import android.os.Looper;
 import android.os.RemoteException;
 import android.os.ResultReceiver;
-import android.provider.Settings;
 import android.util.Log;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.networkstack.apishim.SettingsShimImpl;
+import com.android.networkstack.apishim.common.SettingsShim;
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
@@ -66,6 +67,7 @@
     private static final String TAG = TetheringService.class.getSimpleName();
 
     private TetheringConnector mConnector;
+    private SettingsShim mSettingsShim;
 
     @Override
     public void onCreate() {
@@ -73,6 +75,8 @@
         // The Tethering object needs a fully functional context to start, so this can't be done
         // in the constructor.
         mConnector = new TetheringConnector(makeTethering(deps), TetheringService.this);
+
+        mSettingsShim = SettingsShimImpl.newInstance();
     }
 
     /**
@@ -294,10 +298,8 @@
     boolean checkAndNoteWriteSettingsOperation(@NonNull Context context, int uid,
             @NonNull String callingPackage, @Nullable String callingAttributionTag,
             boolean throwException) {
-        // TODO: on S and above, pass the attribution tag to Settings instead of throwing it away.
-        // This will likely require a SettingsShim class.
-        return Settings.checkAndNoteWriteSettingsOperation(context, uid, callingPackage,
-                throwException);
+        return mSettingsShim.checkAndNoteWriteSettingsOperation(context, uid, callingPackage,
+                callingAttributionTag, throwException);
     }
 
     /**
diff --git a/Tethering/tests/mts/Android.bp b/Tethering/tests/mts/Android.bp
index 221771f..e51d531 100644
--- a/Tethering/tests/mts/Android.bp
+++ b/Tethering/tests/mts/Android.bp
@@ -50,6 +50,8 @@
         "libstaticjvmtiagent",
     ],
 
+    defaults: ["framework-connectivity-test-defaults"],
+
     platform_apis: true,
 
     // Tag this module as a mts test artifact
diff --git a/Tethering/tests/unit/Android.bp b/Tethering/tests/unit/Android.bp
index b4b3977..c6f19d7 100644
--- a/Tethering/tests/unit/Android.bp
+++ b/Tethering/tests/unit/Android.bp
@@ -33,6 +33,7 @@
     sdk_version: "core_platform",
     libs: [
         "framework-minus-apex",
+        "framework-connectivity.impl",
         "framework-tethering.impl",
     ],
     visibility: [
@@ -66,6 +67,7 @@
         "ext",
         "framework-minus-apex",
         "framework-res",
+        "framework-connectivity.impl",
         "framework-tethering.impl",
         "framework-wifi.stubs.module_lib",
     ],
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/OffloadControllerTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/OffloadControllerTest.java
index 9bd82f9..d800816 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/OffloadControllerTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/OffloadControllerTest.java
@@ -29,6 +29,8 @@
 import static com.android.networkstack.tethering.OffloadController.StatsType.STATS_PER_IFACE;
 import static com.android.networkstack.tethering.OffloadController.StatsType.STATS_PER_UID;
 import static com.android.networkstack.tethering.OffloadHardwareInterface.ForwardedStats;
+import static com.android.networkstack.tethering.OffloadHardwareInterface.OFFLOAD_HAL_VERSION_1_0;
+import static com.android.networkstack.tethering.OffloadHardwareInterface.OFFLOAD_HAL_VERSION_1_1;
 import static com.android.networkstack.tethering.TetheringConfiguration.DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS;
 import static com.android.testutils.MiscAsserts.assertContainsAll;
 import static com.android.testutils.MiscAsserts.assertThrows;
@@ -56,7 +58,6 @@
 import android.app.usage.NetworkStatsManager;
 import android.content.Context;
 import android.content.pm.ApplicationInfo;
-import android.net.ITetheringStatsProvider;
 import android.net.IpPrefix;
 import android.net.LinkAddress;
 import android.net.LinkProperties;
@@ -141,13 +142,14 @@
         FakeSettingsProvider.clearSettingsProvider();
     }
 
-    private void setupFunctioningHardwareInterface() {
+    private void setupFunctioningHardwareInterface(int controlVersion) {
         when(mHardware.initOffloadConfig()).thenReturn(true);
         when(mHardware.initOffloadControl(mControlCallbackCaptor.capture()))
-                .thenReturn(true);
+                .thenReturn(controlVersion);
         when(mHardware.setUpstreamParameters(anyString(), any(), any(), any())).thenReturn(true);
         when(mHardware.getForwardedStats(any())).thenReturn(new ForwardedStats());
         when(mHardware.setDataLimit(anyString(), anyLong())).thenReturn(true);
+        when(mHardware.setDataWarningAndLimit(anyString(), anyLong(), anyLong())).thenReturn(true);
     }
 
     private void enableOffload() {
@@ -170,6 +172,7 @@
                 ArgumentCaptor.forClass(OffloadController.OffloadTetheringStatsProvider.class);
         verify(mStatsManager).registerNetworkStatsProvider(anyString(),
                 tetherStatsProviderCaptor.capture());
+        reset(mStatsManager);
         mTetherStatsProvider = tetherStatsProviderCaptor.getValue();
         assertNotNull(mTetherStatsProvider);
         mTetherStatsProviderCb = new TestableNetworkStatsProviderCbBinder();
@@ -177,10 +180,18 @@
         return offload;
     }
 
+    @Test
+    public void testStartStop() throws Exception {
+        stopOffloadController(
+                startOffloadController(OFFLOAD_HAL_VERSION_1_0, true /*expectStart*/));
+        stopOffloadController(
+                startOffloadController(OFFLOAD_HAL_VERSION_1_1, true /*expectStart*/));
+    }
+
     @NonNull
-    private OffloadController startOffloadController(boolean expectStart)
+    private OffloadController startOffloadController(int controlVersion, boolean expectStart)
             throws Exception {
-        setupFunctioningHardwareInterface();
+        setupFunctioningHardwareInterface(controlVersion);
         final OffloadController offload = makeOffloadController();
         offload.start();
 
@@ -208,7 +219,7 @@
         when(mHardware.getDefaultTetherOffloadDisabled()).thenReturn(1);
         assertThrows(SettingNotFoundException.class, () ->
                 Settings.Global.getInt(mContentResolver, TETHER_OFFLOAD_DISABLED));
-        startOffloadController(false /*expectStart*/);
+        startOffloadController(OFFLOAD_HAL_VERSION_1_0, false /*expectStart*/);
     }
 
     @Test
@@ -216,26 +227,26 @@
         when(mHardware.getDefaultTetherOffloadDisabled()).thenReturn(0);
         assertThrows(SettingNotFoundException.class, () ->
                 Settings.Global.getInt(mContentResolver, TETHER_OFFLOAD_DISABLED));
-        startOffloadController(true /*expectStart*/);
+        startOffloadController(OFFLOAD_HAL_VERSION_1_0, true /*expectStart*/);
     }
 
     @Test
     public void testSettingsAllowsStart() throws Exception {
         Settings.Global.putInt(mContentResolver, TETHER_OFFLOAD_DISABLED, 0);
-        startOffloadController(true /*expectStart*/);
+        startOffloadController(OFFLOAD_HAL_VERSION_1_0, true /*expectStart*/);
     }
 
     @Test
     public void testSettingsDisablesStart() throws Exception {
         Settings.Global.putInt(mContentResolver, TETHER_OFFLOAD_DISABLED, 1);
-        startOffloadController(false /*expectStart*/);
+        startOffloadController(OFFLOAD_HAL_VERSION_1_0, false /*expectStart*/);
     }
 
     @Test
     public void testSetUpstreamLinkPropertiesWorking() throws Exception {
         enableOffload();
         final OffloadController offload =
-                startOffloadController(true /*expectStart*/);
+                startOffloadController(OFFLOAD_HAL_VERSION_1_0, true /*expectStart*/);
 
         // In reality, the UpstreamNetworkMonitor would have passed down to us
         // a covering set of local prefixes representing a minimum essential
@@ -406,7 +417,7 @@
     public void testGetForwardedStats() throws Exception {
         enableOffload();
         final OffloadController offload =
-                startOffloadController(true /*expectStart*/);
+                startOffloadController(OFFLOAD_HAL_VERSION_1_0, true /*expectStart*/);
 
         final String ethernetIface = "eth1";
         final String mobileIface = "rmnet_data0";
@@ -492,84 +503,174 @@
                 expectedUidStatsDiff);
     }
 
+    /**
+     * Test OffloadController with different combinations of HAL and framework versions can set
+     * data warning and/or limit correctly.
+     */
     @Test
-    public void testSetInterfaceQuota() throws Exception {
+    public void testSetDataWarningAndLimit() throws Exception {
+        // Verify the OffloadController is called by R framework, where the framework doesn't send
+        // warning.
+        checkSetDataWarningAndLimit(false, OFFLOAD_HAL_VERSION_1_0);
+        checkSetDataWarningAndLimit(false, OFFLOAD_HAL_VERSION_1_1);
+        // Verify the OffloadController is called by S+ framework, where the framework sends
+        // warning along with limit.
+        checkSetDataWarningAndLimit(true, OFFLOAD_HAL_VERSION_1_0);
+        checkSetDataWarningAndLimit(true, OFFLOAD_HAL_VERSION_1_1);
+    }
+
+    private void checkSetDataWarningAndLimit(boolean isProviderSetWarning, int controlVersion)
+            throws Exception {
         enableOffload();
         final OffloadController offload =
-                startOffloadController(true /*expectStart*/);
+                startOffloadController(controlVersion, true /*expectStart*/);
 
         final String ethernetIface = "eth1";
         final String mobileIface = "rmnet_data0";
         final long ethernetLimit = 12345;
+        final long mobileWarning = 123456;
         final long mobileLimit = 12345678;
 
         final LinkProperties lp = new LinkProperties();
         lp.setInterfaceName(ethernetIface);
-        offload.setUpstreamLinkProperties(lp);
 
         final InOrder inOrder = inOrder(mHardware);
-        when(mHardware.setUpstreamParameters(any(), any(), any(), any())).thenReturn(true);
+        when(mHardware.setUpstreamParameters(
+                any(), any(), any(), any())).thenReturn(true);
         when(mHardware.setDataLimit(anyString(), anyLong())).thenReturn(true);
+        when(mHardware.setDataWarningAndLimit(anyString(), anyLong(), anyLong())).thenReturn(true);
+        offload.setUpstreamLinkProperties(lp);
+        // Applying an interface sends the initial quota to the hardware.
+        if (controlVersion >= OFFLOAD_HAL_VERSION_1_1) {
+            inOrder.verify(mHardware).setDataWarningAndLimit(ethernetIface, Long.MAX_VALUE,
+                    Long.MAX_VALUE);
+        } else {
+            inOrder.verify(mHardware).setDataLimit(ethernetIface, Long.MAX_VALUE);
+        }
+        inOrder.verifyNoMoreInteractions();
+
+        // Verify that set to unlimited again won't cause duplicated calls to the hardware.
+        if (isProviderSetWarning) {
+            mTetherStatsProvider.onSetWarningAndLimit(ethernetIface,
+                    NetworkStatsProvider.QUOTA_UNLIMITED, NetworkStatsProvider.QUOTA_UNLIMITED);
+        } else {
+            mTetherStatsProvider.onSetLimit(ethernetIface, NetworkStatsProvider.QUOTA_UNLIMITED);
+        }
+        waitForIdle();
+        inOrder.verifyNoMoreInteractions();
 
         // Applying an interface quota to the current upstream immediately sends it to the hardware.
-        mTetherStatsProvider.onSetLimit(ethernetIface, ethernetLimit);
+        if (isProviderSetWarning) {
+            mTetherStatsProvider.onSetWarningAndLimit(ethernetIface,
+                    NetworkStatsProvider.QUOTA_UNLIMITED, ethernetLimit);
+        } else {
+            mTetherStatsProvider.onSetLimit(ethernetIface, ethernetLimit);
+        }
         waitForIdle();
-        inOrder.verify(mHardware).setDataLimit(ethernetIface, ethernetLimit);
+        if (controlVersion >= OFFLOAD_HAL_VERSION_1_1) {
+            inOrder.verify(mHardware).setDataWarningAndLimit(ethernetIface, Long.MAX_VALUE,
+                    ethernetLimit);
+        } else {
+            inOrder.verify(mHardware).setDataLimit(ethernetIface, ethernetLimit);
+        }
         inOrder.verifyNoMoreInteractions();
 
         // Applying an interface quota to another upstream does not take any immediate action.
-        mTetherStatsProvider.onSetLimit(mobileIface, mobileLimit);
+        if (isProviderSetWarning) {
+            mTetherStatsProvider.onSetWarningAndLimit(mobileIface, mobileWarning, mobileLimit);
+        } else {
+            mTetherStatsProvider.onSetLimit(mobileIface, mobileLimit);
+        }
         waitForIdle();
-        inOrder.verify(mHardware, never()).setDataLimit(anyString(), anyLong());
+        if (controlVersion >= OFFLOAD_HAL_VERSION_1_1) {
+            inOrder.verify(mHardware, never()).setDataWarningAndLimit(anyString(), anyLong(),
+                    anyLong());
+        } else {
+            inOrder.verify(mHardware, never()).setDataLimit(anyString(), anyLong());
+        }
 
         // Switching to that upstream causes the quota to be applied if the parameters were applied
         // correctly.
         lp.setInterfaceName(mobileIface);
         offload.setUpstreamLinkProperties(lp);
         waitForIdle();
-        inOrder.verify(mHardware).setDataLimit(mobileIface, mobileLimit);
+        if (controlVersion >= OFFLOAD_HAL_VERSION_1_1) {
+            inOrder.verify(mHardware).setDataWarningAndLimit(mobileIface,
+                    isProviderSetWarning ? mobileWarning : Long.MAX_VALUE,
+                    mobileLimit);
+        } else {
+            inOrder.verify(mHardware).setDataLimit(mobileIface, mobileLimit);
+        }
 
-        // Setting a limit of ITetheringStatsProvider.QUOTA_UNLIMITED causes the limit to be set
+        // Setting a limit of NetworkStatsProvider.QUOTA_UNLIMITED causes the limit to be set
         // to Long.MAX_VALUE.
-        mTetherStatsProvider.onSetLimit(mobileIface, ITetheringStatsProvider.QUOTA_UNLIMITED);
+        if (isProviderSetWarning) {
+            mTetherStatsProvider.onSetWarningAndLimit(mobileIface,
+                    NetworkStatsProvider.QUOTA_UNLIMITED, NetworkStatsProvider.QUOTA_UNLIMITED);
+        } else {
+            mTetherStatsProvider.onSetLimit(mobileIface, NetworkStatsProvider.QUOTA_UNLIMITED);
+        }
         waitForIdle();
-        inOrder.verify(mHardware).setDataLimit(mobileIface, Long.MAX_VALUE);
+        if (controlVersion >= OFFLOAD_HAL_VERSION_1_1) {
+            inOrder.verify(mHardware).setDataWarningAndLimit(mobileIface, Long.MAX_VALUE,
+                    Long.MAX_VALUE);
+        } else {
+            inOrder.verify(mHardware).setDataLimit(mobileIface, Long.MAX_VALUE);
+        }
 
-        // If setting upstream parameters fails, then the data limit is not set.
+        // If setting upstream parameters fails, then the data warning and limit is not set.
         when(mHardware.setUpstreamParameters(any(), any(), any(), any())).thenReturn(false);
         lp.setInterfaceName(ethernetIface);
         offload.setUpstreamLinkProperties(lp);
-        mTetherStatsProvider.onSetLimit(mobileIface, mobileLimit);
+        if (isProviderSetWarning) {
+            mTetherStatsProvider.onSetWarningAndLimit(mobileIface, mobileWarning, mobileLimit);
+        } else {
+            mTetherStatsProvider.onSetLimit(mobileIface, mobileLimit);
+        }
         waitForIdle();
         inOrder.verify(mHardware, never()).setDataLimit(anyString(), anyLong());
+        inOrder.verify(mHardware, never()).setDataWarningAndLimit(anyString(), anyLong(),
+                anyLong());
 
-        // If setting the data limit fails while changing upstreams, offload is stopped.
+        // If setting the data warning and/or limit fails while changing upstreams, offload is
+        // stopped.
         when(mHardware.setUpstreamParameters(any(), any(), any(), any())).thenReturn(true);
         when(mHardware.setDataLimit(anyString(), anyLong())).thenReturn(false);
+        when(mHardware.setDataWarningAndLimit(anyString(), anyLong(), anyLong())).thenReturn(false);
         lp.setInterfaceName(mobileIface);
         offload.setUpstreamLinkProperties(lp);
-        mTetherStatsProvider.onSetLimit(mobileIface, mobileLimit);
+        if (isProviderSetWarning) {
+            mTetherStatsProvider.onSetWarningAndLimit(mobileIface, mobileWarning, mobileLimit);
+        } else {
+            mTetherStatsProvider.onSetLimit(mobileIface, mobileLimit);
+        }
         waitForIdle();
         inOrder.verify(mHardware).getForwardedStats(ethernetIface);
         inOrder.verify(mHardware).stopOffloadControl();
     }
 
     @Test
-    public void testDataLimitCallback() throws Exception {
+    public void testDataWarningAndLimitCallback() throws Exception {
         enableOffload();
-        final OffloadController offload =
-                startOffloadController(true /*expectStart*/);
+        startOffloadController(OFFLOAD_HAL_VERSION_1_0, true /*expectStart*/);
 
         OffloadHardwareInterface.ControlCallback callback = mControlCallbackCaptor.getValue();
         callback.onStoppedLimitReached();
         mTetherStatsProviderCb.expectNotifyStatsUpdated();
+        mTetherStatsProviderCb.expectNotifyWarningOrLimitReached();
+
+        startOffloadController(OFFLOAD_HAL_VERSION_1_1, true /*expectStart*/);
+        callback = mControlCallbackCaptor.getValue();
+        callback.onWarningReached();
+        mTetherStatsProviderCb.expectNotifyStatsUpdated();
+        mTetherStatsProviderCb.expectNotifyWarningOrLimitReached();
     }
 
     @Test
     public void testAddRemoveDownstreams() throws Exception {
         enableOffload();
         final OffloadController offload =
-                startOffloadController(true /*expectStart*/);
+                startOffloadController(OFFLOAD_HAL_VERSION_1_0, true /*expectStart*/);
         final InOrder inOrder = inOrder(mHardware);
 
         // Tethering makes several calls to setLocalPrefixes() before add/remove
@@ -636,7 +737,7 @@
     public void testControlCallbackOnStoppedUnsupportedFetchesAllStats() throws Exception {
         enableOffload();
         final OffloadController offload =
-                startOffloadController(true /*expectStart*/);
+                startOffloadController(OFFLOAD_HAL_VERSION_1_0, true /*expectStart*/);
 
         // Pretend to set a few different upstreams (only the interface name
         // matters for this test; we're ignoring IP and route information).
@@ -667,7 +768,7 @@
             throws Exception {
         enableOffload();
         final OffloadController offload =
-                startOffloadController(true /*expectStart*/);
+                startOffloadController(OFFLOAD_HAL_VERSION_1_0, true /*expectStart*/);
 
         // Pretend to set a few different upstreams (only the interface name
         // matters for this test; we're ignoring IP and route information).
@@ -745,14 +846,12 @@
         enableOffload();
         setOffloadPollInterval(DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS);
         final OffloadController offload =
-                startOffloadController(true /*expectStart*/);
+                startOffloadController(OFFLOAD_HAL_VERSION_1_0, true /*expectStart*/);
 
         // Initialize with fake eth upstream.
         final String ethernetIface = "eth1";
         InOrder inOrder = inOrder(mHardware);
-        final LinkProperties lp = new LinkProperties();
-        lp.setInterfaceName(ethernetIface);
-        offload.setUpstreamLinkProperties(lp);
+        offload.setUpstreamLinkProperties(makeEthernetLinkProperties());
         // Previous upstream was null, so no stats are fetched.
         inOrder.verify(mHardware, never()).getForwardedStats(any());
 
@@ -785,4 +884,33 @@
         mTetherStatsProviderCb.assertNoCallback();
         verify(mHardware, never()).getForwardedStats(any());
     }
+
+    private static LinkProperties makeEthernetLinkProperties() {
+        final String ethernetIface = "eth1";
+        final LinkProperties lp = new LinkProperties();
+        lp.setInterfaceName(ethernetIface);
+        return lp;
+    }
+
+    private void checkSoftwarePollingUsed(int controlVersion) throws Exception {
+        enableOffload();
+        setOffloadPollInterval(DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS);
+        OffloadController offload =
+                startOffloadController(controlVersion, true /*expectStart*/);
+        offload.setUpstreamLinkProperties(makeEthernetLinkProperties());
+        mTetherStatsProvider.onSetAlert(0);
+        waitForIdle();
+        if (controlVersion >= OFFLOAD_HAL_VERSION_1_1) {
+            mTetherStatsProviderCb.assertNoCallback();
+        } else {
+            mTetherStatsProviderCb.expectNotifyAlertReached();
+        }
+        verify(mHardware, never()).getForwardedStats(any());
+    }
+
+    @Test
+    public void testSoftwarePollingUsed() throws Exception {
+        checkSoftwarePollingUsed(OFFLOAD_HAL_VERSION_1_0);
+        checkSoftwarePollingUsed(OFFLOAD_HAL_VERSION_1_1);
+    }
 }
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/OffloadHardwareInterfaceTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/OffloadHardwareInterfaceTest.java
index 38b19dd..a8b3b92 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/OffloadHardwareInterfaceTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/OffloadHardwareInterfaceTest.java
@@ -21,21 +21,28 @@
 import static android.system.OsConstants.AF_UNIX;
 import static android.system.OsConstants.SOCK_STREAM;
 
+import static com.android.networkstack.tethering.OffloadHardwareInterface.OFFLOAD_HAL_VERSION_1_0;
+import static com.android.networkstack.tethering.OffloadHardwareInterface.OFFLOAD_HAL_VERSION_1_1;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.fail;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 import android.hardware.tetheroffload.config.V1_0.IOffloadConfig;
 import android.hardware.tetheroffload.control.V1_0.IOffloadControl;
-import android.hardware.tetheroffload.control.V1_0.ITetheringOffloadCallback;
 import android.hardware.tetheroffload.control.V1_0.NatTimeoutUpdate;
 import android.hardware.tetheroffload.control.V1_0.NetworkProtocol;
-import android.hardware.tetheroffload.control.V1_0.OffloadCallbackEvent;
+import android.hardware.tetheroffload.control.V1_1.ITetheringOffloadCallback;
+import android.hardware.tetheroffload.control.V1_1.OffloadCallbackEvent;
 import android.net.netlink.StructNfGenMsg;
 import android.net.netlink.StructNlMsgHdr;
 import android.net.util.SharedLog;
@@ -45,6 +52,7 @@
 import android.system.ErrnoException;
 import android.system.Os;
 import android.system.OsConstants;
+import android.util.Pair;
 
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
@@ -53,6 +61,7 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
+import org.mockito.InOrder;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
@@ -73,7 +82,7 @@
     private OffloadHardwareInterface.ControlCallback mControlCallback;
 
     @Mock private IOffloadConfig mIOffloadConfig;
-    @Mock private IOffloadControl mIOffloadControl;
+    private IOffloadControl mIOffloadControl;
     @Mock private NativeHandle mNativeHandle;
 
     // Random values to test Netlink message.
@@ -81,8 +90,10 @@
     private static final short TEST_FLAGS = 263;
 
     class MyDependencies extends OffloadHardwareInterface.Dependencies {
-        MyDependencies(SharedLog log) {
+        private final int mMockControlVersion;
+        MyDependencies(SharedLog log, final int mockControlVersion) {
             super(log);
+            mMockControlVersion = mockControlVersion;
         }
 
         @Override
@@ -91,8 +102,20 @@
         }
 
         @Override
-        public IOffloadControl getOffloadControl() {
-            return mIOffloadControl;
+        public Pair<IOffloadControl, Integer> getOffloadControl() {
+            switch (mMockControlVersion) {
+                case OFFLOAD_HAL_VERSION_1_0:
+                    mIOffloadControl = mock(IOffloadControl.class);
+                    break;
+                case OFFLOAD_HAL_VERSION_1_1:
+                    mIOffloadControl =
+                            mock(android.hardware.tetheroffload.control.V1_1.IOffloadControl.class);
+                    break;
+                default:
+                    throw new IllegalArgumentException("Invalid offload control version "
+                            + mMockControlVersion);
+            }
+            return new Pair<IOffloadControl, Integer>(mIOffloadControl, mMockControlVersion);
         }
 
         @Override
@@ -104,13 +127,13 @@
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
-        final SharedLog log = new SharedLog("test");
-        mOffloadHw = new OffloadHardwareInterface(new Handler(mTestLooper.getLooper()), log,
-                new MyDependencies(log));
         mControlCallback = spy(new OffloadHardwareInterface.ControlCallback());
     }
 
-    private void startOffloadHardwareInterface() throws Exception {
+    private void startOffloadHardwareInterface(int controlVersion) throws Exception {
+        final SharedLog log = new SharedLog("test");
+        mOffloadHw = new OffloadHardwareInterface(new Handler(mTestLooper.getLooper()), log,
+                new MyDependencies(log, controlVersion));
         mOffloadHw.initOffloadConfig();
         mOffloadHw.initOffloadControl(mControlCallback);
         final ArgumentCaptor<ITetheringOffloadCallback> mOffloadCallbackCaptor =
@@ -121,7 +144,7 @@
 
     @Test
     public void testGetForwardedStats() throws Exception {
-        startOffloadHardwareInterface();
+        startOffloadHardwareInterface(OFFLOAD_HAL_VERSION_1_0);
         final OffloadHardwareInterface.ForwardedStats stats = mOffloadHw.getForwardedStats(RMNET0);
         verify(mIOffloadControl).getForwardedStats(eq(RMNET0), any());
         assertNotNull(stats);
@@ -129,7 +152,7 @@
 
     @Test
     public void testSetLocalPrefixes() throws Exception {
-        startOffloadHardwareInterface();
+        startOffloadHardwareInterface(OFFLOAD_HAL_VERSION_1_0);
         final ArrayList<String> localPrefixes = new ArrayList<>();
         localPrefixes.add("127.0.0.0/8");
         localPrefixes.add("fe80::/64");
@@ -139,15 +162,32 @@
 
     @Test
     public void testSetDataLimit() throws Exception {
-        startOffloadHardwareInterface();
+        startOffloadHardwareInterface(OFFLOAD_HAL_VERSION_1_0);
         final long limit = 12345;
         mOffloadHw.setDataLimit(RMNET0, limit);
         verify(mIOffloadControl).setDataLimit(eq(RMNET0), eq(limit), any());
     }
 
     @Test
+    public void testSetDataWarningAndLimit() throws Exception {
+        // Verify V1.0 control HAL would reject the function call with exception.
+        startOffloadHardwareInterface(OFFLOAD_HAL_VERSION_1_0);
+        final long warning = 12345;
+        final long limit = 67890;
+        assertThrows(IllegalArgumentException.class,
+                () -> mOffloadHw.setDataWarningAndLimit(RMNET0, warning, limit));
+        reset(mIOffloadControl);
+
+        // Verify V1.1 control HAL could receive this function call.
+        startOffloadHardwareInterface(OFFLOAD_HAL_VERSION_1_1);
+        mOffloadHw.setDataWarningAndLimit(RMNET0, warning, limit);
+        verify((android.hardware.tetheroffload.control.V1_1.IOffloadControl) mIOffloadControl)
+                .setDataWarningAndLimit(eq(RMNET0), eq(warning), eq(limit), any());
+    }
+
+    @Test
     public void testSetUpstreamParameters() throws Exception {
-        startOffloadHardwareInterface();
+        startOffloadHardwareInterface(OFFLOAD_HAL_VERSION_1_0);
         final String v4addr = "192.168.10.1";
         final String v4gateway = "192.168.10.255";
         final ArrayList<String> v6gws = new ArrayList<>(0);
@@ -166,7 +206,7 @@
 
     @Test
     public void testUpdateDownstreamPrefix() throws Exception {
-        startOffloadHardwareInterface();
+        startOffloadHardwareInterface(OFFLOAD_HAL_VERSION_1_0);
         final String ifName = "wlan1";
         final String prefix = "192.168.43.0/24";
         mOffloadHw.addDownstreamPrefix(ifName, prefix);
@@ -178,7 +218,7 @@
 
     @Test
     public void testTetheringOffloadCallback() throws Exception {
-        startOffloadHardwareInterface();
+        startOffloadHardwareInterface(OFFLOAD_HAL_VERSION_1_0);
 
         mTetheringOffloadCallback.onEvent(OffloadCallbackEvent.OFFLOAD_STARTED);
         mTestLooper.dispatchAll();
@@ -217,10 +257,26 @@
                 eq(uint16(udpParams.src.port)),
                 eq(udpParams.dst.addr),
                 eq(uint16(udpParams.dst.port)));
+        reset(mControlCallback);
+
+        startOffloadHardwareInterface(OFFLOAD_HAL_VERSION_1_1);
+
+        // Verify the interface will process the events that comes from V1.1 HAL.
+        mTetheringOffloadCallback.onEvent_1_1(OffloadCallbackEvent.OFFLOAD_STARTED);
+        mTestLooper.dispatchAll();
+        final InOrder inOrder = inOrder(mControlCallback);
+        inOrder.verify(mControlCallback).onStarted();
+        inOrder.verifyNoMoreInteractions();
+
+        mTetheringOffloadCallback.onEvent_1_1(OffloadCallbackEvent.OFFLOAD_WARNING_REACHED);
+        mTestLooper.dispatchAll();
+        inOrder.verify(mControlCallback).onWarningReached();
+        inOrder.verifyNoMoreInteractions();
     }
 
     @Test
     public void testSendIpv4NfGenMsg() throws Exception {
+        startOffloadHardwareInterface(OFFLOAD_HAL_VERSION_1_0);
         FileDescriptor writeSocket = new FileDescriptor();
         FileDescriptor readSocket = new FileDescriptor();
         try {
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 2bc7fc4..69c1758 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
@@ -62,6 +62,8 @@
 
 import static com.android.net.module.util.Inet4AddressUtils.inet4AddressToIntHTH;
 import static com.android.net.module.util.Inet4AddressUtils.intToInet4AddressHTH;
+import static com.android.networkstack.tethering.OffloadHardwareInterface.OFFLOAD_HAL_VERSION_1_0;
+import static com.android.networkstack.tethering.OffloadHardwareInterface.OFFLOAD_HAL_VERSION_NONE;
 import static com.android.networkstack.tethering.TestConnectivityManager.BROADCAST_FIRST;
 import static com.android.networkstack.tethering.TestConnectivityManager.CALLBACKS_FIRST;
 import static com.android.networkstack.tethering.Tethering.UserRestrictionActionListener;
@@ -617,7 +619,7 @@
         mInterfaceConfiguration.flags = new String[0];
         when(mRouterAdvertisementDaemon.start())
                 .thenReturn(true);
-        initOffloadConfiguration(true /* offloadConfig */, true /* offloadControl */,
+        initOffloadConfiguration(true /* offloadConfig */, OFFLOAD_HAL_VERSION_1_0,
                 0 /* defaultDisabled */);
         when(mOffloadHardwareInterface.getForwardedStats(any())).thenReturn(mForwardedStats);
 
@@ -1837,7 +1839,7 @@
         callback.expectOffloadStatusChanged(TETHER_HARDWARE_OFFLOAD_STOPPED);
 
         // 1. Offload fail if no OffloadConfig.
-        initOffloadConfiguration(false /* offloadConfig */, true /* offloadControl */,
+        initOffloadConfiguration(false /* offloadConfig */, OFFLOAD_HAL_VERSION_1_0,
                 0 /* defaultDisabled */);
         runUsbTethering(upstreamState);
         callback.expectOffloadStatusChanged(TETHER_HARDWARE_OFFLOAD_FAILED);
@@ -1845,7 +1847,7 @@
         callback.expectOffloadStatusChanged(TETHER_HARDWARE_OFFLOAD_STOPPED);
         reset(mUsbManager);
         // 2. Offload fail if no OffloadControl.
-        initOffloadConfiguration(true /* offloadConfig */, false /* offloadControl */,
+        initOffloadConfiguration(true /* offloadConfig */, OFFLOAD_HAL_VERSION_NONE,
                 0 /* defaultDisabled */);
         runUsbTethering(upstreamState);
         callback.expectOffloadStatusChanged(TETHER_HARDWARE_OFFLOAD_FAILED);
@@ -1853,7 +1855,7 @@
         callback.expectOffloadStatusChanged(TETHER_HARDWARE_OFFLOAD_STOPPED);
         reset(mUsbManager);
         // 3. Offload fail if disabled by settings.
-        initOffloadConfiguration(true /* offloadConfig */, true /* offloadControl */,
+        initOffloadConfiguration(true /* offloadConfig */, OFFLOAD_HAL_VERSION_1_0,
                 1 /* defaultDisabled */);
         runUsbTethering(upstreamState);
         callback.expectOffloadStatusChanged(TETHER_HARDWARE_OFFLOAD_FAILED);
@@ -1869,9 +1871,10 @@
     }
 
     private void initOffloadConfiguration(final boolean offloadConfig,
-            final boolean offloadControl, final int defaultDisabled) {
+            @OffloadHardwareInterface.OffloadHalVersion final int offloadControlVersion,
+            final int defaultDisabled) {
         when(mOffloadHardwareInterface.initOffloadConfig()).thenReturn(offloadConfig);
-        when(mOffloadHardwareInterface.initOffloadControl(any())).thenReturn(offloadControl);
+        when(mOffloadHardwareInterface.initOffloadControl(any())).thenReturn(offloadControlVersion);
         when(mOffloadHardwareInterface.getDefaultTetherOffloadDisabled()).thenReturn(
                 defaultDisabled);
     }
diff --git a/tests/cts/hostside/AndroidTest.xml b/tests/cts/hostside/AndroidTest.xml
index b7fefaf..7a73313 100644
--- a/tests/cts/hostside/AndroidTest.xml
+++ b/tests/cts/hostside/AndroidTest.xml
@@ -23,7 +23,10 @@
     <target_preparer class="com.android.compatibility.common.tradefed.targetprep.LocationCheck" />
     <target_preparer class="com.android.cts.net.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.net.hostside.app2" />
+        <option name="teardown-command" value="am compat reset ALLOW_TEST_API_ACCESS com.android.cts.net.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" />
diff --git a/tests/cts/hostside/aidl/com/android/cts/net/hostside/IMyService.aidl b/tests/cts/hostside/aidl/com/android/cts/net/hostside/IMyService.aidl
index f523745..28437c2 100644
--- a/tests/cts/hostside/aidl/com/android/cts/net/hostside/IMyService.aidl
+++ b/tests/cts/hostside/aidl/com/android/cts/net/hostside/IMyService.aidl
@@ -16,6 +16,8 @@
 
 package com.android.cts.net.hostside;
 
+import android.app.job.JobInfo;
+
 import com.android.cts.net.hostside.INetworkCallback;
 
 interface IMyService {
@@ -26,4 +28,5 @@
     void sendNotification(int notificationId, String notificationType);
     void registerNetworkCallback(in NetworkRequest request, in INetworkCallback cb);
     void unregisterNetworkCallback();
+    void scheduleJob(in JobInfo jobInfo);
 }
diff --git a/tests/cts/hostside/aidl/com/android/cts/net/hostside/INetworkStateObserver.aidl b/tests/cts/hostside/aidl/com/android/cts/net/hostside/INetworkStateObserver.aidl
index 165f530..19198c5 100644
--- a/tests/cts/hostside/aidl/com/android/cts/net/hostside/INetworkStateObserver.aidl
+++ b/tests/cts/hostside/aidl/com/android/cts/net/hostside/INetworkStateObserver.aidl
@@ -17,6 +17,10 @@
 package com.android.cts.net.hostside;
 
 interface INetworkStateObserver {
-    boolean isForeground();
-    void onNetworkStateChecked(String resultData);
+    void onNetworkStateChecked(int resultCode, String resultData);
+
+    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/app/Android.bp b/tests/cts/hostside/app/Android.bp
index a9686de..5b2369c 100644
--- a/tests/cts/hostside/app/Android.bp
+++ b/tests/cts/hostside/app/Android.bp
@@ -20,8 +20,10 @@
 
 android_test_helper_app {
     name: "CtsHostsideNetworkTestsApp",
-    defaults: ["cts_support_defaults"],
-    //sdk_version: "current",
+    defaults: [
+        "cts_support_defaults",
+        "framework-connectivity-test-defaults",
+    ],
     platform_apis: true,
     static_libs: [
         "androidx.test.rules",
@@ -38,10 +40,6 @@
         "android.test.base",
     ],
     srcs: ["src/**/*.java"],
-    // STOPSHIP: remove this before releasing any networking modules.
-    exclude_srcs: [
-        "src/com/android/cts/net/hostside/VpnTest.java",
-    ],
     // Tag this module as a cts test artifact
     test_suites: [
         "cts",
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractAppIdleTestCase.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractAppIdleTestCase.java
index f9e30b6..d9ff539 100644
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractAppIdleTestCase.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractAppIdleTestCase.java
@@ -50,7 +50,7 @@
     public final void tearDown() throws Exception {
         super.tearDown();
 
-        executeSilentShellCommand("cmd battery reset");
+        resetBatteryState();
         setAppIdle(false);
     }
 
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractDozeModeTestCase.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractDozeModeTestCase.java
index 6f32c56..e0ce4ea 100644
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractDozeModeTestCase.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractDozeModeTestCase.java
@@ -101,7 +101,7 @@
     @Test
     public void testBackgroundNetworkAccess_enabledButWhitelistedOnNotificationAction()
             throws Exception {
-        setPendingIntentWhitelistDuration(NETWORK_TIMEOUT_MS);
+        setPendingIntentAllowlistDuration(NETWORK_TIMEOUT_MS);
         try {
             registerNotificationListenerService();
             setDozeMode(true);
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractExpeditedJobTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractExpeditedJobTest.java
new file mode 100644
index 0000000..a850e3b
--- /dev/null
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractExpeditedJobTest.java
@@ -0,0 +1,134 @@
+/*
+ * 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.net.hostside;
+
+import static com.android.cts.net.hostside.NetworkPolicyTestUtils.setRestrictBackground;
+import static com.android.cts.net.hostside.Property.APP_STANDBY_MODE;
+import static com.android.cts.net.hostside.Property.BATTERY_SAVER_MODE;
+import static com.android.cts.net.hostside.Property.DATA_SAVER_MODE;
+import static com.android.cts.net.hostside.Property.DOZE_MODE;
+import static com.android.cts.net.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();
+        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/app/src/com/android/cts/net/hostside/AbstractRestrictBackgroundNetworkTestCase.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractRestrictBackgroundNetworkTestCase.java
index 1afbfb0..f9454ad 100644
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractRestrictBackgroundNetworkTestCase.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractRestrictBackgroundNetworkTestCase.java
@@ -22,6 +22,7 @@
 import static android.os.BatteryManager.BATTERY_PLUGGED_WIRELESS;
 
 import static com.android.cts.net.hostside.NetworkPolicyTestUtils.executeShellCommand;
+import static com.android.cts.net.hostside.NetworkPolicyTestUtils.forceRunJob;
 import static com.android.cts.net.hostside.NetworkPolicyTestUtils.getConnectivityManager;
 import static com.android.cts.net.hostside.NetworkPolicyTestUtils.getContext;
 import static com.android.cts.net.hostside.NetworkPolicyTestUtils.getInstrumentation;
@@ -39,6 +40,7 @@
 import android.app.ActivityManager;
 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;
@@ -52,13 +54,19 @@
 import android.os.Binder;
 import android.os.Bundle;
 import android.os.SystemClock;
+import android.provider.DeviceConfig;
 import android.service.notification.NotificationListenerService;
 import android.util.Log;
+import android.util.Pair;
+
+import com.android.compatibility.common.util.BatteryUtils;
+import com.android.compatibility.common.util.DeviceConfigStateHelper;
 
 import org.junit.Rule;
 import org.junit.rules.RuleChain;
 import org.junit.runner.RunWith;
 
+import java.util.ArrayList;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.LinkedBlockingQueue;
 import java.util.concurrent.TimeUnit;
@@ -75,12 +83,22 @@
 
     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.net.hostside.app2.action.FINISH_ACTIVITY";
+    private static final String ACTION_FINISH_JOB =
+            "com.android.cts.net.hostside.app2.action.FINISH_JOB";
 
     private static final String ACTION_RECEIVER_READY =
             "com.android.cts.net.hostside.app2.action.RECEIVER_READY";
@@ -102,17 +120,21 @@
     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 int PROCESS_STATE_FOREGROUND_SERVICE;
 
     private static final String KEY_NETWORK_STATE_OBSERVER = TEST_PKG + ".observer";
+    private static final String KEY_SKIP_VALIDATION_CHECKS = TEST_PKG + ".skip_validation_checks";
 
     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 FOREGROUND_PROC_NETWORK_TIMEOUT_MS = 6000;
+    private static final int ACTIVITY_NETWORK_STATE_TIMEOUT_MS = 6_000;
+    private static final int JOB_NETWORK_STATE_TIMEOUT_MS = 10_000;
 
     // Must be higher than NETWORK_TIMEOUT_MS
     private static final int ORDERED_BROADCAST_TIMEOUT_MS = NETWORK_TIMEOUT_MS * 4;
@@ -130,24 +152,25 @@
     protected int mUid;
     private int mMyUid;
     private MyServiceClient mServiceClient;
-    private String mDeviceIdleConstantsSetting;
+    private DeviceConfigStateHelper mDeviceIdleDeviceConfigStateHelper;
 
     @Rule
     public final RuleChain mRuleChain = RuleChain.outerRule(new RequiredPropertiesRule())
             .around(new MeterednessConfigurationRule());
 
     protected void setUp() throws Exception {
-
+        // TODO: Annotate these constants with @TestApi instead of obtaining them using reflection
         PROCESS_STATE_FOREGROUND_SERVICE = (Integer) ActivityManager.class
                 .getDeclaredField("PROCESS_STATE_FOREGROUND_SERVICE").get(null);
         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);
         mServiceClient.bind();
-        mDeviceIdleConstantsSetting = "device_idle_constants";
         executeShellCommand("cmd netpolicy start-watching " + mUid);
         setAppIdle(false);
 
@@ -254,8 +277,8 @@
     /**
      * Asserts that an app always have access while on foreground or running a foreground service.
      *
-     * <p>This method will launch an activity and a foreground service to make the assertion, but
-     * will finish the activity / stop the service afterwards.
+     * <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.
@@ -267,6 +290,16 @@
         stopForegroundService();
     }
 
+    protected void assertExpeditedJobHasNetworkAccess() throws Exception {
+        launchComponentAndAssertNetworkAccess(TYPE_EXPEDITED_JOB);
+        finishExpeditedJob();
+    }
+
+    protected void assertExpeditedJobHasNoNetworkAccess() throws Exception {
+        launchComponentAndAssertNetworkAccess(TYPE_EXPEDITED_JOB, false);
+        finishExpeditedJob();
+    }
+
     protected final void assertBackgroundState() throws Exception {
         final int maxTries = 30;
         ProcessState state = null;
@@ -338,7 +371,7 @@
         for (int i = 1; i <= maxTries; i++) {
             error = checkNetworkAccess(expectAvailable);
 
-            if (error.isEmpty()) return;
+            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
@@ -410,7 +443,7 @@
             errors.append("\tnetworkInfo: " + networkInfo + "\n");
             errors.append("\tconnectionCheckDetails: " + connectionCheckDetails + "\n");
         }
-        return errors.toString();
+        return errors.length() == 0 ? null : errors.toString();
     }
 
     /**
@@ -602,6 +635,10 @@
         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) {
@@ -737,18 +774,21 @@
                 nm.isNotificationListenerAccessGranted(listenerComponent));
     }
 
-    protected void setPendingIntentWhitelistDuration(int durationMs) throws Exception {
-        executeSilentShellCommand(String.format(
-                "settings put global %s %s=%d", mDeviceIdleConstantsSetting,
-                "notification_whitelist_duration", durationMs));
+    protected void setPendingIntentAllowlistDuration(long durationMs) {
+        mDeviceIdleDeviceConfigStateHelper.set("notification_allowlist_duration_ms",
+                String.valueOf(durationMs));
     }
 
-    protected void resetDeviceIdleSettings() throws Exception {
-        executeShellCommand(String.format("settings delete global %s",
-                mDeviceIdleConstantsSetting));
+    protected void resetDeviceIdleSettings() {
+        mDeviceIdleDeviceConfigStateHelper.restoreOriginalValues();
     }
 
     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();
@@ -760,21 +800,61 @@
             final CountDownLatch latch = new CountDownLatch(1);
             final Intent launchIntent = getIntentForComponent(type);
             final Bundle extras = new Bundle();
-            final String[] errors = new String[]{null};
-            extras.putBinder(KEY_NETWORK_STATE_OBSERVER, getNewNetworkStateObserver(latch, errors));
+            final ArrayList<Pair<Integer, String>> result = new ArrayList<>(1);
+            extras.putBinder(KEY_NETWORK_STATE_OBSERVER, getNewNetworkStateObserver(latch, result));
+            extras.putBoolean(KEY_SKIP_VALIDATION_CHECKS, !expectAvailable);
             launchIntent.putExtras(extras);
             mContext.startActivity(launchIntent);
-            if (latch.await(FOREGROUND_PROC_NETWORK_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
-                if (!errors[0].isEmpty()) {
-                    if (errors[0] == APP_NOT_FOREGROUND_ERROR) {
-                        // App didn't come to foreground when the activity is started, so try again.
-                        assertForegroundNetworkAccess();
-                    } else {
-                        fail("Network is not available for app2 (" + mUid + "): " + errors[0]);
+            if (latch.await(ACTIVITY_NETWORK_STATE_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
+                final int resultCode = result.get(0).first;
+                final String resultData = result.get(0).second;
+                if (resultCode == INetworkStateObserver.RESULT_SUCCESS_NETWORK_STATE_CHECKED) {
+                    final String error = checkForAvailabilityInResultData(
+                            resultData, expectAvailable);
+                    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, resultData);
+                    // App didn't come to foreground when the activity is started, so try again.
+                    assertForegroundNetworkAccess();
+                } else {
+                    fail("Unexpected resultCode=" + resultCode + "; received=[" + resultData + "]");
                 }
             } else {
-                fail("Timed out waiting for network availability status from app2 (" + mUid + ")");
+                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 ArrayList<Pair<Integer, String>> result = new ArrayList<>(1);
+            final CountDownLatch latch = new CountDownLatch(1);
+            extras.putBinder(KEY_NETWORK_STATE_OBSERVER, getNewNetworkStateObserver(latch, result));
+            extras.putBoolean(KEY_SKIP_VALIDATION_CHECKS, !expectAvailable);
+            final JobInfo jobInfo = new JobInfo.Builder(TEST_JOB_ID, TEST_JOB_COMPONENT)
+                    .setExpedited(true)
+                    .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
+                    .setTransientExtras(extras)
+                    .build();
+            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(0).first;
+                final String resultData = result.get(0).second;
+                if (resultCode == INetworkStateObserver.RESULT_SUCCESS_NETWORK_STATE_CHECKED) {
+                    final String error = checkForAvailabilityInResultData(
+                            resultData, expectAvailable);
+                    if (error != null) {
+                        fail("Network is not available for expedited job in app2 (" + mUid + "): "
+                                + error);
+                    }
+                } else {
+                    fail("Unexpected resultCode=" + resultCode + "; received=[" + resultData + "]");
+                }
+            } else {
+                fail("Timed out waiting for network availability status from app2's expedited job ("
+                        + mUid + ")");
             }
         } else {
             throw new IllegalArgumentException("Unknown type: " + type);
@@ -808,36 +888,34 @@
     }
 
     private Binder getNewNetworkStateObserver(final CountDownLatch latch,
-            final String[] errors) {
+            final ArrayList<Pair<Integer, String>> result) {
         return new INetworkStateObserver.Stub() {
             @Override
-            public boolean isForeground() {
-                try {
-                    final ProcessState state = getProcessStateByUid(mUid);
-                    return !isBackground(state.state);
-                } catch (Exception e) {
-                    Log.d(TAG, "Error while reading the proc state for " + mUid + ": " + e);
-                    return false;
-                }
-            }
-
-            @Override
-            public void onNetworkStateChecked(String resultData) {
-                errors[0] = resultData == null
-                        ? APP_NOT_FOREGROUND_ERROR
-                        : checkForAvailabilityInResultData(resultData, true);
+            public void onNetworkStateChecked(int resultCode, String resultData) {
+                result.add(Pair.create(resultCode, resultData));
                 latch.countDown();
             }
         };
     }
 
     /**
-     * Finishes an activity on app2 so its process is demoted fromforeground status.
+     * Finishes an activity on app2 so its process is demoted from foreground status.
      */
     protected void finishActivity() throws Exception {
-        executeShellCommand("am broadcast -a "
-                + " com.android.cts.net.hostside.app2.action.FINISH_ACTIVITY "
-                + "--receiver-foreground --receiver-registered-only");
+        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 {
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/ExpeditedJobMeteredTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/ExpeditedJobMeteredTest.java
new file mode 100644
index 0000000..3809534
--- /dev/null
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/ExpeditedJobMeteredTest.java
@@ -0,0 +1,23 @@
+/*
+ * 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.net.hostside;
+
+import static com.android.cts.net.hostside.Property.METERED_NETWORK;
+
+@RequiredProperties({METERED_NETWORK})
+public class ExpeditedJobMeteredTest extends AbstractExpeditedJobTest {
+}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/ExpeditedJobNonMeteredTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/ExpeditedJobNonMeteredTest.java
new file mode 100644
index 0000000..6596269
--- /dev/null
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/ExpeditedJobNonMeteredTest.java
@@ -0,0 +1,23 @@
+/*
+ * 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.net.hostside;
+
+import static com.android.cts.net.hostside.Property.NON_METERED_NETWORK;
+
+@RequiredProperties({NON_METERED_NETWORK})
+public class ExpeditedJobNonMeteredTest extends AbstractExpeditedJobTest {
+}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyServiceClient.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyServiceClient.java
index c37e8d5..8b70f9b 100644
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyServiceClient.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyServiceClient.java
@@ -16,6 +16,7 @@
 
 package com.android.cts.net.hostside;
 
+import android.app.job.JobInfo;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
@@ -105,4 +106,8 @@
     public void unregisterNetworkCallback() throws RemoteException {
         mService.unregisterNetworkCallback();
     }
+
+    public void scheduleJob(JobInfo jobInfo) throws RemoteException {
+        mService.scheduleJob(jobInfo);
+    }
 }
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkPolicyTestUtils.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkPolicyTestUtils.java
index e62d557..7da1a21 100644
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkPolicyTestUtils.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkPolicyTestUtils.java
@@ -48,6 +48,7 @@
 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;
@@ -137,6 +138,12 @@
         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);
diff --git a/tests/cts/hostside/app2/Android.bp b/tests/cts/hostside/app2/Android.bp
index b448459..dd33eed 100644
--- a/tests/cts/hostside/app2/Android.bp
+++ b/tests/cts/hostside/app2/Android.bp
@@ -21,7 +21,7 @@
 android_test_helper_app {
     name: "CtsHostsideNetworkTestsApp2",
     defaults: ["cts_support_defaults"],
-    sdk_version: "current",
+    sdk_version: "test_current",
     static_libs: ["CtsHostsideNetworkTestsAidl"],
     srcs: ["src/**/*.java"],
     // Tag this module as a cts test artifact
diff --git a/tests/cts/hostside/app2/AndroidManifest.xml b/tests/cts/hostside/app2/AndroidManifest.xml
index eb777f2..4ac4bcb 100644
--- a/tests/cts/hostside/app2/AndroidManifest.xml
+++ b/tests/cts/hostside/app2/AndroidManifest.xml
@@ -21,20 +21,24 @@
     <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
     <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
     <uses-permission android:name="android.permission.INTERNET"/>
+    <uses-permission android:name="android.permission.PACKAGE_USAGE_STATS" />
 
     <!--
-                 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.
+     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.
+     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">
+     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"
@@ -55,6 +59,8 @@
                 <action android:name="com.android.cts.net.hostside.app2.action.SHOW_TOAST"/>
                 </intent-filter>
         </receiver>
+        <service android:name=".MyJobService"
+            android:permission="android.permission.BIND_JOB_SERVICE" />
     </application>
 
 </manifest>
diff --git a/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/Common.java b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/Common.java
index 351733e..62b508c 100644
--- a/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/Common.java
+++ b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/Common.java
@@ -15,11 +15,13 @@
  */
 package com.android.cts.net.hostside.app2;
 
+import android.app.ActivityManager;
 import android.content.Context;
 import android.content.Intent;
 import android.content.pm.PackageManager.NameNotFoundException;
 import android.os.AsyncTask;
 import android.os.Bundle;
+import android.os.Process;
 import android.os.RemoteException;
 import android.util.Log;
 
@@ -38,6 +40,8 @@
             "com.android.cts.net.hostside.app2.action.RECEIVER_READY";
     static final String ACTION_FINISH_ACTIVITY =
             "com.android.cts.net.hostside.app2.action.FINISH_ACTIVITY";
+    static final String ACTION_FINISH_JOB =
+            "com.android.cts.net.hostside.app2.action.FINISH_JOB";
     static final String ACTION_SHOW_TOAST =
             "com.android.cts.net.hostside.app2.action.SHOW_TOAST";
 
@@ -51,6 +55,11 @@
 
     static final String TEST_PKG = "com.android.cts.net.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 int TYPE_COMPONENT_ACTIVTY = 0;
+    static final int TYPE_COMPONENT_FOREGROUND_SERVICE = 1;
+    static final int TYPE_COMPONENT_EXPEDITED_JOB = 2;
 
     static int getUid(Context context) {
         final String packageName = context.getPackageName();
@@ -61,11 +70,57 @@
         }
     }
 
-    static void notifyNetworkStateObserver(Context context, Intent intent) {
+    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(
+                            INetworkStateObserver.RESULT_ERROR_UNEXPECTED_PROC_STATE,
+                            "Unexpected procstate: " + procState);
+                    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(
+                            INetworkStateObserver.RESULT_ERROR_UNEXPECTED_PROC_STATE,
+                            "Unexpected procstate: " + procState);
+                    return false;
+                }
+                return true;
+            }
+            case TYPE_COMPONENT_EXPEDITED_JOB: {
+                final int capabilities = activityManager.getUidProcessCapabilities(Process.myUid());
+                if ((capabilities & ActivityManager.PROCESS_CAPABILITY_NETWORK) == 0) {
+                    observer.onNetworkStateChecked(
+                            INetworkStateObserver.RESULT_ERROR_UNEXPECTED_CAPABILITIES,
+                            "Unexpected capabilities: " + capabilities);
+                    return false;
+                }
+                return true;
+            }
+            default: {
+                observer.onNetworkStateChecked(INetworkStateObserver.RESULT_ERROR_OTHER,
+                        "Unknown component type: " + componentType);
+                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;
         }
@@ -73,17 +128,17 @@
                 extras.getBinder(KEY_NETWORK_STATE_OBSERVER));
         if (observer != null) {
             try {
-                if (!observer.isForeground()) {
-                    Log.e(TAG, "App didn't come to foreground");
-                    observer.onNetworkStateChecked(null);
+                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 reading the proc state: " + e);
+                Log.e(TAG, "Error occurred while informing the validation result: " + e);
             }
             AsyncTask.execute(() -> {
                 try {
                     observer.onNetworkStateChecked(
+                            INetworkStateObserver.RESULT_SUCCESS_NETWORK_STATE_CHECKED,
                             MyBroadcastReceiver.checkNetworkStatus(context));
                 } catch (RemoteException e) {
                     Log.e(TAG, "Error occurred while notifying the observer: " + e);
diff --git a/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyActivity.java b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyActivity.java
index 286cc2f..9fdb9c9 100644
--- a/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyActivity.java
+++ b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyActivity.java
@@ -18,6 +18,7 @@
 import static com.android.cts.net.hostside.app2.Common.ACTION_FINISH_ACTIVITY;
 import static com.android.cts.net.hostside.app2.Common.TAG;
 import static com.android.cts.net.hostside.app2.Common.TEST_PKG;
+import static com.android.cts.net.hostside.app2.Common.TYPE_COMPONENT_ACTIVTY;
 
 import android.app.Activity;
 import android.content.BroadcastReceiver;
@@ -42,7 +43,7 @@
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         Log.d(TAG, "MyActivity.onCreate()");
-        Common.notifyNetworkStateObserver(this, getIntent());
+        Common.notifyNetworkStateObserver(this, getIntent(), TYPE_COMPONENT_ACTIVTY);
         finishCommandReceiver = new BroadcastReceiver() {
             @Override
             public void onReceive(Context context, Intent intent) {
diff --git a/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyBroadcastReceiver.java b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyBroadcastReceiver.java
index aa54075..c9ae16f 100644
--- a/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyBroadcastReceiver.java
+++ b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyBroadcastReceiver.java
@@ -201,7 +201,7 @@
         Log.d(TAG, "sendNotification: id=" + notificationId + ", type=" + notificationType);
         final Intent serviceIntent = new Intent(context, MyService.class);
         final PendingIntent pendingIntent = PendingIntent.getService(context, 0, serviceIntent,
-                notificationId);
+                PendingIntent.FLAG_MUTABLE);
         final Bundle bundle = new Bundle();
         bundle.putCharSequence("parcelable", "I am not");
 
diff --git a/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyForegroundService.java b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyForegroundService.java
index ff4ba65..b55761c 100644
--- a/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyForegroundService.java
+++ b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyForegroundService.java
@@ -17,6 +17,7 @@
 
 import static com.android.cts.net.hostside.app2.Common.TAG;
 import static com.android.cts.net.hostside.app2.Common.TEST_PKG;
+import static com.android.cts.net.hostside.app2.Common.TYPE_COMPONENT_FOREGROUND_SERVICE;
 
 import android.R;
 import android.app.Notification;
@@ -58,7 +59,7 @@
                 startForeground(42, new Notification.Builder(this, NOTIFICATION_CHANNEL_ID)
                         .setSmallIcon(R.drawable.ic_dialog_alert) // any icon is fine
                         .build());
-                Common.notifyNetworkStateObserver(this, intent);
+                Common.notifyNetworkStateObserver(this, intent, TYPE_COMPONENT_FOREGROUND_SERVICE);
                 break;
             case FLAG_STOP_FOREGROUND:
                 Log.d(TAG, "Stopping foreground");
diff --git a/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyJobService.java b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyJobService.java
new file mode 100644
index 0000000..51c3157
--- /dev/null
+++ b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyJobService.java
@@ -0,0 +1,81 @@
+/*
+ * 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.net.hostside.app2;
+
+import static com.android.cts.net.hostside.app2.Common.ACTION_FINISH_JOB;
+import static com.android.cts.net.hostside.app2.Common.TAG;
+import static com.android.cts.net.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));
+        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/app2/src/com/android/cts/net/hostside/app2/MyService.java b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyService.java
index 717ccb1..7dc4b9c 100644
--- a/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyService.java
+++ b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyService.java
@@ -24,6 +24,8 @@
 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;
@@ -154,6 +156,13 @@
                 mNetworkCallback = null;
             }
         }
+
+        @Override
+        public void scheduleJob(JobInfo jobInfo) {
+            final JobScheduler jobScheduler = getApplicationContext()
+                    .getSystemService(JobScheduler.class);
+            jobScheduler.schedule(jobInfo);
+        }
       };
 
     @Override
diff --git a/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkTestCase.java b/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkTestCase.java
index 37420bf..89c79d3 100644
--- a/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkTestCase.java
+++ b/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkTestCase.java
@@ -80,7 +80,7 @@
             DeviceNotAvailableException {
         CompatibilityBuildHelper buildHelper = new CompatibilityBuildHelper(mCtsBuild);
         assertNull(getDevice().installPackage(buildHelper.getTestFile(apk),
-                false /* reinstall */, true /* grantPermissions */));
+                false /* reinstall */, true /* grantPermissions */, "-t"));
     }
 
     protected void uninstallPackage(String packageName, boolean shouldSucceed)
diff --git a/tests/cts/hostside/src/com/android/cts/net/HostsideRestrictBackgroundNetworkTests.java b/tests/cts/hostside/src/com/android/cts/net/HostsideRestrictBackgroundNetworkTests.java
index 0e25d5e..d026fe0 100644
--- a/tests/cts/hostside/src/com/android/cts/net/HostsideRestrictBackgroundNetworkTests.java
+++ b/tests/cts/hostside/src/com/android/cts/net/HostsideRestrictBackgroundNetworkTests.java
@@ -318,11 +318,23 @@
     /**************************
      * Restricted mode tests. *
      **************************/
-    public void testRestrictedMode_networkAccess() throws Exception {
+    public void testNetworkAccess_restrictedMode() throws Exception {
         runDeviceTests(TEST_PKG, TEST_PKG + ".RestrictedModeTest",
                 "testNetworkAccess");
     }
 
+    /************************
+     * Expedited job tests. *
+     ************************/
+
+    public void testMeteredNetworkAccess_expeditedJob() throws Exception {
+        runDeviceTests(TEST_PKG, TEST_PKG + ".ExpeditedJobMeteredTest");
+    }
+
+    public void testNonMeteredNetworkAccess_expeditedJob() throws Exception {
+        runDeviceTests(TEST_PKG, TEST_PKG + ".ExpeditedJobNonMeteredTest");
+    }
+
     /*******************
      * Helper methods. *
      *******************/
diff --git a/tests/cts/net/Android.bp b/tests/cts/net/Android.bp
index 4a1020d..25596a9 100644
--- a/tests/cts/net/Android.bp
+++ b/tests/cts/net/Android.bp
@@ -18,7 +18,10 @@
 
 java_defaults {
     name: "CtsNetTestCasesDefaults",
-    defaults: ["cts_defaults"],
+    defaults: [
+        "cts_defaults",
+        "framework-connectivity-test-defaults",
+    ],
 
     // Include both the 32 and 64 bit versions
     compile_multilib: "both",
@@ -39,10 +42,6 @@
         "src/**/*.java",
         "src/**/*.kt",
     ],
-    exclude_srcs: [
-        // TODO: delete this after mainline-prod snaps to sc-dev.
-        "src/android/net/cts/NetworkAgentTest.kt",
-    ],
     jarjar_rules: "jarjar-rules-shared.txt",
     static_libs: [
         "bouncycastle-unbundled",
@@ -68,7 +67,6 @@
 // devices.
 android_test {
     name: "CtsNetTestCases",
-    enabled: false, // Disabled in mainline-prod
     defaults: ["CtsNetTestCasesDefaults"],
     // TODO: CTS should not depend on the entirety of the networkstack code.
     static_libs: [
diff --git a/tests/cts/net/AndroidTestTemplate.xml b/tests/cts/net/AndroidTestTemplate.xml
index 78a01e2..474eefe 100644
--- a/tests/cts/net/AndroidTestTemplate.xml
+++ b/tests/cts/net/AndroidTestTemplate.xml
@@ -21,6 +21,7 @@
     <option name="config-descriptor:metadata" key="parameter" value="secondary_user" />
 
     <option name="config-descriptor:metadata" key="mainline-param" value="CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex" />
+    <option name="config-descriptor:metadata" key="mainline-param" value="CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex" />
     <option name="not-shardable" value="true" />
     <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
         <option name="cleanup-apks" value="true" />
diff --git a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
index 8023aa0..4dfc669 100644
--- a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
@@ -132,6 +132,7 @@
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.internal.util.ArrayUtils;
+import com.android.modules.utils.build.SdkLevel;
 import com.android.networkstack.apishim.ConnectivityManagerShimImpl;
 import com.android.networkstack.apishim.ConstantsShim;
 import com.android.networkstack.apishim.NetworkInformationShimImpl;
@@ -1421,7 +1422,10 @@
             return;
         }
 
-        final int firstSdk = Build.VERSION.FIRST_SDK_INT;
+        final int firstSdk = SdkLevel.isAtLeastS()
+                ? Build.VERSION.DEVICE_INITIAL_SDK_INT
+                // FIRST_SDK_INT was a @TestApi field renamed to DEVICE_INITIAL_SDK_INT in S
+                : Build.VERSION.class.getField("FIRST_SDK_INT").getInt(null);
         if (firstSdk < Build.VERSION_CODES.Q) {
             Log.i(TAG, "testSocketKeepaliveLimitTelephony: skip test for devices launching"
                     + " before Q: " + firstSdk);
diff --git a/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt b/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
index 3214531..e13e92e 100644
--- a/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
+++ b/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
@@ -68,6 +68,7 @@
 import android.os.HandlerThread
 import android.os.Looper
 import android.os.Message
+import android.os.SystemClock
 import android.util.DebugUtils.valueToString
 import androidx.test.InstrumentationRegistry
 import com.android.modules.utils.build.SdkLevel
@@ -76,6 +77,7 @@
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
 import com.android.testutils.DevSdkIgnoreRunner
 import com.android.testutils.RecorderCallback.CallbackEntry.Available
+import com.android.testutils.RecorderCallback.CallbackEntry.Losing
 import com.android.testutils.RecorderCallback.CallbackEntry.Lost
 import com.android.testutils.TestableNetworkCallback
 import org.junit.After
@@ -114,6 +116,7 @@
 // requests filed by the test and should never match normal internet requests. 70 is the default
 // score of Ethernet networks, it's as good a value as any other.
 private const val TEST_NETWORK_SCORE = 70
+private const val WORSE_NETWORK_SCORE = 65
 private const val BETTER_NETWORK_SCORE = 75
 private const val FAKE_NET_ID = 1098
 private val instrumentation: Instrumentation
@@ -539,16 +542,23 @@
         // Connect the first Network
         createConnectedNetworkAgent(name = name1).let { (agent1, _) ->
             callback.expectAvailableThenValidatedCallbacks(agent1.network)
-            // Upgrade agent1 to a better score so that there is no ambiguity when
-            // agent2 connects that agent1 is still better
-            agent1.sendNetworkScore(BETTER_NETWORK_SCORE - 1)
+            // If using the int ranking, agent1 must be upgraded to a better score so that there is
+            // no ambiguity when agent2 connects that agent1 is still better. If using policy
+            // ranking, this is not necessary.
+            agent1.sendNetworkScore(NetworkScore.Builder().setLegacyInt(BETTER_NETWORK_SCORE)
+                    .build())
             // Connect the second agent
             createConnectedNetworkAgent(name = name2).let { (agent2, _) ->
                 agent2.markConnected()
-                // The callback should not see anything yet
+                // The callback should not see anything yet. With int ranking, agent1 was upgraded
+                // to a stronger score beforehand. With policy ranking, agent1 is preferred by
+                // virtue of already satisfying the request.
                 callback.assertNoCallback(NO_CALLBACK_TIMEOUT)
-                // Now update the score and expect the callback now prefers agent2
-                agent2.sendNetworkScore(BETTER_NETWORK_SCORE)
+                // Now downgrade the score and expect the callback now prefers agent2
+                agent1.sendNetworkScore(NetworkScore.Builder()
+                        .setLegacyInt(WORSE_NETWORK_SCORE)
+                        .setExiting(true)
+                        .build())
                 callback.expectCallback<Available>(agent2.network)
             }
         }
@@ -758,4 +768,71 @@
 
         // tearDown() will unregister the requests and agents
     }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.R)
+    fun testSetLingerDuration() {
+        // This test will create two networks and check that the one with the stronger
+        // score wins out for a request that matches them both. And the weaker agent will
+        // be disconnected after customized linger duration.
+
+        // Connect the first Network
+        val name1 = UUID.randomUUID().toString()
+        val name2 = UUID.randomUUID().toString()
+        val (agent1, callback) = createConnectedNetworkAgent(name = name1)
+        callback.expectAvailableThenValidatedCallbacks(agent1.network!!)
+        // Downgrade agent1 to a worse score so that there is no ambiguity when
+        // agent2 connects.
+        agent1.sendNetworkScore(NetworkScore.Builder().setLegacyInt(WORSE_NETWORK_SCORE)
+                .setExiting(true).build())
+
+        // Verify invalid linger duration cannot be set.
+        assertFailsWith<IllegalArgumentException> {
+            agent1.setLingerDuration(Duration.ofMillis(-1))
+        }
+        assertFailsWith<IllegalArgumentException> { agent1.setLingerDuration(Duration.ZERO) }
+        assertFailsWith<IllegalArgumentException> {
+            agent1.setLingerDuration(Duration.ofMillis(Integer.MIN_VALUE.toLong()))
+        }
+        assertFailsWith<IllegalArgumentException> {
+            agent1.setLingerDuration(Duration.ofMillis(Integer.MAX_VALUE.toLong() + 1))
+        }
+        assertFailsWith<IllegalArgumentException> {
+            agent1.setLingerDuration(Duration.ofMillis(
+                    NetworkAgent.MIN_LINGER_TIMER_MS.toLong() - 1))
+        }
+        // Verify valid linger timer can be set, but it should not take effect since the network
+        // is still needed.
+        agent1.setLingerDuration(Duration.ofMillis(Integer.MAX_VALUE.toLong()))
+        callback.assertNoCallback(NO_CALLBACK_TIMEOUT)
+        // Set to the value we want to verify the functionality.
+        agent1.setLingerDuration(Duration.ofMillis(NetworkAgent.MIN_LINGER_TIMER_MS.toLong()))
+        // Make a listener which can observe agent1 lost later.
+        val callbackWeaker = TestableNetworkCallback(timeoutMs = DEFAULT_TIMEOUT_MS)
+        registerNetworkCallback(NetworkRequest.Builder()
+                .clearCapabilities()
+                .addTransportType(TRANSPORT_TEST)
+                .setNetworkSpecifier(CompatUtil.makeEthernetNetworkSpecifier(name1))
+                .build(), callbackWeaker)
+        callbackWeaker.expectAvailableCallbacks(agent1.network!!)
+
+        // Connect the second agent with a score better than agent1. Verify the callback for
+        // agent1 sees the linger expiry while the callback for both sees the winner.
+        // Record linger start timestamp prior to send score to prevent possible race, the actual
+        // timestamp should be slightly late than this since the service handles update
+        // network score asynchronously.
+        val lingerStart = SystemClock.elapsedRealtime()
+        val agent2 = createNetworkAgent(name = name2)
+        agent2.register()
+        agent2.markConnected()
+        callback.expectAvailableCallbacks(agent2.network!!)
+        callbackWeaker.expectCallback<Losing>(agent1.network!!)
+        val expectedRemainingLingerDuration = lingerStart +
+                NetworkAgent.MIN_LINGER_TIMER_MS.toLong() - SystemClock.elapsedRealtime()
+        // If the available callback is too late. The remaining duration will be reduced.
+        assertTrue(expectedRemainingLingerDuration > 0,
+                "expected remaining linger duration is $expectedRemainingLingerDuration")
+        callbackWeaker.assertNoCallback(expectedRemainingLingerDuration)
+        callbackWeaker.expectCallback<Lost>(agent1.network!!)
+    }
 }
diff --git a/tests/cts/net/src/android/net/cts/NetworkRequestTest.java b/tests/cts/net/src/android/net/cts/NetworkRequestTest.java
index 8c35b97..bca4456 100644
--- a/tests/cts/net/src/android/net/cts/NetworkRequestTest.java
+++ b/tests/cts/net/src/android/net/cts/NetworkRequestTest.java
@@ -32,6 +32,7 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assume.assumeTrue;
@@ -309,6 +310,43 @@
                 request.networkCapabilities.satisfiedByNetworkCapabilities(nc));
     }
 
+    private static Set<Range<Integer>> uidRangesForUid(int uid) {
+        final Range<Integer> range = new Range<>(uid, uid);
+        return Set.of(range);
+    }
+
+    @Test
+    public void testSetIncludeOtherUidNetworks() throws Exception {
+        assumeTrue(TestUtils.shouldTestSApis());
+        final NetworkRequestShim shim = NetworkRequestShimImpl.newInstance();
+
+        final NetworkRequest.Builder builder = new NetworkRequest.Builder();
+        // NetworkRequests have NET_CAPABILITY_NOT_VCN_MANAGED by default.
+        builder.removeCapability(ConstantsShim.NET_CAPABILITY_NOT_VCN_MANAGED);
+        shim.setIncludeOtherUidNetworks(builder, false);
+        final NetworkRequest request = builder.build();
+
+        final NetworkRequest.Builder otherUidsBuilder = new NetworkRequest.Builder();
+        otherUidsBuilder.removeCapability(ConstantsShim.NET_CAPABILITY_NOT_VCN_MANAGED);
+        shim.setIncludeOtherUidNetworks(otherUidsBuilder, true);
+        final NetworkRequest otherUidsRequest = otherUidsBuilder.build();
+
+        assertNotEquals(Process.SYSTEM_UID, Process.myUid());
+        final NetworkCapabilities ncWithMyUid = new NetworkCapabilities()
+                .setUids(uidRangesForUid(Process.myUid()));
+        final NetworkCapabilities ncWithOtherUid = new NetworkCapabilities()
+                .setUids(uidRangesForUid(Process.SYSTEM_UID));
+
+        assertTrue(request + " should be satisfied by " + ncWithMyUid,
+                request.canBeSatisfiedBy(ncWithMyUid));
+        assertTrue(otherUidsRequest + " should be satisfied by " + ncWithMyUid,
+                otherUidsRequest.canBeSatisfiedBy(ncWithMyUid));
+        assertFalse(request + " should not be satisfied by " +  ncWithOtherUid,
+                request.canBeSatisfiedBy(ncWithOtherUid));
+        assertTrue(otherUidsRequest + " should be satisfied by " + ncWithOtherUid,
+                otherUidsRequest.canBeSatisfiedBy(ncWithOtherUid));
+    }
+
     @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
     public void testRequestorUid() {
         final NetworkCapabilities nc = new NetworkCapabilities();