diff options
5 files changed, 974 insertions, 88 deletions
diff --git a/core/java/android/util/LongArrayQueue.java b/core/java/android/util/LongArrayQueue.java new file mode 100644 index 000000000000..d5f048434b32 --- /dev/null +++ b/core/java/android/util/LongArrayQueue.java @@ -0,0 +1,165 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.util; + +import com.android.internal.util.ArrayUtils; +import com.android.internal.util.GrowingArrayUtils; + +import libcore.util.EmptyArray; + +import java.util.NoSuchElementException; + +/** + * A lightweight implementation for a queue with long values. + * Additionally supports getting an element with a specified position from the head of the queue. + * The queue grows in size if needed to accommodate new elements. + * + * @hide + */ +public class LongArrayQueue { + + private long[] mValues; + private int mSize; + private int mHead; + private int mTail; + + /** + * Initializes a queue with the given starting capacity. + * + * @param initialCapacity the capacity. + */ + public LongArrayQueue(int initialCapacity) { + if (initialCapacity == 0) { + mValues = EmptyArray.LONG; + } else { + mValues = ArrayUtils.newUnpaddedLongArray(initialCapacity); + } + mSize = 0; + mHead = mTail = 0; + } + + /** + * Initializes a queue with default starting capacity. + */ + public LongArrayQueue() { + this(16); + } + + private void grow() { + if (mSize < mValues.length) { + throw new IllegalStateException("Queue not full yet!"); + } + final int newSize = GrowingArrayUtils.growSize(mSize); + final long[] newArray = ArrayUtils.newUnpaddedLongArray(newSize); + final int r = mValues.length - mHead; // Number of elements on and to the right of head. + System.arraycopy(mValues, mHead, newArray, 0, r); + System.arraycopy(mValues, 0, newArray, r, mHead); + mValues = newArray; + mHead = 0; + mTail = mSize; + } + + /** + * Returns the number of elements in the queue. + */ + public int size() { + return mSize; + } + + /** + * Removes all elements from this queue. + */ + public void clear() { + mSize = 0; + mHead = mTail = 0; + } + + /** + * Adds a value to the tail of the queue. + * + * @param value the value to be added. + */ + public void addLast(long value) { + if (mSize == mValues.length) { + grow(); + } + mValues[mTail] = value; + mTail = (mTail + 1) % mValues.length; + mSize++; + } + + /** + * Removes an element from the head of the queue. + * + * @return the element at the head of the queue. + * @throws NoSuchElementException if the queue is empty. + */ + public long removeFirst() { + if (mSize == 0) { + throw new NoSuchElementException("Queue is empty!"); + } + final long ret = mValues[mHead]; + mHead = (mHead + 1) % mValues.length; + mSize--; + return ret; + } + + /** + * Returns the element at the given position from the head of the queue, where 0 represents the + * head of the queue. + * + * @param position the position from the head of the queue. + * @return the element found at the given position. + * @throws IndexOutOfBoundsException if {@code position} < {@code 0} or + * {@code position} >= {@link #size()} + */ + public long get(int position) { + if (position < 0 || position >= mSize) { + throw new IndexOutOfBoundsException("Index " + position + + " not valid for a queue of size " + mSize); + } + final int index = (mHead + position) % mValues.length; + return mValues[index]; + } + + /** + * Returns the element at the head of the queue, without removing it. + * + * @return the element at the head of the queue. + * @throws NoSuchElementException if the queue is empty + */ + public long peekFirst() { + if (mSize == 0) { + throw new NoSuchElementException("Queue is empty!"); + } + return mValues[mHead]; + } + + /** + * Returns the element at the tail of the queue. + * + * @return the element at the tail of the queue. + * @throws NoSuchElementException if the queue is empty. + */ + public long peekLast() { + if (mSize == 0) { + throw new NoSuchElementException("Queue is empty!"); + } + final int index = (mTail == 0) ? mValues.length - 1 : mTail - 1; + return mValues[index]; + } +} diff --git a/core/tests/coretests/src/android/util/LongArrayQueueTest.java b/core/tests/coretests/src/android/util/LongArrayQueueTest.java new file mode 100644 index 000000000000..77e8d608810f --- /dev/null +++ b/core/tests/coretests/src/android/util/LongArrayQueueTest.java @@ -0,0 +1,238 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.util; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.fail; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.NoSuchElementException; + +/** + * Internal tests for {@link LongArrayQueue}. + */ +@SmallTest +@RunWith(AndroidJUnit4.class) +public class LongArrayQueueTest { + + private LongArrayQueue mQueueUnderTest; + + @Before + public void setUp() { + mQueueUnderTest = new LongArrayQueue(); + } + + @Test + public void removeFirstOnEmptyQueue() { + try { + mQueueUnderTest.removeFirst(); + fail("removeFirst() succeeded on an empty queue!"); + } catch (NoSuchElementException e) { + } + mQueueUnderTest.addLast(5); + mQueueUnderTest.removeFirst(); + try { + mQueueUnderTest.removeFirst(); + fail("removeFirst() succeeded on an empty queue!"); + } catch (NoSuchElementException e) { + } + } + + @Test + public void addLastRemoveFirstFifo() { + mQueueUnderTest.addLast(1); + assertEquals(1, mQueueUnderTest.removeFirst()); + int n = 890; + int removes = 0; + for (int i = 0; i < n; i++) { + mQueueUnderTest.addLast(i); + if ((i % 3) == 0) { + assertEquals(removes++, mQueueUnderTest.removeFirst()); + } + } + while (removes < n) { + assertEquals(removes++, mQueueUnderTest.removeFirst()); + } + } + + @Test + public void peekFirstOnEmptyQueue() { + try { + mQueueUnderTest.peekFirst(); + fail("peekFirst() succeeded on an empty queue!"); + } catch (NoSuchElementException e) { + } + mQueueUnderTest.addLast(5); + mQueueUnderTest.removeFirst(); + try { + mQueueUnderTest.peekFirst(); + fail("peekFirst() succeeded on an empty queue!"); + } catch (NoSuchElementException e) { + } + } + + @Test + public void peekFirstChanges() { + mQueueUnderTest.addLast(1); + assertEquals(1, mQueueUnderTest.peekFirst()); + mQueueUnderTest.addLast(2); + mQueueUnderTest.addLast(3); + mQueueUnderTest.addLast(4); + // addLast() has no effect on peekFirst(). + assertEquals(1, mQueueUnderTest.peekFirst()); + mQueueUnderTest.removeFirst(); + mQueueUnderTest.removeFirst(); + assertEquals(3, mQueueUnderTest.peekFirst()); + } + + @Test + public void peekLastOnEmptyQueue() { + try { + mQueueUnderTest.peekLast(); + fail("peekLast() succeeded on an empty queue!"); + } catch (NoSuchElementException e) { + } + mQueueUnderTest.addLast(5); + mQueueUnderTest.removeFirst(); + try { + mQueueUnderTest.peekLast(); + fail("peekLast() succeeded on an empty queue!"); + } catch (NoSuchElementException e) { + } + } + + @Test + public void peekLastChanges() { + mQueueUnderTest.addLast(1); + assertEquals(1, mQueueUnderTest.peekLast()); + mQueueUnderTest.addLast(2); + mQueueUnderTest.addLast(3); + mQueueUnderTest.addLast(4); + assertEquals(4, mQueueUnderTest.peekLast()); + mQueueUnderTest.removeFirst(); + mQueueUnderTest.removeFirst(); + // removeFirst() has no effect on peekLast(). + assertEquals(4, mQueueUnderTest.peekLast()); + } + + @Test + public void peekFirstVsPeekLast() { + mQueueUnderTest.addLast(2); + assertEquals(mQueueUnderTest.peekFirst(), mQueueUnderTest.peekLast()); + mQueueUnderTest.addLast(3); + assertNotEquals(mQueueUnderTest.peekFirst(), mQueueUnderTest.peekLast()); + mQueueUnderTest.removeFirst(); + assertEquals(mQueueUnderTest.peekFirst(), mQueueUnderTest.peekLast()); + } + + @Test + public void peekFirstVsRemoveFirst() { + int n = 25; + for (int i = 0; i < n; i++) { + mQueueUnderTest.addLast(i + 1); + } + for (int i = 0; i < n; i++) { + long peekVal = mQueueUnderTest.peekFirst(); + assertEquals(peekVal, mQueueUnderTest.removeFirst()); + } + } + + @Test + public void sizeOfEmptyQueue() { + assertEquals(0, mQueueUnderTest.size()); + mQueueUnderTest = new LongArrayQueue(1000); + // capacity doesn't affect size. + assertEquals(0, mQueueUnderTest.size()); + } + + @Test + public void sizeAfterOperations() { + final int added = 1200; + for (int i = 0; i < added; i++) { + mQueueUnderTest.addLast(i); + } + // each add increments the size by 1. + assertEquals(added, mQueueUnderTest.size()); + mQueueUnderTest.peekLast(); + mQueueUnderTest.peekFirst(); + // peeks don't change the size. + assertEquals(added, mQueueUnderTest.size()); + final int removed = 345; + for (int i = 0; i < removed; i++) { + mQueueUnderTest.removeFirst(); + } + // each remove decrements the size by 1. + assertEquals(added - removed, mQueueUnderTest.size()); + mQueueUnderTest.clear(); + // clear reduces the size to 0. + assertEquals(0, mQueueUnderTest.size()); + } + + @Test + public void getInvalidPositions() { + try { + mQueueUnderTest.get(0); + fail("get(0) succeeded on an empty queue!"); + } catch (IndexOutOfBoundsException e) { + } + int n = 520; + for (int i = 0; i < 2 * n; i++) { + mQueueUnderTest.addLast(i + 1); + } + for (int i = 0; i < n; i++) { + mQueueUnderTest.removeFirst(); + } + try { + mQueueUnderTest.get(-3); + fail("get(-3) succeeded"); + } catch (IndexOutOfBoundsException e) { + } + assertEquals(n, mQueueUnderTest.size()); + try { + mQueueUnderTest.get(n); + fail("get(" + n + ") succeeded on a queue with " + n + " elements"); + } catch (IndexOutOfBoundsException e) { + } + try { + mQueueUnderTest.get(n + 3); + fail("get(" + (n + 3) + ") succeeded on a queue with " + n + " elements"); + } catch (IndexOutOfBoundsException e) { + } + } + + @Test + public void getValidPositions() { + int added = 423; + int removed = 212; + for (int i = 0; i < added; i++) { + mQueueUnderTest.addLast(i); + } + for (int i = 0; i < removed; i++) { + mQueueUnderTest.removeFirst(); + } + for (int i = 0; i < (added - removed); i++) { + assertEquals(removed + i, mQueueUnderTest.get(i)); + } + } +} diff --git a/services/core/java/com/android/server/AlarmManagerService.java b/services/core/java/com/android/server/AlarmManagerService.java index fcd136c65169..e3dcb7d331cf 100644 --- a/services/core/java/com/android/server/AlarmManagerService.java +++ b/services/core/java/com/android/server/AlarmManagerService.java @@ -77,6 +77,7 @@ import android.util.ArrayMap; import android.util.ArraySet; import android.util.KeyValueListParser; import android.util.Log; +import android.util.LongArrayQueue; import android.util.NtpTrustedTime; import android.util.Pair; import android.util.Slog; @@ -91,6 +92,7 @@ import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.ArrayUtils; import com.android.internal.util.DumpUtils; +import com.android.internal.util.IndentingPrintWriter; import com.android.internal.util.LocalLog; import com.android.internal.util.StatLogger; import com.android.server.AppStateTracker.Listener; @@ -145,6 +147,7 @@ class AlarmManagerService extends SystemService { static final String TIMEZONE_PROPERTY = "persist.sys.timezone"; static final int TICK_HISTORY_DEPTH = 10; + static final long MILLIS_IN_DAY = 24 * 60 * 60 * 1000; // Indices into the APP_STANDBY_MIN_DELAYS and KEYS_APP_STANDBY_DELAY arrays static final int ACTIVE_INDEX = 0; @@ -195,6 +198,7 @@ class AlarmManagerService extends SystemService { ArrayList<Alarm> mPendingNonWakeupAlarms = new ArrayList<>(); ArrayList<InFlight> mInFlight = new ArrayList<>(); AlarmHandler mHandler; + AppWakeupHistory mAppWakeupHistory; ClockReceiver mClockReceiver; final DeliveryTracker mDeliveryTracker = new DeliveryTracker(); Intent mTimeTickIntent; @@ -277,7 +281,91 @@ class AlarmManagerService extends SystemService { private AppStateTracker mAppStateTracker; private boolean mAppStandbyParole; - private ArrayMap<Pair<String, Integer>, Long> mLastAlarmDeliveredForPackage = new ArrayMap<>(); + + /** + * A rolling window history of previous times when an alarm was sent to a package. + */ + private static class AppWakeupHistory { + private ArrayMap<Pair<String, Integer>, LongArrayQueue> mPackageHistory = + new ArrayMap<>(); + private long mWindowSize; + + AppWakeupHistory(long windowSize) { + mWindowSize = windowSize; + } + + void recordAlarmForPackage(String packageName, int userId, long nowElapsed) { + final Pair<String, Integer> packageUser = Pair.create(packageName, userId); + LongArrayQueue history = mPackageHistory.get(packageUser); + if (history == null) { + history = new LongArrayQueue(); + mPackageHistory.put(packageUser, history); + } + if (history.size() == 0 || history.peekLast() < nowElapsed) { + history.addLast(nowElapsed); + } + snapToWindow(history); + } + + void removeForUser(int userId) { + for (int i = mPackageHistory.size() - 1; i >= 0; i--) { + final Pair<String, Integer> packageUserKey = mPackageHistory.keyAt(i); + if (packageUserKey.second == userId) { + mPackageHistory.removeAt(i); + } + } + } + + void removeForPackage(String packageName, int userId) { + final Pair<String, Integer> packageUser = Pair.create(packageName, userId); + mPackageHistory.remove(packageUser); + } + + private void snapToWindow(LongArrayQueue history) { + while (history.peekFirst() + mWindowSize < history.peekLast()) { + history.removeFirst(); + } + } + + int getTotalWakeupsInWindow(String packageName, int userId) { + final LongArrayQueue history = mPackageHistory.get(Pair.create(packageName, userId)); + return (history == null) ? 0 : history.size(); + } + + long getLastWakeupForPackage(String packageName, int userId, int positionFromEnd) { + final LongArrayQueue history = mPackageHistory.get(Pair.create(packageName, userId)); + if (history == null) { + return 0; + } + final int i = history.size() - positionFromEnd; + return (i < 0) ? 0 : history.get(i); + } + + void dump(PrintWriter pw, String prefix, long nowElapsed) { + dump(new IndentingPrintWriter(pw, " ").setIndent(prefix), nowElapsed); + } + + void dump(IndentingPrintWriter pw, long nowElapsed) { + pw.println("App Alarm history:"); + pw.increaseIndent(); + for (int i = 0; i < mPackageHistory.size(); i++) { + final Pair<String, Integer> packageUser = mPackageHistory.keyAt(i); + final LongArrayQueue timestamps = mPackageHistory.valueAt(i); + pw.print(packageUser.first); + pw.print(", u"); + pw.print(packageUser.second); + pw.print(": "); + // limit dumping to a max of 100 values + final int lastIdx = Math.max(0, timestamps.size() - 100); + for (int j = timestamps.size() - 1; j >= lastIdx; j--) { + TimeUtils.formatDuration(timestamps.get(j), nowElapsed, pw); + pw.print(", "); + } + pw.println(); + } + pw.decreaseIndent(); + } + } /** * All times are in milliseconds. These constants are kept synchronized with the system @@ -302,6 +390,17 @@ class AlarmManagerService extends SystemService { = "allow_while_idle_whitelist_duration"; @VisibleForTesting static final String KEY_LISTENER_TIMEOUT = "listener_timeout"; + @VisibleForTesting + static final String KEY_APP_STANDBY_QUOTAS_ENABLED = "app_standby_quotas_enabled"; + private static final String KEY_APP_STANDBY_WINDOW = "app_standby_window"; + @VisibleForTesting + final String[] KEYS_APP_STANDBY_QUOTAS = { + "standby_active_quota", + "standby_working_quota", + "standby_frequent_quota", + "standby_rare_quota", + "standby_never_quota", + }; // Keys for specifying throttling delay based on app standby bucketing private final String[] KEYS_APP_STANDBY_DELAY = { @@ -319,6 +418,18 @@ class AlarmManagerService extends SystemService { private static final long DEFAULT_ALLOW_WHILE_IDLE_LONG_TIME = 9*60*1000; private static final long DEFAULT_ALLOW_WHILE_IDLE_WHITELIST_DURATION = 10*1000; private static final long DEFAULT_LISTENER_TIMEOUT = 5 * 1000; + private static final boolean DEFAULT_APP_STANDBY_QUOTAS_ENABLED = true; + private static final long DEFAULT_APP_STANDBY_WINDOW = 60 * 60 * 1000; // 1 hr + /** + * Max number of times an app can receive alarms in {@link #APP_STANDBY_WINDOW} + */ + private final int[] DEFAULT_APP_STANDBY_QUOTAS = { + 720, // Active + 10, // Working + 2, // Frequent + 1, // Rare + 0 // Never + }; private final long[] DEFAULT_APP_STANDBY_DELAYS = { 0, // Active 6 * 60_000, // Working @@ -348,8 +459,11 @@ class AlarmManagerService extends SystemService { // Direct alarm listener callback timeout public long LISTENER_TIMEOUT = DEFAULT_LISTENER_TIMEOUT; + public boolean APP_STANDBY_QUOTAS_ENABLED = DEFAULT_APP_STANDBY_QUOTAS_ENABLED; + public long APP_STANDBY_WINDOW = DEFAULT_APP_STANDBY_WINDOW; public long[] APP_STANDBY_MIN_DELAYS = new long[DEFAULT_APP_STANDBY_DELAYS.length]; + public int[] APP_STANDBY_QUOTAS = new int[DEFAULT_APP_STANDBY_QUOTAS.length]; private ContentResolver mResolver; private final KeyValueListParser mParser = new KeyValueListParser(','); @@ -409,48 +523,90 @@ class AlarmManagerService extends SystemService { DEFAULT_APP_STANDBY_DELAYS[ACTIVE_INDEX]); for (int i = WORKING_INDEX; i < KEYS_APP_STANDBY_DELAY.length; i++) { APP_STANDBY_MIN_DELAYS[i] = mParser.getDurationMillis(KEYS_APP_STANDBY_DELAY[i], - Math.max(APP_STANDBY_MIN_DELAYS[i-1], DEFAULT_APP_STANDBY_DELAYS[i])); + Math.max(APP_STANDBY_MIN_DELAYS[i - 1], DEFAULT_APP_STANDBY_DELAYS[i])); + } + + APP_STANDBY_QUOTAS_ENABLED = mParser.getBoolean(KEY_APP_STANDBY_QUOTAS_ENABLED, + DEFAULT_APP_STANDBY_QUOTAS_ENABLED); + + APP_STANDBY_WINDOW = mParser.getLong(KEY_APP_STANDBY_WINDOW, + DEFAULT_APP_STANDBY_WINDOW); + if (APP_STANDBY_WINDOW > DEFAULT_APP_STANDBY_WINDOW) { + Slog.w(TAG, "Cannot exceed the app_standby_window size of " + + DEFAULT_APP_STANDBY_WINDOW); + APP_STANDBY_WINDOW = DEFAULT_APP_STANDBY_WINDOW; + } else if (APP_STANDBY_WINDOW < DEFAULT_APP_STANDBY_WINDOW) { + // Not recommended outside of testing. + Slog.w(TAG, "Using a non-default app_standby_window of " + APP_STANDBY_WINDOW); + } + + APP_STANDBY_QUOTAS[ACTIVE_INDEX] = mParser.getInt( + KEYS_APP_STANDBY_QUOTAS[ACTIVE_INDEX], + DEFAULT_APP_STANDBY_QUOTAS[ACTIVE_INDEX]); + for (int i = WORKING_INDEX; i < KEYS_APP_STANDBY_QUOTAS.length; i++) { + APP_STANDBY_QUOTAS[i] = mParser.getInt(KEYS_APP_STANDBY_QUOTAS[i], + Math.min(APP_STANDBY_QUOTAS[i - 1], DEFAULT_APP_STANDBY_QUOTAS[i])); } updateAllowWhileIdleWhitelistDurationLocked(); } } - void dump(PrintWriter pw) { - pw.println(" Settings:"); + void dump(PrintWriter pw, String prefix) { + dump(new IndentingPrintWriter(pw, " ").setIndent(prefix)); + } + + void dump(IndentingPrintWriter pw) { + pw.println("Settings:"); - pw.print(" "); pw.print(KEY_MIN_FUTURITY); pw.print("="); + pw.increaseIndent(); + + pw.print(KEY_MIN_FUTURITY); pw.print("="); TimeUtils.formatDuration(MIN_FUTURITY, pw); pw.println(); - pw.print(" "); pw.print(KEY_MIN_INTERVAL); pw.print("="); + pw.print(KEY_MIN_INTERVAL); pw.print("="); TimeUtils.formatDuration(MIN_INTERVAL, pw); pw.println(); - pw.print(" "); pw.print(KEY_MAX_INTERVAL); pw.print("="); + pw.print(KEY_MAX_INTERVAL); pw.print("="); TimeUtils.formatDuration(MAX_INTERVAL, pw); pw.println(); - pw.print(" "); pw.print(KEY_LISTENER_TIMEOUT); pw.print("="); + pw.print(KEY_LISTENER_TIMEOUT); pw.print("="); TimeUtils.formatDuration(LISTENER_TIMEOUT, pw); pw.println(); - pw.print(" "); pw.print(KEY_ALLOW_WHILE_IDLE_SHORT_TIME); pw.print("="); + pw.print(KEY_ALLOW_WHILE_IDLE_SHORT_TIME); pw.print("="); TimeUtils.formatDuration(ALLOW_WHILE_IDLE_SHORT_TIME, pw); pw.println(); - pw.print(" "); pw.print(KEY_ALLOW_WHILE_IDLE_LONG_TIME); pw.print("="); + pw.print(KEY_ALLOW_WHILE_IDLE_LONG_TIME); pw.print("="); TimeUtils.formatDuration(ALLOW_WHILE_IDLE_LONG_TIME, pw); pw.println(); - pw.print(" "); pw.print(KEY_ALLOW_WHILE_IDLE_WHITELIST_DURATION); pw.print("="); + pw.print(KEY_ALLOW_WHILE_IDLE_WHITELIST_DURATION); pw.print("="); TimeUtils.formatDuration(ALLOW_WHILE_IDLE_WHITELIST_DURATION, pw); pw.println(); for (int i = 0; i < KEYS_APP_STANDBY_DELAY.length; i++) { - pw.print(" "); pw.print(KEYS_APP_STANDBY_DELAY[i]); pw.print("="); + pw.print(KEYS_APP_STANDBY_DELAY[i]); pw.print("="); TimeUtils.formatDuration(APP_STANDBY_MIN_DELAYS[i], pw); pw.println(); } + + pw.print(KEY_APP_STANDBY_QUOTAS_ENABLED); pw.print("="); + pw.println(APP_STANDBY_QUOTAS_ENABLED); + + pw.print(KEY_APP_STANDBY_WINDOW); pw.print("="); + TimeUtils.formatDuration(APP_STANDBY_WINDOW, pw); + pw.println(); + + for (int i = 0; i < KEYS_APP_STANDBY_QUOTAS.length; i++) { + pw.print(KEYS_APP_STANDBY_QUOTAS[i]); pw.print("="); + pw.println(APP_STANDBY_QUOTAS[i]); + } + + pw.decreaseIndent(); } void dumpProto(ProtoOutputStream proto, long fieldId) { @@ -925,7 +1081,7 @@ class AlarmManagerService extends SystemService { if (targetPackages != null && !targetPackages.contains(packageUser)) { continue; } - if (adjustDeliveryTimeBasedOnStandbyBucketLocked(alarm)) { + if (adjustDeliveryTimeBasedOnBucketLocked(alarm)) { batch.remove(alarm); rescheduledAlarms.add(alarm); } @@ -1300,6 +1456,7 @@ class AlarmManagerService extends SystemService { synchronized (mLock) { mHandler = new AlarmHandler(); mConstants = new Constants(mHandler); + mAppWakeupHistory = new AppWakeupHistory(Constants.DEFAULT_APP_STANDBY_WINDOW); mNextWakeup = mNextNonWakeup = 0; @@ -1583,6 +1740,27 @@ class AlarmManagerService extends SystemService { } /** + * Returns the maximum alarms that an app in the specified bucket can receive in a rolling time + * window given by {@link Constants#APP_STANDBY_WINDOW} + */ + @VisibleForTesting + int getQuotaForBucketLocked(int bucket) { + final int index; + if (bucket <= UsageStatsManager.STANDBY_BUCKET_ACTIVE) { + index = ACTIVE_INDEX; + } else if (bucket <= UsageStatsManager.STANDBY_BUCKET_WORKING_SET) { + index = WORKING_INDEX; + } else if (bucket <= UsageStatsManager.STANDBY_BUCKET_FREQUENT) { + index = FREQUENT_INDEX; + } else if (bucket < UsageStatsManager.STANDBY_BUCKET_NEVER) { + index = RARE_INDEX; + } else { + index = NEVER_INDEX; + } + return mConstants.APP_STANDBY_QUOTAS[index]; + } + + /** * Return the minimum time that should elapse before an app in the specified bucket * can receive alarms again */ @@ -1608,7 +1786,7 @@ class AlarmManagerService extends SystemService { * @param alarm The alarm to adjust * @return true if the alarm delivery time was updated. */ - private boolean adjustDeliveryTimeBasedOnStandbyBucketLocked(Alarm alarm) { + private boolean adjustDeliveryTimeBasedOnBucketLocked(Alarm alarm) { if (isExemptFromAppStandby(alarm)) { return false; } @@ -1629,18 +1807,49 @@ class AlarmManagerService extends SystemService { final int standbyBucket = mUsageStatsManagerInternal.getAppStandbyBucket( sourcePackage, sourceUserId, mInjector.getElapsedRealtime()); - final Pair<String, Integer> packageUser = Pair.create(sourcePackage, sourceUserId); - final long lastElapsed = mLastAlarmDeliveredForPackage.getOrDefault(packageUser, 0L); - if (lastElapsed > 0) { - final long minElapsed = lastElapsed + getMinDelayForBucketLocked(standbyBucket); - if (alarm.expectedWhenElapsed < minElapsed) { - alarm.whenElapsed = alarm.maxWhenElapsed = minElapsed; - } else { - // app is now eligible to run alarms at the originally requested window. + if (mConstants.APP_STANDBY_QUOTAS_ENABLED) { + // Quota deferring implementation: + final int wakeupsInWindow = mAppWakeupHistory.getTotalWakeupsInWindow(sourcePackage, + sourceUserId); + final int quotaForBucket = getQuotaForBucketLocked(standbyBucket); + boolean deferred = false; + if (wakeupsInWindow >= quotaForBucket) { + final long minElapsed; + if (quotaForBucket <= 0) { + // Just keep deferring for a day till the quota changes + minElapsed = mInjector.getElapsedRealtime() + MILLIS_IN_DAY; + } else { + // Suppose the quota for window was q, and the qth last delivery time for this + // package was t(q) then the next delivery must be after t(q) + <window_size> + final long t = mAppWakeupHistory.getLastWakeupForPackage(sourcePackage, + sourceUserId, quotaForBucket); + minElapsed = t + 1 + mConstants.APP_STANDBY_WINDOW; + } + if (alarm.expectedWhenElapsed < minElapsed) { + alarm.whenElapsed = alarm.maxWhenElapsed = minElapsed; + deferred = true; + } + } + if (!deferred) { // Restore original requirements in case they were changed earlier. alarm.whenElapsed = alarm.expectedWhenElapsed; alarm.maxWhenElapsed = alarm.expectedMaxWhenElapsed; } + } else { + // Minimum delay deferring implementation: + final long lastElapsed = mAppWakeupHistory.getLastWakeupForPackage(sourcePackage, + sourceUserId, 1); + if (lastElapsed > 0) { + final long minElapsed = lastElapsed + getMinDelayForBucketLocked(standbyBucket); + if (alarm.expectedWhenElapsed < minElapsed) { + alarm.whenElapsed = alarm.maxWhenElapsed = minElapsed; + } else { + // app is now eligible to run alarms at the originally requested window. + // Restore original requirements in case they were changed earlier. + alarm.whenElapsed = alarm.expectedWhenElapsed; + alarm.maxWhenElapsed = alarm.expectedMaxWhenElapsed; + } + } } return (oldWhenElapsed != alarm.whenElapsed || oldMaxWhenElapsed != alarm.maxWhenElapsed); } @@ -1696,7 +1905,7 @@ class AlarmManagerService extends SystemService { mAllowWhileIdleDispatches.add(ent); } } - adjustDeliveryTimeBasedOnStandbyBucketLocked(a); + adjustDeliveryTimeBasedOnBucketLocked(a); insertAndBatchAlarmLocked(a); if (a.alarmClock != null) { @@ -1915,7 +2124,7 @@ class AlarmManagerService extends SystemService { void dumpImpl(PrintWriter pw) { synchronized (mLock) { pw.println("Current Alarm Manager state:"); - mConstants.dump(pw); + mConstants.dump(pw, " "); pw.println(); if (mAppStateTracker != null) { @@ -2065,14 +2274,7 @@ class AlarmManagerService extends SystemService { pw.println(" none"); } - pw.println(" mLastAlarmDeliveredForPackage:"); - for (int i = 0; i < mLastAlarmDeliveredForPackage.size(); i++) { - Pair<String, Integer> packageUser = mLastAlarmDeliveredForPackage.keyAt(i); - pw.print(" Package " + packageUser.first + ", User " + packageUser.second + ":"); - TimeUtils.formatDuration(mLastAlarmDeliveredForPackage.valueAt(i), nowELAPSED, pw); - pw.println(); - } - pw.println(); + mAppWakeupHistory.dump(pw, " ", nowELAPSED); if (mPendingIdleUntil != null || mPendingWhileIdleAlarms.size() > 0) { pw.println(); @@ -3862,6 +4064,7 @@ class AlarmManagerService extends SystemService { obtainMessage(REMOVE_FOR_STOPPED, uid, 0).sendToTarget(); } + @Override public void handleMessage(Message msg) { switch (msg.what) { case ALARM_EVENT: { @@ -4030,64 +4233,57 @@ class AlarmManagerService extends SystemService { public void onReceive(Context context, Intent intent) { final int uid = intent.getIntExtra(Intent.EXTRA_UID, -1); synchronized (mLock) { - String action = intent.getAction(); String pkgList[] = null; - if (Intent.ACTION_QUERY_PACKAGE_RESTART.equals(action)) { - pkgList = intent.getStringArrayExtra(Intent.EXTRA_PACKAGES); - for (String packageName : pkgList) { - if (lookForPackageLocked(packageName)) { - setResultCode(Activity.RESULT_OK); - return; - } - } - return; - } else if (Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE.equals(action)) { - pkgList = intent.getStringArrayExtra(Intent.EXTRA_CHANGED_PACKAGE_LIST); - } else if (Intent.ACTION_USER_STOPPED.equals(action)) { - int userHandle = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1); - if (userHandle >= 0) { - removeUserLocked(userHandle); - for (int i = mLastAlarmDeliveredForPackage.size() - 1; i >= 0; i--) { - final Pair<String, Integer> packageUser = - mLastAlarmDeliveredForPackage.keyAt(i); - if (packageUser.second == userHandle) { - mLastAlarmDeliveredForPackage.removeAt(i); + switch (intent.getAction()) { + case Intent.ACTION_QUERY_PACKAGE_RESTART: + pkgList = intent.getStringArrayExtra(Intent.EXTRA_PACKAGES); + for (String packageName : pkgList) { + if (lookForPackageLocked(packageName)) { + setResultCode(Activity.RESULT_OK); + return; } } - } - } else if (Intent.ACTION_UID_REMOVED.equals(action)) { - if (uid >= 0) { - mLastAllowWhileIdleDispatch.delete(uid); - mUseAllowWhileIdleShortTime.delete(uid); - } - } else { - if (Intent.ACTION_PACKAGE_REMOVED.equals(action) - && intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) { - // This package is being updated; don't kill its alarms. return; - } - Uri data = intent.getData(); - if (data != null) { - String pkg = data.getSchemeSpecificPart(); - if (pkg != null) { - pkgList = new String[]{pkg}; + case Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE: + pkgList = intent.getStringArrayExtra(Intent.EXTRA_CHANGED_PACKAGE_LIST); + break; + case Intent.ACTION_USER_STOPPED: + final int userHandle = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1); + if (userHandle >= 0) { + removeUserLocked(userHandle); + mAppWakeupHistory.removeForUser(userHandle); } - } + return; + case Intent.ACTION_UID_REMOVED: + if (uid >= 0) { + mLastAllowWhileIdleDispatch.delete(uid); + mUseAllowWhileIdleShortTime.delete(uid); + } + return; + case Intent.ACTION_PACKAGE_REMOVED: + if (intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) { + // This package is being updated; don't kill its alarms. + return; + } + // Intentional fall-through. + case Intent.ACTION_PACKAGE_RESTARTED: + final Uri data = intent.getData(); + if (data != null) { + final String pkg = data.getSchemeSpecificPart(); + if (pkg != null) { + pkgList = new String[]{pkg}; + } + } + break; } if (pkgList != null && (pkgList.length > 0)) { - for (int i = mLastAlarmDeliveredForPackage.size() - 1; i >= 0; i--) { - Pair<String, Integer> packageUser = mLastAlarmDeliveredForPackage.keyAt(i); - if (ArrayUtils.contains(pkgList, packageUser.first) - && packageUser.second == UserHandle.getUserId(uid)) { - mLastAlarmDeliveredForPackage.removeAt(i); - } - } for (String pkg : pkgList) { if (uid >= 0) { - // package-removed case + // package-removed and package-restarted case + mAppWakeupHistory.removeForPackage(pkg, UserHandle.getUserId(uid)); removeLocked(uid); } else { - // external-applications-unavailable etc case + // external-applications-unavailable case removeLocked(pkg); } mPriorities.remove(pkg); @@ -4131,7 +4327,8 @@ class AlarmManagerService extends SystemService { /** * Tracking of app assignments to standby buckets */ - final class AppStandbyTracker extends UsageStatsManagerInternal.AppIdleStateChangeListener { + private final class AppStandbyTracker extends + UsageStatsManagerInternal.AppIdleStateChangeListener { @Override public void onAppIdleStateChanged(final String packageName, final @UserIdInt int userId, boolean idle, int bucket, int reason) { @@ -4474,7 +4671,8 @@ class AlarmManagerService extends SystemService { if (!isExemptFromAppStandby(alarm)) { final Pair<String, Integer> packageUser = Pair.create(alarm.sourcePackage, UserHandle.getUserId(alarm.creatorUid)); - mLastAlarmDeliveredForPackage.put(packageUser, nowELAPSED); + mAppWakeupHistory.recordAlarmForPackage(alarm.sourcePackage, + UserHandle.getUserId(alarm.creatorUid), nowELAPSED); } final BroadcastStats bs = inflight.mBroadcastStats; diff --git a/services/core/java/com/android/server/TEST_MAPPING b/services/core/java/com/android/server/TEST_MAPPING index 16b12f1f1d68..1870f8d95977 100644 --- a/services/core/java/com/android/server/TEST_MAPPING +++ b/services/core/java/com/android/server/TEST_MAPPING @@ -2,6 +2,7 @@ "presubmit": [ { "name": "FrameworksMockingServicesTests", + "file_patterns": ["AlarmManagerService\\.java"], "options": [ { "include-annotation": "android.platform.test.annotations.Presubmit" diff --git a/services/tests/mockingservicestests/src/com/android/server/AlarmManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/AlarmManagerServiceTest.java index 6a153d5346ed..6386b3b396ae 100644 --- a/services/tests/mockingservicestests/src/com/android/server/AlarmManagerServiceTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/AlarmManagerServiceTest.java @@ -28,14 +28,18 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSess import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; import static com.android.dx.mockito.inline.extended.ExtendedMockito.when; +import static com.android.server.AlarmManagerService.ACTIVE_INDEX; +import static com.android.server.AlarmManagerService.AlarmHandler.APP_STANDBY_BUCKET_CHANGED; +import static com.android.server.AlarmManagerService.AlarmHandler.APP_STANDBY_PAROLE_CHANGED; import static com.android.server.AlarmManagerService.Constants.KEY_ALLOW_WHILE_IDLE_LONG_TIME; import static com.android.server.AlarmManagerService.Constants.KEY_ALLOW_WHILE_IDLE_SHORT_TIME; -import static com.android.server.AlarmManagerService.Constants - .KEY_ALLOW_WHILE_IDLE_WHITELIST_DURATION; +import static com.android.server.AlarmManagerService.Constants.KEY_ALLOW_WHILE_IDLE_WHITELIST_DURATION; +import static com.android.server.AlarmManagerService.Constants.KEY_APP_STANDBY_QUOTAS_ENABLED; import static com.android.server.AlarmManagerService.Constants.KEY_LISTENER_TIMEOUT; import static com.android.server.AlarmManagerService.Constants.KEY_MAX_INTERVAL; import static com.android.server.AlarmManagerService.Constants.KEY_MIN_FUTURITY; import static com.android.server.AlarmManagerService.Constants.KEY_MIN_INTERVAL; +import static com.android.server.AlarmManagerService.WORKING_INDEX; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; @@ -58,6 +62,7 @@ import android.content.Context; import android.content.Intent; import android.os.Handler; import android.os.Looper; +import android.os.Message; import android.os.PowerManager; import android.os.UserHandle; import android.platform.test.annotations.Presubmit; @@ -65,7 +70,6 @@ import android.provider.Settings; import android.util.Log; import android.util.SparseArray; -import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; import com.android.internal.annotations.GuardedBy; @@ -83,7 +87,6 @@ import org.mockito.quality.Strictness; import java.util.ArrayList; @Presubmit -@SmallTest @RunWith(AndroidJUnit4.class) public class AlarmManagerServiceTest { private static final String TAG = AlarmManagerServiceTest.class.getSimpleName(); @@ -91,7 +94,9 @@ public class AlarmManagerServiceTest { private static final int SYSTEM_UI_UID = 123456789; private static final int TEST_CALLING_UID = 12345; + private long mAppStandbyWindow; private AlarmManagerService mService; + private UsageStatsManagerInternal.AppIdleStateChangeListener mAppStandbyListener; @Mock private ContentResolver mMockResolver; @Mock @@ -229,16 +234,23 @@ public class AlarmManagerServiceTest { mService = new AlarmManagerService(mMockContext, mInjector); spyOn(mService); doNothing().when(mService).publishBinderService(any(), any()); + mService.onStart(); - mService.onBootPhase(SystemService.PHASE_SYSTEM_SERVICES_READY); spyOn(mService.mHandler); - - assertEquals(0, mService.mConstants.MIN_FUTURITY); assertEquals(mService.mSystemUiUid, SYSTEM_UI_UID); assertEquals(mService.mClockReceiver, mClockReceiver); assertEquals(mService.mWakeLock, mWakeLock); verify(mIActivityManager).registerUidObserver(any(IUidObserver.class), anyInt(), anyInt(), isNull()); + + // Other boot phases don't matter + mService.onBootPhase(SystemService.PHASE_SYSTEM_SERVICES_READY); + assertEquals(0, mService.mConstants.MIN_FUTURITY); + mAppStandbyWindow = mService.mConstants.APP_STANDBY_WINDOW; + ArgumentCaptor<UsageStatsManagerInternal.AppIdleStateChangeListener> captor = + ArgumentCaptor.forClass(UsageStatsManagerInternal.AppIdleStateChangeListener.class); + verify(mUsageStatsManagerInternal).addAppIdleStateChangeListener(captor.capture()); + mAppStandbyListener = captor.getValue(); } private void setTestAlarm(int type, long triggerTime, PendingIntent operation) { @@ -254,6 +266,28 @@ public class AlarmManagerServiceTest { return mockPi; } + /** + * Careful while calling as this will replace any existing settings for the calling test. + */ + private void setQuotasEnabled(boolean enabled) { + final StringBuilder constantsBuilder = new StringBuilder(); + constantsBuilder.append(KEY_MIN_FUTURITY); + constantsBuilder.append("=0,"); + // Capping active and working quotas to make testing feasible. + constantsBuilder.append(mService.mConstants.KEYS_APP_STANDBY_QUOTAS[ACTIVE_INDEX]); + constantsBuilder.append("=8,"); + constantsBuilder.append(mService.mConstants.KEYS_APP_STANDBY_QUOTAS[WORKING_INDEX]); + constantsBuilder.append("=5,"); + if (!enabled) { + constantsBuilder.append(KEY_APP_STANDBY_QUOTAS_ENABLED); + constantsBuilder.append("=false,"); + } + doReturn(constantsBuilder.toString()).when(() -> Settings.Global.getString(mMockResolver, + Settings.Global.ALARM_MANAGER_CONSTANTS)); + mService.mConstants.onChange(false, null); + assertEquals(mService.mConstants.APP_STANDBY_QUOTAS_ENABLED, enabled); + } + @Test public void testSingleAlarmSet() { final long triggerTime = mNowElapsedTest + 5000; @@ -346,6 +380,7 @@ public class AlarmManagerServiceTest { @Test public void testStandbyBucketDelay_workingSet() throws Exception { + setQuotasEnabled(false); setTestAlarm(ELAPSED_REALTIME_WAKEUP, mNowElapsedTest + 5, getNewMockPendingIntent()); setTestAlarm(ELAPSED_REALTIME_WAKEUP, mNowElapsedTest + 6, getNewMockPendingIntent()); assertEquals(mNowElapsedTest + 5, mTestTimer.getElapsed()); @@ -366,6 +401,7 @@ public class AlarmManagerServiceTest { @Test public void testStandbyBucketDelay_frequent() throws Exception { + setQuotasEnabled(false); setTestAlarm(ELAPSED_REALTIME_WAKEUP, mNowElapsedTest + 5, getNewMockPendingIntent()); setTestAlarm(ELAPSED_REALTIME_WAKEUP, mNowElapsedTest + 6, getNewMockPendingIntent()); assertEquals(mNowElapsedTest + 5, mTestTimer.getElapsed()); @@ -385,6 +421,7 @@ public class AlarmManagerServiceTest { @Test public void testStandbyBucketDelay_rare() throws Exception { + setQuotasEnabled(false); setTestAlarm(ELAPSED_REALTIME_WAKEUP, mNowElapsedTest + 5, getNewMockPendingIntent()); setTestAlarm(ELAPSED_REALTIME_WAKEUP, mNowElapsedTest + 6, getNewMockPendingIntent()); assertEquals(mNowElapsedTest + 5, mTestTimer.getElapsed()); @@ -402,6 +439,253 @@ public class AlarmManagerServiceTest { assertEquals("Incorrect next alarm trigger.", expectedNextTrigger, mTestTimer.getElapsed()); } + private void testQuotasDeferralOnSet(int standbyBucket) throws Exception { + final int quota = mService.getQuotaForBucketLocked(standbyBucket); + when(mUsageStatsManagerInternal.getAppStandbyBucket(eq(TEST_CALLING_PACKAGE), anyInt(), + anyLong())).thenReturn(standbyBucket); + final long firstTrigger = mNowElapsedTest + 10; + for (int i = 0; i < quota; i++) { + setTestAlarm(ELAPSED_REALTIME_WAKEUP, mNowElapsedTest + 10 + i, + getNewMockPendingIntent()); + mNowElapsedTest = mTestTimer.getElapsed(); + mTestTimer.expire(); + } + // This one should get deferred on set + setTestAlarm(ELAPSED_REALTIME_WAKEUP, mNowElapsedTest + quota + 10, + getNewMockPendingIntent()); + final long expectedNextTrigger = firstTrigger + 1 + mAppStandbyWindow; + assertEquals("Incorrect next alarm trigger", expectedNextTrigger, mTestTimer.getElapsed()); + } + + private void testQuotasDeferralOnExpiration(int standbyBucket) throws Exception { + final int quota = mService.getQuotaForBucketLocked(standbyBucket); + when(mUsageStatsManagerInternal.getAppStandbyBucket(eq(TEST_CALLING_PACKAGE), anyInt(), + anyLong())).thenReturn(standbyBucket); + final long firstTrigger = mNowElapsedTest + 10; + for (int i = 0; i < quota; i++) { + setTestAlarm(ELAPSED_REALTIME_WAKEUP, mNowElapsedTest + 10 + i, + getNewMockPendingIntent()); + } + // This one should get deferred after the latest alarm expires + setTestAlarm(ELAPSED_REALTIME_WAKEUP, mNowElapsedTest + quota + 10, + getNewMockPendingIntent()); + for (int i = 0; i < quota; i++) { + mNowElapsedTest = mTestTimer.getElapsed(); + mTestTimer.expire(); + } + final long expectedNextTrigger = firstTrigger + 1 + mAppStandbyWindow; + assertEquals("Incorrect next alarm trigger", expectedNextTrigger, mTestTimer.getElapsed()); + } + + private void testQuotasNoDeferral(int standbyBucket) throws Exception { + final int quota = mService.getQuotaForBucketLocked(standbyBucket); + when(mUsageStatsManagerInternal.getAppStandbyBucket(eq(TEST_CALLING_PACKAGE), anyInt(), + anyLong())).thenReturn(standbyBucket); + final long firstTrigger = mNowElapsedTest + 10; + for (int i = 0; i < quota; i++) { + setTestAlarm(ELAPSED_REALTIME_WAKEUP, mNowElapsedTest + 10 + i, + getNewMockPendingIntent()); + } + // This delivery time maintains the quota invariant. Should not be deferred. + final long expectedNextTrigger = firstTrigger + mAppStandbyWindow + 5; + setTestAlarm(ELAPSED_REALTIME_WAKEUP, expectedNextTrigger, getNewMockPendingIntent()); + for (int i = 0; i < quota; i++) { + mNowElapsedTest = mTestTimer.getElapsed(); + mTestTimer.expire(); + } + assertEquals("Incorrect next alarm trigger", expectedNextTrigger, mTestTimer.getElapsed()); + } + + @Test + public void testActiveQuota_deferredOnSet() throws Exception { + setQuotasEnabled(true); + testQuotasDeferralOnSet(STANDBY_BUCKET_ACTIVE); + } + + @Test + public void testActiveQuota_deferredOnExpiration() throws Exception { + setQuotasEnabled(true); + testQuotasDeferralOnExpiration(STANDBY_BUCKET_ACTIVE); + } + + @Test + public void testActiveQuota_notDeferred() throws Exception { + setQuotasEnabled(true); + testQuotasNoDeferral(STANDBY_BUCKET_ACTIVE); + } + + @Test + public void testWorkingQuota_deferredOnSet() throws Exception { + setQuotasEnabled(true); + testQuotasDeferralOnSet(STANDBY_BUCKET_WORKING_SET); + } + + @Test + public void testWorkingQuota_deferredOnExpiration() throws Exception { + setQuotasEnabled(true); + testQuotasDeferralOnExpiration(STANDBY_BUCKET_WORKING_SET); + } + + @Test + public void testWorkingQuota_notDeferred() throws Exception { + setQuotasEnabled(true); + testQuotasNoDeferral(STANDBY_BUCKET_WORKING_SET); + } + + @Test + public void testFrequentQuota_deferredOnSet() throws Exception { + setQuotasEnabled(true); + testQuotasDeferralOnSet(STANDBY_BUCKET_FREQUENT); + } + + @Test + public void testFrequentQuota_deferredOnExpiration() throws Exception { + setQuotasEnabled(true); + testQuotasDeferralOnExpiration(STANDBY_BUCKET_FREQUENT); + } + + @Test + public void testFrequentQuota_notDeferred() throws Exception { + setQuotasEnabled(true); + testQuotasNoDeferral(STANDBY_BUCKET_FREQUENT); + } + + @Test + public void testRareQuota_deferredOnSet() throws Exception { + setQuotasEnabled(true); + testQuotasDeferralOnSet(STANDBY_BUCKET_RARE); + } + + @Test + public void testRareQuota_deferredOnExpiration() throws Exception { + setQuotasEnabled(true); + testQuotasDeferralOnExpiration(STANDBY_BUCKET_RARE); + } + + @Test + public void testRareQuota_notDeferred() throws Exception { + setQuotasEnabled(true); + testQuotasNoDeferral(STANDBY_BUCKET_RARE); + } + + private void sendAndHandleBucketChanged(int bucket) { + when(mUsageStatsManagerInternal.getAppStandbyBucket(eq(TEST_CALLING_PACKAGE), anyInt(), + anyLong())).thenReturn(bucket); + // Stubbing the handler call to simulate it synchronously here. + doReturn(true).when(mService.mHandler).sendMessage(any(Message.class)); + mAppStandbyListener.onAppIdleStateChanged(TEST_CALLING_PACKAGE, + UserHandle.getUserId(TEST_CALLING_UID), false, bucket, 0); + final ArgumentCaptor<Message> messageCaptor = ArgumentCaptor.forClass(Message.class); + verify(mService.mHandler, atLeastOnce()).sendMessage(messageCaptor.capture()); + final Message lastMessage = messageCaptor.getValue(); + assertEquals("Unexpected message send to handler", lastMessage.what, + APP_STANDBY_BUCKET_CHANGED); + mService.mHandler.handleMessage(lastMessage); + } + + @Test + public void testQuotaDowngrade() throws Exception { + setQuotasEnabled(true); + final int workingQuota = mService.getQuotaForBucketLocked(STANDBY_BUCKET_WORKING_SET); + when(mUsageStatsManagerInternal.getAppStandbyBucket(eq(TEST_CALLING_PACKAGE), anyInt(), + anyLong())).thenReturn(STANDBY_BUCKET_WORKING_SET); + + final long firstTrigger = mNowElapsedTest + 10; + for (int i = 0; i < workingQuota; i++) { + setTestAlarm(ELAPSED_REALTIME_WAKEUP, firstTrigger + i, getNewMockPendingIntent()); + } + // No deferrals now. + for (int i = 0; i < workingQuota - 1; i++) { + mNowElapsedTest = mTestTimer.getElapsed(); + assertEquals(firstTrigger + i, mNowElapsedTest); + mTestTimer.expire(); + } + // The next upcoming alarm in queue should also be set as expected. + assertEquals(firstTrigger + workingQuota - 1, mTestTimer.getElapsed()); + // Downgrading the bucket now + sendAndHandleBucketChanged(STANDBY_BUCKET_RARE); + final int rareQuota = mService.getQuotaForBucketLocked(STANDBY_BUCKET_RARE); + // The last alarm should now be deferred. + final long expectedNextTrigger = (firstTrigger + workingQuota - 1 - rareQuota) + + mAppStandbyWindow + 1; + assertEquals("Incorrect next alarm trigger", expectedNextTrigger, mTestTimer.getElapsed()); + } + + @Test + public void testQuotaUpgrade() throws Exception { + setQuotasEnabled(true); + final int frequentQuota = mService.getQuotaForBucketLocked(STANDBY_BUCKET_FREQUENT); + when(mUsageStatsManagerInternal.getAppStandbyBucket(eq(TEST_CALLING_PACKAGE), anyInt(), + anyLong())).thenReturn(STANDBY_BUCKET_FREQUENT); + + final long firstTrigger = mNowElapsedTest + 10; + for (int i = 0; i < frequentQuota + 1; i++) { + setTestAlarm(ELAPSED_REALTIME_WAKEUP, firstTrigger + i, getNewMockPendingIntent()); + if (i < frequentQuota) { + mNowElapsedTest = mTestTimer.getElapsed(); + mTestTimer.expire(); + } + } + // The last alarm should be deferred due to exceeding the quota + final long deferredTrigger = firstTrigger + 1 + mAppStandbyWindow; + assertEquals(deferredTrigger, mTestTimer.getElapsed()); + + // Upgrading the bucket now + sendAndHandleBucketChanged(STANDBY_BUCKET_ACTIVE); + // The last alarm should now be rescheduled to go as per original expectations + final long originalTrigger = firstTrigger + frequentQuota; + assertEquals("Incorrect next alarm trigger", originalTrigger, mTestTimer.getElapsed()); + } + + private void sendAndHandleParoleChanged(boolean parole) { + // Stubbing the handler call to simulate it synchronously here. + doReturn(true).when(mService.mHandler).sendMessage(any(Message.class)); + mAppStandbyListener.onParoleStateChanged(parole); + final ArgumentCaptor<Message> messageCaptor = ArgumentCaptor.forClass(Message.class); + verify(mService.mHandler, atLeastOnce()).sendMessage(messageCaptor.capture()); + final Message lastMessage = messageCaptor.getValue(); + assertEquals("Unexpected message send to handler", lastMessage.what, + APP_STANDBY_PAROLE_CHANGED); + mService.mHandler.handleMessage(lastMessage); + } + + @Test + public void testParole() throws Exception { + setQuotasEnabled(true); + final int workingQuota = mService.getQuotaForBucketLocked(STANDBY_BUCKET_WORKING_SET); + when(mUsageStatsManagerInternal.getAppStandbyBucket(eq(TEST_CALLING_PACKAGE), anyInt(), + anyLong())).thenReturn(STANDBY_BUCKET_WORKING_SET); + + final long firstTrigger = mNowElapsedTest + 10; + final int totalAlarms = workingQuota + 10; + for (int i = 0; i < totalAlarms; i++) { + setTestAlarm(ELAPSED_REALTIME_WAKEUP, firstTrigger + i, getNewMockPendingIntent()); + } + // Use up the quota, no deferrals expected. + for (int i = 0; i < workingQuota; i++) { + mNowElapsedTest = mTestTimer.getElapsed(); + assertEquals(firstTrigger + i, mNowElapsedTest); + mTestTimer.expire(); + } + // Any subsequent alarms in queue should all be deferred + assertEquals(firstTrigger + mAppStandbyWindow + 1, mTestTimer.getElapsed()); + // Paroling now + sendAndHandleParoleChanged(true); + + // Subsequent alarms should now go off as per original expectations. + for (int i = 0; i < 5; i++) { + mNowElapsedTest = mTestTimer.getElapsed(); + assertEquals(firstTrigger + workingQuota + i, mNowElapsedTest); + mTestTimer.expire(); + } + // Come out of parole + sendAndHandleParoleChanged(false); + + // Subsequent alarms should again get deferred + final long expectedNextTrigger = (firstTrigger + 5) + 1 + mAppStandbyWindow; + assertEquals("Incorrect next alarm trigger", expectedNextTrigger, mTestTimer.getElapsed()); + } + @Test public void testAlarmRestrictedInBatterSaver() throws Exception { final ArgumentCaptor<AppStateTracker.Listener> listenerArgumentCaptor = |