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)}.