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       * The threshold used to consider a new trigger time to be significantly different from the
58       * currently used trigger time.
59       */
60      private static final long SIGNIFICANT_TRIGGER_TIME_CHANGE_THRESHOLD_MS = MINUTE_IN_MILLIS;
61  
62      /**
63       * Internal priority queue for each key's alarm, ordered by the time the alarm should go off.
64       * The pair is the key and its associated alarm time (in the elapsed realtime timebase).
65       */
66      private static class AlarmPriorityQueue<Q> extends PriorityQueue<Pair<Q, Long>> {
67          private static final Comparator<Pair<?, Long>> sTimeComparator =
68                  (o1, o2) -> Long.compare(o1.second, o2.second);
69  
AlarmPriorityQueue()70          AlarmPriorityQueue() {
71              super(1, sTimeComparator);
72          }
73  
74          /**
75           * Remove any instances of the key from the queue.
76           *
77           * @return true if an instance was removed, false otherwise.
78           */
removeKey(@onNull Q key)79          public boolean removeKey(@NonNull Q key) {
80              boolean removed = false;
81              Pair[] alarms = toArray(new Pair[size()]);
82              for (int i = alarms.length - 1; i >= 0; --i) {
83                  if (key.equals(alarms[i].first)) {
84                      remove(alarms[i]);
85                      removed = true;
86                  }
87              }
88              return removed;
89          }
90      }
91  
92      @VisibleForTesting
93      static class Injector {
getElapsedRealtime()94          long getElapsedRealtime() {
95              return SystemClock.elapsedRealtime();
96          }
97      }
98  
99      /** Runnable used to schedule an alarm with AlarmManager. NEVER run this with the lock held. */
100      private final Runnable mScheduleAlarmRunnable = new Runnable() {
101          @Override
102          public void run() {
103              mHandler.removeCallbacks(this);
104  
105              final AlarmManager alarmManager = mContext.getSystemService(AlarmManager.class);
106              if (alarmManager == null) {
107                  // The system isn't fully booted. Clients of this class may not have
108                  // direct access to (be notified when) the system is ready, so retry
109                  // setting the alarm after some delay. Leave enough time so that we don't cause
110                  // any unneeded startup delay.
111                  mHandler.postDelayed(this, 30_000);
112                  return;
113              }
114              final long nextTriggerTimeElapsed;
115              final long minTimeBetweenAlarmsMs;
116              synchronized (mLock) {
117                  if (mTriggerTimeElapsed == NOT_SCHEDULED) {
118                      return;
119                  }
120                  nextTriggerTimeElapsed = mTriggerTimeElapsed;
121                  minTimeBetweenAlarmsMs = mMinTimeBetweenAlarmsMs;
122              }
123              // Never call out to AlarmManager with the lock held. This could sit below AM.
124              if (mExactAlarm) {
125                  alarmManager.setExact(AlarmManager.ELAPSED_REALTIME,
126                          nextTriggerTimeElapsed, mAlarmTag, AlarmQueue.this, mHandler);
127              } else {
128                  alarmManager.setWindow(AlarmManager.ELAPSED_REALTIME,
129                          nextTriggerTimeElapsed, minTimeBetweenAlarmsMs / 2,
130                          mAlarmTag, AlarmQueue.this, mHandler);
131              }
132          }
133      };
134  
135      private final Object mLock = new Object();
136  
137      private final Context mContext;
138      private final Handler mHandler;
139      private final Injector mInjector;
140  
141      @GuardedBy("mLock")
142      private final AlarmPriorityQueue<K> mAlarmPriorityQueue = new AlarmPriorityQueue<>();
143      private final String mAlarmTag;
144      private final String mDumpTitle;
145      /** Whether to use an exact alarm or an inexact alarm. */
146      private final boolean mExactAlarm;
147      /** The minimum amount of time between check alarms. */
148      @GuardedBy("mLock")
149      private long mMinTimeBetweenAlarmsMs;
150      /** The next time the alarm is set to go off, in the elapsed realtime timebase. */
151      @GuardedBy("mLock")
152      @ElapsedRealtimeLong
153      private long mTriggerTimeElapsed = NOT_SCHEDULED;
154      /** The last time an alarm went off (ie. the last time {@link #onAlarm()} was called}). */
155      @GuardedBy("mLock")
156      @ElapsedRealtimeLong
157      private long mLastFireTimeElapsed;
158  
159      /**
160       * @param alarmTag               The tag to use when scheduling the alarm with AlarmManager.
161       * @param dumpTitle              The title to use when dumping state.
162       * @param exactAlarm             Whether or not to use an exact alarm. If false, this will use
163       *                               an inexact window alarm.
164       * @param minTimeBetweenAlarmsMs The minimum amount of time that should be between alarms. If
165       *                               one alarm will go off too soon after another, the second one
166       *                               will be delayed to meet this minimum time.
167       */
AlarmQueue(@onNull Context context, @NonNull Looper looper, @NonNull String alarmTag, @NonNull String dumpTitle, boolean exactAlarm, long minTimeBetweenAlarmsMs)168      public AlarmQueue(@NonNull Context context, @NonNull Looper looper, @NonNull String alarmTag,
169              @NonNull String dumpTitle, boolean exactAlarm, long minTimeBetweenAlarmsMs) {
170          this(context, looper, alarmTag, dumpTitle, exactAlarm, minTimeBetweenAlarmsMs,
171                  new Injector());
172      }
173  
174      @VisibleForTesting
AlarmQueue(@onNull Context context, @NonNull Looper looper, @NonNull String alarmTag, @NonNull String dumpTitle, boolean exactAlarm, long minTimeBetweenAlarmsMs, @NonNull Injector injector)175      AlarmQueue(@NonNull Context context, @NonNull Looper looper, @NonNull String alarmTag,
176              @NonNull String dumpTitle, boolean exactAlarm, long minTimeBetweenAlarmsMs,
177              @NonNull Injector injector) {
178          mContext = context;
179          mAlarmTag = alarmTag;
180          mDumpTitle = dumpTitle.trim();
181          mExactAlarm = exactAlarm;
182          mHandler = new Handler(looper);
183          mInjector = injector;
184          if (minTimeBetweenAlarmsMs < 0) {
185              throw new IllegalArgumentException("min time between alarms must be non-negative");
186          }
187          mMinTimeBetweenAlarmsMs = minTimeBetweenAlarmsMs;
188      }
189  
190      /**
191       * Add an alarm for the specified key that should go off at the provided time
192       * (in the elapsed realtime timebase). This will also remove any existing alarm for the key.
193       */
addAlarm(K key, @ElapsedRealtimeLong long alarmTimeElapsed)194      public void addAlarm(K key, @ElapsedRealtimeLong long alarmTimeElapsed) {
195          synchronized (mLock) {
196              final boolean removed = mAlarmPriorityQueue.removeKey(key);
197              mAlarmPriorityQueue.offer(new Pair<>(key, alarmTimeElapsed));
198              if (mTriggerTimeElapsed == NOT_SCHEDULED || removed
199                      || alarmTimeElapsed < mTriggerTimeElapsed) {
200                  setNextAlarmLocked();
201              }
202          }
203      }
204  
205      /**
206       * Get the current minimum time between alarms.
207       *
208       * @see #setMinTimeBetweenAlarmsMs(long)
209       */
getMinTimeBetweenAlarmsMs()210      public long getMinTimeBetweenAlarmsMs() {
211          synchronized (mLock) {
212              return mMinTimeBetweenAlarmsMs;
213          }
214      }
215  
216      /** Remove the alarm for this specific key. */
removeAlarmForKey(K key)217      public void removeAlarmForKey(K key) {
218          synchronized (mLock) {
219              if (mAlarmPriorityQueue.removeKey(key)) {
220                  setNextAlarmLocked();
221              }
222          }
223      }
224  
225      /** Remove all alarms tied to the specified user. */
removeAlarmsForUserId(@serIdInt int userId)226      public void removeAlarmsForUserId(@UserIdInt int userId) {
227          boolean removed = false;
228          synchronized (mLock) {
229              Pair[] alarms = mAlarmPriorityQueue.toArray(new Pair[mAlarmPriorityQueue.size()]);
230              for (int i = alarms.length - 1; i >= 0; --i) {
231                  final K key = (K) alarms[i].first;
232                  if (isForUser(key, userId)) {
233                      mAlarmPriorityQueue.remove(alarms[i]);
234                      removed = true;
235                  }
236              }
237              if (removed) {
238                  setNextAlarmLocked();
239              }
240          }
241      }
242  
243      /** Cancel and remove all alarms. */
removeAllAlarms()244      public void removeAllAlarms() {
245          synchronized (mLock) {
246              mAlarmPriorityQueue.clear();
247              setNextAlarmLocked(0);
248          }
249      }
250  
251      /** Remove all alarms that satisfy the predicate. */
removeAlarmsIf(@onNull Predicate<K> predicate)252      protected void removeAlarmsIf(@NonNull Predicate<K> predicate) {
253          boolean removed = false;
254          synchronized (mLock) {
255              Pair[] alarms = mAlarmPriorityQueue.toArray(new Pair[mAlarmPriorityQueue.size()]);
256              for (int i = alarms.length - 1; i >= 0; --i) {
257                  final K key = (K) alarms[i].first;
258                  if (predicate.test(key)) {
259                      mAlarmPriorityQueue.remove(alarms[i]);
260                      removed = true;
261                  }
262              }
263              if (removed) {
264                  setNextAlarmLocked();
265              }
266          }
267      }
268  
269      /**
270       * Update the minimum time that should be between alarms. This helps avoid thrashing when alarms
271       * are scheduled very closely together and may result in some batching of expired alarms.
272       */
setMinTimeBetweenAlarmsMs(long minTimeMs)273      public void setMinTimeBetweenAlarmsMs(long minTimeMs) {
274          if (minTimeMs < 0) {
275              throw new IllegalArgumentException("min time between alarms must be non-negative");
276          }
277          synchronized (mLock) {
278              mMinTimeBetweenAlarmsMs = minTimeMs;
279          }
280      }
281  
282      /** Return true if the key is for the specified user. */
isForUser(@onNull K key, int userId)283      protected abstract boolean isForUser(@NonNull K key, int userId);
284  
285      /** Handle all of the alarms that have now expired (their trigger time has passed). */
processExpiredAlarms(@onNull ArraySet<K> expired)286      protected abstract void processExpiredAlarms(@NonNull ArraySet<K> expired);
287  
288      /** Sets an alarm with {@link AlarmManager} for the earliest alarm in the queue after now. */
289      @GuardedBy("mLock")
setNextAlarmLocked()290      private void setNextAlarmLocked() {
291          setNextAlarmLocked(mLastFireTimeElapsed + mMinTimeBetweenAlarmsMs);
292      }
293  
294      /**
295       * Sets an alarm with {@link AlarmManager} for the earliest alarm in the queue, using
296       * {@code earliestTriggerElapsed} as a floor.
297       */
298      @GuardedBy("mLock")
setNextAlarmLocked(long earliestTriggerElapsed)299      private void setNextAlarmLocked(long earliestTriggerElapsed) {
300          if (mAlarmPriorityQueue.size() == 0) {
301              mHandler.post(() -> {
302                  // Never call out to AlarmManager with the lock held. This could sit below AM.
303                  final AlarmManager alarmManager = mContext.getSystemService(AlarmManager.class);
304                  if (alarmManager != null) {
305                      // This should only be null at boot time. No concerns around not
306                      // cancelling if we get null here, so no need to retry.
307                      alarmManager.cancel(this);
308                  }
309              });
310              mTriggerTimeElapsed = NOT_SCHEDULED;
311              return;
312          }
313  
314          final Pair<K, Long> alarm = mAlarmPriorityQueue.peek();
315          final long nextTriggerTimeElapsed = Math.max(earliestTriggerElapsed, alarm.second);
316          // Only schedule the alarm if one of the following is true:
317          // 1. There isn't one currently scheduled
318          // 2. The new alarm is significantly earlier than the previous alarm. If it's
319          // earlier but not significantly so, then we essentially delay the check for some
320          // apps by up to a minute.
321          // 3. The alarm is after the current alarm.
322          final long timeShiftThresholdMs =
323                  Math.min(SIGNIFICANT_TRIGGER_TIME_CHANGE_THRESHOLD_MS, mMinTimeBetweenAlarmsMs);
324          if (mTriggerTimeElapsed == NOT_SCHEDULED
325                  || nextTriggerTimeElapsed < mTriggerTimeElapsed - timeShiftThresholdMs
326                  || mTriggerTimeElapsed < nextTriggerTimeElapsed) {
327              if (DEBUG) {
328                  Slog.d(TAG, "Scheduling alarm at " + nextTriggerTimeElapsed
329                          + " for key " + alarm.first);
330              }
331              mTriggerTimeElapsed = nextTriggerTimeElapsed;
332              mHandler.post(mScheduleAlarmRunnable);
333          }
334      }
335  
336      @Override
onAlarm()337      public void onAlarm() {
338          final ArraySet<K> expired = new ArraySet<>();
339          synchronized (mLock) {
340              final long nowElapsed = mInjector.getElapsedRealtime();
341              mLastFireTimeElapsed = nowElapsed;
342              while (mAlarmPriorityQueue.size() > 0) {
343                  final Pair<K, Long> alarm = mAlarmPriorityQueue.peek();
344                  if (alarm.second <= nowElapsed) {
345                      expired.add(alarm.first);
346                      mAlarmPriorityQueue.remove(alarm);
347                  } else {
348                      break;
349                  }
350              }
351              setNextAlarmLocked(nowElapsed + mMinTimeBetweenAlarmsMs);
352          }
353          // Don't "call out" with the lock held to avoid potential deadlocks.
354          if (expired.size() > 0) {
355              processExpiredAlarms(expired);
356          }
357      }
358  
359      /** Dump internal state. */
dump(IndentingPrintWriter pw)360      public void dump(IndentingPrintWriter pw) {
361          synchronized (mLock) {
362              pw.print(mDumpTitle);
363              pw.println(" alarms:");
364              pw.increaseIndent();
365  
366              if (mAlarmPriorityQueue.size() == 0) {
367                  pw.println("NOT WAITING");
368              } else {
369                  Pair[] alarms = mAlarmPriorityQueue.toArray(new Pair[mAlarmPriorityQueue.size()]);
370                  for (int i = 0; i < alarms.length; ++i) {
371                      final K key = (K) alarms[i].first;
372                      pw.print(key);
373                      pw.print(": ");
374                      pw.print(alarms[i].second);
375                      pw.println();
376                  }
377              }
378  
379              pw.decreaseIndent();
380          }
381      }
382  }
383