blob: 09ba19508476e76bf84d24c61540fe1864bfd36d [file] [log] [blame]
/*
* Copyright (C) 2021 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.utils;
import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
import android.annotation.ElapsedRealtimeLong;
import android.annotation.NonNull;
import android.annotation.UserIdInt;
import android.app.AlarmManager;
import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import android.os.SystemClock;
import android.util.ArraySet;
import android.util.IndentingPrintWriter;
import android.util.Pair;
import android.util.Slog;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import java.util.Comparator;
import java.util.PriorityQueue;
import java.util.function.Predicate;
/**
* An {@link AlarmManager.OnAlarmListener} that will queue up all pending alarms and only
* schedule one alarm for the earliest alarm. Since {@link AlarmManager} has a maximum limit on the
* number of alarms that can be set at one time, this allows clients to maintain alarm times for
* various keys without risking hitting the AlarmManager alarm limit. Only one alarm time will be
* kept for each key {@code K}.
*
* @param <K> Any class that will be used as the key. Must have a proper equals() implementation.
* @hide
*/
public abstract class AlarmQueue<K> implements AlarmManager.OnAlarmListener {
private static final String TAG = AlarmQueue.class.getSimpleName();
private static final boolean DEBUG = false;
private static final long NOT_SCHEDULED = -1;
/**
* The threshold used to consider a new trigger time to be significantly different from the
* currently used trigger time.
*/
private static final long SIGNIFICANT_TRIGGER_TIME_CHANGE_THRESHOLD_MS = MINUTE_IN_MILLIS;
/**
* Internal priority queue for each key's alarm, ordered by the time the alarm should go off.
* The pair is the key and its associated alarm time (in the elapsed realtime timebase).
*/
private static class AlarmPriorityQueue<Q> extends PriorityQueue<Pair<Q, Long>> {
private static final Comparator<Pair<?, Long>> sTimeComparator =
(o1, o2) -> Long.compare(o1.second, o2.second);
AlarmPriorityQueue() {
super(1, sTimeComparator);
}
/**
* Remove any instances of the key from the queue.
*
* @return true if an instance was removed, false otherwise.
*/
public boolean removeKey(@NonNull Q key) {
boolean removed = false;
Pair[] alarms = toArray(new Pair[size()]);
for (int i = alarms.length - 1; i >= 0; --i) {
if (key.equals(alarms[i].first)) {
remove(alarms[i]);
removed = true;
}
}
return removed;
}
}
@VisibleForTesting
static class Injector {
long getElapsedRealtime() {
return SystemClock.elapsedRealtime();
}
}
/** Runnable used to schedule an alarm with AlarmManager. NEVER run this with the lock held. */
private final Runnable mScheduleAlarmRunnable = new Runnable() {
@Override
public void run() {
mHandler.removeCallbacks(this);
final AlarmManager alarmManager = mContext.getSystemService(AlarmManager.class);
if (alarmManager == null) {
// The system isn't fully booted. Clients of this class may not have
// direct access to (be notified when) the system is ready, so retry
// setting the alarm after some delay. Leave enough time so that we don't cause
// any unneeded startup delay.
mHandler.postDelayed(this, 30_000);
return;
}
final long nextTriggerTimeElapsed;
final long minTimeBetweenAlarmsMs;
synchronized (mLock) {
if (mTriggerTimeElapsed == NOT_SCHEDULED) {
return;
}
nextTriggerTimeElapsed = mTriggerTimeElapsed;
minTimeBetweenAlarmsMs = mMinTimeBetweenAlarmsMs;
}
// Never call out to AlarmManager with the lock held. This could sit below AM.
if (mExactAlarm) {
alarmManager.setExact(AlarmManager.ELAPSED_REALTIME,
nextTriggerTimeElapsed, mAlarmTag, AlarmQueue.this, mHandler);
} else {
alarmManager.setWindow(AlarmManager.ELAPSED_REALTIME,
nextTriggerTimeElapsed, minTimeBetweenAlarmsMs / 2,
mAlarmTag, AlarmQueue.this, mHandler);
}
}
};
private final Object mLock = new Object();
private final Context mContext;
private final Handler mHandler;
private final Injector mInjector;
@GuardedBy("mLock")
private final AlarmPriorityQueue<K> mAlarmPriorityQueue = new AlarmPriorityQueue<>();
private final String mAlarmTag;
private final String mDumpTitle;
/** Whether to use an exact alarm or an inexact alarm. */
private final boolean mExactAlarm;
/** The minimum amount of time between check alarms. */
@GuardedBy("mLock")
private long mMinTimeBetweenAlarmsMs;
/** The next time the alarm is set to go off, in the elapsed realtime timebase. */
@GuardedBy("mLock")
@ElapsedRealtimeLong
private long mTriggerTimeElapsed = NOT_SCHEDULED;
/**
* @param alarmTag The tag to use when scheduling the alarm with AlarmManager.
* @param dumpTitle The title to use when dumping state.
* @param exactAlarm Whether or not to use an exact alarm. If false, this will use
* an inexact window alarm.
* @param minTimeBetweenAlarmsMs The minimum amount of time that should be between alarms. If
* one alarm will go off too soon after another, the second one
* will be delayed to meet this minimum time.
*/
public AlarmQueue(@NonNull Context context, @NonNull Looper looper, @NonNull String alarmTag,
@NonNull String dumpTitle, boolean exactAlarm, long minTimeBetweenAlarmsMs) {
this(context, looper, alarmTag, dumpTitle, exactAlarm, minTimeBetweenAlarmsMs,
new Injector());
}
@VisibleForTesting
AlarmQueue(@NonNull Context context, @NonNull Looper looper, @NonNull String alarmTag,
@NonNull String dumpTitle, boolean exactAlarm, long minTimeBetweenAlarmsMs,
@NonNull Injector injector) {
mContext = context;
mAlarmTag = alarmTag;
mDumpTitle = dumpTitle.trim();
mExactAlarm = exactAlarm;
mHandler = new Handler(looper);
mInjector = injector;
if (minTimeBetweenAlarmsMs < 0) {
throw new IllegalArgumentException("min time between alarms must be non-negative");
}
mMinTimeBetweenAlarmsMs = minTimeBetweenAlarmsMs;
}
/**
* Add an alarm for the specified key that should go off at the provided time
* (in the elapsed realtime timebase). This will also remove any existing alarm for the key.
*/
public void addAlarm(K key, @ElapsedRealtimeLong long alarmTimeElapsed) {
synchronized (mLock) {
final boolean removed = mAlarmPriorityQueue.removeKey(key);
mAlarmPriorityQueue.offer(new Pair<>(key, alarmTimeElapsed));
if (mTriggerTimeElapsed == NOT_SCHEDULED || removed
|| alarmTimeElapsed < mTriggerTimeElapsed) {
setNextAlarmLocked();
}
}
}
/**
* Get the current minimum time between alarms.
*
* @see #setMinTimeBetweenAlarmsMs(long)
*/
public long getMinTimeBetweenAlarmsMs() {
synchronized (mLock) {
return mMinTimeBetweenAlarmsMs;
}
}
/** Remove the alarm for this specific key. */
public void removeAlarmForKey(K key) {
synchronized (mLock) {
if (mAlarmPriorityQueue.removeKey(key)) {
setNextAlarmLocked();
}
}
}
/** Remove all alarms tied to the specified user. */
public void removeAlarmsForUserId(@UserIdInt int userId) {
boolean removed = false;
synchronized (mLock) {
Pair[] alarms = mAlarmPriorityQueue.toArray(new Pair[mAlarmPriorityQueue.size()]);
for (int i = alarms.length - 1; i >= 0; --i) {
final K key = (K) alarms[i].first;
if (isForUser(key, userId)) {
mAlarmPriorityQueue.remove(alarms[i]);
removed = true;
}
}
if (removed) {
setNextAlarmLocked();
}
}
}
/** Cancel and remove all alarms. */
public void removeAllAlarms() {
synchronized (mLock) {
mAlarmPriorityQueue.clear();
setNextAlarmLocked(0);
}
}
/** Remove all alarms that satisfy the predicate. */
protected void removeAlarmsIf(@NonNull Predicate<K> predicate) {
boolean removed = false;
synchronized (mLock) {
Pair[] alarms = mAlarmPriorityQueue.toArray(new Pair[mAlarmPriorityQueue.size()]);
for (int i = alarms.length - 1; i >= 0; --i) {
final K key = (K) alarms[i].first;
if (predicate.test(key)) {
mAlarmPriorityQueue.remove(alarms[i]);
removed = true;
}
}
if (removed) {
setNextAlarmLocked();
}
}
}
/**
* Update the minimum time that should be between alarms. This helps avoid thrashing when alarms
* are scheduled very closely together and may result in some batching of expired alarms.
*/
public void setMinTimeBetweenAlarmsMs(long minTimeMs) {
if (minTimeMs < 0) {
throw new IllegalArgumentException("min time between alarms must be non-negative");
}
synchronized (mLock) {
mMinTimeBetweenAlarmsMs = minTimeMs;
}
}
/** Return true if the key is for the specified user. */
protected abstract boolean isForUser(@NonNull K key, int userId);
/** Handle all of the alarms that have now expired (their trigger time has passed). */
protected abstract void processExpiredAlarms(@NonNull ArraySet<K> expired);
/** Sets an alarm with {@link AlarmManager} for the earliest alarm in the queue after now. */
@GuardedBy("mLock")
private void setNextAlarmLocked() {
setNextAlarmLocked(mInjector.getElapsedRealtime());
}
/**
* Sets an alarm with {@link AlarmManager} for the earliest alarm in the queue, using
* {@code earliestTriggerElapsed} as a floor.
*/
@GuardedBy("mLock")
private void setNextAlarmLocked(long earliestTriggerElapsed) {
if (mAlarmPriorityQueue.size() == 0) {
mHandler.post(() -> {
// Never call out to AlarmManager with the lock held. This could sit below AM.
final AlarmManager alarmManager = mContext.getSystemService(AlarmManager.class);
if (alarmManager != null) {
// This should only be null at boot time. No concerns around not
// cancelling if we get null here, so no need to retry.
alarmManager.cancel(this);
}
});
mTriggerTimeElapsed = NOT_SCHEDULED;
return;
}
final Pair<K, Long> alarm = mAlarmPriorityQueue.peek();
final long nextTriggerTimeElapsed = Math.max(earliestTriggerElapsed, alarm.second);
// Only schedule the alarm if one of the following is true:
// 1. There isn't one currently scheduled
// 2. The new alarm is significantly earlier than the previous alarm. If it's
// earlier but not significantly so, then we essentially delay the check for some
// apps by up to a minute.
// 3. The alarm is after the current alarm.
final long timeShiftThresholdMs =
Math.min(SIGNIFICANT_TRIGGER_TIME_CHANGE_THRESHOLD_MS, mMinTimeBetweenAlarmsMs);
if (mTriggerTimeElapsed == NOT_SCHEDULED
|| nextTriggerTimeElapsed < mTriggerTimeElapsed - timeShiftThresholdMs
|| mTriggerTimeElapsed < nextTriggerTimeElapsed) {
if (DEBUG) {
Slog.d(TAG, "Scheduling alarm at " + nextTriggerTimeElapsed
+ " for key " + alarm.first);
}
mTriggerTimeElapsed = nextTriggerTimeElapsed;
mHandler.post(mScheduleAlarmRunnable);
}
}
@Override
public void onAlarm() {
final ArraySet<K> expired = new ArraySet<>();
synchronized (mLock) {
final long nowElapsed = mInjector.getElapsedRealtime();
while (mAlarmPriorityQueue.size() > 0) {
final Pair<K, Long> alarm = mAlarmPriorityQueue.peek();
if (alarm.second <= nowElapsed) {
expired.add(alarm.first);
mAlarmPriorityQueue.remove(alarm);
} else {
break;
}
}
setNextAlarmLocked(nowElapsed + mMinTimeBetweenAlarmsMs);
}
// Don't "call out" with the lock held to avoid potential deadlocks.
if (expired.size() > 0) {
processExpiredAlarms(expired);
}
}
/** Dump internal state. */
public void dump(IndentingPrintWriter pw) {
synchronized (mLock) {
pw.print(mDumpTitle);
pw.println(" alarms:");
pw.increaseIndent();
if (mAlarmPriorityQueue.size() == 0) {
pw.println("NOT WAITING");
} else {
Pair[] alarms = mAlarmPriorityQueue.toArray(new Pair[mAlarmPriorityQueue.size()]);
for (int i = 0; i < alarms.length; ++i) {
final K key = (K) alarms[i].first;
pw.print(key);
pw.print(": ");
pw.print(alarms[i].second);
pw.println();
}
}
pw.decreaseIndent();
}
}
}