From 355586523305bba2246c76aa5954bb48f073049b Mon Sep 17 00:00:00 2001 From: Suprabh Shukla Date: Mon, 23 May 2022 20:08:15 -0700 Subject: [PATCH] Adding a reserve of temporary quota for alarms Adding a mechanism to allocate temporary per-app quotas that can be used by them after running out of their standard standby quota. This can be used to give back apps quota in case they are not directly being opened by the user but interact with the user in indirect ways like notifications. Currently this quota is set to expire 24 hours from the time it is last replenished. Test: atest FrameworksMockingServicesTests:AlarmManagerServiceTest Existing CTS tests pass: atest CtsAlarmManagerTestCases Bug: 231925670 Change-Id: I18e5b2fbbb40b378b2853c4e86fa4729f87f76e4 --- .../java/com/android/server/alarm/Alarm.java | 5 + .../server/alarm/AlarmManagerService.java | 203 ++++++++++++++- .../server/alarm/AlarmManagerServiceTest.java | 231 +++++++++++++++++- 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 2e6b8bdb0ad4b..fd2bb1347fd36 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 7baf80502a3c5..0de0a1cf9c8e9 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> mRemovalHistory = new SparseArray<>(); ClockReceiver mClockReceiver; @@ -358,6 +361,133 @@ public class AlarmManagerService extends SystemService { @VisibleForTesting 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, 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 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 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 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 packageUserKey = mQuotaBuffer.keyAt(i); + if (packageUserKey.second == userId) { + mQuotaBuffer.removeAt(i); + } + } + } + + void removeForPackage(String packageName, int userId) { + final Pair 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 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}. + *

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> 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 cdc9d7198cb51..494246491e478 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) {