diff --git a/apex/jobscheduler/framework/java/com/android/server/usage/AppStandbyInternal.java b/apex/jobscheduler/framework/java/com/android/server/usage/AppStandbyInternal.java index e4306e5e2bda6..dd0fe95e174e6 100644 --- a/apex/jobscheduler/framework/java/com/android/server/usage/AppStandbyInternal.java +++ b/apex/jobscheduler/framework/java/com/android/server/usage/AppStandbyInternal.java @@ -59,6 +59,13 @@ public interface AppStandbyInternal { public void onUserInteractionStarted(String packageName, @UserIdInt int userId) { // No-op by default } + + /** + * Optional callback to inform the listener to give the app a temporary quota bump. + */ + public void triggerTemporaryQuotaBump(String packageName, @UserIdInt int userId) { + // No-op by default + } } void onBootPhase(int phase); 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 c6ba1eac56c0e..bb8d175c2375b 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 @@ -270,13 +270,12 @@ public final class QuotaController extends StateController { private final SparseArrayMap mEJPkgTimers = new SparseArrayMap<>(); /** List of all regular timing sessions for a package-userId combo, in chronological order. */ - private final SparseArrayMap> mTimingSessions = - new SparseArrayMap<>(); + private final SparseArrayMap> mTimingEvents = new SparseArrayMap<>(); /** * List of all expedited job timing sessions for a package-userId combo, in chronological order. */ - private final SparseArrayMap> mEJTimingSessions = + private final SparseArrayMap> mEJTimingSessions = new SparseArrayMap<>(); /** @@ -519,6 +518,14 @@ public final class QuotaController extends StateController { private long mEJGracePeriodTopAppMs = QcConstants.DEFAULT_EJ_GRACE_PERIOD_TOP_APP_MS; + private long mQuotaBumpAdditionalDurationMs = + QcConstants.DEFAULT_QUOTA_BUMP_ADDITIONAL_DURATION_MS; + private int mQuotaBumpAdditionalJobCount = QcConstants.DEFAULT_QUOTA_BUMP_ADDITIONAL_JOB_COUNT; + private int mQuotaBumpAdditionalSessionCount = + QcConstants.DEFAULT_QUOTA_BUMP_ADDITIONAL_SESSION_COUNT; + private long mQuotaBumpWindowSizeMs = QcConstants.DEFAULT_QUOTA_BUMP_WINDOW_SIZE_MS; + private int mQuotaBumpLimit = QcConstants.DEFAULT_QUOTA_BUMP_LIMIT; + /** * List of system apps with the {@link android.Manifest.permission#INSTALL_PACKAGES} permission * granted for each user. @@ -707,7 +714,7 @@ public final class QuotaController extends StateController { mTrackedJobs.delete(userId); mPkgTimers.delete(userId); mEJPkgTimers.delete(userId); - mTimingSessions.delete(userId); + mTimingEvents.delete(userId); mEJTimingSessions.delete(userId); mInQuotaAlarmQueue.removeAlarmsForUserId(userId); mExecutionStatsCache.delete(userId); @@ -738,7 +745,7 @@ public final class QuotaController extends StateController { timer.dropEverythingLocked(); } } - mTimingSessions.delete(userId, packageName); + mTimingEvents.delete(userId, packageName); mEJTimingSessions.delete(userId, packageName); mInQuotaAlarmQueue.removeAlarmForKey(new Package(userId, packageName)); mExecutionStatsCache.delete(userId, packageName); @@ -928,7 +935,7 @@ public final class QuotaController extends StateController { (stats.jobRateLimitExpirationTimeElapsed <= now || stats.jobCountInRateLimitingWindow < mMaxJobCountPerRateLimitingWindow); return isUnderAllowedTimeQuota - && (stats.bgJobCountInWindow < mMaxBucketJobCounts[standbyBucket]); + && stats.bgJobCountInWindow < stats.jobCountLimit; } private boolean isUnderSessionCountQuotaLocked(@NonNull ExecutionStats stats, @@ -937,7 +944,7 @@ public final class QuotaController extends StateController { final boolean isUnderAllowedTimeQuota = (stats.sessionRateLimitExpirationTimeElapsed <= now || stats.sessionCountInRateLimitingWindow < mMaxSessionCountPerRateLimitingWindow); return isUnderAllowedTimeQuota - && stats.sessionCountInWindow < mMaxBucketSessionCounts[standbyBucket]; + && stats.sessionCountInWindow < stats.sessionCountLimit; } @VisibleForTesting @@ -984,12 +991,12 @@ public final class QuotaController extends StateController { long remainingMs = limitMs - quota.getTallyLocked(); // Stale sessions may still be factored into tally. Make sure they're removed. - List timingSessions = mEJTimingSessions.get(userId, packageName); + List timingSessions = mEJTimingSessions.get(userId, packageName); final long nowElapsed = sElapsedRealtimeClock.millis(); final long windowStartTimeElapsed = nowElapsed - mEJLimitWindowSizeMs; if (timingSessions != null) { while (timingSessions.size() > 0) { - TimingSession ts = timingSessions.get(0); + TimingSession ts = (TimingSession) timingSessions.get(0); if (ts.endTimeElapsed < windowStartTimeElapsed) { final long duration = ts.endTimeElapsed - ts.startTimeElapsed; remainingMs += duration; @@ -1042,9 +1049,9 @@ public final class QuotaController extends StateController { return 0; } - List sessions = mTimingSessions.get(userId, packageName); + List events = mTimingEvents.get(userId, packageName); final ExecutionStats stats = getExecutionStatsLocked(userId, packageName, standbyBucket); - if (sessions == null || sessions.size() == 0) { + if (events == null || events.size() == 0) { // Regular ACTIVE case. Since the bucket size equals the allowed time, the app jobs can // essentially run until they reach the maximum limit. if (stats.windowSizeMs == mAllowedTimePerPeriodMs[standbyBucket]) { @@ -1064,7 +1071,7 @@ public final class QuotaController extends StateController { // essentially run until they reach the maximum limit. if (stats.windowSizeMs == mAllowedTimePerPeriodMs[standbyBucket]) { return calculateTimeUntilQuotaConsumedLocked( - sessions, startMaxElapsed, maxExecutionTimeRemainingMs); + events, startMaxElapsed, maxExecutionTimeRemainingMs, false); } // Need to check both max time and period time in case one is less than the other. @@ -1073,9 +1080,9 @@ public final class QuotaController extends StateController { // bucket value. return Math.min( calculateTimeUntilQuotaConsumedLocked( - sessions, startMaxElapsed, maxExecutionTimeRemainingMs), + events, startMaxElapsed, maxExecutionTimeRemainingMs, false), calculateTimeUntilQuotaConsumedLocked( - sessions, startWindowElapsed, allowedTimeRemainingMs)); + events, startWindowElapsed, allowedTimeRemainingMs, true)); } /** @@ -1084,12 +1091,36 @@ public final class QuotaController extends StateController { * @param windowStartElapsed The start of the window, in the elapsed realtime timebase. * @param deadSpaceMs How much time can be allowed to count towards the quota */ - private long calculateTimeUntilQuotaConsumedLocked(@NonNull List sessions, - final long windowStartElapsed, long deadSpaceMs) { + private long calculateTimeUntilQuotaConsumedLocked(@NonNull List sessions, + final long windowStartElapsed, long deadSpaceMs, boolean allowQuotaBumps) { long timeUntilQuotaConsumedMs = 0; long start = windowStartElapsed; - for (int i = 0; i < sessions.size(); ++i) { - TimingSession session = sessions.get(i); + int numQuotaBumps = 0; + final long quotaBumpWindowStartElapsed = + sElapsedRealtimeClock.millis() - mQuotaBumpWindowSizeMs; + final int numSessions = sessions.size(); + if (allowQuotaBumps) { + for (int i = numSessions - 1; i >= 0; --i) { + TimedEvent event = sessions.get(i); + + if (event instanceof QuotaBump) { + if (event.getEndTimeElapsed() >= quotaBumpWindowStartElapsed + && numQuotaBumps++ < mQuotaBumpLimit) { + deadSpaceMs += mQuotaBumpAdditionalDurationMs; + } else { + break; + } + } + } + } + for (int i = 0; i < numSessions; ++i) { + TimedEvent event = sessions.get(i); + + if (event instanceof QuotaBump) { + continue; + } + + TimingSession session = (TimingSession) event; if (session.endTimeElapsed < windowStartElapsed) { // Outside of window. Ignore. @@ -1129,7 +1160,7 @@ public final class QuotaController extends StateController { final long remainingExecutionTimeMs = getRemainingEJExecutionTimeLocked(userId, packageName); - List sessions = mEJTimingSessions.get(userId, packageName); + List sessions = mEJTimingSessions.get(userId, packageName); if (sessions == null || sessions.size() == 0) { return remainingExecutionTimeMs; } @@ -1146,7 +1177,7 @@ public final class QuotaController extends StateController { long phasedOutSessionTimeMs = 0; for (int i = 0; i < sessions.size(); ++i) { - TimingSession session = sessions.get(i); + TimingSession session = (TimingSession) sessions.get(i); if (session.endTimeElapsed < startWindowElapsed) { // Edge case where a session became stale in the time between the call to // getRemainingEJExecutionTimeLocked and this line. @@ -1159,7 +1190,7 @@ public final class QuotaController extends StateController { } else { // Session fully inside window final long timeBetweenSessions = session.startTimeElapsed - - (i == 0 ? startWindowElapsed : sessions.get(i - 1).endTimeElapsed); + - (i == 0 ? startWindowElapsed : sessions.get(i - 1).getEndTimeElapsed()); final long usedDeadSpaceMs = Math.min(remainingDeadSpaceMs, timeBetweenSessions); deadSpaceMs += usedDeadSpaceMs; if (usedDeadSpaceMs == timeBetweenSessions) { @@ -1267,22 +1298,49 @@ public final class QuotaController extends StateController { } } - List sessions = mTimingSessions.get(userId, packageName); - if (sessions == null || sessions.size() == 0) { + List events = mTimingEvents.get(userId, packageName); + if (events == null || events.size() == 0) { return; } final long startWindowElapsed = nowElapsed - stats.windowSizeMs; final long startMaxElapsed = nowElapsed - MAX_PERIOD_MS; int sessionCountInWindow = 0; - // The minimum time between the start time and the beginning of the sessions that were + int numQuotaBumps = 0; + final long quotaBumpWindowStartElapsed = nowElapsed - mQuotaBumpWindowSizeMs; + // The minimum time between the start time and the beginning of the events that were // looked at --> how much time the stats will be valid for. long emptyTimeMs = Long.MAX_VALUE; // Sessions are non-overlapping and in order of occurrence, so iterating backwards will get // the most recent ones. - final int loopStart = sessions.size() - 1; + final int loopStart = events.size() - 1; + // Process QuotaBumps first to ensure the limits are properly adjusted. for (int i = loopStart; i >= 0; --i) { - TimingSession session = sessions.get(i); + TimedEvent event = events.get(i); + + if (event.getEndTimeElapsed() < quotaBumpWindowStartElapsed + || numQuotaBumps >= mQuotaBumpLimit) { + break; + } + + if (event instanceof QuotaBump) { + stats.allowedTimePerPeriodMs += mQuotaBumpAdditionalDurationMs; + stats.jobCountLimit += mQuotaBumpAdditionalJobCount; + stats.sessionCountLimit += mQuotaBumpAdditionalSessionCount; + emptyTimeMs = Math.min(emptyTimeMs, + event.getEndTimeElapsed() - quotaBumpWindowStartElapsed); + numQuotaBumps++; + } + } + TimingSession lastSeenTimingSession = null; + for (int i = loopStart; i >= 0; --i) { + TimedEvent event = events.get(i); + + if (event instanceof QuotaBump) { + continue; + } + + TimingSession session = (TimingSession) event; // Window management. if (startWindowElapsed < session.endTimeElapsed) { @@ -1309,10 +1367,11 @@ public final class QuotaController extends StateController { final long inQuotaTime = session.endTimeElapsed + stats.windowSizeMs; stats.inQuotaTimeElapsed = Math.max(stats.inQuotaTimeElapsed, inQuotaTime); } - if (i == loopStart - || (sessions.get(i + 1).startTimeElapsed - session.endTimeElapsed) - > mTimingSessionCoalescingDurationMs) { - // Coalesce sessions if they are very close to each other in time + // Coalesce sessions if they are very close to each other in time + boolean shouldCoalesce = lastSeenTimingSession != null + && lastSeenTimingSession.startTimeElapsed - session.endTimeElapsed + <= mTimingSessionCoalescingDurationMs; + if (!shouldCoalesce) { sessionCountInWindow++; if (sessionCountInWindow >= stats.sessionCountLimit) { @@ -1348,6 +1407,8 @@ public final class QuotaController extends StateController { // This session ended before the window. No point in going any further. break; } + + lastSeenTimingSession = session; } stats.expirationTimeElapsed = nowElapsed + emptyTimeMs; stats.sessionCountInWindow = sessionCountInWindow; @@ -1437,9 +1498,9 @@ public final class QuotaController extends StateController { private void saveTimingSession(final int userId, @NonNull final String packageName, @NonNull final TimingSession session, boolean isExpedited, long debitAdjustment) { synchronized (mLock) { - final SparseArrayMap> sessionMap = - isExpedited ? mEJTimingSessions : mTimingSessions; - List sessions = sessionMap.get(userId, packageName); + final SparseArrayMap> sessionMap = + isExpedited ? mEJTimingSessions : mTimingEvents; + List sessions = sessionMap.get(userId, packageName); if (sessions == null) { sessions = new ArrayList<>(); sessionMap.add(userId, packageName, sessions); @@ -1460,6 +1521,9 @@ public final class QuotaController extends StateController { private void grantRewardForInstantEvent( final int userId, @NonNull final String packageName, final long credit) { + if (credit == 0) { + return; + } synchronized (mLock) { final long nowElapsed = sElapsedRealtimeClock.millis(); final ShrinkableDebits quota = getEJDebitsLocked(userId, packageName); @@ -1489,13 +1553,14 @@ public final class QuotaController extends StateController { return changed; } - private final class EarliestEndTimeFunctor implements Consumer> { + private final class EarliestEndTimeFunctor implements Consumer> { public long earliestEndElapsed = Long.MAX_VALUE; @Override - public void accept(List sessions) { - if (sessions != null && sessions.size() > 0) { - earliestEndElapsed = Math.min(earliestEndElapsed, sessions.get(0).endTimeElapsed); + public void accept(List events) { + if (events != null && events.size() > 0) { + earliestEndElapsed = + Math.min(earliestEndElapsed, events.get(0).getEndTimeElapsed()); } } @@ -1521,7 +1586,7 @@ public final class QuotaController extends StateController { return; } mEarliestEndTimeFunctor.reset(); - mTimingSessions.forEach(mEarliestEndTimeFunctor); + mTimingEvents.forEach(mEarliestEndTimeFunctor); mEJTimingSessions.forEach(mEarliestEndTimeFunctor); final long earliestEndElapsed = mEarliestEndTimeFunctor.earliestEndElapsed; if (earliestEndElapsed == Long.MAX_VALUE) { @@ -1809,10 +1874,10 @@ public final class QuotaController extends StateController { inEJQuotaTimeElapsed = (nowElapsed - limitMs) + mEJLimitWindowSizeMs; } } - List timingSessions = mEJTimingSessions.get(userId, packageName); + List timingSessions = mEJTimingSessions.get(userId, packageName); if (timingSessions != null) { for (int i = timingSessions.size() - 1; i >= 0; --i) { - TimingSession ts = timingSessions.get(i); + TimingSession ts = (TimingSession) timingSessions.get(i); final long durationMs = ts.endTimeElapsed - ts.startTimeElapsed; sumMs += durationMs; if (sumMs >= limitMs) { @@ -1881,7 +1946,14 @@ public final class QuotaController extends StateController { } @VisibleForTesting - static final class TimingSession { + interface TimedEvent { + long getEndTimeElapsed(); + + void dump(IndentingPrintWriter pw); + } + + @VisibleForTesting + static final class TimingSession implements TimedEvent { // Start timestamp in elapsed realtime timebase. public final long startTimeElapsed; // End timestamp in elapsed realtime timebase. @@ -1903,6 +1975,11 @@ public final class QuotaController extends StateController { mHashCode = hashCode; } + @Override + public long getEndTimeElapsed() { + return endTimeElapsed; + } + @Override public String toString() { return "TimingSession{" + startTimeElapsed + "->" + endTimeElapsed + ", " + bgJobCount @@ -1926,6 +2003,7 @@ public final class QuotaController extends StateController { return mHashCode; } + @Override public void dump(IndentingPrintWriter pw) { pw.print(startTimeElapsed); pw.print(" -> "); @@ -1952,6 +2030,28 @@ public final class QuotaController extends StateController { } } + @VisibleForTesting + static final class QuotaBump implements TimedEvent { + // Event timestamp in elapsed realtime timebase. + public final long eventTimeElapsed; + + QuotaBump(long eventElapsed) { + this.eventTimeElapsed = eventElapsed; + } + + @Override + public long getEndTimeElapsed() { + return eventTimeElapsed; + } + + @Override + public void dump(IndentingPrintWriter pw) { + pw.print("Quota bump @ "); + pw.print(eventTimeElapsed); + pw.println(); + } + } + @VisibleForTesting static final class ShrinkableDebits { /** The amount of quota remaining. Can be negative if limit changes. */ @@ -2381,6 +2481,21 @@ public final class QuotaController extends StateController { updateStandbyBucket(userId, packageName, bucketIndex); }); } + + @Override + public void triggerTemporaryQuotaBump(String packageName, @UserIdInt int userId) { + synchronized (mLock) { + List events = mTimingEvents.get(userId, packageName); + if (events == null || events.size() == 0) { + // If the app hasn't run any jobs, there's no point giving it a quota bump. + return; + } + events.add(new QuotaBump(sElapsedRealtimeClock.millis())); + invalidateAllExecutionStatsLocked(userId, packageName); + } + // Update jobs out of band. + mHandler.obtainMessage(MSG_CHECK_PACKAGE, userId, 0, packageName).sendToTarget(); + } } @VisibleForTesting @@ -2476,7 +2591,7 @@ public final class QuotaController extends StateController { } } - private static final class TimingSessionTooOldPredicate implements Predicate { + private static final class TimedEventTooOldPredicate implements Predicate { private long mNowElapsed; private void updateNow() { @@ -2484,27 +2599,26 @@ public final class QuotaController extends StateController { } @Override - public boolean test(TimingSession ts) { - return ts.endTimeElapsed <= mNowElapsed - MAX_PERIOD_MS; + public boolean test(TimedEvent ts) { + return ts.getEndTimeElapsed() <= mNowElapsed - MAX_PERIOD_MS; } } - private final TimingSessionTooOldPredicate mTimingSessionTooOld = - new TimingSessionTooOldPredicate(); + private final TimedEventTooOldPredicate mTimedEventTooOld = new TimedEventTooOldPredicate(); - private final Consumer> mDeleteOldSessionsFunctor = sessions -> { - if (sessions != null) { + private final Consumer> mDeleteOldEventsFunctor = events -> { + if (events != null) { // Remove everything older than MAX_PERIOD_MS time ago. - sessions.removeIf(mTimingSessionTooOld); + events.removeIf(mTimedEventTooOld); } }; @VisibleForTesting void deleteObsoleteSessionsLocked() { - mTimingSessionTooOld.updateNow(); + mTimedEventTooOld.updateNow(); // Regular sessions - mTimingSessions.forEach(mDeleteOldSessionsFunctor); + mTimingEvents.forEach(mDeleteOldEventsFunctor); // EJ sessions for (int uIdx = 0; uIdx < mEJTimingSessions.numMaps(); ++uIdx) { @@ -2512,14 +2626,14 @@ public final class QuotaController extends StateController { for (int pIdx = 0; pIdx < mEJTimingSessions.numElementsForKey(userId); ++pIdx) { final String packageName = mEJTimingSessions.keyAt(uIdx, pIdx); final ShrinkableDebits debits = getEJDebitsLocked(userId, packageName); - final List sessions = mEJTimingSessions.get(userId, packageName); + final List sessions = mEJTimingSessions.get(userId, packageName); if (sessions == null) { continue; } while (sessions.size() > 0) { - final TimingSession ts = sessions.get(0); - if (mTimingSessionTooOld.test(ts)) { + final TimingSession ts = (TimingSession) sessions.get(0); + if (mTimedEventTooOld.test(ts)) { // Stale sessions may still be factored into tally. Remove them. final long duration = ts.endTimeElapsed - ts.startTimeElapsed; debits.transactLocked(-duration); @@ -2801,6 +2915,7 @@ public final class QuotaController extends StateController { mQcConstants.mRateLimitingConstantsUpdated = false; mQcConstants.mExecutionPeriodConstantsUpdated = false; mQcConstants.mEJLimitConstantsUpdated = false; + mQcConstants.mQuotaBumpConstantsUpdated = false; } @Override @@ -2828,6 +2943,7 @@ public final class QuotaController extends StateController { private boolean mRateLimitingConstantsUpdated = false; private boolean mExecutionPeriodConstantsUpdated = false; private boolean mEJLimitConstantsUpdated = false; + private boolean mQuotaBumpConstantsUpdated = false; /** Prefix to use with all constant keys in order to "sub-namespace" the keys. */ private static final String QC_CONSTANT_PREFIX = "qc_"; @@ -2975,6 +3091,21 @@ public final class QuotaController extends StateController { @VisibleForTesting static final String KEY_EJ_GRACE_PERIOD_TOP_APP_MS = QC_CONSTANT_PREFIX + "ej_grace_period_top_app_ms"; + @VisibleForTesting + static final String KEY_QUOTA_BUMP_ADDITIONAL_DURATION_MS = + QC_CONSTANT_PREFIX + "quota_bump_additional_duration_ms"; + @VisibleForTesting + static final String KEY_QUOTA_BUMP_ADDITIONAL_JOB_COUNT = + QC_CONSTANT_PREFIX + "quota_bump_additional_job_count"; + @VisibleForTesting + static final String KEY_QUOTA_BUMP_ADDITIONAL_SESSION_COUNT = + QC_CONSTANT_PREFIX + "quota_bump_additional_session_count"; + @VisibleForTesting + static final String KEY_QUOTA_BUMP_WINDOW_SIZE_MS = + QC_CONSTANT_PREFIX + "quota_bump_window_size_ms"; + @VisibleForTesting + static final String KEY_QUOTA_BUMP_LIMIT = + QC_CONSTANT_PREFIX + "quota_bump_limit"; private static final long DEFAULT_ALLOWED_TIME_PER_PERIOD_EXEMPTED_MS = 10 * 60 * 1000L; // 10 minutes @@ -3046,6 +3177,11 @@ public final class QuotaController extends StateController { private static final long DEFAULT_EJ_REWARD_NOTIFICATION_SEEN_MS = 0; private static final long DEFAULT_EJ_GRACE_PERIOD_TEMP_ALLOWLIST_MS = 3 * MINUTE_IN_MILLIS; private static final long DEFAULT_EJ_GRACE_PERIOD_TOP_APP_MS = 1 * MINUTE_IN_MILLIS; + private static final long DEFAULT_QUOTA_BUMP_ADDITIONAL_DURATION_MS = 1 * MINUTE_IN_MILLIS; + private static final int DEFAULT_QUOTA_BUMP_ADDITIONAL_JOB_COUNT = 2; + private static final int DEFAULT_QUOTA_BUMP_ADDITIONAL_SESSION_COUNT = 1; + private static final long DEFAULT_QUOTA_BUMP_WINDOW_SIZE_MS = 8 * HOUR_IN_MILLIS; + private static final int DEFAULT_QUOTA_BUMP_LIMIT = 8; /** * How much time each app in the exempted bucket will have to run jobs within their standby @@ -3347,6 +3483,33 @@ public final class QuotaController extends StateController { */ public long EJ_GRACE_PERIOD_TOP_APP_MS = DEFAULT_EJ_GRACE_PERIOD_TOP_APP_MS; + /** + * How much additional session duration to give an app for each accepted quota bump. + */ + public long QUOTA_BUMP_ADDITIONAL_DURATION_MS = DEFAULT_QUOTA_BUMP_ADDITIONAL_DURATION_MS; + + /** + * How many additional regular jobs to give an app for each accepted quota bump. + */ + public int QUOTA_BUMP_ADDITIONAL_JOB_COUNT = DEFAULT_QUOTA_BUMP_ADDITIONAL_JOB_COUNT; + + /** + * How many additional sessions to give an app for each accepted quota bump. + */ + public int QUOTA_BUMP_ADDITIONAL_SESSION_COUNT = + DEFAULT_QUOTA_BUMP_ADDITIONAL_SESSION_COUNT; + + /** + * The rolling window size within which to accept and apply quota bump events. + */ + public long QUOTA_BUMP_WINDOW_SIZE_MS = DEFAULT_QUOTA_BUMP_WINDOW_SIZE_MS; + + /** + * The maximum number of quota bumps to accept and apply within the + * {@link #QUOTA_BUMP_WINDOW_SIZE_MS window}. + */ + public int QUOTA_BUMP_LIMIT = DEFAULT_QUOTA_BUMP_LIMIT; + public void processConstantLocked(@NonNull DeviceConfig.Properties properties, @NonNull String key) { switch (key) { @@ -3383,6 +3546,14 @@ public final class QuotaController extends StateController { updateEJLimitConstantsLocked(); break; + case KEY_QUOTA_BUMP_ADDITIONAL_DURATION_MS: + case KEY_QUOTA_BUMP_ADDITIONAL_JOB_COUNT: + case KEY_QUOTA_BUMP_ADDITIONAL_SESSION_COUNT: + case KEY_QUOTA_BUMP_WINDOW_SIZE_MS: + case KEY_QUOTA_BUMP_LIMIT: + updateQuotaBumpConstantsLocked(); + break; + case KEY_MAX_JOB_COUNT_EXEMPTED: MAX_JOB_COUNT_EXEMPTED = properties.getInt(key, DEFAULT_MAX_JOB_COUNT_EXEMPTED); int newExemptedMaxJobCount = @@ -3881,6 +4052,65 @@ public final class QuotaController extends StateController { } } + private void updateQuotaBumpConstantsLocked() { + if (mQuotaBumpConstantsUpdated) { + return; + } + mQuotaBumpConstantsUpdated = true; + + // Query the values as an atomic set. + final DeviceConfig.Properties properties = DeviceConfig.getProperties( + DeviceConfig.NAMESPACE_JOB_SCHEDULER, + KEY_QUOTA_BUMP_ADDITIONAL_DURATION_MS, + KEY_QUOTA_BUMP_ADDITIONAL_JOB_COUNT, KEY_QUOTA_BUMP_ADDITIONAL_SESSION_COUNT, + KEY_QUOTA_BUMP_WINDOW_SIZE_MS, KEY_QUOTA_BUMP_LIMIT); + QUOTA_BUMP_ADDITIONAL_DURATION_MS = properties.getLong( + KEY_QUOTA_BUMP_ADDITIONAL_DURATION_MS, + DEFAULT_QUOTA_BUMP_ADDITIONAL_DURATION_MS); + QUOTA_BUMP_ADDITIONAL_JOB_COUNT = properties.getInt( + KEY_QUOTA_BUMP_ADDITIONAL_JOB_COUNT, DEFAULT_QUOTA_BUMP_ADDITIONAL_JOB_COUNT); + QUOTA_BUMP_ADDITIONAL_SESSION_COUNT = properties.getInt( + KEY_QUOTA_BUMP_ADDITIONAL_SESSION_COUNT, + DEFAULT_QUOTA_BUMP_ADDITIONAL_SESSION_COUNT); + QUOTA_BUMP_WINDOW_SIZE_MS = properties.getLong( + KEY_QUOTA_BUMP_WINDOW_SIZE_MS, DEFAULT_QUOTA_BUMP_WINDOW_SIZE_MS); + QUOTA_BUMP_LIMIT = properties.getInt( + KEY_QUOTA_BUMP_LIMIT, DEFAULT_QUOTA_BUMP_LIMIT); + + // The window must be in the range [1 hour, 24 hours]. + long newWindowSizeMs = Math.max(HOUR_IN_MILLIS, + Math.min(MAX_PERIOD_MS, QUOTA_BUMP_WINDOW_SIZE_MS)); + if (mQuotaBumpWindowSizeMs != newWindowSizeMs) { + mQuotaBumpWindowSizeMs = newWindowSizeMs; + mShouldReevaluateConstraints = true; + } + // The limit must be nonnegative. + int newLimit = Math.max(0, QUOTA_BUMP_LIMIT); + if (mQuotaBumpLimit != newLimit) { + mQuotaBumpLimit = newLimit; + mShouldReevaluateConstraints = true; + } + // The job count must be nonnegative. + int newJobAddition = Math.max(0, QUOTA_BUMP_ADDITIONAL_JOB_COUNT); + if (mQuotaBumpAdditionalJobCount != newJobAddition) { + mQuotaBumpAdditionalJobCount = newJobAddition; + mShouldReevaluateConstraints = true; + } + // The session count must be nonnegative. + int newSessionAddition = Math.max(0, QUOTA_BUMP_ADDITIONAL_SESSION_COUNT); + if (mQuotaBumpAdditionalSessionCount != newSessionAddition) { + mQuotaBumpAdditionalSessionCount = newSessionAddition; + mShouldReevaluateConstraints = true; + } + // The additional duration must be in the range [0, 10 minutes]. + long newAdditionalDuration = Math.max(0, + Math.min(10 * MINUTE_IN_MILLIS, QUOTA_BUMP_ADDITIONAL_DURATION_MS)); + if (mQuotaBumpAdditionalDurationMs != newAdditionalDuration) { + mQuotaBumpAdditionalDurationMs = newAdditionalDuration; + mShouldReevaluateConstraints = true; + } + } + private void dump(IndentingPrintWriter pw) { pw.println(); pw.println("QuotaController:"); @@ -3943,6 +4173,15 @@ public final class QuotaController extends StateController { EJ_GRACE_PERIOD_TEMP_ALLOWLIST_MS).println(); pw.print(KEY_EJ_GRACE_PERIOD_TOP_APP_MS, EJ_GRACE_PERIOD_TOP_APP_MS).println(); + pw.print(KEY_QUOTA_BUMP_ADDITIONAL_DURATION_MS, + QUOTA_BUMP_ADDITIONAL_DURATION_MS).println(); + pw.print(KEY_QUOTA_BUMP_ADDITIONAL_JOB_COUNT, + QUOTA_BUMP_ADDITIONAL_JOB_COUNT).println(); + pw.print(KEY_QUOTA_BUMP_ADDITIONAL_SESSION_COUNT, + QUOTA_BUMP_ADDITIONAL_SESSION_COUNT).println(); + pw.print(KEY_QUOTA_BUMP_WINDOW_SIZE_MS, QUOTA_BUMP_WINDOW_SIZE_MS).println(); + pw.print(KEY_QUOTA_BUMP_LIMIT, QUOTA_BUMP_LIMIT).println(); + pw.decreaseIndent(); } @@ -4103,7 +4342,7 @@ public final class QuotaController extends StateController { @VisibleForTesting @Nullable - List getEJTimingSessions(int userId, String packageName) { + List getEJTimingSessions(int userId, String packageName) { return mEJTimingSessions.get(userId, packageName); } @@ -4150,8 +4389,8 @@ public final class QuotaController extends StateController { @VisibleForTesting @Nullable - List getTimingSessions(int userId, String packageName) { - return mTimingSessions.get(userId, packageName); + List getTimingSessions(int userId, String packageName) { + return mTimingEvents.get(userId, packageName); } @VisibleForTesting @@ -4160,6 +4399,31 @@ public final class QuotaController extends StateController { return mQcConstants; } + @VisibleForTesting + long getQuotaBumpAdditionDurationMs() { + return mQuotaBumpAdditionalDurationMs; + } + + @VisibleForTesting + int getQuotaBumpAdditionJobCount() { + return mQuotaBumpAdditionalJobCount; + } + + @VisibleForTesting + int getQuotaBumpAdditionSessionCount() { + return mQuotaBumpAdditionalSessionCount; + } + + @VisibleForTesting + int getQuotaBumpLimit() { + return mQuotaBumpLimit; + } + + @VisibleForTesting + long getQuotaBumpWindowSizeMs() { + return mQuotaBumpWindowSizeMs; + } + //////////////////////////// DATA DUMP ////////////////////////////// @NeverCompile // Avoid size overhead of debugging code. @@ -4250,14 +4514,14 @@ public final class QuotaController extends StateController { final String pkgName = mPkgTimers.keyAt(u, p); mPkgTimers.valueAt(u, p).dump(pw, predicate); pw.println(); - List sessions = mTimingSessions.get(userId, pkgName); - if (sessions != null) { + List events = mTimingEvents.get(userId, pkgName); + if (events != null) { pw.increaseIndent(); - pw.println("Saved sessions:"); + pw.println("Saved events:"); pw.increaseIndent(); - for (int j = sessions.size() - 1; j >= 0; j--) { - TimingSession session = sessions.get(j); - session.dump(pw); + for (int j = events.size() - 1; j >= 0; j--) { + TimedEvent event = events.get(j); + event.dump(pw); } pw.decreaseIndent(); pw.decreaseIndent(); @@ -4273,13 +4537,13 @@ public final class QuotaController extends StateController { final String pkgName = mEJPkgTimers.keyAt(u, p); mEJPkgTimers.valueAt(u, p).dump(pw, predicate); pw.println(); - List sessions = mEJTimingSessions.get(userId, pkgName); + List sessions = mEJTimingSessions.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); + TimedEvent session = sessions.get(j); session.dump(pw); } pw.decreaseIndent(); @@ -4398,10 +4662,14 @@ public final class QuotaController extends StateController { predicate); } - List sessions = mTimingSessions.get(userId, pkgName); - if (sessions != null) { - for (int j = sessions.size() - 1; j >= 0; j--) { - TimingSession session = sessions.get(j); + List events = mTimingEvents.get(userId, pkgName); + if (events != null) { + for (int j = events.size() - 1; j >= 0; j--) { + TimedEvent event = events.get(j); + if (!(event instanceof TimingSession)) { + continue; + } + TimingSession session = (TimingSession) event; session.dump(proto, StateControllerProto.QuotaController.PackageStats.SAVED_SESSIONS); } diff --git a/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java b/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java index e57724909306a..1c4ec857538fb 100644 --- a/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java +++ b/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java @@ -289,6 +289,7 @@ public class AppStandbyController static final int MSG_INFORM_LISTENERS = 3; static final int MSG_FORCE_IDLE_STATE = 4; static final int MSG_CHECK_IDLE_STATES = 5; + static final int MSG_TRIGGER_LISTENER_QUOTA_BUMP = 7; static final int MSG_REPORT_CONTENT_PROVIDER_USAGE = 8; static final int MSG_PAROLE_STATE_CHANGED = 9; static final int MSG_ONE_TIME_CHECK_IDLE_STATES = 10; @@ -318,6 +319,12 @@ public class AppStandbyController /** The standby bucket that an app will be promoted on a notification-seen event */ int mNotificationSeenPromotedBucket = ConstantsObserver.DEFAULT_NOTIFICATION_SEEN_PROMOTED_BUCKET; + /** + * If true, tell each {@link AppIdleStateChangeListener} to give quota bump for each + * notification seen event. + */ + private boolean mTriggerQuotaBumpOnNotificationSeen = + ConstantsObserver.DEFAULT_TRIGGER_QUOTA_BUMP_ON_NOTIFICATION_SEEN; /** Minimum time a system update event should keep the buckets elevated. */ long mSystemUpdateUsageTimeoutMillis = ConstantsObserver.DEFAULT_SYSTEM_UPDATE_TIMEOUT; /** Maximum time to wait for a prediction before using simple timeouts to downgrade buckets. */ @@ -767,6 +774,17 @@ public class AppStandbyController appUsage.bucketingReason, false); } + /** Trigger a quota bump in the listeners. */ + private void triggerListenerQuotaBump(String packageName, int userId) { + if (!mAppIdleEnabled) return; + + synchronized (mPackageAccessListeners) { + for (AppIdleStateChangeListener listener : mPackageAccessListeners) { + listener.triggerTemporaryQuotaBump(packageName, userId); + } + } + } + @VisibleForTesting void setChargingState(boolean isCharging) { if (mIsCharging != isCharging) { @@ -1080,6 +1098,10 @@ public class AppStandbyController final int subReason = usageEventToSubReason(eventType); final int reason = REASON_MAIN_USAGE | subReason; if (eventType == UsageEvents.Event.NOTIFICATION_SEEN) { + if (mTriggerQuotaBumpOnNotificationSeen) { + mHandler.obtainMessage(MSG_TRIGGER_LISTENER_QUOTA_BUMP, userId, -1, pkg) + .sendToTarget(); + } // Notification-seen elevates to a higher bucket (depending on // {@link ConstantsObserver#KEY_NOTIFICATION_SEEN_PROMOTED_BUCKET}) but doesn't // change usage time. @@ -2201,6 +2223,9 @@ public class AppStandbyController pw.print(" mNotificationSeenPromotedBucket="); pw.print(standbyBucketToString(mNotificationSeenPromotedBucket)); pw.println(); + pw.print(" mTriggerQuotaBumpOnNotificationSeen="); + pw.print(mTriggerQuotaBumpOnNotificationSeen); + pw.println(); pw.print(" mSlicePinnedTimeoutMillis="); TimeUtils.formatDuration(mSlicePinnedTimeoutMillis, pw); pw.println(); @@ -2597,6 +2622,10 @@ public class AppStandbyController checkIdleStates(UserHandle.USER_ALL); break; + case MSG_TRIGGER_LISTENER_QUOTA_BUMP: + triggerListenerQuotaBump((String) msg.obj, msg.arg1); + break; + case MSG_REPORT_CONTENT_PROVIDER_USAGE: ContentProviderUsageRecord record = (ContentProviderUsageRecord) msg.obj; reportContentProviderUsage(record.name, record.packageName, record.userId); @@ -2683,6 +2712,8 @@ public class AppStandbyController "notification_seen_duration"; private static final String KEY_NOTIFICATION_SEEN_PROMOTED_BUCKET = "notification_seen_promoted_bucket"; + private static final String KEY_TRIGGER_QUOTA_BUMP_ON_NOTIFICATION_SEEN = + "trigger_quota_bump_on_notification_seen"; private static final String KEY_SLICE_PINNED_HOLD_DURATION = "slice_pinned_duration"; private static final String KEY_SYSTEM_UPDATE_HOLD_DURATION = @@ -2742,6 +2773,7 @@ public class AppStandbyController COMPRESS_TIME ? 12 * ONE_MINUTE : 12 * ONE_HOUR; public static final int DEFAULT_NOTIFICATION_SEEN_PROMOTED_BUCKET = STANDBY_BUCKET_WORKING_SET; + public static final boolean DEFAULT_TRIGGER_QUOTA_BUMP_ON_NOTIFICATION_SEEN = false; public static final long DEFAULT_SYSTEM_UPDATE_TIMEOUT = COMPRESS_TIME ? 2 * ONE_MINUTE : 2 * ONE_HOUR; public static final long DEFAULT_SYSTEM_INTERACTION_TIMEOUT = @@ -2842,6 +2874,11 @@ public class AppStandbyController KEY_NOTIFICATION_SEEN_PROMOTED_BUCKET, DEFAULT_NOTIFICATION_SEEN_PROMOTED_BUCKET); break; + case KEY_TRIGGER_QUOTA_BUMP_ON_NOTIFICATION_SEEN: + mTriggerQuotaBumpOnNotificationSeen = properties.getBoolean( + KEY_TRIGGER_QUOTA_BUMP_ON_NOTIFICATION_SEEN, + DEFAULT_TRIGGER_QUOTA_BUMP_ON_NOTIFICATION_SEEN); + break; case KEY_SLICE_PINNED_HOLD_DURATION: mSlicePinnedTimeoutMillis = properties.getLong( KEY_SLICE_PINNED_HOLD_DURATION, 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 43ba39adcb85d..29a3eb1d7e76e 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 @@ -90,7 +90,9 @@ import com.android.server.job.JobSchedulerService; import com.android.server.job.JobStore; import com.android.server.job.controllers.QuotaController.ExecutionStats; import com.android.server.job.controllers.QuotaController.QcConstants; +import com.android.server.job.controllers.QuotaController.QuotaBump; import com.android.server.job.controllers.QuotaController.ShrinkableDebits; +import com.android.server.job.controllers.QuotaController.TimedEvent; import com.android.server.job.controllers.QuotaController.TimingSession; import com.android.server.usage.AppStandbyInternal; @@ -128,6 +130,7 @@ public class QuotaControllerTest { private QuotaController.QcConstants mQcConstants; private JobSchedulerService.Constants mConstants = new JobSchedulerService.Constants(); private int mSourceUid; + private AppStandbyInternal.AppIdleStateChangeListener mAppIdleStateChangeListener; private PowerAllowlistInternal.TempAllowlistChangeListener mTempAllowlistListener; private IUidObserver mUidObserver; private UsageStatsManagerInternal.UsageEventListener mUsageEventListener; @@ -178,7 +181,8 @@ public class QuotaControllerTest { when(mContext.getSystemService(AlarmManager.class)).thenReturn(mAlarmManager); doReturn(mActivityMangerInternal) .when(() -> LocalServices.getService(ActivityManagerInternal.class)); - doReturn(mock(AppStandbyInternal.class)) + final AppStandbyInternal appStandbyInternal = mock(AppStandbyInternal.class); + doReturn(appStandbyInternal) .when(() -> LocalServices.getService(AppStandbyInternal.class)); doReturn(mock(BatteryManagerInternal.class)) .when(() -> LocalServices.getService(BatteryManagerInternal.class)); @@ -222,6 +226,8 @@ public class QuotaControllerTest { // Initialize real objects. // Capture the listeners. + ArgumentCaptor aiscListenerCaptor = + ArgumentCaptor.forClass(AppStandbyInternal.AppIdleStateChangeListener.class); ArgumentCaptor uidObserverCaptor = ArgumentCaptor.forClass(IUidObserver.class); ArgumentCaptor taChangeCaptor = @@ -231,6 +237,8 @@ public class QuotaControllerTest { mQuotaController = new QuotaController(mJobSchedulerService, mock(BackgroundJobsController.class), mock(ConnectivityController.class)); + verify(appStandbyInternal).addListener(aiscListenerCaptor.capture()); + mAppIdleStateChangeListener = aiscListenerCaptor.getValue(); verify(mPowerAllowlistInternal) .registerTempAllowlistChangeListener(taChangeCaptor.capture()); mTempAllowlistListener = taChangeCaptor.getValue(); @@ -457,26 +465,31 @@ public class QuotaControllerTest { now - 10 * MINUTE_IN_MILLIS, 9 * MINUTE_IN_MILLIS, 3); TimingSession two = createTimingSession( now - (70 * MINUTE_IN_MILLIS), 9 * MINUTE_IN_MILLIS, 1); + QuotaBump bump1 = new QuotaBump(now - 2 * HOUR_IN_MILLIS); TimingSession thr = createTimingSession( now - (3 * HOUR_IN_MILLIS + 10 * MINUTE_IN_MILLIS), 9 * MINUTE_IN_MILLIS, 1); // Overlaps 24 hour boundary. TimingSession fou = createTimingSession( now - (24 * HOUR_IN_MILLIS + 2 * MINUTE_IN_MILLIS), 7 * MINUTE_IN_MILLIS, 1); // Way past the 24 hour boundary. + QuotaBump bump2 = new QuotaBump(now - 24 * HOUR_IN_MILLIS - 5 * MINUTE_IN_MILLIS); TimingSession fiv = createTimingSession( now - (25 * HOUR_IN_MILLIS), 5 * MINUTE_IN_MILLIS, 4); - List expectedRegular = new ArrayList<>(); - List expectedEJ = new ArrayList<>(); + List expectedRegular = new ArrayList<>(); + List expectedEJ = new ArrayList<>(); // Added in correct (chronological) order. expectedRegular.add(fou); expectedRegular.add(thr); + expectedRegular.add(bump1); expectedRegular.add(two); expectedRegular.add(one); expectedEJ.add(fou); expectedEJ.add(one); mQuotaController.saveTimingSession(0, "com.android.test", fiv, false); + mQuotaController.getTimingSessions(0, "com.android.test").add(bump2); mQuotaController.saveTimingSession(0, "com.android.test", fou, false); mQuotaController.saveTimingSession(0, "com.android.test", thr, false); + mQuotaController.getTimingSessions(0, "com.android.test").add(bump1); mQuotaController.saveTimingSession(0, "com.android.test", two, false); mQuotaController.saveTimingSession(0, "com.android.test", one, false); mQuotaController.saveTimingSession(0, "com.android.test", fiv, true); @@ -1773,6 +1786,129 @@ public class QuotaControllerTest { } } + /** + * Test getTimeUntilQuotaConsumedLocked when the determination is based within the bucket + * window and there are valid QuotaBumps in the history. + */ + @Test + public void testGetTimeUntilQuotaConsumedLocked_QuotaBump() { + setDeviceConfigLong(QcConstants.KEY_QUOTA_BUMP_ADDITIONAL_DURATION_MS, MINUTE_IN_MILLIS); + setDeviceConfigLong(QcConstants.KEY_QUOTA_BUMP_WINDOW_SIZE_MS, 8 * HOUR_IN_MILLIS); + + final long now = JobSchedulerService.sElapsedRealtimeClock.millis(); + // 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), false); + mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE) + .add(new QuotaBump(now - 16 * HOUR_IN_MILLIS)); + mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE) + .add(new QuotaBump(now - 12 * HOUR_IN_MILLIS)); + mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE) + .add(new QuotaBump(now - 8 * HOUR_IN_MILLIS)); + // Far away from FREQUENT boundary. + mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE, + createTimingSession(now - (7 * HOUR_IN_MILLIS), 3 * MINUTE_IN_MILLIS, 5), false); + // Overlap WORKING_SET boundary. + mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE) + .add(new QuotaBump(now - 2 * HOUR_IN_MILLIS)); + mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE, + createTimingSession(now - (2 * HOUR_IN_MILLIS + MINUTE_IN_MILLIS), + 3 * MINUTE_IN_MILLIS, 5), false); + mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE) + .add(new QuotaBump(now - 15 * MINUTE_IN_MILLIS)); + // Close to ACTIVE boundary. + mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE, + createTimingSession(now - (9 * MINUTE_IN_MILLIS), 3 * MINUTE_IN_MILLIS, 5), false); + + setStandbyBucket(RARE_INDEX); + synchronized (mQuotaController.mLock) { + assertEquals(3 * MINUTE_IN_MILLIS + 30 * SECOND_IN_MILLIS, + mQuotaController.getRemainingExecutionTimeLocked( + SOURCE_USER_ID, SOURCE_PACKAGE)); + assertEquals(4 * MINUTE_IN_MILLIS, + mQuotaController.getTimeUntilQuotaConsumedLocked( + SOURCE_USER_ID, SOURCE_PACKAGE)); + } + + setStandbyBucket(FREQUENT_INDEX); + synchronized (mQuotaController.mLock) { + assertEquals(4 * MINUTE_IN_MILLIS, + mQuotaController.getRemainingExecutionTimeLocked( + SOURCE_USER_ID, SOURCE_PACKAGE)); + assertEquals(4 * MINUTE_IN_MILLIS, + mQuotaController.getTimeUntilQuotaConsumedLocked( + SOURCE_USER_ID, SOURCE_PACKAGE)); + } + + setStandbyBucket(WORKING_INDEX); + synchronized (mQuotaController.mLock) { + assertEquals(8 * MINUTE_IN_MILLIS, + mQuotaController.getRemainingExecutionTimeLocked( + SOURCE_USER_ID, SOURCE_PACKAGE)); + assertEquals(10 * MINUTE_IN_MILLIS, + mQuotaController.getTimeUntilQuotaConsumedLocked( + SOURCE_USER_ID, SOURCE_PACKAGE)); + } + + // ACTIVE window = allowed time, so jobs can essentially run non-stop until they reach the + // max execution time. + setStandbyBucket(ACTIVE_INDEX); + synchronized (mQuotaController.mLock) { + assertEquals(10 * MINUTE_IN_MILLIS, + mQuotaController.getRemainingExecutionTimeLocked( + SOURCE_USER_ID, SOURCE_PACKAGE)); + assertEquals(mQcConstants.MAX_EXECUTION_TIME_MS - 9 * MINUTE_IN_MILLIS, + mQuotaController.getTimeUntilQuotaConsumedLocked( + SOURCE_USER_ID, SOURCE_PACKAGE)); + } + } + + /** + * Test getTimeUntilQuotaConsumedLocked when there are valid QuotaBumps in recent history that + * provide enough additional quota to bridge gaps between sessions. + */ + @Test + public void testGetTimeUntilQuotaConsumedLocked_QuotaBump_CrucialBumps() { + setDeviceConfigLong(QcConstants.KEY_QUOTA_BUMP_ADDITIONAL_DURATION_MS, MINUTE_IN_MILLIS); + setDeviceConfigLong(QcConstants.KEY_QUOTA_BUMP_WINDOW_SIZE_MS, 8 * HOUR_IN_MILLIS); + + final long now = JobSchedulerService.sElapsedRealtimeClock.millis(); + mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE, + createTimingSession(now - (25 * HOUR_IN_MILLIS), + 30 * MINUTE_IN_MILLIS, 25), false); + mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE) + .add(new QuotaBump(now - 16 * HOUR_IN_MILLIS)); + mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE) + .add(new QuotaBump(now - 12 * HOUR_IN_MILLIS)); + mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE) + .add(new QuotaBump(now - 8 * HOUR_IN_MILLIS)); + // Without the valid quota bumps, the app would only 3 minutes until the quota was consumed. + // The quota bumps provide enough quota to bridge the gap between the two earliest sessions. + mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE, + createTimingSession(now - (8 * HOUR_IN_MILLIS), 3 * MINUTE_IN_MILLIS, 1), false); + mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE, + createTimingSession(now - (8 * HOUR_IN_MILLIS - 5 * MINUTE_IN_MILLIS), + 2 * MINUTE_IN_MILLIS, 5), false); + mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE, + createTimingSession(now - (2 * HOUR_IN_MILLIS + MINUTE_IN_MILLIS), + 3 * MINUTE_IN_MILLIS, 1), false); + mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE) + .add(new QuotaBump(now - 15 * MINUTE_IN_MILLIS)); + mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE, + createTimingSession(now - (9 * MINUTE_IN_MILLIS), 2 * MINUTE_IN_MILLIS, 1), false); + + setStandbyBucket(FREQUENT_INDEX); + synchronized (mQuotaController.mLock) { + assertEquals(2 * MINUTE_IN_MILLIS, + mQuotaController.getRemainingExecutionTimeLocked( + SOURCE_USER_ID, SOURCE_PACKAGE)); + assertEquals(7 * MINUTE_IN_MILLIS, + mQuotaController.getTimeUntilQuotaConsumedLocked( + SOURCE_USER_ID, SOURCE_PACKAGE)); + } + } + @Test public void testIsWithinQuotaLocked_NeverApp() { synchronized (mQuotaController.mLock) { @@ -2026,6 +2162,271 @@ public class QuotaControllerTest { } } + @Test + public void testIsWithinQuotaLocked_WithQuotaBump_Duration() { + setDischarging(); + int standbyBucket = WORKING_INDEX; + setStandbyBucket(standbyBucket); + setDeviceConfigLong(QcConstants.KEY_ALLOWED_TIME_PER_PERIOD_WORKING_MS, + 5 * MINUTE_IN_MILLIS); + setDeviceConfigInt(QcConstants.KEY_MAX_JOB_COUNT_WORKING, 10); + setDeviceConfigLong(QcConstants.KEY_QUOTA_BUMP_ADDITIONAL_DURATION_MS, MINUTE_IN_MILLIS); + setDeviceConfigInt(QcConstants.KEY_QUOTA_BUMP_ADDITIONAL_JOB_COUNT, 0); + setDeviceConfigInt(QcConstants.KEY_QUOTA_BUMP_ADDITIONAL_SESSION_COUNT, 0); + setDeviceConfigLong(QcConstants.KEY_QUOTA_BUMP_WINDOW_SIZE_MS, 8 * HOUR_IN_MILLIS); + setDeviceConfigInt(QcConstants.KEY_QUOTA_BUMP_LIMIT, 5); + + long now = JobSchedulerService.sElapsedRealtimeClock.millis(); + mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE, + createTimingSession( + now - (HOUR_IN_MILLIS - 2 * MINUTE_IN_MILLIS), 5 * MINUTE_IN_MILLIS, 1), + false); + final ExecutionStats stats; + synchronized (mQuotaController.mLock) { + stats = mQuotaController.getExecutionStatsLocked( + SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket); + mQuotaController.incrementJobCountLocked(SOURCE_USER_ID, SOURCE_PACKAGE, 1); + assertFalse(mQuotaController + .isWithinQuotaLocked(SOURCE_USER_ID, SOURCE_PACKAGE, WORKING_INDEX)); + assertEquals(5 * MINUTE_IN_MILLIS, stats.allowedTimePerPeriodMs); + } + mAppIdleStateChangeListener.triggerTemporaryQuotaBump(SOURCE_PACKAGE, SOURCE_USER_ID); + synchronized (mQuotaController.mLock) { + assertTrue(mQuotaController + .isWithinQuotaLocked(SOURCE_USER_ID, SOURCE_PACKAGE, WORKING_INDEX)); + assertEquals(6 * MINUTE_IN_MILLIS, stats.allowedTimePerPeriodMs); + } + + advanceElapsedClock(HOUR_IN_MILLIS); + + synchronized (mQuotaController.mLock) { + assertTrue(mQuotaController + .isWithinQuotaLocked(SOURCE_USER_ID, SOURCE_PACKAGE, WORKING_INDEX)); + assertEquals(6 * MINUTE_IN_MILLIS, stats.allowedTimePerPeriodMs); + } + + // Emulate a quota bump while some jobs are executing + JobStatus job1 = createJobStatus("testIsWithinQuotaLocked_WithQuotaBump_Duration", 1); + JobStatus job2 = createJobStatus("testIsWithinQuotaLocked_WithQuotaBump_Duration", 2); + + synchronized (mQuotaController.mLock) { + mQuotaController.maybeStartTrackingJobLocked(job1, null); + mQuotaController.prepareForExecutionLocked(job1); + } + + advanceElapsedClock(MINUTE_IN_MILLIS); + mAppIdleStateChangeListener.triggerTemporaryQuotaBump(SOURCE_PACKAGE, SOURCE_USER_ID); + synchronized (mQuotaController.mLock) { + assertTrue(mQuotaController + .isWithinQuotaLocked(SOURCE_USER_ID, SOURCE_PACKAGE, WORKING_INDEX)); + assertEquals(7 * MINUTE_IN_MILLIS, stats.allowedTimePerPeriodMs); + mQuotaController.maybeStartTrackingJobLocked(job2, null); + mQuotaController.prepareForExecutionLocked(job2); + } + + advanceElapsedClock(MINUTE_IN_MILLIS); + synchronized (mQuotaController.mLock) { + mQuotaController.maybeStopTrackingJobLocked(job1, null, false); + mQuotaController.maybeStopTrackingJobLocked(job2, null, false); + assertFalse(mQuotaController + .isWithinQuotaLocked(SOURCE_USER_ID, SOURCE_PACKAGE, WORKING_INDEX)); + assertEquals(7 * MINUTE_IN_MILLIS, stats.allowedTimePerPeriodMs); + } + + // Phase out the first session + advanceElapsedClock(5 * MINUTE_IN_MILLIS); + synchronized (mQuotaController.mLock) { + assertTrue(mQuotaController + .isWithinQuotaLocked(SOURCE_USER_ID, SOURCE_PACKAGE, WORKING_INDEX)); + assertEquals(7 * MINUTE_IN_MILLIS, stats.allowedTimePerPeriodMs); + } + + // Phase out the first quota bump + advanceElapsedClock(7 * HOUR_IN_MILLIS); + synchronized (mQuotaController.mLock) { + assertTrue(mQuotaController + .isWithinQuotaLocked(SOURCE_USER_ID, SOURCE_PACKAGE, WORKING_INDEX)); + assertEquals(6 * MINUTE_IN_MILLIS, stats.allowedTimePerPeriodMs); + } + } + + @Test + public void testIsWithinQuotaLocked_WithQuotaBump_JobCount() { + setDischarging(); + int standbyBucket = WORKING_INDEX; + setStandbyBucket(standbyBucket); + setDeviceConfigLong(QcConstants.KEY_ALLOWED_TIME_PER_PERIOD_WORKING_MS, + 20 * MINUTE_IN_MILLIS); + setDeviceConfigInt(QcConstants.KEY_MAX_JOB_COUNT_WORKING, 10); + setDeviceConfigLong(QcConstants.KEY_QUOTA_BUMP_ADDITIONAL_DURATION_MS, 0); + setDeviceConfigInt(QcConstants.KEY_QUOTA_BUMP_ADDITIONAL_JOB_COUNT, 1); + setDeviceConfigInt(QcConstants.KEY_QUOTA_BUMP_ADDITIONAL_SESSION_COUNT, 0); + setDeviceConfigLong(QcConstants.KEY_QUOTA_BUMP_WINDOW_SIZE_MS, 8 * HOUR_IN_MILLIS); + setDeviceConfigInt(QcConstants.KEY_QUOTA_BUMP_LIMIT, 5); + + long now = JobSchedulerService.sElapsedRealtimeClock.millis(); + mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE, + createTimingSession(now - (HOUR_IN_MILLIS), 5 * MINUTE_IN_MILLIS, 10), false); + final ExecutionStats stats; + synchronized (mQuotaController.mLock) { + stats = mQuotaController.getExecutionStatsLocked( + SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket); + mQuotaController.incrementJobCountLocked(SOURCE_USER_ID, SOURCE_PACKAGE, 10); + assertFalse(mQuotaController + .isWithinQuotaLocked(SOURCE_USER_ID, SOURCE_PACKAGE, WORKING_INDEX)); + assertEquals(10, stats.jobCountLimit); + } + mAppIdleStateChangeListener.triggerTemporaryQuotaBump(SOURCE_PACKAGE, SOURCE_USER_ID); + synchronized (mQuotaController.mLock) { + assertTrue(mQuotaController + .isWithinQuotaLocked(SOURCE_USER_ID, SOURCE_PACKAGE, WORKING_INDEX)); + assertEquals(11, stats.jobCountLimit); + } + + advanceElapsedClock(HOUR_IN_MILLIS); + + synchronized (mQuotaController.mLock) { + assertTrue(mQuotaController + .isWithinQuotaLocked(SOURCE_USER_ID, SOURCE_PACKAGE, WORKING_INDEX)); + assertEquals(11, stats.jobCountLimit); + } + + // Emulate a quota bump while some jobs are executing + JobStatus job1 = createJobStatus("testIsWithinQuotaLocked_WithQuotaBump_JobCount", 1); + JobStatus job2 = createJobStatus("testIsWithinQuotaLocked_WithQuotaBump_JobCount", 2); + + synchronized (mQuotaController.mLock) { + mQuotaController.maybeStartTrackingJobLocked(job1, null); + mQuotaController.prepareForExecutionLocked(job1); + } + + advanceElapsedClock(MINUTE_IN_MILLIS); + mAppIdleStateChangeListener.triggerTemporaryQuotaBump(SOURCE_PACKAGE, SOURCE_USER_ID); + synchronized (mQuotaController.mLock) { + assertTrue(mQuotaController + .isWithinQuotaLocked(SOURCE_USER_ID, SOURCE_PACKAGE, WORKING_INDEX)); + assertEquals(12, stats.jobCountLimit); + mQuotaController.maybeStartTrackingJobLocked(job2, null); + mQuotaController.prepareForExecutionLocked(job2); + } + + advanceElapsedClock(MINUTE_IN_MILLIS); + synchronized (mQuotaController.mLock) { + mQuotaController.maybeStopTrackingJobLocked(job1, null, false); + mQuotaController.maybeStopTrackingJobLocked(job2, null, false); + assertFalse(mQuotaController + .isWithinQuotaLocked(SOURCE_USER_ID, SOURCE_PACKAGE, WORKING_INDEX)); + assertEquals(12, stats.jobCountLimit); + } + + // Phase out the first session + advanceElapsedClock(3 * MINUTE_IN_MILLIS); + synchronized (mQuotaController.mLock) { + assertTrue(mQuotaController + .isWithinQuotaLocked(SOURCE_USER_ID, SOURCE_PACKAGE, WORKING_INDEX)); + assertEquals(12, stats.jobCountLimit); + } + + // Phase out the first quota bump + advanceElapsedClock(7 * HOUR_IN_MILLIS); + synchronized (mQuotaController.mLock) { + assertTrue(mQuotaController + .isWithinQuotaLocked(SOURCE_USER_ID, SOURCE_PACKAGE, WORKING_INDEX)); + assertEquals(11, stats.jobCountLimit); + } + } + + @Test + public void testIsWithinQuotaLocked_WithQuotaBump_SessionCount() { + setDischarging(); + int standbyBucket = WORKING_INDEX; + setStandbyBucket(standbyBucket); + setDeviceConfigLong(QcConstants.KEY_ALLOWED_TIME_PER_PERIOD_WORKING_MS, + 20 * MINUTE_IN_MILLIS); + setDeviceConfigInt(QcConstants.KEY_MAX_SESSION_COUNT_WORKING, 2); + setDeviceConfigLong(QcConstants.KEY_QUOTA_BUMP_ADDITIONAL_DURATION_MS, 0); + setDeviceConfigInt(QcConstants.KEY_QUOTA_BUMP_ADDITIONAL_JOB_COUNT, 0); + setDeviceConfigInt(QcConstants.KEY_QUOTA_BUMP_ADDITIONAL_SESSION_COUNT, 1); + setDeviceConfigLong(QcConstants.KEY_QUOTA_BUMP_WINDOW_SIZE_MS, 8 * HOUR_IN_MILLIS); + setDeviceConfigInt(QcConstants.KEY_QUOTA_BUMP_LIMIT, 5); + + long now = JobSchedulerService.sElapsedRealtimeClock.millis(); + mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE, + createTimingSession(now - (HOUR_IN_MILLIS), 5 * MINUTE_IN_MILLIS, 1), false); + mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE, + createTimingSession(now - (30 * MINUTE_IN_MILLIS), MINUTE_IN_MILLIS, 1), false); + final ExecutionStats stats; + synchronized (mQuotaController.mLock) { + stats = mQuotaController.getExecutionStatsLocked( + SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket); + assertFalse(mQuotaController + .isWithinQuotaLocked(SOURCE_USER_ID, SOURCE_PACKAGE, WORKING_INDEX)); + assertEquals(2, stats.sessionCountLimit); + } + mAppIdleStateChangeListener.triggerTemporaryQuotaBump(SOURCE_PACKAGE, SOURCE_USER_ID); + synchronized (mQuotaController.mLock) { + assertTrue(mQuotaController + .isWithinQuotaLocked(SOURCE_USER_ID, SOURCE_PACKAGE, WORKING_INDEX)); + assertEquals(3, stats.sessionCountLimit); + } + + advanceElapsedClock(HOUR_IN_MILLIS); + + synchronized (mQuotaController.mLock) { + assertTrue(mQuotaController + .isWithinQuotaLocked(SOURCE_USER_ID, SOURCE_PACKAGE, WORKING_INDEX)); + assertEquals(3, stats.sessionCountLimit); + } + + // Emulate a quota bump while some jobs are executing + JobStatus job1 = createJobStatus("testIsWithinQuotaLocked_WithQuotaBump_JobCount", 1); + JobStatus job2 = createJobStatus("testIsWithinQuotaLocked_WithQuotaBump_JobCount", 2); + + synchronized (mQuotaController.mLock) { + mQuotaController.maybeStartTrackingJobLocked(job1, null); + mQuotaController.prepareForExecutionLocked(job1); + } + + advanceElapsedClock(MINUTE_IN_MILLIS); + mAppIdleStateChangeListener.triggerTemporaryQuotaBump(SOURCE_PACKAGE, SOURCE_USER_ID); + synchronized (mQuotaController.mLock) { + mQuotaController.maybeStopTrackingJobLocked(job1, null, false); + assertTrue(mQuotaController + .isWithinQuotaLocked(SOURCE_USER_ID, SOURCE_PACKAGE, WORKING_INDEX)); + assertEquals(4, stats.sessionCountLimit); + } + + advanceElapsedClock(MINUTE_IN_MILLIS); + synchronized (mQuotaController.mLock) { + mQuotaController.maybeStartTrackingJobLocked(job2, null); + mQuotaController.prepareForExecutionLocked(job2); + } + + advanceElapsedClock(MINUTE_IN_MILLIS); + synchronized (mQuotaController.mLock) { + mQuotaController.maybeStopTrackingJobLocked(job2, null, false); + assertFalse(mQuotaController + .isWithinQuotaLocked(SOURCE_USER_ID, SOURCE_PACKAGE, WORKING_INDEX)); + assertEquals(4, stats.sessionCountLimit); + } + + // Phase out the first session + advanceElapsedClock(2 * MINUTE_IN_MILLIS); + synchronized (mQuotaController.mLock) { + assertTrue(mQuotaController + .isWithinQuotaLocked(SOURCE_USER_ID, SOURCE_PACKAGE, WORKING_INDEX)); + assertEquals(4, stats.sessionCountLimit); + } + + // Phase out the first quota bump + advanceElapsedClock(7 * HOUR_IN_MILLIS); + synchronized (mQuotaController.mLock) { + assertTrue(mQuotaController + .isWithinQuotaLocked(SOURCE_USER_ID, SOURCE_PACKAGE, WORKING_INDEX)); + assertEquals(3, stats.sessionCountLimit); + } + } + @Test public void testIsWithinEJQuotaLocked_NeverApp() { JobStatus js = createExpeditedJobStatus("testIsWithinEJQuotaLocked_NeverApp", 1); @@ -3012,6 +3413,12 @@ public class QuotaControllerTest { setDeviceConfigLong(QcConstants.KEY_EJ_GRACE_PERIOD_TEMP_ALLOWLIST_MS, 84 * SECOND_IN_MILLIS); setDeviceConfigLong(QcConstants.KEY_EJ_GRACE_PERIOD_TOP_APP_MS, 83 * SECOND_IN_MILLIS); + setDeviceConfigLong(QcConstants.KEY_QUOTA_BUMP_ADDITIONAL_DURATION_MS, + 93 * SECOND_IN_MILLIS); + setDeviceConfigInt(QcConstants.KEY_QUOTA_BUMP_ADDITIONAL_JOB_COUNT, 92); + setDeviceConfigInt(QcConstants.KEY_QUOTA_BUMP_ADDITIONAL_SESSION_COUNT, 91); + setDeviceConfigLong(QcConstants.KEY_QUOTA_BUMP_WINDOW_SIZE_MS, 90 * MINUTE_IN_MILLIS); + setDeviceConfigInt(QcConstants.KEY_QUOTA_BUMP_LIMIT, 89); assertEquals(8 * MINUTE_IN_MILLIS, mQuotaController.getAllowedTimePerPeriodMs()[EXEMPTED_INDEX]); @@ -3069,6 +3476,11 @@ public class QuotaControllerTest { assertEquals(85 * SECOND_IN_MILLIS, mQuotaController.getEJRewardNotificationSeenMs()); assertEquals(84 * SECOND_IN_MILLIS, mQuotaController.getEJGracePeriodTempAllowlistMs()); assertEquals(83 * SECOND_IN_MILLIS, mQuotaController.getEJGracePeriodTopAppMs()); + assertEquals(93 * SECOND_IN_MILLIS, mQuotaController.getQuotaBumpAdditionDurationMs()); + assertEquals(92, mQuotaController.getQuotaBumpAdditionJobCount()); + assertEquals(91, mQuotaController.getQuotaBumpAdditionSessionCount()); + assertEquals(90 * MINUTE_IN_MILLIS, mQuotaController.getQuotaBumpWindowSizeMs()); + assertEquals(89, mQuotaController.getQuotaBumpLimit()); } @Test @@ -3121,6 +3533,11 @@ public class QuotaControllerTest { setDeviceConfigLong(QcConstants.KEY_EJ_REWARD_NOTIFICATION_SEEN_MS, -1); setDeviceConfigLong(QcConstants.KEY_EJ_GRACE_PERIOD_TEMP_ALLOWLIST_MS, -1); setDeviceConfigLong(QcConstants.KEY_EJ_GRACE_PERIOD_TOP_APP_MS, -1); + setDeviceConfigLong(QcConstants.KEY_QUOTA_BUMP_ADDITIONAL_DURATION_MS, -1); + setDeviceConfigInt(QcConstants.KEY_QUOTA_BUMP_ADDITIONAL_JOB_COUNT, -1); + setDeviceConfigInt(QcConstants.KEY_QUOTA_BUMP_ADDITIONAL_SESSION_COUNT, -1); + setDeviceConfigLong(QcConstants.KEY_QUOTA_BUMP_WINDOW_SIZE_MS, 59 * MINUTE_IN_MILLIS); + setDeviceConfigInt(QcConstants.KEY_QUOTA_BUMP_LIMIT, -1); assertEquals(MINUTE_IN_MILLIS, mQuotaController.getAllowedTimePerPeriodMs()[EXEMPTED_INDEX]); @@ -3171,6 +3588,11 @@ public class QuotaControllerTest { assertEquals(0, mQuotaController.getEJRewardNotificationSeenMs()); assertEquals(0, mQuotaController.getEJGracePeriodTempAllowlistMs()); assertEquals(0, mQuotaController.getEJGracePeriodTopAppMs()); + assertEquals(0, mQuotaController.getQuotaBumpAdditionDurationMs()); + assertEquals(0, mQuotaController.getQuotaBumpAdditionJobCount()); + assertEquals(0, mQuotaController.getQuotaBumpAdditionSessionCount()); + assertEquals(60 * MINUTE_IN_MILLIS, mQuotaController.getQuotaBumpWindowSizeMs()); + assertEquals(0, mQuotaController.getQuotaBumpLimit()); // Invalid configurations. // In_QUOTA_BUFFER should never be greater than ALLOWED_TIME_PER_PERIOD @@ -3228,6 +3650,7 @@ public class QuotaControllerTest { setDeviceConfigLong(QcConstants.KEY_EJ_REWARD_NOTIFICATION_SEEN_MS, 25 * HOUR_IN_MILLIS); setDeviceConfigLong(QcConstants.KEY_EJ_GRACE_PERIOD_TEMP_ALLOWLIST_MS, 25 * HOUR_IN_MILLIS); setDeviceConfigLong(QcConstants.KEY_EJ_GRACE_PERIOD_TOP_APP_MS, 25 * HOUR_IN_MILLIS); + setDeviceConfigLong(QcConstants.KEY_QUOTA_BUMP_WINDOW_SIZE_MS, 25 * HOUR_IN_MILLIS); assertEquals(24 * HOUR_IN_MILLIS, mQuotaController.getAllowedTimePerPeriodMs()[EXEMPTED_INDEX]); @@ -3268,6 +3691,7 @@ public class QuotaControllerTest { assertEquals(5 * MINUTE_IN_MILLIS, mQuotaController.getEJRewardNotificationSeenMs()); assertEquals(HOUR_IN_MILLIS, mQuotaController.getEJGracePeriodTempAllowlistMs()); assertEquals(HOUR_IN_MILLIS, mQuotaController.getEJGracePeriodTopAppMs()); + assertEquals(24 * HOUR_IN_MILLIS, mQuotaController.getQuotaBumpWindowSizeMs()); } /** Tests that TimingSessions aren't saved when the device is charging. */ diff --git a/services/tests/servicestests/src/com/android/server/usage/AppStandbyControllerTests.java b/services/tests/servicestests/src/com/android/server/usage/AppStandbyControllerTests.java index fb4d84cc75f2e..c016406fc96a5 100644 --- a/services/tests/servicestests/src/com/android/server/usage/AppStandbyControllerTests.java +++ b/services/tests/servicestests/src/com/android/server/usage/AppStandbyControllerTests.java @@ -179,7 +179,9 @@ public class AppStandbyControllerTests { private AppStandbyController mController; private CountDownLatch mStateChangedLatch = new CountDownLatch(1); + private CountDownLatch mQuotaBumpLatch = new CountDownLatch(1); private String mLatchPkgName = null; + private int mLatchUserId = -1; private AppIdleStateChangeListener mListener = new AppIdleStateChangeListener() { @Override public void onAppIdleStateChanged(String packageName, int userId, @@ -188,6 +190,16 @@ public class AppStandbyControllerTests { if (mLatchPkgName != null && !mLatchPkgName.equals(packageName)) return; mStateChangedLatch.countDown(); } + + @Override + public void triggerTemporaryQuotaBump(String packageName, int userId) { + // Ignore events not related to mLatchPkgName, if set. + if ((mLatchPkgName != null && !mLatchPkgName.equals(packageName)) + || (mLatchUserId != -1 && mLatchUserId != userId)) { + return; + } + mQuotaBumpLatch.countDown(); + } }; static class MyContextWrapper extends ContextWrapper { @@ -880,20 +892,27 @@ public class AppStandbyControllerTests { } @Test - public void testNotificationEvent() throws Exception { + public void testNotificationEvent_bucketPromotion() throws Exception { + mInjector.mPropertiesChangedListener + .onPropertiesChanged(mInjector.getDeviceConfigProperties()); + reportEvent(mController, USER_INTERACTION, 0, PACKAGE_1); assertEquals(STANDBY_BUCKET_ACTIVE, getStandbyBucket(mController, PACKAGE_1)); mInjector.mElapsedRealtime = 1; + rearmQuotaBumpLatch(PACKAGE_1, USER_ID); reportEvent(mController, NOTIFICATION_SEEN, mInjector.mElapsedRealtime, PACKAGE_1); assertEquals(STANDBY_BUCKET_ACTIVE, getStandbyBucket(mController, PACKAGE_1)); mController.forceIdleState(PACKAGE_1, USER_ID, true); reportEvent(mController, NOTIFICATION_SEEN, mInjector.mElapsedRealtime, PACKAGE_1); assertEquals(STANDBY_BUCKET_WORKING_SET, getStandbyBucket(mController, PACKAGE_1)); + assertFalse(mQuotaBumpLatch.await(1, TimeUnit.SECONDS)); } @Test - public void testNotificationEvent_changePromotedBucket() throws Exception { + public void testNotificationEvent_bucketPromotion_changePromotedBucket() throws Exception { + mInjector.mPropertiesChangedListener + .onPropertiesChanged(mInjector.getDeviceConfigProperties()); mController.forceIdleState(PACKAGE_1, USER_ID, true); reportEvent(mController, NOTIFICATION_SEEN, mInjector.mElapsedRealtime, PACKAGE_1); assertEquals(STANDBY_BUCKET_WORKING_SET, getStandbyBucket(mController, PACKAGE_1)); @@ -908,6 +927,28 @@ public class AppStandbyControllerTests { assertEquals(STANDBY_BUCKET_FREQUENT, getStandbyBucket(mController, PACKAGE_1)); } + @Test + public void testNotificationEvent_quotaBump() throws Exception { + mInjector.mSettingsBuilder + .setBoolean("trigger_quota_bump_on_notification_seen", true); + mInjector.mSettingsBuilder + .setInt("notification_seen_promoted_bucket", STANDBY_BUCKET_NEVER); + mInjector.mPropertiesChangedListener + .onPropertiesChanged(mInjector.getDeviceConfigProperties()); + + reportEvent(mController, USER_INTERACTION, 0, PACKAGE_1); + assertEquals(STANDBY_BUCKET_ACTIVE, getStandbyBucket(mController, PACKAGE_1)); + mInjector.mElapsedRealtime = RARE_THRESHOLD * 2; + setAndAssertBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RARE, REASON_MAIN_FORCED_BY_SYSTEM); + + rearmQuotaBumpLatch(PACKAGE_1, USER_ID); + mInjector.mElapsedRealtime += 1; + + reportEvent(mController, NOTIFICATION_SEEN, mInjector.mElapsedRealtime, PACKAGE_1); + assertTrue(mQuotaBumpLatch.await(1, TimeUnit.SECONDS)); + assertEquals(STANDBY_BUCKET_RARE, getStandbyBucket(mController, PACKAGE_1)); + } + @Test @FlakyTest(bugId = 185169504) public void testSlicePinnedEvent() throws Exception { @@ -1384,6 +1425,9 @@ public class AppStandbyControllerTests { @Test @FlakyTest(bugId = 185169504) public void testCascadingTimeouts() throws Exception { + mInjector.mPropertiesChangedListener + .onPropertiesChanged(mInjector.getDeviceConfigProperties()); + reportEvent(mController, USER_INTERACTION, 0, PACKAGE_1); assertBucket(STANDBY_BUCKET_ACTIVE); @@ -1408,6 +1452,9 @@ public class AppStandbyControllerTests { @Test @FlakyTest(bugId = 185169504) public void testOverlappingTimeouts() throws Exception { + mInjector.mPropertiesChangedListener + .onPropertiesChanged(mInjector.getDeviceConfigProperties()); + reportEvent(mController, USER_INTERACTION, 0, PACKAGE_1); assertBucket(STANDBY_BUCKET_ACTIVE); @@ -1498,6 +1545,9 @@ public class AppStandbyControllerTests { @Test @FlakyTest(bugId = 185169504) public void testPredictionNotOverridden() throws Exception { + mInjector.mPropertiesChangedListener + .onPropertiesChanged(mInjector.getDeviceConfigProperties()); + reportEvent(mController, USER_INTERACTION, 0, PACKAGE_1); assertBucket(STANDBY_BUCKET_ACTIVE); @@ -2096,7 +2146,7 @@ public class AppStandbyControllerTests { private void setAndAssertBucket(String pkg, int user, int bucket, int reason) throws Exception { rearmLatch(pkg); mController.setAppStandbyBucket(pkg, user, bucket, reason); - mStateChangedLatch.await(100, TimeUnit.MILLISECONDS); + mStateChangedLatch.await(1, TimeUnit.SECONDS); assertEquals("Failed to set package bucket", bucket, getStandbyBucket(mController, PACKAGE_1)); } @@ -2109,4 +2159,10 @@ public class AppStandbyControllerTests { private void rearmLatch() { rearmLatch(null); } + + private void rearmQuotaBumpLatch(String pkgName, int userId) { + mLatchPkgName = pkgName; + mLatchUserId = userId; + mQuotaBumpLatch = new CountDownLatch(1); + } }