From 9252b34065809731ea2f6d3ffad91f678f809c93 Mon Sep 17 00:00:00 2001 From: Jeff Sharkey Date: Fri, 19 Jan 2018 07:58:35 +0900 Subject: [PATCH] Use data plans for better job scheduling. Now that we have data plan information from the carrier, we can start using it to influence when we schedule jobs. As a first pass algorithm: -- If the network is congested, and a job is less than 50% through its runnable window, then we'll defer it for awhile. -- If the network has a surplus of data, we'll consider using some of it to improve the user experience by running prefetching jobs. Provider APIs for carrier apps to override their connections to be temporarily marked as either "unmetered" or "congested", along with automatic timeouts if desired. Flag for developers to indicate which jobs will have a material positive impact on end users. (We don't want to promote jobs that are simply doing logs upload; for example.) Glue code to quickly return targetSdk of a specific package. More tweaking to the exact algorithms will come in future CLs. Test: bit FrameworksServicesTests:com.android.server.job. Bug: 64133169 Change-Id: Iabb9f90a7a65958ad648b091edec378fc3bf785a --- api/current.txt | 1 + api/system-current.txt | 2 + core/java/android/app/job/JobInfo.java | 27 +++ .../content/pm/PackageManagerInternal.java | 5 + .../android/net/INetworkPolicyManager.aidl | 1 + .../controllers/ConnectivityController.java | 115 ++++++++---- .../server/job/controllers/JobStatus.java | 53 +++++- .../net/NetworkPolicyManagerInternal.java | 19 ++ .../net/NetworkPolicyManagerService.java | 133 +++++++++++++- .../server/pm/PackageManagerService.java | 15 ++ .../com/android/server/job/JobStoreTest.java | 13 ++ .../ConnectivityControllerTest.java | 169 ++++++++++++++++++ .../server/job/controllers/JobStatusTest.java | 76 ++++++++ .../telephony/SubscriptionManager.java | 75 +++++++- 14 files changed, 667 insertions(+), 37 deletions(-) create mode 100644 services/tests/servicestests/src/com/android/server/job/controllers/ConnectivityControllerTest.java create mode 100644 services/tests/servicestests/src/com/android/server/job/controllers/JobStatusTest.java diff --git a/api/current.txt b/api/current.txt index 5515b010a4c39..85c7d9ddd1356 100644 --- a/api/current.txt +++ b/api/current.txt @@ -6992,6 +6992,7 @@ package android.app.job { 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 87cc6b5fd6b7c..f35984aaf8d1d 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -4381,6 +4381,8 @@ package android.telephony { public class SubscriptionManager { method public java.util.List getSubscriptionPlans(int); + method public void setSubscriptionOverrideCongested(int, boolean, long); + method public void setSubscriptionOverrideUnmetered(int, boolean, long); method public void setSubscriptionPlans(int, java.util.List); 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 7c40b4eaf3757..cba9dcc3c4cc8 100644 --- a/core/java/android/app/job/JobInfo.java +++ b/core/java/android/app/job/JobInfo.java @@ -250,6 +250,11 @@ public class JobInfo implements Parcelable { */ public static final int FLAG_IMPORTANT_WHILE_FOREGROUND = 1 << 1; + /** + * @hide + */ + public static final int FLAG_IS_PREFETCH = 1 << 2; + /** * @hide */ @@ -1363,6 +1368,28 @@ public class JobInfo implements Parcelable { return this; } + /** + * 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. + *

+ * 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. * diff --git a/core/java/android/content/pm/PackageManagerInternal.java b/core/java/android/content/pm/PackageManagerInternal.java index 2c45b8d8b30e1..6f093ba8d005c 100644 --- a/core/java/android/content/pm/PackageManagerInternal.java +++ b/core/java/android/content/pm/PackageManagerInternal.java @@ -436,6 +436,11 @@ public abstract class PackageManagerInternal { */ 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 7e37432fb06a6..476e2f43649ff 100644 --- a/core/java/android/net/INetworkPolicyManager.aidl +++ b/core/java/android/net/INetworkPolicyManager.aidl @@ -71,6 +71,7 @@ interface INetworkPolicyManager { 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 5a30eb4428f07..373d87d971b86 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.Slog; 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 @@ public final class ConnectivityController extends StateController implements private final ArraySet 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 @@ public final class ConnectivityController extends StateController implements } /** - * 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. *

* 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 @@ public final class ConnectivityController extends StateController implements // 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 @@ public final class ConnectivityController extends StateController implements 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 1add1ca95f375..59529e0c9f109 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.JobInfo; 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 @@ public final class JobStatus { 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 @@ public final class JobStatus { 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 @@ public final class JobStatus { /** 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 @@ public final class JobStatus { long earliestRunTimeElapsedMillis, long latestRunTimeElapsedMillis, long lastSuccessfulRunTime, long lastFailedRunTime, Pair persistedExecutionTimesUTC) { - this(job, callingUid, sourcePkgName, sourceUserId, + this(job, callingUid, resolveTargetSdkVersion(job), sourcePkgName, sourceUserId, standbyBucket, baseHeartbeat, sourceTag, 0, earliestRunTimeElapsedMillis, latestRunTimeElapsedMillis, @@ -357,7 +360,7 @@ public final class JobStatus { 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 @@ public final class JobStatus { 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 @@ public final class JobStatus { return job.getId(); } + public int getTargetSdkVersion() { + return targetSdkVersion; + } + public void printUniqueId(PrintWriter pw) { UserHandle.formatUid(pw, callingUid); pw.print("/"); @@ -715,6 +722,37 @@ public final class JobStatus { return latestRunTimeElapsedMillis; } + /** + * Return the fractional position of "now" within the "run time" window of + * this job. + *

+ * 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. + *

+ * 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 getPersistedUtcTimes() { return mPersistedUtcTimes; } @@ -1093,6 +1131,11 @@ public final class JobStatus { } } + 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 7934a96824267..e458f485c9ab8 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 @@ public abstract class NetworkPolicyManagerInternal { * @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 ff9b2fd4ae2d1..a06b11a410244 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_ENABLED 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.net.TrafficStats.MB_IN_BYTES; 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.NetworkPolicy; 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.text.format.Formatter; 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.Slog; 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.nio.charset.StandardCharsets; 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 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { 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 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { @GuardedBy("mNetworkPoliciesSecondLock") final SparseArray 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 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { @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 mListeners = new RemoteCallbackList<>(); @@ -1504,8 +1518,10 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { // First, generate identities of all connected networks so we can // quickly compare them against all defined policies below. + mNetIdToSubId.clear(); final ArrayMap 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 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { } 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 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(); @@ -2814,6 +2866,27 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { } } + @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 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { } } + 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 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { 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 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { 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 dd374fe76b2a6..2585cf3d7fa5b 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 @@ Slog.e("TODD", 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) { @@ -23418,6 +23426,13 @@ Slog.v(TAG, ":: stepped forward, applying functor at tag " + parser.getName()); } } + @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 d8e3be951bd0c..43d026d8efc37 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 android.net.NetworkCapabilities.TRANSPORT_WIFI; 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 android.util.Pair; 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 @@ public class JobStoreTest { 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 0000000000000..f6a749df1df6a --- /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 0000000000000..15c24ac7efd68 --- /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 423dc80af54db..14060935f4ff7 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.pm.PackageManager; 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.RemoteException; 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; @@ -1737,6 +1741,75 @@ public class SubscriptionManager { } } + /** + * 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}. + *

+ * This method is only accessible to the following narrow set of apps: + *

    + *
  • The carrier app for this subscriberId, as determined by + * {@link TelephonyManager#hasCarrierPrivileges()}. + *
  • The carrier app explicitly delegated access through + * {@link CarrierConfigManager#KEY_CONFIG_PLANS_PACKAGE_OVERRIDE_STRING}. + *
+ * + * @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. + *

+ * This method is only accessible to the following narrow set of apps: + *

    + *
  • The carrier app for this subscriberId, as determined by + * {@link TelephonyManager#hasCarrierPrivileges()}. + *
  • The carrier app explicitly delegated access through + * {@link CarrierConfigManager#KEY_CONFIG_PLANS_PACKAGE_OVERRIDE_STRING}. + *
+ * + * @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