diff --git a/apex/jobscheduler/framework/java/android/app/job/JobInfo.java b/apex/jobscheduler/framework/java/android/app/job/JobInfo.java
index 3597c27224bba..d16217d5cb411 100644
--- a/apex/jobscheduler/framework/java/android/app/job/JobInfo.java
+++ b/apex/jobscheduler/framework/java/android/app/job/JobInfo.java
@@ -274,12 +274,19 @@ public class JobInfo implements Parcelable {
/**
* This job needs to be exempted from the app standby throttling. Only the system (UID 1000)
- * can set it. Jobs with a time constrant must not have it.
+ * can set it. Jobs with a time constraint must not have it.
*
* @hide
*/
public static final int FLAG_EXEMPT_FROM_APP_STANDBY = 1 << 3;
+ /**
+ * Whether it's a so-called "HPJ" or not.
+ *
+ * @hide
+ */
+ public static final int FLAG_FOREGROUND_JOB = 1 << 4;
+
/**
* @hide
*/
@@ -571,12 +578,20 @@ public class JobInfo implements Parcelable {
/**
* Return the backoff policy of this job.
+ *
* @see JobInfo.Builder#setBackoffCriteria(long, int)
*/
public @BackoffPolicy int getBackoffPolicy() {
return backoffPolicy;
}
+ /**
+ * @see JobInfo.Builder#setForeground(boolean)
+ */
+ public boolean isForegroundJob() {
+ return (flags & FLAG_FOREGROUND_JOB) != 0;
+ }
+
/**
* @see JobInfo.Builder#setImportantWhileForeground(boolean)
*/
@@ -1441,6 +1456,41 @@ public class JobInfo implements Parcelable {
return this;
}
+ /**
+ * Setting this to true indicates that this job is important and needs to run as soon as
+ * possible with stronger guarantees than regular jobs. These "foreground" jobs will:
+ *
+ * - Run as soon as possible
+ * - Be exempted from Doze and battery saver restrictions
+ * - Have network access
+ *
+ *
+ * Since these jobs have stronger guarantees than regular jobs, they will be subject to
+ * stricter quotas. As long as an app has available foreground quota, jobs scheduled with
+ * this set to true will run with these guarantees. If an app has run out of available
+ * foreground quota, any pending foreground jobs will run as regular jobs.
+ * {@link JobParameters#isForegroundJob()} can be used to know whether the executing job
+ * has foreground guarantees or not. In addition, {@link JobScheduler#schedule(JobInfo)}
+ * will immediately return {@link JobScheduler#RESULT_FAILURE} if the app does not have
+ * available quota (and the job will not be successfully scheduled).
+ *
+ * Foreground jobs may only set network constraints. No other constraints are allowed.
+ *
+ * Note: Even though foreground jobs are meant to run as soon as possible, they may be
+ * deferred if the system is under heavy load or the network constraint is satisfied
+ *
+ * @see JobInfo#isForegroundJob()
+ */
+ @NonNull
+ public Builder setForeground(boolean foreground) {
+ if (foreground) {
+ mFlags |= FLAG_FOREGROUND_JOB;
+ } else {
+ mFlags &= (~FLAG_FOREGROUND_JOB);
+ }
+ return this;
+ }
+
/**
* Setting this to true indicates that this job is important while the scheduling app
* is in the foreground or on the temporary whitelist for background restrictions.
@@ -1456,7 +1506,9 @@ public class JobInfo implements Parcelable {
* @param importantWhileForeground whether to relax doze restrictions for this job when the
* app is in the foreground. False by default.
* @see JobInfo#isImportantWhileForeground()
+ * @deprecated Use {@link #setForeground(boolean)} instead.
*/
+ @Deprecated
public Builder setImportantWhileForeground(boolean importantWhileForeground) {
if (importantWhileForeground) {
mFlags |= FLAG_IMPORTANT_WHILE_FOREGROUND;
@@ -1580,6 +1632,29 @@ public class JobInfo implements Parcelable {
throw new IllegalArgumentException(
"An important while foreground job cannot have a time delay");
}
+
+ if ((flags & FLAG_FOREGROUND_JOB) != 0) {
+ if (hasEarlyConstraint) {
+ throw new IllegalArgumentException("A foreground job cannot have a time delay");
+ }
+ if (hasLateConstraint) {
+ throw new IllegalArgumentException("A foreground job cannot have a deadline");
+ }
+ if (isPeriodic) {
+ throw new IllegalArgumentException("A foreground job cannot be periodic");
+ }
+ if (isPersisted) {
+ throw new IllegalArgumentException("A foreground job cannot be persisted");
+ }
+ if (constraintFlags != 0 || (flags & ~FLAG_FOREGROUND_JOB) != 0) {
+ throw new IllegalArgumentException(
+ "A foreground job can only have network constraints");
+ }
+ if (triggerContentUris != null && triggerContentUris.length > 0) {
+ throw new IllegalArgumentException(
+ "Can't call addTriggerContentUri() on a foreground job");
+ }
+ }
}
/**
diff --git a/apex/jobscheduler/framework/java/android/app/job/JobParameters.java b/apex/jobscheduler/framework/java/android/app/job/JobParameters.java
index e251ff00a47d1..a71b8562753ec 100644
--- a/apex/jobscheduler/framework/java/android/app/job/JobParameters.java
+++ b/apex/jobscheduler/framework/java/android/app/job/JobParameters.java
@@ -111,6 +111,9 @@ public class JobParameters implements Parcelable {
@UnsupportedAppUsage
private final IBinder callback;
private final boolean overrideDeadlineExpired;
+ // HPJs = foreground jobs.
+ // TODO(171305774): clean up naming
+ private final boolean mIsHpj;
private final Uri[] mTriggeredContentUris;
private final String[] mTriggeredContentAuthorities;
private final Network network;
@@ -121,7 +124,7 @@ public class JobParameters implements Parcelable {
/** @hide */
public JobParameters(IBinder callback, int jobId, PersistableBundle extras,
Bundle transientExtras, ClipData clipData, int clipGrantFlags,
- boolean overrideDeadlineExpired, Uri[] triggeredContentUris,
+ boolean overrideDeadlineExpired, boolean isHpj, Uri[] triggeredContentUris,
String[] triggeredContentAuthorities, Network network) {
this.jobId = jobId;
this.extras = extras;
@@ -130,6 +133,7 @@ public class JobParameters implements Parcelable {
this.clipGrantFlags = clipGrantFlags;
this.callback = callback;
this.overrideDeadlineExpired = overrideDeadlineExpired;
+ this.mIsHpj = isHpj;
this.mTriggeredContentUris = triggeredContentUris;
this.mTriggeredContentAuthorities = triggeredContentAuthorities;
this.network = network;
@@ -194,6 +198,17 @@ public class JobParameters implements Parcelable {
return clipGrantFlags;
}
+ /**
+ * @return Whether this job is running as a foreground job or not. A job is guaranteed to have
+ * all foreground job guarantees for the duration of the job execution if this returns
+ * {@code true}. This will return {@code false} if the job that wasn't requested to run as a
+ * foreground job, or if it was requested to run as a foreground job but the app didn't have
+ * any remaining foreground job quota at the time of execution.
+ */
+ public boolean isForegroundJob() {
+ return mIsHpj;
+ }
+
/**
* For jobs with {@link android.app.job.JobInfo.Builder#setOverrideDeadline(long)} set, this
* provides an easy way to tell whether the job is being executed due to the deadline
@@ -337,6 +352,7 @@ public class JobParameters implements Parcelable {
}
callback = in.readStrongBinder();
overrideDeadlineExpired = in.readInt() == 1;
+ mIsHpj = in.readBoolean();
mTriggeredContentUris = in.createTypedArray(Uri.CREATOR);
mTriggeredContentAuthorities = in.createStringArray();
if (in.readInt() != 0) {
@@ -373,6 +389,7 @@ public class JobParameters implements Parcelable {
}
dest.writeStrongBinder(callback);
dest.writeInt(overrideDeadlineExpired ? 1 : 0);
+ dest.writeBoolean(mIsHpj);
dest.writeTypedArray(mTriggeredContentUris, flags);
dest.writeStringArray(mTriggeredContentAuthorities);
if (network != null) {
diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java b/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java
index b35a7be1f6894..0feaade2b51d5 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java
@@ -205,7 +205,9 @@ class JobConcurrencyManager {
}
private boolean isFgJob(JobStatus job) {
- return job.lastEvaluatedPriority >= JobInfo.PRIORITY_TOP_APP;
+ // (It's super confusing PRIORITY_BOUND_FOREGROUND_SERVICE isn't FG here)
+ return job.lastEvaluatedPriority >= JobInfo.PRIORITY_TOP_APP
+ || job.shouldTreatAsForegroundJob();
}
@GuardedBy("mLock")
@@ -336,6 +338,8 @@ class JobConcurrencyManager {
continue;
}
+ // TODO(171305774): make sure HPJs aren't pre-empted and add dedicated contexts for them
+
final boolean isPendingFg = isFgJob(nextPending);
// Find an available slot for nextPending. The context should be available OR
diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
index 255e2f6d87792..9ea402c56088a 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
@@ -834,6 +834,13 @@ public class JobSchedulerService extends com.android.server.SystemService
// Higher override state (OVERRIDE_FULL) should be before lower state (OVERRIDE_SOFT)
return o2.overrideState - o1.overrideState;
}
+ if (o1.getSourceUid() == o2.getSourceUid()) {
+ final boolean o1FGJ = o1.isRequestedForegroundJob();
+ if (o1FGJ != o2.isRequestedForegroundJob()) {
+ // Attempt to run requested HPJs ahead of regular jobs, regardless of HPJ quota.
+ return o1FGJ ? -1 : 1;
+ }
+ }
if (o1.enqueueTime < o2.enqueueTime) {
return -1;
}
@@ -1136,6 +1143,12 @@ public class JobSchedulerService extends com.android.server.SystemService
JobStatus jobStatus = JobStatus.createFromJobInfo(job, uId, packageName, userId, tag);
+ // Return failure early if HPJ quota used up.
+ if (jobStatus.isRequestedForegroundJob()
+ && !mQuotaController.isWithinHpjQuotaLocked(jobStatus)) {
+ return JobScheduler.RESULT_FAILURE;
+ }
+
// Give exemption if the source is in the foreground just now.
// Note if it's a sync job, this method is called on the handler so it's not exactly
// the state when requestSync() was called, but that should be fine because of the
@@ -1791,9 +1804,9 @@ public class JobSchedulerService extends com.android.server.SystemService
* time of the job to be the time of completion (i.e. the time at which this function is
* called).
* This could be inaccurate b/c the job can run for as long as
- * {@link com.android.server.job.JobServiceContext#EXECUTING_TIMESLICE_MILLIS}, but will lead
- * to underscheduling at least, rather than if we had taken the last execution time to be the
- * start of the execution.
+ * {@link com.android.server.job.JobServiceContext#DEFAULT_EXECUTING_TIMESLICE_MILLIS}, but
+ * will lead to underscheduling at least, rather than if we had taken the last execution time
+ * to be the start of the execution.
*
* @return A new job representing the execution criteria for this instantiation of the
* recurring job.
@@ -1884,6 +1897,10 @@ public class JobSchedulerService extends com.android.server.SystemService
Slog.d(TAG, "Completed " + jobStatus + ", reschedule=" + needsReschedule);
}
+ // Intentionally not checking HPJ quota here. An app can't find out if it's run out of quota
+ // when it asks JS to reschedule an HPJ. Instead, the rescheduled HPJ will just be demoted
+ // to a regular job if the app has no HPJ quota left.
+
// If the job wants to be rescheduled, we first need to make the next upcoming
// job so we can transfer any appropriate state over from the previous job when
// we stop it.
@@ -2382,7 +2399,7 @@ public class JobSchedulerService extends com.android.server.SystemService
public long getMaxJobExecutionTimeMs(JobStatus job) {
synchronized (mLock) {
return Math.min(mQuotaController.getMaxJobExecutionTimeMsLocked(job),
- JobServiceContext.EXECUTING_TIMESLICE_MILLIS);
+ JobServiceContext.DEFAULT_EXECUTING_TIMESLICE_MILLIS);
}
}
@@ -2600,7 +2617,6 @@ public class JobSchedulerService extends com.android.server.SystemService
// job that runs one of the app's services, as well as verifying that the
// named service properly requires the BIND_JOB_SERVICE permission
private void enforceValidJobRequest(int uid, JobInfo job) {
- job.enforceValidity();
final PackageManager pm = getContext()
.createContextAsUser(UserHandle.getUserHandleForUid(uid), 0)
.getPackageManager();
@@ -2649,6 +2665,7 @@ public class JobSchedulerService extends com.android.server.SystemService
}
private void validateJobFlags(JobInfo job, int callingUid) {
+ job.enforceValidity();
if ((job.getFlags() & JobInfo.FLAG_WILL_BE_FOREGROUND) != 0) {
getContext().enforceCallingOrSelfPermission(
android.Manifest.permission.CONNECTIVITY_INTERNAL, TAG);
diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java b/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java
index 565ed959aeb4d..5fed1995dfc3e 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java
@@ -16,6 +16,7 @@
package com.android.server.job;
+import static com.android.server.job.JobSchedulerService.RESTRICTED_INDEX;
import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock;
import android.app.job.IJobCallback;
@@ -73,7 +74,13 @@ public final class JobServiceContext implements ServiceConnection {
private static final String TAG = "JobServiceContext";
/** Amount of time a job is allowed to execute for before being considered timed-out. */
- public static final long EXECUTING_TIMESLICE_MILLIS = 10 * 60 * 1000; // 10mins.
+ public static final long DEFAULT_EXECUTING_TIMESLICE_MILLIS = 10 * 60 * 1000; // 10mins.
+ /**
+ * Amount of time a RESTRICTED HPJ is allowed to execute for before being considered
+ * timed-out.
+ */
+ public static final long DEFAULT_RESTRICTED_HPJ_EXECUTING_TIMESLICE_MILLIS =
+ DEFAULT_EXECUTING_TIMESLICE_MILLIS / 2;
/** Amount of time the JobScheduler waits for the initial service launch+bind. */
private static final long OP_BIND_TIMEOUT_MILLIS = 18 * 1000;
/** Amount of time the JobScheduler will wait for a response from an app for a message. */
@@ -224,7 +231,8 @@ public final class JobServiceContext implements ServiceConnection {
final JobInfo ji = job.getJob();
mParams = new JobParameters(mRunningCallback, job.getJobId(), ji.getExtras(),
ji.getTransientExtras(), ji.getClipData(), ji.getClipGrantFlags(),
- isDeadlineExpired, triggeredUris, triggeredAuthorities, job.network);
+ isDeadlineExpired, job.shouldTreatAsForegroundJob(),
+ triggeredUris, triggeredAuthorities, job.network);
mExecutionStartTimeElapsed = sElapsedRealtimeClock.millis();
final long whenDeferred = job.getWhenStandbyDeferred();
@@ -250,9 +258,18 @@ public final class JobServiceContext implements ServiceConnection {
final Intent intent = new Intent().setComponent(job.getServiceComponent());
boolean binding = false;
try {
- binding = mContext.bindServiceAsUser(intent, this,
- Context.BIND_AUTO_CREATE | Context.BIND_NOT_FOREGROUND
- | Context.BIND_NOT_PERCEPTIBLE,
+ final int bindFlags;
+ if (job.shouldTreatAsForegroundJob()) {
+ // Add BIND_FOREGROUND_SERVICE to make it BFGS. Without it, it'll be
+ // PROCESS_STATE_IMPORTANT_FOREGROUND. Unclear which is better here.
+ // TODO(171305774): The job should run on the little cores. We'll probably need
+ // another binding flag for that.
+ bindFlags = Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE;
+ } else {
+ bindFlags = Context.BIND_AUTO_CREATE | Context.BIND_NOT_FOREGROUND
+ | Context.BIND_NOT_PERCEPTIBLE;
+ }
+ binding = mContext.bindServiceAsUser(intent, this, bindFlags,
UserHandle.of(job.getUserId()));
} catch (SecurityException e) {
// Some permission policy, for example INTERACT_ACROSS_USERS and
@@ -848,7 +865,10 @@ public final class JobServiceContext implements ServiceConnection {
final long timeoutMillis;
switch (mVerb) {
case VERB_EXECUTING:
- timeoutMillis = EXECUTING_TIMESLICE_MILLIS;
+ timeoutMillis = mRunningJob.shouldTreatAsForegroundJob()
+ && mRunningJob.getStandbyBucket() == RESTRICTED_INDEX
+ ? DEFAULT_RESTRICTED_HPJ_EXECUTING_TIMESLICE_MILLIS
+ : DEFAULT_EXECUTING_TIMESLICE_MILLIS;
break;
case VERB_BINDING:
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java
index 8f6c68dad36c2..5ec1b8951a7b9 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java
@@ -85,6 +85,7 @@ public final class JobStatus {
static final int CONSTRAINT_CONTENT_TRIGGER = 1<<26;
static final int CONSTRAINT_DEVICE_NOT_DOZING = 1 << 25; // Implicit constraint
static final int CONSTRAINT_WITHIN_QUOTA = 1 << 24; // Implicit constraint
+ static final int CONSTRAINT_WITHIN_HPJ_QUOTA = 1 << 23; // Implicit constraint
static final int CONSTRAINT_BACKGROUND_NOT_RESTRICTED = 1 << 22; // Implicit constraint
/**
@@ -376,6 +377,9 @@ public final class JobStatus {
/** The job is within its quota based on its standby bucket. */
private boolean mReadyWithinQuota;
+ /** The job is a foreground job with sufficient quota to run as a foreground job. */
+ private boolean mReadyWithinHpjQuota;
+
/** The job's dynamic requirements have been satisfied. */
private boolean mReadyDynamicSatisfied;
@@ -1036,20 +1040,34 @@ public final class JobStatus {
mPersistedUtcTimes = null;
}
+ /** @return true if the app has requested that this run as a foreground job. */
+ public boolean isRequestedForegroundJob() {
+ return (getFlags() & JobInfo.FLAG_FOREGROUND_JOB) != 0;
+ }
+
+ /**
+ * @return true if all foreground job requirements are satisfied and therefore this should be
+ * treated as a foreground job.
+ */
+ public boolean shouldTreatAsForegroundJob() {
+ return mReadyWithinHpjQuota && isRequestedForegroundJob();
+ }
+
/**
* @return true if the job is exempted from Doze restrictions and therefore allowed to run
* in Doze.
*/
public boolean canRunInDoze() {
- return (getFlags() & JobInfo.FLAG_WILL_BE_FOREGROUND) != 0;
+ return (getFlags() & JobInfo.FLAG_WILL_BE_FOREGROUND) != 0 || shouldTreatAsForegroundJob();
}
boolean canRunInBatterySaver() {
- return (getInternalFlags() & INTERNAL_FLAG_HAS_FOREGROUND_EXEMPTION) != 0;
+ return (getInternalFlags() & INTERNAL_FLAG_HAS_FOREGROUND_EXEMPTION) != 0
+ || shouldTreatAsForegroundJob();
}
boolean shouldIgnoreNetworkBlocking() {
- return (getFlags() & JobInfo.FLAG_WILL_BE_FOREGROUND) != 0;
+ return (getFlags() & JobInfo.FLAG_WILL_BE_FOREGROUND) != 0 || shouldTreatAsForegroundJob();
}
/** @return true if the constraint was changed, false otherwise. */
@@ -1128,6 +1146,16 @@ public final class JobStatus {
return false;
}
+ /** @return true if the constraint was changed, false otherwise. */
+ boolean setForegroundJobQuotaConstraintSatisfied(boolean state) {
+ if (setConstraintSatisfied(CONSTRAINT_WITHIN_HPJ_QUOTA, state)) {
+ // The constraint was changed. Update the ready flag.
+ mReadyWithinHpjQuota = state;
+ return true;
+ }
+ return false;
+ }
+
/** @return true if the state was changed, false otherwise. */
boolean setUidActive(final boolean newActiveState) {
if (newActiveState != uidActive) {
@@ -1257,6 +1285,10 @@ public final class JobStatus {
oldValue = mReadyWithinQuota;
mReadyWithinQuota = true;
break;
+ case CONSTRAINT_WITHIN_HPJ_QUOTA:
+ oldValue = mReadyWithinHpjQuota;
+ mReadyWithinHpjQuota = true;
+ break;
default:
satisfied |= constraint;
mReadyDynamicSatisfied = mDynamicConstraints != 0
@@ -1279,6 +1311,9 @@ public final class JobStatus {
case CONSTRAINT_WITHIN_QUOTA:
mReadyWithinQuota = oldValue;
break;
+ case CONSTRAINT_WITHIN_HPJ_QUOTA:
+ mReadyWithinHpjQuota = oldValue;
+ break;
default:
mReadyDynamicSatisfied = mDynamicConstraints != 0
&& mDynamicConstraints == (satisfiedConstraints & mDynamicConstraints);
@@ -1293,7 +1328,7 @@ public final class JobStatus {
// sessions (exempt from dynamic restrictions), we need the additional check to ensure
// that NEVER jobs don't run.
// TODO: cleanup quota and standby bucket management so we don't need the additional checks
- if ((!mReadyWithinQuota && !mReadyDynamicSatisfied)
+ if ((!mReadyWithinQuota && !mReadyDynamicSatisfied && !shouldTreatAsForegroundJob())
|| getEffectiveStandbyBucket() == NEVER_INDEX) {
return false;
}
@@ -1506,6 +1541,9 @@ public final class JobStatus {
if ((constraints & CONSTRAINT_WITHIN_QUOTA) != 0) {
pw.print(" WITHIN_QUOTA");
}
+ if ((constraints & CONSTRAINT_WITHIN_HPJ_QUOTA) != 0) {
+ pw.print(" WITHIN_HPJ_QUOTA");
+ }
if (constraints != 0) {
pw.print(" [0x");
pw.print(Integer.toHexString(constraints));
@@ -1578,6 +1616,9 @@ public final class JobStatus {
if ((constraints & CONSTRAINT_BACKGROUND_NOT_RESTRICTED) != 0) {
proto.write(fieldId, JobServerProtoEnums.CONSTRAINT_BACKGROUND_NOT_RESTRICTED);
}
+ if ((constraints & CONSTRAINT_WITHIN_HPJ_QUOTA) != 0) {
+ proto.write(fieldId, JobServerProtoEnums.CONSTRAINT_WITHIN_HPJ_QUOTA);
+ }
}
private void dumpJobWorkItem(PrintWriter pw, String prefix, JobWorkItem work, int index) {
@@ -1795,6 +1836,11 @@ public final class JobStatus {
pw.print(prefix);
pw.print(" readyComponentEnabled: ");
pw.println(serviceInfo != null);
+ if ((getFlags() & JobInfo.FLAG_FOREGROUND_JOB) != 0) {
+ pw.print(prefix);
+ pw.print(" mReadyWithinHpjQuota: ");
+ pw.println(mReadyWithinHpjQuota);
+ }
if (changedAuthorities != null) {
pw.print(prefix); pw.println("Changed authorities:");
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java
index 1d72b42d824d3..a3a20c6c10913 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java
@@ -36,6 +36,9 @@ import android.app.ActivityManagerInternal;
import android.app.AlarmManager;
import android.app.AppGlobals;
import android.app.IUidObserver;
+import android.app.usage.UsageEvents;
+import android.app.usage.UsageStatsManagerInternal;
+import android.app.usage.UsageStatsManagerInternal.UsageEventListener;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
@@ -53,6 +56,7 @@ import android.util.IndentingPrintWriter;
import android.util.Log;
import android.util.Pair;
import android.util.Slog;
+import android.util.SparseArray;
import android.util.SparseArrayMap;
import android.util.SparseBooleanArray;
import android.util.SparseSetArray;
@@ -297,10 +301,17 @@ public final class QuotaController extends StateController {
/** Timer for each package-userId combo. */
private final SparseArrayMap mPkgTimers = new SparseArrayMap<>();
- /** List of all timing sessions for a package-userId combo, in chronological order. */
+ /** Timer for HPJs for each package-userId combo. */
+ private final SparseArrayMap mHpjPkgTimers = new SparseArrayMap<>();
+
+ /** List of all regular timing sessions for a package-userId combo, in chronological order. */
private final SparseArrayMap> mTimingSessions =
new SparseArrayMap<>();
+ /** List of all hpj timing sessions for a package-userId combo, in chronological order. */
+ private final SparseArrayMap> mHpjTimingSessions =
+ new SparseArrayMap<>();
+
/**
* Listener to track and manage when each package comes back within quota.
*/
@@ -311,6 +322,10 @@ public final class QuotaController extends StateController {
private final SparseArrayMap mExecutionStatsCache =
new SparseArrayMap<>();
+ private final SparseArrayMap mHpjStats = new SparseArrayMap<>();
+
+ private final SparseArrayMap mTopAppTrackers = new SparseArrayMap<>();
+
/** List of UIDs currently in the foreground. */
private final SparseBooleanArray mForegroundUids = new SparseBooleanArray();
@@ -327,7 +342,7 @@ public final class QuotaController extends StateController {
private final ActivityManagerInternal mActivityManagerInternal;
private final AlarmManager mAlarmManager;
private final ChargingTracker mChargeTracker;
- private final Handler mHandler;
+ private final QcHandler mHandler;
private final QcConstants mQcConstants;
/** How much time each app will have to run jobs within their standby bucket window. */
@@ -474,14 +489,63 @@ public final class QuotaController extends StateController {
private long mTimingSessionCoalescingDurationMs =
QcConstants.DEFAULT_TIMING_SESSION_COALESCING_DURATION_MS;
+ /**
+ * The rolling window size for each standby bucket. Within each window, an app will have 10
+ * minutes to run its jobs.
+ */
+ private final long[] mHpjLimitsMs = new long[]{
+ QcConstants.DEFAULT_HPJ_LIMIT_ACTIVE_MS,
+ QcConstants.DEFAULT_HPJ_LIMIT_WORKING_MS,
+ QcConstants.DEFAULT_HPJ_LIMIT_FREQUENT_MS,
+ QcConstants.DEFAULT_HPJ_LIMIT_RARE_MS,
+ 0, // NEVER
+ QcConstants.DEFAULT_HPJ_LIMIT_RESTRICTED_MS
+ };
+
+ /**
+ * The period of time used to calculate HPJ sessions. Apps can only have HPJ sessions
+ * totalling {@link #mHpjLimitsMs}[bucket within this period of time (without factoring in any
+ * rewards or free HPJs).
+ */
+ private long mHpjLimitWindowSizeMs = QcConstants.DEFAULT_HPJ_WINDOW_SIZE_MS;
+
+ /**
+ * Length of time used to split an app's top time into chunks.
+ */
+ public long mHpjTopAppTimeChunkSizeMs = QcConstants.DEFAULT_HPJ_TOP_APP_TIME_CHUNK_SIZE_MS;
+
+ /**
+ * How much HPJ quota to give back to an app based on the number of top app time chunks it had.
+ */
+ public long mHpjRewardTopAppMs = QcConstants.DEFAULT_HPJ_REWARD_TOP_APP_MS;
+
+ /**
+ * How much HPJ quota to give back to an app based on each non-top user interaction.
+ */
+ public long mHpjRewardInteractionMs = QcConstants.DEFAULT_HPJ_REWARD_INTERACTION_MS;
+
+ /**
+ * How much HPJ quota to give back to an app based on each notification seen event.
+ */
+ public long mHpjRewardNotificationSeenMs = QcConstants.DEFAULT_HPJ_REWARD_NOTIFICATION_SEEN_MS;
+
/** An app has reached its quota. The message should contain a {@link Package} object. */
- private static final int MSG_REACHED_QUOTA = 0;
+ @VisibleForTesting
+ static final int MSG_REACHED_QUOTA = 0;
/** Drop any old timing sessions. */
private static final int MSG_CLEAN_UP_SESSIONS = 1;
/** Check if a package is now within its quota. */
private static final int MSG_CHECK_PACKAGE = 2;
/** Process state for a UID has changed. */
private static final int MSG_UID_PROCESS_STATE_CHANGED = 3;
+ /** An app has reached its HPJ quota. The message should contain a {@link Package} object. */
+ @VisibleForTesting
+ static final int MSG_REACHED_HPJ_QUOTA = 4;
+ /**
+ * Process a new {@link UsageEvents.Event}. The event will be the message's object and the
+ * userId will the first arg.
+ */
+ private static final int MSG_PROCESS_USAGE_EVENT = 5;
public QuotaController(JobSchedulerService service) {
super(service);
@@ -499,6 +563,9 @@ public final class QuotaController extends StateController {
AppStandbyInternal appStandby = LocalServices.getService(AppStandbyInternal.class);
appStandby.addListener(new StandbyTracker());
+ UsageStatsManagerInternal usmi = LocalServices.getService(UsageStatsManagerInternal.class);
+ usmi.registerListener(new UsageEventTracker());
+
try {
ActivityManager.getService().registerUidObserver(mUidObserver,
ActivityManager.UID_OBSERVER_PROCSTATE,
@@ -521,7 +588,15 @@ public final class QuotaController extends StateController {
jobStatus.setTrackingController(JobStatus.TRACKING_QUOTA);
final boolean isWithinQuota = isWithinQuotaLocked(jobStatus);
setConstraintSatisfied(jobStatus, isWithinQuota);
- if (!isWithinQuota) {
+ final boolean outOfHpjQuota;
+ if (jobStatus.isRequestedForegroundJob()) {
+ final boolean isWithinHpjQuota = isWithinHpjQuotaLocked(jobStatus);
+ jobStatus.setForegroundJobQuotaConstraintSatisfied(isWithinHpjQuota);
+ outOfHpjQuota = !isWithinHpjQuota;
+ } else {
+ outOfHpjQuota = false;
+ }
+ if (!isWithinQuota || outOfHpjQuota) {
maybeScheduleStartAlarmLocked(userId, pkgName, jobStatus.getEffectiveStandbyBucket());
}
}
@@ -544,10 +619,12 @@ public final class QuotaController extends StateController {
final int userId = jobStatus.getSourceUserId();
final String packageName = jobStatus.getSourcePackageName();
- Timer timer = mPkgTimers.get(userId, packageName);
+ final SparseArrayMap timerMap =
+ jobStatus.shouldTreatAsForegroundJob() ? mHpjPkgTimers : mPkgTimers;
+ Timer timer = timerMap.get(userId, packageName);
if (timer == null) {
- timer = new Timer(uid, userId, packageName);
- mPkgTimers.add(userId, packageName, timer);
+ timer = new Timer(uid, userId, packageName, !jobStatus.shouldTreatAsForegroundJob());
+ timerMap.add(userId, packageName, timer);
}
timer.startTrackingJobLocked(jobStatus);
}
@@ -561,6 +638,13 @@ public final class QuotaController extends StateController {
if (timer != null) {
timer.stopTrackingJob(jobStatus);
}
+ if (jobStatus.isRequestedForegroundJob()) {
+ timer = mHpjPkgTimers.get(jobStatus.getSourceUserId(),
+ jobStatus.getSourcePackageName());
+ if (timer != null) {
+ timer.stopTrackingJob(jobStatus);
+ }
+ }
ArraySet jobs = mTrackedJobs.get(jobStatus.getSourceUserId(),
jobStatus.getSourcePackageName());
if (jobs != null) {
@@ -585,26 +669,37 @@ public final class QuotaController extends StateController {
public void onUserRemovedLocked(int userId) {
mTrackedJobs.delete(userId);
mPkgTimers.delete(userId);
+ mHpjPkgTimers.delete(userId);
mTimingSessions.delete(userId);
+ mHpjTimingSessions.delete(userId);
mInQuotaAlarmListener.removeAlarmsLocked(userId);
mExecutionStatsCache.delete(userId);
+ mHpjStats.delete(userId);
mUidToPackageCache.clear();
}
/** Drop all historical stats and stop tracking any active sessions for the specified app. */
public void clearAppStatsLocked(int userId, @NonNull String packageName) {
mTrackedJobs.delete(userId, packageName);
- Timer timer = mPkgTimers.get(userId, packageName);
+ Timer timer = mPkgTimers.delete(userId, packageName);
if (timer != null) {
if (timer.isActive()) {
Slog.e(TAG, "clearAppStats called before Timer turned off.");
timer.dropEverythingLocked();
}
- mPkgTimers.delete(userId, packageName);
+ }
+ timer = mHpjPkgTimers.delete(userId, packageName);
+ if (timer != null) {
+ if (timer.isActive()) {
+ Slog.e(TAG, "clearAppStats called before HPJ Timer turned off.");
+ timer.dropEverythingLocked();
+ }
}
mTimingSessions.delete(userId, packageName);
+ mHpjTimingSessions.delete(userId, packageName);
mInQuotaAlarmListener.removeAlarmLocked(userId, packageName);
mExecutionStatsCache.delete(userId, packageName);
+ mHpjStats.delete(userId, packageName);
}
private boolean isUidInForeground(int uid) {
@@ -627,11 +722,53 @@ public final class QuotaController extends StateController {
if (mChargeTracker.isCharging()
|| isTopStartedJobLocked(jobStatus)
|| isUidInForeground(jobStatus.getSourceUid())) {
- return JobServiceContext.EXECUTING_TIMESLICE_MILLIS;
+ return JobServiceContext.DEFAULT_EXECUTING_TIMESLICE_MILLIS;
+ }
+ if (jobStatus.shouldTreatAsForegroundJob()) {
+ return jobStatus.getStandbyBucket() == RESTRICTED_INDEX
+ ? JobServiceContext.DEFAULT_RESTRICTED_HPJ_EXECUTING_TIMESLICE_MILLIS
+ : JobServiceContext.DEFAULT_EXECUTING_TIMESLICE_MILLIS;
}
return getRemainingExecutionTimeLocked(jobStatus);
}
+ /** @return true if the job is within hpj quota. */
+ public boolean isWithinHpjQuotaLocked(@NonNull final JobStatus jobStatus) {
+ if (isQuotaFree(jobStatus.getEffectiveStandbyBucket())) {
+ return true;
+ }
+ // A job is within quota if one of the following is true:
+ // 1. it's already running (already executing HPJS should be allowed to finish)
+ // 2. the app is currently in the foreground
+ // 3. the app overall is within its quota
+ if (isTopStartedJobLocked(jobStatus) || isUidInForeground(jobStatus.getSourceUid())) {
+ return true;
+ }
+ Timer hpjTimer = mHpjPkgTimers.get(jobStatus.getSourceUserId(),
+ jobStatus.getSourcePackageName());
+ // Any already executing HPJs should be allowed to finish.
+ if (hpjTimer != null && hpjTimer.isRunning(jobStatus)) {
+ return true;
+ }
+
+ return 0 < getRemainingHpjExecutionTimeLocked(
+ jobStatus.getSourceUserId(), jobStatus.getSourcePackageName());
+ }
+
+ @NonNull
+ private ShrinkableDebits getHpjQuotaLocked(final int userId,
+ @NonNull final String packageName) {
+ ShrinkableDebits debits = mHpjStats.get(userId, packageName);
+ if (debits == null) {
+ debits = new ShrinkableDebits(
+ JobSchedulerService.standbyBucketForPackage(
+ packageName, userId, sElapsedRealtimeClock.millis())
+ );
+ mHpjStats.add(userId, packageName, debits);
+ }
+ return debits;
+ }
+
@VisibleForTesting
boolean isWithinQuotaLocked(@NonNull final JobStatus jobStatus) {
final int standbyBucket = jobStatus.getEffectiveStandbyBucket();
@@ -645,19 +782,22 @@ public final class QuotaController extends StateController {
jobStatus.getSourceUserId(), jobStatus.getSourcePackageName(), standbyBucket);
}
+ private boolean isQuotaFree(final int standbyBucket) {
+ // Quota constraint is not enforced while charging.
+ if (mChargeTracker.isCharging()) {
+ // Restricted jobs require additional constraints when charging, so don't immediately
+ // mark quota as free when charging.
+ return standbyBucket != RESTRICTED_INDEX;
+ }
+ return false;
+ }
+
@VisibleForTesting
boolean isWithinQuotaLocked(final int userId, @NonNull final String packageName,
final int standbyBucket) {
if (standbyBucket == NEVER_INDEX) return false;
- // Quota constraint is not enforced while charging.
- if (mChargeTracker.isCharging()) {
- // Restricted jobs require additional constraints when charging, so don't immediately
- // mark quota as free when charging.
- if (standbyBucket != RESTRICTED_INDEX) {
- return true;
- }
- }
+ if (isQuotaFree(standbyBucket)) return true;
ExecutionStats stats = getExecutionStatsLocked(userId, packageName, standbyBucket);
return getRemainingExecutionTimeLocked(stats) > 0
@@ -717,6 +857,49 @@ public final class QuotaController extends StateController {
mMaxExecutionTimeMs - stats.executionTimeInMaxPeriodMs);
}
+ @VisibleForTesting
+ long getRemainingHpjExecutionTimeLocked(final int userId, @NonNull final String packageName) {
+ ShrinkableDebits quota = getHpjQuotaLocked(userId, packageName);
+ if (quota.getStandbyBucketLocked() == NEVER_INDEX) {
+ return 0;
+ }
+ final long limitMs = mHpjLimitsMs[quota.getStandbyBucketLocked()];
+ long remainingMs = limitMs - quota.getTallyLocked();
+
+ // Stale sessions may still be factored into tally. Make sure they're removed.
+ List timingSessions = mHpjTimingSessions.get(userId, packageName);
+ final long nowElapsed = sElapsedRealtimeClock.millis();
+ final long windowStartTimeElapsed = nowElapsed - mHpjLimitWindowSizeMs;
+ if (timingSessions != null) {
+ while (timingSessions.size() > 0) {
+ TimingSession ts = timingSessions.get(0);
+ if (ts.endTimeElapsed < windowStartTimeElapsed) {
+ final long duration = ts.endTimeElapsed - ts.startTimeElapsed;
+ remainingMs += duration;
+ quota.transactOnDebitsLocked(-duration);
+ timingSessions.remove(0);
+ } else if (ts.startTimeElapsed < windowStartTimeElapsed) {
+ remainingMs += windowStartTimeElapsed - ts.startTimeElapsed;
+ break;
+ } else {
+ // Fully within the window.
+ break;
+ }
+ }
+ }
+
+ Timer timer = mHpjPkgTimers.get(userId, packageName);
+ if (timer == null) {
+ return remainingMs;
+ }
+ // There's a case where the debits tally is 0 but a currently running HPJ still counts
+ // towards quota. If the app gets a reward in this case, the reward is lost and the HPJ
+ // run is still fully counted.
+ // TODO(171305774)/STOPSHIP: make sure getting rewards while HPJ currently executing isn't
+ // treated negatively
+ return remainingMs - timer.getCurrentDuration(sElapsedRealtimeClock.millis());
+ }
+
/**
* Returns the amount of time, in milliseconds, until the package would have reached its
* duration quota, assuming it has a job counting towards its quota the entire time. This takes
@@ -802,6 +985,61 @@ public final class QuotaController extends StateController {
return timeUntilQuotaConsumedMs;
}
+ /**
+ * Returns the amount of time, in milliseconds, until the package would have reached its HPJ
+ * quota, assuming it has a job counting towards the quota the entire time and the quota isn't
+ * replenished at all in that time.
+ */
+ @VisibleForTesting
+ long getTimeUntilHpjQuotaConsumedLocked(final int userId, @NonNull final String packageName) {
+ final long remainingExecutionTimeMs =
+ getRemainingHpjExecutionTimeLocked(userId, packageName);
+
+ List sessions = mHpjTimingSessions.get(userId, packageName);
+ if (sessions == null || sessions.size() == 0) {
+ return remainingExecutionTimeMs;
+ }
+
+ final long nowElapsed = sElapsedRealtimeClock.millis();
+ ShrinkableDebits quota = getHpjQuotaLocked(userId, packageName);
+ final long limitMs = mHpjLimitsMs[quota.getStandbyBucketLocked()];
+ final long startWindowElapsed = Math.max(0, nowElapsed - mHpjLimitWindowSizeMs);
+ long remainingDeadSpaceMs = remainingExecutionTimeMs;
+ // Total time looked at where a session wouldn't be phasing out.
+ long deadSpaceMs = 0;
+ // Time regained from sessions phasing out
+ long phasedOutSessionTimeMs = 0;
+
+ for (int i = 0; i < sessions.size(); ++i) {
+ TimingSession session = sessions.get(i);
+ if (session.endTimeElapsed < startWindowElapsed) {
+ // Edge case where a session became stale in the time between the call to
+ // getRemainingHpjExecutionTimeLocked and this line.
+ remainingDeadSpaceMs += session.endTimeElapsed - session.startTimeElapsed;
+ sessions.remove(i);
+ i--;
+ } else if (session.startTimeElapsed < startWindowElapsed) {
+ // Session straddles start of window
+ phasedOutSessionTimeMs = session.endTimeElapsed - startWindowElapsed;
+ } else {
+ // Session fully inside window
+ final long timeBetweenSessions = session.startTimeElapsed
+ - (i == 0 ? startWindowElapsed : sessions.get(i - 1).endTimeElapsed);
+ final long usedDeadSpaceMs = Math.min(remainingDeadSpaceMs, timeBetweenSessions);
+ deadSpaceMs += usedDeadSpaceMs;
+ if (usedDeadSpaceMs == timeBetweenSessions) {
+ phasedOutSessionTimeMs += session.endTimeElapsed - session.startTimeElapsed;
+ }
+ remainingDeadSpaceMs -= usedDeadSpaceMs;
+ if (remainingDeadSpaceMs <= 0) {
+ break;
+ }
+ }
+ }
+
+ return Math.min(limitMs, deadSpaceMs + phasedOutSessionTimeMs + remainingDeadSpaceMs);
+ }
+
/** Returns the execution stats of the app in the most recent window. */
@VisibleForTesting
@NonNull
@@ -1053,18 +1291,36 @@ public final class QuotaController extends StateController {
@VisibleForTesting
void saveTimingSession(final int userId, @NonNull final String packageName,
- @NonNull final TimingSession session) {
+ @NonNull final TimingSession session, boolean isHpj) {
synchronized (mLock) {
- List sessions = mTimingSessions.get(userId, packageName);
+ final SparseArrayMap> sessionMap =
+ isHpj ? mHpjTimingSessions : mTimingSessions;
+ List sessions = sessionMap.get(userId, packageName);
if (sessions == null) {
sessions = new ArrayList<>();
- mTimingSessions.add(userId, packageName, sessions);
+ sessionMap.add(userId, packageName, sessions);
}
sessions.add(session);
- // Adding a new session means that the current stats are now incorrect.
- invalidateAllExecutionStatsLocked(userId, packageName);
+ if (isHpj) {
+ final ShrinkableDebits quota = getHpjQuotaLocked(userId, packageName);
+ quota.transactOnDebitsLocked(session.endTimeElapsed - session.startTimeElapsed);
+ } else {
+ // Adding a new session means that the current stats are now incorrect.
+ invalidateAllExecutionStatsLocked(userId, packageName);
- maybeScheduleCleanupAlarmLocked();
+ maybeScheduleCleanupAlarmLocked();
+ }
+ }
+ }
+
+ private void grantRewardForInstantEvent(
+ final int userId, @NonNull final String packageName, final long credit) {
+ synchronized (mLock) {
+ final ShrinkableDebits quota = getHpjQuotaLocked(userId, packageName);
+ quota.transactOnDebitsLocked(-credit);
+ if (maybeUpdateConstraintForPkgLocked(userId, packageName)) {
+ mStateChangedListener.onControllerStateChanged();
+ }
}
}
@@ -1100,6 +1356,7 @@ public final class QuotaController extends StateController {
}
mEarliestEndTimeFunctor.reset();
mTimingSessions.forEach(mEarliestEndTimeFunctor);
+ mHpjTimingSessions.forEach(mEarliestEndTimeFunctor);
final long earliestEndElapsed = mEarliestEndTimeFunctor.earliestEndElapsed;
if (earliestEndElapsed == Long.MAX_VALUE) {
// Couldn't find a good time to clean up. Maybe this was called after we deleted all
@@ -1155,6 +1412,7 @@ public final class QuotaController extends StateController {
Slog.d(TAG, "handleNewChargingStateLocked: " + mChargeTracker.isCharging());
}
// Deal with Timers first.
+ mHpjPkgTimers.forEach(mTimerChargingUpdateFunctor);
mPkgTimers.forEach(mTimerChargingUpdateFunctor);
// Now update jobs.
maybeUpdateAllConstraintsLocked();
@@ -1189,6 +1447,7 @@ public final class QuotaController extends StateController {
// Quota is the same for all jobs within a package.
final int realStandbyBucket = jobs.valueAt(0).getStandbyBucket();
final boolean realInQuota = isWithinQuotaLocked(userId, packageName, realStandbyBucket);
+ boolean outOfHpjQuota = false;
boolean changed = false;
for (int i = jobs.size() - 1; i >= 0; --i) {
final JobStatus js = jobs.valueAt(i);
@@ -1206,8 +1465,14 @@ public final class QuotaController extends StateController {
// This job is somehow exempted. Need to determine its own quota status.
changed |= setConstraintSatisfied(js, isWithinQuotaLocked(js));
}
+
+ if (js.isRequestedForegroundJob()) {
+ boolean isWithinHpjQuota = isWithinHpjQuotaLocked(js);
+ changed |= js.setForegroundJobQuotaConstraintSatisfied(isWithinHpjQuota);
+ outOfHpjQuota |= !isWithinHpjQuota;
+ }
}
- if (!realInQuota) {
+ if (!realInQuota || outOfHpjQuota) {
// Don't want to use the effective standby bucket here since that bump the bucket to
// ACTIVE for one of the jobs, which doesn't help with other jobs that aren't
// exempted.
@@ -1226,10 +1491,22 @@ public final class QuotaController extends StateController {
@Override
public void accept(JobStatus jobStatus) {
wasJobChanged |= setConstraintSatisfied(jobStatus, isWithinQuotaLocked(jobStatus));
+ final boolean outOfHpjQuota;
+ if (jobStatus.isRequestedForegroundJob()) {
+ final boolean isWithinHpjQuota = isWithinHpjQuotaLocked(jobStatus);
+ wasJobChanged |= jobStatus.setForegroundJobQuotaConstraintSatisfied(
+ isWithinHpjQuota);
+ outOfHpjQuota = !isWithinHpjQuota;
+ } else {
+ outOfHpjQuota = false;
+ }
+
final int userId = jobStatus.getSourceUserId();
final String packageName = jobStatus.getSourcePackageName();
final int realStandbyBucket = jobStatus.getStandbyBucket();
- if (isWithinQuotaLocked(userId, packageName, realStandbyBucket)) {
+ if (isWithinQuotaLocked(userId, packageName, realStandbyBucket) && !outOfHpjQuota) {
+ // TODO(141645789): we probably shouldn't cancel the alarm until we've verified
+ // that all jobs for the userId-package are within quota.
mInQuotaAlarmListener.removeAlarmLocked(userId, packageName);
} else {
mToScheduleStartAlarms.add(userId, packageName, realStandbyBucket);
@@ -1280,11 +1557,13 @@ public final class QuotaController extends StateController {
final boolean isUnderJobCountQuota = isUnderJobCountQuotaLocked(stats, standbyBucket);
final boolean isUnderTimingSessionCountQuota = isUnderSessionCountQuotaLocked(stats,
standbyBucket);
+ final long remainingHpjQuota = getRemainingHpjExecutionTimeLocked(userId, packageName);
if (stats.executionTimeInWindowMs < mAllowedTimePerPeriodMs
&& stats.executionTimeInMaxPeriodMs < mMaxExecutionTimeMs
&& isUnderJobCountQuota
- && isUnderTimingSessionCountQuota) {
+ && isUnderTimingSessionCountQuota
+ && remainingHpjQuota > 0) {
// Already in quota. Why was this method called?
if (DEBUG) {
Slog.e(TAG, "maybeScheduleStartAlarmLocked called for " + pkgString
@@ -1297,19 +1576,44 @@ public final class QuotaController extends StateController {
return;
}
- // The time this app will have quota again.
- long inQuotaTimeElapsed = stats.inQuotaTimeElapsed;
- if (!isUnderJobCountQuota && stats.bgJobCountInWindow < stats.jobCountLimit) {
- // App hit the rate limit.
- inQuotaTimeElapsed = Math.max(inQuotaTimeElapsed,
- stats.jobRateLimitExpirationTimeElapsed);
+ long inRegularQuotaTimeElapsed = Long.MAX_VALUE;
+ long inHpjQuotaTimeElapsed = Long.MAX_VALUE;
+ if (!(stats.executionTimeInWindowMs < mAllowedTimePerPeriodMs
+ && stats.executionTimeInMaxPeriodMs < mMaxExecutionTimeMs
+ && isUnderJobCountQuota
+ && isUnderTimingSessionCountQuota)) {
+ // The time this app will have quota again.
+ long inQuotaTimeElapsed = stats.inQuotaTimeElapsed;
+ if (!isUnderJobCountQuota && stats.bgJobCountInWindow < stats.jobCountLimit) {
+ // App hit the rate limit.
+ inQuotaTimeElapsed =
+ Math.max(inQuotaTimeElapsed, stats.jobRateLimitExpirationTimeElapsed);
+ }
+ if (!isUnderTimingSessionCountQuota
+ && stats.sessionCountInWindow < stats.sessionCountLimit) {
+ // App hit the rate limit.
+ inQuotaTimeElapsed =
+ Math.max(inQuotaTimeElapsed, stats.sessionRateLimitExpirationTimeElapsed);
+ }
+ inRegularQuotaTimeElapsed = inQuotaTimeElapsed;
}
- if (!isUnderTimingSessionCountQuota
- && stats.sessionCountInWindow < stats.sessionCountLimit) {
- // App hit the rate limit.
- inQuotaTimeElapsed = Math.max(inQuotaTimeElapsed,
- stats.sessionRateLimitExpirationTimeElapsed);
+ if (remainingHpjQuota <= 0) {
+ final long limitMs = mHpjLimitsMs[standbyBucket] - mQuotaBufferMs;
+ List timingSessions = mHpjTimingSessions.get(userId, packageName);
+ long sumMs = 0;
+ for (int i = timingSessions.size() - 1; i >= 0; --i) {
+ TimingSession ts = timingSessions.get(i);
+ final long durationMs = ts.endTimeElapsed - ts.startTimeElapsed;
+ sumMs += durationMs;
+ if (sumMs >= limitMs) {
+ inHpjQuotaTimeElapsed =
+ ts.startTimeElapsed + (sumMs - limitMs) + mHpjLimitWindowSizeMs;
+ break;
+ }
+ }
}
+ long inQuotaTimeElapsed = Math.min(inRegularQuotaTimeElapsed, inHpjQuotaTimeElapsed);
+
if (inQuotaTimeElapsed <= sElapsedRealtimeClock.millis()) {
final long nowElapsed = sElapsedRealtimeClock.millis();
Slog.wtf(TAG,
@@ -1450,9 +1754,52 @@ public final class QuotaController extends StateController {
}
}
+ private static final class ShrinkableDebits {
+ /** The amount of quota remaining. Can be negative if limit changes. */
+ private long mDebitTally;
+ private int mStandbyBucket;
+
+ ShrinkableDebits(int standbyBucket) {
+ mDebitTally = 0;
+ mStandbyBucket = standbyBucket;
+ }
+
+ long getTallyLocked() {
+ return mDebitTally;
+ }
+
+ /**
+ * Negative if the tally should decrease (therefore increasing available quota);
+ * or positive if the tally should increase (therefore decreasing available quota).
+ */
+ void transactOnDebitsLocked(final long amount) {
+ mDebitTally = Math.max(0, mDebitTally + amount);
+ }
+
+ void setStandbyBucketLocked(int standbyBucket) {
+ mStandbyBucket = standbyBucket;
+ }
+
+ int getStandbyBucketLocked() {
+ return mStandbyBucket;
+ }
+
+ @Override
+ public String toString() {
+ return "ShrinkableDebits { debit tally: "
+ + mDebitTally + ", bucket: " + mStandbyBucket
+ + " }";
+ }
+
+ void dumpLocked(IndentingPrintWriter pw) {
+ pw.println(toString());
+ }
+ }
+
private final class Timer {
private final Package mPkg;
private final int mUid;
+ private final boolean mRegularJobTimer;
// List of jobs currently running for this app that started when the app wasn't in the
// foreground.
@@ -1460,9 +1807,10 @@ public final class QuotaController extends StateController {
private long mStartTimeElapsed;
private int mBgJobCount;
- Timer(int uid, int userId, String packageName) {
+ Timer(int uid, int userId, String packageName, boolean regularJobTimer) {
mPkg = new Package(userId, packageName);
mUid = uid;
+ mRegularJobTimer = regularJobTimer;
}
void startTrackingJobLocked(@NonNull JobStatus jobStatus) {
@@ -1482,12 +1830,17 @@ public final class QuotaController extends StateController {
mRunningBgJobs.add(jobStatus);
if (shouldTrackLocked()) {
mBgJobCount++;
- incrementJobCountLocked(mPkg.userId, mPkg.packageName, 1);
+ if (mRegularJobTimer) {
+ incrementJobCountLocked(mPkg.userId, mPkg.packageName, 1);
+ }
if (mRunningBgJobs.size() == 1) {
// Started tracking the first job.
mStartTimeElapsed = sElapsedRealtimeClock.millis();
- // Starting the timer means that all cached execution stats are now incorrect.
- invalidateAllExecutionStatsLocked(mPkg.userId, mPkg.packageName);
+ if (mRegularJobTimer) {
+ // Starting the timer means that all cached execution stats are now
+ // incorrect.
+ invalidateAllExecutionStatsLocked(mPkg.userId, mPkg.packageName);
+ }
scheduleCutoff();
}
}
@@ -1529,13 +1882,15 @@ public final class QuotaController extends StateController {
return;
}
TimingSession ts = new TimingSession(mStartTimeElapsed, nowElapsed, mBgJobCount);
- saveTimingSession(mPkg.userId, mPkg.packageName, ts);
+ saveTimingSession(mPkg.userId, mPkg.packageName, ts, !mRegularJobTimer);
mBgJobCount = 0;
// Don't reset the tracked jobs list as we need to keep tracking the current number
// of jobs.
// However, cancel the currently scheduled cutoff since it's not currently useful.
cancelCutoff();
- incrementTimingSessionCountLocked(mPkg.userId, mPkg.packageName);
+ if (mRegularJobTimer) {
+ incrementTimingSessionCountLocked(mPkg.userId, mPkg.packageName);
+ }
}
/**
@@ -1582,10 +1937,13 @@ public final class QuotaController extends StateController {
// repeatedly plugged in and unplugged, or an app changes foreground state
// very frequently, the job count for a package may be artificially high.
mBgJobCount = mRunningBgJobs.size();
- incrementJobCountLocked(mPkg.userId, mPkg.packageName, mBgJobCount);
- // Starting the timer means that all cached execution stats are now
- // incorrect.
- invalidateAllExecutionStatsLocked(mPkg.userId, mPkg.packageName);
+
+ if (mRegularJobTimer) {
+ incrementJobCountLocked(mPkg.userId, mPkg.packageName, mBgJobCount);
+ // Starting the timer means that all cached execution stats are now
+ // incorrect.
+ invalidateAllExecutionStatsLocked(mPkg.userId, mPkg.packageName);
+ }
// Schedule cutoff since we're now actively tracking for quotas again.
scheduleCutoff();
}
@@ -1605,11 +1963,15 @@ public final class QuotaController extends StateController {
if (!isActive()) {
return;
}
- Message msg = mHandler.obtainMessage(MSG_REACHED_QUOTA, mPkg);
- final long timeRemainingMs = getTimeUntilQuotaConsumedLocked(mPkg.userId,
- mPkg.packageName);
+ Message msg = mHandler.obtainMessage(
+ mRegularJobTimer ? MSG_REACHED_QUOTA : MSG_REACHED_HPJ_QUOTA, mPkg);
+ final long timeRemainingMs = mRegularJobTimer
+ ? getTimeUntilQuotaConsumedLocked(mPkg.userId, mPkg.packageName)
+ : getTimeUntilHpjQuotaConsumedLocked(mPkg.userId, mPkg.packageName);
if (DEBUG) {
- Slog.i(TAG, "Job for " + mPkg + " has " + timeRemainingMs + "ms left.");
+ Slog.i(TAG,
+ (mRegularJobTimer ? "Regular job" : "HPJ") + " for " + mPkg + " has "
+ + timeRemainingMs + "ms left.");
}
// If the job was running the entire time, then the system would be up, so it's
// fine to use uptime millis for these messages.
@@ -1618,11 +1980,14 @@ public final class QuotaController extends StateController {
}
private void cancelCutoff() {
- mHandler.removeMessages(MSG_REACHED_QUOTA, mPkg);
+ mHandler.removeMessages(
+ mRegularJobTimer ? MSG_REACHED_QUOTA : MSG_REACHED_HPJ_QUOTA, mPkg);
}
public void dump(IndentingPrintWriter pw, Predicate predicate) {
- pw.print("Timer{");
+ pw.print("Timer<");
+ pw.print(mRegularJobTimer ? "REG" : " HPJ");
+ pw.print(">{");
pw.print(mPkg);
pw.print("} ");
if (isActive()) {
@@ -1668,6 +2033,98 @@ public final class QuotaController extends StateController {
}
}
+ private final class TopAppTimer {
+ private final Package mPkg;
+
+ // List of jobs currently running for this app that started when the app wasn't in the
+ // foreground.
+ private final SparseArray mActivities = new SparseArray<>();
+ private long mStartTimeElapsed;
+
+ TopAppTimer(int userId, String packageName) {
+ mPkg = new Package(userId, packageName);
+ }
+
+ void processEventLocked(@NonNull UsageEvents.Event event) {
+ final long nowElapsed = sElapsedRealtimeClock.millis();
+ switch (event.getEventType()) {
+ case UsageEvents.Event.ACTIVITY_RESUMED:
+ if (mActivities.size() == 0) {
+ mStartTimeElapsed = nowElapsed;
+ }
+ mActivities.put(event.mInstanceId, event);
+ break;
+ case UsageEvents.Event.ACTIVITY_PAUSED:
+ case UsageEvents.Event.ACTIVITY_STOPPED:
+ case UsageEvents.Event.ACTIVITY_DESTROYED:
+ final UsageEvents.Event existingEvent =
+ mActivities.removeReturnOld(event.mInstanceId);
+ if (existingEvent != null && mActivities.size() == 0) {
+ final long totalTopTimeMs = nowElapsed - mStartTimeElapsed;
+ int numTimeChunks = (int) (totalTopTimeMs / mHpjTopAppTimeChunkSizeMs);
+ final long remainderMs = totalTopTimeMs % mHpjTopAppTimeChunkSizeMs;
+ if (remainderMs >= SECOND_IN_MILLIS) {
+ // "Round up"
+ numTimeChunks++;
+ }
+ if (DEBUG) {
+ Slog.d(TAG,
+ "Crediting " + mPkg + " for " + numTimeChunks + " time chunks");
+ }
+ final ShrinkableDebits quota =
+ getHpjQuotaLocked(mPkg.userId, mPkg.packageName);
+ quota.transactOnDebitsLocked(-mHpjRewardTopAppMs * numTimeChunks);
+ if (maybeUpdateConstraintForPkgLocked(mPkg.userId, mPkg.packageName)) {
+ mStateChangedListener.onControllerStateChanged();
+ }
+ }
+ break;
+ }
+ }
+
+ boolean isActive() {
+ synchronized (mLock) {
+ return mActivities.size() > 0;
+ }
+ }
+
+ public void dump(IndentingPrintWriter pw) {
+ pw.print("TopAppTimer{");
+ pw.print(mPkg);
+ pw.print("} ");
+ if (isActive()) {
+ pw.print("started at ");
+ pw.print(mStartTimeElapsed);
+ pw.print(" (");
+ pw.print(sElapsedRealtimeClock.millis() - mStartTimeElapsed);
+ pw.print("ms ago)");
+ } else {
+ pw.print("NOT active");
+ }
+ pw.println();
+ pw.increaseIndent();
+ for (int i = 0; i < mActivities.size(); i++) {
+ UsageEvents.Event event = mActivities.valueAt(i);
+ pw.println(event.getClassName());
+ }
+ pw.decreaseIndent();
+ }
+
+ public void dump(ProtoOutputStream proto, long fieldId) {
+ final long token = proto.start(fieldId);
+
+ mPkg.dumpDebug(proto, StateControllerProto.QuotaController.TopAppTimer.PKG);
+ proto.write(StateControllerProto.QuotaController.TopAppTimer.IS_ACTIVE, isActive());
+ proto.write(StateControllerProto.QuotaController.TopAppTimer.START_TIME_ELAPSED,
+ mStartTimeElapsed);
+ proto.write(StateControllerProto.QuotaController.TopAppTimer.ACTIVITY_COUNT,
+ mActivities.size());
+ // TODO: maybe dump activities/events
+
+ proto.end(token);
+ }
+ }
+
/**
* Tracking of app assignments to standby buckets
*/
@@ -1693,8 +2150,14 @@ public final class QuotaController extends StateController {
}
List restrictedChanges = new ArrayList<>();
synchronized (mLock) {
+ ShrinkableDebits debits = mHpjStats.get(userId, packageName);
+ if (debits != null) {
+ debits.setStandbyBucketLocked(bucketIndex);
+ }
+
ArraySet jobs = mTrackedJobs.get(userId, packageName);
if (jobs == null || jobs.size() == 0) {
+ // Nothing further to do.
return;
}
for (int i = jobs.size() - 1; i >= 0; i--) {
@@ -1711,6 +2174,10 @@ public final class QuotaController extends StateController {
if (timer != null && timer.isActive()) {
timer.rescheduleCutoff();
}
+ timer = mHpjPkgTimers.get(userId, packageName);
+ if (timer != null && timer.isActive()) {
+ timer.rescheduleCutoff();
+ }
if (maybeUpdateConstraintForPkgLocked(userId, packageName)) {
mStateChangedListener.onControllerStateChanged();
}
@@ -1720,6 +2187,16 @@ public final class QuotaController extends StateController {
}
}
+ final class UsageEventTracker implements UsageEventListener {
+ /**
+ * Callback to inform listeners of a new event.
+ */
+ @Override
+ public void onUsageEvent(int userId, @NonNull UsageEvents.Event event) {
+ mHandler.obtainMessage(MSG_PROCESS_USAGE_EVENT, userId, 0, event);
+ }
+ }
+
private final class DeleteTimingSessionsFunctor implements Consumer> {
private final Predicate mTooOld = new Predicate() {
public boolean test(TimingSession ts) {
@@ -1742,9 +2219,13 @@ public final class QuotaController extends StateController {
@VisibleForTesting
void deleteObsoleteSessionsLocked() {
mTimingSessions.forEach(mDeleteOldSessionsFunctor);
+ // Don't delete HPJ timing sessions here. They'll be removed in
+ // getRemainingHpjExecutionTimeLocked().
}
private class QcHandler extends Handler {
+ private boolean mIsProcessing;
+
QcHandler(Looper looper) {
super(looper);
}
@@ -1752,6 +2233,8 @@ public final class QuotaController extends StateController {
@Override
public void handleMessage(Message msg) {
synchronized (mLock) {
+ mIsProcessing = true;
+
switch (msg.what) {
case MSG_REACHED_QUOTA: {
Package pkg = (Package) msg.obj;
@@ -1781,6 +2264,33 @@ public final class QuotaController extends StateController {
}
break;
}
+ case MSG_REACHED_HPJ_QUOTA: {
+ Package pkg = (Package) msg.obj;
+ if (DEBUG) {
+ Slog.d(TAG, "Checking if " + pkg + " has reached its HPJ quota.");
+ }
+
+ long timeRemainingMs = getRemainingHpjExecutionTimeLocked(
+ pkg.userId, pkg.packageName);
+ if (timeRemainingMs <= 0) {
+ if (DEBUG) Slog.d(TAG, pkg + " has reached its HPJ quota.");
+ if (maybeUpdateConstraintForPkgLocked(pkg.userId, pkg.packageName)) {
+ mStateChangedListener.onControllerStateChanged();
+ }
+ } else {
+ // This could potentially happen if an old session phases out while a
+ // job is currently running.
+ // Reschedule message
+ Message rescheduleMsg = obtainMessage(MSG_REACHED_HPJ_QUOTA, pkg);
+ timeRemainingMs = getTimeUntilHpjQuotaConsumedLocked(
+ pkg.userId, pkg.packageName);
+ if (DEBUG) {
+ Slog.d(TAG, pkg + " has " + timeRemainingMs + "ms left for HPJ");
+ }
+ sendMessageDelayed(rescheduleMsg, timeRemainingMs);
+ }
+ break;
+ }
case MSG_CLEAN_UP_SESSIONS:
if (DEBUG) {
Slog.d(TAG, "Cleaning up timing sessions.");
@@ -1816,7 +2326,8 @@ public final class QuotaController extends StateController {
isQuotaFree = false;
}
// Update Timers first.
- if (mPkgTimers.indexOfKey(userId) >= 0) {
+ if (mPkgTimers.indexOfKey(userId) >= 0
+ || mHpjPkgTimers.indexOfKey(userId) >= 0) {
ArraySet packages = mUidToPackageCache.get(uid);
if (packages == null) {
try {
@@ -1834,7 +2345,11 @@ public final class QuotaController extends StateController {
}
if (packages != null) {
for (int i = packages.size() - 1; i >= 0; --i) {
- Timer t = mPkgTimers.get(userId, packages.valueAt(i));
+ Timer t = mHpjPkgTimers.get(userId, packages.valueAt(i));
+ if (t != null) {
+ t.onStateChangedLocked(nowElapsed, isQuotaFree);
+ }
+ t = mPkgTimers.get(userId, packages.valueAt(i));
if (t != null) {
t.onStateChangedLocked(nowElapsed, isQuotaFree);
}
@@ -1847,8 +2362,46 @@ public final class QuotaController extends StateController {
}
break;
}
+ case MSG_PROCESS_USAGE_EVENT: {
+ final int userId = msg.arg1;
+ final UsageEvents.Event event = (UsageEvents.Event) msg.obj;
+ final String pkgName = event.getPackageName();
+ switch (event.getEventType()) {
+ case UsageEvents.Event.ACTIVITY_RESUMED:
+ case UsageEvents.Event.ACTIVITY_PAUSED:
+ case UsageEvents.Event.ACTIVITY_STOPPED:
+ case UsageEvents.Event.ACTIVITY_DESTROYED:
+ synchronized (mLock) {
+ TopAppTimer timer = mTopAppTrackers.get(userId, pkgName);
+ if (timer == null) {
+ timer = new TopAppTimer(userId, pkgName);
+ mTopAppTrackers.add(userId, pkgName, timer);
+ }
+ timer.processEventLocked(event);
+ }
+ break;
+ case UsageEvents.Event.USER_INTERACTION:
+ case UsageEvents.Event.CHOOSER_ACTION:
+ case UsageEvents.Event.NOTIFICATION_INTERRUPTION:
+ // Don't need to include SHORTCUT_INVOCATION. The app will be
+ // launched through it (if it's not already on top).
+ grantRewardForInstantEvent(
+ userId, pkgName, mHpjRewardInteractionMs);
+ break;
+ case UsageEvents.Event.NOTIFICATION_SEEN:
+ // Intentionally don't give too much for notification seen.
+ // Interactions will award more.
+ grantRewardForInstantEvent(
+ userId, pkgName, mHpjRewardNotificationSeenMs);
+ break;
+ }
+
+ break;
+ }
}
}
+
+ mIsProcessing = false;
}
}
@@ -2030,6 +2583,7 @@ public final class QuotaController extends StateController {
mQcConstants.mShouldReevaluateConstraints = false;
mQcConstants.mRateLimitingConstantsUpdated = false;
mQcConstants.mExecutionPeriodConstantsUpdated = false;
+ mQcConstants.mHpjLimitConstantsUpdated = false;
}
@Override
@@ -2055,6 +2609,7 @@ public final class QuotaController extends StateController {
private boolean mShouldReevaluateConstraints = false;
private boolean mRateLimitingConstantsUpdated = false;
private boolean mExecutionPeriodConstantsUpdated = false;
+ private boolean mHpjLimitConstantsUpdated = false;
/** Prefix to use with all constant keys in order to "sub-namespace" the keys. */
private static final String QC_CONSTANT_PREFIX = "qc_";
@@ -2128,6 +2683,36 @@ public final class QuotaController extends StateController {
@VisibleForTesting
static final String KEY_MIN_QUOTA_CHECK_DELAY_MS =
QC_CONSTANT_PREFIX + "min_quota_check_delay_ms";
+ @VisibleForTesting
+ static final String KEY_HPJ_LIMIT_ACTIVE_MS =
+ QC_CONSTANT_PREFIX + "hpj_limit_active_ms";
+ @VisibleForTesting
+ static final String KEY_HPJ_LIMIT_WORKING_MS =
+ QC_CONSTANT_PREFIX + "hpj_limit_working_ms";
+ @VisibleForTesting
+ static final String KEY_HPJ_LIMIT_FREQUENT_MS =
+ QC_CONSTANT_PREFIX + "hpj_limit_frequent_ms";
+ @VisibleForTesting
+ static final String KEY_HPJ_LIMIT_RARE_MS =
+ QC_CONSTANT_PREFIX + "hpj_limit_rare_ms";
+ @VisibleForTesting
+ static final String KEY_HPJ_LIMIT_RESTRICTED_MS =
+ QC_CONSTANT_PREFIX + "hpj_limit_restricted_ms";
+ @VisibleForTesting
+ static final String KEY_HPJ_WINDOW_SIZE_MS =
+ QC_CONSTANT_PREFIX + "hpj_window_size_ms";
+ @VisibleForTesting
+ static final String KEY_HPJ_TOP_APP_TIME_CHUNK_SIZE_MS =
+ QC_CONSTANT_PREFIX + "hpj_top_app_time_chunk_size_ms";
+ @VisibleForTesting
+ static final String KEY_HPJ_REWARD_TOP_APP_MS =
+ QC_CONSTANT_PREFIX + "hpj_reward_top_app_ms";
+ @VisibleForTesting
+ static final String KEY_HPJ_REWARD_INTERACTION_MS =
+ QC_CONSTANT_PREFIX + "hpj_reward_interaction_ms";
+ @VisibleForTesting
+ static final String KEY_HPJ_REWARD_NOTIFICATION_SEEN_MS =
+ QC_CONSTANT_PREFIX + "hpj_reward_notification_seen_ms";
private static final long DEFAULT_ALLOWED_TIME_PER_PERIOD_MS =
10 * 60 * 1000L; // 10 minutes
@@ -2169,6 +2754,16 @@ public final class QuotaController extends StateController {
private static final int DEFAULT_MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW = 20;
private static final long DEFAULT_TIMING_SESSION_COALESCING_DURATION_MS = 5000; // 5 seconds
private static final long DEFAULT_MIN_QUOTA_CHECK_DELAY_MS = MINUTE_IN_MILLIS;
+ private static final long DEFAULT_HPJ_LIMIT_ACTIVE_MS = 30 * MINUTE_IN_MILLIS;
+ private static final long DEFAULT_HPJ_LIMIT_WORKING_MS = DEFAULT_HPJ_LIMIT_ACTIVE_MS;
+ private static final long DEFAULT_HPJ_LIMIT_FREQUENT_MS = 10 * MINUTE_IN_MILLIS;
+ private static final long DEFAULT_HPJ_LIMIT_RARE_MS = DEFAULT_HPJ_LIMIT_FREQUENT_MS;
+ private static final long DEFAULT_HPJ_LIMIT_RESTRICTED_MS = 5 * MINUTE_IN_MILLIS;
+ private static final long DEFAULT_HPJ_WINDOW_SIZE_MS = 24 * HOUR_IN_MILLIS;
+ private static final long DEFAULT_HPJ_TOP_APP_TIME_CHUNK_SIZE_MS = 30 * SECOND_IN_MILLIS;
+ private static final long DEFAULT_HPJ_REWARD_TOP_APP_MS = 10 * SECOND_IN_MILLIS;
+ private static final long DEFAULT_HPJ_REWARD_INTERACTION_MS = 15 * SECOND_IN_MILLIS;
+ private static final long DEFAULT_HPJ_REWARD_NOTIFICATION_SEEN_MS = 0;
/** How much time each app will have to run jobs within their standby bucket window. */
public long ALLOWED_TIME_PER_PERIOD_MS = DEFAULT_ALLOWED_TIME_PER_PERIOD_MS;
@@ -2328,6 +2923,74 @@ public final class QuotaController extends StateController {
/** The minimum value that {@link #RATE_LIMITING_WINDOW_MS} can have. */
private static final long MIN_RATE_LIMITING_WINDOW_MS = 30 * SECOND_IN_MILLIS;
+ /**
+ * The total session limit of the particular standby bucket. Apps in this standby bucket
+ * can
+ * only have HPJ sessions totalling HPJ_LIMIT (without factoring in any rewards or free
+ * HPJs).
+ */
+ public long HPJ_LIMIT_ACTIVE_MS = DEFAULT_HPJ_LIMIT_ACTIVE_MS;
+
+ /**
+ * The total session limit of the particular standby bucket. Apps in this standby bucket
+ * can
+ * only have HPJ sessions totalling HPJ_LIMIT (without factoring in any rewards or free
+ * HPJs).
+ */
+ public long HPJ_LIMIT_WORKING_MS = DEFAULT_HPJ_LIMIT_WORKING_MS;
+
+ /**
+ * The total session limit of the particular standby bucket. Apps in this standby bucket
+ * can
+ * only have HPJ sessions totalling HPJ_LIMIT (without factoring in any rewards or free
+ * HPJs).
+ */
+ public long HPJ_LIMIT_FREQUENT_MS = DEFAULT_HPJ_LIMIT_FREQUENT_MS;
+
+ /**
+ * The total session limit of the particular standby bucket. Apps in this standby bucket
+ * can
+ * only have HPJ sessions totalling HPJ_LIMIT (without factoring in any rewards or free
+ * HPJs).
+ */
+ public long HPJ_LIMIT_RARE_MS = DEFAULT_HPJ_LIMIT_RARE_MS;
+
+ /**
+ * The total session limit of the particular standby bucket. Apps in this standby bucket
+ * can
+ * only have HPJ sessions totalling HPJ_LIMIT (without factoring in any rewards or free
+ * HPJs).
+ */
+ public long HPJ_LIMIT_RESTRICTED_MS = DEFAULT_HPJ_LIMIT_RESTRICTED_MS;
+
+ /**
+ * The period of time used to calculate HPJ sessions. Apps can only have HPJ sessions
+ * totalling HPJ_LIMIT__MS within this period of time (without factoring in any
+ * rewards or free HPJs).
+ */
+ public long HPJ_WINDOW_SIZE_MS = DEFAULT_HPJ_WINDOW_SIZE_MS;
+
+ /**
+ * Length of time used to split an app's top time into chunks.
+ */
+ public long HPJ_TOP_APP_TIME_CHUNK_SIZE_MS = DEFAULT_HPJ_TOP_APP_TIME_CHUNK_SIZE_MS;
+
+ /**
+ * How much HPJ quota to give back to an app based on the number of top app time chunks it
+ * had.
+ */
+ public long HPJ_REWARD_TOP_APP_MS = DEFAULT_HPJ_REWARD_TOP_APP_MS;
+
+ /**
+ * How much HPJ quota to give back to an app based on each non-top user interaction.
+ */
+ public long HPJ_REWARD_INTERACTION_MS = DEFAULT_HPJ_REWARD_INTERACTION_MS;
+
+ /**
+ * How much HPJ quota to give back to an app based on each notification seen event.
+ */
+ public long HPJ_REWARD_NOTIFICATION_SEEN_MS = DEFAULT_HPJ_REWARD_NOTIFICATION_SEEN_MS;
+
public void processConstantLocked(@NonNull DeviceConfig.Properties properties,
@NonNull String key) {
switch (key) {
@@ -2348,6 +3011,15 @@ public final class QuotaController extends StateController {
updateRateLimitingConstantsLocked();
break;
+ case KEY_HPJ_LIMIT_ACTIVE_MS:
+ case KEY_HPJ_LIMIT_WORKING_MS:
+ case KEY_HPJ_LIMIT_FREQUENT_MS:
+ case KEY_HPJ_LIMIT_RARE_MS:
+ case KEY_HPJ_LIMIT_RESTRICTED_MS:
+ case KEY_HPJ_WINDOW_SIZE_MS:
+ updateHpjLimitConstantsLocked();
+ break;
+
case KEY_MAX_JOB_COUNT_ACTIVE:
MAX_JOB_COUNT_ACTIVE = properties.getInt(key, DEFAULT_MAX_JOB_COUNT_ACTIVE);
int newActiveMaxJobCount = Math.max(MIN_BUCKET_JOB_COUNT, MAX_JOB_COUNT_ACTIVE);
@@ -2458,6 +3130,64 @@ public final class QuotaController extends StateController {
mInQuotaAlarmListener.setMinQuotaCheckDelayMs(
Math.min(15 * MINUTE_IN_MILLIS, Math.max(0, MIN_QUOTA_CHECK_DELAY_MS)));
break;
+ case KEY_HPJ_TOP_APP_TIME_CHUNK_SIZE_MS:
+ // We don't need to re-evaluate execution stats or constraint status for this.
+ HPJ_TOP_APP_TIME_CHUNK_SIZE_MS =
+ properties.getLong(key, DEFAULT_HPJ_TOP_APP_TIME_CHUNK_SIZE_MS);
+ // Limit chunking to be in the range [1 millisecond, 15 minutes] per event.
+ long newChunkSizeMs = Math.min(15 * MINUTE_IN_MILLIS,
+ Math.max(1, HPJ_TOP_APP_TIME_CHUNK_SIZE_MS));
+ if (mHpjTopAppTimeChunkSizeMs != newChunkSizeMs) {
+ mHpjTopAppTimeChunkSizeMs = newChunkSizeMs;
+ if (mHpjTopAppTimeChunkSizeMs < mHpjRewardTopAppMs) {
+ // Not making chunk sizes and top rewards to be the upper/lower
+ // limits of the other to allow trying different policies. Just log
+ // the discrepancy.
+ Slog.w(TAG, "HPJ top app time chunk less than reward: "
+ + mHpjTopAppTimeChunkSizeMs + " vs " + mHpjRewardTopAppMs);
+ }
+ }
+ break;
+ case KEY_HPJ_REWARD_TOP_APP_MS:
+ // We don't need to re-evaluate execution stats or constraint status for this.
+ HPJ_REWARD_TOP_APP_MS =
+ properties.getLong(key, DEFAULT_HPJ_REWARD_TOP_APP_MS);
+ // Limit top reward to be in the range [10 seconds, 15 minutes] per event.
+ long newTopReward = Math.min(15 * MINUTE_IN_MILLIS,
+ Math.max(10 * SECOND_IN_MILLIS, HPJ_REWARD_TOP_APP_MS));
+ if (mHpjRewardTopAppMs != newTopReward) {
+ mHpjRewardTopAppMs = newTopReward;
+ if (mHpjTopAppTimeChunkSizeMs < mHpjRewardTopAppMs) {
+ // Not making chunk sizes and top rewards to be the upper/lower
+ // limits of the other to allow trying different policies. Just log
+ // the discrepancy.
+ Slog.w(TAG, "HPJ top app time chunk less than reward: "
+ + mHpjTopAppTimeChunkSizeMs + " vs " + mHpjRewardTopAppMs);
+ }
+ }
+ break;
+ case KEY_HPJ_REWARD_INTERACTION_MS:
+ // We don't need to re-evaluate execution stats or constraint status for this.
+ HPJ_REWARD_INTERACTION_MS =
+ properties.getLong(key, DEFAULT_HPJ_REWARD_INTERACTION_MS);
+ // Limit interaction reward to be in the range [5 seconds, 15 minutes] per
+ // event.
+ long newInteractionReward = Math.min(15 * MINUTE_IN_MILLIS,
+ Math.max(5 * SECOND_IN_MILLIS, HPJ_REWARD_INTERACTION_MS));
+ if (mHpjRewardInteractionMs != newInteractionReward) {
+ mHpjRewardInteractionMs = newInteractionReward;
+ }
+ break;
+ case KEY_HPJ_REWARD_NOTIFICATION_SEEN_MS:
+ // We don't need to re-evaluate execution stats or constraint status for this.
+ HPJ_REWARD_NOTIFICATION_SEEN_MS =
+ properties.getLong(key, DEFAULT_HPJ_REWARD_NOTIFICATION_SEEN_MS);
+ // Limit notification seen reward to be in the range [0, 5] minutes per event.
+ long newNotiSeenReward = Math.min(5 * MINUTE_IN_MILLIS,
+ Math.max(0, HPJ_REWARD_NOTIFICATION_SEEN_MS));
+ if (mHpjRewardNotificationSeenMs != newNotiSeenReward) {
+ mHpjRewardNotificationSeenMs = newNotiSeenReward;
+ }
}
}
@@ -2598,6 +3328,75 @@ public final class QuotaController extends StateController {
}
}
+ private void updateHpjLimitConstantsLocked() {
+ if (mHpjLimitConstantsUpdated) {
+ return;
+ }
+ mHpjLimitConstantsUpdated = true;
+
+ // Query the values as an atomic set.
+ final DeviceConfig.Properties properties = DeviceConfig.getProperties(
+ DeviceConfig.NAMESPACE_JOB_SCHEDULER,
+ KEY_HPJ_LIMIT_ACTIVE_MS, KEY_HPJ_LIMIT_WORKING_MS,
+ KEY_HPJ_LIMIT_FREQUENT_MS, KEY_HPJ_LIMIT_RARE_MS,
+ KEY_HPJ_LIMIT_RESTRICTED_MS, KEY_HPJ_WINDOW_SIZE_MS);
+ HPJ_LIMIT_ACTIVE_MS = properties.getLong(
+ KEY_HPJ_LIMIT_ACTIVE_MS, DEFAULT_HPJ_LIMIT_ACTIVE_MS);
+ HPJ_LIMIT_WORKING_MS = properties.getLong(
+ KEY_HPJ_LIMIT_WORKING_MS, DEFAULT_HPJ_LIMIT_WORKING_MS);
+ HPJ_LIMIT_FREQUENT_MS = properties.getLong(
+ KEY_HPJ_LIMIT_FREQUENT_MS, DEFAULT_HPJ_LIMIT_FREQUENT_MS);
+ HPJ_LIMIT_RARE_MS = properties.getLong(
+ KEY_HPJ_LIMIT_RARE_MS, DEFAULT_HPJ_LIMIT_RARE_MS);
+ HPJ_LIMIT_RESTRICTED_MS = properties.getLong(
+ KEY_HPJ_LIMIT_RESTRICTED_MS, DEFAULT_HPJ_LIMIT_RESTRICTED_MS);
+ HPJ_WINDOW_SIZE_MS = properties.getLong(
+ KEY_HPJ_WINDOW_SIZE_MS, DEFAULT_HPJ_WINDOW_SIZE_MS);
+
+ // The window must be in the range [1 hour, 24 hours].
+ long newWindowSizeMs = Math.max(HOUR_IN_MILLIS,
+ Math.min(MAX_PERIOD_MS, HPJ_WINDOW_SIZE_MS));
+ if (mHpjLimitWindowSizeMs != newWindowSizeMs) {
+ mHpjLimitWindowSizeMs = newWindowSizeMs;
+ mShouldReevaluateConstraints = true;
+ }
+ // The limit must be in the range [15 minutes, window size].
+ long newActiveLimitMs = Math.max(15 * MINUTE_IN_MILLIS,
+ Math.min(newWindowSizeMs, HPJ_LIMIT_ACTIVE_MS));
+ if (mHpjLimitsMs[ACTIVE_INDEX] != newActiveLimitMs) {
+ mHpjLimitsMs[ACTIVE_INDEX] = newActiveLimitMs;
+ mShouldReevaluateConstraints = true;
+ }
+ // The limit must be in the range [15 minutes, active limit].
+ long newWorkingLimitMs = Math.max(15 * MINUTE_IN_MILLIS,
+ Math.min(newActiveLimitMs, HPJ_LIMIT_WORKING_MS));
+ if (mHpjLimitsMs[WORKING_INDEX] != newWorkingLimitMs) {
+ mHpjLimitsMs[WORKING_INDEX] = newWorkingLimitMs;
+ mShouldReevaluateConstraints = true;
+ }
+ // The limit must be in the range [10 minutes, working limit].
+ long newFrequentLimitMs = Math.max(10 * MINUTE_IN_MILLIS,
+ Math.min(newWorkingLimitMs, HPJ_LIMIT_FREQUENT_MS));
+ if (mHpjLimitsMs[FREQUENT_INDEX] != newFrequentLimitMs) {
+ mHpjLimitsMs[FREQUENT_INDEX] = newFrequentLimitMs;
+ mShouldReevaluateConstraints = true;
+ }
+ // The limit must be in the range [10 minutes, frequent limit].
+ long newRareLimitMs = Math.max(10 * MINUTE_IN_MILLIS,
+ Math.min(newFrequentLimitMs, HPJ_LIMIT_RARE_MS));
+ if (mHpjLimitsMs[RARE_INDEX] != newRareLimitMs) {
+ mHpjLimitsMs[RARE_INDEX] = newRareLimitMs;
+ mShouldReevaluateConstraints = true;
+ }
+ // The limit must be in the range [0 minutes, rare limit].
+ long newRestrictedLimitMs = Math.max(0,
+ Math.min(newRareLimitMs, HPJ_LIMIT_RESTRICTED_MS));
+ if (mHpjLimitsMs[RESTRICTED_INDEX] != newRestrictedLimitMs) {
+ mHpjLimitsMs[RESTRICTED_INDEX] = newRestrictedLimitMs;
+ mShouldReevaluateConstraints = true;
+ }
+ }
+
private void dump(IndentingPrintWriter pw) {
pw.println();
pw.println("QuotaController:");
@@ -2628,6 +3427,19 @@ public final class QuotaController extends StateController {
pw.print(KEY_TIMING_SESSION_COALESCING_DURATION_MS,
TIMING_SESSION_COALESCING_DURATION_MS).println();
pw.print(KEY_MIN_QUOTA_CHECK_DELAY_MS, MIN_QUOTA_CHECK_DELAY_MS).println();
+
+ pw.print(KEY_HPJ_LIMIT_ACTIVE_MS, HPJ_LIMIT_ACTIVE_MS).println();
+ pw.print(KEY_HPJ_LIMIT_WORKING_MS, HPJ_LIMIT_WORKING_MS).println();
+ pw.print(KEY_HPJ_LIMIT_FREQUENT_MS, HPJ_LIMIT_FREQUENT_MS).println();
+ pw.print(KEY_HPJ_LIMIT_RARE_MS, HPJ_LIMIT_RARE_MS).println();
+ pw.print(KEY_HPJ_LIMIT_RESTRICTED_MS, HPJ_LIMIT_RESTRICTED_MS).println();
+ pw.print(KEY_HPJ_WINDOW_SIZE_MS, HPJ_WINDOW_SIZE_MS).println();
+ pw.print(KEY_HPJ_TOP_APP_TIME_CHUNK_SIZE_MS, HPJ_TOP_APP_TIME_CHUNK_SIZE_MS).println();
+ pw.print(KEY_HPJ_REWARD_TOP_APP_MS, HPJ_REWARD_TOP_APP_MS).println();
+ pw.print(KEY_HPJ_REWARD_INTERACTION_MS, HPJ_REWARD_INTERACTION_MS).println();
+ pw.print(KEY_HPJ_REWARD_NOTIFICATION_SEEN_MS,
+ HPJ_REWARD_NOTIFICATION_SEEN_MS).println();
+
pw.decreaseIndent();
}
@@ -2675,6 +3487,24 @@ public final class QuotaController extends StateController {
TIMING_SESSION_COALESCING_DURATION_MS);
proto.write(ConstantsProto.QuotaController.MIN_QUOTA_CHECK_DELAY_MS,
MIN_QUOTA_CHECK_DELAY_MS);
+
+ proto.write(ConstantsProto.QuotaController.HPJ_LIMIT_ACTIVE_MS, HPJ_LIMIT_ACTIVE_MS);
+ proto.write(ConstantsProto.QuotaController.HPJ_LIMIT_WORKING_MS, HPJ_LIMIT_WORKING_MS);
+ proto.write(ConstantsProto.QuotaController.HPJ_LIMIT_FREQUENT_MS,
+ HPJ_LIMIT_FREQUENT_MS);
+ proto.write(ConstantsProto.QuotaController.HPJ_LIMIT_RARE_MS, HPJ_LIMIT_RARE_MS);
+ proto.write(ConstantsProto.QuotaController.HPJ_LIMIT_RESTRICTED_MS,
+ HPJ_LIMIT_RESTRICTED_MS);
+ proto.write(ConstantsProto.QuotaController.HPJ_WINDOW_SIZE_MS, HPJ_WINDOW_SIZE_MS);
+ proto.write(ConstantsProto.QuotaController.HPJ_TOP_APP_TIME_CHUNK_SIZE_MS,
+ HPJ_TOP_APP_TIME_CHUNK_SIZE_MS);
+ proto.write(ConstantsProto.QuotaController.HPJ_REWARD_TOP_APP_MS,
+ HPJ_REWARD_TOP_APP_MS);
+ proto.write(ConstantsProto.QuotaController.HPJ_REWARD_INTERACTION_MS,
+ HPJ_REWARD_INTERACTION_MS);
+ proto.write(ConstantsProto.QuotaController.HPJ_REWARD_NOTIFICATION_SEEN_MS,
+ HPJ_REWARD_NOTIFICATION_SEEN_MS);
+
proto.end(qcToken);
}
}
@@ -2716,6 +3546,49 @@ public final class QuotaController extends StateController {
return mHandler;
}
+ @VisibleForTesting
+ @NonNull
+ long[] getHpjLimitsMs() {
+ return mHpjLimitsMs;
+ }
+
+ @VisibleForTesting
+ @NonNull
+ long getHpjLimitWindowSizeMs() {
+ return mHpjLimitWindowSizeMs;
+ }
+
+ @VisibleForTesting
+ @NonNull
+ long getHpjRewardInteractionMs() {
+ return mHpjRewardInteractionMs;
+ }
+
+ @VisibleForTesting
+ @NonNull
+ long getHpjRewardNotificationSeenMs() {
+ return mHpjRewardNotificationSeenMs;
+ }
+
+ @VisibleForTesting
+ @NonNull
+ long getHpjRewardTopAppMs() {
+ return mHpjRewardTopAppMs;
+ }
+
+
+ @VisibleForTesting
+ @Nullable
+ List getHpjTimingSessions(int userId, String packageName) {
+ return mHpjTimingSessions.get(userId, packageName);
+ }
+
+ @VisibleForTesting
+ @NonNull
+ long getHpjTopAppTimeChunkSizeMs() {
+ return mHpjTopAppTimeChunkSizeMs;
+ }
+
@VisibleForTesting
long getInQuotaBufferMs() {
return mQuotaBufferMs;
@@ -2763,6 +3636,11 @@ public final class QuotaController extends StateController {
return mQcConstants;
}
+ @VisibleForTesting
+ boolean isActiveBackgroundProcessing() {
+ return mHandler.mIsProcessing;
+ }
+
//////////////////////////// DATA DUMP //////////////////////////////
@Override
@@ -2805,16 +3683,24 @@ public final class QuotaController extends StateController {
pw.increaseIndent();
pw.print(JobStatus.bucketName(js.getEffectiveStandbyBucket()));
pw.print(", ");
- if (js.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA)) {
- pw.print("within quota");
+ if (js.shouldTreatAsForegroundJob()) {
+ pw.print("within HPJ quota");
+ } else if (js.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA)) {
+ pw.print("within regular quota");
} else {
pw.print("not within quota");
}
pw.print(", ");
- pw.print(getRemainingExecutionTimeLocked(js));
- pw.print("ms remaining in quota");
- pw.decreaseIndent();
+ if (js.shouldTreatAsForegroundJob()) {
+ pw.print(getRemainingHpjExecutionTimeLocked(
+ js.getSourceUserId(), js.getSourcePackageName()));
+ pw.print("ms remaining in HPJ quota");
+ } else {
+ pw.print(getRemainingExecutionTimeLocked(js));
+ pw.print("ms remaining in quota");
+ }
pw.println();
+ pw.decreaseIndent();
}
});
@@ -2841,6 +3727,33 @@ public final class QuotaController extends StateController {
}
}
+ pw.println();
+ for (int u = 0; u < mHpjPkgTimers.numMaps(); ++u) {
+ final int userId = mHpjPkgTimers.keyAt(u);
+ for (int p = 0; p < mHpjPkgTimers.numElementsForKey(userId); ++p) {
+ final String pkgName = mHpjPkgTimers.keyAt(u, p);
+ mHpjPkgTimers.valueAt(u, p).dump(pw, predicate);
+ pw.println();
+ List sessions = mHpjTimingSessions.get(userId, pkgName);
+ if (sessions != null) {
+ pw.increaseIndent();
+ pw.println("Saved sessions:");
+ pw.increaseIndent();
+ for (int j = sessions.size() - 1; j >= 0; j--) {
+ TimingSession session = sessions.get(j);
+ session.dump(pw);
+ }
+ pw.decreaseIndent();
+ pw.decreaseIndent();
+ pw.println();
+ }
+ }
+ }
+
+ pw.println();
+ mTopAppTrackers.forEach((timer) -> timer.dump(pw));
+
+ pw.println();
pw.println("Cached execution stats:");
pw.increaseIndent();
for (int u = 0; u < mExecutionStatsCache.numMaps(); ++u) {
@@ -2864,6 +3777,22 @@ public final class QuotaController extends StateController {
}
pw.decreaseIndent();
+ pw.println();
+ pw.println("HPJ debits:");
+ pw.increaseIndent();
+ for (int u = 0; u < mHpjStats.numMaps(); ++u) {
+ final int userId = mHpjStats.keyAt(u);
+ for (int p = 0; p < mHpjStats.numElementsForKey(userId); ++p) {
+ final String pkgName = mHpjStats.keyAt(u, p);
+ ShrinkableDebits debits = mHpjStats.valueAt(u, p);
+
+ pw.print(string(userId, pkgName));
+ pw.print(": ");
+ debits.dumpLocked(pw);
+ }
+ }
+ pw.decreaseIndent();
+
pw.println();
mInQuotaAlarmListener.dumpLocked(pw);
pw.decreaseIndent();
@@ -2919,6 +3848,12 @@ public final class QuotaController extends StateController {
js.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA));
proto.write(StateControllerProto.QuotaController.TrackedJob.REMAINING_QUOTA_MS,
getRemainingExecutionTimeLocked(js));
+ proto.write(
+ StateControllerProto.QuotaController.TrackedJob.IS_REQUESTED_FOREGROUND_JOB,
+ js.isRequestedForegroundJob());
+ proto.write(
+ StateControllerProto.QuotaController.TrackedJob.IS_WITHIN_FG_JOB_QUOTA,
+ js.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_HPJ_QUOTA));
proto.end(jsToken);
}
});
@@ -2929,8 +3864,15 @@ public final class QuotaController extends StateController {
final String pkgName = mPkgTimers.keyAt(u, p);
final long psToken = proto.start(
StateControllerProto.QuotaController.PACKAGE_STATS);
+
mPkgTimers.valueAt(u, p).dump(proto,
StateControllerProto.QuotaController.PackageStats.TIMER, predicate);
+ final Timer hpjTimer = mHpjPkgTimers.get(userId, pkgName);
+ if (hpjTimer != null) {
+ hpjTimer.dump(proto,
+ StateControllerProto.QuotaController.PackageStats.FG_JOB_TIMER,
+ predicate);
+ }
List sessions = mTimingSessions.get(userId, pkgName);
if (sessions != null) {
diff --git a/core/api/current.txt b/core/api/current.txt
index 5af08e33d9e15..d92214a105e0f 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -7686,6 +7686,7 @@ package android.app.job {
method public long getTriggerContentMaxDelay();
method public long getTriggerContentUpdateDelay();
method @Nullable public android.app.job.JobInfo.TriggerContentUri[] getTriggerContentUris();
+ method public boolean isForegroundJob();
method public boolean isImportantWhileForeground();
method public boolean isPeriodic();
method public boolean isPersisted();
@@ -7717,7 +7718,8 @@ package android.app.job {
method public android.app.job.JobInfo.Builder setClipData(@Nullable android.content.ClipData, int);
method public android.app.job.JobInfo.Builder setEstimatedNetworkBytes(long, long);
method public android.app.job.JobInfo.Builder setExtras(@NonNull android.os.PersistableBundle);
- method public android.app.job.JobInfo.Builder setImportantWhileForeground(boolean);
+ method @NonNull public android.app.job.JobInfo.Builder setForeground(boolean);
+ method @Deprecated public android.app.job.JobInfo.Builder setImportantWhileForeground(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);
@@ -7757,6 +7759,7 @@ package android.app.job {
method @NonNull public android.os.Bundle getTransientExtras();
method @Nullable public String[] getTriggeredContentAuthorities();
method @Nullable public android.net.Uri[] getTriggeredContentUris();
+ method public boolean isForegroundJob();
method public boolean isOverrideDeadlineExpired();
method public void writeToParcel(android.os.Parcel, int);
field @NonNull public static final android.os.Parcelable.Creator CREATOR;
diff --git a/core/java/android/app/usage/UsageEvents.java b/core/java/android/app/usage/UsageEvents.java
index 3522b1b8aff57..081f4fdc1b12b 100644
--- a/core/java/android/app/usage/UsageEvents.java
+++ b/core/java/android/app/usage/UsageEvents.java
@@ -383,6 +383,10 @@ public final class UsageEvents implements Parcelable {
public int mClassToken = UNASSIGNED_TOKEN;
/**
+ * Uniquely identifies an activity. It's possible for two activities with the same
+ * pkg/class name to be in lifecycle at the same time. The mInstanceId is guaranteed to be
+ * unique per activity across all apps (not just within a single app).
+ *
* {@hide}
*/
public int mInstanceId;
diff --git a/core/proto/android/server/jobscheduler.proto b/core/proto/android/server/jobscheduler.proto
index d187220491093..91131115da83c 100644
--- a/core/proto/android/server/jobscheduler.proto
+++ b/core/proto/android/server/jobscheduler.proto
@@ -313,7 +313,41 @@ message ConstantsProto {
// The minimum amount of time between quota check alarms.
optional int64 min_quota_check_delay_ms = 23;
- // Next tag: 24
+ // The total session limit of the particular standby bucket. Apps in this standby bucket can
+ // only have HPJ sessions totalling HPJ_LIMIT (without factoring in any rewards or free
+ // HPJs).
+ optional int64 hpj_limit_active_ms = 24;
+ // The total session limit of the particular standby bucket. Apps in this standby bucket can
+ // only have HPJ sessions totalling HPJ_LIMIT (without factoring in any rewards or free
+ // HPJs).
+ optional int64 hpj_limit_working_ms = 25;
+ // The total session limit of the particular standby bucket. Apps in this standby bucket can
+ // only have HPJ sessions totalling HPJ_LIMIT (without factoring in any rewards or free
+ // HPJs).
+ optional int64 hpj_limit_frequent_ms = 26;
+ // The total session limit of the particular standby bucket. Apps in this standby bucket can
+ // only have HPJ sessions totalling HPJ_LIMIT (without factoring in any rewards or free
+ // HPJs).
+ optional int64 hpj_limit_rare_ms = 27;
+ // The total session limit of the particular standby bucket. Apps in this standby bucket can
+ // only have HPJ sessions totalling HPJ_LIMIT (without factoring in any rewards or free
+ // HPJs).
+ optional int64 hpj_limit_restricted_ms = 28;
+ // The period of time used to calculate HPJ sessions. Apps can only have HPJ sessions
+ // totalling HPJ_LIMIT__MS within this period of time (without factoring in any
+ // rewards or free HPJs).
+ optional int64 hpj_window_size_ms = 29;
+ // Length of time used to split an app's top time into chunks.
+ optional int64 hpj_top_app_time_chunk_size_ms = 30;
+ // How much HPJ quota to give back to an app based on the number of top app time chunks
+ // it had.
+ optional int64 hpj_reward_top_app_ms = 31;
+ // How much HPJ quota to give back to an app based on each non-top user interaction.
+ optional int64 hpj_reward_interaction_ms = 32;
+ // How much HPJ quota to give back to an app based on each notification seen event.
+ optional int64 hpj_reward_notification_seen_ms = 33;
+
+ // Next tag: 34
}
optional QuotaController quota_controller = 24;
@@ -560,6 +594,11 @@ message StateControllerProto {
// The amount of time that this job has remaining in its quota. This
// can be negative if the job is out of quota.
optional int64 remaining_quota_ms = 6;
+ // True if the app has requested that this be a foreground job.
+ optional bool is_requested_foreground_job = 7;
+ // True if this job is within the foreground quota bounds and is therefore allowed to
+ // run as a foreground job. Valid only if is_foreground_requested_job is true.
+ optional bool is_within_fg_job_quota = 8;
}
repeated TrackedJob tracked_jobs = 4;
@@ -665,6 +704,19 @@ message StateControllerProto {
repeated JobStatusShortInfoProto running_jobs = 5;
}
+ message TopAppTimer {
+ option (.android.msg_privacy).dest = DEST_AUTOMATIC;
+
+ optional Package pkg = 1;
+ // True if the Timer is actively tracking jobs.
+ optional bool is_active = 2;
+ // The time this timer last became active. Only valid if is_active is true.
+ optional int64 start_time_elapsed = 3;
+ // How many activities are currently in the RESUMED state. Valid only if is_active is
+ // true.
+ optional int32 activity_count = 4;
+ }
+
message PackageStats {
option (.android.msg_privacy).dest = DEST_AUTOMATIC;
@@ -677,6 +729,8 @@ message StateControllerProto {
repeated ExecutionStats execution_stats = 4;
reserved 5; // in_quota_alarm_listener
+
+ optional Timer fg_job_timer = 6;
}
repeated PackageStats package_stats = 5;
diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/ConnectivityControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/ConnectivityControllerTest.java
index 7f86faa4b3936..78bcc13c92650 100644
--- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/ConnectivityControllerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/ConnectivityControllerTest.java
@@ -145,7 +145,7 @@ public class ConnectivityControllerTest {
final ConnectivityController controller = new ConnectivityController(mService);
when(mService.getMaxJobExecutionTimeMs(any()))
- .thenReturn(JobServiceContext.EXECUTING_TIMESLICE_MILLIS);
+ .thenReturn(JobServiceContext.DEFAULT_EXECUTING_TIMESLICE_MILLIS);
// Slow network is too slow
assertFalse(controller.isSatisfied(createJobStatus(job), net,
diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java
index 8795d7711c633..4f3564d704162 100644
--- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java
@@ -35,6 +35,7 @@ import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
@@ -42,6 +43,7 @@ import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.Mockito.atLeast;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.never;
@@ -76,6 +78,7 @@ import android.util.SparseBooleanArray;
import androidx.test.runner.AndroidJUnit4;
+import com.android.internal.util.ArrayUtils;
import com.android.server.LocalServices;
import com.android.server.job.JobSchedulerService;
import com.android.server.job.JobSchedulerService.Constants;
@@ -269,8 +272,9 @@ public class QuotaControllerTest {
verify(foregroundUids, timeout(2 * SECOND_IN_MILLIS).times(1)).delete(eq(uid));
assertFalse(foregroundUids.get(uid));
}
- } catch (RemoteException e) {
- fail("registerUidObserver threw exception: " + e.getMessage());
+ waitForQuietBackground();
+ } catch (Exception e) {
+ fail("exception encountered: " + e.getMessage());
}
}
@@ -323,6 +327,14 @@ public class QuotaControllerTest {
return createJobStatus(testTag, SOURCE_PACKAGE, CALLING_UID, jobInfo);
}
+ private JobStatus createHpjJobStatus(String testTag, int jobId) {
+ JobInfo jobInfo = new JobInfo.Builder(jobId,
+ new ComponentName(mContext, "TestQuotaHpjJobService"))
+ .setForeground(true)
+ .build();
+ return createJobStatus(testTag, SOURCE_PACKAGE, CALLING_UID, jobInfo);
+ }
+
private JobStatus createJobStatus(String testTag, String packageName, int callingUid,
JobInfo jobInfo) {
JobStatus js = JobStatus.createFromJobInfo(
@@ -356,26 +368,49 @@ public class QuotaControllerTest {
}
}
+ private void waitForQuietBackground() throws Exception {
+ for (int i = 0; i < 5; ++i) {
+ if (!mQuotaController.isActiveBackgroundProcessing()) {
+ break;
+ }
+ Thread.sleep(500);
+ }
+ }
+
@Test
public void testSaveTimingSession() {
assertNull(mQuotaController.getTimingSessions(0, "com.android.test"));
- List expected = new ArrayList<>();
+ List expectedRegular = new ArrayList<>();
+ List expectedHpj = new ArrayList<>();
TimingSession one = new TimingSession(1, 10, 1);
TimingSession two = new TimingSession(11, 20, 2);
TimingSession thr = new TimingSession(21, 30, 3);
+ TimingSession fou = new TimingSession(31, 40, 4);
- mQuotaController.saveTimingSession(0, "com.android.test", one);
- expected.add(one);
- assertEquals(expected, mQuotaController.getTimingSessions(0, "com.android.test"));
+ mQuotaController.saveTimingSession(0, "com.android.test", one, false);
+ expectedRegular.add(one);
+ assertEquals(expectedRegular, mQuotaController.getTimingSessions(0, "com.android.test"));
+ assertTrue(
+ ArrayUtils.isEmpty(mQuotaController.getHpjTimingSessions(0, "com.android.test")));
- mQuotaController.saveTimingSession(0, "com.android.test", two);
- expected.add(two);
- assertEquals(expected, mQuotaController.getTimingSessions(0, "com.android.test"));
+ mQuotaController.saveTimingSession(0, "com.android.test", two, false);
+ expectedRegular.add(two);
+ assertEquals(expectedRegular, mQuotaController.getTimingSessions(0, "com.android.test"));
+ assertTrue(
+ ArrayUtils.isEmpty(mQuotaController.getHpjTimingSessions(0, "com.android.test")));
- mQuotaController.saveTimingSession(0, "com.android.test", thr);
- expected.add(thr);
- assertEquals(expected, mQuotaController.getTimingSessions(0, "com.android.test"));
+ mQuotaController.saveTimingSession(0, "com.android.test", thr, true);
+ expectedHpj.add(thr);
+ assertEquals(expectedRegular, mQuotaController.getTimingSessions(0, "com.android.test"));
+ assertEquals(expectedHpj, mQuotaController.getHpjTimingSessions(0, "com.android.test"));
+
+ mQuotaController.saveTimingSession(0, "com.android.test", fou, false);
+ mQuotaController.saveTimingSession(0, "com.android.test", fou, true);
+ expectedRegular.add(fou);
+ expectedHpj.add(fou);
+ assertEquals(expectedRegular, mQuotaController.getTimingSessions(0, "com.android.test"));
+ assertEquals(expectedHpj, mQuotaController.getHpjTimingSessions(0, "com.android.test"));
}
@Test
@@ -393,35 +428,48 @@ public class QuotaControllerTest {
// Way past the 24 hour boundary.
TimingSession fiv = createTimingSession(
now - (25 * HOUR_IN_MILLIS), 5 * MINUTE_IN_MILLIS, 4);
- List expected = new ArrayList<>();
+ List expectedRegular = new ArrayList<>();
+ List expectedHpj = new ArrayList<>();
// Added in correct (chronological) order.
- expected.add(fou);
- expected.add(thr);
- expected.add(two);
- expected.add(one);
- mQuotaController.saveTimingSession(0, "com.android.test", fiv);
- mQuotaController.saveTimingSession(0, "com.android.test", fou);
- mQuotaController.saveTimingSession(0, "com.android.test", thr);
- mQuotaController.saveTimingSession(0, "com.android.test", two);
- mQuotaController.saveTimingSession(0, "com.android.test", one);
+ expectedRegular.add(fou);
+ expectedRegular.add(thr);
+ expectedRegular.add(two);
+ expectedRegular.add(one);
+ expectedHpj.add(fiv); // HPJ list should be unaffected
+ expectedHpj.add(fou);
+ expectedHpj.add(one);
+ mQuotaController.saveTimingSession(0, "com.android.test", fiv, false);
+ mQuotaController.saveTimingSession(0, "com.android.test", fou, false);
+ mQuotaController.saveTimingSession(0, "com.android.test", thr, false);
+ mQuotaController.saveTimingSession(0, "com.android.test", two, false);
+ mQuotaController.saveTimingSession(0, "com.android.test", one, false);
+ mQuotaController.saveTimingSession(0, "com.android.test", fiv, true);
+ mQuotaController.saveTimingSession(0, "com.android.test", fou, true);
+ mQuotaController.saveTimingSession(0, "com.android.test", one, true);
synchronized (mQuotaController.mLock) {
mQuotaController.deleteObsoleteSessionsLocked();
}
- assertEquals(expected, mQuotaController.getTimingSessions(0, "com.android.test"));
+ assertEquals(expectedRegular, mQuotaController.getTimingSessions(0, "com.android.test"));
+ assertEquals(expectedHpj, mQuotaController.getHpjTimingSessions(0, "com.android.test"));
}
@Test
public void testOnAppRemovedLocked() {
final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
mQuotaController.saveTimingSession(0, "com.android.test.remove",
- createTimingSession(now - (6 * HOUR_IN_MILLIS), 10 * MINUTE_IN_MILLIS, 5));
+ createTimingSession(now - (6 * HOUR_IN_MILLIS), 10 * MINUTE_IN_MILLIS, 5), false);
mQuotaController.saveTimingSession(0, "com.android.test.remove",
createTimingSession(
- now - (2 * HOUR_IN_MILLIS + MINUTE_IN_MILLIS), 6 * MINUTE_IN_MILLIS, 5));
+ now - (2 * HOUR_IN_MILLIS + MINUTE_IN_MILLIS), 6 * MINUTE_IN_MILLIS, 5),
+ false);
mQuotaController.saveTimingSession(0, "com.android.test.remove",
- createTimingSession(now - (HOUR_IN_MILLIS), MINUTE_IN_MILLIS, 1));
+ createTimingSession(now - (HOUR_IN_MILLIS), MINUTE_IN_MILLIS, 1), false);
+ mQuotaController.saveTimingSession(0, "com.android.test.remove",
+ createTimingSession(now - (30 * MINUTE_IN_MILLIS), MINUTE_IN_MILLIS, 1), true);
+ mQuotaController.saveTimingSession(0, "com.android.test.remove",
+ createTimingSession(now - (15 * MINUTE_IN_MILLIS), MINUTE_IN_MILLIS, 1), true);
// Test that another app isn't affected.
TimingSession one = createTimingSession(
now - 10 * MINUTE_IN_MILLIS, 9 * MINUTE_IN_MILLIS, 3);
@@ -431,8 +479,14 @@ public class QuotaControllerTest {
// Added in correct (chronological) order.
expected.add(two);
expected.add(one);
- mQuotaController.saveTimingSession(0, "com.android.test.stay", two);
- mQuotaController.saveTimingSession(0, "com.android.test.stay", one);
+ mQuotaController.saveTimingSession(0, "com.android.test.stay", two, false);
+ mQuotaController.saveTimingSession(0, "com.android.test.stay", one, false);
+ mQuotaController.saveTimingSession(0, "com.android.test.stay", one, true);
+
+ assertNotNull(mQuotaController.getTimingSessions(0, "com.android.test.remove"));
+ assertNotNull(mQuotaController.getHpjTimingSessions(0, "com.android.test.remove"));
+ assertNotNull(mQuotaController.getTimingSessions(0, "com.android.test.stay"));
+ assertNotNull(mQuotaController.getHpjTimingSessions(0, "com.android.test.stay"));
ExecutionStats expectedStats = new ExecutionStats();
expectedStats.expirationTimeElapsed = now + 24 * HOUR_IN_MILLIS;
@@ -445,6 +499,7 @@ public class QuotaControllerTest {
mQuotaController.onAppRemovedLocked("com.android.test.remove", uid);
}
assertNull(mQuotaController.getTimingSessions(0, "com.android.test.remove"));
+ assertNull(mQuotaController.getHpjTimingSessions(0, "com.android.test.remove"));
assertEquals(expected, mQuotaController.getTimingSessions(0, "com.android.test.stay"));
synchronized (mQuotaController.mLock) {
assertEquals(expectedStats,
@@ -462,23 +517,36 @@ public class QuotaControllerTest {
public void testOnUserRemovedLocked() {
final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
mQuotaController.saveTimingSession(0, "com.android.test",
- createTimingSession(now - (6 * HOUR_IN_MILLIS), 10 * MINUTE_IN_MILLIS, 5));
+ createTimingSession(now - (6 * HOUR_IN_MILLIS), 10 * MINUTE_IN_MILLIS, 5), false);
mQuotaController.saveTimingSession(0, "com.android.test",
createTimingSession(
- now - (2 * HOUR_IN_MILLIS + MINUTE_IN_MILLIS), 6 * MINUTE_IN_MILLIS, 5));
+ now - (2 * HOUR_IN_MILLIS + MINUTE_IN_MILLIS), 6 * MINUTE_IN_MILLIS, 5),
+ false);
mQuotaController.saveTimingSession(0, "com.android.test",
- createTimingSession(now - (HOUR_IN_MILLIS), MINUTE_IN_MILLIS, 1));
+ createTimingSession(now - (HOUR_IN_MILLIS), MINUTE_IN_MILLIS, 1), false);
+ mQuotaController.saveTimingSession(0, "com.android.test",
+ createTimingSession(now - (30 * MINUTE_IN_MILLIS), MINUTE_IN_MILLIS, 1), true);
+ mQuotaController.saveTimingSession(0, "com.android.test",
+ createTimingSession(now - (15 * MINUTE_IN_MILLIS), MINUTE_IN_MILLIS, 1), true);
// Test that another user isn't affected.
TimingSession one = createTimingSession(
now - 10 * MINUTE_IN_MILLIS, 9 * MINUTE_IN_MILLIS, 3);
TimingSession two = createTimingSession(
now - (70 * MINUTE_IN_MILLIS), 9 * MINUTE_IN_MILLIS, 1);
- List expected = new ArrayList<>();
+ List expectedRegular = new ArrayList<>();
+ List expectedHpj = new ArrayList<>();
// Added in correct (chronological) order.
- expected.add(two);
- expected.add(one);
- mQuotaController.saveTimingSession(10, "com.android.test", two);
- mQuotaController.saveTimingSession(10, "com.android.test", one);
+ expectedRegular.add(two);
+ expectedRegular.add(one);
+ expectedHpj.add(one);
+ mQuotaController.saveTimingSession(10, "com.android.test", two, false);
+ mQuotaController.saveTimingSession(10, "com.android.test", one, false);
+ mQuotaController.saveTimingSession(10, "com.android.test", one, true);
+
+ assertNotNull(mQuotaController.getTimingSessions(0, "com.android.test"));
+ assertNotNull(mQuotaController.getHpjTimingSessions(0, "com.android.test"));
+ assertNotNull(mQuotaController.getTimingSessions(10, "com.android.test"));
+ assertNotNull(mQuotaController.getHpjTimingSessions(10, "com.android.test"));
ExecutionStats expectedStats = new ExecutionStats();
expectedStats.expirationTimeElapsed = now + 24 * HOUR_IN_MILLIS;
@@ -489,7 +557,11 @@ public class QuotaControllerTest {
synchronized (mQuotaController.mLock) {
mQuotaController.onUserRemovedLocked(0);
assertNull(mQuotaController.getTimingSessions(0, "com.android.test"));
- assertEquals(expected, mQuotaController.getTimingSessions(10, "com.android.test"));
+ assertNull(mQuotaController.getHpjTimingSessions(0, "com.android.test"));
+ assertEquals(expectedRegular,
+ mQuotaController.getTimingSessions(10, "com.android.test"));
+ assertEquals(expectedHpj,
+ mQuotaController.getHpjTimingSessions(10, "com.android.test"));
assertEquals(expectedStats,
mQuotaController.getExecutionStatsLocked(0, "com.android.test", RARE_INDEX));
assertNotEquals(expectedStats,
@@ -502,17 +574,19 @@ public class QuotaControllerTest {
final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
// Added in chronological order.
mQuotaController.saveTimingSession(0, "com.android.test",
- createTimingSession(now - (6 * HOUR_IN_MILLIS), 10 * MINUTE_IN_MILLIS, 5));
+ createTimingSession(now - (6 * HOUR_IN_MILLIS), 10 * MINUTE_IN_MILLIS, 5), false);
mQuotaController.saveTimingSession(0, "com.android.test",
createTimingSession(
- now - (2 * HOUR_IN_MILLIS + MINUTE_IN_MILLIS), 6 * MINUTE_IN_MILLIS, 5));
+ now - (2 * HOUR_IN_MILLIS + MINUTE_IN_MILLIS), 6 * MINUTE_IN_MILLIS, 5),
+ false);
mQuotaController.saveTimingSession(0, "com.android.test",
- createTimingSession(now - (HOUR_IN_MILLIS), MINUTE_IN_MILLIS, 1));
+ createTimingSession(now - (HOUR_IN_MILLIS), MINUTE_IN_MILLIS, 1), false);
mQuotaController.saveTimingSession(0, "com.android.test",
createTimingSession(
- now - (HOUR_IN_MILLIS - 10 * MINUTE_IN_MILLIS), MINUTE_IN_MILLIS, 1));
+ now - (HOUR_IN_MILLIS - 10 * MINUTE_IN_MILLIS), MINUTE_IN_MILLIS, 1),
+ false);
mQuotaController.saveTimingSession(0, "com.android.test",
- createTimingSession(now - 5 * MINUTE_IN_MILLIS, 4 * MINUTE_IN_MILLIS, 3));
+ createTimingSession(now - 5 * MINUTE_IN_MILLIS, 4 * MINUTE_IN_MILLIS, 3), false);
// Test an app that hasn't had any activity.
ExecutionStats expectedStats = new ExecutionStats();
@@ -703,7 +777,6 @@ public class QuotaControllerTest {
@Test
public void testUpdateExecutionStatsLocked_WithTimer() {
- final long now = sElapsedRealtimeClock.millis();
setProcessState(ActivityManager.PROCESS_STATE_SERVICE);
ExecutionStats expectedStats = new ExecutionStats();
@@ -741,7 +814,7 @@ public class QuotaControllerTest {
// Add old session. Make sure values are combined correctly.
mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
createTimingSession(sElapsedRealtimeClock.millis() - (6 * HOUR_IN_MILLIS),
- 10 * MINUTE_IN_MILLIS, 5));
+ 10 * MINUTE_IN_MILLIS, 5), false);
expectedStats.sessionCountInWindow = 1;
expectedStats.expirationTimeElapsed = sElapsedRealtimeClock.millis() + 18 * HOUR_IN_MILLIS;
@@ -794,13 +867,13 @@ public class QuotaControllerTest {
public void testGetExecutionStatsLocked_Values() {
final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
mQuotaController.saveTimingSession(0, "com.android.test",
- createTimingSession(now - (23 * HOUR_IN_MILLIS), 10 * MINUTE_IN_MILLIS, 5));
+ createTimingSession(now - (23 * HOUR_IN_MILLIS), 10 * MINUTE_IN_MILLIS, 5), false);
mQuotaController.saveTimingSession(0, "com.android.test",
- createTimingSession(now - (7 * HOUR_IN_MILLIS), 10 * MINUTE_IN_MILLIS, 5));
+ createTimingSession(now - (7 * HOUR_IN_MILLIS), 10 * MINUTE_IN_MILLIS, 5), false);
mQuotaController.saveTimingSession(0, "com.android.test",
- createTimingSession(now - (2 * HOUR_IN_MILLIS), 10 * MINUTE_IN_MILLIS, 5));
+ createTimingSession(now - (2 * HOUR_IN_MILLIS), 10 * MINUTE_IN_MILLIS, 5), false);
mQuotaController.saveTimingSession(0, "com.android.test",
- createTimingSession(now - (6 * MINUTE_IN_MILLIS), 3 * MINUTE_IN_MILLIS, 5));
+ createTimingSession(now - (6 * MINUTE_IN_MILLIS), 3 * MINUTE_IN_MILLIS, 5), false);
ExecutionStats expectedStats = new ExecutionStats();
@@ -882,7 +955,7 @@ public class QuotaControllerTest {
advanceElapsedClock(3 * MINUTE_IN_MILLIS);
mQuotaController.saveTimingSession(0, "com.android.test",
- createTimingSession(MINUTE_IN_MILLIS, MINUTE_IN_MILLIS, 2));
+ createTimingSession(MINUTE_IN_MILLIS, MINUTE_IN_MILLIS, 2), false);
ExecutionStats expectedStats = new ExecutionStats();
@@ -942,24 +1015,24 @@ public class QuotaControllerTest {
mQuotaController.saveTimingSession(0, "com.android.test",
createTimingSession(
JobSchedulerService.sElapsedRealtimeClock.millis(),
- 5 * MINUTE_IN_MILLIS, 5));
+ 5 * MINUTE_IN_MILLIS, 5), false);
advanceElapsedClock(5 * MINUTE_IN_MILLIS);
advanceElapsedClock(5 * MINUTE_IN_MILLIS);
for (int j = 0; j < 5; ++j) {
mQuotaController.saveTimingSession(0, "com.android.test",
createTimingSession(
JobSchedulerService.sElapsedRealtimeClock.millis(),
- MINUTE_IN_MILLIS, 2));
+ MINUTE_IN_MILLIS, 2), false);
advanceElapsedClock(MINUTE_IN_MILLIS);
advanceElapsedClock(54 * SECOND_IN_MILLIS);
mQuotaController.saveTimingSession(0, "com.android.test",
createTimingSession(
- JobSchedulerService.sElapsedRealtimeClock.millis(), 500, 1));
+ JobSchedulerService.sElapsedRealtimeClock.millis(), 500, 1), false);
advanceElapsedClock(500);
advanceElapsedClock(400);
mQuotaController.saveTimingSession(0, "com.android.test",
createTimingSession(
- JobSchedulerService.sElapsedRealtimeClock.millis(), 100, 1));
+ JobSchedulerService.sElapsedRealtimeClock.millis(), 100, 1), false);
advanceElapsedClock(100);
advanceElapsedClock(5 * SECOND_IN_MILLIS);
}
@@ -1095,13 +1168,13 @@ public class QuotaControllerTest {
final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
mQuotaController.saveTimingSession(0, "com.android.test",
- createTimingSession(now - (23 * HOUR_IN_MILLIS), 10 * MINUTE_IN_MILLIS, 5));
+ createTimingSession(now - (23 * HOUR_IN_MILLIS), 10 * MINUTE_IN_MILLIS, 5), false);
mQuotaController.saveTimingSession(0, "com.android.test",
- createTimingSession(now - (7 * HOUR_IN_MILLIS), 10 * MINUTE_IN_MILLIS, 5));
+ createTimingSession(now - (7 * HOUR_IN_MILLIS), 10 * MINUTE_IN_MILLIS, 5), false);
mQuotaController.saveTimingSession(0, "com.android.test",
- createTimingSession(now - (2 * HOUR_IN_MILLIS), 10 * MINUTE_IN_MILLIS, 5));
+ createTimingSession(now - (2 * HOUR_IN_MILLIS), 10 * MINUTE_IN_MILLIS, 5), false);
mQuotaController.saveTimingSession(0, "com.android.test",
- createTimingSession(now - (6 * MINUTE_IN_MILLIS), 3 * MINUTE_IN_MILLIS, 5));
+ createTimingSession(now - (6 * MINUTE_IN_MILLIS), 3 * MINUTE_IN_MILLIS, 5), false);
final ExecutionStats originalStatsActive;
final ExecutionStats originalStatsWorking;
final ExecutionStats originalStatsFrequent;
@@ -1195,20 +1268,20 @@ public class QuotaControllerTest {
public void testGetMaxJobExecutionTimeLocked() {
mQuotaController.saveTimingSession(0, SOURCE_PACKAGE,
createTimingSession(sElapsedRealtimeClock.millis() - (6 * MINUTE_IN_MILLIS),
- 3 * MINUTE_IN_MILLIS, 5));
+ 3 * MINUTE_IN_MILLIS, 5), false);
JobStatus job = createJobStatus("testGetMaxJobExecutionTimeLocked", 0);
job.setStandbyBucket(RARE_INDEX);
setCharging();
synchronized (mQuotaController.mLock) {
- assertEquals(JobServiceContext.EXECUTING_TIMESLICE_MILLIS,
+ assertEquals(JobServiceContext.DEFAULT_EXECUTING_TIMESLICE_MILLIS,
mQuotaController.getMaxJobExecutionTimeMsLocked((job)));
}
setDischarging();
setProcessState(ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE);
synchronized (mQuotaController.mLock) {
- assertEquals(JobServiceContext.EXECUTING_TIMESLICE_MILLIS,
+ assertEquals(JobServiceContext.DEFAULT_EXECUTING_TIMESLICE_MILLIS,
mQuotaController.getMaxJobExecutionTimeMsLocked((job)));
}
@@ -1220,7 +1293,7 @@ public class QuotaControllerTest {
}
setProcessState(ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND);
synchronized (mQuotaController.mLock) {
- assertEquals(JobServiceContext.EXECUTING_TIMESLICE_MILLIS,
+ assertEquals(JobServiceContext.DEFAULT_EXECUTING_TIMESLICE_MILLIS,
mQuotaController.getMaxJobExecutionTimeMsLocked((job)));
mQuotaController.maybeStopTrackingJobLocked(job, null, false);
}
@@ -1242,17 +1315,17 @@ public class QuotaControllerTest {
// Close to RARE boundary.
mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
createTimingSession(now - (24 * HOUR_IN_MILLIS - 30 * SECOND_IN_MILLIS),
- 30 * SECOND_IN_MILLIS, 5));
+ 30 * SECOND_IN_MILLIS, 5), false);
// Far away from FREQUENT boundary.
mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
- createTimingSession(now - (7 * HOUR_IN_MILLIS), 3 * MINUTE_IN_MILLIS, 5));
+ createTimingSession(now - (7 * HOUR_IN_MILLIS), 3 * MINUTE_IN_MILLIS, 5), false);
// Overlap WORKING_SET boundary.
mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
createTimingSession(now - (2 * HOUR_IN_MILLIS + MINUTE_IN_MILLIS),
- 3 * MINUTE_IN_MILLIS, 5));
+ 3 * MINUTE_IN_MILLIS, 5), false);
// Close to ACTIVE boundary.
mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
- createTimingSession(now - (9 * MINUTE_IN_MILLIS), 3 * MINUTE_IN_MILLIS, 5));
+ createTimingSession(now - (9 * MINUTE_IN_MILLIS), 3 * MINUTE_IN_MILLIS, 5), false);
setStandbyBucket(RARE_INDEX);
synchronized (mQuotaController.mLock) {
@@ -1306,7 +1379,8 @@ public class QuotaControllerTest {
// Overlap boundary.
mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
createTimingSession(
- now - (24 * HOUR_IN_MILLIS + 8 * MINUTE_IN_MILLIS), 4 * HOUR_IN_MILLIS, 5));
+ now - (24 * HOUR_IN_MILLIS + 8 * MINUTE_IN_MILLIS), 4 * HOUR_IN_MILLIS, 5),
+ false);
setStandbyBucket(WORKING_INDEX);
synchronized (mQuotaController.mLock) {
@@ -1323,7 +1397,7 @@ public class QuotaControllerTest {
// Close to boundary.
mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
createTimingSession(now - (24 * HOUR_IN_MILLIS - MINUTE_IN_MILLIS),
- 4 * HOUR_IN_MILLIS - 5 * MINUTE_IN_MILLIS, 5));
+ 4 * HOUR_IN_MILLIS - 5 * MINUTE_IN_MILLIS, 5), false);
setStandbyBucket(WORKING_INDEX);
synchronized (mQuotaController.mLock) {
@@ -1339,7 +1413,8 @@ public class QuotaControllerTest {
// Far from boundary.
mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
createTimingSession(
- now - (20 * HOUR_IN_MILLIS), 4 * HOUR_IN_MILLIS - 3 * MINUTE_IN_MILLIS, 5));
+ now - (20 * HOUR_IN_MILLIS), 4 * HOUR_IN_MILLIS - 3 * MINUTE_IN_MILLIS, 5),
+ false);
setStandbyBucket(WORKING_INDEX);
synchronized (mQuotaController.mLock) {
@@ -1366,10 +1441,11 @@ public class QuotaControllerTest {
createTimingSession(
now - (24 * HOUR_IN_MILLIS + 11 * MINUTE_IN_MILLIS),
4 * HOUR_IN_MILLIS,
- 5));
+ 5), false);
mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
createTimingSession(
- now - (8 * HOUR_IN_MILLIS + MINUTE_IN_MILLIS), 3 * MINUTE_IN_MILLIS, 5));
+ now - (8 * HOUR_IN_MILLIS + MINUTE_IN_MILLIS), 3 * MINUTE_IN_MILLIS, 5),
+ false);
synchronized (mQuotaController.mLock) {
// Both max and bucket time have 8 minutes left.
@@ -1387,15 +1463,17 @@ public class QuotaControllerTest {
// Overlap boundary.
mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
createTimingSession(
- now - (24 * HOUR_IN_MILLIS + MINUTE_IN_MILLIS), 2 * MINUTE_IN_MILLIS, 5));
+ now - (24 * HOUR_IN_MILLIS + MINUTE_IN_MILLIS), 2 * MINUTE_IN_MILLIS, 5),
+ false);
mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
createTimingSession(
now - (20 * HOUR_IN_MILLIS),
3 * HOUR_IN_MILLIS + 48 * MINUTE_IN_MILLIS,
- 5));
+ 5), false);
mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
createTimingSession(
- now - (8 * HOUR_IN_MILLIS + MINUTE_IN_MILLIS), 3 * MINUTE_IN_MILLIS, 5));
+ now - (8 * HOUR_IN_MILLIS + MINUTE_IN_MILLIS), 3 * MINUTE_IN_MILLIS, 5),
+ false);
synchronized (mQuotaController.mLock) {
// Both max and bucket time have 8 minutes left.
@@ -1430,9 +1508,9 @@ public class QuotaControllerTest {
setDischarging();
final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
mQuotaController.saveTimingSession(0, "com.android.test",
- createTimingSession(now - (HOUR_IN_MILLIS), 3 * MINUTE_IN_MILLIS, 5));
+ createTimingSession(now - (HOUR_IN_MILLIS), 3 * MINUTE_IN_MILLIS, 5), false);
mQuotaController.saveTimingSession(0, "com.android.test",
- createTimingSession(now - (5 * MINUTE_IN_MILLIS), 3 * MINUTE_IN_MILLIS, 5));
+ createTimingSession(now - (5 * MINUTE_IN_MILLIS), 3 * MINUTE_IN_MILLIS, 5), false);
synchronized (mQuotaController.mLock) {
mQuotaController.incrementJobCountLocked(0, "com.android.test", 5);
assertTrue(mQuotaController.isWithinQuotaLocked(0, "com.android.test", WORKING_INDEX));
@@ -1445,9 +1523,10 @@ public class QuotaControllerTest {
final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
final int jobCount = mQcConstants.MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW;
mQuotaController.saveTimingSession(0, "com.android.test.spam",
- createTimingSession(now - (HOUR_IN_MILLIS), 15 * MINUTE_IN_MILLIS, 25));
+ createTimingSession(now - (HOUR_IN_MILLIS), 15 * MINUTE_IN_MILLIS, 25), false);
mQuotaController.saveTimingSession(0, "com.android.test.spam",
- createTimingSession(now - (5 * MINUTE_IN_MILLIS), 3 * MINUTE_IN_MILLIS, jobCount));
+ createTimingSession(now - (5 * MINUTE_IN_MILLIS), 3 * MINUTE_IN_MILLIS, jobCount),
+ false);
synchronized (mQuotaController.mLock) {
mQuotaController.incrementJobCountLocked(0, "com.android.test.spam", jobCount);
assertFalse(mQuotaController.isWithinQuotaLocked(
@@ -1455,9 +1534,10 @@ public class QuotaControllerTest {
}
mQuotaController.saveTimingSession(0, "com.android.test.frequent",
- createTimingSession(now - (2 * HOUR_IN_MILLIS), 15 * MINUTE_IN_MILLIS, 2000));
+ createTimingSession(now - (2 * HOUR_IN_MILLIS), 15 * MINUTE_IN_MILLIS, 2000),
+ false);
mQuotaController.saveTimingSession(0, "com.android.test.frequent",
- createTimingSession(now - (HOUR_IN_MILLIS), 3 * MINUTE_IN_MILLIS, 500));
+ createTimingSession(now - (HOUR_IN_MILLIS), 3 * MINUTE_IN_MILLIS, 500), false);
synchronized (mQuotaController.mLock) {
assertFalse(mQuotaController.isWithinQuotaLocked(
0, "com.android.test.frequent", FREQUENT_INDEX));
@@ -1469,11 +1549,11 @@ public class QuotaControllerTest {
setDischarging();
final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
mQuotaController.saveTimingSession(0, "com.android.test",
- createTimingSession(now - (HOUR_IN_MILLIS), 3 * MINUTE_IN_MILLIS, 5));
+ createTimingSession(now - (HOUR_IN_MILLIS), 3 * MINUTE_IN_MILLIS, 5), false);
mQuotaController.saveTimingSession(0, "com.android.test",
- createTimingSession(now - (30 * MINUTE_IN_MILLIS), 3 * MINUTE_IN_MILLIS, 5));
+ createTimingSession(now - (30 * MINUTE_IN_MILLIS), 3 * MINUTE_IN_MILLIS, 5), false);
mQuotaController.saveTimingSession(0, "com.android.test",
- createTimingSession(now - (5 * MINUTE_IN_MILLIS), 4 * MINUTE_IN_MILLIS, 5));
+ createTimingSession(now - (5 * MINUTE_IN_MILLIS), 4 * MINUTE_IN_MILLIS, 5), false);
synchronized (mQuotaController.mLock) {
mQuotaController.incrementJobCountLocked(0, "com.android.test", 5);
assertFalse(mQuotaController.isWithinQuotaLocked(0, "com.android.test", WORKING_INDEX));
@@ -1486,9 +1566,10 @@ public class QuotaControllerTest {
final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
final int jobCount = mQcConstants.MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW;
mQuotaController.saveTimingSession(0, "com.android.test",
- createTimingSession(now - (HOUR_IN_MILLIS), 15 * MINUTE_IN_MILLIS, 25));
+ createTimingSession(now - (HOUR_IN_MILLIS), 15 * MINUTE_IN_MILLIS, 25), false);
mQuotaController.saveTimingSession(0, "com.android.test",
- createTimingSession(now - (5 * MINUTE_IN_MILLIS), 3 * MINUTE_IN_MILLIS, jobCount));
+ createTimingSession(now - (5 * MINUTE_IN_MILLIS), 3 * MINUTE_IN_MILLIS, jobCount),
+ false);
synchronized (mQuotaController.mLock) {
mQuotaController.incrementJobCountLocked(0, "com.android.test", jobCount);
assertFalse(mQuotaController.isWithinQuotaLocked(0, "com.android.test", WORKING_INDEX));
@@ -1639,7 +1720,7 @@ public class QuotaControllerTest {
for (int i = 0; i < 7; ++i) {
mQuotaController.saveTimingSession(0, "com.android.test",
createTimingSession(now - ((10 - i) * MINUTE_IN_MILLIS), 30 * SECOND_IN_MILLIS,
- 2));
+ 2), false);
synchronized (mQuotaController.mLock) {
mQuotaController.incrementJobCountLocked(0, "com.android.test", 2);
@@ -1673,7 +1754,7 @@ public class QuotaControllerTest {
final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
final long end = now - (6 * HOUR_IN_MILLIS - 5 * MINUTE_IN_MILLIS);
mQuotaController.saveTimingSession(0, "com.android.test",
- new TimingSession(now - 6 * HOUR_IN_MILLIS, end, 1));
+ new TimingSession(now - 6 * HOUR_IN_MILLIS, end, 1), false);
synchronized (mQuotaController.mLock) {
mQuotaController.maybeScheduleCleanupAlarmLocked();
}
@@ -1682,9 +1763,9 @@ public class QuotaControllerTest {
// Test with new (more recent) timing sessions saved. AlarmManger shouldn't be called again.
mQuotaController.saveTimingSession(0, "com.android.test",
- createTimingSession(now - 3 * HOUR_IN_MILLIS, MINUTE_IN_MILLIS, 1));
+ createTimingSession(now - 3 * HOUR_IN_MILLIS, MINUTE_IN_MILLIS, 1), false);
mQuotaController.saveTimingSession(0, "com.android.test",
- createTimingSession(now - HOUR_IN_MILLIS, 3 * MINUTE_IN_MILLIS, 1));
+ createTimingSession(now - HOUR_IN_MILLIS, 3 * MINUTE_IN_MILLIS, 1), false);
synchronized (mQuotaController.mLock) {
mQuotaController.maybeScheduleCleanupAlarmLocked();
}
@@ -1715,11 +1796,11 @@ public class QuotaControllerTest {
final long expectedAlarmTime =
(now - 18 * HOUR_IN_MILLIS) + 24 * HOUR_IN_MILLIS + mQcConstants.IN_QUOTA_BUFFER_MS;
mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
- createTimingSession(now - 18 * HOUR_IN_MILLIS, HOUR_IN_MILLIS, 1));
+ createTimingSession(now - 18 * HOUR_IN_MILLIS, HOUR_IN_MILLIS, 1), false);
mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
- createTimingSession(now - 12 * HOUR_IN_MILLIS, HOUR_IN_MILLIS, 1));
+ createTimingSession(now - 12 * HOUR_IN_MILLIS, HOUR_IN_MILLIS, 1), false);
mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
- createTimingSession(now - 7 * HOUR_IN_MILLIS, HOUR_IN_MILLIS, 1));
+ createTimingSession(now - 7 * HOUR_IN_MILLIS, HOUR_IN_MILLIS, 1), false);
synchronized (mQuotaController.mLock) {
mQuotaController.maybeScheduleStartAlarmLocked(
SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket);
@@ -1727,7 +1808,7 @@ public class QuotaControllerTest {
verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
- createTimingSession(now - 2 * HOUR_IN_MILLIS, 55 * MINUTE_IN_MILLIS, 1));
+ createTimingSession(now - 2 * HOUR_IN_MILLIS, 55 * MINUTE_IN_MILLIS, 1), false);
synchronized (mQuotaController.mLock) {
mQuotaController.maybeScheduleStartAlarmLocked(
SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket);
@@ -1772,7 +1853,7 @@ public class QuotaControllerTest {
// Test with timing sessions out of window.
final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
mQuotaController.saveTimingSession(0, "com.android.test",
- createTimingSession(now - 10 * HOUR_IN_MILLIS, 5 * MINUTE_IN_MILLIS, 1));
+ createTimingSession(now - 10 * HOUR_IN_MILLIS, 5 * MINUTE_IN_MILLIS, 1), false);
synchronized (mQuotaController.mLock) {
mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", standbyBucket);
}
@@ -1784,7 +1865,7 @@ public class QuotaControllerTest {
final long expectedAlarmTime =
end - MINUTE_IN_MILLIS + 2 * HOUR_IN_MILLIS + mQcConstants.IN_QUOTA_BUFFER_MS;
mQuotaController.saveTimingSession(0, "com.android.test",
- new TimingSession(now - 2 * HOUR_IN_MILLIS, end, 1));
+ new TimingSession(now - 2 * HOUR_IN_MILLIS, end, 1), false);
synchronized (mQuotaController.mLock) {
mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", standbyBucket);
}
@@ -1792,9 +1873,9 @@ public class QuotaControllerTest {
// Add some more sessions, but still in quota.
mQuotaController.saveTimingSession(0, "com.android.test",
- createTimingSession(now - HOUR_IN_MILLIS, MINUTE_IN_MILLIS, 1));
+ createTimingSession(now - HOUR_IN_MILLIS, MINUTE_IN_MILLIS, 1), false);
mQuotaController.saveTimingSession(0, "com.android.test",
- createTimingSession(now - (50 * MINUTE_IN_MILLIS), 3 * MINUTE_IN_MILLIS, 1));
+ createTimingSession(now - (50 * MINUTE_IN_MILLIS), 3 * MINUTE_IN_MILLIS, 1), false);
synchronized (mQuotaController.mLock) {
mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", standbyBucket);
}
@@ -1802,7 +1883,7 @@ public class QuotaControllerTest {
// Test when out of quota.
mQuotaController.saveTimingSession(0, "com.android.test",
- createTimingSession(now - 30 * MINUTE_IN_MILLIS, 5 * MINUTE_IN_MILLIS, 1));
+ createTimingSession(now - 30 * MINUTE_IN_MILLIS, 5 * MINUTE_IN_MILLIS, 1), false);
synchronized (mQuotaController.mLock) {
mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", standbyBucket);
}
@@ -1836,7 +1917,7 @@ public class QuotaControllerTest {
// Test with timing sessions out of window.
final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
mQuotaController.saveTimingSession(0, "com.android.test",
- createTimingSession(now - 10 * HOUR_IN_MILLIS, 5 * MINUTE_IN_MILLIS, 1));
+ createTimingSession(now - 10 * HOUR_IN_MILLIS, 5 * MINUTE_IN_MILLIS, 1), false);
synchronized (mQuotaController.mLock) {
mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", standbyBucket);
}
@@ -1846,7 +1927,7 @@ public class QuotaControllerTest {
final long start = now - (6 * HOUR_IN_MILLIS);
final long expectedAlarmTime = start + 8 * HOUR_IN_MILLIS + mQcConstants.IN_QUOTA_BUFFER_MS;
mQuotaController.saveTimingSession(0, "com.android.test",
- createTimingSession(start, 5 * MINUTE_IN_MILLIS, 1));
+ createTimingSession(start, 5 * MINUTE_IN_MILLIS, 1), false);
synchronized (mQuotaController.mLock) {
mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", standbyBucket);
}
@@ -1854,9 +1935,9 @@ public class QuotaControllerTest {
// Add some more sessions, but still in quota.
mQuotaController.saveTimingSession(0, "com.android.test",
- createTimingSession(now - 3 * HOUR_IN_MILLIS, MINUTE_IN_MILLIS, 1));
+ createTimingSession(now - 3 * HOUR_IN_MILLIS, MINUTE_IN_MILLIS, 1), false);
mQuotaController.saveTimingSession(0, "com.android.test",
- createTimingSession(now - HOUR_IN_MILLIS, 3 * MINUTE_IN_MILLIS, 1));
+ createTimingSession(now - HOUR_IN_MILLIS, 3 * MINUTE_IN_MILLIS, 1), false);
synchronized (mQuotaController.mLock) {
mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", standbyBucket);
}
@@ -1864,7 +1945,7 @@ public class QuotaControllerTest {
// Test when out of quota.
mQuotaController.saveTimingSession(0, "com.android.test",
- createTimingSession(now - HOUR_IN_MILLIS, MINUTE_IN_MILLIS, 1));
+ createTimingSession(now - HOUR_IN_MILLIS, MINUTE_IN_MILLIS, 1), false);
synchronized (mQuotaController.mLock) {
mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", standbyBucket);
}
@@ -1901,7 +1982,7 @@ public class QuotaControllerTest {
// Test with timing sessions out of window.
final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
mQuotaController.saveTimingSession(0, "com.android.test",
- createTimingSession(now - 25 * HOUR_IN_MILLIS, 5 * MINUTE_IN_MILLIS, 1));
+ createTimingSession(now - 25 * HOUR_IN_MILLIS, 5 * MINUTE_IN_MILLIS, 1), false);
synchronized (mQuotaController.mLock) {
mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", standbyBucket);
}
@@ -1915,7 +1996,7 @@ public class QuotaControllerTest {
start + MINUTE_IN_MILLIS + 24 * HOUR_IN_MILLIS
+ mQcConstants.IN_QUOTA_BUFFER_MS;
mQuotaController.saveTimingSession(0, "com.android.test",
- createTimingSession(start, 5 * MINUTE_IN_MILLIS, 1));
+ createTimingSession(start, 5 * MINUTE_IN_MILLIS, 1), false);
synchronized (mQuotaController.mLock) {
mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", standbyBucket);
}
@@ -1923,9 +2004,9 @@ public class QuotaControllerTest {
// Add some more sessions, but still in quota.
mQuotaController.saveTimingSession(0, "com.android.test",
- createTimingSession(now - 3 * HOUR_IN_MILLIS, MINUTE_IN_MILLIS, 1));
+ createTimingSession(now - 3 * HOUR_IN_MILLIS, MINUTE_IN_MILLIS, 1), false);
mQuotaController.saveTimingSession(0, "com.android.test",
- createTimingSession(now - HOUR_IN_MILLIS, 3 * MINUTE_IN_MILLIS, 1));
+ createTimingSession(now - HOUR_IN_MILLIS, 3 * MINUTE_IN_MILLIS, 1), false);
synchronized (mQuotaController.mLock) {
mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", standbyBucket);
}
@@ -1933,7 +2014,7 @@ public class QuotaControllerTest {
// Test when out of quota.
mQuotaController.saveTimingSession(0, "com.android.test",
- createTimingSession(now - HOUR_IN_MILLIS, 2 * MINUTE_IN_MILLIS, 1));
+ createTimingSession(now - HOUR_IN_MILLIS, 2 * MINUTE_IN_MILLIS, 1), false);
synchronized (mQuotaController.mLock) {
mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", standbyBucket);
}
@@ -1960,17 +2041,17 @@ public class QuotaControllerTest {
// Affects rare bucket
mQuotaController.saveTimingSession(0, "com.android.test",
- createTimingSession(now - 12 * HOUR_IN_MILLIS, 9 * MINUTE_IN_MILLIS, 3));
+ createTimingSession(now - 12 * HOUR_IN_MILLIS, 9 * MINUTE_IN_MILLIS, 3), false);
// Affects frequent and rare buckets
mQuotaController.saveTimingSession(0, "com.android.test",
- createTimingSession(now - 4 * HOUR_IN_MILLIS, 4 * MINUTE_IN_MILLIS, 3));
+ createTimingSession(now - 4 * HOUR_IN_MILLIS, 4 * MINUTE_IN_MILLIS, 3), false);
// Affects working, frequent, and rare buckets
final long outOfQuotaTime = now - HOUR_IN_MILLIS;
mQuotaController.saveTimingSession(0, "com.android.test",
- createTimingSession(outOfQuotaTime, 7 * MINUTE_IN_MILLIS, 10));
+ createTimingSession(outOfQuotaTime, 7 * MINUTE_IN_MILLIS, 10), false);
// Affects all buckets
mQuotaController.saveTimingSession(0, "com.android.test",
- createTimingSession(now - 5 * MINUTE_IN_MILLIS, 3 * MINUTE_IN_MILLIS, 3));
+ createTimingSession(now - 5 * MINUTE_IN_MILLIS, 3 * MINUTE_IN_MILLIS, 3), false);
InOrder inOrder = inOrder(mAlarmManager);
@@ -2143,9 +2224,9 @@ public class QuotaControllerTest {
// the quota.
mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
createTimingSession(now - (2 * HOUR_IN_MILLIS + 3 * MINUTE_IN_MILLIS),
- 3 * MINUTE_IN_MILLIS + contributionMs, 3));
+ 3 * MINUTE_IN_MILLIS + contributionMs, 3), false);
mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
- createTimingSession(now - HOUR_IN_MILLIS, remainingTimeMs, 2));
+ createTimingSession(now - HOUR_IN_MILLIS, remainingTimeMs, 2), false);
// Expected alarm time should be when the app will have QUOTA_BUFFER_MS time of quota, which
// is 2 hours + (QUOTA_BUFFER_MS - contributionMs) after the start of the second session.
final long expectedAlarmTime = now - HOUR_IN_MILLIS + 2 * HOUR_IN_MILLIS
@@ -2175,9 +2256,9 @@ public class QuotaControllerTest {
// the quota.
mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
createTimingSession(now - (24 * HOUR_IN_MILLIS + 3 * MINUTE_IN_MILLIS),
- 3 * MINUTE_IN_MILLIS + contributionMs, 3));
+ 3 * MINUTE_IN_MILLIS + contributionMs, 3), false);
mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
- createTimingSession(now - 20 * HOUR_IN_MILLIS, remainingTimeMs, 300));
+ createTimingSession(now - 20 * HOUR_IN_MILLIS, remainingTimeMs, 300), false);
// Expected alarm time should be when the app will have QUOTA_BUFFER_MS time of quota, which
// is 24 hours + (QUOTA_BUFFER_MS - contributionMs) after the start of the second session.
final long expectedAlarmTime = now - 20 * HOUR_IN_MILLIS
@@ -2217,6 +2298,16 @@ public class QuotaControllerTest {
setDeviceConfigLong(QcConstants.KEY_TIMING_SESSION_COALESCING_DURATION_MS,
10 * SECOND_IN_MILLIS);
setDeviceConfigLong(QcConstants.KEY_MIN_QUOTA_CHECK_DELAY_MS, 7 * MINUTE_IN_MILLIS);
+ setDeviceConfigLong(QcConstants.KEY_HPJ_LIMIT_ACTIVE_MS, 2 * HOUR_IN_MILLIS);
+ setDeviceConfigLong(QcConstants.KEY_HPJ_LIMIT_WORKING_MS, 90 * MINUTE_IN_MILLIS);
+ setDeviceConfigLong(QcConstants.KEY_HPJ_LIMIT_FREQUENT_MS, 1 * HOUR_IN_MILLIS);
+ setDeviceConfigLong(QcConstants.KEY_HPJ_LIMIT_RARE_MS, 30 * MINUTE_IN_MILLIS);
+ setDeviceConfigLong(QcConstants.KEY_HPJ_LIMIT_RESTRICTED_MS, 27 * MINUTE_IN_MILLIS);
+ setDeviceConfigLong(QcConstants.KEY_HPJ_WINDOW_SIZE_MS, 12 * HOUR_IN_MILLIS);
+ setDeviceConfigLong(QcConstants.KEY_HPJ_TOP_APP_TIME_CHUNK_SIZE_MS, 10 * MINUTE_IN_MILLIS);
+ setDeviceConfigLong(QcConstants.KEY_HPJ_REWARD_TOP_APP_MS, 87 * SECOND_IN_MILLIS);
+ setDeviceConfigLong(QcConstants.KEY_HPJ_REWARD_INTERACTION_MS, 86 * SECOND_IN_MILLIS);
+ setDeviceConfigLong(QcConstants.KEY_HPJ_REWARD_NOTIFICATION_SEEN_MS, 85 * SECOND_IN_MILLIS);
assertEquals(5 * MINUTE_IN_MILLIS, mQuotaController.getAllowedTimePerPeriodMs());
assertEquals(2 * MINUTE_IN_MILLIS, mQuotaController.getInQuotaBufferMs());
@@ -2244,6 +2335,16 @@ public class QuotaControllerTest {
assertEquals(10 * SECOND_IN_MILLIS,
mQuotaController.getTimingSessionCoalescingDurationMs());
assertEquals(7 * MINUTE_IN_MILLIS, mQuotaController.getMinQuotaCheckDelayMs());
+ assertEquals(2 * HOUR_IN_MILLIS, mQuotaController.getHpjLimitsMs()[ACTIVE_INDEX]);
+ assertEquals(90 * MINUTE_IN_MILLIS, mQuotaController.getHpjLimitsMs()[WORKING_INDEX]);
+ assertEquals(HOUR_IN_MILLIS, mQuotaController.getHpjLimitsMs()[FREQUENT_INDEX]);
+ assertEquals(30 * MINUTE_IN_MILLIS, mQuotaController.getHpjLimitsMs()[RARE_INDEX]);
+ assertEquals(27 * MINUTE_IN_MILLIS, mQuotaController.getHpjLimitsMs()[RESTRICTED_INDEX]);
+ assertEquals(12 * HOUR_IN_MILLIS, mQuotaController.getHpjLimitWindowSizeMs());
+ assertEquals(10 * MINUTE_IN_MILLIS, mQuotaController.getHpjTopAppTimeChunkSizeMs());
+ assertEquals(87 * SECOND_IN_MILLIS, mQuotaController.getHpjRewardTopAppMs());
+ assertEquals(86 * SECOND_IN_MILLIS, mQuotaController.getHpjRewardInteractionMs());
+ assertEquals(85 * SECOND_IN_MILLIS, mQuotaController.getHpjRewardNotificationSeenMs());
}
@Test
@@ -2272,6 +2373,16 @@ public class QuotaControllerTest {
setDeviceConfigInt(QcConstants.KEY_MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW, 0);
setDeviceConfigLong(QcConstants.KEY_TIMING_SESSION_COALESCING_DURATION_MS, -1);
setDeviceConfigLong(QcConstants.KEY_MIN_QUOTA_CHECK_DELAY_MS, -1);
+ setDeviceConfigLong(QcConstants.KEY_HPJ_LIMIT_ACTIVE_MS, -1);
+ setDeviceConfigLong(QcConstants.KEY_HPJ_LIMIT_WORKING_MS, -1);
+ setDeviceConfigLong(QcConstants.KEY_HPJ_LIMIT_FREQUENT_MS, -1);
+ setDeviceConfigLong(QcConstants.KEY_HPJ_LIMIT_RARE_MS, -1);
+ setDeviceConfigLong(QcConstants.KEY_HPJ_LIMIT_RESTRICTED_MS, -1);
+ setDeviceConfigLong(QcConstants.KEY_HPJ_WINDOW_SIZE_MS, -1);
+ setDeviceConfigLong(QcConstants.KEY_HPJ_TOP_APP_TIME_CHUNK_SIZE_MS, -1);
+ setDeviceConfigLong(QcConstants.KEY_HPJ_REWARD_TOP_APP_MS, -1);
+ setDeviceConfigLong(QcConstants.KEY_HPJ_REWARD_INTERACTION_MS, -1);
+ setDeviceConfigLong(QcConstants.KEY_HPJ_REWARD_NOTIFICATION_SEEN_MS, -1);
assertEquals(MINUTE_IN_MILLIS, mQuotaController.getAllowedTimePerPeriodMs());
assertEquals(0, mQuotaController.getInQuotaBufferMs());
@@ -2296,6 +2407,16 @@ public class QuotaControllerTest {
assertEquals(0, mQuotaController.getBucketMaxSessionCounts()[RESTRICTED_INDEX]);
assertEquals(0, mQuotaController.getTimingSessionCoalescingDurationMs());
assertEquals(0, mQuotaController.getMinQuotaCheckDelayMs());
+ assertEquals(15 * MINUTE_IN_MILLIS, mQuotaController.getHpjLimitsMs()[ACTIVE_INDEX]);
+ assertEquals(15 * MINUTE_IN_MILLIS, mQuotaController.getHpjLimitsMs()[WORKING_INDEX]);
+ assertEquals(10 * MINUTE_IN_MILLIS, mQuotaController.getHpjLimitsMs()[FREQUENT_INDEX]);
+ assertEquals(10 * MINUTE_IN_MILLIS, mQuotaController.getHpjLimitsMs()[RARE_INDEX]);
+ assertEquals(0, mQuotaController.getHpjLimitsMs()[RESTRICTED_INDEX]);
+ assertEquals(HOUR_IN_MILLIS, mQuotaController.getHpjLimitWindowSizeMs());
+ assertEquals(1, mQuotaController.getHpjTopAppTimeChunkSizeMs());
+ assertEquals(10 * SECOND_IN_MILLIS, mQuotaController.getHpjRewardTopAppMs());
+ assertEquals(5 * SECOND_IN_MILLIS, mQuotaController.getHpjRewardInteractionMs());
+ assertEquals(0, mQuotaController.getHpjRewardNotificationSeenMs());
// Invalid configurations.
// In_QUOTA_BUFFER should never be greater than ALLOWED_TIME_PER_PERIOD
@@ -2318,6 +2439,16 @@ public class QuotaControllerTest {
setDeviceConfigLong(QcConstants.KEY_TIMING_SESSION_COALESCING_DURATION_MS,
25 * HOUR_IN_MILLIS);
setDeviceConfigLong(QcConstants.KEY_MIN_QUOTA_CHECK_DELAY_MS, 25 * HOUR_IN_MILLIS);
+ setDeviceConfigLong(QcConstants.KEY_HPJ_LIMIT_ACTIVE_MS, 25 * HOUR_IN_MILLIS);
+ setDeviceConfigLong(QcConstants.KEY_HPJ_LIMIT_WORKING_MS, 25 * HOUR_IN_MILLIS);
+ setDeviceConfigLong(QcConstants.KEY_HPJ_LIMIT_FREQUENT_MS, 25 * HOUR_IN_MILLIS);
+ setDeviceConfigLong(QcConstants.KEY_HPJ_LIMIT_RARE_MS, 25 * HOUR_IN_MILLIS);
+ setDeviceConfigLong(QcConstants.KEY_HPJ_LIMIT_RESTRICTED_MS, 25 * HOUR_IN_MILLIS);
+ setDeviceConfigLong(QcConstants.KEY_HPJ_WINDOW_SIZE_MS, 25 * HOUR_IN_MILLIS);
+ setDeviceConfigLong(QcConstants.KEY_HPJ_TOP_APP_TIME_CHUNK_SIZE_MS, 25 * HOUR_IN_MILLIS);
+ setDeviceConfigLong(QcConstants.KEY_HPJ_REWARD_TOP_APP_MS, 25 * HOUR_IN_MILLIS);
+ setDeviceConfigLong(QcConstants.KEY_HPJ_REWARD_INTERACTION_MS, 25 * HOUR_IN_MILLIS);
+ setDeviceConfigLong(QcConstants.KEY_HPJ_REWARD_NOTIFICATION_SEEN_MS, 25 * HOUR_IN_MILLIS);
assertEquals(24 * HOUR_IN_MILLIS, mQuotaController.getAllowedTimePerPeriodMs());
assertEquals(5 * MINUTE_IN_MILLIS, mQuotaController.getInQuotaBufferMs());
@@ -2332,6 +2463,16 @@ public class QuotaControllerTest {
assertEquals(15 * MINUTE_IN_MILLIS,
mQuotaController.getTimingSessionCoalescingDurationMs());
assertEquals(15 * MINUTE_IN_MILLIS, mQuotaController.getMinQuotaCheckDelayMs());
+ assertEquals(24 * HOUR_IN_MILLIS, mQuotaController.getHpjLimitsMs()[ACTIVE_INDEX]);
+ assertEquals(24 * HOUR_IN_MILLIS, mQuotaController.getHpjLimitsMs()[WORKING_INDEX]);
+ assertEquals(24 * HOUR_IN_MILLIS, mQuotaController.getHpjLimitsMs()[FREQUENT_INDEX]);
+ assertEquals(24 * HOUR_IN_MILLIS, mQuotaController.getHpjLimitsMs()[RARE_INDEX]);
+ assertEquals(24 * HOUR_IN_MILLIS, mQuotaController.getHpjLimitsMs()[RESTRICTED_INDEX]);
+ assertEquals(24 * HOUR_IN_MILLIS, mQuotaController.getHpjLimitWindowSizeMs());
+ assertEquals(15 * MINUTE_IN_MILLIS, mQuotaController.getHpjTopAppTimeChunkSizeMs());
+ assertEquals(15 * MINUTE_IN_MILLIS, mQuotaController.getHpjRewardTopAppMs());
+ assertEquals(15 * MINUTE_IN_MILLIS, mQuotaController.getHpjRewardInteractionMs());
+ assertEquals(5 * MINUTE_IN_MILLIS, mQuotaController.getHpjRewardNotificationSeenMs());
}
/** Tests that TimingSessions aren't saved when the device is charging. */
@@ -2937,7 +3078,7 @@ public class QuotaControllerTest {
mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
createTimingSession(
JobSchedulerService.sElapsedRealtimeClock.millis() - HOUR_IN_MILLIS,
- 10 * MINUTE_IN_MILLIS - remainingTimeMs, 1));
+ 10 * MINUTE_IN_MILLIS - remainingTimeMs, 1), false);
InOrder inOrder = inOrder(mJobSchedulerService);
@@ -3031,7 +3172,7 @@ public class QuotaControllerTest {
mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
createTimingSession(
JobSchedulerService.sElapsedRealtimeClock.millis() - HOUR_IN_MILLIS,
- 10 * MINUTE_IN_MILLIS - remainingTimeMs, 1));
+ 10 * MINUTE_IN_MILLIS - remainingTimeMs, 1), false);
// Start the job.
synchronized (mQuotaController.mLock) {
@@ -3069,10 +3210,10 @@ public class QuotaControllerTest {
// window, so as the package "reaches its quota" it will have more to keep running.
mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
createTimingSession(now - 2 * HOUR_IN_MILLIS,
- 10 * SECOND_IN_MILLIS - remainingTimeMs, 1));
+ 10 * SECOND_IN_MILLIS - remainingTimeMs, 1), false);
mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
createTimingSession(now - HOUR_IN_MILLIS,
- 9 * MINUTE_IN_MILLIS + 50 * SECOND_IN_MILLIS, 1));
+ 9 * MINUTE_IN_MILLIS + 50 * SECOND_IN_MILLIS, 1), false);
synchronized (mQuotaController.mLock) {
assertEquals(remainingTimeMs,
@@ -3098,7 +3239,9 @@ public class QuotaControllerTest {
}
// Handler is told to check when the quota will be consumed, not when the initial
// remaining time is over.
- verify(handler, atLeast(1)).sendMessageDelayed(any(), eq(10 * SECOND_IN_MILLIS));
+ verify(handler, atLeast(1)).sendMessageDelayed(
+ argThat(msg -> msg.what == QuotaController.MSG_REACHED_QUOTA),
+ eq(10 * SECOND_IN_MILLIS));
verify(handler, never()).sendMessageDelayed(any(), eq(remainingTimeMs));
// After 10 seconds, the job should finally be out of quota.
@@ -3242,4 +3385,1337 @@ public class QuotaControllerTest {
verify(mAlarmManager, times(1))
.set(anyInt(), eq(expectedWorkingAlarmTime), eq(TAG_QUOTA_CHECK), any(), any());
}
+
+ @Test
+ public void testGetRemainingHpjExecutionTimeLocked_NoHistory() {
+ final long[] limits = mQuotaController.getHpjLimitsMs();
+ for (int i = 0; i < limits.length; ++i) {
+ setStandbyBucket(i);
+ assertEquals("Got wrong remaining HPJ execution time for bucket #" + i,
+ limits[i],
+ mQuotaController.getRemainingHpjExecutionTimeLocked(
+ SOURCE_USER_ID, SOURCE_PACKAGE));
+ }
+ }
+
+ @Test
+ public void testGetRemainingHpjExecutionTimeLocked_AllSessionsWithinWindow() {
+ final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
+ mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+ createTimingSession(now - mQcConstants.HPJ_WINDOW_SIZE_MS, MINUTE_IN_MILLIS, 5),
+ true);
+ mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+ createTimingSession(now - 40 * MINUTE_IN_MILLIS, MINUTE_IN_MILLIS, 5), true);
+ mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+ createTimingSession(now - 30 * MINUTE_IN_MILLIS, MINUTE_IN_MILLIS, 5), true);
+ mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+ createTimingSession(now - 20 * MINUTE_IN_MILLIS, MINUTE_IN_MILLIS, 5), true);
+ mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+ createTimingSession(now - 10 * MINUTE_IN_MILLIS, MINUTE_IN_MILLIS, 5), true);
+
+ final long[] limits = mQuotaController.getHpjLimitsMs();
+ for (int i = 0; i < limits.length; ++i) {
+ setStandbyBucket(i);
+ assertEquals("Got wrong remaining HPJ execution time for bucket #" + i,
+ i == NEVER_INDEX ? 0 : (limits[i] - 5 * MINUTE_IN_MILLIS),
+ mQuotaController.getRemainingHpjExecutionTimeLocked(
+ SOURCE_USER_ID, SOURCE_PACKAGE));
+ }
+ }
+
+ @Test
+ public void testGetRemainingHpjExecutionTimeLocked_OneSessionStraddlesEdge() {
+ final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
+ final long[] limits = mQuotaController.getHpjLimitsMs();
+ for (int i = 0; i < limits.length; ++i) {
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.onUserRemovedLocked(SOURCE_USER_ID);
+ }
+ mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+ createTimingSession(now - (mQcConstants.HPJ_WINDOW_SIZE_MS + MINUTE_IN_MILLIS),
+ 2 * MINUTE_IN_MILLIS, 5), true);
+ mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+ createTimingSession(now - 40 * MINUTE_IN_MILLIS, MINUTE_IN_MILLIS, 5), true);
+ mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+ createTimingSession(now - 30 * MINUTE_IN_MILLIS, MINUTE_IN_MILLIS, 5), true);
+ mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+ createTimingSession(now - 20 * MINUTE_IN_MILLIS, MINUTE_IN_MILLIS, 5), true);
+ mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+ createTimingSession(now - 10 * MINUTE_IN_MILLIS, MINUTE_IN_MILLIS, 5), true);
+
+ setStandbyBucket(i);
+ assertEquals("Got wrong remaining HPJ execution time for bucket #" + i,
+ i == NEVER_INDEX ? 0 : (limits[i] - 5 * MINUTE_IN_MILLIS),
+ mQuotaController.getRemainingHpjExecutionTimeLocked(
+ SOURCE_USER_ID, SOURCE_PACKAGE));
+ }
+ }
+
+ @Test
+ public void testGetRemainingHpjExecutionTimeLocked_WithStaleSessions() {
+ final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
+
+ final long[] limits = mQuotaController.getHpjLimitsMs();
+ for (int i = 0; i < limits.length; ++i) {
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.onUserRemovedLocked(SOURCE_USER_ID);
+ }
+ mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+ createTimingSession(
+ now - (mQcConstants.HPJ_WINDOW_SIZE_MS + 10 * MINUTE_IN_MILLIS),
+ 2 * MINUTE_IN_MILLIS, 5), true);
+ mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+ createTimingSession(
+ now - (mQcConstants.HPJ_WINDOW_SIZE_MS + 5 * MINUTE_IN_MILLIS),
+ MINUTE_IN_MILLIS, 5), true);
+ mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+ createTimingSession(now - (mQcConstants.HPJ_WINDOW_SIZE_MS + MINUTE_IN_MILLIS),
+ 2 * MINUTE_IN_MILLIS, 5), true);
+ mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+ createTimingSession(now - 40 * MINUTE_IN_MILLIS, MINUTE_IN_MILLIS, 5), true);
+ mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+ createTimingSession(now - 30 * MINUTE_IN_MILLIS, MINUTE_IN_MILLIS, 5), true);
+ mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+ createTimingSession(now - 20 * MINUTE_IN_MILLIS, MINUTE_IN_MILLIS, 5), true);
+ mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+ createTimingSession(now - 10 * MINUTE_IN_MILLIS, MINUTE_IN_MILLIS, 5), true);
+
+ setStandbyBucket(i);
+ assertEquals("Got wrong remaining HPJ execution time for bucket #" + i,
+ i == NEVER_INDEX ? 0 : (limits[i] - 5 * MINUTE_IN_MILLIS),
+ mQuotaController.getRemainingHpjExecutionTimeLocked(
+ SOURCE_USER_ID, SOURCE_PACKAGE));
+ }
+ }
+
+ /**
+ * Tests that getRemainingHpjExecutionTimeLocked returns the correct stats soon after device
+ * startup.
+ */
+ @Test
+ public void testGetRemainingHpjExecutionTimeLocked_BeginningOfTime() {
+ // Set time to 3 minutes after boot.
+ advanceElapsedClock(-JobSchedulerService.sElapsedRealtimeClock.millis());
+ advanceElapsedClock(3 * MINUTE_IN_MILLIS);
+
+ mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+ createTimingSession(MINUTE_IN_MILLIS, MINUTE_IN_MILLIS, 2), true);
+ mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+ createTimingSession(150 * SECOND_IN_MILLIS, 15 * SECOND_IN_MILLIS, 5), true);
+
+ final long[] limits = mQuotaController.getHpjLimitsMs();
+ for (int i = 0; i < limits.length; ++i) {
+ setStandbyBucket(i);
+ assertEquals("Got wrong remaining HPJ execution time for bucket #" + i,
+ i == NEVER_INDEX ? 0 : (limits[i] - 75 * SECOND_IN_MILLIS),
+ mQuotaController.getRemainingHpjExecutionTimeLocked(
+ SOURCE_USER_ID, SOURCE_PACKAGE));
+ }
+ }
+
+ @Test
+ public void testGetTimeUntilHpjQuotaConsumedLocked_NoHistory() {
+ final long[] limits = mQuotaController.getHpjLimitsMs();
+ for (int i = 0; i < limits.length; ++i) {
+ setStandbyBucket(i);
+ assertEquals("Got wrong time until HPJ quota consumed for bucket #" + i,
+ limits[i], mQuotaController.getTimeUntilHpjQuotaConsumedLocked(
+ SOURCE_USER_ID, SOURCE_PACKAGE));
+ }
+ }
+
+ @Test
+ public void testGetTimeUntilHpjQuotaConsumedLocked_AllSessionsWithinWindow() {
+ final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
+ mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+ createTimingSession(now - 40 * MINUTE_IN_MILLIS, MINUTE_IN_MILLIS, 5), true);
+ mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+ createTimingSession(now - 30 * MINUTE_IN_MILLIS, MINUTE_IN_MILLIS, 5), true);
+ mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+ createTimingSession(now - 20 * MINUTE_IN_MILLIS, 2 * MINUTE_IN_MILLIS, 5), true);
+ mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+ createTimingSession(now - 10 * MINUTE_IN_MILLIS, MINUTE_IN_MILLIS, 5), true);
+
+ final long[] limits = mQuotaController.getHpjLimitsMs();
+ for (int i = 0; i < limits.length; ++i) {
+ setStandbyBucket(i);
+ assertEquals("Got wrong time until HPJ quota consumed for bucket #" + i,
+ i == NEVER_INDEX ? 0 : (limits[i] - 5 * MINUTE_IN_MILLIS),
+ mQuotaController.getTimeUntilHpjQuotaConsumedLocked(
+ SOURCE_USER_ID, SOURCE_PACKAGE));
+ }
+ }
+
+ @Test
+ public void testGetTimeUntilHpjQuotaConsumedLocked_SessionsAtEdgeOfWindow() {
+ final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
+ mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+ createTimingSession(now - mQcConstants.HPJ_WINDOW_SIZE_MS, MINUTE_IN_MILLIS, 5),
+ true);
+ mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+ createTimingSession(now - (mQcConstants.HPJ_WINDOW_SIZE_MS - 2 * MINUTE_IN_MILLIS),
+ MINUTE_IN_MILLIS, 5), true);
+ mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+ createTimingSession(now - (mQcConstants.HPJ_WINDOW_SIZE_MS - 10 * MINUTE_IN_MILLIS),
+ MINUTE_IN_MILLIS, 5), true);
+ mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+ createTimingSession(now - 20 * MINUTE_IN_MILLIS, MINUTE_IN_MILLIS, 5), true);
+ mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+ createTimingSession(now - 10 * MINUTE_IN_MILLIS, MINUTE_IN_MILLIS, 5), true);
+
+ setDeviceConfigLong(QcConstants.KEY_HPJ_LIMIT_ACTIVE_MS, 30 * MINUTE_IN_MILLIS);
+ setDeviceConfigLong(QcConstants.KEY_HPJ_LIMIT_WORKING_MS, 20 * MINUTE_IN_MILLIS);
+ setDeviceConfigLong(QcConstants.KEY_HPJ_LIMIT_FREQUENT_MS, 15 * MINUTE_IN_MILLIS);
+ setDeviceConfigLong(QcConstants.KEY_HPJ_LIMIT_RARE_MS, 10 * MINUTE_IN_MILLIS);
+ setDeviceConfigLong(QcConstants.KEY_HPJ_LIMIT_RESTRICTED_MS, 5 * MINUTE_IN_MILLIS);
+
+ setStandbyBucket(ACTIVE_INDEX);
+ assertEquals("Got wrong time until HPJ quota consumed for bucket #" + ACTIVE_INDEX,
+ 28 * MINUTE_IN_MILLIS,
+ mQuotaController.getTimeUntilHpjQuotaConsumedLocked(
+ SOURCE_USER_ID, SOURCE_PACKAGE));
+
+ setStandbyBucket(WORKING_INDEX);
+ assertEquals("Got wrong time until HPJ quota consumed for bucket #" + WORKING_INDEX,
+ 18 * MINUTE_IN_MILLIS,
+ mQuotaController.getTimeUntilHpjQuotaConsumedLocked(
+ SOURCE_USER_ID, SOURCE_PACKAGE));
+
+ setStandbyBucket(FREQUENT_INDEX);
+ assertEquals("Got wrong time until HPJ quota consumed for bucket #" + FREQUENT_INDEX,
+ 13 * MINUTE_IN_MILLIS,
+ mQuotaController.getTimeUntilHpjQuotaConsumedLocked(
+ SOURCE_USER_ID, SOURCE_PACKAGE));
+
+ setStandbyBucket(RARE_INDEX);
+ assertEquals("Got wrong time until HPJ quota consumed for bucket #" + RARE_INDEX,
+ 7 * MINUTE_IN_MILLIS,
+ mQuotaController.getTimeUntilHpjQuotaConsumedLocked(
+ SOURCE_USER_ID, SOURCE_PACKAGE));
+
+ setStandbyBucket(RESTRICTED_INDEX);
+ assertEquals("Got wrong time until HPJ quota consumed for bucket #" + RESTRICTED_INDEX,
+ MINUTE_IN_MILLIS,
+ mQuotaController.getTimeUntilHpjQuotaConsumedLocked(
+ SOURCE_USER_ID, SOURCE_PACKAGE));
+ }
+
+ @Test
+ public void testGetTimeUntilHpjQuotaConsumedLocked_OneSessionStraddlesEdge() {
+ final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
+
+ mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+ createTimingSession(now - (mQcConstants.HPJ_WINDOW_SIZE_MS + MINUTE_IN_MILLIS),
+ 2 * MINUTE_IN_MILLIS, 5), true);
+ mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+ createTimingSession(now - 40 * MINUTE_IN_MILLIS, MINUTE_IN_MILLIS, 5), true);
+ mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+ createTimingSession(now - 30 * MINUTE_IN_MILLIS, MINUTE_IN_MILLIS, 5), true);
+ mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+ createTimingSession(now - 20 * MINUTE_IN_MILLIS, MINUTE_IN_MILLIS, 5), true);
+ mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+ createTimingSession(now - 10 * MINUTE_IN_MILLIS, MINUTE_IN_MILLIS, 5), true);
+
+ setDeviceConfigLong(QcConstants.KEY_HPJ_LIMIT_ACTIVE_MS, 30 * MINUTE_IN_MILLIS);
+ setDeviceConfigLong(QcConstants.KEY_HPJ_LIMIT_WORKING_MS, 20 * MINUTE_IN_MILLIS);
+ setDeviceConfigLong(QcConstants.KEY_HPJ_LIMIT_FREQUENT_MS, 15 * MINUTE_IN_MILLIS);
+ setDeviceConfigLong(QcConstants.KEY_HPJ_LIMIT_RARE_MS, 10 * MINUTE_IN_MILLIS);
+ setDeviceConfigLong(QcConstants.KEY_HPJ_LIMIT_RESTRICTED_MS, 5 * MINUTE_IN_MILLIS);
+
+ setStandbyBucket(ACTIVE_INDEX);
+ assertEquals("Got wrong time until HPJ quota consumed for bucket #" + ACTIVE_INDEX,
+ 26 * MINUTE_IN_MILLIS,
+ mQuotaController.getTimeUntilHpjQuotaConsumedLocked(
+ SOURCE_USER_ID, SOURCE_PACKAGE));
+
+ setStandbyBucket(WORKING_INDEX);
+ assertEquals("Got wrong time until HPJ quota consumed for bucket #" + WORKING_INDEX,
+ 16 * MINUTE_IN_MILLIS,
+ mQuotaController.getTimeUntilHpjQuotaConsumedLocked(
+ SOURCE_USER_ID, SOURCE_PACKAGE));
+
+ setStandbyBucket(FREQUENT_INDEX);
+ assertEquals("Got wrong time until HPJ quota consumed for bucket #" + FREQUENT_INDEX,
+ 11 * MINUTE_IN_MILLIS,
+ mQuotaController.getTimeUntilHpjQuotaConsumedLocked(
+ SOURCE_USER_ID, SOURCE_PACKAGE));
+
+ setStandbyBucket(RARE_INDEX);
+ assertEquals("Got wrong time until HPJ quota consumed for bucket #" + RARE_INDEX,
+ 6 * MINUTE_IN_MILLIS,
+ mQuotaController.getTimeUntilHpjQuotaConsumedLocked(
+ SOURCE_USER_ID, SOURCE_PACKAGE));
+
+ setStandbyBucket(RESTRICTED_INDEX);
+ assertEquals("Got wrong time until HPJ quota consumed for bucket #" + RESTRICTED_INDEX,
+ MINUTE_IN_MILLIS,
+ mQuotaController.getTimeUntilHpjQuotaConsumedLocked(
+ SOURCE_USER_ID, SOURCE_PACKAGE));
+ }
+
+ @Test
+ public void testGetTimeUntilHpjQuotaConsumedLocked_WithStaleSessions() {
+ final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
+
+ List timingSessions = new ArrayList<>();
+ timingSessions.add(
+ createTimingSession(now - (mQcConstants.HPJ_WINDOW_SIZE_MS + 10 * MINUTE_IN_MILLIS),
+ 2 * MINUTE_IN_MILLIS, 5));
+ timingSessions.add(
+ createTimingSession(now - (mQcConstants.HPJ_WINDOW_SIZE_MS + 5 * MINUTE_IN_MILLIS),
+ MINUTE_IN_MILLIS, 5));
+ timingSessions.add(
+ createTimingSession(now - (mQcConstants.HPJ_WINDOW_SIZE_MS + MINUTE_IN_MILLIS),
+ 2 * MINUTE_IN_MILLIS, 5));
+ timingSessions.add(
+ createTimingSession(now - 40 * MINUTE_IN_MILLIS, MINUTE_IN_MILLIS, 5));
+ timingSessions.add(
+ createTimingSession(now - 30 * MINUTE_IN_MILLIS, MINUTE_IN_MILLIS, 5));
+ timingSessions.add(
+ createTimingSession(now - 20 * MINUTE_IN_MILLIS, MINUTE_IN_MILLIS, 5));
+ timingSessions.add(
+ createTimingSession(now - 10 * MINUTE_IN_MILLIS, MINUTE_IN_MILLIS, 5));
+
+ setDeviceConfigLong(QcConstants.KEY_HPJ_LIMIT_ACTIVE_MS, 30 * MINUTE_IN_MILLIS);
+ setDeviceConfigLong(QcConstants.KEY_HPJ_LIMIT_WORKING_MS, 20 * MINUTE_IN_MILLIS);
+ setDeviceConfigLong(QcConstants.KEY_HPJ_LIMIT_FREQUENT_MS, 15 * MINUTE_IN_MILLIS);
+ setDeviceConfigLong(QcConstants.KEY_HPJ_LIMIT_RARE_MS, 10 * MINUTE_IN_MILLIS);
+ setDeviceConfigLong(QcConstants.KEY_HPJ_LIMIT_RESTRICTED_MS, 5 * MINUTE_IN_MILLIS);
+
+ runTestGetTimeUntilHpjQuotaConsumedLocked(
+ timingSessions, ACTIVE_INDEX, 26 * MINUTE_IN_MILLIS);
+ runTestGetTimeUntilHpjQuotaConsumedLocked(
+ timingSessions, WORKING_INDEX, 16 * MINUTE_IN_MILLIS);
+ runTestGetTimeUntilHpjQuotaConsumedLocked(
+ timingSessions, FREQUENT_INDEX, 11 * MINUTE_IN_MILLIS);
+ runTestGetTimeUntilHpjQuotaConsumedLocked(timingSessions, RARE_INDEX, 6 * MINUTE_IN_MILLIS);
+ runTestGetTimeUntilHpjQuotaConsumedLocked(
+ timingSessions, RESTRICTED_INDEX, MINUTE_IN_MILLIS);
+ }
+
+ /**
+ * Tests that getTimeUntilHpjQuotaConsumedLocked returns the correct stats soon after device
+ * startup.
+ */
+ @Test
+ public void testGetTimeUntilHpjQuotaConsumedLocked_BeginningOfTime() {
+ // Set time to 3 minutes after boot.
+ advanceElapsedClock(-JobSchedulerService.sElapsedRealtimeClock.millis());
+ advanceElapsedClock(3 * MINUTE_IN_MILLIS);
+
+ mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+ createTimingSession(MINUTE_IN_MILLIS, MINUTE_IN_MILLIS, 2), true);
+ mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+ createTimingSession(150 * SECOND_IN_MILLIS, 15 * SECOND_IN_MILLIS, 5), true);
+
+ final long[] limits = mQuotaController.getHpjLimitsMs();
+ for (int i = 0; i < limits.length; ++i) {
+ setStandbyBucket(i);
+ assertEquals("Got wrong time until HPJ quota consumed for bucket #" + i,
+ limits[i], // All existing sessions will phase out
+ mQuotaController.getTimeUntilHpjQuotaConsumedLocked(
+ SOURCE_USER_ID, SOURCE_PACKAGE));
+ }
+ }
+
+ private void runTestGetTimeUntilHpjQuotaConsumedLocked(
+ List timingSessions, int bucketIndex, long expectedValue) {
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.onUserRemovedLocked(SOURCE_USER_ID);
+ }
+ if (timingSessions != null) {
+ for (TimingSession session : timingSessions) {
+ mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE, session, true);
+ }
+ }
+
+ setStandbyBucket(bucketIndex);
+ assertEquals("Got wrong time until HPJ quota consumed for bucket #" + bucketIndex,
+ expectedValue,
+ mQuotaController.getTimeUntilHpjQuotaConsumedLocked(
+ SOURCE_USER_ID, SOURCE_PACKAGE));
+ }
+
+ @Test
+ public void testMaybeScheduleStartAlarmLocked_Hpj() {
+ // saveTimingSession calls maybeScheduleCleanupAlarmLocked which interferes with these tests
+ // because it schedules an alarm too. Prevent it from doing so.
+ spyOn(mQuotaController);
+ doNothing().when(mQuotaController).maybeScheduleCleanupAlarmLocked();
+
+ final int standbyBucket = WORKING_INDEX;
+ setStandbyBucket(standbyBucket);
+ setDeviceConfigLong(QcConstants.KEY_HPJ_LIMIT_WORKING_MS, 20 * MINUTE_IN_MILLIS);
+
+ InOrder inOrder = inOrder(mAlarmManager);
+
+ synchronized (mQuotaController.mLock) {
+ // No sessions saved yet.
+ mQuotaController.maybeScheduleStartAlarmLocked(
+ SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket);
+ }
+ inOrder.verify(mAlarmManager, never())
+ .set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+
+ // Test with timing sessions out of window.
+ final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
+ mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+ createTimingSession(now - 25 * HOUR_IN_MILLIS, 30 * MINUTE_IN_MILLIS, 1), true);
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.maybeScheduleStartAlarmLocked(
+ SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket);
+ }
+ inOrder.verify(mAlarmManager, never())
+ .set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+
+ // Test with timing sessions in window but still in quota.
+ final long end = now - (22 * HOUR_IN_MILLIS - 5 * MINUTE_IN_MILLIS);
+ final long expectedAlarmTime = now + 2 * HOUR_IN_MILLIS + mQcConstants.IN_QUOTA_BUFFER_MS;
+ mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+ new TimingSession(now - 22 * HOUR_IN_MILLIS, end, 1), true);
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.maybeScheduleStartAlarmLocked(
+ SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket);
+ }
+ inOrder.verify(mAlarmManager, never())
+ .set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+
+ // Add some more sessions, but still in quota.
+ mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+ createTimingSession(now - HOUR_IN_MILLIS, 5 * MINUTE_IN_MILLIS, 1), true);
+ mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+ createTimingSession(now - (50 * MINUTE_IN_MILLIS), 4 * MINUTE_IN_MILLIS, 1), true);
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.maybeScheduleStartAlarmLocked(
+ SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket);
+ }
+ inOrder.verify(mAlarmManager, never())
+ .set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+
+ // Test when out of quota.
+ mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+ createTimingSession(now - 30 * MINUTE_IN_MILLIS, 6 * MINUTE_IN_MILLIS, 1), true);
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.maybeScheduleStartAlarmLocked(
+ SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket);
+ }
+ inOrder.verify(mAlarmManager, times(1))
+ .set(anyInt(), eq(expectedAlarmTime), eq(TAG_QUOTA_CHECK), any(), any());
+
+ // Alarm already scheduled, so make sure it's not scheduled again.
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.maybeScheduleStartAlarmLocked(
+ SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket);
+ }
+ inOrder.verify(mAlarmManager, never())
+ .set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+ }
+
+ /** Tests that the start alarm is properly rescheduled if the app's bucket is changed. */
+ @Test
+ public void testMaybeScheduleStartAlarmLocked_Hpj_BucketChange() {
+ // saveTimingSession calls maybeScheduleCleanupAlarmLocked which interferes with these tests
+ // because it schedules an alarm too. Prevent it from doing so.
+ spyOn(mQuotaController);
+ doNothing().when(mQuotaController).maybeScheduleCleanupAlarmLocked();
+
+ setDeviceConfigLong(QcConstants.KEY_HPJ_LIMIT_ACTIVE_MS, 30 * MINUTE_IN_MILLIS);
+ setDeviceConfigLong(QcConstants.KEY_HPJ_LIMIT_WORKING_MS, 20 * MINUTE_IN_MILLIS);
+ setDeviceConfigLong(QcConstants.KEY_HPJ_LIMIT_FREQUENT_MS, 15 * MINUTE_IN_MILLIS);
+ setDeviceConfigLong(QcConstants.KEY_HPJ_LIMIT_RARE_MS, 10 * MINUTE_IN_MILLIS);
+
+ final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
+ // Affects active bucket
+ mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+ createTimingSession(now - 12 * HOUR_IN_MILLIS, 9 * MINUTE_IN_MILLIS, 3), true);
+ // Affects active and working buckets
+ mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+ createTimingSession(now - 4 * HOUR_IN_MILLIS, 5 * MINUTE_IN_MILLIS, 3), true);
+ // Affects active, working, and frequent buckets
+ mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+ createTimingSession(now - HOUR_IN_MILLIS, 5 * MINUTE_IN_MILLIS, 10), true);
+ // Affects all buckets
+ mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+ createTimingSession(now - 5 * MINUTE_IN_MILLIS, 10 * MINUTE_IN_MILLIS, 3), true);
+
+ InOrder inOrder = inOrder(mAlarmManager);
+
+ // Start in ACTIVE bucket.
+ setStandbyBucket(ACTIVE_INDEX);
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.maybeScheduleStartAlarmLocked(
+ SOURCE_USER_ID, SOURCE_PACKAGE, ACTIVE_INDEX);
+ }
+ inOrder.verify(mAlarmManager, never())
+ .set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+ inOrder.verify(mAlarmManager, never()).cancel(any(AlarmManager.OnAlarmListener.class));
+
+ // And down from there.
+ setStandbyBucket(WORKING_INDEX);
+ final long expectedWorkingAlarmTime =
+ (now - 4 * HOUR_IN_MILLIS) + (24 * HOUR_IN_MILLIS)
+ + mQcConstants.IN_QUOTA_BUFFER_MS;
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.maybeScheduleStartAlarmLocked(
+ SOURCE_USER_ID, SOURCE_PACKAGE, WORKING_INDEX);
+ }
+ inOrder.verify(mAlarmManager, times(1))
+ .set(anyInt(), eq(expectedWorkingAlarmTime), eq(TAG_QUOTA_CHECK), any(), any());
+
+ setStandbyBucket(FREQUENT_INDEX);
+ final long expectedFrequentAlarmTime =
+ (now - HOUR_IN_MILLIS) + (24 * HOUR_IN_MILLIS) + mQcConstants.IN_QUOTA_BUFFER_MS;
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.maybeScheduleStartAlarmLocked(
+ SOURCE_USER_ID, SOURCE_PACKAGE, FREQUENT_INDEX);
+ }
+ inOrder.verify(mAlarmManager, times(1))
+ .set(anyInt(), eq(expectedFrequentAlarmTime), eq(TAG_QUOTA_CHECK), any(), any());
+
+ setStandbyBucket(RARE_INDEX);
+ final long expectedRareAlarmTime =
+ (now - 5 * MINUTE_IN_MILLIS) + (24 * HOUR_IN_MILLIS)
+ + mQcConstants.IN_QUOTA_BUFFER_MS;
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.maybeScheduleStartAlarmLocked(
+ SOURCE_USER_ID, SOURCE_PACKAGE, RARE_INDEX);
+ }
+ inOrder.verify(mAlarmManager, times(1))
+ .set(anyInt(), eq(expectedRareAlarmTime), eq(TAG_QUOTA_CHECK), any(), any());
+
+ // And back up again.
+ setStandbyBucket(FREQUENT_INDEX);
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.maybeScheduleStartAlarmLocked(
+ SOURCE_USER_ID, SOURCE_PACKAGE, FREQUENT_INDEX);
+ }
+ inOrder.verify(mAlarmManager, times(1))
+ .set(anyInt(), eq(expectedFrequentAlarmTime), eq(TAG_QUOTA_CHECK), any(), any());
+
+ setStandbyBucket(WORKING_INDEX);
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.maybeScheduleStartAlarmLocked(
+ SOURCE_USER_ID, SOURCE_PACKAGE, WORKING_INDEX);
+ }
+ inOrder.verify(mAlarmManager, times(1))
+ .set(anyInt(), eq(expectedWorkingAlarmTime), eq(TAG_QUOTA_CHECK), any(), any());
+
+ setStandbyBucket(ACTIVE_INDEX);
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.maybeScheduleStartAlarmLocked(
+ SOURCE_USER_ID, SOURCE_PACKAGE, ACTIVE_INDEX);
+ }
+ inOrder.verify(mAlarmManager, never())
+ .set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+ inOrder.verify(mAlarmManager, times(1)).cancel(any(AlarmManager.OnAlarmListener.class));
+ }
+
+ /**
+ * Tests that the start alarm is properly rescheduled if the earliest session that contributes
+ * to the app being out of quota contributes less than the quota buffer time.
+ */
+ @Test
+ public void testMaybeScheduleStartAlarmLocked_Hpj_SmallRollingQuota() {
+ // saveTimingSession calls maybeScheduleCleanupAlarmLocked which interferes with these tests
+ // because it schedules an alarm too. Prevent it from doing so.
+ spyOn(mQuotaController);
+ doNothing().when(mQuotaController).maybeScheduleCleanupAlarmLocked();
+
+ final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
+ setStandbyBucket(WORKING_INDEX);
+ final long contributionMs = mQcConstants.IN_QUOTA_BUFFER_MS / 2;
+ final long remainingTimeMs = mQcConstants.HPJ_LIMIT_WORKING_MS - contributionMs;
+
+ // Session straddles edge of bucket window. Only the contribution should be counted towards
+ // the quota.
+ mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+ createTimingSession(now - (24 * HOUR_IN_MILLIS + 3 * MINUTE_IN_MILLIS),
+ 3 * MINUTE_IN_MILLIS + contributionMs, 3), true);
+ mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+ createTimingSession(now - 23 * HOUR_IN_MILLIS, remainingTimeMs, 2), true);
+ // Expected alarm time should be when the app will have QUOTA_BUFFER_MS time of quota, which
+ // is 24 hours + (QUOTA_BUFFER_MS - contributionMs) after the start of the second session.
+ final long expectedAlarmTime =
+ now + HOUR_IN_MILLIS + (mQcConstants.IN_QUOTA_BUFFER_MS - contributionMs);
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.maybeScheduleStartAlarmLocked(
+ SOURCE_USER_ID, SOURCE_PACKAGE, WORKING_INDEX);
+ }
+ verify(mAlarmManager, times(1))
+ .set(anyInt(), eq(expectedAlarmTime), eq(TAG_QUOTA_CHECK), any(), any());
+ }
+
+ /** Tests that TimingSessions aren't saved when the device is charging. */
+ @Test
+ public void testHpjTimerTracking_Charging() {
+ setCharging();
+
+ JobStatus jobStatus = createHpjJobStatus("testHpjTimerTracking_Charging", 1);
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.maybeStartTrackingJobLocked(jobStatus, null);
+ }
+
+ assertNull(mQuotaController.getHpjTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));
+
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.prepareForExecutionLocked(jobStatus);
+ }
+ advanceElapsedClock(5 * SECOND_IN_MILLIS);
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.maybeStopTrackingJobLocked(jobStatus, null, false);
+ }
+ assertNull(mQuotaController.getHpjTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));
+ }
+
+ /** Tests that TimingSessions are saved properly when the device is discharging. */
+ @Test
+ public void testHpjTimerTracking_Discharging() {
+ setDischarging();
+ setProcessState(ActivityManager.PROCESS_STATE_BACKUP);
+
+ JobStatus jobStatus = createHpjJobStatus("testHpjTimerTracking_Discharging", 1);
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.maybeStartTrackingJobLocked(jobStatus, null);
+ }
+
+ assertNull(mQuotaController.getHpjTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));
+
+ List expected = new ArrayList<>();
+
+ long start = JobSchedulerService.sElapsedRealtimeClock.millis();
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.prepareForExecutionLocked(jobStatus);
+ }
+ advanceElapsedClock(5 * SECOND_IN_MILLIS);
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.maybeStopTrackingJobLocked(jobStatus, null, false);
+ }
+ expected.add(createTimingSession(start, 5 * SECOND_IN_MILLIS, 1));
+ assertEquals(expected,
+ mQuotaController.getHpjTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));
+
+ // Test overlapping jobs.
+ JobStatus jobStatus2 = createHpjJobStatus("testHpjTimerTracking_Discharging", 2);
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.maybeStartTrackingJobLocked(jobStatus2, null);
+ }
+
+ JobStatus jobStatus3 = createHpjJobStatus("testHpjTimerTracking_Discharging", 3);
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.maybeStartTrackingJobLocked(jobStatus3, null);
+ }
+
+ advanceElapsedClock(SECOND_IN_MILLIS);
+
+ start = JobSchedulerService.sElapsedRealtimeClock.millis();
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.maybeStartTrackingJobLocked(jobStatus, null);
+ mQuotaController.prepareForExecutionLocked(jobStatus);
+ }
+ advanceElapsedClock(10 * SECOND_IN_MILLIS);
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.prepareForExecutionLocked(jobStatus2);
+ }
+ advanceElapsedClock(10 * SECOND_IN_MILLIS);
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.maybeStopTrackingJobLocked(jobStatus, null, false);
+ }
+ advanceElapsedClock(10 * SECOND_IN_MILLIS);
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.prepareForExecutionLocked(jobStatus3);
+ }
+ advanceElapsedClock(20 * SECOND_IN_MILLIS);
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.maybeStopTrackingJobLocked(jobStatus3, null, false);
+ }
+ advanceElapsedClock(10 * SECOND_IN_MILLIS);
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.maybeStopTrackingJobLocked(jobStatus2, null, false);
+ }
+ expected.add(createTimingSession(start, MINUTE_IN_MILLIS, 3));
+ assertEquals(expected,
+ mQuotaController.getHpjTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));
+ }
+
+ /**
+ * Tests that TimingSessions are saved properly when the device alternates between
+ * charging and discharging.
+ */
+ @Test
+ public void testHpjTimerTracking_ChargingAndDischarging() {
+ setProcessState(ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND);
+
+ JobStatus jobStatus = createHpjJobStatus("testHpjTimerTracking_ChargingAndDischarging", 1);
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.maybeStartTrackingJobLocked(jobStatus, null);
+ }
+ JobStatus jobStatus2 = createHpjJobStatus("testHpjTimerTracking_ChargingAndDischarging", 2);
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.maybeStartTrackingJobLocked(jobStatus2, null);
+ }
+ JobStatus jobStatus3 = createHpjJobStatus("testHpjTimerTracking_ChargingAndDischarging", 3);
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.maybeStartTrackingJobLocked(jobStatus3, null);
+ }
+ assertNull(mQuotaController.getHpjTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));
+ List expected = new ArrayList<>();
+
+ // A job starting while charging. Only the portion that runs during the discharging period
+ // should be counted.
+ setCharging();
+
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.prepareForExecutionLocked(jobStatus);
+ }
+ advanceElapsedClock(10 * SECOND_IN_MILLIS);
+ setDischarging();
+ long start = JobSchedulerService.sElapsedRealtimeClock.millis();
+ advanceElapsedClock(10 * SECOND_IN_MILLIS);
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.maybeStopTrackingJobLocked(jobStatus, jobStatus, true);
+ }
+ expected.add(createTimingSession(start, 10 * SECOND_IN_MILLIS, 1));
+ assertEquals(expected,
+ mQuotaController.getHpjTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));
+
+ advanceElapsedClock(SECOND_IN_MILLIS);
+
+ // One job starts while discharging, spans a charging session, and ends after the charging
+ // session. Only the portions during the discharging periods should be counted. This should
+ // result in two TimingSessions. A second job starts while discharging and ends within the
+ // charging session. Only the portion during the first discharging portion should be
+ // counted. A third job starts and ends within the charging session. The third job
+ // shouldn't be included in either job count.
+ setDischarging();
+ start = JobSchedulerService.sElapsedRealtimeClock.millis();
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.maybeStartTrackingJobLocked(jobStatus, null);
+ mQuotaController.prepareForExecutionLocked(jobStatus);
+ }
+ advanceElapsedClock(10 * SECOND_IN_MILLIS);
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.prepareForExecutionLocked(jobStatus2);
+ }
+ advanceElapsedClock(10 * SECOND_IN_MILLIS);
+ setCharging();
+ expected.add(createTimingSession(start, 20 * SECOND_IN_MILLIS, 2));
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.prepareForExecutionLocked(jobStatus3);
+ }
+ advanceElapsedClock(10 * SECOND_IN_MILLIS);
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.maybeStopTrackingJobLocked(jobStatus3, null, false);
+ }
+ advanceElapsedClock(10 * SECOND_IN_MILLIS);
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.maybeStopTrackingJobLocked(jobStatus2, null, false);
+ }
+ advanceElapsedClock(10 * SECOND_IN_MILLIS);
+ setDischarging();
+ start = JobSchedulerService.sElapsedRealtimeClock.millis();
+ advanceElapsedClock(20 * SECOND_IN_MILLIS);
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.maybeStopTrackingJobLocked(jobStatus, null, false);
+ }
+ expected.add(createTimingSession(start, 20 * SECOND_IN_MILLIS, 1));
+ assertEquals(expected,
+ mQuotaController.getHpjTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));
+
+ // A job starting while discharging and ending while charging. Only the portion that runs
+ // during the discharging period should be counted.
+ setDischarging();
+ start = JobSchedulerService.sElapsedRealtimeClock.millis();
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.maybeStartTrackingJobLocked(jobStatus2, null);
+ mQuotaController.prepareForExecutionLocked(jobStatus2);
+ }
+ advanceElapsedClock(10 * SECOND_IN_MILLIS);
+ expected.add(createTimingSession(start, 10 * SECOND_IN_MILLIS, 1));
+ setCharging();
+ advanceElapsedClock(10 * SECOND_IN_MILLIS);
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.maybeStopTrackingJobLocked(jobStatus2, null, false);
+ }
+ assertEquals(expected,
+ mQuotaController.getHpjTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));
+ }
+
+ /** Tests that TimingSessions are saved properly when all the jobs are background jobs. */
+ @Test
+ public void testHpjTimerTracking_AllBackground() {
+ setDischarging();
+ setProcessState(ActivityManager.PROCESS_STATE_RECEIVER);
+
+ JobStatus jobStatus = createHpjJobStatus("testHpjTimerTracking_AllBackground", 1);
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.maybeStartTrackingJobLocked(jobStatus, null);
+ }
+ assertNull(mQuotaController.getHpjTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));
+
+ List expected = new ArrayList<>();
+
+ // Test single job.
+ long start = JobSchedulerService.sElapsedRealtimeClock.millis();
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.prepareForExecutionLocked(jobStatus);
+ }
+ advanceElapsedClock(5 * SECOND_IN_MILLIS);
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.maybeStopTrackingJobLocked(jobStatus, null, false);
+ }
+ expected.add(createTimingSession(start, 5 * SECOND_IN_MILLIS, 1));
+ assertEquals(expected,
+ mQuotaController.getHpjTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));
+
+ // Test overlapping jobs.
+ JobStatus jobStatus2 = createHpjJobStatus("testHpjTimerTracking_AllBackground", 2);
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.maybeStartTrackingJobLocked(jobStatus2, null);
+ }
+
+ JobStatus jobStatus3 = createHpjJobStatus("testHpjTimerTracking_AllBackground", 3);
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.maybeStartTrackingJobLocked(jobStatus3, null);
+ }
+
+ advanceElapsedClock(SECOND_IN_MILLIS);
+
+ start = JobSchedulerService.sElapsedRealtimeClock.millis();
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.maybeStartTrackingJobLocked(jobStatus, null);
+ mQuotaController.prepareForExecutionLocked(jobStatus);
+ }
+ advanceElapsedClock(10 * SECOND_IN_MILLIS);
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.prepareForExecutionLocked(jobStatus2);
+ }
+ advanceElapsedClock(10 * SECOND_IN_MILLIS);
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.maybeStopTrackingJobLocked(jobStatus, null, false);
+ }
+ advanceElapsedClock(10 * SECOND_IN_MILLIS);
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.prepareForExecutionLocked(jobStatus3);
+ }
+ advanceElapsedClock(20 * SECOND_IN_MILLIS);
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.maybeStopTrackingJobLocked(jobStatus3, null, false);
+ }
+ advanceElapsedClock(10 * SECOND_IN_MILLIS);
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.maybeStopTrackingJobLocked(jobStatus2, null, false);
+ }
+ expected.add(createTimingSession(start, MINUTE_IN_MILLIS, 3));
+ assertEquals(expected,
+ mQuotaController.getHpjTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));
+ }
+
+ /** Tests that Timers don't count foreground jobs. */
+ @Test
+ public void testHpjTimerTracking_AllForeground() {
+ setDischarging();
+
+ JobStatus jobStatus = createHpjJobStatus("testHpjTimerTracking_AllForeground", 1);
+ setProcessState(ActivityManager.PROCESS_STATE_TOP);
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.maybeStartTrackingJobLocked(jobStatus, null);
+ }
+
+ assertNull(mQuotaController.getHpjTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));
+
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.prepareForExecutionLocked(jobStatus);
+ }
+ advanceElapsedClock(5 * SECOND_IN_MILLIS);
+ // Change to a state that should still be considered foreground.
+ setProcessState(ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE);
+ advanceElapsedClock(5 * SECOND_IN_MILLIS);
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.maybeStopTrackingJobLocked(jobStatus, null, false);
+ }
+ assertNull(mQuotaController.getHpjTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));
+ }
+
+ /**
+ * Tests that Timers properly track sessions when switching between foreground and background
+ * states.
+ */
+ @Test
+ public void testHpjTimerTracking_ForegroundAndBackground() {
+ setDischarging();
+
+ JobStatus jobBg1 = createHpjJobStatus("testHpjTimerTracking_ForegroundAndBackground", 1);
+ JobStatus jobBg2 = createHpjJobStatus("testHpjTimerTracking_ForegroundAndBackground", 2);
+ JobStatus jobFg3 = createHpjJobStatus("testHpjTimerTracking_ForegroundAndBackground", 3);
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.maybeStartTrackingJobLocked(jobBg1, null);
+ mQuotaController.maybeStartTrackingJobLocked(jobBg2, null);
+ mQuotaController.maybeStartTrackingJobLocked(jobFg3, null);
+ }
+ assertNull(mQuotaController.getHpjTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));
+ List expected = new ArrayList<>();
+
+ // UID starts out inactive.
+ setProcessState(ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND);
+ long start = JobSchedulerService.sElapsedRealtimeClock.millis();
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.prepareForExecutionLocked(jobBg1);
+ }
+ advanceElapsedClock(10 * SECOND_IN_MILLIS);
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.maybeStopTrackingJobLocked(jobBg1, jobBg1, true);
+ }
+ expected.add(createTimingSession(start, 10 * SECOND_IN_MILLIS, 1));
+ assertEquals(expected,
+ mQuotaController.getHpjTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));
+
+ advanceElapsedClock(SECOND_IN_MILLIS);
+
+ // Bg job starts while inactive, spans an entire active session, and ends after the
+ // active session.
+ // App switching to foreground state then fg job starts.
+ // App remains in foreground state after coming to foreground, so there should only be one
+ // session.
+ start = JobSchedulerService.sElapsedRealtimeClock.millis();
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.maybeStartTrackingJobLocked(jobBg2, null);
+ mQuotaController.prepareForExecutionLocked(jobBg2);
+ }
+ advanceElapsedClock(10 * SECOND_IN_MILLIS);
+ expected.add(createTimingSession(start, 10 * SECOND_IN_MILLIS, 1));
+ setProcessState(ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE);
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.prepareForExecutionLocked(jobFg3);
+ }
+ advanceElapsedClock(10 * SECOND_IN_MILLIS);
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.maybeStopTrackingJobLocked(jobFg3, null, false);
+ }
+ advanceElapsedClock(10 * SECOND_IN_MILLIS);
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.maybeStopTrackingJobLocked(jobBg2, null, false);
+ }
+ assertEquals(expected,
+ mQuotaController.getHpjTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));
+
+ advanceElapsedClock(SECOND_IN_MILLIS);
+
+ // Bg job 1 starts, then fg job starts. Bg job 1 job ends. Shortly after, uid goes
+ // "inactive" and then bg job 2 starts. Then fg job ends.
+ // This should result in two TimingSessions:
+ // * The first should have a count of 1
+ // * The second should have a count of 2 since it will include both jobs
+ start = JobSchedulerService.sElapsedRealtimeClock.millis();
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.maybeStartTrackingJobLocked(jobBg1, null);
+ mQuotaController.maybeStartTrackingJobLocked(jobBg2, null);
+ mQuotaController.maybeStartTrackingJobLocked(jobFg3, null);
+ }
+ setProcessState(ActivityManager.PROCESS_STATE_LAST_ACTIVITY);
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.prepareForExecutionLocked(jobBg1);
+ }
+ advanceElapsedClock(10 * SECOND_IN_MILLIS);
+ expected.add(createTimingSession(start, 10 * SECOND_IN_MILLIS, 1));
+ setProcessState(ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE);
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.prepareForExecutionLocked(jobFg3);
+ }
+ advanceElapsedClock(10 * SECOND_IN_MILLIS);
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.maybeStopTrackingJobLocked(jobBg1, jobBg1, true);
+ }
+ advanceElapsedClock(10 * SECOND_IN_MILLIS); // UID "inactive" now
+ start = JobSchedulerService.sElapsedRealtimeClock.millis();
+ setProcessState(ActivityManager.PROCESS_STATE_TOP_SLEEPING);
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.prepareForExecutionLocked(jobBg2);
+ }
+ advanceElapsedClock(10 * SECOND_IN_MILLIS);
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.maybeStopTrackingJobLocked(jobFg3, null, false);
+ }
+ advanceElapsedClock(10 * SECOND_IN_MILLIS);
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.maybeStopTrackingJobLocked(jobBg2, null, false);
+ }
+ expected.add(createTimingSession(start, 20 * SECOND_IN_MILLIS, 2));
+ assertEquals(expected,
+ mQuotaController.getHpjTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));
+ }
+
+ /**
+ * Tests that Timers properly track overlapping top and background jobs.
+ */
+ @Test
+ public void testHpjTimerTracking_TopAndNonTop() {
+ setDischarging();
+
+ JobStatus jobBg1 = createHpjJobStatus("testHpjTimerTracking_TopAndNonTop", 1);
+ JobStatus jobBg2 = createHpjJobStatus("testHpjTimerTracking_TopAndNonTop", 2);
+ JobStatus jobFg1 = createHpjJobStatus("testHpjTimerTracking_TopAndNonTop", 3);
+ JobStatus jobTop = createHpjJobStatus("testHpjTimerTracking_TopAndNonTop", 4);
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.maybeStartTrackingJobLocked(jobBg1, null);
+ mQuotaController.maybeStartTrackingJobLocked(jobBg2, null);
+ mQuotaController.maybeStartTrackingJobLocked(jobFg1, null);
+ mQuotaController.maybeStartTrackingJobLocked(jobTop, null);
+ }
+ assertNull(mQuotaController.getHpjTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));
+ List expected = new ArrayList<>();
+
+ // UID starts out inactive.
+ setProcessState(ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND);
+ long start = JobSchedulerService.sElapsedRealtimeClock.millis();
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.prepareForExecutionLocked(jobBg1);
+ }
+ advanceElapsedClock(10 * SECOND_IN_MILLIS);
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.maybeStopTrackingJobLocked(jobBg1, jobBg1, true);
+ }
+ expected.add(createTimingSession(start, 10 * SECOND_IN_MILLIS, 1));
+ assertEquals(expected,
+ mQuotaController.getHpjTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));
+
+ advanceElapsedClock(SECOND_IN_MILLIS);
+
+ // Bg job starts while inactive, spans an entire active session, and ends after the
+ // active session.
+ // App switching to top state then fg job starts.
+ // App remains in top state after coming to top, so there should only be one
+ // session.
+ start = JobSchedulerService.sElapsedRealtimeClock.millis();
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.maybeStartTrackingJobLocked(jobBg2, null);
+ mQuotaController.prepareForExecutionLocked(jobBg2);
+ }
+ advanceElapsedClock(10 * SECOND_IN_MILLIS);
+ expected.add(createTimingSession(start, 10 * SECOND_IN_MILLIS, 1));
+ setProcessState(ActivityManager.PROCESS_STATE_TOP);
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.prepareForExecutionLocked(jobTop);
+ }
+ advanceElapsedClock(10 * SECOND_IN_MILLIS);
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.maybeStopTrackingJobLocked(jobTop, null, false);
+ }
+ advanceElapsedClock(10 * SECOND_IN_MILLIS);
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.maybeStopTrackingJobLocked(jobBg2, null, false);
+ }
+ assertEquals(expected,
+ mQuotaController.getHpjTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));
+
+ advanceElapsedClock(SECOND_IN_MILLIS);
+
+ // Bg job 1 starts, then top job starts. Bg job 1 job ends. Then app goes to
+ // foreground_service and a new job starts. Shortly after, uid goes
+ // "inactive" and then bg job 2 starts. Then top job ends, followed by bg and fg jobs.
+ // This should result in two TimingSessions:
+ // * The first should have a count of 1
+ // * The second should have a count of 2, which accounts for the bg2 and fg, but not top
+ // jobs.
+ start = JobSchedulerService.sElapsedRealtimeClock.millis();
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.maybeStartTrackingJobLocked(jobBg1, null);
+ mQuotaController.maybeStartTrackingJobLocked(jobBg2, null);
+ mQuotaController.maybeStartTrackingJobLocked(jobTop, null);
+ }
+ setProcessState(ActivityManager.PROCESS_STATE_LAST_ACTIVITY);
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.prepareForExecutionLocked(jobBg1);
+ }
+ advanceElapsedClock(10 * SECOND_IN_MILLIS);
+ expected.add(createTimingSession(start, 10 * SECOND_IN_MILLIS, 1));
+ setProcessState(ActivityManager.PROCESS_STATE_TOP);
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.prepareForExecutionLocked(jobTop);
+ }
+ advanceElapsedClock(10 * SECOND_IN_MILLIS);
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.maybeStopTrackingJobLocked(jobBg1, jobBg1, true);
+ }
+ advanceElapsedClock(5 * SECOND_IN_MILLIS);
+ setProcessState(ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE);
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.prepareForExecutionLocked(jobFg1);
+ }
+ advanceElapsedClock(5 * SECOND_IN_MILLIS);
+ setProcessState(ActivityManager.PROCESS_STATE_TOP);
+ advanceElapsedClock(10 * SECOND_IN_MILLIS); // UID "inactive" now
+ start = JobSchedulerService.sElapsedRealtimeClock.millis();
+ setProcessState(ActivityManager.PROCESS_STATE_TOP_SLEEPING);
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.prepareForExecutionLocked(jobBg2);
+ }
+ advanceElapsedClock(10 * SECOND_IN_MILLIS);
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.maybeStopTrackingJobLocked(jobTop, null, false);
+ }
+ advanceElapsedClock(10 * SECOND_IN_MILLIS);
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.maybeStopTrackingJobLocked(jobBg2, null, false);
+ mQuotaController.maybeStopTrackingJobLocked(jobFg1, null, false);
+ }
+ expected.add(createTimingSession(start, 20 * SECOND_IN_MILLIS, 2));
+ assertEquals(expected,
+ mQuotaController.getHpjTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));
+ }
+
+ /**
+ * Tests that HPJs aren't stopped when an app runs out of quota.
+ */
+ @Test
+ public void testHpjTracking_OutOfQuota_ForegroundAndBackground() {
+ setDischarging();
+
+ JobStatus jobBg =
+ createHpjJobStatus("testHpjTracking_OutOfQuota_ForegroundAndBackground", 1);
+ JobStatus jobTop =
+ createHpjJobStatus("testHpjTracking_OutOfQuota_ForegroundAndBackground", 2);
+ JobStatus jobUnstarted =
+ createHpjJobStatus("testHpjTracking_OutOfQuota_ForegroundAndBackground", 3);
+ trackJobs(jobBg, jobTop, jobUnstarted);
+ setStandbyBucket(WORKING_INDEX, jobTop, jobBg, jobUnstarted);
+ // Now the package only has 20 seconds to run.
+ final long remainingTimeMs = 20 * SECOND_IN_MILLIS;
+ mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+ createTimingSession(
+ JobSchedulerService.sElapsedRealtimeClock.millis() - HOUR_IN_MILLIS,
+ mQcConstants.HPJ_LIMIT_WORKING_MS - remainingTimeMs, 1), true);
+
+ InOrder inOrder = inOrder(mJobSchedulerService);
+
+ // UID starts out inactive.
+ setProcessState(ActivityManager.PROCESS_STATE_SERVICE);
+ // Start the job.
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.prepareForExecutionLocked(jobBg);
+ }
+ advanceElapsedClock(remainingTimeMs / 2);
+ // New job starts after UID is in the foreground. Since the app is now in the foreground, it
+ // should continue to have remainingTimeMs / 2 time remaining.
+ setProcessState(ActivityManager.PROCESS_STATE_TOP);
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.prepareForExecutionLocked(jobTop);
+ }
+ advanceElapsedClock(remainingTimeMs);
+
+ // Wait for some extra time to allow for job processing.
+ inOrder.verify(mJobSchedulerService,
+ timeout(remainingTimeMs + 2 * SECOND_IN_MILLIS).times(0))
+ .onControllerStateChanged();
+ synchronized (mQuotaController.mLock) {
+ assertEquals(remainingTimeMs / 2,
+ mQuotaController.getRemainingHpjExecutionTimeLocked(
+ SOURCE_USER_ID, SOURCE_PACKAGE));
+ }
+ // Go to a background state.
+ setProcessState(ActivityManager.PROCESS_STATE_TOP_SLEEPING);
+ advanceElapsedClock(remainingTimeMs / 2 + 1);
+ inOrder.verify(mJobSchedulerService,
+ timeout(remainingTimeMs / 2 + 2 * SECOND_IN_MILLIS).times(1))
+ .onControllerStateChanged();
+ // Top and bg HPJs should still be allowed to run since they started before the app ran
+ // out of quota.
+ assertTrue(jobBg.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_HPJ_QUOTA));
+ assertTrue(jobTop.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_HPJ_QUOTA));
+ assertFalse(jobUnstarted.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_HPJ_QUOTA));
+ synchronized (mQuotaController.mLock) {
+ assertTrue(
+ 0 >= mQuotaController
+ .getRemainingHpjExecutionTimeLocked(SOURCE_USER_ID, SOURCE_PACKAGE));
+ }
+
+ // New jobs to run.
+ JobStatus jobBg2 =
+ createHpjJobStatus("testHpjTracking_OutOfQuota_ForegroundAndBackground", 4);
+ JobStatus jobTop2 =
+ createHpjJobStatus("testHpjTracking_OutOfQuota_ForegroundAndBackground", 5);
+ JobStatus jobFg =
+ createHpjJobStatus("testHpjTracking_OutOfQuota_ForegroundAndBackground", 6);
+ setStandbyBucket(WORKING_INDEX, jobBg2, jobTop2, jobFg);
+
+ advanceElapsedClock(20 * SECOND_IN_MILLIS);
+ setProcessState(ActivityManager.PROCESS_STATE_TOP);
+ // Confirm QC recognizes that jobUnstarted has changed from out-of-quota to in-quota.
+ inOrder.verify(mJobSchedulerService, timeout(SECOND_IN_MILLIS).times(1))
+ .onControllerStateChanged();
+ trackJobs(jobTop2, jobFg);
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.prepareForExecutionLocked(jobTop2);
+ }
+ assertTrue(jobTop2.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_HPJ_QUOTA));
+ assertTrue(jobFg.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_HPJ_QUOTA));
+ assertTrue(jobBg.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_HPJ_QUOTA));
+ assertTrue(jobUnstarted.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_HPJ_QUOTA));
+
+ // App still in foreground so everything should be in quota.
+ advanceElapsedClock(20 * SECOND_IN_MILLIS);
+ setProcessState(ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE);
+ assertTrue(jobTop2.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_HPJ_QUOTA));
+ assertTrue(jobFg.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_HPJ_QUOTA));
+ assertTrue(jobBg.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_HPJ_QUOTA));
+ assertTrue(jobUnstarted.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_HPJ_QUOTA));
+
+ advanceElapsedClock(20 * SECOND_IN_MILLIS);
+ setProcessState(ActivityManager.PROCESS_STATE_SERVICE);
+ inOrder.verify(mJobSchedulerService, timeout(SECOND_IN_MILLIS).times(1))
+ .onControllerStateChanged();
+ // App is now in background and out of quota. Fg should now change to out of quota since it
+ // wasn't started. Top should remain in quota since it started when the app was in TOP.
+ assertTrue(jobTop2.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_HPJ_QUOTA));
+ assertFalse(jobFg.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_HPJ_QUOTA));
+ assertTrue(jobBg.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_HPJ_QUOTA));
+ trackJobs(jobBg2);
+ assertFalse(jobBg2.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_HPJ_QUOTA));
+ assertFalse(jobUnstarted.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_HPJ_QUOTA));
+ synchronized (mQuotaController.mLock) {
+ assertTrue(
+ 0 >= mQuotaController
+ .getRemainingHpjExecutionTimeLocked(SOURCE_USER_ID, SOURCE_PACKAGE));
+ }
+ }
+
+ /**
+ * Tests that Timers properly track overlapping top and background jobs.
+ */
+ @Test
+ public void testHpjTimerTrackingSeparateFromRegularTracking() {
+ setDischarging();
+ setProcessState(ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND);
+
+ JobStatus jobReg1 = createJobStatus("testHpjTimerTrackingSeparateFromRegularTracking", 1);
+ JobStatus jobHpj1 =
+ createHpjJobStatus("testHpjTimerTrackingSeparateFromRegularTracking", 2);
+ JobStatus jobReg2 = createJobStatus("testHpjTimerTrackingSeparateFromRegularTracking", 3);
+ JobStatus jobHpj2 =
+ createHpjJobStatus("testHpjTimerTrackingSeparateFromRegularTracking", 4);
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.maybeStartTrackingJobLocked(jobReg1, null);
+ mQuotaController.maybeStartTrackingJobLocked(jobHpj1, null);
+ mQuotaController.maybeStartTrackingJobLocked(jobReg2, null);
+ mQuotaController.maybeStartTrackingJobLocked(jobHpj2, null);
+ }
+ assertNull(mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));
+ assertNull(mQuotaController.getHpjTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));
+ List expectedRegular = new ArrayList<>();
+ List expectedHpj = new ArrayList<>();
+
+ // First, regular job runs by itself.
+ long start = JobSchedulerService.sElapsedRealtimeClock.millis();
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.prepareForExecutionLocked(jobReg1);
+ }
+ advanceElapsedClock(10 * SECOND_IN_MILLIS);
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.maybeStopTrackingJobLocked(jobReg1, jobReg1, true);
+ }
+ expectedRegular.add(createTimingSession(start, 10 * SECOND_IN_MILLIS, 1));
+ assertEquals(expectedRegular,
+ mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));
+ assertNull(mQuotaController.getHpjTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));
+
+ advanceElapsedClock(SECOND_IN_MILLIS);
+
+ // Next, HPJ runs by itself.
+ start = JobSchedulerService.sElapsedRealtimeClock.millis();
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.prepareForExecutionLocked(jobHpj1);
+ }
+ advanceElapsedClock(10 * SECOND_IN_MILLIS);
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.maybeStopTrackingJobLocked(jobHpj1, null, false);
+ }
+ expectedHpj.add(createTimingSession(start, 10 * SECOND_IN_MILLIS, 1));
+ assertEquals(expectedRegular,
+ mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));
+ assertEquals(expectedHpj,
+ mQuotaController.getHpjTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));
+
+ advanceElapsedClock(SECOND_IN_MILLIS);
+
+ // Finally, a regular job and HPJ happen to overlap runs.
+ start = JobSchedulerService.sElapsedRealtimeClock.millis();
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.prepareForExecutionLocked(jobHpj2);
+ }
+ advanceElapsedClock(5 * SECOND_IN_MILLIS);
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.prepareForExecutionLocked(jobReg2);
+ }
+ advanceElapsedClock(5 * SECOND_IN_MILLIS);
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.maybeStopTrackingJobLocked(jobHpj2, null, false);
+ }
+ expectedHpj.add(createTimingSession(start, 10 * SECOND_IN_MILLIS, 1));
+ advanceElapsedClock(5 * SECOND_IN_MILLIS);
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.maybeStopTrackingJobLocked(jobReg2, null, false);
+ }
+ expectedRegular.add(
+ createTimingSession(start + 5 * SECOND_IN_MILLIS, 10 * SECOND_IN_MILLIS, 1));
+ assertEquals(expectedRegular,
+ mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));
+ assertEquals(expectedHpj,
+ mQuotaController.getHpjTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));
+ }
+
+ /**
+ * Tests that a job is properly handled when it's at the edge of its quota and the old quota is
+ * being phased out.
+ */
+ @Test
+ public void testHpjTracking_RollingQuota() {
+ JobStatus jobStatus = createHpjJobStatus("testHpjTracking_RollingQuota", 1);
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.maybeStartTrackingJobLocked(jobStatus, null);
+ }
+ setStandbyBucket(WORKING_INDEX, jobStatus);
+ setProcessState(ActivityManager.PROCESS_STATE_SERVICE);
+ Handler handler = mQuotaController.getHandler();
+ spyOn(handler);
+
+ long now = JobSchedulerService.sElapsedRealtimeClock.millis();
+ final long remainingTimeMs = SECOND_IN_MILLIS;
+ // The package only has one second to run, but this session is at the edge of the rolling
+ // window, so as the package "reaches its quota" it will have more to keep running.
+ mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+ createTimingSession(now - mQcConstants.HPJ_WINDOW_SIZE_MS,
+ 10 * SECOND_IN_MILLIS - remainingTimeMs, 1), true);
+ mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+ createTimingSession(now - HOUR_IN_MILLIS,
+ mQcConstants.HPJ_LIMIT_WORKING_MS - 10 * SECOND_IN_MILLIS, 1), true);
+
+ synchronized (mQuotaController.mLock) {
+ assertEquals(remainingTimeMs,
+ mQuotaController.getRemainingHpjExecutionTimeLocked(
+ SOURCE_USER_ID, SOURCE_PACKAGE));
+
+ // Start the job.
+ mQuotaController.prepareForExecutionLocked(jobStatus);
+ }
+ advanceElapsedClock(remainingTimeMs);
+
+ // Wait for some extra time to allow for job processing.
+ verify(mJobSchedulerService,
+ timeout(remainingTimeMs + 2 * SECOND_IN_MILLIS).times(0))
+ .onControllerStateChanged();
+ assertTrue(jobStatus.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_HPJ_QUOTA));
+ // The job used up the remaining quota, but in that time, the same amount of time in the
+ // old TimingSession also fell out of the quota window, so it should still have the same
+ // amount of remaining time left its quota.
+ synchronized (mQuotaController.mLock) {
+ assertEquals(remainingTimeMs,
+ mQuotaController.getRemainingHpjExecutionTimeLocked(
+ SOURCE_USER_ID, SOURCE_PACKAGE));
+ }
+ // Handler is told to check when the quota will be consumed, not when the initial
+ // remaining time is over.
+ verify(handler, atLeast(1)).sendMessageDelayed(
+ argThat(msg -> msg.what == QuotaController.MSG_REACHED_HPJ_QUOTA),
+ eq(10 * SECOND_IN_MILLIS));
+ verify(handler, never()).sendMessageDelayed(any(), eq(remainingTimeMs));
+ }
}