From 5c46b18ddb769444657d6be251a82f1a3d680fb9 Mon Sep 17 00:00:00 2001 From: Suprabh Shukla Date: Thu, 29 Oct 2020 16:31:01 -0700 Subject: [PATCH] Introducing a lazy alarm store Lazily batching alarms should perform better. A flag has been added to switch the implementation at runtime to be able to experiment and measure the differences. Test: atest CtsAppTestCases:android.app.cts.AlarmManagerTest atest CtsAlarmManagerTestCases atest FrameworksMockingServicesTests:com.android.server.alarm Bug: 161497385 Change-Id: I240f4f3c5439d39b3a000ab47c512d2b0bd14f6f --- .../server/alarm/AlarmManagerService.java | 44 +++- .../com/android/server/alarm/AlarmStore.java | 15 +- .../server/alarm/BatchingAlarmStore.java | 59 ++--- .../android/server/alarm/LazyAlarmStore.java | 217 ++++++++++++++++++ .../server/alarm/alarmmanagerservice.proto | 2 + .../server/alarm/AlarmManagerServiceTest.java | 39 +++- .../android/server/alarm/AlarmStoreTest.java | 55 ++++- 7 files changed, 379 insertions(+), 52 deletions(-) create mode 100644 apex/jobscheduler/service/java/com/android/server/alarm/LazyAlarmStore.java 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 c8a04d674739d..ba2a8a34a92be 100644 --- a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java +++ b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java @@ -268,6 +268,7 @@ public class AlarmManagerService extends SystemService { */ Bundle mIdleOptions; + // TODO(b/172085676): Move inside alarm store. private final SparseArray mNextAlarmClockForUser = new SparseArray<>(); private final SparseArray mTmpSparseAlarmClockArray = @@ -276,6 +277,9 @@ public class AlarmManagerService extends SystemService { new SparseBooleanArray(); private boolean mNextAlarmClockMayChange; + @GuardedBy("mLock") + private final Runnable mAlarmClockUpdater = () -> mNextAlarmClockMayChange = true; + // May only use on mHandler's thread, locking not required. private final SparseArray mHandlerSparseAlarmClockArray = new SparseArray<>(); @@ -410,6 +414,9 @@ public class AlarmManagerService extends SystemService { private static final String KEY_APP_STANDBY_RESTRICTED_WINDOW = "app_standby_restricted_window"; + @VisibleForTesting + static final String KEY_LAZY_BATCHING = "lazy_batching"; + private static final long DEFAULT_MIN_FUTURITY = 5 * 1000; private static final long DEFAULT_MIN_INTERVAL = 60 * 1000; private static final long DEFAULT_MAX_INTERVAL = 365 * DateUtils.DAY_IN_MILLIS; @@ -432,6 +439,8 @@ public class AlarmManagerService extends SystemService { private static final int DEFAULT_APP_STANDBY_RESTRICTED_QUOTA = 1; private static final long DEFAULT_APP_STANDBY_RESTRICTED_WINDOW = MILLIS_IN_DAY; + private static final boolean DEFAULT_LAZY_BATCHING = false; + // Minimum futurity of a new alarm public long MIN_FUTURITY = DEFAULT_MIN_FUTURITY; @@ -460,6 +469,8 @@ public class AlarmManagerService extends SystemService { public int APP_STANDBY_RESTRICTED_QUOTA = DEFAULT_APP_STANDBY_RESTRICTED_QUOTA; public long APP_STANDBY_RESTRICTED_WINDOW = DEFAULT_APP_STANDBY_RESTRICTED_WINDOW; + public boolean LAZY_BATCHING = DEFAULT_LAZY_BATCHING; + private long mLastAllowWhileIdleWhitelistDuration = -1; Constants() { @@ -538,6 +549,14 @@ public class AlarmManagerService extends SystemService { case KEY_APP_STANDBY_RESTRICTED_WINDOW: updateStandbyWindowsLocked(); break; + case KEY_LAZY_BATCHING: + final boolean oldLazyBatching = LAZY_BATCHING; + LAZY_BATCHING = properties.getBoolean( + KEY_LAZY_BATCHING, DEFAULT_LAZY_BATCHING); + if (oldLazyBatching != LAZY_BATCHING) { + migrateAlarmsToNewStoreLocked(); + } + break; default: if (name.startsWith(KEY_PREFIX_STANDBY_QUOTA) && !standbyQuotaUpdated) { // The quotas need to be updated in order, so we can't just rely @@ -551,6 +570,15 @@ public class AlarmManagerService extends SystemService { } } + private void migrateAlarmsToNewStoreLocked() { + final AlarmStore newStore = LAZY_BATCHING ? new LazyAlarmStore() + : new BatchingAlarmStore(); + final ArrayList allAlarms = mAlarmStore.remove((unused) -> true); + newStore.addAll(allAlarms); + mAlarmStore = newStore; + mAlarmStore.setAlarmClockRemovalListener(mAlarmClockUpdater); + } + private void updateStandbyQuotasLocked() { // The bucket quotas need to be read as an atomic unit but the properties passed to // onPropertiesChanged may only have one key populated at a time. @@ -659,6 +687,9 @@ public class AlarmManagerService extends SystemService { TimeUtils.formatDuration(APP_STANDBY_RESTRICTED_WINDOW, pw); pw.println(); + pw.print(KEY_LAZY_BATCHING, LAZY_BATCHING); + pw.println(); + pw.decreaseIndent(); } @@ -770,7 +801,7 @@ public class AlarmManagerService extends SystemService { // minimum recurrence period or alarm futurity for us to be able to fuzz it static final long MIN_FUZZABLE_INTERVAL = 10000; @GuardedBy("mLock") - final AlarmStore mAlarmStore; + AlarmStore mAlarmStore; // set to non-null if in idle mode; while in this mode, any alarms we don't want // to run during this time are rescehduled to go off after this alarm. @@ -781,7 +812,6 @@ public class AlarmManagerService extends SystemService { AlarmManagerService(Context context, Injector injector) { super(context); mInjector = injector; - mAlarmStore = new BatchingAlarmStore(() -> mNextAlarmClockMayChange = true); } public AlarmManagerService(Context context) { @@ -1219,6 +1249,11 @@ public class AlarmManagerService extends SystemService { synchronized (mLock) { mHandler = new AlarmHandler(); mConstants = new Constants(); + + mAlarmStore = mConstants.LAZY_BATCHING ? new LazyAlarmStore() + : new BatchingAlarmStore(); + mAlarmStore.setAlarmClockRemovalListener(mAlarmClockUpdater); + mAppWakeupHistory = new AppWakeupHistory(Constants.DEFAULT_APP_STANDBY_WINDOW); mNextWakeup = mNextNonWakeup = 0; @@ -3055,12 +3090,13 @@ public class AlarmManagerService extends SystemService { static final void dumpAlarmList(IndentingPrintWriter ipw, ArrayList list, long nowELAPSED, SimpleDateFormat sdf) { - for (int i = list.size() - 1; i >= 0; i--) { + final int n = list.size(); + for (int i = n - 1; i >= 0; i--) { final Alarm a = list.get(i); final String label = Alarm.typeToString(a.type); ipw.print(label); ipw.print(" #"); - ipw.print(i); + ipw.print(n - i); ipw.print(": "); ipw.println(a); ipw.increaseIndent(); diff --git a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmStore.java b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmStore.java index 7a846b9b82db7..0e442d09d5a53 100644 --- a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmStore.java +++ b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmStore.java @@ -39,6 +39,13 @@ public interface AlarmStore { */ void add(Alarm a); + /** + * Adds all the given alarms to this store. + * + * @param alarms The alarms to add. + */ + void addAll(ArrayList alarms); + /** * Removes alarms that pass the given predicate. * @@ -47,12 +54,18 @@ public interface AlarmStore { */ ArrayList remove(Predicate whichAlarms); + /** + * Set a listener to be invoked whenever an alarm clock is removed by a call to + * {@link #remove(Predicate) remove} from this store. + */ + void setAlarmClockRemovalListener(Runnable listener); + /** * Gets the earliest alarm with the flag {@link android.app.AlarmManager#FLAG_WAKE_FROM_IDLE} * based on {@link Alarm#getWhenElapsed()}. * * @return An alarm object matching the description above or {@code null} if no such alarm was - * found. + * found. */ Alarm getNextWakeFromIdleAlarm(); diff --git a/apex/jobscheduler/service/java/com/android/server/alarm/BatchingAlarmStore.java b/apex/jobscheduler/service/java/com/android/server/alarm/BatchingAlarmStore.java index cbfe80bdce249..e7edfb7b56b9f 100644 --- a/apex/jobscheduler/service/java/com/android/server/alarm/BatchingAlarmStore.java +++ b/apex/jobscheduler/service/java/com/android/server/alarm/BatchingAlarmStore.java @@ -41,45 +41,22 @@ import java.util.function.Predicate; */ public class BatchingAlarmStore implements AlarmStore { - private ArrayList mAlarmBatches = new ArrayList<>(); + private final ArrayList mAlarmBatches = new ArrayList<>(); private int mSize; - private AlarmClockRemovalListener mAlarmClockRemovalListener; + private Runnable mOnAlarmClockRemoved; interface Stats { int REBATCH_ALL_ALARMS = 0; } - final StatLogger mStatLogger = new StatLogger("Alarm store stats", new String[]{ + final StatLogger mStatLogger = new StatLogger("BatchingAlarmStore stats", new String[]{ "REBATCH_ALL_ALARMS", }); - private static final Comparator sBatchOrder = (b1, b2) -> { - long when1 = b1.mStart; - long when2 = b2.mStart; - if (when1 > when2) { - return 1; - } - if (when1 < when2) { - return -1; - } - return 0; - }; + private static final Comparator sBatchOrder = Comparator.comparingLong(b -> b.mStart); - private static final Comparator sIncreasingTimeOrder = (a1, a2) -> { - long when1 = a1.getWhenElapsed(); - long when2 = a2.getWhenElapsed(); - if (when1 > when2) { - return 1; - } - if (when1 < when2) { - return -1; - } - return 0; - }; - - BatchingAlarmStore(AlarmClockRemovalListener listener) { - mAlarmClockRemovalListener = listener; - } + private static final Comparator sIncreasingTimeOrder = Comparator.comparingLong( + Alarm::getWhenElapsed); @Override public void add(Alarm a) { @@ -87,6 +64,16 @@ public class BatchingAlarmStore implements AlarmStore { mSize++; } + @Override + public void addAll(ArrayList alarms) { + if (alarms == null) { + return; + } + for (final Alarm a : alarms) { + add(a); + } + } + @Override public ArrayList remove(Predicate whichAlarms) { final ArrayList removed = new ArrayList<>(); @@ -105,6 +92,11 @@ public class BatchingAlarmStore implements AlarmStore { return removed; } + @Override + public void setAlarmClockRemovalListener(Runnable listener) { + mOnAlarmClockRemoved = listener; + } + @Override public Alarm getNextWakeFromIdleAlarm() { for (final Batch batch : mAlarmBatches) { @@ -317,8 +309,8 @@ public class BatchingAlarmStore implements AlarmStore { Alarm alarm = mAlarms.get(i); if (predicate.test(alarm)) { removed.add(mAlarms.remove(i)); - if (alarm.alarmClock != null && mAlarmClockRemovalListener != null) { - mAlarmClockRemovalListener.onRemoved(); + if (alarm.alarmClock != null && mOnAlarmClockRemoved != null) { + mOnAlarmClockRemoved.run(); } if (isTimeTickAlarm(alarm)) { // This code path is not invoked when delivering alarms, only when removing @@ -388,9 +380,4 @@ public class BatchingAlarmStore implements AlarmStore { proto.end(token); } } - - @FunctionalInterface - interface AlarmClockRemovalListener { - void onRemoved(); - } } diff --git a/apex/jobscheduler/service/java/com/android/server/alarm/LazyAlarmStore.java b/apex/jobscheduler/service/java/com/android/server/alarm/LazyAlarmStore.java new file mode 100644 index 0000000000000..8ca14463a3b56 --- /dev/null +++ b/apex/jobscheduler/service/java/com/android/server/alarm/LazyAlarmStore.java @@ -0,0 +1,217 @@ +/* + * Copyright (C) 2020 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 com.android.server.alarm; + +import static com.android.server.alarm.AlarmManagerService.TAG; +import static com.android.server.alarm.AlarmManagerService.dumpAlarmList; +import static com.android.server.alarm.AlarmManagerService.isTimeTickAlarm; + +import android.app.AlarmManager; +import android.util.IndentingPrintWriter; +import android.util.Slog; +import android.util.proto.ProtoOutputStream; + +import com.android.internal.util.StatLogger; + +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.function.Predicate; + +/** + * Lazy implementation of an alarm store. + * This keeps the alarms in a sorted list, and only batches them at the time of delivery. + */ +public class LazyAlarmStore implements AlarmStore { + + private final ArrayList mAlarms = new ArrayList<>(); + private Runnable mOnAlarmClockRemoved; + + interface Stats { + int GET_NEXT_DELIVERY_TIME = 0; + int GET_NEXT_WAKEUP_DELIVERY_TIME = 1; + } + + final StatLogger mStatLogger = new StatLogger("LazyAlarmStore stats", new String[]{ + "GET_NEXT_DELIVERY_TIME", + "GET_NEXT_WAKEUP_DELIVERY_TIME", + }); + + // Decreasing time order because it is more efficient to remove from the tail of an array list. + private static final Comparator sDecreasingTimeOrder = Comparator.comparingLong( + Alarm::getWhenElapsed).reversed(); + + @Override + public void add(Alarm a) { + int index = Collections.binarySearch(mAlarms, a, sDecreasingTimeOrder); + if (index < 0) { + index = 0 - index - 1; + } + mAlarms.add(index, a); + } + + @Override + public void addAll(ArrayList alarms) { + if (alarms == null) { + return; + } + mAlarms.addAll(alarms); + Collections.sort(alarms, sDecreasingTimeOrder); + } + + @Override + public ArrayList remove(Predicate whichAlarms) { + final ArrayList removedAlarms = new ArrayList<>(); + for (int i = mAlarms.size() - 1; i >= 0; i--) { + if (whichAlarms.test(mAlarms.get(i))) { + final Alarm removed = mAlarms.remove(i); + if (removed.alarmClock != null && mOnAlarmClockRemoved != null) { + mOnAlarmClockRemoved.run(); + } + if (isTimeTickAlarm(removed)) { + // This code path is not invoked when delivering alarms, only when removing + // alarms due to the caller cancelling it or getting uninstalled, etc. + Slog.wtf(TAG, "Removed TIME_TICK alarm"); + } + removedAlarms.add(removed); + } + } + return removedAlarms; + } + + @Override + public void setAlarmClockRemovalListener(Runnable listener) { + mOnAlarmClockRemoved = listener; + } + + @Override + public Alarm getNextWakeFromIdleAlarm() { + for (int i = mAlarms.size() - 1; i >= 0; i--) { + final Alarm alarm = mAlarms.get(i); + if ((alarm.flags & AlarmManager.FLAG_WAKE_FROM_IDLE) != 0) { + return alarm; + } + } + return null; + } + + @Override + public int size() { + return mAlarms.size(); + } + + @Override + public long getNextWakeupDeliveryTime() { + final long start = mStatLogger.getTime(); + long nextWakeup = 0; + for (int i = mAlarms.size() - 1; i >= 0; i--) { + final Alarm a = mAlarms.get(i); + if (!a.wakeup) { + continue; + } + if (nextWakeup == 0) { + nextWakeup = a.getMaxWhenElapsed(); + } else { + if (a.getWhenElapsed() > nextWakeup) { + break; + } + nextWakeup = Math.min(nextWakeup, a.getMaxWhenElapsed()); + } + } + mStatLogger.logDurationStat(Stats.GET_NEXT_WAKEUP_DELIVERY_TIME, start); + return nextWakeup; + } + + @Override + public long getNextDeliveryTime() { + final long start = mStatLogger.getTime(); + final int n = mAlarms.size(); + if (n == 0) { + return 0; + } + long nextDelivery = mAlarms.get(n - 1).getMaxWhenElapsed(); + for (int i = n - 2; i >= 0; i--) { + final Alarm a = mAlarms.get(i); + if (a.getWhenElapsed() > nextDelivery) { + break; + } + nextDelivery = Math.min(nextDelivery, a.getMaxWhenElapsed()); + } + mStatLogger.logDurationStat(Stats.GET_NEXT_DELIVERY_TIME, start); + return nextDelivery; + } + + @Override + public ArrayList removePendingAlarms(long nowElapsed) { + final ArrayList pending = new ArrayList<>(); + final ArrayList standAlones = new ArrayList<>(); + + for (int i = mAlarms.size() - 1; i >= 0; i--) { + final Alarm alarm = mAlarms.get(i); + if (alarm.getWhenElapsed() > nowElapsed) { + break; + } + pending.add(alarm); + if ((alarm.flags & AlarmManager.FLAG_STANDALONE) != 0) { + standAlones.add(alarm); + } + } + if (!standAlones.isEmpty()) { + // If there are deliverable standalone alarms, others must not go out yet. + mAlarms.removeAll(standAlones); + return standAlones; + } + mAlarms.removeAll(pending); + return pending; + } + + @Override + public boolean updateAlarmDeliveries(AlarmDeliveryCalculator deliveryCalculator) { + boolean changed = false; + for (final Alarm alarm : mAlarms) { + changed |= deliveryCalculator.updateAlarmDelivery(alarm); + } + if (changed) { + Collections.sort(mAlarms, sDecreasingTimeOrder); + } + return changed; + } + + @Override + public ArrayList asList() { + final ArrayList copy = new ArrayList<>(mAlarms); + Collections.reverse(copy); + return copy; + } + + @Override + public void dump(IndentingPrintWriter ipw, long nowElapsed, SimpleDateFormat sdf) { + ipw.println(mAlarms.size() + " pending alarms: "); + ipw.increaseIndent(); + dumpAlarmList(ipw, mAlarms, nowElapsed, sdf); + ipw.decreaseIndent(); + mStatLogger.dump(ipw); + } + + @Override + public void dumpProto(ProtoOutputStream pos, long nowElapsed) { + for (final Alarm a : mAlarms) { + a.dumpDebug(pos, AlarmManagerServiceDumpProto.PENDING_ALARMS, nowElapsed); + } + } +} diff --git a/core/proto/android/server/alarm/alarmmanagerservice.proto b/core/proto/android/server/alarm/alarmmanagerservice.proto index e1240245d20f9..8fe1bfc81702d 100644 --- a/core/proto/android/server/alarm/alarmmanagerservice.proto +++ b/core/proto/android/server/alarm/alarmmanagerservice.proto @@ -144,6 +144,8 @@ message AlarmManagerServiceDumpProto { repeated IdleDispatchEntryProto allow_while_idle_dispatches = 40; repeated WakeupEventProto recent_wakeup_history = 41; + + repeated AlarmProto pending_alarms = 42; } // This is a soft wrapper for alarm clock information. It is not representative 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 db4aba519a6a2..8e4942e1ad5c6 100644 --- a/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java @@ -47,6 +47,7 @@ import static com.android.server.alarm.AlarmManagerService.AlarmHandler.REMOVE_F import static com.android.server.alarm.AlarmManagerService.Constants.KEY_ALLOW_WHILE_IDLE_LONG_TIME; import static com.android.server.alarm.AlarmManagerService.Constants.KEY_ALLOW_WHILE_IDLE_SHORT_TIME; import static com.android.server.alarm.AlarmManagerService.Constants.KEY_ALLOW_WHILE_IDLE_WHITELIST_DURATION; +import static com.android.server.alarm.AlarmManagerService.Constants.KEY_LAZY_BATCHING; import static com.android.server.alarm.AlarmManagerService.Constants.KEY_LISTENER_TIMEOUT; import static com.android.server.alarm.AlarmManagerService.Constants.KEY_MAX_INTERVAL; import static com.android.server.alarm.AlarmManagerService.Constants.KEY_MIN_FUTURITY; @@ -62,6 +63,7 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyString; @@ -365,7 +367,7 @@ public class AlarmManagerServiceTest { } private void setIdleUntilAlarm(int type, long triggerTime, PendingIntent pi) { - setTestAlarm(type, triggerTime, pi, 0, FLAG_IDLE_UNTIL, TEST_CALLING_UID); + setTestAlarm(type, triggerTime, pi, 0, FLAG_IDLE_UNTIL | FLAG_STANDALONE, TEST_CALLING_UID); } private void setWakeFromIdle(int type, long triggerTime, PendingIntent pi) { @@ -410,6 +412,12 @@ public class AlarmManagerServiceTest { mService.mConstants.onPropertiesChanged(mDeviceConfigProperties); } + private void setDeviceConfigBoolean(String key, boolean val) { + mDeviceConfigKeys.add(key); + doReturn(val).when(mDeviceConfigProperties).getBoolean(eq(key), anyBoolean()); + mService.mConstants.onPropertiesChanged(mDeviceConfigProperties); + } + /** * Lowers quotas to make testing feasible. Careful while calling as this will replace any * existing settings for the calling test. @@ -1382,6 +1390,35 @@ public class AlarmManagerServiceTest { } } + @Test + public void alarmStoreMigration() { + setDeviceConfigBoolean(KEY_LAZY_BATCHING, false); + final int numAlarms = 10; + final PendingIntent[] pis = new PendingIntent[numAlarms]; + for (int i = 0; i < numAlarms; i++) { + pis[i] = getNewMockPendingIntent(); + setTestAlarm(ELAPSED_REALTIME, mNowElapsedTest + i + 1, pis[i]); + } + + final ArrayList alarmsBefore = mService.mAlarmStore.asList(); + assertEquals(numAlarms, alarmsBefore.size()); + for (int i = 0; i < numAlarms; i++) { + final PendingIntent pi = pis[i]; + assertTrue(i + "th PendingIntent missing: ", + alarmsBefore.removeIf(a -> a.matches(pi, null))); + } + + setDeviceConfigBoolean(KEY_LAZY_BATCHING, true); + + final ArrayList alarmsAfter = mService.mAlarmStore.asList(); + assertEquals(numAlarms, alarmsAfter.size()); + for (int i = 0; i < numAlarms; i++) { + final PendingIntent pi = pis[i]; + assertTrue(i + "th PendingIntent missing: ", + alarmsAfter.removeIf(a -> a.matches(pi, null))); + } + } + @After public void tearDown() { if (mMockingSession != null) { diff --git a/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmStoreTest.java b/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmStoreTest.java index c4fc61a5aa6e3..42fa3d480046c 100644 --- a/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmStoreTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmStoreTest.java @@ -16,6 +16,9 @@ package com.android.server.alarm; +import static android.app.AlarmManager.ELAPSED_REALTIME; +import static android.app.AlarmManager.ELAPSED_REALTIME_WAKEUP; + import static com.android.server.alarm.Constants.TEST_CALLING_PACKAGE; import static com.android.server.alarm.Constants.TEST_CALLING_UID; @@ -23,35 +26,50 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; import android.app.AlarmManager; import android.app.PendingIntent; import android.platform.test.annotations.Presubmit; -import androidx.test.runner.AndroidJUnit4; - -import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; +import org.junit.runners.Parameterized.Parameters; import java.util.ArrayList; @Presubmit -@RunWith(AndroidJUnit4.class) +@RunWith(Parameterized.class) public class AlarmStoreTest { - private AlarmStore mAlarmStore; - @Before - public void setUp() { - mAlarmStore = new BatchingAlarmStore(null); + @Parameter + public AlarmStore mAlarmStore; + + @Parameters + public static Object[] stores() { + return new AlarmStore[]{ + new LazyAlarmStore(), + new BatchingAlarmStore(), + }; } private static Alarm createAlarm(long whenElapsed, long windowLength) { - return createAlarm(AlarmManager.ELAPSED_REALTIME, whenElapsed, windowLength, 0); + return createAlarm(ELAPSED_REALTIME, whenElapsed, windowLength, 0); } private static Alarm createWakeupAlarm(long whenElapsed, long windowLength, int flags) { - return createAlarm(AlarmManager.ELAPSED_REALTIME_WAKEUP, whenElapsed, windowLength, flags); + return createAlarm(ELAPSED_REALTIME_WAKEUP, whenElapsed, windowLength, flags); + } + + private static Alarm createAlarmClock(long whenElapsed) { + final AlarmManager.AlarmClockInfo info = new AlarmManager.AlarmClockInfo(whenElapsed, + mock(PendingIntent.class)); + return new Alarm(ELAPSED_REALTIME_WAKEUP, whenElapsed, whenElapsed, 0, 0, + mock(PendingIntent.class), null, null, null, 0, info, TEST_CALLING_UID, + TEST_CALLING_PACKAGE); } private static Alarm createAlarm(int type, long whenElapsed, long windowLength, int flags) { @@ -206,4 +224,21 @@ public class AlarmStoreTest { }); assertEquals(7, mAlarmStore.getNextDeliveryTime()); } + + @Test + public void alarmClockRemovalListener() { + final Runnable onRemoved = mock(Runnable.class); + mAlarmStore.setAlarmClockRemovalListener(onRemoved); + + final Alarm simpleAlarm = createAlarm(5, 0); + final Alarm alarmClock = createAlarmClock(10); + + addAlarmsToStore(simpleAlarm, alarmClock); + + mAlarmStore.remove(simpleAlarm::equals); + verifyZeroInteractions(onRemoved); + + mAlarmStore.remove(alarmClock::equals); + verify(onRemoved).run(); + } }