Merge "Use data plans for better job scheduling."
diff --git a/api/current.txt b/api/current.txt
index b06e52e..c065fe9 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -6992,6 +6992,7 @@
     method public android.app.job.JobInfo.Builder setEstimatedNetworkBytes(long);
     method public android.app.job.JobInfo.Builder setExtras(android.os.PersistableBundle);
     method public android.app.job.JobInfo.Builder setImportantWhileForeground(boolean);
+    method public android.app.job.JobInfo.Builder setIsPrefetch(boolean);
     method public android.app.job.JobInfo.Builder setMinimumLatency(long);
     method public android.app.job.JobInfo.Builder setOverrideDeadline(long);
     method public android.app.job.JobInfo.Builder setPeriodic(long);
diff --git a/api/system-current.txt b/api/system-current.txt
index 87cc6b5..f35984a 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -4381,6 +4381,8 @@
 
   public class SubscriptionManager {
     method public java.util.List<android.telephony.SubscriptionPlan> getSubscriptionPlans(int);
+    method public void setSubscriptionOverrideCongested(int, boolean, long);
+    method public void setSubscriptionOverrideUnmetered(int, boolean, long);
     method public void setSubscriptionPlans(int, java.util.List<android.telephony.SubscriptionPlan>);
     field public static final java.lang.String ACTION_MANAGE_SUBSCRIPTION_PLANS = "android.telephony.action.MANAGE_SUBSCRIPTION_PLANS";
     field public static final java.lang.String ACTION_REFRESH_SUBSCRIPTION_PLANS = "android.telephony.action.REFRESH_SUBSCRIPTION_PLANS";
diff --git a/core/java/android/app/job/JobInfo.java b/core/java/android/app/job/JobInfo.java
index 7c40b4e..cba9dcc 100644
--- a/core/java/android/app/job/JobInfo.java
+++ b/core/java/android/app/job/JobInfo.java
@@ -253,6 +253,11 @@
     /**
      * @hide
      */
+    public static final int FLAG_IS_PREFETCH = 1 << 2;
+
+    /**
+     * @hide
+     */
     public static final int CONSTRAINT_FLAG_CHARGING = 1 << 0;
 
     /**
@@ -1364,6 +1369,28 @@
         }
 
         /**
+         * Setting this to true indicates that this job is designed to prefetch
+         * content that will make a material improvement to the experience of
+         * the specific user of this device. For example, fetching top headlines
+         * of interest to the current user.
+         * <p>
+         * The system may use this signal to relax the network constraints you
+         * originally requested, such as allowing a
+         * {@link JobInfo#NETWORK_TYPE_UNMETERED} job to run over a metered
+         * network when there is a surplus of metered data available. The system
+         * may also use this signal in combination with end user usage patterns
+         * to ensure data is prefetched before the user launches your app.
+         */
+        public Builder setIsPrefetch(boolean isPrefetch) {
+            if (isPrefetch) {
+                mFlags |= FLAG_IS_PREFETCH;
+            } else {
+                mFlags &= (~FLAG_IS_PREFETCH);
+            }
+            return this;
+        }
+
+        /**
          * Set whether or not to persist this job across device reboots.
          *
          * @param isPersisted True to indicate that the job will be written to
diff --git a/core/java/android/content/pm/PackageManagerInternal.java b/core/java/android/content/pm/PackageManagerInternal.java
index 2c45b8d..6f093ba 100644
--- a/core/java/android/content/pm/PackageManagerInternal.java
+++ b/core/java/android/content/pm/PackageManagerInternal.java
@@ -436,6 +436,11 @@
      */
     public abstract int getUidTargetSdkVersion(int uid);
 
+    /**
+     * Return the taget SDK version for the app with the given package name.
+     */
+    public abstract int getPackageTargetSdkVersion(String packageName);
+
     /** Whether the binder caller can access instant apps. */
     public abstract boolean canAccessInstantApps(int callingUid, int userId);
 
diff --git a/core/java/android/net/INetworkPolicyManager.aidl b/core/java/android/net/INetworkPolicyManager.aidl
index 7e37432..476e2f4 100644
--- a/core/java/android/net/INetworkPolicyManager.aidl
+++ b/core/java/android/net/INetworkPolicyManager.aidl
@@ -71,6 +71,7 @@
     SubscriptionPlan[] getSubscriptionPlans(int subId, String callingPackage);
     void setSubscriptionPlans(int subId, in SubscriptionPlan[] plans, String callingPackage);
     String getSubscriptionPlansOwner(int subId);
+    void setSubscriptionOverride(int subId, int overrideMask, int overrideValue, long timeoutMillis, String callingPackage);
 
     void factoryReset(String subscriber);
 
diff --git a/services/core/java/com/android/server/job/controllers/ConnectivityController.java b/services/core/java/com/android/server/job/controllers/ConnectivityController.java
index 5a30eb4..373d87d 100644
--- a/services/core/java/com/android/server/job/controllers/ConnectivityController.java
+++ b/services/core/java/com/android/server/job/controllers/ConnectivityController.java
@@ -16,6 +16,10 @@
 
 package com.android.server.job.controllers;
 
+import static android.net.NetworkCapabilities.LINK_BANDWIDTH_UNSPECIFIED;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED;
+
 import android.app.job.JobInfo;
 import android.content.Context;
 import android.net.ConnectivityManager;
@@ -35,6 +39,7 @@
 import android.util.proto.ProtoOutputStream;
 
 import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.job.JobSchedulerService;
 import com.android.server.job.JobServiceContext;
 import com.android.server.job.StateChangedListener;
@@ -62,15 +67,15 @@
     private final ArraySet<JobStatus> mTrackedJobs = new ArraySet<>();
 
     /** Singleton. */
-    private static ConnectivityController mSingleton;
+    private static ConnectivityController sSingleton;
     private static Object sCreationLock = new Object();
 
     public static ConnectivityController get(JobSchedulerService jms) {
         synchronized (sCreationLock) {
-            if (mSingleton == null) {
-                mSingleton = new ConnectivityController(jms, jms.getContext(), jms.getLock());
+            if (sSingleton == null) {
+                sSingleton = new ConnectivityController(jms, jms.getContext(), jms.getLock());
             }
-            return mSingleton;
+            return sSingleton;
         }
     }
 
@@ -105,37 +110,29 @@
     }
 
     /**
-     * Test to see if running the given job on the given network is sane.
+     * Test to see if running the given job on the given network is insane.
      * <p>
      * For example, if a job is trying to send 10MB over a 128Kbps EDGE
      * connection, it would take 10.4 minutes, and has no chance of succeeding
      * before the job times out, so we'd be insane to try running it.
      */
-    private boolean isSane(JobStatus jobStatus, NetworkCapabilities capabilities) {
+    @SuppressWarnings("unused")
+    private static boolean isInsane(JobStatus jobStatus, Network network,
+            NetworkCapabilities capabilities) {
         final long estimatedBytes = jobStatus.getEstimatedNetworkBytes();
         if (estimatedBytes == JobInfo.NETWORK_BYTES_UNKNOWN) {
             // We don't know how large the job is; cross our fingers!
-            return true;
-        }
-        if (capabilities == null) {
-            // We don't know what the network is like; cross our fingers!
-            return true;
+            return false;
         }
 
         // We don't ask developers to differentiate between upstream/downstream
         // in their size estimates, so test against the slowest link direction.
-        final long downstream = capabilities.getLinkDownstreamBandwidthKbps();
-        final long upstream = capabilities.getLinkUpstreamBandwidthKbps();
-        final long slowest;
-        if (downstream > 0 && upstream > 0) {
-            slowest = Math.min(downstream, upstream);
-        } else if (downstream > 0) {
-            slowest = downstream;
-        } else if (upstream > 0) {
-            slowest = upstream;
-        } else {
+        final long slowest = NetworkCapabilities.minBandwidth(
+                capabilities.getLinkDownstreamBandwidthKbps(),
+                capabilities.getLinkUpstreamBandwidthKbps());
+        if (slowest == LINK_BANDWIDTH_UNSPECIFIED) {
             // We don't know what the network is like; cross our fingers!
-            return true;
+            return false;
         }
 
         final long estimatedMillis = ((estimatedBytes * DateUtils.SECOND_IN_MILLIS)
@@ -144,28 +141,87 @@
             // If we'd never finish before the timeout, we'd be insane!
             Slog.w(TAG, "Estimated " + estimatedBytes + " bytes over " + slowest
                     + " kbps network would take " + estimatedMillis + "ms; that's insane!");
-            return false;
-        } else {
             return true;
+        } else {
+            return false;
         }
     }
 
+    @SuppressWarnings("unused")
+    private static boolean isCongestionDelayed(JobStatus jobStatus, Network network,
+            NetworkCapabilities capabilities) {
+        // If network is congested, and job is less than 50% through the
+        // developer-requested window, then we're okay delaying the job.
+        if (!capabilities.hasCapability(NET_CAPABILITY_NOT_CONGESTED)) {
+            return jobStatus.getFractionRunTime() < 0.5;
+        } else {
+            return false;
+        }
+    }
+
+    @SuppressWarnings("unused")
+    private static boolean isStrictSatisfied(JobStatus jobStatus, Network network,
+            NetworkCapabilities capabilities) {
+        return jobStatus.getJob().getRequiredNetwork().networkCapabilities
+                .satisfiedByNetworkCapabilities(capabilities);
+    }
+
+    @SuppressWarnings("unused")
+    private static boolean isRelaxedSatisfied(JobStatus jobStatus, Network network,
+            NetworkCapabilities capabilities) {
+        // Only consider doing this for prefetching jobs
+        if ((jobStatus.getJob().getFlags() & JobInfo.FLAG_IS_PREFETCH) == 0) {
+            return false;
+        }
+
+        // See if we match after relaxing any unmetered request
+        final NetworkCapabilities relaxed = new NetworkCapabilities(
+                jobStatus.getJob().getRequiredNetwork().networkCapabilities)
+                        .removeCapability(NET_CAPABILITY_NOT_METERED);
+        if (relaxed.satisfiedByNetworkCapabilities(capabilities)) {
+            // TODO: treat this as "maybe" response; need to check quotas
+            return jobStatus.getFractionRunTime() > 0.5;
+        } else {
+            return false;
+        }
+    }
+
+    @VisibleForTesting
+    static boolean isSatisfied(JobStatus jobStatus, Network network,
+            NetworkCapabilities capabilities) {
+        // Zeroth, we gotta have a network to think about being satisfied
+        if (network == null || capabilities == null) return false;
+
+        // First, are we insane?
+        if (isInsane(jobStatus, network, capabilities)) return false;
+
+        // Second, is the network congested?
+        if (isCongestionDelayed(jobStatus, network, capabilities)) return false;
+
+        // Third, is the network a strict match?
+        if (isStrictSatisfied(jobStatus, network, capabilities)) return true;
+
+        // Third, is the network a relaxed match?
+        if (isRelaxedSatisfied(jobStatus, network, capabilities)) return true;
+
+        return false;
+    }
+
     private boolean updateConstraintsSatisfied(JobStatus jobStatus) {
         // TODO: consider matching against non-active networks
 
         final int jobUid = jobStatus.getSourceUid();
         final boolean ignoreBlocked = (jobStatus.getFlags() & JobInfo.FLAG_WILL_BE_FOREGROUND) != 0;
+
         final Network network = mConnManager.getActiveNetworkForUid(jobUid, ignoreBlocked);
         final NetworkInfo info = mConnManager.getNetworkInfoForUid(network, jobUid, ignoreBlocked);
         final NetworkCapabilities capabilities = mConnManager.getNetworkCapabilities(network);
 
         final boolean connected = (info != null) && info.isConnected();
-        final boolean satisfied = jobStatus.getJob().getRequiredNetwork().networkCapabilities
-                .satisfiedByNetworkCapabilities(capabilities);
-        final boolean sane = isSane(jobStatus, capabilities);
+        final boolean satisfied = isSatisfied(jobStatus, network, capabilities);
 
         final boolean changed = jobStatus
-                .setConnectivityConstraintSatisfied(connected && satisfied && sane);
+                .setConnectivityConstraintSatisfied(connected && satisfied);
 
         // Pass along the evaluated network for job to use; prevents race
         // conditions as default routes change over time, and opens the door to
@@ -181,8 +237,7 @@
         if (DEBUG) {
             Slog.i(TAG, "Connectivity " + (changed ? "CHANGED" : "unchanged")
                     + " for " + jobStatus + ": connected=" + connected
-                    + " satisfied=" + satisfied
-                    + " sane=" + sane);
+                    + " satisfied=" + satisfied);
         }
         return changed;
     }
diff --git a/services/core/java/com/android/server/job/controllers/JobStatus.java b/services/core/java/com/android/server/job/controllers/JobStatus.java
index 1add1ca..59529e0 100644
--- a/services/core/java/com/android/server/job/controllers/JobStatus.java
+++ b/services/core/java/com/android/server/job/controllers/JobStatus.java
@@ -24,6 +24,7 @@
 import android.app.job.JobWorkItem;
 import android.content.ClipData;
 import android.content.ComponentName;
+import android.content.pm.PackageManagerInternal;
 import android.net.Network;
 import android.net.Uri;
 import android.os.RemoteException;
@@ -96,6 +97,7 @@
     final JobInfo job;
     /** Uid of the package requesting this job. */
     final int callingUid;
+    final int targetSdkVersion;
     final String batteryName;
 
     final String sourcePackageName;
@@ -243,12 +245,13 @@
         return callingUid;
     }
 
-    private JobStatus(JobInfo job, int callingUid, String sourcePackageName,
+    private JobStatus(JobInfo job, int callingUid, int targetSdkVersion, String sourcePackageName,
             int sourceUserId, int standbyBucket, long heartbeat, String tag, int numFailures,
             long earliestRunTimeElapsedMillis, long latestRunTimeElapsedMillis,
             long lastSuccessfulRunTime, long lastFailedRunTime) {
         this.job = job;
         this.callingUid = callingUid;
+        this.targetSdkVersion = targetSdkVersion;
         this.standbyBucket = standbyBucket;
         this.baseHeartbeat = heartbeat;
 
@@ -307,7 +310,7 @@
     /** Copy constructor: used specifically when cloning JobStatus objects for persistence,
      *   so we preserve RTC window bounds if the source object has them. */
     public JobStatus(JobStatus jobStatus) {
-        this(jobStatus.getJob(), jobStatus.getUid(),
+        this(jobStatus.getJob(), jobStatus.getUid(), jobStatus.targetSdkVersion,
                 jobStatus.getSourcePackageName(), jobStatus.getSourceUserId(),
                 jobStatus.getStandbyBucket(), jobStatus.getBaseHeartbeat(),
                 jobStatus.getSourceTag(), jobStatus.getNumFailures(),
@@ -334,7 +337,7 @@
             long earliestRunTimeElapsedMillis, long latestRunTimeElapsedMillis,
             long lastSuccessfulRunTime, long lastFailedRunTime,
             Pair<Long, Long> persistedExecutionTimesUTC) {
-        this(job, callingUid, sourcePkgName, sourceUserId,
+        this(job, callingUid, resolveTargetSdkVersion(job), sourcePkgName, sourceUserId,
                 standbyBucket, baseHeartbeat,
                 sourceTag, 0,
                 earliestRunTimeElapsedMillis, latestRunTimeElapsedMillis,
@@ -357,7 +360,7 @@
             long newEarliestRuntimeElapsedMillis,
             long newLatestRuntimeElapsedMillis, int backoffAttempt,
             long lastSuccessfulRunTime, long lastFailedRunTime) {
-        this(rescheduling.job, rescheduling.getUid(),
+        this(rescheduling.job, rescheduling.getUid(), resolveTargetSdkVersion(rescheduling.job),
                 rescheduling.getSourcePackageName(), rescheduling.getSourceUserId(),
                 rescheduling.getStandbyBucket(), newBaseHeartbeat,
                 rescheduling.getSourceTag(), backoffAttempt, newEarliestRuntimeElapsedMillis,
@@ -394,7 +397,7 @@
         long currentHeartbeat = js != null
                 ? js.baseHeartbeatForApp(jobPackage, sourceUserId, standbyBucket)
                 : 0;
-        return new JobStatus(job, callingUid, sourcePkg, sourceUserId,
+        return new JobStatus(job, callingUid, resolveTargetSdkVersion(job), sourcePkg, sourceUserId,
                 standbyBucket, currentHeartbeat, tag, 0,
                 earliestRunTimeElapsedMillis, latestRunTimeElapsedMillis,
                 0 /* lastSuccessfulRunTime */, 0 /* lastFailedRunTime */);
@@ -541,6 +544,10 @@
         return job.getId();
     }
 
+    public int getTargetSdkVersion() {
+        return targetSdkVersion;
+    }
+
     public void printUniqueId(PrintWriter pw) {
         UserHandle.formatUid(pw, callingUid);
         pw.print("/");
@@ -715,6 +722,37 @@
         return latestRunTimeElapsedMillis;
     }
 
+    /**
+     * Return the fractional position of "now" within the "run time" window of
+     * this job.
+     * <p>
+     * For example, if the earliest run time was 10 minutes ago, and the latest
+     * run time is 30 minutes from now, this would return 0.25.
+     * <p>
+     * If the job has no window defined, returns 1. When only an earliest or
+     * latest time is defined, it's treated as an infinitely small window at
+     * that time.
+     */
+    public float getFractionRunTime() {
+        final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
+        if (earliestRunTimeElapsedMillis == 0 && latestRunTimeElapsedMillis == Long.MAX_VALUE) {
+            return 1;
+        } else if (earliestRunTimeElapsedMillis == 0) {
+            return now >= latestRunTimeElapsedMillis ? 1 : 0;
+        } else if (latestRunTimeElapsedMillis == Long.MAX_VALUE) {
+            return now >= earliestRunTimeElapsedMillis ? 1 : 0;
+        } else {
+            if (now <= earliestRunTimeElapsedMillis) {
+                return 0;
+            } else if (now >= latestRunTimeElapsedMillis) {
+                return 1;
+            } else {
+                return (float) (now - earliestRunTimeElapsedMillis)
+                        / (float) (latestRunTimeElapsedMillis - earliestRunTimeElapsedMillis);
+            }
+        }
+    }
+
     public Pair<Long, Long> getPersistedUtcTimes() {
         return mPersistedUtcTimes;
     }
@@ -1093,6 +1131,11 @@
         }
     }
 
+    private static int resolveTargetSdkVersion(JobInfo job) {
+        return LocalServices.getService(PackageManagerInternal.class)
+                .getPackageTargetSdkVersion(job.getService().getPackageName());
+    }
+
     // Dumpsys infrastructure
     public void dump(PrintWriter pw, String prefix, boolean full, long elapsedRealtimeMillis) {
         pw.print(prefix); UserHandle.formatUid(pw, callingUid);
diff --git a/services/core/java/com/android/server/net/NetworkPolicyManagerInternal.java b/services/core/java/com/android/server/net/NetworkPolicyManagerInternal.java
index 7934a96..e458f48 100644
--- a/services/core/java/com/android/server/net/NetworkPolicyManagerInternal.java
+++ b/services/core/java/com/android/server/net/NetworkPolicyManagerInternal.java
@@ -16,6 +16,9 @@
 
 package com.android.server.net;
 
+import android.net.Network;
+import android.telephony.SubscriptionPlan;
+
 /**
  * Network Policy Manager local system service interface.
  *
@@ -47,4 +50,20 @@
      * @param added Denotes whether the {@param appId} has been added or removed from the whitelist.
      */
     public abstract void onTempPowerSaveWhitelistChange(int appId, boolean added);
+
+    /**
+     * Return the active {@link SubscriptionPlan} for the given network.
+     */
+    public abstract SubscriptionPlan getSubscriptionPlan(Network network);
+
+    public static final int QUOTA_TYPE_JOBS = 1;
+    public static final int QUOTA_TYPE_MULTIPATH = 2;
+
+    /**
+     * Return the daily quota (in bytes) that can be opportunistically used on
+     * the given network to improve the end user experience. It's called
+     * "opportunistic" because it's traffic that would typically not use the
+     * given network.
+     */
+    public abstract long getSubscriptionOpportunisticQuota(Network network, int quotaType);
 }
diff --git a/services/core/java/com/android/server/net/NetworkPolicyManagerService.java b/services/core/java/com/android/server/net/NetworkPolicyManagerService.java
index ff9b2fd..a06b11a 100644
--- a/services/core/java/com/android/server/net/NetworkPolicyManagerService.java
+++ b/services/core/java/com/android/server/net/NetworkPolicyManagerService.java
@@ -34,6 +34,7 @@
 import static android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_WHITELISTED;
 import static android.net.ConnectivityManager.TYPE_MOBILE;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED;
+import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
 import static android.net.NetworkPolicy.LIMIT_DISABLED;
 import static android.net.NetworkPolicy.SNOOZE_NEVER;
 import static android.net.NetworkPolicy.WARNING_DISABLED;
@@ -69,6 +70,7 @@
 import static android.telephony.CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED;
 import static android.telephony.CarrierConfigManager.DATA_CYCLE_THRESHOLD_DISABLED;
 import static android.telephony.CarrierConfigManager.DATA_CYCLE_USE_PLATFORM_DEFAULT;
+import static android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID;
 import static android.text.format.DateUtils.DAY_IN_MILLIS;
 
 import static com.android.internal.util.ArrayUtils.appendInt;
@@ -134,8 +136,10 @@
 import android.net.NetworkPolicyManager;
 import android.net.NetworkQuotaInfo;
 import android.net.NetworkRequest;
+import android.net.NetworkSpecifier;
 import android.net.NetworkState;
 import android.net.NetworkTemplate;
+import android.net.StringNetworkSpecifier;
 import android.net.TrafficStats;
 import android.net.wifi.WifiConfiguration;
 import android.net.wifi.WifiManager;
@@ -174,6 +178,7 @@
 import android.util.ArrayMap;
 import android.util.ArraySet;
 import android.util.AtomicFile;
+import android.util.DataUnit;
 import android.util.Log;
 import android.util.NtpTrustedTime;
 import android.util.Pair;
@@ -182,6 +187,7 @@
 import android.util.SparseArray;
 import android.util.SparseBooleanArray;
 import android.util.SparseIntArray;
+import android.util.SparseLongArray;
 import android.util.TrustedTime;
 import android.util.Xml;
 
@@ -219,7 +225,6 @@
 import java.time.ZoneId;
 import java.time.ZonedDateTime;
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.Calendar;
 import java.util.List;
 import java.util.Objects;
@@ -332,6 +337,7 @@
     private static final int MSG_REMOVE_INTERFACE_QUOTA = 11;
     private static final int MSG_POLICIES_CHANGED = 13;
     private static final int MSG_RESET_FIREWALL_RULES_BY_UID = 15;
+    private static final int MSG_SUBSCRIPTION_OVERRIDE = 16;
 
     private static final int UID_MSG_STATE_CHANGED = 100;
     private static final int UID_MSG_GONE = 101;
@@ -384,6 +390,10 @@
     @GuardedBy("mNetworkPoliciesSecondLock")
     final SparseArray<String> mSubscriptionPlansOwner = new SparseArray<>();
 
+    /** Map from subId to daily opportunistic quota. */
+    @GuardedBy("mNetworkPoliciesSecondLock")
+    final SparseLongArray mSubscriptionOpportunisticQuota = new SparseLongArray();
+
     /** Defined UID policies. */
     @GuardedBy("mUidRulesFirstLock") final SparseIntArray mUidPolicy = new SparseIntArray();
     /** Currently derived rules for each UID. */
@@ -453,6 +463,10 @@
     @GuardedBy("mNetworkPoliciesSecondLock")
     private final SparseBooleanArray mNetworkMetered = new SparseBooleanArray();
 
+    /** Map from netId to subId as of last update */
+    @GuardedBy("mNetworkPoliciesSecondLock")
+    private final SparseIntArray mNetIdToSubId = new SparseIntArray();
+
     private final RemoteCallbackList<INetworkPolicyListener>
             mListeners = new RemoteCallbackList<>();
 
@@ -1504,8 +1518,10 @@
 
         // First, generate identities of all connected networks so we can
         // quickly compare them against all defined policies below.
+        mNetIdToSubId.clear();
         final ArrayMap<NetworkState, NetworkIdentity> identified = new ArrayMap<>();
         for (NetworkState state : states) {
+            mNetIdToSubId.put(state.network.netId, parseSubId(state));
             if (state.networkInfo != null && state.networkInfo.isConnected()) {
                 final NetworkIdentity ident = NetworkIdentity.buildNetworkIdentity(mContext, state);
                 identified.put(state, ident);
@@ -1607,6 +1623,42 @@
         }
         mMeteredIfaces = newMeteredIfaces;
 
+        // Finally, calculate our opportunistic quotas
+        // TODO: add experiments support to disable or tweak ratios
+        mSubscriptionOpportunisticQuota.clear();
+        for (NetworkState state : states) {
+            final int subId = getSubIdLocked(state.network);
+            final SubscriptionPlan[] plans = mSubscriptionPlans.get(subId);
+            final SubscriptionPlan plan = ArrayUtils.isEmpty(plans) ? null : plans[0];
+            if (plan == null) continue;
+
+            // By default assume we have no quota
+            long limitBytes = plan.getDataLimitBytes();
+            long quotaBytes = 0;
+
+            if (limitBytes == SubscriptionPlan.BYTES_UNKNOWN) {
+                // Ignore missing limits
+            } else if (plan.getDataLimitBytes() == SubscriptionPlan.BYTES_UNLIMITED) {
+                // Unlimited data; let's use 20MiB/day (600MiB/month)
+                quotaBytes = DataUnit.MEBIBYTES.toBytes(20);
+            } else {
+                // Limited data; let's only use 10% of remaining budget
+                final Pair<ZonedDateTime, ZonedDateTime> cycle = plans[0].cycleIterator().next();
+                final long start = cycle.first.toInstant().toEpochMilli();
+                final long end = cycle.second.toInstant().toEpochMilli();
+                final long totalBytes = getTotalBytes(
+                        NetworkTemplate.buildTemplateMobileAll(state.subscriberId), start, end);
+                final long remainingBytes = limitBytes - totalBytes;
+                final long remainingDays = Math.min(1, (end - RecurrenceRule.sClock.millis())
+                        / TimeUnit.DAYS.toMillis(1));
+                if (remainingBytes > 0) {
+                    quotaBytes = (remainingBytes / remainingDays) / 10;
+                }
+            }
+
+            mSubscriptionOpportunisticQuota.put(subId, quotaBytes);
+        }
+
         final String[] meteredIfaces = mMeteredIfaces.toArray(new String[mMeteredIfaces.size()]);
         mHandler.obtainMessage(MSG_METERED_IFACES_CHANGED, meteredIfaces).sendToTarget();
 
@@ -2815,6 +2867,27 @@
     }
 
     @Override
+    public void setSubscriptionOverride(int subId, int overrideMask, int overrideValue,
+            long timeoutMillis, String callingPackage) {
+        enforceSubscriptionPlanAccess(subId, Binder.getCallingUid(), callingPackage);
+
+        // We can only override when carrier told us about plans
+        synchronized (mNetworkPoliciesSecondLock) {
+            if (ArrayUtils.isEmpty(mSubscriptionPlans.get(subId))) {
+                throw new IllegalStateException(
+                        "Must provide SubscriptionPlan information before overriding");
+            }
+        }
+
+        mHandler.sendMessage(mHandler.obtainMessage(MSG_SUBSCRIPTION_OVERRIDE,
+                overrideMask, overrideValue, subId));
+        if (timeoutMillis > 0) {
+            mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_SUBSCRIPTION_OVERRIDE,
+                    overrideMask, 0, subId), timeoutMillis);
+        }
+    }
+
+    @Override
     protected void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
         if (!DumpUtils.checkDumpPermission(mContext, TAG, writer)) return;
 
@@ -3819,6 +3892,16 @@
         }
     }
 
+    private void dispatchSubscriptionOverride(INetworkPolicyListener listener, int subId,
+            int overrideMask, int overrideValue) {
+        if (listener != null) {
+            try {
+                listener.onSubscriptionOverride(subId, overrideMask, overrideValue);
+            } catch (RemoteException ignored) {
+            }
+        }
+    }
+
     private final Handler.Callback mHandlerCallback = new Handler.Callback() {
         @Override
         public boolean handleMessage(Message msg) {
@@ -3922,6 +4005,18 @@
                     resetUidFirewallRules(msg.arg1);
                     return true;
                 }
+                case MSG_SUBSCRIPTION_OVERRIDE: {
+                    final int overrideMask = msg.arg1;
+                    final int overrideValue = msg.arg2;
+                    final int subId = (int) msg.obj;
+                    final int length = mListeners.beginBroadcast();
+                    for (int i = 0; i < length; i++) {
+                        final INetworkPolicyListener listener = mListeners.getBroadcastItem(i);
+                        dispatchSubscriptionOverride(listener, subId, overrideMask, overrideValue);
+                    }
+                    mListeners.finishBroadcast();
+                    return true;
+                }
                 default: {
                     return false;
                 }
@@ -4404,6 +4499,42 @@
                 updateRulesForTempWhitelistChangeUL(appId);
             }
         }
+
+        @Override
+        public SubscriptionPlan getSubscriptionPlan(Network network) {
+            synchronized (mNetworkPoliciesSecondLock) {
+                final SubscriptionPlan[] plans = mSubscriptionPlans.get(getSubIdLocked(network));
+                return ArrayUtils.isEmpty(plans) ? null : plans[0];
+            }
+        }
+
+        @Override
+        public long getSubscriptionOpportunisticQuota(Network network, int quotaType) {
+            synchronized (mNetworkPoliciesSecondLock) {
+                // TODO: handle splitting quota between use-cases
+                return mSubscriptionOpportunisticQuota.get(getSubIdLocked(network));
+            }
+        }
+    }
+
+    private int parseSubId(NetworkState state) {
+        // TODO: moved to using a legitimate NetworkSpecifier instead of string parsing
+        int subId = INVALID_SUBSCRIPTION_ID;
+        if (state != null && state.networkCapabilities != null
+                && state.networkCapabilities.hasTransport(TRANSPORT_CELLULAR)) {
+            NetworkSpecifier spec = state.networkCapabilities.getNetworkSpecifier();
+            if (spec instanceof StringNetworkSpecifier) {
+                try {
+                    subId = Integer.parseInt(((StringNetworkSpecifier) spec).specifier);
+                } catch (NumberFormatException e) {
+                }
+            }
+        }
+        return subId;
+    }
+
+    private int getSubIdLocked(Network network) {
+        return mNetIdToSubId.get(network.netId, INVALID_SUBSCRIPTION_ID);
     }
 
     private static boolean hasRule(int uidRules, int rule) {
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index dd374fe..2585cf3 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -18900,6 +18900,14 @@
         return Build.VERSION_CODES.CUR_DEVELOPMENT;
     }
 
+    private int getPackageTargetSdkVersionLockedLPr(String packageName) {
+        final PackageParser.Package p = mPackages.get(packageName);
+        if (p != null) {
+            return p.applicationInfo.targetSdkVersion;
+        }
+        return Build.VERSION_CODES.CUR_DEVELOPMENT;
+    }
+
     @Override
     public void addPreferredActivity(IntentFilter filter, int match,
             ComponentName[] set, ComponentName activity, int userId) {
@@ -23419,6 +23427,13 @@
         }
 
         @Override
+        public int getPackageTargetSdkVersion(String packageName) {
+            synchronized (mPackages) {
+                return getPackageTargetSdkVersionLockedLPr(packageName);
+            }
+        }
+
+        @Override
         public boolean canAccessInstantApps(int callingUid, int userId) {
             return PackageManagerService.this.canViewInstantApps(callingUid, userId);
         }
diff --git a/services/tests/servicestests/src/com/android/server/job/JobStoreTest.java b/services/tests/servicestests/src/com/android/server/job/JobStoreTest.java
index d8e3be9..43d026d 100644
--- a/services/tests/servicestests/src/com/android/server/job/JobStoreTest.java
+++ b/services/tests/servicestests/src/com/android/server/job/JobStoreTest.java
@@ -6,12 +6,17 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
 
 import android.app.job.JobInfo;
 import android.app.job.JobInfo.Builder;
 import android.content.ComponentName;
 import android.content.Context;
+import android.content.pm.PackageManagerInternal;
 import android.net.NetworkRequest;
+import android.os.Build;
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.os.PersistableBundle;
@@ -24,6 +29,7 @@
 
 import com.android.internal.util.HexDump;
 import com.android.server.IoThread;
+import com.android.server.LocalServices;
 import com.android.server.job.JobStore.JobSet;
 import com.android.server.job.controllers.JobStatus;
 
@@ -65,6 +71,13 @@
                 JobStore.initAndGetForTesting(mTestContext, mTestContext.getFilesDir());
         mComponent = new ComponentName(getContext().getPackageName(), StubClass.class.getName());
 
+        // Assume all packages are current SDK
+        final PackageManagerInternal pm = mock(PackageManagerInternal.class);
+        when(pm.getPackageTargetSdkVersion(anyString()))
+                .thenReturn(Build.VERSION_CODES.CUR_DEVELOPMENT);
+        LocalServices.removeServiceForTest(PackageManagerInternal.class);
+        LocalServices.addService(PackageManagerInternal.class, pm);
+
         // Freeze the clocks at this moment in time
         JobSchedulerService.sSystemClock =
                 Clock.fixed(Clock.systemUTC().instant(), ZoneOffset.UTC);
diff --git a/services/tests/servicestests/src/com/android/server/job/controllers/ConnectivityControllerTest.java b/services/tests/servicestests/src/com/android/server/job/controllers/ConnectivityControllerTest.java
new file mode 100644
index 0000000..f6a749d
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/job/controllers/ConnectivityControllerTest.java
@@ -0,0 +1,169 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.job.controllers;
+
+import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import android.app.job.JobInfo;
+import android.content.ComponentName;
+import android.content.pm.PackageManagerInternal;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.os.Build;
+import android.os.SystemClock;
+import android.support.test.runner.AndroidJUnit4;
+import android.util.DataUnit;
+
+import com.android.server.LocalServices;
+import com.android.server.job.JobSchedulerService;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.time.Clock;
+import java.time.ZoneOffset;
+
+@RunWith(AndroidJUnit4.class)
+public class ConnectivityControllerTest {
+    @Before
+    public void setUp() throws Exception {
+        // Assume all packages are current SDK
+        final PackageManagerInternal pm = mock(PackageManagerInternal.class);
+        when(pm.getPackageTargetSdkVersion(anyString()))
+                .thenReturn(Build.VERSION_CODES.CUR_DEVELOPMENT);
+        LocalServices.removeServiceForTest(PackageManagerInternal.class);
+        LocalServices.addService(PackageManagerInternal.class, pm);
+
+        // Freeze the clocks at this moment in time
+        JobSchedulerService.sSystemClock =
+                Clock.fixed(Clock.systemUTC().instant(), ZoneOffset.UTC);
+        JobSchedulerService.sUptimeMillisClock =
+                Clock.fixed(SystemClock.uptimeMillisClock().instant(), ZoneOffset.UTC);
+        JobSchedulerService.sElapsedRealtimeClock =
+                Clock.fixed(SystemClock.elapsedRealtimeClock().instant(), ZoneOffset.UTC);
+    }
+
+    @Test
+    public void testInsane() throws Exception {
+        final Network network = new Network(101);
+        final JobInfo.Builder job = createJob()
+                .setEstimatedNetworkBytes(DataUnit.MEBIBYTES.toBytes(1))
+                .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY);
+
+        // Slow network is too slow
+        assertFalse(ConnectivityController.isSatisfied(createJobStatus(job), network,
+                createCapabilities().setLinkUpstreamBandwidthKbps(1)
+                        .setLinkDownstreamBandwidthKbps(1)));
+        // Fast network looks great
+        assertTrue(ConnectivityController.isSatisfied(createJobStatus(job), network,
+                createCapabilities().setLinkUpstreamBandwidthKbps(1024)
+                        .setLinkDownstreamBandwidthKbps(1024)));
+    }
+
+    @Test
+    public void testCongestion() throws Exception {
+        final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
+        final JobInfo.Builder job = createJob()
+                .setEstimatedNetworkBytes(DataUnit.MEBIBYTES.toBytes(1))
+                .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY);
+        final JobStatus early = createJobStatus(job, now - 1000, now + 2000);
+        final JobStatus late = createJobStatus(job, now - 2000, now + 1000);
+
+        // Uncongested network is whenever
+        {
+            final Network network = new Network(101);
+            final NetworkCapabilities capabilities = createCapabilities()
+                    .addCapability(NET_CAPABILITY_NOT_CONGESTED);
+            assertTrue(ConnectivityController.isSatisfied(early, network, capabilities));
+            assertTrue(ConnectivityController.isSatisfied(late, network, capabilities));
+        }
+
+        // Congested network is more selective
+        {
+            final Network network = new Network(101);
+            final NetworkCapabilities capabilities = createCapabilities();
+            assertFalse(ConnectivityController.isSatisfied(early, network, capabilities));
+            assertTrue(ConnectivityController.isSatisfied(late, network, capabilities));
+        }
+    }
+
+    @Test
+    public void testRelaxed() throws Exception {
+        final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
+        final JobInfo.Builder job = createJob()
+                .setEstimatedNetworkBytes(DataUnit.MEBIBYTES.toBytes(1))
+                .setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED);
+        final JobStatus early = createJobStatus(job, now - 1000, now + 2000);
+        final JobStatus late = createJobStatus(job, now - 2000, now + 1000);
+
+        job.setIsPrefetch(true);
+        final JobStatus earlyPrefetch = createJobStatus(job, now - 1000, now + 2000);
+        final JobStatus latePrefetch = createJobStatus(job, now - 2000, now + 1000);
+
+        // Unmetered network is whenever
+        {
+            final Network network = new Network(101);
+            final NetworkCapabilities capabilities = createCapabilities()
+                    .addCapability(NET_CAPABILITY_NOT_CONGESTED)
+                    .addCapability(NET_CAPABILITY_NOT_METERED);
+            assertTrue(ConnectivityController.isSatisfied(early, network, capabilities));
+            assertTrue(ConnectivityController.isSatisfied(late, network, capabilities));
+            assertTrue(ConnectivityController.isSatisfied(earlyPrefetch, network, capabilities));
+            assertTrue(ConnectivityController.isSatisfied(latePrefetch, network, capabilities));
+        }
+
+        // Metered network is only when prefetching and late
+        {
+            final Network network = new Network(101);
+            final NetworkCapabilities capabilities = createCapabilities()
+                    .addCapability(NET_CAPABILITY_NOT_CONGESTED);
+            assertFalse(ConnectivityController.isSatisfied(early, network, capabilities));
+            assertFalse(ConnectivityController.isSatisfied(late, network, capabilities));
+            assertFalse(ConnectivityController.isSatisfied(earlyPrefetch, network, capabilities));
+            assertTrue(ConnectivityController.isSatisfied(latePrefetch, network, capabilities));
+        }
+    }
+
+    private static NetworkCapabilities createCapabilities() {
+        return new NetworkCapabilities().addCapability(NET_CAPABILITY_INTERNET)
+                .addCapability(NET_CAPABILITY_VALIDATED);
+    }
+
+    private static JobInfo.Builder createJob() {
+        return new JobInfo.Builder(101, new ComponentName("foo", "bar"));
+    }
+
+    private static JobStatus createJobStatus(JobInfo.Builder job) {
+        return createJobStatus(job, 0, Long.MAX_VALUE);
+    }
+
+    private static JobStatus createJobStatus(JobInfo.Builder job, long earliestRunTimeElapsedMillis,
+            long latestRunTimeElapsedMillis) {
+        return new JobStatus(job.build(), 0, null, -1, 0, 0, null, earliestRunTimeElapsedMillis,
+                latestRunTimeElapsedMillis, 0, 0, null);
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/job/controllers/JobStatusTest.java b/services/tests/servicestests/src/com/android/server/job/controllers/JobStatusTest.java
new file mode 100644
index 0000000..15c24ac
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/job/controllers/JobStatusTest.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.job.controllers;
+
+import static org.junit.Assert.assertEquals;
+
+import android.app.job.JobInfo;
+import android.content.ComponentName;
+import android.os.SystemClock;
+import android.support.test.runner.AndroidJUnit4;
+
+import com.android.server.job.JobSchedulerService;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.time.Clock;
+import java.time.ZoneOffset;
+
+@RunWith(AndroidJUnit4.class)
+public class JobStatusTest {
+    private static final double DELTA = 0.00001;
+
+    @Before
+    public void setUp() throws Exception {
+        // Freeze the clocks at this moment in time
+        JobSchedulerService.sSystemClock =
+                Clock.fixed(Clock.systemUTC().instant(), ZoneOffset.UTC);
+        JobSchedulerService.sUptimeMillisClock =
+                Clock.fixed(SystemClock.uptimeMillisClock().instant(), ZoneOffset.UTC);
+        JobSchedulerService.sElapsedRealtimeClock =
+                Clock.fixed(SystemClock.elapsedRealtimeClock().instant(), ZoneOffset.UTC);
+    }
+
+    @Test
+    public void testFraction() throws Exception {
+        final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
+
+        assertEquals(1, createJobStatus(0, Long.MAX_VALUE).getFractionRunTime(), DELTA);
+
+        assertEquals(1, createJobStatus(0, now - 1000).getFractionRunTime(), DELTA);
+        assertEquals(0, createJobStatus(0, now + 1000).getFractionRunTime(), DELTA);
+
+        assertEquals(1, createJobStatus(now - 1000, Long.MAX_VALUE).getFractionRunTime(), DELTA);
+        assertEquals(0, createJobStatus(now + 1000, Long.MAX_VALUE).getFractionRunTime(), DELTA);
+
+        assertEquals(0, createJobStatus(now, now + 2000).getFractionRunTime(), DELTA);
+        assertEquals(0.25, createJobStatus(now - 500, now + 1500).getFractionRunTime(), DELTA);
+        assertEquals(0.5, createJobStatus(now - 1000, now + 1000).getFractionRunTime(), DELTA);
+        assertEquals(0.75, createJobStatus(now - 1500, now + 500).getFractionRunTime(), DELTA);
+        assertEquals(1, createJobStatus(now - 2000, now).getFractionRunTime(), DELTA);
+    }
+
+    private static JobStatus createJobStatus(long earliestRunTimeElapsedMillis,
+            long latestRunTimeElapsedMillis) {
+        final JobInfo job = new JobInfo.Builder(101, new ComponentName("foo", "bar"))
+                .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY).build();
+        return new JobStatus(job, 0, null, -1, 0, 0, null, earliestRunTimeElapsedMillis,
+                latestRunTimeElapsedMillis, 0, 0, null);
+    }
+}
diff --git a/telephony/java/android/telephony/SubscriptionManager.java b/telephony/java/android/telephony/SubscriptionManager.java
index 423dc80..1406093 100644
--- a/telephony/java/android/telephony/SubscriptionManager.java
+++ b/telephony/java/android/telephony/SubscriptionManager.java
@@ -16,6 +16,10 @@
 
 package android.telephony;
 
