diff options
3 files changed, 431 insertions, 8 deletions
diff --git a/apex/jobscheduler/service/java/com/android/server/alarm/Alarm.java b/apex/jobscheduler/service/java/com/android/server/alarm/Alarm.java index 2e6b8bdb0ad4..fd2bb1347fd3 100644 --- a/apex/jobscheduler/service/java/com/android/server/alarm/Alarm.java +++ b/apex/jobscheduler/service/java/com/android/server/alarm/Alarm.java @@ -123,6 +123,7 @@ class Alarm { public AlarmManagerService.PriorityClass priorityClass; /** Broadcast options to use when delivering this alarm */ public Bundle mIdleOptions; + public boolean mUsingReserveQuota; Alarm(int type, long when, long requestedWhenElapsed, long windowLength, long interval, PendingIntent op, IAlarmListener rec, String listenerTag, WorkSource ws, int flags, @@ -151,6 +152,7 @@ class Alarm { mExactAllowReason = exactAllowReason; sourcePackage = (operation != null) ? operation.getCreatorPackage() : packageName; creatorUid = (operation != null) ? operation.getCreatorUid() : this.uid; + mUsingReserveQuota = false; } public static String makeTag(PendingIntent pi, String tag, int type) { @@ -340,6 +342,9 @@ class Alarm { TimeUtils.formatDuration(getWhenElapsed(), nowELAPSED, ipw); ipw.print(" maxWhenElapsed="); TimeUtils.formatDuration(mMaxWhenElapsed, nowELAPSED, ipw); + if (mUsingReserveQuota) { + ipw.print(" usingReserveQuota=true"); + } ipw.println(); if (alarmClock != null) { diff --git a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java index 7baf80502a3c..0de0a1cf9c8e 100644 --- a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java +++ b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java @@ -213,6 +213,8 @@ public class AlarmManagerService extends SystemService { static final int RARE_INDEX = 3; static final int NEVER_INDEX = 4; + private static final long TEMPORARY_QUOTA_DURATION = INTERVAL_DAY; + private final Intent mBackgroundIntent = new Intent().addFlags(Intent.FLAG_FROM_BACKGROUND); @@ -282,6 +284,7 @@ public class AlarmManagerService extends SystemService { AppWakeupHistory mAppWakeupHistory; AppWakeupHistory mAllowWhileIdleHistory; AppWakeupHistory mAllowWhileIdleCompatHistory; + TemporaryQuotaReserve mTemporaryQuotaReserve; private final SparseLongArray mLastPriorityAlarmDispatch = new SparseLongArray(); private final SparseArray<RingBuffer<RemovedAlarm>> mRemovalHistory = new SparseArray<>(); ClockReceiver mClockReceiver; @@ -359,6 +362,133 @@ public class AlarmManagerService extends SystemService { boolean mAppStandbyParole; /** + * Holds information about temporary quota that can be allotted to apps to use as a "reserve" + * when they run out of their standard app-standby quota. + * This reserve only lasts for a fixed duration of time from when it was last replenished. + */ + static class TemporaryQuotaReserve { + + private static class QuotaInfo { + public int remainingQuota; + public long expirationTime; + public long lastUsage; + } + /** Map of {package, user} -> {quotaInfo} */ + private final ArrayMap<Pair<String, Integer>, QuotaInfo> mQuotaBuffer = new ArrayMap<>(); + + private long mMaxDuration; + + TemporaryQuotaReserve(long maxDuration) { + mMaxDuration = maxDuration; + } + + void replenishQuota(String packageName, int userId, int quota, long nowElapsed) { + if (quota <= 0) { + return; + } + final Pair<String, Integer> packageUser = Pair.create(packageName, userId); + QuotaInfo currentQuotaInfo = mQuotaBuffer.get(packageUser); + if (currentQuotaInfo == null) { + currentQuotaInfo = new QuotaInfo(); + mQuotaBuffer.put(packageUser, currentQuotaInfo); + } + currentQuotaInfo.remainingQuota = quota; + currentQuotaInfo.expirationTime = nowElapsed + mMaxDuration; + } + + /** Returns if the supplied package has reserve quota to fire at the given time. */ + boolean hasQuota(String packageName, int userId, long triggerElapsed) { + final Pair<String, Integer> packageUser = Pair.create(packageName, userId); + final QuotaInfo quotaInfo = mQuotaBuffer.get(packageUser); + + return quotaInfo != null && quotaInfo.remainingQuota > 0 + && triggerElapsed <= quotaInfo.expirationTime; + } + + /** + * Records quota usage of the given package at the given time and subtracts quota if + * required. + */ + void recordUsage(String packageName, int userId, long nowElapsed) { + final Pair<String, Integer> packageUser = Pair.create(packageName, userId); + final QuotaInfo quotaInfo = mQuotaBuffer.get(packageUser); + + if (quotaInfo == null) { + Slog.wtf(TAG, "Temporary quota being consumed at " + nowElapsed + + " but not found for package: " + packageName + ", user: " + userId); + return; + } + // Only consume quota if this usage is later than the last one recorded. This is + // needed as this can be called multiple times when a batch of alarms is delivered. + if (nowElapsed > quotaInfo.lastUsage) { + if (quotaInfo.remainingQuota <= 0) { + Slog.wtf(TAG, "Temporary quota being consumed at " + nowElapsed + + " but remaining only " + quotaInfo.remainingQuota + + " for package: " + packageName + ", user: " + userId); + } else if (quotaInfo.expirationTime < nowElapsed) { + Slog.wtf(TAG, "Temporary quota being consumed at " + nowElapsed + + " but expired at " + quotaInfo.expirationTime + + " for package: " + packageName + ", user: " + userId); + } else { + quotaInfo.remainingQuota--; + // We keep the quotaInfo entry even if remaining quota reduces to 0 as + // following calls can be made with nowElapsed <= lastUsage. The object will + // eventually be removed in cleanUpExpiredQuotas or reused in replenishQuota. + } + quotaInfo.lastUsage = nowElapsed; + } + } + + /** Clean up any quotas that have expired before the given time. */ + void cleanUpExpiredQuotas(long nowElapsed) { + for (int i = mQuotaBuffer.size() - 1; i >= 0; i--) { + final QuotaInfo quotaInfo = mQuotaBuffer.valueAt(i); + if (quotaInfo.expirationTime < nowElapsed) { + mQuotaBuffer.removeAt(i); + } + } + } + + void removeForUser(int userId) { + for (int i = mQuotaBuffer.size() - 1; i >= 0; i--) { + final Pair<String, Integer> packageUserKey = mQuotaBuffer.keyAt(i); + if (packageUserKey.second == userId) { + mQuotaBuffer.removeAt(i); + } + } + } + + void removeForPackage(String packageName, int userId) { + final Pair<String, Integer> packageUser = Pair.create(packageName, userId); + mQuotaBuffer.remove(packageUser); + } + + void dump(IndentingPrintWriter pw, long nowElapsed) { + pw.increaseIndent(); + for (int i = 0; i < mQuotaBuffer.size(); i++) { + final Pair<String, Integer> packageUser = mQuotaBuffer.keyAt(i); + final QuotaInfo quotaInfo = mQuotaBuffer.valueAt(i); + pw.print(packageUser.first); + pw.print(", u"); + pw.print(packageUser.second); + pw.print(": "); + if (quotaInfo == null) { + pw.print("--"); + } else { + pw.print("quota: "); + pw.print(quotaInfo.remainingQuota); + pw.print(", expiration: "); + TimeUtils.formatDuration(quotaInfo.expirationTime, nowElapsed, pw); + pw.print(" last used: "); + TimeUtils.formatDuration(quotaInfo.lastUsage, nowElapsed, pw); + } + pw.println(); + } + pw.decreaseIndent(); + } + } + + /** * A container to keep rolling window history of previous times when an alarm was sent to * a package. */ @@ -569,6 +699,8 @@ public class AlarmManagerService extends SystemService { @VisibleForTesting static final String KEY_KILL_ON_SCHEDULE_EXACT_ALARM_REVOKED = "kill_on_schedule_exact_alarm_revoked"; + @VisibleForTesting + static final String KEY_TEMPORARY_QUOTA_BUMP = "temporary_quota_bump"; private static final long DEFAULT_MIN_FUTURITY = 5 * 1000; private static final long DEFAULT_MIN_INTERVAL = 60 * 1000; @@ -613,6 +745,8 @@ public class AlarmManagerService extends SystemService { private static final boolean DEFAULT_KILL_ON_SCHEDULE_EXACT_ALARM_REVOKED = true; + private static final int DEFAULT_TEMPORARY_QUOTA_BUMP = 0; + // Minimum futurity of a new alarm public long MIN_FUTURITY = DEFAULT_MIN_FUTURITY; @@ -702,6 +836,17 @@ public class AlarmManagerService extends SystemService { public boolean USE_TARE_POLICY = Settings.Global.DEFAULT_ENABLE_TARE == 1; + /** + * The amount of temporary reserve quota to give apps on receiving the + * {@link AppIdleStateChangeListener#triggerTemporaryQuotaBump(String, int)} callback + * from {@link com.android.server.usage.AppStandbyController}. + * <p> This quota adds on top of the standard standby bucket quota available to the app, and + * works the same way, i.e. each count of quota denotes one point in time when the app can + * receive any number of alarms together. + * This quota is tracked per package and expires after {@link #TEMPORARY_QUOTA_DURATION}. + */ + public int TEMPORARY_QUOTA_BUMP = DEFAULT_TEMPORARY_QUOTA_BUMP; + private long mLastAllowWhileIdleWhitelistDuration = -1; private int mVersion = 0; @@ -886,6 +1031,10 @@ public class AlarmManagerService extends SystemService { KEY_KILL_ON_SCHEDULE_EXACT_ALARM_REVOKED, DEFAULT_KILL_ON_SCHEDULE_EXACT_ALARM_REVOKED); break; + case KEY_TEMPORARY_QUOTA_BUMP: + TEMPORARY_QUOTA_BUMP = properties.getInt(KEY_TEMPORARY_QUOTA_BUMP, + DEFAULT_TEMPORARY_QUOTA_BUMP); + break; default: if (name.startsWith(KEY_PREFIX_STANDBY_QUOTA) && !standbyQuotaUpdated) { // The quotas need to be updated in order, so we can't just rely @@ -1136,6 +1285,9 @@ public class AlarmManagerService extends SystemService { pw.print(Settings.Global.ENABLE_TARE, USE_TARE_POLICY); pw.println(); + pw.print(KEY_TEMPORARY_QUOTA_BUMP, TEMPORARY_QUOTA_BUMP); + pw.println(); + pw.decreaseIndent(); } @@ -1748,6 +1900,8 @@ public class AlarmManagerService extends SystemService { mAllowWhileIdleHistory = new AppWakeupHistory(INTERVAL_HOUR); mAllowWhileIdleCompatHistory = new AppWakeupHistory(INTERVAL_HOUR); + mTemporaryQuotaReserve = new TemporaryQuotaReserve(TEMPORARY_QUOTA_DURATION); + mNextWakeup = mNextNonWakeup = 0; // We have to set current TimeZone info to kernel @@ -2391,6 +2545,12 @@ public class AlarmManagerService extends SystemService { final int quotaForBucket = getQuotaForBucketLocked(standbyBucket); if (wakeupsInWindow >= quotaForBucket) { final long minElapsed; + if (mTemporaryQuotaReserve.hasQuota(sourcePackage, sourceUserId, nowElapsed)) { + // We will let this alarm go out as usual, but mark it so it consumes the quota + // at the time of delivery. + alarm.mUsingReserveQuota = true; + return alarm.setPolicyElapsed(APP_STANDBY_POLICY_INDEX, nowElapsed); + } if (quotaForBucket <= 0) { // Just keep deferring indefinitely till the quota changes. minElapsed = nowElapsed + INDEFINITE_DELAY; @@ -2405,6 +2565,7 @@ public class AlarmManagerService extends SystemService { } } // wakeupsInWindow are less than the permitted quota, hence no deferring is needed. + alarm.mUsingReserveQuota = false; return alarm.setPolicyElapsed(APP_STANDBY_POLICY_INDEX, nowElapsed); } @@ -3165,6 +3326,10 @@ public class AlarmManagerService extends SystemService { pw.println("App Alarm history:"); mAppWakeupHistory.dump(pw, nowELAPSED); + pw.println(); + pw.println("Temporary Quota Reserves:"); + mTemporaryQuotaReserve.dump(pw, nowELAPSED); + if (mPendingIdleUntil != null) { pw.println(); pw.println("Idle mode state:"); @@ -4573,6 +4738,7 @@ public class AlarmManagerService extends SystemService { } } deliverAlarmsLocked(triggerList, nowELAPSED); + mTemporaryQuotaReserve.cleanUpExpiredQuotas(nowELAPSED); if (mConstants.USE_TARE_POLICY) { reorderAlarmsBasedOnTare(triggerPackages); } else { @@ -4682,6 +4848,7 @@ public class AlarmManagerService extends SystemService { public static final int REFRESH_EXACT_ALARM_CANDIDATES = 11; public static final int TARE_AFFORDABILITY_CHANGED = 12; public static final int CHECK_EXACT_ALARM_PERMISSION_ON_UPDATE = 13; + public static final int TEMPORARY_QUOTA_CHANGED = 14; AlarmHandler() { super(Looper.myLooper()); @@ -4747,6 +4914,7 @@ public class AlarmManagerService extends SystemService { } break; + case TEMPORARY_QUOTA_CHANGED: case APP_STANDBY_BUCKET_CHANGED: synchronized (mLock) { final ArraySet<Pair<String, Integer>> filterPackages = new ArraySet<>(); @@ -4958,6 +5126,7 @@ public class AlarmManagerService extends SystemService { mAppWakeupHistory.removeForUser(userHandle); mAllowWhileIdleHistory.removeForUser(userHandle); mAllowWhileIdleCompatHistory.removeForUser(userHandle); + mTemporaryQuotaReserve.removeForUser(userHandle); } return; case Intent.ACTION_UID_REMOVED: @@ -5006,6 +5175,7 @@ public class AlarmManagerService extends SystemService { mAllowWhileIdleHistory.removeForPackage(pkg, UserHandle.getUserId(uid)); mAllowWhileIdleCompatHistory.removeForPackage(pkg, UserHandle.getUserId(uid)); + mTemporaryQuotaReserve.removeForPackage(pkg, UserHandle.getUserId(uid)); removeLocked(uid, REMOVE_REASON_UNDEFINED); } else { // external-applications-unavailable case @@ -5040,6 +5210,30 @@ public class AlarmManagerService extends SystemService { mHandler.obtainMessage(AlarmHandler.APP_STANDBY_BUCKET_CHANGED, userId, -1, packageName) .sendToTarget(); } + + @Override + public void triggerTemporaryQuotaBump(String packageName, int userId) { + final int quotaBump; + synchronized (mLock) { + quotaBump = mConstants.TEMPORARY_QUOTA_BUMP; + } + if (quotaBump <= 0) { + return; + } + final int uid = mPackageManagerInternal.getPackageUid(packageName, 0, userId); + if (uid < 0 || UserHandle.isCore(uid)) { + return; + } + if (DEBUG_STANDBY) { + Slog.d(TAG, "Bumping quota temporarily for " + packageName + " for user " + userId); + } + synchronized (mLock) { + mTemporaryQuotaReserve.replenishQuota(packageName, userId, quotaBump, + mInjector.getElapsedRealtime()); + } + mHandler.obtainMessage(AlarmHandler.TEMPORARY_QUOTA_CHANGED, userId, -1, + packageName).sendToTarget(); + } } private final EconomyManagerInternal.AffordabilityChangeListener mAffordabilityChangeListener = @@ -5448,8 +5642,13 @@ public class AlarmManagerService extends SystemService { } } if (!isExemptFromAppStandby(alarm)) { - mAppWakeupHistory.recordAlarmForPackage(alarm.sourcePackage, - UserHandle.getUserId(alarm.creatorUid), nowELAPSED); + final int userId = UserHandle.getUserId(alarm.creatorUid); + if (alarm.mUsingReserveQuota) { + mTemporaryQuotaReserve.recordUsage(alarm.sourcePackage, userId, nowELAPSED); + } else { + mAppWakeupHistory.recordAlarmForPackage(alarm.sourcePackage, userId, + nowELAPSED); + } } final BroadcastStats bs = inflight.mBroadcastStats; bs.count++; diff --git a/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java index cdc9d7198cb5..494246491e47 100644 --- a/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java @@ -67,6 +67,7 @@ import static com.android.server.alarm.AlarmManagerService.AlarmHandler.REFRESH_ import static com.android.server.alarm.AlarmManagerService.AlarmHandler.REMOVE_EXACT_ALARMS; import static com.android.server.alarm.AlarmManagerService.AlarmHandler.REMOVE_FOR_CANCELED; import static com.android.server.alarm.AlarmManagerService.AlarmHandler.TARE_AFFORDABILITY_CHANGED; +import static com.android.server.alarm.AlarmManagerService.AlarmHandler.TEMPORARY_QUOTA_CHANGED; import static com.android.server.alarm.AlarmManagerService.Constants.KEY_ALLOW_WHILE_IDLE_COMPAT_QUOTA; import static com.android.server.alarm.AlarmManagerService.Constants.KEY_ALLOW_WHILE_IDLE_COMPAT_WINDOW; import static com.android.server.alarm.AlarmManagerService.Constants.KEY_ALLOW_WHILE_IDLE_QUOTA; @@ -83,6 +84,7 @@ import static com.android.server.alarm.AlarmManagerService.Constants.KEY_MIN_FUT import static com.android.server.alarm.AlarmManagerService.Constants.KEY_MIN_INTERVAL; import static com.android.server.alarm.AlarmManagerService.Constants.KEY_MIN_WINDOW; import static com.android.server.alarm.AlarmManagerService.Constants.KEY_PRIORITY_ALARM_DELAY; +import static com.android.server.alarm.AlarmManagerService.Constants.KEY_TEMPORARY_QUOTA_BUMP; import static com.android.server.alarm.AlarmManagerService.Constants.MAX_EXACT_ALARM_DENY_LIST_SIZE; import static com.android.server.alarm.AlarmManagerService.FREQUENT_INDEX; import static com.android.server.alarm.AlarmManagerService.INDEFINITE_DELAY; @@ -110,6 +112,7 @@ import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verifyZeroInteractions; import android.Manifest; import android.app.ActivityManager; @@ -654,6 +657,7 @@ public class AlarmManagerServiceTest { setDeviceConfigLong(KEY_MIN_INTERVAL, 0); mDeviceConfigKeys.add(mService.mConstants.KEYS_APP_STANDBY_QUOTAS[ACTIVE_INDEX]); mDeviceConfigKeys.add(mService.mConstants.KEYS_APP_STANDBY_QUOTAS[WORKING_INDEX]); + mDeviceConfigKeys.add(mService.mConstants.KEYS_APP_STANDBY_QUOTAS[FREQUENT_INDEX]); doReturn(50).when(mDeviceConfigProperties) .getInt(eq(mService.mConstants.KEYS_APP_STANDBY_QUOTAS[ACTIVE_INDEX]), anyInt()); doReturn(35).when(mDeviceConfigProperties) @@ -732,6 +736,7 @@ public class AlarmManagerServiceTest { setDeviceConfigLong(KEY_PRIORITY_ALARM_DELAY, 55); setDeviceConfigLong(KEY_MIN_DEVICE_IDLE_FUZZ, 60); setDeviceConfigLong(KEY_MAX_DEVICE_IDLE_FUZZ, 65); + setDeviceConfigInt(KEY_TEMPORARY_QUOTA_BUMP, 70); assertEquals(5, mService.mConstants.MIN_FUTURITY); assertEquals(10, mService.mConstants.MIN_INTERVAL); assertEquals(15, mService.mConstants.MAX_INTERVAL); @@ -745,6 +750,7 @@ public class AlarmManagerServiceTest { assertEquals(55, mService.mConstants.PRIORITY_ALARM_DELAY); assertEquals(60, mService.mConstants.MIN_DEVICE_IDLE_FUZZ); assertEquals(65, mService.mConstants.MAX_DEVICE_IDLE_FUZZ); + assertEquals(70, mService.mConstants.TEMPORARY_QUOTA_BUMP); } @Test @@ -879,6 +885,7 @@ public class AlarmManagerServiceTest { for (int i = 0; i < quota; i++) { alarmSetter.accept(firstTrigger + i); mNowElapsedTest = mTestTimer.getElapsed(); + assertEquals("Incorrect trigger time at i=" + i, firstTrigger + i, mNowElapsedTest); mTestTimer.expire(); } // This one should get deferred on set @@ -897,6 +904,7 @@ public class AlarmManagerServiceTest { alarmSetter.accept(firstTrigger + quota); for (int i = 0; i < quota; i++) { mNowElapsedTest = mTestTimer.getElapsed(); + assertEquals("Incorrect trigger time at i=" + i, firstTrigger + i, mNowElapsedTest); mTestTimer.expire(); } final long expectedNextTrigger = firstTrigger + window; @@ -914,6 +922,7 @@ public class AlarmManagerServiceTest { alarmSetter.accept(expectedNextTrigger); for (int i = 0; i < quota; i++) { mNowElapsedTest = mTestTimer.getElapsed(); + assertEquals("Incorrect trigger time at i=" + i, firstTrigger + i, mNowElapsedTest); mTestTimer.expire(); } assertEquals("Incorrect next alarm trigger", expectedNextTrigger, mTestTimer.getElapsed()); @@ -1919,16 +1928,14 @@ public class AlarmManagerServiceTest { getNewMockPendingIntent(), false, false), quota, mAllowWhileIdleWindow); // Refresh the state - mService.removeLocked(TEST_CALLING_UID, - REMOVE_REASON_UNDEFINED); + mService.removeLocked(TEST_CALLING_UID, REMOVE_REASON_UNDEFINED); mService.mAllowWhileIdleHistory.removeForPackage(TEST_CALLING_PACKAGE, TEST_CALLING_USER); testQuotasDeferralOnExpiration(trigger -> setAllowWhileIdleAlarm(ELAPSED_REALTIME_WAKEUP, trigger, getNewMockPendingIntent(), false, false), quota, mAllowWhileIdleWindow); // Refresh the state - mService.removeLocked(TEST_CALLING_UID, - REMOVE_REASON_UNDEFINED); + mService.removeLocked(TEST_CALLING_UID, REMOVE_REASON_UNDEFINED); mService.mAllowWhileIdleHistory.removeForPackage(TEST_CALLING_PACKAGE, TEST_CALLING_USER); testQuotasNoDeferral(trigger -> setAllowWhileIdleAlarm(ELAPSED_REALTIME_WAKEUP, trigger, @@ -3303,7 +3310,7 @@ public class AlarmManagerServiceTest { Arrays.asList(package4)); mockChangeEnabled(SCHEDULE_EXACT_ALARM_DENIED_BY_DEFAULT, false); - mService.mConstants.EXACT_ALARM_DENY_LIST = new ArraySet<>(new String[] { + mService.mConstants.EXACT_ALARM_DENY_LIST = new ArraySet<>(new String[]{ package1, package3, }); @@ -3315,7 +3322,7 @@ public class AlarmManagerServiceTest { assertTrue(mService.isScheduleExactAlarmAllowedByDefault(package4, uid4)); mockChangeEnabled(SCHEDULE_EXACT_ALARM_DENIED_BY_DEFAULT, true); - mService.mConstants.EXACT_ALARM_DENY_LIST = new ArraySet<>(new String[] { + mService.mConstants.EXACT_ALARM_DENY_LIST = new ArraySet<>(new String[]{ package1, package3, }); @@ -3407,6 +3414,218 @@ public class AlarmManagerServiceTest { assertFalse(mService.hasUseExactAlarmInternal(TEST_CALLING_PACKAGE, TEST_CALLING_UID)); } + @Test + public void temporaryQuotaReserve_hasQuota() { + final int quotaToFill = 5; + final String package1 = "package1"; + final int user1 = 123; + final long startTime = 54; + final long quotaDuration = 17; + + final AlarmManagerService.TemporaryQuotaReserve quotaReserve = + new AlarmManagerService.TemporaryQuotaReserve(quotaDuration); + quotaReserve.replenishQuota(package1, user1, quotaToFill, startTime); + + for (long time = startTime; time <= startTime + quotaDuration; time++) { + assertTrue(quotaReserve.hasQuota(package1, user1, time)); + assertFalse(quotaReserve.hasQuota("some.other.package", 21, time)); + assertFalse(quotaReserve.hasQuota(package1, 321, time)); + } + + assertFalse(quotaReserve.hasQuota(package1, user1, startTime + quotaDuration + 1)); + assertFalse(quotaReserve.hasQuota(package1, user1, startTime + quotaDuration + 435421)); + + for (int i = 0; i < quotaToFill - 1; i++) { + assertTrue(i < quotaDuration); + // Use record usage multiple times with the same timestamp. + quotaReserve.recordUsage(package1, user1, startTime + i); + quotaReserve.recordUsage(package1, user1, startTime + i); + quotaReserve.recordUsage(package1, user1, startTime + i); + quotaReserve.recordUsage(package1, user1, startTime + i); + + // Quota should not run out in this loop. + assertTrue(quotaReserve.hasQuota(package1, user1, startTime + i)); + } + quotaReserve.recordUsage(package1, user1, startTime + quotaDuration); + + // Should be out of quota now. + for (long time = startTime; time <= startTime + quotaDuration; time++) { + assertFalse(quotaReserve.hasQuota(package1, user1, time)); + } + } + + @Test + public void temporaryQuotaReserve_removeForPackage() { + final String[] packages = new String[]{"package1", "test.package2"}; + final int userId = 472; + final long startTime = 59; + final long quotaDuration = 100; + + final AlarmManagerService.TemporaryQuotaReserve quotaReserve = + new AlarmManagerService.TemporaryQuotaReserve(quotaDuration); + + quotaReserve.replenishQuota(packages[0], userId, 10, startTime); + quotaReserve.replenishQuota(packages[1], userId, 10, startTime); + + assertTrue(quotaReserve.hasQuota(packages[0], userId, startTime + 1)); + assertTrue(quotaReserve.hasQuota(packages[1], userId, startTime + 1)); + + quotaReserve.removeForPackage(packages[0], userId); + + assertFalse(quotaReserve.hasQuota(packages[0], userId, startTime + 1)); + assertTrue(quotaReserve.hasQuota(packages[1], userId, startTime + 1)); + } + + @Test + public void temporaryQuotaReserve_removeForUser() { + final String[] packagesUser1 = new String[]{"test1.package1", "test1.package2"}; + final String[] packagesUser2 = new String[]{"test2.p1", "test2.p2", "test2.p3"}; + final int user1 = 3201; + final int user2 = 5409; + final long startTime = 59; + final long quotaDuration = 100; + + final AlarmManagerService.TemporaryQuotaReserve quotaReserve = + new AlarmManagerService.TemporaryQuotaReserve(quotaDuration); + + for (String packageUser1 : packagesUser1) { + quotaReserve.replenishQuota(packageUser1, user1, 10, startTime); + } + for (String packageUser2 : packagesUser2) { + quotaReserve.replenishQuota(packageUser2, user2, 10, startTime); + } + + for (String packageUser1 : packagesUser1) { + assertTrue(quotaReserve.hasQuota(packageUser1, user1, startTime)); + } + for (String packageUser2 : packagesUser2) { + assertTrue(quotaReserve.hasQuota(packageUser2, user2, startTime)); + } + + quotaReserve.removeForUser(user2); + + for (String packageUser1 : packagesUser1) { + assertTrue(quotaReserve.hasQuota(packageUser1, user1, startTime)); + } + for (String packageUser2 : packagesUser2) { + assertFalse(quotaReserve.hasQuota(packageUser2, user2, startTime)); + } + } + + @Test + public void triggerTemporaryQuotaBump_zeroQuota() { + setDeviceConfigInt(KEY_TEMPORARY_QUOTA_BUMP, 0); + + mAppStandbyListener.triggerTemporaryQuotaBump(TEST_CALLING_PACKAGE, TEST_CALLING_USER); + verifyZeroInteractions(mPackageManagerInternal); + verifyZeroInteractions(mService.mHandler); + } + + private void testTemporaryQuota_bumpedAfterDeferral(int standbyBucket) throws Exception { + final int temporaryQuota = 31; + setDeviceConfigInt(KEY_TEMPORARY_QUOTA_BUMP, temporaryQuota); + + final int standbyQuota = mService.getQuotaForBucketLocked(standbyBucket); + when(mUsageStatsManagerInternal.getAppStandbyBucket(eq(TEST_CALLING_PACKAGE), anyInt(), + anyLong())).thenReturn(standbyBucket); + + final long firstTrigger = mNowElapsedTest + 10; + for (int i = 0; i < standbyQuota + 1; i++) { + setTestAlarm(ELAPSED_REALTIME_WAKEUP, firstTrigger + i, getNewMockPendingIntent()); + } + + for (int i = 0; i < standbyQuota; i++) { + mNowElapsedTest = mTestTimer.getElapsed(); + assertEquals("Incorrect trigger time at i=" + i, firstTrigger + i, mNowElapsedTest); + mTestTimer.expire(); + } + + // The last alarm should be deferred due to exceeding the quota + final long deferredTrigger = firstTrigger + mAppStandbyWindow; + assertEquals(deferredTrigger, mTestTimer.getElapsed()); + + // Triggering temporary quota now. + mAppStandbyListener.triggerTemporaryQuotaBump(TEST_CALLING_PACKAGE, TEST_CALLING_USER); + assertAndHandleMessageSync(TEMPORARY_QUOTA_CHANGED); + // The last alarm should now be rescheduled to go as per original expectations + final long originalTrigger = firstTrigger + standbyQuota; + assertEquals("Incorrect next alarm trigger", originalTrigger, mTestTimer.getElapsed()); + } + + + @Test + public void temporaryQuota_bumpedAfterDeferral_active() throws Exception { + testTemporaryQuota_bumpedAfterDeferral(STANDBY_BUCKET_ACTIVE); + } + + @Test + public void temporaryQuota_bumpedAfterDeferral_working() throws Exception { + testTemporaryQuota_bumpedAfterDeferral(STANDBY_BUCKET_WORKING_SET); + } + + @Test + public void temporaryQuota_bumpedAfterDeferral_frequent() throws Exception { + testTemporaryQuota_bumpedAfterDeferral(STANDBY_BUCKET_FREQUENT); + } + + @Test + public void temporaryQuota_bumpedAfterDeferral_rare() throws Exception { + testTemporaryQuota_bumpedAfterDeferral(STANDBY_BUCKET_RARE); + } + + private void testTemporaryQuota_bumpedBeforeDeferral(int standbyBucket) throws Exception { + final int temporaryQuota = 7; + setDeviceConfigInt(KEY_TEMPORARY_QUOTA_BUMP, temporaryQuota); + + final int standbyQuota = mService.getQuotaForBucketLocked(standbyBucket); + when(mUsageStatsManagerInternal.getAppStandbyBucket(eq(TEST_CALLING_PACKAGE), anyInt(), + anyLong())).thenReturn(standbyBucket); + + mAppStandbyListener.triggerTemporaryQuotaBump(TEST_CALLING_PACKAGE, TEST_CALLING_USER); + // No need to handle message TEMPORARY_QUOTA_CHANGED, as the quota change doesn't need to + // trigger a re-evaluation in this test. + testQuotasDeferralOnExpiration(trigger -> setTestAlarm(ELAPSED_REALTIME_WAKEUP, trigger, + getNewMockPendingIntent()), standbyQuota + temporaryQuota, mAppStandbyWindow); + + // refresh the state. + mService.removeLocked(TEST_CALLING_PACKAGE); + mService.mAppWakeupHistory.removeForPackage(TEST_CALLING_PACKAGE, TEST_CALLING_USER); + mService.mTemporaryQuotaReserve.removeForPackage(TEST_CALLING_PACKAGE, TEST_CALLING_USER); + + mAppStandbyListener.triggerTemporaryQuotaBump(TEST_CALLING_PACKAGE, TEST_CALLING_USER); + testQuotasDeferralOnSet(trigger -> setTestAlarm(ELAPSED_REALTIME_WAKEUP, trigger, + getNewMockPendingIntent()), standbyQuota + temporaryQuota, mAppStandbyWindow); + + // refresh the state. + mService.removeLocked(TEST_CALLING_PACKAGE); + mService.mAppWakeupHistory.removeForPackage(TEST_CALLING_PACKAGE, TEST_CALLING_USER); + mService.mTemporaryQuotaReserve.removeForPackage(TEST_CALLING_PACKAGE, TEST_CALLING_USER); + + mAppStandbyListener.triggerTemporaryQuotaBump(TEST_CALLING_PACKAGE, TEST_CALLING_USER); + testQuotasNoDeferral(trigger -> setTestAlarm(ELAPSED_REALTIME_WAKEUP, trigger, + getNewMockPendingIntent()), standbyQuota + temporaryQuota, mAppStandbyWindow); + } + + @Test + public void temporaryQuota_bumpedBeforeDeferral_active() throws Exception { + testTemporaryQuota_bumpedBeforeDeferral(STANDBY_BUCKET_ACTIVE); + } + + @Test + public void temporaryQuota_bumpedBeforeDeferral_working() throws Exception { + testTemporaryQuota_bumpedBeforeDeferral(STANDBY_BUCKET_WORKING_SET); + } + + @Test + public void temporaryQuota_bumpedBeforeDeferral_frequent() throws Exception { + testTemporaryQuota_bumpedBeforeDeferral(STANDBY_BUCKET_FREQUENT); + } + + @Test + public void temporaryQuota_bumpedBeforeDeferral_rare() throws Exception { + testTemporaryQuota_bumpedBeforeDeferral(STANDBY_BUCKET_RARE); + } + @After public void tearDown() { if (mMockingSession != null) { |