• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2021 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.server.utils;
18 
19 import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
20 
21 import android.annotation.ElapsedRealtimeLong;
22 import android.annotation.NonNull;
23 import android.annotation.UserIdInt;
24 import android.app.AlarmManager;
25 import android.content.Context;
26 import android.os.Handler;
27 import android.os.Looper;
28 import android.os.SystemClock;
29 import android.util.ArraySet;
30 import android.util.IndentingPrintWriter;
31 import android.util.Pair;
32 import android.util.Slog;
33 
34 import com.android.internal.annotations.GuardedBy;
35 import com.android.internal.annotations.VisibleForTesting;
36 
37 import java.util.Comparator;
38 import java.util.PriorityQueue;
39 import java.util.function.Predicate;
40 
41 /**
42  * An {@link AlarmManager.OnAlarmListener} that will queue up all pending alarms and only
43  * schedule one alarm for the earliest alarm. Since {@link AlarmManager} has a maximum limit on the
44  * number of alarms that can be set at one time, this allows clients to maintain alarm times for
45  * various keys without risking hitting the AlarmManager alarm limit. Only one alarm time will be
46  * kept for each key {@code K}.
47  *
48  * @param <K> Any class that will be used as the key. Must have a proper equals() implementation.
49  * @hide
50  */
51 public abstract class AlarmQueue<K> implements AlarmManager.OnAlarmListener {
52     private static final String TAG = AlarmQueue.class.getSimpleName();
53     private static final boolean DEBUG = false;
54 
55     private static final long NOT_SCHEDULED = -1;
56 
57     /**
58      * Internal priority queue for each key's alarm, ordered by the time the alarm should go off.
59      * The pair is the key and its associated alarm time (in the elapsed realtime timebase).
60      */
61     private static class AlarmPriorityQueue<Q> extends PriorityQueue<Pair<Q, Long>> {
62         private static final Comparator<Pair<?, Long>> sTimeComparator =
63                 (o1, o2) -> Long.compare(o1.second, o2.second);
64 
AlarmPriorityQueue()65         AlarmPriorityQueue() {
66             super(1, sTimeComparator);
67         }
68 
69         /**
70          * Remove any instances of the key from the queue.
71          *
72          * @return true if an instance was removed, false otherwise.
73          */
removeKey(@onNull Q key)74         public boolean removeKey(@NonNull Q key) {
75             boolean removed = false;
76             Pair[] alarms = toArray(new Pair[size()]);
77             for (int i = alarms.length - 1; i >= 0; --i) {
78                 if (key.equals(alarms[i].first)) {
79                     remove(alarms[i]);
80                     removed = true;
81                 }
82             }
83             return removed;
84         }
85     }
86 
87     @VisibleForTesting
88     static class Injector {
getElapsedRealtime()89         long getElapsedRealtime() {
90             return SystemClock.elapsedRealtime();
91         }
92     }
93 
94     /** Runnable used to schedule an alarm with AlarmManager. NEVER run this with the lock held. */
95     private final Runnable mScheduleAlarmRunnable = new Runnable() {
96         @Override
97         public void run() {
98             mHandler.removeCallbacks(this);
99 
100             final AlarmManager alarmManager = mContext.getSystemService(AlarmManager.class);
101             if (alarmManager == null) {
102                 // The system isn't fully booted. Clients of this class may not have
103                 // direct access to (be notified when) the system is ready, so retry
104                 // setting the alarm after some delay. Leave enough time so that we don't cause
105                 // any unneeded startup delay.
106                 mHandler.postDelayed(this, 30_000);
107                 return;
108             }
109             final long nextTriggerTimeElapsed;
110             final long minTimeBetweenAlarmsMs;
111             synchronized (mLock) {
112                 if (mTriggerTimeElapsed == NOT_SCHEDULED) {
113                     return;
114                 }
115                 nextTriggerTimeElapsed = mTriggerTimeElapsed;
116                 minTimeBetweenAlarmsMs = mMinTimeBetweenAlarmsMs;
117             }
118             // Never call out to AlarmManager with the lock held. This could sit below AM.
119             if (mExactAlarm) {
120                 alarmManager.setExact(AlarmManager.ELAPSED_REALTIME,
121                         nextTriggerTimeElapsed, mAlarmTag, AlarmQueue.this, mHandler);
122             } else {
123                 alarmManager.setWindow(AlarmManager.ELAPSED_REALTIME,
124                         nextTriggerTimeElapsed, minTimeBetweenAlarmsMs / 2,
125                         mAlarmTag, AlarmQueue.this, mHandler);
126             }
127         }
128     };
129 
130     private final Object mLock = new Object();
131 
132     private final Context mContext;
133     private final Handler mHandler;
134     private final Injector mInjector;
135 
136     @GuardedBy("mLock")
137     private final AlarmPriorityQueue<K> mAlarmPriorityQueue = new AlarmPriorityQueue<>();
138     private final String mAlarmTag;
139     private final String mDumpTitle;
140     /** Whether to use an exact alarm or an inexact alarm. */
141     private final boolean mExactAlarm;
142     /** The minimum amount of time between check alarms. */
143     @GuardedBy("mLock")
144     private long mMinTimeBetweenAlarmsMs;
145     /** The next time the alarm is set to go off, in the elapsed realtime timebase. */
146     @GuardedBy("mLock")
147     @ElapsedRealtimeLong
148     private long mTriggerTimeElapsed = NOT_SCHEDULED;
149 
150     /**
151      * @param alarmTag               The tag to use when scheduling the alarm with AlarmManager.
152      * @param dumpTitle              The title to use when dumping state.
153      * @param exactAlarm             Whether or not to use an exact alarm. If false, this will use
154      *                               an inexact window alarm.
155      * @param minTimeBetweenAlarmsMs The minimum amount of time that should be between alarms. If
156      *                               one alarm will go off too soon after another, the second one
157      *                               will be delayed to meet this minimum time.
158      */
AlarmQueue(@onNull Context context, @NonNull Looper looper, @NonNull String alarmTag, @NonNull String dumpTitle, boolean exactAlarm, long minTimeBetweenAlarmsMs)159     public AlarmQueue(@NonNull Context context, @NonNull Looper looper, @NonNull String alarmTag,
160             @NonNull String dumpTitle, boolean exactAlarm, long minTimeBetweenAlarmsMs) {
161         this(context, looper, alarmTag, dumpTitle, exactAlarm, minTimeBetweenAlarmsMs,
162                 new Injector());
163     }
164 
165     @VisibleForTesting
AlarmQueue(@onNull Context context, @NonNull Looper looper, @NonNull String alarmTag, @NonNull String dumpTitle, boolean exactAlarm, long minTimeBetweenAlarmsMs, @NonNull Injector injector)166     AlarmQueue(@NonNull Context context, @NonNull Looper looper, @NonNull String alarmTag,
167             @NonNull String dumpTitle, boolean exactAlarm, long minTimeBetweenAlarmsMs,
168             @NonNull Injector injector) {
169         mContext = context;
170         mAlarmTag = alarmTag;
171         mDumpTitle = dumpTitle.trim();
172         mExactAlarm = exactAlarm;
173         mHandler = new Handler(looper);
174         mInjector = injector;
175         if (minTimeBetweenAlarmsMs < 0) {
176             throw new IllegalArgumentException("min time between alarms must be non-negative");
177         }
178         mMinTimeBetweenAlarmsMs = minTimeBetweenAlarmsMs;
179     }
180 
181     /**
182      * Add an alarm for the specified key that should go off at the provided time
183      * (in the elapsed realtime timebase). This will also remove any existing alarm for the key.
184      */
addAlarm(K key, @ElapsedRealtimeLong long alarmTimeElapsed)185     public void addAlarm(K key, @ElapsedRealtimeLong long alarmTimeElapsed) {
186         synchronized (mLock) {
187             final boolean removed = mAlarmPriorityQueue.removeKey(key);
188             mAlarmPriorityQueue.offer(new Pair<>(key, alarmTimeElapsed));
189             if (mTriggerTimeElapsed == NOT_SCHEDULED || removed
190                     || alarmTimeElapsed < mTriggerTimeElapsed) {
191                 setNextAlarmLocked();
192             }
193         }
194     }
195 
196     /**
197      * Get the current minimum time between alarms.
198      *
199      * @see #setMinTimeBetweenAlarmsMs(long)
200      */
getMinTimeBetweenAlarmsMs()201     public long getMinTimeBetweenAlarmsMs() {
202         synchronized (mLock) {
203             return mMinTimeBetweenAlarmsMs;
204         }
205     }
206 
207     /** Remove the alarm for this specific key. */
removeAlarmForKey(K key)208     public void removeAlarmForKey(K key) {
209         synchronized (mLock) {
210             if (mAlarmPriorityQueue.removeKey(key)) {
211                 setNextAlarmLocked();
212             }
213         }
214     }
215 
216     /** Remove all alarms tied to the specified user. */
removeAlarmsForUserId(@serIdInt int userId)217     public void removeAlarmsForUserId(@UserIdInt int userId) {
218         boolean removed = false;
219         synchronized (mLock) {
220             Pair[] alarms = mAlarmPriorityQueue.toArray(new Pair[mAlarmPriorityQueue.size()]);
221             for (int i = alarms.length - 1; i >= 0; --i) {
222                 final K key = (K) alarms[i].first;
223                 if (isForUser(key, userId)) {
224                     mAlarmPriorityQueue.remove(alarms[i]);
225                     removed = true;
226                 }
227             }
228             if (removed) {
229                 setNextAlarmLocked();
230             }
231         }
232     }
233 
234     /** Cancel and remove all alarms. */
removeAllAlarms()235     public void removeAllAlarms() {
236         synchronized (mLock) {
237             mAlarmPriorityQueue.clear();
238             setNextAlarmLocked(0);
239         }
240     }
241 
242     /** Remove all alarms that satisfy the predicate. */
removeAlarmsIf(@onNull Predicate<K> predicate)243     protected void removeAlarmsIf(@NonNull Predicate<K> predicate) {
244         boolean removed = false;
245         synchronized (mLock) {
246             Pair[] alarms = mAlarmPriorityQueue.toArray(new Pair[mAlarmPriorityQueue.size()]);
247             for (int i = alarms.length - 1; i >= 0; --i) {
248                 final K key = (K) alarms[i].first;
249                 if (predicate.test(key)) {
250                     mAlarmPriorityQueue.remove(alarms[i]);
251                     removed = true;
252                 }
253             }
254             if (removed) {
255                 setNextAlarmLocked();
256             }
257         }
258     }
259 
260     /**
261      * Update the minimum time that should be between alarms. This helps avoid thrashing when alarms
262      * are scheduled very closely together and may result in some batching of expired alarms.
263      */
setMinTimeBetweenAlarmsMs(long minTimeMs)264     public void setMinTimeBetweenAlarmsMs(long minTimeMs) {
265         if (minTimeMs < 0) {
266             throw new IllegalArgumentException("min time between alarms must be non-negative");
267         }
268         synchronized (mLock) {
269             mMinTimeBetweenAlarmsMs = minTimeMs;
270         }
271     }
272 
273     /** Return true if the key is for the specified user. */
isForUser(@onNull K key, int userId)274     protected abstract boolean isForUser(@NonNull K key, int userId);
275 
276     /** Handle all of the alarms that have now expired (their trigger time has passed). */
processExpiredAlarms(@onNull ArraySet<K> expired)277     protected abstract void processExpiredAlarms(@NonNull ArraySet<K> expired);
278 
279     /** Sets an alarm with {@link AlarmManager} for the earliest alarm in the queue after now. */
280     @GuardedBy("mLock")
setNextAlarmLocked()281     private void setNextAlarmLocked() {
282         setNextAlarmLocked(mInjector.getElapsedRealtime());
283     }
284 
285     /**
286      * Sets an alarm with {@link AlarmManager} for the earliest alarm in the queue, using
287      * {@code earliestTriggerElapsed} as a floor.
288      */
289     @GuardedBy("mLock")
setNextAlarmLocked(long earliestTriggerElapsed)290     private void setNextAlarmLocked(long earliestTriggerElapsed) {
291         if (mAlarmPriorityQueue.size() == 0) {
292             mHandler.post(() -> {
293                 // Never call out to AlarmManager with the lock held. This could sit below AM.
294                 final AlarmManager alarmManager = mContext.getSystemService(AlarmManager.class);
295                 if (alarmManager != null) {
296                     // This should only be null at boot time. No concerns around not
297                     // cancelling if we get null here, so no need to retry.
298                     alarmManager.cancel(this);
299                 }
300             });
301             mTriggerTimeElapsed = NOT_SCHEDULED;
302             return;
303         }
304 
305         final Pair<K, Long> alarm = mAlarmPriorityQueue.peek();
306         final long nextTriggerTimeElapsed = Math.max(earliestTriggerElapsed, alarm.second);
307         // Only schedule the alarm if one of the following is true:
308         // 1. There isn't one currently scheduled
309         // 2. The new alarm is significantly earlier than the previous alarm. If it's
310         // earlier but not significantly so, then we essentially delay the check for some
311         // apps by up to a minute.
312         // 3. The alarm is after the current alarm.
313         if (mTriggerTimeElapsed == NOT_SCHEDULED
314                 || nextTriggerTimeElapsed < mTriggerTimeElapsed - MINUTE_IN_MILLIS
315                 || mTriggerTimeElapsed < nextTriggerTimeElapsed) {
316             if (DEBUG) {
317                 Slog.d(TAG, "Scheduling alarm at " + nextTriggerTimeElapsed
318                         + " for key " + alarm.first);
319             }
320             mTriggerTimeElapsed = nextTriggerTimeElapsed;
321             mHandler.post(mScheduleAlarmRunnable);
322         }
323     }
324 
325     @Override
onAlarm()326     public void onAlarm() {
327         final ArraySet<K> expired = new ArraySet<>();
328         synchronized (mLock) {
329             final long nowElapsed = mInjector.getElapsedRealtime();
330             while (mAlarmPriorityQueue.size() > 0) {
331                 final Pair<K, Long> alarm = mAlarmPriorityQueue.peek();
332                 if (alarm.second <= nowElapsed) {
333                     expired.add(alarm.first);
334                     mAlarmPriorityQueue.remove(alarm);
335                 } else {
336                     break;
337                 }
338             }
339             setNextAlarmLocked(nowElapsed + mMinTimeBetweenAlarmsMs);
340         }
341         // Don't "call out" with the lock held to avoid potential deadlocks.
342         if (expired.size() > 0) {
343             processExpiredAlarms(expired);
344         }
345     }
346 
347     /** Dump internal state. */
dump(IndentingPrintWriter pw)348     public void dump(IndentingPrintWriter pw) {
349         synchronized (mLock) {
350             pw.print(mDumpTitle);
351             pw.println(" alarms:");
352             pw.increaseIndent();
353 
354             if (mAlarmPriorityQueue.size() == 0) {
355                 pw.println("NOT WAITING");
356             } else {
357                 Pair[] alarms = mAlarmPriorityQueue.toArray(new Pair[mAlarmPriorityQueue.size()]);
358                 for (int i = 0; i < alarms.length; ++i) {
359                     final K key = (K) alarms[i].first;
360                     pw.print(key);
361                     pw.print(": ");
362                     pw.print(alarms[i].second);
363                     pw.println();
364                 }
365             }
366 
367             pw.decreaseIndent();
368         }
369     }
370 }
371