+import static android.net.NetworkPolicyManager.OVERRIDE_CONGESTED;
+import static android.net.NetworkPolicyManager.OVERRIDE_UNMETERED;
+
+import android.annotation.DurationMillisLong;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.RequiresPermission;
@@ -30,6 +34,7 @@
 import android.content.res.Configuration;
 import android.content.res.Resources;
 import android.net.INetworkPolicyManager;
+import android.net.NetworkCapabilities;
 import android.net.Uri;
 import android.os.Handler;
 import android.os.Looper;
@@ -38,7 +43,6 @@
 import android.os.ServiceManager;
 import android.os.ServiceManager.ServiceNotFoundException;
 import android.util.DisplayMetrics;
-import android.util.Log;
 
 import com.android.internal.telephony.IOnSubscriptionsChangedListener;
 import com.android.internal.telephony.ISub;
@@ -1738,6 +1742,75 @@
     }
 
     /**
+     * Temporarily override the billing relationship plan between a carrier and
+     * a specific subscriber to be considered unmetered. This will be reflected
+     * to apps via {@link NetworkCapabilities#NET_CAPABILITY_NOT_METERED}.
+     * <p>
+     * This method is only accessible to the following narrow set of apps:
+     * <ul>
+     * <li>The carrier app for this subscriberId, as determined by
+     * {@link TelephonyManager#hasCarrierPrivileges()}.
+     * <li>The carrier app explicitly delegated access through
+     * {@link CarrierConfigManager#KEY_CONFIG_PLANS_PACKAGE_OVERRIDE_STRING}.
+     * </ul>
+     *
+     * @param subId the subscriber this override applies to.
+     * @param overrideUnmetered set if the billing relationship should be
+     *            considered unmetered.
+     * @param timeoutMillis the timeout after which the requested override will
+     *            be automatically cleared, or {@code 0} to leave in the
+     *            requested state until explicitly cleared, or the next reboot,
+     *            whichever happens first.
+     * @hide
+     */
+    @SystemApi
+    public void setSubscriptionOverrideUnmetered(int subId, boolean overrideUnmetered,
+            @DurationMillisLong long timeoutMillis) {
+        try {
+            final int overrideValue = overrideUnmetered ? OVERRIDE_UNMETERED : 0;
+            mNetworkPolicy.setSubscriptionOverride(subId, OVERRIDE_UNMETERED, overrideValue,
+                    timeoutMillis, mContext.getOpPackageName());
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Temporarily override the billing relationship plan between a carrier and
+     * a specific subscriber to be considered congested. This will cause the
+     * device to delay certain network requests when possible, such as developer
+     * jobs that are willing to run in a flexible time window.
+     * <p>
+     * This method is only accessible to the following narrow set of apps:
+     * <ul>
+     * <li>The carrier app for this subscriberId, as determined by
+     * {@link TelephonyManager#hasCarrierPrivileges()}.
+     * <li>The carrier app explicitly delegated access through
+     * {@link CarrierConfigManager#KEY_CONFIG_PLANS_PACKAGE_OVERRIDE_STRING}.
+     * </ul>
+     *
+     * @param subId the subscriber this override applies to.
+     * @param overrideCongested set if the subscription should be considered
+     *            congested.
+     * @param timeoutMillis the timeout after which the requested override will
+     *            be automatically cleared, or {@code 0} to leave in the
+     *            requested state until explicitly cleared, or the next reboot,
+     *            whichever happens first.
+     * @hide
+     */
+    @SystemApi
+    public void setSubscriptionOverrideCongested(int subId, boolean overrideCongested,
+            @DurationMillisLong long timeoutMillis) {
+        try {
+            final int overrideValue = overrideCongested ? OVERRIDE_CONGESTED : 0;
+            mNetworkPolicy.setSubscriptionOverride(subId, OVERRIDE_CONGESTED, overrideValue,
+                    timeoutMillis, mContext.getOpPackageName());
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
      * Create an {@link Intent} that can be launched towards the carrier app
      * that is currently defining the billing relationship plan through
      * {@link #setSubscriptionPlans(int, List)}.