From 64345146b94be09ecfa2064ab6d4e1d5ac88b997 Mon Sep 17 00:00:00 2001 From: Kweku Adams Date: Thu, 5 Nov 2020 13:57:54 -0800 Subject: [PATCH] First version of HPJs. This is the base implementation for HPJs. It allows scheduling and running of HPJs with their own quota. Quota is regained when previous sessions expire and when certain events take place. A few things still aren't implemented yet. They'll be done in a separate CL: 1. Free temp-allowlist jobs 2. Dedicated JobServiceContexts for HPJs 3. Additional quota for system-critical roles Bug: 171305774 Test: atest --rerun-until-failure 5 com.android.server.job.controllers.QuotaControllerTest Test: atest CtsJobSchedulerTestCases Change-Id: I4b704cb0b406769f6de8b60bf762dd1f4eb10c68 --- .../java/android/app/job/JobInfo.java | 77 +- .../java/android/app/job/JobParameters.java | 19 +- .../server/job/JobConcurrencyManager.java | 6 +- .../server/job/JobSchedulerService.java | 27 +- .../android/server/job/JobServiceContext.java | 32 +- .../server/job/controllers/JobStatus.java | 54 +- .../job/controllers/QuotaController.java | 1064 +++++++++- core/api/current.txt | 5 +- core/java/android/app/usage/UsageEvents.java | 4 + core/proto/android/server/jobscheduler.proto | 56 +- .../ConnectivityControllerTest.java | 2 +- .../job/controllers/QuotaControllerTest.java | 1716 +++++++++++++++-- 12 files changed, 2860 insertions(+), 202 deletions(-) diff --git a/apex/jobscheduler/framework/java/android/app/job/JobInfo.java b/apex/jobscheduler/framework/java/android/app/job/JobInfo.java index 2c8a5589ca515..7418265316bcd 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 e302b25873b40..bbd575f3fbecf 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -7684,6 +7684,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(); @@ -7715,7 +7716,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); @@ -7755,6 +7757,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)); + } }