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: + *
    + *
  1. Run as soon as possible
  2. + *
  3. Be exempted from Doze and battery saver restrictions
  4. + *
  5. Have network access
  6. + *
+ * + * 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)); + } }