diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java index 4ea46ebb752da..1016294fba2e9 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java @@ -56,7 +56,6 @@ import android.provider.DeviceConfig; import android.util.ArraySet; import android.util.IndentingPrintWriter; import android.util.Log; -import android.util.Pair; import android.util.Slog; import android.util.SparseArray; import android.util.SparseArrayMap; @@ -76,11 +75,11 @@ import com.android.server.job.JobSchedulerService; import com.android.server.job.StateControllerProto; import com.android.server.usage.AppStandbyInternal; import com.android.server.usage.AppStandbyInternal.AppIdleStateChangeListener; +import com.android.server.utils.AlarmQueue; import java.util.ArrayList; import java.util.List; import java.util.Objects; -import java.util.PriorityQueue; import java.util.function.Consumer; import java.util.function.Predicate; @@ -323,10 +322,10 @@ public final class QuotaController extends StateController { new SparseArrayMap<>(); /** - * Listener to track and manage when each package comes back within quota. + * Queue to track and manage when each package comes back within quota. */ @GuardedBy("mLock") - private final InQuotaAlarmListener mInQuotaAlarmListener = new InQuotaAlarmListener(); + private final InQuotaAlarmQueue mInQuotaAlarmQueue; /** Cached calculation results for each app, with the standby buckets as the array indices. */ private final SparseArrayMap mExecutionStatsCache = @@ -589,11 +588,12 @@ public final class QuotaController extends StateController { mHandler = new QcHandler(mContext.getMainLooper()); mChargeTracker = new ChargingTracker(); mChargeTracker.startTracking(); - mAlarmManager = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE); + mAlarmManager = mContext.getSystemService(AlarmManager.class); mQcConstants = new QcConstants(); mBackgroundJobsController = backgroundJobsController; mConnectivityController = connectivityController; mIsEnabled = !mConstants.USE_TARE_POLICY; + mInQuotaAlarmQueue = new InQuotaAlarmQueue(mContext, mContext.getMainLooper()); // Set up the app standby bucketing tracker AppStandbyInternal appStandby = LocalServices.getService(AppStandbyInternal.class); @@ -742,7 +742,7 @@ public final class QuotaController extends StateController { mEJPkgTimers.delete(userId); mTimingSessions.delete(userId); mEJTimingSessions.delete(userId); - mInQuotaAlarmListener.removeAlarmsLocked(userId); + mInQuotaAlarmQueue.removeAlarmsForUserId(userId); mExecutionStatsCache.delete(userId); mEJStats.delete(userId); mSystemInstallers.remove(userId); @@ -768,7 +768,7 @@ public final class QuotaController extends StateController { } mTimingSessions.delete(userId, packageName); mEJTimingSessions.delete(userId, packageName); - mInQuotaAlarmListener.removeAlarmLocked(userId, packageName); + mInQuotaAlarmQueue.removeAlarmForKey(new Package(userId, packageName)); mExecutionStatsCache.delete(userId, packageName); mEJStats.delete(userId, packageName); mTopAppTrackers.delete(userId, packageName); @@ -1657,7 +1657,7 @@ public final class QuotaController extends StateController { // exempted. maybeScheduleStartAlarmLocked(userId, packageName, realStandbyBucket); } else { - mInQuotaAlarmListener.removeAlarmLocked(userId, packageName); + mInQuotaAlarmQueue.removeAlarmForKey(new Package(userId, packageName)); } return changed; } @@ -1695,7 +1695,7 @@ public final class QuotaController extends StateController { if (isWithinQuotaLocked(userId, packageName, realStandbyBucket) && isWithinEJQuota) { // TODO(141645789): we probably shouldn't cancel the alarm until we've verified // that all jobs for the userId-package are within quota. - mInQuotaAlarmListener.removeAlarmLocked(userId, packageName); + mInQuotaAlarmQueue.removeAlarmForKey(new Package(userId, packageName)); } else { mToScheduleStartAlarms.add(userId, packageName, realStandbyBucket); } @@ -1760,7 +1760,7 @@ public final class QuotaController extends StateController { + getRemainingExecutionTimeLocked(userId, packageName, standbyBucket) + "ms in its quota."); } - mInQuotaAlarmListener.removeAlarmLocked(userId, packageName); + mInQuotaAlarmQueue.removeAlarmForKey(new Package(userId, packageName)); mHandler.obtainMessage(MSG_CHECK_PACKAGE, userId, 0, packageName).sendToTarget(); return; } @@ -1825,7 +1825,7 @@ public final class QuotaController extends StateController { + nowElapsed + ", inQuotaTime=" + inQuotaTimeElapsed + ": " + stats); inQuotaTimeElapsed = nowElapsed + 5 * MINUTE_IN_MILLIS; } - mInQuotaAlarmListener.addAlarmLocked(userId, packageName, inQuotaTimeElapsed); + mInQuotaAlarmQueue.addAlarm(new Package(userId, packageName), inQuotaTimeElapsed); } private boolean setConstraintSatisfied(@NonNull JobStatus jobStatus, long nowElapsed, @@ -2805,176 +2805,25 @@ public final class QuotaController extends StateController { } } - static class AlarmQueue extends PriorityQueue> { - AlarmQueue() { - super(1, (o1, o2) -> (int) (o1.second - o2.second)); - } - - /** - * Remove any instances of the Package from the queue. - * - * @return true if an instance was removed, false otherwise. - */ - boolean remove(@NonNull Package pkg) { - boolean removed = false; - Pair[] alarms = toArray(new Pair[size()]); - for (int i = alarms.length - 1; i >= 0; --i) { - if (pkg.equals(alarms[i].first)) { - remove(alarms[i]); - removed = true; - } - } - return removed; - } - } - /** Track when UPTCs are expected to come back into quota. */ - private class InQuotaAlarmListener implements AlarmManager.OnAlarmListener { - @GuardedBy("mLock") - private final AlarmQueue mAlarmQueue = new AlarmQueue(); - /** The next time the alarm is set to go off, in the elapsed realtime timebase. */ - @GuardedBy("mLock") - private long mTriggerTimeElapsed = 0; - /** The minimum amount of time between quota check alarms. */ - @GuardedBy("mLock") - private long mMinQuotaCheckDelayMs = QcConstants.DEFAULT_MIN_QUOTA_CHECK_DELAY_MS; - - @GuardedBy("mLock") - void addAlarmLocked(int userId, @NonNull String pkgName, long inQuotaTimeElapsed) { - final Package pkg = new Package(userId, pkgName); - mAlarmQueue.remove(pkg); - mAlarmQueue.offer(new Pair<>(pkg, inQuotaTimeElapsed)); - setNextAlarmLocked(); - } - - @GuardedBy("mLock") - void setMinQuotaCheckDelayMs(long minDelayMs) { - mMinQuotaCheckDelayMs = minDelayMs; - } - - @GuardedBy("mLock") - void removeAlarmLocked(@NonNull Package pkg) { - if (mAlarmQueue.remove(pkg)) { - setNextAlarmLocked(); - } - } - - @GuardedBy("mLock") - void removeAlarmLocked(int userId, @NonNull String packageName) { - removeAlarmLocked(new Package(userId, packageName)); - } - - @GuardedBy("mLock") - void removeAlarmsLocked(int userId) { - boolean removed = false; - Pair[] alarms = mAlarmQueue.toArray(new Pair[mAlarmQueue.size()]); - for (int i = alarms.length - 1; i >= 0; --i) { - final Package pkg = (Package) alarms[i].first; - if (userId == pkg.userId) { - mAlarmQueue.remove(alarms[i]); - removed = true; - } - } - if (removed) { - setNextAlarmLocked(); - } - } - - @GuardedBy("mLock") - private void setNextAlarmLocked() { - setNextAlarmLocked(sElapsedRealtimeClock.millis()); - } - - @GuardedBy("mLock") - private void setNextAlarmLocked(long earliestTriggerElapsed) { - if (mAlarmQueue.size() > 0) { - final Pair alarm = mAlarmQueue.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 job a few extra - // minutes. - // 3. The alarm is after the current alarm. - if (mTriggerTimeElapsed == 0 - || nextTriggerTimeElapsed < mTriggerTimeElapsed - 3 * MINUTE_IN_MILLIS - || mTriggerTimeElapsed < nextTriggerTimeElapsed) { - if (DEBUG) { - Slog.d(TAG, "Scheduling start alarm at " + nextTriggerTimeElapsed - + " for app " + alarm.first); - } - mAlarmManager.set(AlarmManager.ELAPSED_REALTIME, nextTriggerTimeElapsed, - ALARM_TAG_QUOTA_CHECK, this, mHandler); - mTriggerTimeElapsed = nextTriggerTimeElapsed; - } - } else { - mAlarmManager.cancel(this); - mTriggerTimeElapsed = 0; - } + private class InQuotaAlarmQueue extends AlarmQueue { + private InQuotaAlarmQueue(Context context, Looper looper) { + super(context, looper, ALARM_TAG_QUOTA_CHECK, "In quota", false, + QcConstants.DEFAULT_MIN_QUOTA_CHECK_DELAY_MS); } @Override - public void onAlarm() { - synchronized (mLock) { - while (mAlarmQueue.size() > 0) { - final Pair alarm = mAlarmQueue.peek(); - if (alarm.second <= sElapsedRealtimeClock.millis()) { - mHandler.obtainMessage(MSG_CHECK_PACKAGE, alarm.first.userId, 0, - alarm.first.packageName).sendToTarget(); - mAlarmQueue.remove(alarm); - } else { - break; - } - } - setNextAlarmLocked(sElapsedRealtimeClock.millis() + mMinQuotaCheckDelayMs); - } + protected boolean isForUser(@NonNull Package key, int userId) { + return key.userId == userId; } - @GuardedBy("mLock") - void dumpLocked(IndentingPrintWriter pw) { - pw.println("In quota alarms:"); - pw.increaseIndent(); - - if (mAlarmQueue.size() == 0) { - pw.println("NOT WAITING"); - } else { - Pair[] alarms = mAlarmQueue.toArray(new Pair[mAlarmQueue.size()]); - for (int i = 0; i < alarms.length; ++i) { - final Package pkg = (Package) alarms[i].first; - pw.print(pkg); - pw.print(": "); - pw.print(alarms[i].second); - pw.println(); - } + @Override + protected void processExpiredAlarms(@NonNull ArraySet expired) { + for (int i = 0; i < expired.size(); ++i) { + Package p = expired.valueAt(i); + mHandler.obtainMessage(MSG_CHECK_PACKAGE, p.userId, 0, p.packageName) + .sendToTarget(); } - - pw.decreaseIndent(); - } - - @GuardedBy("mLock") - void dumpLocked(ProtoOutputStream proto, long fieldId) { - final long token = proto.start(fieldId); - - proto.write( - StateControllerProto.QuotaController.InQuotaAlarmListener.TRIGGER_TIME_ELAPSED, - mTriggerTimeElapsed); - - Pair[] alarms = mAlarmQueue.toArray(new Pair[mAlarmQueue.size()]); - for (int i = 0; i < alarms.length; ++i) { - final long aToken = proto.start( - StateControllerProto.QuotaController.InQuotaAlarmListener.ALARMS); - - final Package pkg = (Package) alarms[i].first; - pkg.dumpDebug(proto, - StateControllerProto.QuotaController.InQuotaAlarmListener.Alarm.PKG); - proto.write( - StateControllerProto.QuotaController.InQuotaAlarmListener.Alarm.IN_QUOTA_TIME_ELAPSED, - (Long) alarms[i].second); - - proto.end(aToken); - } - - proto.end(token); } } @@ -3563,7 +3412,7 @@ public final class QuotaController extends StateController { properties.getLong(key, DEFAULT_MIN_QUOTA_CHECK_DELAY_MS); // We don't need to re-evaluate execution stats or constraint status for this. // Limit the delay to the range [0, 15] minutes. - mInQuotaAlarmListener.setMinQuotaCheckDelayMs( + mInQuotaAlarmQueue.setMinTimeBetweenAlarmsMs( Math.min(15 * MINUTE_IN_MILLIS, Math.max(0, MIN_QUOTA_CHECK_DELAY_MS))); break; case KEY_EJ_TOP_APP_TIME_CHUNK_SIZE_MS: @@ -4104,7 +3953,7 @@ public final class QuotaController extends StateController { @VisibleForTesting long getMinQuotaCheckDelayMs() { - return mInQuotaAlarmListener.mMinQuotaCheckDelayMs; + return mInQuotaAlarmQueue.getMinTimeBetweenAlarmsMs(); } @VisibleForTesting @@ -4302,7 +4151,7 @@ public final class QuotaController extends StateController { pw.decreaseIndent(); pw.println(); - mInQuotaAlarmListener.dumpLocked(pw); + mInQuotaAlarmQueue.dump(pw); pw.decreaseIndent(); } @@ -4438,9 +4287,6 @@ public final class QuotaController extends StateController { } } - mInQuotaAlarmListener.dumpLocked(proto, - StateControllerProto.QuotaController.IN_QUOTA_ALARM_LISTENER); - proto.end(mToken); proto.end(token); } diff --git a/apex/jobscheduler/service/java/com/android/server/tare/Agent.java b/apex/jobscheduler/service/java/com/android/server/tare/Agent.java index ad56d9345457f..035c9d27f73ed 100644 --- a/apex/jobscheduler/service/java/com/android/server/tare/Agent.java +++ b/apex/jobscheduler/service/java/com/android/server/tare/Agent.java @@ -17,7 +17,6 @@ package com.android.server.tare; import static android.text.format.DateUtils.HOUR_IN_MILLIS; -import static android.text.format.DateUtils.MINUTE_IN_MILLIS; import static com.android.server.tare.EconomicPolicy.REGULATION_BASIC_INCOME; import static com.android.server.tare.EconomicPolicy.REGULATION_BIRTHRIGHT; @@ -32,7 +31,7 @@ import static com.android.server.tare.TareUtils.narcToString; import android.annotation.NonNull; import android.annotation.Nullable; -import android.app.AlarmManager; +import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.os.Handler; @@ -43,7 +42,6 @@ import android.os.UserHandle; import android.util.ArraySet; import android.util.IndentingPrintWriter; import android.util.Log; -import android.util.Pair; import android.util.Slog; import android.util.SparseArrayMap; @@ -52,13 +50,13 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.server.LocalServices; import com.android.server.pm.UserManagerInternal; import com.android.server.usage.AppStandbyInternal; +import com.android.server.utils.AlarmQueue; import libcore.util.EmptyArray; import java.util.Comparator; import java.util.List; import java.util.Objects; -import java.util.PriorityQueue; import java.util.function.Consumer; /** @@ -103,12 +101,11 @@ class Agent { mActionAffordabilityNotes = new SparseArrayMap<>(); /** - * Listener to track and manage when apps will cross the closest affordability threshold (in + * Queue to track and manage when apps will cross the closest affordability threshold (in * both directions). */ @GuardedBy("mLock") - private final BalanceThresholdAlarmListener mBalanceThresholdAlarmListener = - new BalanceThresholdAlarmListener(); + private final BalanceThresholdAlarmQueue mBalanceThresholdAlarmQueue; /** * Comparator to use to sort apps before we distribute ARCs so that we try to give the most @@ -159,7 +156,6 @@ class Agent { }; private static final int MSG_CHECK_BALANCE = 0; - private static final int MSG_SET_BALANCE_ALARM = 1; Agent(@NonNull InternalResourceService irs, @NonNull Scribe scribe) { mLock = irs.getLock(); @@ -167,6 +163,8 @@ class Agent { mScribe = scribe; mHandler = new AgentHandler(TareHandlerThread.get().getLooper()); mAppStandbyInternal = LocalServices.getService(AppStandbyInternal.class); + mBalanceThresholdAlarmQueue = new BalanceThresholdAlarmQueue( + mIrs.getContext(), TareHandlerThread.get().getLooper()); } private class TotalDeltaCalculator implements Consumer { @@ -692,7 +690,7 @@ class Agent { @GuardedBy("mLock") void onPackageRemovedLocked(final int userId, @NonNull final String pkgName) { reclaimAssetsLocked(userId, pkgName); - mBalanceThresholdAlarmListener.removeAlarmLocked(userId, pkgName); + mBalanceThresholdAlarmQueue.removeAlarmForKey(new Package(userId, pkgName)); } /** @@ -712,7 +710,7 @@ class Agent { @GuardedBy("mLock") void onUserRemovedLocked(final int userId, @NonNull final List pkgNames) { reclaimAssetsLocked(userId, pkgNames); - mBalanceThresholdAlarmListener.removeAlarmsLocked(userId); + mBalanceThresholdAlarmQueue.removeAlarmsForUserId(userId); } @GuardedBy("mLock") @@ -817,7 +815,7 @@ class Agent { mCurrentOngoingEvents.get(userId, pkgName); if (ongoingEvents == null) { // No ongoing transactions. No reason to schedule - mBalanceThresholdAlarmListener.removeAlarmLocked(userId, pkgName); + mBalanceThresholdAlarmQueue.removeAlarmForKey(new Package(userId, pkgName)); return; } mTrendCalculator.reset( @@ -829,7 +827,7 @@ class Agent { if (lowerTimeMs == TrendCalculator.WILL_NOT_CROSS_THRESHOLD) { if (upperTimeMs == TrendCalculator.WILL_NOT_CROSS_THRESHOLD) { // Will never cross a threshold based on current events. - mBalanceThresholdAlarmListener.removeAlarmLocked(userId, pkgName); + mBalanceThresholdAlarmQueue.removeAlarmForKey(new Package(userId, pkgName)); return; } timeToThresholdMs = upperTimeMs; @@ -837,14 +835,14 @@ class Agent { timeToThresholdMs = (upperTimeMs == TrendCalculator.WILL_NOT_CROSS_THRESHOLD) ? lowerTimeMs : Math.min(lowerTimeMs, upperTimeMs); } - mBalanceThresholdAlarmListener.addAlarmLocked(userId, pkgName, + mBalanceThresholdAlarmQueue.addAlarm(new Package(userId, pkgName), SystemClock.elapsedRealtime() + timeToThresholdMs); } @GuardedBy("mLock") void tearDownLocked() { mCurrentOngoingEvents.clear(); - mBalanceThresholdAlarmListener.dropAllAlarmsLocked(); + mBalanceThresholdAlarmQueue.removeAllAlarms(); } @VisibleForTesting @@ -869,261 +867,60 @@ class Agent { } } - /** - * An {@link AlarmManager.OnAlarmListener} that will queue up all pending alarms and only - * schedule one alarm for the earliest alarm. - */ - private abstract class AlarmQueueListener implements AlarmManager.OnAlarmListener { - final class Package { - public final String packageName; - public final int userId; + private static final class Package { + public final String packageName; + public final int userId; - Package(int userId, String packageName) { - this.userId = userId; - this.packageName = packageName; - } - - @Override - public String toString() { - return appToString(userId, packageName); - } - - @Override - public boolean equals(Object obj) { - if (obj == null) { - return false; - } - if (this == obj) { - return true; - } - if (obj instanceof Package) { - Package other = (Package) obj; - return userId == other.userId && Objects.equals(packageName, other.packageName); - } else { - return false; - } - } - - @Override - public int hashCode() { - return packageName.hashCode() + userId; - } + Package(int userId, String packageName) { + this.userId = userId; + this.packageName = packageName; } - class AlarmQueue extends PriorityQueue> { - AlarmQueue() { - super(1, (o1, o2) -> (int) (o1.second - o2.second)); - } - - boolean contains(@NonNull Package pkg) { - Pair[] alarms = toArray(new Pair[size()]); - for (int i = alarms.length - 1; i >= 0; --i) { - if (pkg.equals(alarms[i].first)) { - return true; - } - } - return false; - } - - /** - * Remove any instances of the Package from the queue. - * - * @return true if an instance was removed, false otherwise. - */ - boolean remove(@NonNull Package pkg) { - boolean removed = false; - Pair[] alarms = toArray(new Pair[size()]); - for (int i = alarms.length - 1; i >= 0; --i) { - if (pkg.equals(alarms[i].first)) { - remove(alarms[i]); - removed = true; - } - } - return removed; - } - } - - @GuardedBy("mLock") - private final AlarmQueue mAlarmQueue = new AlarmQueue(); - private final String mAlarmTag; - /** Whether to use an exact alarm or an inexact alarm. */ - private final boolean mExactAlarm; - /** The minimum amount of time between check alarms. */ - private final long mMinTimeBetweenAlarmsMs; - /** The next time the alarm is set to go off, in the elapsed realtime timebase. */ - @GuardedBy("mLock") - private long mTriggerTimeElapsed = 0; - - protected AlarmQueueListener(@NonNull String alarmTag, boolean exactAlarm, - long minTimeBetweenAlarmsMs) { - mAlarmTag = alarmTag; - mExactAlarm = exactAlarm; - mMinTimeBetweenAlarmsMs = minTimeBetweenAlarmsMs; - } - - @GuardedBy("mLock") - boolean hasAlarmScheduledLocked(int userId, @NonNull String pkgName) { - final Package pkg = new Package(userId, pkgName); - return mAlarmQueue.contains(pkg); - } - - @GuardedBy("mLock") - void addAlarmLocked(int userId, @NonNull String pkgName, long alarmTimeElapsed) { - final Package pkg = new Package(userId, pkgName); - mAlarmQueue.remove(pkg); - mAlarmQueue.offer(new Pair<>(pkg, alarmTimeElapsed)); - setNextAlarmLocked(); - } - - @GuardedBy("mLock") - void removeAlarmLocked(@NonNull Package pkg) { - if (mAlarmQueue.remove(pkg)) { - setNextAlarmLocked(); - } - } - - @GuardedBy("mLock") - void removeAlarmLocked(int userId, @NonNull String packageName) { - removeAlarmLocked(new Package(userId, packageName)); - } - - @GuardedBy("mLock") - void removeAlarmsLocked(int userId) { - boolean removed = false; - Pair[] alarms = mAlarmQueue.toArray(new Pair[mAlarmQueue.size()]); - for (int i = alarms.length - 1; i >= 0; --i) { - final Package pkg = (Package) alarms[i].first; - if (userId == pkg.userId) { - mAlarmQueue.remove(alarms[i]); - removed = true; - } - } - if (removed) { - setNextAlarmLocked(); - } - } - - /** Sets an alarm with {@link AlarmManager} for the earliest alarm in the queue. */ - @GuardedBy("mLock") - void setNextAlarmLocked() { - setNextAlarmLocked(SystemClock.elapsedRealtime()); - } - - /** - * 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 (mAlarmQueue.size() > 0) { - final Pair alarm = mAlarmQueue.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. - if (mTriggerTimeElapsed == 0 - || nextTriggerTimeElapsed < mTriggerTimeElapsed - MINUTE_IN_MILLIS - || mTriggerTimeElapsed < nextTriggerTimeElapsed) { - if (DEBUG) { - Slog.d(TAG, "Scheduling start alarm at " + nextTriggerTimeElapsed - + " for app " + alarm.first); - } - mHandler.post(() -> { - // Never call out to AlarmManager with the lock held. This sits below AM. - AlarmManager alarmManager = - mIrs.getContext().getSystemService(AlarmManager.class); - if (alarmManager != null) { - if (mExactAlarm) { - alarmManager.setExact(AlarmManager.ELAPSED_REALTIME, - nextTriggerTimeElapsed, mAlarmTag, this, mHandler); - } else { - alarmManager.setWindow(AlarmManager.ELAPSED_REALTIME, - nextTriggerTimeElapsed, mMinTimeBetweenAlarmsMs / 2, - mAlarmTag, this, mHandler); - } - } else { - mHandler.sendEmptyMessageDelayed(MSG_SET_BALANCE_ALARM, 30_000); - } - }); - mTriggerTimeElapsed = nextTriggerTimeElapsed; - } - } else { - mHandler.post(() -> { - // Never call out to AlarmManager with the lock held. This sits below AM. - AlarmManager alarmManager = - mIrs.getContext().getSystemService(AlarmManager.class); - if (alarmManager != null) { - // This should only be null at boot time. No concerns around not - // cancelling if we get null here. - alarmManager.cancel(this); - } - }); - mTriggerTimeElapsed = 0; - } - } - - @GuardedBy("mLock") - void dropAllAlarmsLocked() { - mAlarmQueue.clear(); - setNextAlarmLocked(0); - } - - @GuardedBy("mLock") - protected abstract void processExpiredAlarmLocked(int userId, @NonNull String packageName); - @Override - public void onAlarm() { - synchronized (mLock) { - final long nowElapsed = SystemClock.elapsedRealtime(); - while (mAlarmQueue.size() > 0) { - final Pair alarm = mAlarmQueue.peek(); - if (alarm.second <= nowElapsed) { - processExpiredAlarmLocked(alarm.first.userId, alarm.first.packageName); - mAlarmQueue.remove(alarm); - } else { - break; - } - } - setNextAlarmLocked(nowElapsed + mMinTimeBetweenAlarmsMs); - } + public String toString() { + return appToString(userId, packageName); } - @GuardedBy("mLock") - void dumpLocked(IndentingPrintWriter pw) { - pw.print(mAlarmTag); - pw.println(" alarms:"); - pw.increaseIndent(); - - if (mAlarmQueue.size() == 0) { - pw.println("NOT WAITING"); - } else { - Pair[] alarms = mAlarmQueue.toArray(new Pair[mAlarmQueue.size()]); - for (int i = 0; i < alarms.length; ++i) { - final Package pkg = (Package) alarms[i].first; - pw.print(pkg); - pw.print(": "); - pw.print(alarms[i].second); - pw.println(); - } + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; } + if (this == obj) { + return true; + } + if (obj instanceof Package) { + Package other = (Package) obj; + return userId == other.userId && Objects.equals(packageName, other.packageName); + } + return false; + } - pw.decreaseIndent(); + @Override + public int hashCode() { + return packageName.hashCode() + userId; } } /** Track when apps will cross the closest affordability threshold (in both directions). */ - private class BalanceThresholdAlarmListener extends AlarmQueueListener { - private BalanceThresholdAlarmListener() { - super(ALARM_TAG_AFFORDABILITY_CHECK, true, 15_000L); + private class BalanceThresholdAlarmQueue extends AlarmQueue { + private BalanceThresholdAlarmQueue(Context context, Looper looper) { + super(context, looper, ALARM_TAG_AFFORDABILITY_CHECK, "Affordability check", true, + 15_000L); } @Override - @GuardedBy("mLock") - protected void processExpiredAlarmLocked(int userId, @NonNull String packageName) { - mHandler.obtainMessage(MSG_CHECK_BALANCE, userId, 0, packageName).sendToTarget(); + protected boolean isForUser(@NonNull Package key, int userId) { + return key.userId == userId; + } + + @Override + protected void processExpiredAlarms(@NonNull ArraySet expired) { + for (int i = 0; i < expired.size(); ++i) { + Package p = expired.valueAt(i); + mHandler.obtainMessage(MSG_CHECK_BALANCE, p.userId, 0, p.packageName) + .sendToTarget(); + } } } @@ -1288,13 +1085,6 @@ class Agent { } } break; - - case MSG_SET_BALANCE_ALARM: { - synchronized (mLock) { - mBalanceThresholdAlarmListener.setNextAlarmLocked(); - } - } - break; } } } @@ -1302,6 +1092,6 @@ class Agent { @GuardedBy("mLock") void dumpLocked(IndentingPrintWriter pw) { pw.println(); - mBalanceThresholdAlarmListener.dumpLocked(pw); + mBalanceThresholdAlarmQueue.dump(pw); } } diff --git a/services/core/java/com/android/server/utils/AlarmQueue.java b/services/core/java/com/android/server/utils/AlarmQueue.java new file mode 100644 index 0000000000000..3f4def6d60ab7 --- /dev/null +++ b/services/core/java/com/android/server/utils/AlarmQueue.java @@ -0,0 +1,366 @@ +/* + * 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.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 Any class that will be used as the key. Must have a proper equals() implementation. + * @hide + */ +public abstract class AlarmQueue implements AlarmManager.OnAlarmListener { + private static final String TAG = AlarmQueue.class.getSimpleName(); + private static final boolean DEBUG = false; + + private static final long NOT_SCHEDULED = -1; + + /** + * 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 extends PriorityQueue> { + AlarmPriorityQueue() { + super(1, (o1, o2) -> (int) (o1.second - o2.second)); + } + + /** + * 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 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 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 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 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. + if (mTriggerTimeElapsed == NOT_SCHEDULED + || nextTriggerTimeElapsed < mTriggerTimeElapsed - MINUTE_IN_MILLIS + || 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 expired = new ArraySet<>(); + synchronized (mLock) { + final long nowElapsed = mInjector.getElapsedRealtime(); + while (mAlarmPriorityQueue.size() > 0) { + final Pair 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(); + } + } +} diff --git a/services/core/java/com/android/server/utils/quota/QuotaTracker.java b/services/core/java/com/android/server/utils/quota/QuotaTracker.java index 802ab5b156c83..a0363ef0c8527 100644 --- a/services/core/java/com/android/server/utils/quota/QuotaTracker.java +++ b/services/core/java/com/android/server/utils/quota/QuotaTracker.java @@ -29,11 +29,11 @@ import android.content.Intent; import android.content.IntentFilter; import android.net.Uri; import android.os.Handler; +import android.os.Looper; import android.os.SystemClock; import android.os.UserHandle; import android.util.ArraySet; import android.util.IndentingPrintWriter; -import android.util.Pair; import android.util.Slog; import android.util.SparseArrayMap; import android.util.proto.ProtoOutputStream; @@ -45,8 +45,7 @@ import com.android.internal.os.BackgroundThread; import com.android.server.FgThread; import com.android.server.LocalServices; import com.android.server.SystemServiceManager; - -import java.util.PriorityQueue; +import com.android.server.utils.AlarmQueue; /** * Base class for trackers that track whether an app has exceeded a count quota. @@ -88,10 +87,10 @@ abstract class QuotaTracker { private final ArraySet mQuotaChangeListeners = new ArraySet<>(); /** - * Listener to track and manage when each package comes back within quota. + * Alarm queue to track and manage when each package comes back within quota. */ @GuardedBy("mLock") - private final InQuotaAlarmListener mInQuotaAlarmListener = new InQuotaAlarmListener(); + private final InQuotaAlarmQueue mInQuotaAlarmQueue; /** "Free quota status" for apps. */ @GuardedBy("mLock") @@ -163,6 +162,8 @@ abstract class QuotaTracker { mContext = context; mInjector = injector; mAlarmManager = mContext.getSystemService(AlarmManager.class); + // The operation should be fast enough to put it on the FgThread. + mInQuotaAlarmQueue = new InQuotaAlarmQueue(mContext, FgThread.getHandler().getLooper()); final IntentFilter filter = new IntentFilter(); filter.addAction(Intent.ACTION_PACKAGE_FULLY_REMOVED); @@ -179,7 +180,7 @@ abstract class QuotaTracker { /** Remove all saved events from the tracker. */ public void clear() { synchronized (mLock) { - mInQuotaAlarmListener.clearLocked(); + mInQuotaAlarmQueue.removeAllAlarms(); mFreeQuota.clear(); dropEverythingLocked(); @@ -367,7 +368,7 @@ abstract class QuotaTracker { return; } - mInQuotaAlarmListener.removeAlarmsLocked(userId, packageName); + mInQuotaAlarmQueue.removeAlarms(userId, packageName); mFreeQuota.delete(userId, packageName); @@ -379,7 +380,7 @@ abstract class QuotaTracker { @GuardedBy("mLock") private void onUserRemovedLocked(int userId) { - mInQuotaAlarmListener.removeAlarmsLocked(userId); + mInQuotaAlarmQueue.removeAlarmsForUserId(userId); mFreeQuota.delete(userId); handleRemovedUserLocked(userId); @@ -434,190 +435,43 @@ abstract class QuotaTracker { Slog.e(TAG, "maybeScheduleStartAlarmLocked called for " + pkgString + " even though it's within quota"); } - mInQuotaAlarmListener.removeAlarmLocked(new Uptc(userId, packageName, tag)); + mInQuotaAlarmQueue.removeAlarmForKey(new Uptc(userId, packageName, tag)); maybeUpdateQuotaStatus(userId, packageName, tag); return; } - mInQuotaAlarmListener.addAlarmLocked(new Uptc(userId, packageName, tag), + mInQuotaAlarmQueue.addAlarm(new Uptc(userId, packageName, tag), getInQuotaTimeElapsedLocked(userId, packageName, tag)); } @GuardedBy("mLock") void cancelScheduledStartAlarmLocked(final int userId, @NonNull final String packageName, @Nullable final String tag) { - mInQuotaAlarmListener.removeAlarmLocked(new Uptc(userId, packageName, tag)); - } - - static class AlarmQueue extends PriorityQueue> { - AlarmQueue() { - super(1, (o1, o2) -> (int) (o1.second - o2.second)); - } - - /** - * Remove any instances of the Uptc from the queue. - * - * @return true if an instance was removed, false otherwise. - */ - boolean remove(@NonNull Uptc uptc) { - boolean removed = false; - Pair[] alarms = toArray(new Pair[size()]); - for (int i = alarms.length - 1; i >= 0; --i) { - if (uptc.equals(alarms[i].first)) { - remove(alarms[i]); - removed = true; - } - } - return removed; - } + mInQuotaAlarmQueue.removeAlarmForKey(new Uptc(userId, packageName, tag)); } /** Track when UPTCs are expected to come back into quota. */ - private class InQuotaAlarmListener implements AlarmManager.OnAlarmListener { - @GuardedBy("mLock") - private final AlarmQueue mAlarmQueue = new AlarmQueue(); - /** The next time the alarm is set to go off, in the elapsed realtime timebase. */ - @GuardedBy("mLock") - private long mTriggerTimeElapsed = 0; - - @GuardedBy("mLock") - void addAlarmLocked(@NonNull Uptc uptc, long inQuotaTimeElapsed) { - mAlarmQueue.remove(uptc); - mAlarmQueue.offer(new Pair<>(uptc, inQuotaTimeElapsed)); - setNextAlarmLocked(); - } - - @GuardedBy("mLock") - void clearLocked() { - cancelAlarm(this); - mAlarmQueue.clear(); - mTriggerTimeElapsed = 0; - } - - @GuardedBy("mLock") - void removeAlarmLocked(@NonNull Uptc uptc) { - if (mAlarmQueue.remove(uptc)) { - if (mAlarmQueue.size() == 0) { - cancelAlarm(this); - } else { - setNextAlarmLocked(); - } - } - } - - @GuardedBy("mLock") - void removeAlarmsLocked(int userId) { - boolean removed = false; - Pair[] alarms = mAlarmQueue.toArray(new Pair[mAlarmQueue.size()]); - for (int i = alarms.length - 1; i >= 0; --i) { - final Uptc uptc = (Uptc) alarms[i].first; - if (userId == uptc.userId) { - mAlarmQueue.remove(alarms[i]); - removed = true; - } - } - if (removed) { - setNextAlarmLocked(); - } - } - - @GuardedBy("mLock") - void removeAlarmsLocked(int userId, @NonNull String packageName) { - boolean removed = false; - Pair[] alarms = mAlarmQueue.toArray(new Pair[mAlarmQueue.size()]); - for (int i = alarms.length - 1; i >= 0; --i) { - final Uptc uptc = (Uptc) alarms[i].first; - if (userId == uptc.userId && packageName.equals(uptc.packageName)) { - mAlarmQueue.remove(alarms[i]); - removed = true; - } - } - if (removed) { - setNextAlarmLocked(); - } - } - - @GuardedBy("mLock") - private void setNextAlarmLocked() { - if (mAlarmQueue.size() > 0) { - final long nextTriggerTimeElapsed = mAlarmQueue.peek().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 notification a - // few extra minutes. - if (mTriggerTimeElapsed == 0 - || nextTriggerTimeElapsed < mTriggerTimeElapsed - 3 * MINUTE_IN_MILLIS - || mTriggerTimeElapsed < nextTriggerTimeElapsed) { - // Use a non-wakeup alarm for this - scheduleAlarm(AlarmManager.ELAPSED_REALTIME, nextTriggerTimeElapsed, - ALARM_TAG_QUOTA_CHECK, this); - mTriggerTimeElapsed = nextTriggerTimeElapsed; - } - } else { - cancelAlarm(this); - mTriggerTimeElapsed = 0; - } + private class InQuotaAlarmQueue extends AlarmQueue { + private InQuotaAlarmQueue(Context context, Looper looper) { + super(context, looper, ALARM_TAG_QUOTA_CHECK, "In quota", false, 0); } @Override - public void onAlarm() { - synchronized (mLock) { - while (mAlarmQueue.size() > 0) { - final Pair alarm = mAlarmQueue.peek(); - if (alarm.second <= mInjector.getElapsedRealtime()) { - getHandler().post(() -> maybeUpdateQuotaStatus( - alarm.first.userId, alarm.first.packageName, alarm.first.tag)); - mAlarmQueue.remove(alarm); - } else { - break; - } - } - setNextAlarmLocked(); - } + protected boolean isForUser(@NonNull Uptc uptc, int userId) { + return userId == uptc.userId; } - @GuardedBy("mLock") - void dumpLocked(IndentingPrintWriter pw) { - pw.println("In quota alarms:"); - pw.increaseIndent(); - - if (mAlarmQueue.size() == 0) { - pw.println("NOT WAITING"); - } else { - Pair[] alarms = mAlarmQueue.toArray(new Pair[mAlarmQueue.size()]); - for (int i = 0; i < alarms.length; ++i) { - final Uptc uptc = (Uptc) alarms[i].first; - pw.print(uptc); - pw.print(": "); - pw.print(alarms[i].second); - pw.println(); - } - } - - pw.decreaseIndent(); + void removeAlarms(int userId, @NonNull String packageName) { + removeAlarmsIf((uptc) -> userId == uptc.userId && packageName.equals(uptc.packageName)); } - @GuardedBy("mLock") - void dumpLocked(ProtoOutputStream proto, long fieldId) { - final long token = proto.start(fieldId); - - proto.write(QuotaTrackerProto.InQuotaAlarmListener.TRIGGER_TIME_ELAPSED, - mTriggerTimeElapsed); - - Pair[] alarms = mAlarmQueue.toArray(new Pair[mAlarmQueue.size()]); - for (int i = 0; i < alarms.length; ++i) { - final long aToken = proto.start(QuotaTrackerProto.InQuotaAlarmListener.ALARMS); - - final Uptc uptc = (Uptc) alarms[i].first; - uptc.dumpDebug(proto, QuotaTrackerProto.InQuotaAlarmListener.Alarm.UPTC); - proto.write(QuotaTrackerProto.InQuotaAlarmListener.Alarm.IN_QUOTA_TIME_ELAPSED, - (Long) alarms[i].second); - - proto.end(aToken); + @Override + protected void processExpiredAlarms(@NonNull ArraySet expired) { + for (int i = 0; i < expired.size(); ++i) { + Uptc uptc = expired.valueAt(i); + getHandler().post( + () -> maybeUpdateQuotaStatus(uptc.userId, uptc.packageName, uptc.tag)); } - - proto.end(token); } } @@ -635,7 +489,7 @@ abstract class QuotaTracker { pw.println(); pw.println(); - mInQuotaAlarmListener.dumpLocked(pw); + mInQuotaAlarmQueue.dump(pw); pw.println(); pw.println("Per-app free quota:"); @@ -669,7 +523,6 @@ abstract class QuotaTracker { proto.write(QuotaTrackerProto.IS_ENABLED, mIsEnabled); proto.write(QuotaTrackerProto.IS_GLOBAL_QUOTA_FREE, mIsQuotaFree); proto.write(QuotaTrackerProto.ELAPSED_REALTIME, mInjector.getElapsedRealtime()); - mInQuotaAlarmListener.dumpLocked(proto, QuotaTrackerProto.IN_QUOTA_ALARM_LISTENER); } proto.end(token); diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java index 473815923056a..300f93feed28e 100644 --- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java @@ -178,7 +178,7 @@ public class QuotaControllerTest { fail("registerUidObserver threw exception: " + e.getMessage()); } when(mContext.getMainLooper()).thenReturn(Looper.getMainLooper()); - when(mContext.getSystemService(Context.ALARM_SERVICE)).thenReturn(mAlarmManager); + when(mContext.getSystemService(AlarmManager.class)).thenReturn(mAlarmManager); doReturn(mActivityMangerInternal) .when(() -> LocalServices.getService(ActivityManagerInternal.class)); doReturn(mock(AppStandbyInternal.class)) @@ -2128,7 +2128,8 @@ public class QuotaControllerTest { mQuotaController.maybeScheduleStartAlarmLocked( SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket); } - verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); + verify(mAlarmManager, timeout(1000).times(0)).setWindow( + anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); final long now = JobSchedulerService.sElapsedRealtimeClock.millis(); // Test with timing sessions out of window but still under max execution limit. @@ -2144,7 +2145,8 @@ public class QuotaControllerTest { mQuotaController.maybeScheduleStartAlarmLocked( SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket); } - verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); + verify(mAlarmManager, timeout(1000).times(0)).setWindow( + anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE, createTimingSession(now - 2 * HOUR_IN_MILLIS, 55 * MINUTE_IN_MILLIS, 1), false); @@ -2152,7 +2154,8 @@ public class QuotaControllerTest { mQuotaController.maybeScheduleStartAlarmLocked( SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket); } - verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); + verify(mAlarmManager, timeout(1000).times(0)).setWindow( + anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); JobStatus jobStatus = createJobStatus("testMaybeScheduleStartAlarmLocked_Active", 1); setStandbyBucket(standbyBucket, jobStatus); @@ -2169,8 +2172,8 @@ public class QuotaControllerTest { mQuotaController.maybeScheduleStartAlarmLocked( SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket); } - verify(mAlarmManager, times(1)).set(anyInt(), eq(expectedAlarmTime), eq(TAG_QUOTA_CHECK), - any(), any()); + verify(mAlarmManager, timeout(1000).times(1)).setWindow( + anyInt(), eq(expectedAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); } @Test @@ -2187,7 +2190,8 @@ public class QuotaControllerTest { // No sessions saved yet. mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", standbyBucket); } - verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); + verify(mAlarmManager, timeout(1000).times(0)).setWindow( + anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); // Test with timing sessions out of window. final long now = JobSchedulerService.sElapsedRealtimeClock.millis(); @@ -2196,7 +2200,8 @@ public class QuotaControllerTest { synchronized (mQuotaController.mLock) { mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", standbyBucket); } - verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); + verify(mAlarmManager, timeout(1000).times(0)).setWindow( + anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); // Test with timing sessions in window but still in quota. final long end = now - (2 * HOUR_IN_MILLIS - 5 * MINUTE_IN_MILLIS); @@ -2208,7 +2213,8 @@ public class QuotaControllerTest { synchronized (mQuotaController.mLock) { mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", standbyBucket); } - verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); + verify(mAlarmManager, timeout(1000).times(0)).setWindow( + anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); // Add some more sessions, but still in quota. mQuotaController.saveTimingSession(0, "com.android.test", @@ -2218,7 +2224,8 @@ public class QuotaControllerTest { synchronized (mQuotaController.mLock) { mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", standbyBucket); } - verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); + verify(mAlarmManager, timeout(1000).times(0)).setWindow( + anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); // Test when out of quota. mQuotaController.saveTimingSession(0, "com.android.test", @@ -2226,15 +2233,15 @@ public class QuotaControllerTest { synchronized (mQuotaController.mLock) { mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", standbyBucket); } - verify(mAlarmManager, times(1)) - .set(anyInt(), eq(expectedAlarmTime), eq(TAG_QUOTA_CHECK), any(), any()); + verify(mAlarmManager, timeout(1000).times(1)).setWindow( + anyInt(), eq(expectedAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); // Alarm already scheduled, so make sure it's not scheduled again. synchronized (mQuotaController.mLock) { mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", standbyBucket); } - verify(mAlarmManager, times(1)) - .set(anyInt(), eq(expectedAlarmTime), eq(TAG_QUOTA_CHECK), any(), any()); + verify(mAlarmManager, timeout(1000).times(1)).setWindow( + anyInt(), eq(expectedAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); } @Test @@ -2251,7 +2258,8 @@ public class QuotaControllerTest { synchronized (mQuotaController.mLock) { mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", standbyBucket); } - verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); + verify(mAlarmManager, timeout(1000).times(0)).setWindow( + anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); // Test with timing sessions out of window. final long now = JobSchedulerService.sElapsedRealtimeClock.millis(); @@ -2260,7 +2268,8 @@ public class QuotaControllerTest { synchronized (mQuotaController.mLock) { mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", standbyBucket); } - verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); + verify(mAlarmManager, timeout(1000).times(0)).setWindow( + anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); // Test with timing sessions in window but still in quota. final long start = now - (6 * HOUR_IN_MILLIS); @@ -2270,7 +2279,8 @@ public class QuotaControllerTest { synchronized (mQuotaController.mLock) { mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", standbyBucket); } - verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); + verify(mAlarmManager, timeout(1000).times(0)).setWindow( + anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); // Add some more sessions, but still in quota. mQuotaController.saveTimingSession(0, "com.android.test", @@ -2280,7 +2290,8 @@ public class QuotaControllerTest { synchronized (mQuotaController.mLock) { mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", standbyBucket); } - verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); + verify(mAlarmManager, timeout(1000).times(0)).setWindow( + anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); // Test when out of quota. mQuotaController.saveTimingSession(0, "com.android.test", @@ -2288,15 +2299,15 @@ public class QuotaControllerTest { synchronized (mQuotaController.mLock) { mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", standbyBucket); } - verify(mAlarmManager, times(1)) - .set(anyInt(), eq(expectedAlarmTime), eq(TAG_QUOTA_CHECK), any(), any()); + verify(mAlarmManager, timeout(1000).times(1)).setWindow( + anyInt(), eq(expectedAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); // Alarm already scheduled, so make sure it's not scheduled again. synchronized (mQuotaController.mLock) { mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", standbyBucket); } - verify(mAlarmManager, times(1)) - .set(anyInt(), eq(expectedAlarmTime), eq(TAG_QUOTA_CHECK), any(), any()); + verify(mAlarmManager, timeout(1000).times(1)).setWindow( + anyInt(), eq(expectedAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); } /** @@ -2319,7 +2330,8 @@ public class QuotaControllerTest { mQuotaController.maybeScheduleStartAlarmLocked( SOURCE_USER_ID, SOURCE_PACKAGE, effectiveStandbyBucket); } - verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); + verify(mAlarmManager, timeout(1000).times(0)).setWindow( + anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); // Test with timing sessions out of window. final long now = JobSchedulerService.sElapsedRealtimeClock.millis(); @@ -2329,7 +2341,8 @@ public class QuotaControllerTest { mQuotaController.maybeScheduleStartAlarmLocked( SOURCE_USER_ID, SOURCE_PACKAGE, effectiveStandbyBucket); } - verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); + verify(mAlarmManager, timeout(1000).times(0)).setWindow( + anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); // Test with timing sessions in window but still in quota. final long start = now - (6 * HOUR_IN_MILLIS); @@ -2340,7 +2353,8 @@ public class QuotaControllerTest { mQuotaController.maybeScheduleStartAlarmLocked( SOURCE_USER_ID, SOURCE_PACKAGE, effectiveStandbyBucket); } - verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); + verify(mAlarmManager, timeout(1000).times(0)).setWindow( + anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); // Add some more sessions, but still in quota. mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE, @@ -2351,7 +2365,8 @@ public class QuotaControllerTest { mQuotaController.maybeScheduleStartAlarmLocked( SOURCE_USER_ID, SOURCE_PACKAGE, effectiveStandbyBucket); } - verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); + verify(mAlarmManager, timeout(1000).times(0)).setWindow( + anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); // Test when out of quota. mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE, @@ -2360,16 +2375,16 @@ public class QuotaControllerTest { mQuotaController.maybeScheduleStartAlarmLocked( SOURCE_USER_ID, SOURCE_PACKAGE, effectiveStandbyBucket); } - verify(mAlarmManager, times(1)) - .set(anyInt(), eq(expectedAlarmTime), eq(TAG_QUOTA_CHECK), any(), any()); + verify(mAlarmManager, timeout(1000).times(1)).setWindow( + anyInt(), eq(expectedAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); // Alarm already scheduled, so make sure it's not scheduled again. synchronized (mQuotaController.mLock) { mQuotaController.maybeScheduleStartAlarmLocked( SOURCE_USER_ID, SOURCE_PACKAGE, effectiveStandbyBucket); } - verify(mAlarmManager, times(1)) - .set(anyInt(), eq(expectedAlarmTime), eq(TAG_QUOTA_CHECK), any(), any()); + verify(mAlarmManager, timeout(1000).times(1)).setWindow( + anyInt(), eq(expectedAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); } @Test @@ -2389,7 +2404,8 @@ public class QuotaControllerTest { synchronized (mQuotaController.mLock) { mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", standbyBucket); } - verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); + verify(mAlarmManager, timeout(1000).times(0)).setWindow( + anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); // Test with timing sessions out of window. final long now = JobSchedulerService.sElapsedRealtimeClock.millis(); @@ -2398,7 +2414,8 @@ public class QuotaControllerTest { synchronized (mQuotaController.mLock) { mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", standbyBucket); } - verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); + verify(mAlarmManager, timeout(1000).times(0)).setWindow( + anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); // Test with timing sessions in window but still in quota. final long start = now - (6 * HOUR_IN_MILLIS); @@ -2412,7 +2429,8 @@ public class QuotaControllerTest { synchronized (mQuotaController.mLock) { mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", standbyBucket); } - verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); + verify(mAlarmManager, timeout(1000).times(0)).setWindow( + anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); // Add some more sessions, but still in quota. mQuotaController.saveTimingSession(0, "com.android.test", @@ -2422,7 +2440,8 @@ public class QuotaControllerTest { synchronized (mQuotaController.mLock) { mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", standbyBucket); } - verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); + verify(mAlarmManager, timeout(1000).times(0)).setWindow( + anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); // Test when out of quota. mQuotaController.saveTimingSession(0, "com.android.test", @@ -2430,15 +2449,15 @@ public class QuotaControllerTest { synchronized (mQuotaController.mLock) { mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", standbyBucket); } - verify(mAlarmManager, times(1)) - .set(anyInt(), eq(expectedAlarmTime), eq(TAG_QUOTA_CHECK), any(), any()); + verify(mAlarmManager, timeout(1000).times(1)).setWindow( + anyInt(), eq(expectedAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); // Alarm already scheduled, so make sure it's not scheduled again. synchronized (mQuotaController.mLock) { mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", standbyBucket); } - verify(mAlarmManager, times(1)) - .set(anyInt(), eq(expectedAlarmTime), eq(TAG_QUOTA_CHECK), any(), any()); + verify(mAlarmManager, timeout(1000).times(1)).setWindow( + anyInt(), eq(expectedAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); } /** Tests that the start alarm is properly rescheduled if the app's bucket is changed. */ @@ -2471,9 +2490,10 @@ public class QuotaControllerTest { synchronized (mQuotaController.mLock) { mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", ACTIVE_INDEX); } - inOrder.verify(mAlarmManager, never()) - .set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); - inOrder.verify(mAlarmManager, never()).cancel(any(AlarmManager.OnAlarmListener.class)); + inOrder.verify(mAlarmManager, timeout(1000).times(0)) + .setWindow(anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); + inOrder.verify(mAlarmManager, timeout(1000).times(0)) + .cancel(any(AlarmManager.OnAlarmListener.class)); // And down from there. final long expectedWorkingAlarmTime = @@ -2482,8 +2502,9 @@ public class QuotaControllerTest { synchronized (mQuotaController.mLock) { mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", WORKING_INDEX); } - inOrder.verify(mAlarmManager, times(1)) - .set(anyInt(), eq(expectedWorkingAlarmTime), eq(TAG_QUOTA_CHECK), any(), any()); + inOrder.verify(mAlarmManager, timeout(1000).times(1)).setWindow( + anyInt(), eq(expectedWorkingAlarmTime), anyLong(), + eq(TAG_QUOTA_CHECK), any(), any()); final long expectedFrequentAlarmTime = outOfQuotaTime + (8 * HOUR_IN_MILLIS) @@ -2491,8 +2512,9 @@ public class QuotaControllerTest { synchronized (mQuotaController.mLock) { mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", FREQUENT_INDEX); } - inOrder.verify(mAlarmManager, times(1)) - .set(anyInt(), eq(expectedFrequentAlarmTime), eq(TAG_QUOTA_CHECK), any(), any()); + inOrder.verify(mAlarmManager, timeout(1000).times(1)).setWindow( + anyInt(), eq(expectedFrequentAlarmTime), anyLong(), + eq(TAG_QUOTA_CHECK), any(), any()); final long expectedRareAlarmTime = outOfQuotaTime + (24 * HOUR_IN_MILLIS) @@ -2500,28 +2522,31 @@ public class QuotaControllerTest { synchronized (mQuotaController.mLock) { mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", RARE_INDEX); } - inOrder.verify(mAlarmManager, times(1)) - .set(anyInt(), eq(expectedRareAlarmTime), eq(TAG_QUOTA_CHECK), any(), any()); + inOrder.verify(mAlarmManager, timeout(1000).times(1)).setWindow( + anyInt(), eq(expectedRareAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); // And back up again. synchronized (mQuotaController.mLock) { mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", FREQUENT_INDEX); } - inOrder.verify(mAlarmManager, times(1)) - .set(anyInt(), eq(expectedFrequentAlarmTime), eq(TAG_QUOTA_CHECK), any(), any()); + inOrder.verify(mAlarmManager, timeout(1000).times(1)).setWindow( + anyInt(), eq(expectedFrequentAlarmTime), anyLong(), + eq(TAG_QUOTA_CHECK), any(), any()); synchronized (mQuotaController.mLock) { mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", WORKING_INDEX); } - inOrder.verify(mAlarmManager, times(1)) - .set(anyInt(), eq(expectedWorkingAlarmTime), eq(TAG_QUOTA_CHECK), any(), any()); + inOrder.verify(mAlarmManager, timeout(1000).times(1)).setWindow( + anyInt(), eq(expectedWorkingAlarmTime), anyLong(), + eq(TAG_QUOTA_CHECK), any(), any()); synchronized (mQuotaController.mLock) { mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", ACTIVE_INDEX); } - inOrder.verify(mAlarmManager, never()) - .set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); - inOrder.verify(mAlarmManager, times(1)).cancel(any(AlarmManager.OnAlarmListener.class)); + inOrder.verify(mAlarmManager, timeout(1000).times(0)) + .setWindow(anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); + inOrder.verify(mAlarmManager, timeout(1000).times(1)) + .cancel(any(AlarmManager.OnAlarmListener.class)); } @Test @@ -2547,7 +2572,8 @@ public class QuotaControllerTest { mQuotaController.maybeScheduleStartAlarmLocked( SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket); } - verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); + verify(mAlarmManager, timeout(1000).times(0)).setWindow( + anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); // Valid time in the future, so the count should be used. stats.jobRateLimitExpirationTimeElapsed = now + 5 * MINUTE_IN_MILLIS / 2; @@ -2556,8 +2582,9 @@ public class QuotaControllerTest { mQuotaController.maybeScheduleStartAlarmLocked( SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket); } - verify(mAlarmManager, times(1)) - .set(anyInt(), eq(expectedWorkingAlarmTime), eq(TAG_QUOTA_CHECK), any(), any()); + verify(mAlarmManager, timeout(1000).times(1)).setWindow( + anyInt(), eq(expectedWorkingAlarmTime), anyLong(), + eq(TAG_QUOTA_CHECK), any(), any()); } /** @@ -2647,8 +2674,8 @@ public class QuotaControllerTest { mQuotaController.maybeScheduleStartAlarmLocked( SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket); } - verify(mAlarmManager, times(1)) - .set(anyInt(), eq(expectedAlarmTime), eq(TAG_QUOTA_CHECK), any(), any()); + verify(mAlarmManager, timeout(1000).times(1)).setWindow( + anyInt(), eq(expectedAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); } @@ -2680,8 +2707,8 @@ public class QuotaControllerTest { mQuotaController.maybeScheduleStartAlarmLocked( SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket); } - verify(mAlarmManager, times(1)) - .set(anyInt(), eq(expectedAlarmTime), eq(TAG_QUOTA_CHECK), any(), any()); + verify(mAlarmManager, timeout(1000).times(1)).setWindow( + anyInt(), eq(expectedAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); } @Test @@ -3828,7 +3855,8 @@ public class QuotaControllerTest { mQuotaController.maybeScheduleStartAlarmLocked( SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket); } - verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); + verify(mAlarmManager, timeout(1000).times(0)).setWindow( + anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); // Ran jobs up to the job limit. All of them should be allowed to run. for (int i = 0; i < mQcConstants.MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW; ++i) { @@ -3846,7 +3874,8 @@ public class QuotaControllerTest { advanceElapsedClock(SECOND_IN_MILLIS); } // Start alarm shouldn't have been scheduled since the app was in quota up until this point. - verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); + verify(mAlarmManager, timeout(1000).times(0)).setWindow( + anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); // The app is now out of job count quota JobStatus throttledJob = createJobStatus( @@ -3863,8 +3892,9 @@ public class QuotaControllerTest { SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket); } final long expectedWorkingAlarmTime = stats.jobRateLimitExpirationTimeElapsed; - verify(mAlarmManager, times(1)) - .set(anyInt(), eq(expectedWorkingAlarmTime), eq(TAG_QUOTA_CHECK), any(), any()); + verify(mAlarmManager, timeout(1000).times(1)).setWindow( + anyInt(), eq(expectedWorkingAlarmTime), anyLong(), + eq(TAG_QUOTA_CHECK), any(), any()); } /** @@ -3894,7 +3924,8 @@ public class QuotaControllerTest { mQuotaController.maybeScheduleStartAlarmLocked( SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket); } - verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); + verify(mAlarmManager, timeout(1000).times(0)).setWindow( + anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); // Ran jobs up to the job limit. All of them should be allowed to run. for (int i = 0; i < mQcConstants.MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW; ++i) { @@ -3914,7 +3945,8 @@ public class QuotaControllerTest { advanceElapsedClock(SECOND_IN_MILLIS); } // Start alarm shouldn't have been scheduled since the app was in quota up until this point. - verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); + verify(mAlarmManager, timeout(1000).times(0)).setWindow( + anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); // The app is now out of session count quota JobStatus throttledJob = createJobStatus( @@ -3932,8 +3964,9 @@ public class QuotaControllerTest { SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket); } final long expectedWorkingAlarmTime = stats.sessionRateLimitExpirationTimeElapsed; - verify(mAlarmManager, times(1)) - .set(anyInt(), eq(expectedWorkingAlarmTime), eq(TAG_QUOTA_CHECK), any(), any()); + verify(mAlarmManager, timeout(1000).times(1)).setWindow( + anyInt(), eq(expectedWorkingAlarmTime), anyLong(), + eq(TAG_QUOTA_CHECK), any(), any()); } @Test @@ -4391,8 +4424,8 @@ public class QuotaControllerTest { mQuotaController.maybeScheduleStartAlarmLocked( SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket); } - inOrder.verify(mAlarmManager, never()) - .set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); + inOrder.verify(mAlarmManager, timeout(1000).times(0)) + .setWindow(anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); // Test with timing sessions out of window. final long now = JobSchedulerService.sElapsedRealtimeClock.millis(); @@ -4402,8 +4435,8 @@ public class QuotaControllerTest { mQuotaController.maybeScheduleStartAlarmLocked( SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket); } - inOrder.verify(mAlarmManager, never()) - .set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); + inOrder.verify(mAlarmManager, timeout(1000).times(0)) + .setWindow(anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); // Test with timing sessions in window but still in quota. final long end = now - (22 * HOUR_IN_MILLIS - 5 * MINUTE_IN_MILLIS); @@ -4414,8 +4447,8 @@ public class QuotaControllerTest { mQuotaController.maybeScheduleStartAlarmLocked( SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket); } - inOrder.verify(mAlarmManager, never()) - .set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); + inOrder.verify(mAlarmManager, timeout(1000).times(0)) + .setWindow(anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); // Add some more sessions, but still in quota. mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE, @@ -4426,8 +4459,8 @@ public class QuotaControllerTest { mQuotaController.maybeScheduleStartAlarmLocked( SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket); } - inOrder.verify(mAlarmManager, never()) - .set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); + inOrder.verify(mAlarmManager, timeout(1000).times(0)) + .setWindow(anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); // Test when out of quota. mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE, @@ -4436,16 +4469,16 @@ public class QuotaControllerTest { mQuotaController.maybeScheduleStartAlarmLocked( SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket); } - inOrder.verify(mAlarmManager, times(1)) - .set(anyInt(), eq(expectedAlarmTime), eq(TAG_QUOTA_CHECK), any(), any()); + inOrder.verify(mAlarmManager, timeout(1000).times(1)).setWindow( + anyInt(), eq(expectedAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); // Alarm already scheduled, so make sure it's not scheduled again. synchronized (mQuotaController.mLock) { mQuotaController.maybeScheduleStartAlarmLocked( SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket); } - inOrder.verify(mAlarmManager, never()) - .set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); + inOrder.verify(mAlarmManager, timeout(1000).times(0)) + .setWindow(anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); } /** Tests that the start alarm is properly rescheduled if the app's bucket is changed. */ @@ -4483,9 +4516,10 @@ public class QuotaControllerTest { mQuotaController.maybeScheduleStartAlarmLocked( SOURCE_USER_ID, SOURCE_PACKAGE, ACTIVE_INDEX); } - inOrder.verify(mAlarmManager, never()) - .set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); - inOrder.verify(mAlarmManager, never()).cancel(any(AlarmManager.OnAlarmListener.class)); + inOrder.verify(mAlarmManager, timeout(1000).times(0)) + .setWindow(anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); + inOrder.verify(mAlarmManager, timeout(1000).times(0)) + .cancel(any(AlarmManager.OnAlarmListener.class)); // And down from there. setStandbyBucket(WORKING_INDEX); @@ -4496,8 +4530,9 @@ public class QuotaControllerTest { mQuotaController.maybeScheduleStartAlarmLocked( SOURCE_USER_ID, SOURCE_PACKAGE, WORKING_INDEX); } - inOrder.verify(mAlarmManager, times(1)) - .set(anyInt(), eq(expectedWorkingAlarmTime), eq(TAG_QUOTA_CHECK), any(), any()); + inOrder.verify(mAlarmManager, timeout(1000).times(1)).setWindow( + anyInt(), eq(expectedWorkingAlarmTime), anyLong(), + eq(TAG_QUOTA_CHECK), any(), any()); setStandbyBucket(FREQUENT_INDEX); final long expectedFrequentAlarmTime = @@ -4506,8 +4541,9 @@ public class QuotaControllerTest { mQuotaController.maybeScheduleStartAlarmLocked( SOURCE_USER_ID, SOURCE_PACKAGE, FREQUENT_INDEX); } - inOrder.verify(mAlarmManager, times(1)) - .set(anyInt(), eq(expectedFrequentAlarmTime), eq(TAG_QUOTA_CHECK), any(), any()); + inOrder.verify(mAlarmManager, timeout(1000).times(1)).setWindow( + anyInt(), eq(expectedFrequentAlarmTime), anyLong(), + eq(TAG_QUOTA_CHECK), any(), any()); setStandbyBucket(RARE_INDEX); final long expectedRareAlarmTime = @@ -4517,8 +4553,8 @@ public class QuotaControllerTest { mQuotaController.maybeScheduleStartAlarmLocked( SOURCE_USER_ID, SOURCE_PACKAGE, RARE_INDEX); } - inOrder.verify(mAlarmManager, times(1)) - .set(anyInt(), eq(expectedRareAlarmTime), eq(TAG_QUOTA_CHECK), any(), any()); + inOrder.verify(mAlarmManager, timeout(1000).times(1)).setWindow( + anyInt(), eq(expectedRareAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); // And back up again. setStandbyBucket(FREQUENT_INDEX); @@ -4526,25 +4562,28 @@ public class QuotaControllerTest { mQuotaController.maybeScheduleStartAlarmLocked( SOURCE_USER_ID, SOURCE_PACKAGE, FREQUENT_INDEX); } - inOrder.verify(mAlarmManager, times(1)) - .set(anyInt(), eq(expectedFrequentAlarmTime), eq(TAG_QUOTA_CHECK), any(), any()); + inOrder.verify(mAlarmManager, timeout(1000).times(1)).setWindow( + anyInt(), eq(expectedFrequentAlarmTime), anyLong(), + eq(TAG_QUOTA_CHECK), any(), any()); setStandbyBucket(WORKING_INDEX); synchronized (mQuotaController.mLock) { mQuotaController.maybeScheduleStartAlarmLocked( SOURCE_USER_ID, SOURCE_PACKAGE, WORKING_INDEX); } - inOrder.verify(mAlarmManager, times(1)) - .set(anyInt(), eq(expectedWorkingAlarmTime), eq(TAG_QUOTA_CHECK), any(), any()); + inOrder.verify(mAlarmManager, timeout(1000).times(1)).setWindow( + anyInt(), eq(expectedWorkingAlarmTime), anyLong(), + eq(TAG_QUOTA_CHECK), any(), any()); setStandbyBucket(ACTIVE_INDEX); synchronized (mQuotaController.mLock) { mQuotaController.maybeScheduleStartAlarmLocked( SOURCE_USER_ID, SOURCE_PACKAGE, ACTIVE_INDEX); } - inOrder.verify(mAlarmManager, never()) - .set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); - inOrder.verify(mAlarmManager, times(1)).cancel(any(AlarmManager.OnAlarmListener.class)); + inOrder.verify(mAlarmManager, timeout(1000).times(0)) + .setWindow(anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); + inOrder.verify(mAlarmManager, timeout(1000).times(1)) + .cancel(any(AlarmManager.OnAlarmListener.class)); } /** @@ -4578,8 +4617,8 @@ public class QuotaControllerTest { mQuotaController.maybeScheduleStartAlarmLocked( SOURCE_USER_ID, SOURCE_PACKAGE, WORKING_INDEX); } - verify(mAlarmManager, times(1)) - .set(anyInt(), eq(expectedAlarmTime), eq(TAG_QUOTA_CHECK), any(), any()); + verify(mAlarmManager, timeout(1000).times(1)).setWindow( + anyInt(), eq(expectedAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); } /** Tests that TimingSessions aren't saved when the device is charging. */ diff --git a/services/tests/mockingservicestests/src/com/android/server/utils/AlarmQueueTest.java b/services/tests/mockingservicestests/src/com/android/server/utils/AlarmQueueTest.java new file mode 100644 index 0000000000000..849e6730ac116 --- /dev/null +++ b/services/tests/mockingservicestests/src/com/android/server/utils/AlarmQueueTest.java @@ -0,0 +1,310 @@ +/* + * 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.HOUR_IN_MILLIS; +import static android.text.format.DateUtils.MINUTE_IN_MILLIS; + +import static com.android.dx.mockito.inline.extended.ExtendedMockito.inOrder; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.AlarmManager; +import android.content.Context; +import android.os.Looper; +import android.os.SystemClock; +import android.util.ArraySet; + +import androidx.annotation.NonNull; +import androidx.test.runner.AndroidJUnit4; + +import com.android.server.LocalServices; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InOrder; +import org.mockito.Mock; +import org.mockito.MockitoSession; +import org.mockito.quality.Strictness; + +/** + * Tests for {@link AlarmQueue}. + */ +@RunWith(AndroidJUnit4.class) +public class AlarmQueueTest { + private static final String ALARM_TAG = "*test*"; + + private final InjectorForTest mInjector = new InjectorForTest(); + private ArraySet mExpiredPackages; + private MockitoSession mMockingSession; + @Mock + private AlarmManager mAlarmManager; + @Mock + private Context mContext; + + private static class InjectorForTest extends AlarmQueue.Injector { + private long mElapsedTime = SystemClock.elapsedRealtime(); + + @Override + long getElapsedRealtime() { + return mElapsedTime; + } + } + + @Before + public void setUp() { + mMockingSession = mockitoSession() + .initMocks(this) + .strictness(Strictness.LENIENT) + .mockStatic(LocalServices.class) + .startMocking(); + + when(mContext.getMainLooper()).thenReturn(Looper.getMainLooper()); + when(mContext.getSystemService(AlarmManager.class)).thenReturn(mAlarmManager); + + // Freeze the clocks at 24 hours after this moment in time. + advanceElapsedClock(24 * HOUR_IN_MILLIS); + } + + @After + public void tearDown() { + if (mMockingSession != null) { + mMockingSession.finishMocking(); + } + } + + private void advanceElapsedClock(long incrementMs) { + mInjector.mElapsedTime += incrementMs; + } + + @NonNull + private AlarmQueue createAlarmQueue(boolean exactAlarm, long minTimeBetweenAlarmsMs) { + return new AlarmQueue(mContext, mContext.getMainLooper(), ALARM_TAG, "Test", + exactAlarm, minTimeBetweenAlarmsMs, mInjector) { + @Override + protected boolean isForUser(String key, int userId) { + return true; + } + + @Override + protected void processExpiredAlarms(@NonNull ArraySet expired) { + mExpiredPackages = expired; + } + }; + } + + @Test + public void testAddingIncreasingAlarms() { + final AlarmQueue alarmQueue = createAlarmQueue(true, 0); + final long nowElapsed = mInjector.getElapsedRealtime(); + + InOrder inOrder = inOrder(mAlarmManager); + + alarmQueue.addAlarm("com.android.test.1", nowElapsed + HOUR_IN_MILLIS); + inOrder.verify(mAlarmManager, timeout(1000).times(1)) + .setExact(anyInt(), eq(nowElapsed + HOUR_IN_MILLIS), eq(ALARM_TAG), any(), any()); + alarmQueue.addAlarm("com.android.test.2", nowElapsed + 2 * HOUR_IN_MILLIS); + inOrder.verify(mAlarmManager, never()) + .setExact(anyInt(), anyLong(), eq(ALARM_TAG), any(), any()); + alarmQueue.addAlarm("com.android.test.3", nowElapsed + 3 * HOUR_IN_MILLIS); + inOrder.verify(mAlarmManager, never()) + .setExact(anyInt(), anyLong(), eq(ALARM_TAG), any(), any()); + } + + @Test + public void testAddingDecreasingAlarms() { + final AlarmQueue alarmQueue = createAlarmQueue(true, 0); + final long nowElapsed = mInjector.getElapsedRealtime(); + + InOrder inOrder = inOrder(mAlarmManager); + + alarmQueue.addAlarm("com.android.test.3", nowElapsed + 3 * HOUR_IN_MILLIS); + inOrder.verify(mAlarmManager, timeout(1000).times(1)).setExact( + anyInt(), eq(nowElapsed + 3 * HOUR_IN_MILLIS), eq(ALARM_TAG), any(), any()); + alarmQueue.addAlarm("com.android.test.2", nowElapsed + 2 * HOUR_IN_MILLIS); + inOrder.verify(mAlarmManager, timeout(1000).times(1)).setExact( + anyInt(), eq(nowElapsed + 2 * HOUR_IN_MILLIS), eq(ALARM_TAG), any(), any()); + alarmQueue.addAlarm("com.android.test.1", nowElapsed + HOUR_IN_MILLIS); + inOrder.verify(mAlarmManager, timeout(1000).times(1)).setExact( + anyInt(), eq(nowElapsed + HOUR_IN_MILLIS), eq(ALARM_TAG), any(), any()); + } + + /** + * Verify that updating the alarm time for a key will result in the AlarmManager alarm changing, + * if needed. + */ + @Test + public void testChangingKeyAlarm() { + final AlarmQueue alarmQueue = createAlarmQueue(true, 0); + final long nowElapsed = mInjector.getElapsedRealtime(); + + InOrder inOrder = inOrder(mAlarmManager); + + alarmQueue.addAlarm("1", nowElapsed + 5 * MINUTE_IN_MILLIS); + inOrder.verify(mAlarmManager, timeout(1000).times(1)).setExact( + anyInt(), eq(nowElapsed + 5 * MINUTE_IN_MILLIS), eq(ALARM_TAG), any(), any()); + + // Only alarm, but the time has changed, so we should reschedule what's set with AM. + alarmQueue.addAlarm("1", nowElapsed + 20 * MINUTE_IN_MILLIS); + inOrder.verify(mAlarmManager, timeout(1000).times(1)).setExact( + anyInt(), eq(nowElapsed + 20 * MINUTE_IN_MILLIS), eq(ALARM_TAG), any(), any()); + + alarmQueue.addAlarm("1", nowElapsed + 10 * MINUTE_IN_MILLIS); + inOrder.verify(mAlarmManager, timeout(1000).times(1)).setExact( + anyInt(), eq(nowElapsed + 10 * MINUTE_IN_MILLIS), eq(ALARM_TAG), any(), any()); + + // Add another keyed alarm and check that we don't bother rescheduling when the changed + // alarm is after the first alarm to go off. + alarmQueue.addAlarm("2", nowElapsed + 11 * MINUTE_IN_MILLIS); + inOrder.verify(mAlarmManager, never()).setExact( + anyInt(), anyLong(), eq(ALARM_TAG), any(), any()); + + alarmQueue.addAlarm("2", nowElapsed + 51 * MINUTE_IN_MILLIS); + inOrder.verify(mAlarmManager, never()).setExact( + anyInt(), anyLong(), eq(ALARM_TAG), any(), any()); + + alarmQueue.addAlarm("1", nowElapsed + 52 * MINUTE_IN_MILLIS); + inOrder.verify(mAlarmManager, timeout(1000).times(1)).setExact( + anyInt(), eq(nowElapsed + 51 * MINUTE_IN_MILLIS), eq(ALARM_TAG), any(), any()); + } + + @Test + public void testInexactQueue() { + final AlarmQueue alarmQueue = createAlarmQueue(false, 0); + final long nowElapsed = mInjector.getElapsedRealtime(); + + alarmQueue.addAlarm("com.android.test.1", nowElapsed + HOUR_IN_MILLIS); + verify(mAlarmManager, timeout(1000).times(1)).setWindow( + anyInt(), eq(nowElapsed + HOUR_IN_MILLIS), anyLong(), eq(ALARM_TAG), any(), any()); + } + + @Test + public void testMinTimeBetweenAlarms() { + final AlarmQueue alarmQueue = createAlarmQueue(true, 2 * HOUR_IN_MILLIS); + final long nowElapsed = mInjector.getElapsedRealtime(); + + InOrder inOrder = inOrder(mAlarmManager); + + final String pkg1 = "com.android.test.1"; + final String pkg2 = "com.android.test.2"; + final String pkg3 = "com.android.test.3"; + alarmQueue.addAlarm(pkg1, nowElapsed + HOUR_IN_MILLIS); + inOrder.verify(mAlarmManager, timeout(1000).times(1)).setExact( + anyInt(), eq(nowElapsed + HOUR_IN_MILLIS), eq(ALARM_TAG), any(), any()); + alarmQueue.addAlarm(pkg2, nowElapsed + 2 * HOUR_IN_MILLIS); + alarmQueue.addAlarm(pkg3, nowElapsed + 3 * HOUR_IN_MILLIS); + alarmQueue.addAlarm("com.android.test.4", nowElapsed + 4 * HOUR_IN_MILLIS); + + advanceElapsedClock(HOUR_IN_MILLIS); + + alarmQueue.onAlarm(); + // Minimum of 2 hours between alarms, so the next alarm should be 2 hours after the first. + inOrder.verify(mAlarmManager, timeout(1000).times(1)).setExact( + anyInt(), eq(nowElapsed + 3 * HOUR_IN_MILLIS), eq(ALARM_TAG), any(), any()); + + advanceElapsedClock(2 * HOUR_IN_MILLIS); + alarmQueue.onAlarm(); + // Minimum of 2 hours between alarms, so the next alarm should be 2 hours after the second. + inOrder.verify(mAlarmManager, timeout(1000).times(1)).setExact( + anyInt(), eq(nowElapsed + 5 * HOUR_IN_MILLIS), eq(ALARM_TAG), any(), any()); + } + + @Test + public void testOnAlarm() { + final AlarmQueue alarmQueue = createAlarmQueue(true, 0); + final long nowElapsed = mInjector.getElapsedRealtime(); + + InOrder inOrder = inOrder(mAlarmManager); + + final String pkg1 = "com.android.test.1"; + final String pkg2 = "com.android.test.2"; + final String pkg3 = "com.android.test.3"; + final String pkg4 = "com.android.test.4"; + alarmQueue.addAlarm(pkg1, nowElapsed + HOUR_IN_MILLIS); + inOrder.verify(mAlarmManager, timeout(1000).times(1)).setExact( + anyInt(), eq(nowElapsed + HOUR_IN_MILLIS), eq(ALARM_TAG), any(), any()); + alarmQueue.addAlarm(pkg2, nowElapsed + 2 * HOUR_IN_MILLIS); + alarmQueue.addAlarm(pkg3, nowElapsed + 3 * HOUR_IN_MILLIS); + alarmQueue.addAlarm(pkg4, nowElapsed + 4 * HOUR_IN_MILLIS); + + advanceElapsedClock(HOUR_IN_MILLIS); + + final ArraySet expectedExpired = new ArraySet<>(); + + expectedExpired.add(pkg1); + alarmQueue.onAlarm(); + assertEquals(expectedExpired, mExpiredPackages); + // The next alarm should also be scheduled. + inOrder.verify(mAlarmManager, timeout(1000).times(1)).setExact( + anyInt(), eq(nowElapsed + 2 * HOUR_IN_MILLIS), eq(ALARM_TAG), any(), any()); + + advanceElapsedClock(2 * HOUR_IN_MILLIS); + + expectedExpired.clear(); + expectedExpired.add(pkg2); + expectedExpired.add(pkg3); + alarmQueue.onAlarm(); + assertEquals(expectedExpired, mExpiredPackages); + // The next alarm should also be scheduled. + inOrder.verify(mAlarmManager, timeout(1000).times(1)).setExact( + anyInt(), eq(nowElapsed + 4 * HOUR_IN_MILLIS), eq(ALARM_TAG), any(), any()); + + advanceElapsedClock(HOUR_IN_MILLIS); + + expectedExpired.clear(); + expectedExpired.add(pkg4); + alarmQueue.onAlarm(); + assertEquals(expectedExpired, mExpiredPackages); + // No more alarms, so nothing should be scheduled with AlarmManager. + inOrder.verify(mAlarmManager, timeout(1000).times(0)) + .setExact(anyInt(), anyLong(), eq(ALARM_TAG), any(), any()); + } + + @Test + public void testSettingMinTimeBetweenAlarms() { + final AlarmQueue alarmQueue = createAlarmQueue(true, 50); + assertEquals(50, alarmQueue.getMinTimeBetweenAlarmsMs()); + + alarmQueue.setMinTimeBetweenAlarmsMs(2345); + assertEquals(2345, alarmQueue.getMinTimeBetweenAlarmsMs()); + + try { + alarmQueue.setMinTimeBetweenAlarmsMs(-1); + fail("Successfully set negative time between alarms"); + } catch (IllegalArgumentException expected) { + // Success + } + try { + createAlarmQueue(false, -1); + fail("Successfully set negative time between alarms"); + } catch (IllegalArgumentException expected) { + // Success + } + } +} diff --git a/services/tests/mockingservicestests/src/com/android/server/utils/quota/CountQuotaTrackerTest.java b/services/tests/mockingservicestests/src/com/android/server/utils/quota/CountQuotaTrackerTest.java index 80aec73035bc4..608b64e7d12ad 100644 --- a/services/tests/mockingservicestests/src/com/android/server/utils/quota/CountQuotaTrackerTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/utils/quota/CountQuotaTrackerTest.java @@ -588,37 +588,41 @@ public class CountQuotaTrackerTest { // No sessions saved yet. mQuotaTracker.maybeScheduleStartAlarmLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG); - verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); + verify(mAlarmManager, never()).setWindow( + anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); // Test with timing sessions out of window. final long now = mInjector.getElapsedRealtime(); logEventsAt(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, now - 10 * HOUR_IN_MILLIS, 20); mQuotaTracker.maybeScheduleStartAlarmLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG); - verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); + verify(mAlarmManager, never()).setWindow( + anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); // Test with timing sessions in window but still in quota. final long start = now - (6 * HOUR_IN_MILLIS); final long expectedAlarmTime = start + 8 * HOUR_IN_MILLIS; logEventsAt(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, start, 5); mQuotaTracker.maybeScheduleStartAlarmLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG); - verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); + verify(mAlarmManager, never()).setWindow( + anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); // Add some more sessions, but still in quota. logEventsAt(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, now - 3 * HOUR_IN_MILLIS, 1); logEventsAt(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, now - HOUR_IN_MILLIS, 3); mQuotaTracker.maybeScheduleStartAlarmLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG); - verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); + verify(mAlarmManager, never()).setWindow( + anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); // Test when out of quota. logEventsAt(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, now - HOUR_IN_MILLIS, 1); mQuotaTracker.maybeScheduleStartAlarmLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG); - verify(mAlarmManager, timeout(1000).times(1)) - .set(anyInt(), eq(expectedAlarmTime), eq(TAG_QUOTA_CHECK), any(), any()); + verify(mAlarmManager, timeout(1000).times(1)).setWindow( + anyInt(), eq(expectedAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); // Alarm already scheduled, so make sure it's not scheduled again. mQuotaTracker.maybeScheduleStartAlarmLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG); - verify(mAlarmManager, times(1)) - .set(anyInt(), eq(expectedAlarmTime), eq(TAG_QUOTA_CHECK), any(), any()); + verify(mAlarmManager, times(1)).setWindow( + anyInt(), eq(expectedAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); } /** Tests that the start alarm is properly rescheduled if the app's category is changed. */ @@ -652,7 +656,7 @@ public class CountQuotaTrackerTest { mCategorizer.mCategoryToUse = ACTIVE_BUCKET_CATEGORY; mQuotaTracker.maybeScheduleStartAlarmLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG); inOrder.verify(mAlarmManager, never()) - .set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); + .setWindow(anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); inOrder.verify(mAlarmManager, never()).cancel(any(AlarmManager.OnAlarmListener.class)); // And down from there. @@ -660,37 +664,42 @@ public class CountQuotaTrackerTest { mCategorizer.mCategoryToUse = WORKING_SET_BUCKET_CATEGORY; mQuotaTracker.maybeScheduleStartAlarmLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG); inOrder.verify(mAlarmManager, timeout(1000).times(1)) - .set(anyInt(), eq(expectedWorkingAlarmTime), eq(TAG_QUOTA_CHECK), any(), any()); + .setWindow(anyInt(), eq(expectedWorkingAlarmTime), anyLong(), + eq(TAG_QUOTA_CHECK), any(), any()); final long expectedFrequentAlarmTime = outOfQuotaTime + (8 * HOUR_IN_MILLIS); mCategorizer.mCategoryToUse = FREQUENT_BUCKET_CATEGORY; mQuotaTracker.maybeScheduleStartAlarmLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG); inOrder.verify(mAlarmManager, timeout(1000).times(1)) - .set(anyInt(), eq(expectedFrequentAlarmTime), eq(TAG_QUOTA_CHECK), any(), any()); + .setWindow(anyInt(), eq(expectedFrequentAlarmTime), anyLong(), + eq(TAG_QUOTA_CHECK), any(), any()); final long expectedRareAlarmTime = outOfQuotaTime + (24 * HOUR_IN_MILLIS); mCategorizer.mCategoryToUse = RARE_BUCKET_CATEGORY; mQuotaTracker.maybeScheduleStartAlarmLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG); inOrder.verify(mAlarmManager, timeout(1000).times(1)) - .set(anyInt(), eq(expectedRareAlarmTime), eq(TAG_QUOTA_CHECK), any(), any()); + .setWindow(anyInt(), eq(expectedRareAlarmTime), anyLong(), + eq(TAG_QUOTA_CHECK), any(), any()); // And back up again. mCategorizer.mCategoryToUse = FREQUENT_BUCKET_CATEGORY; mQuotaTracker.maybeScheduleStartAlarmLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG); inOrder.verify(mAlarmManager, timeout(1000).times(1)) - .set(anyInt(), eq(expectedFrequentAlarmTime), eq(TAG_QUOTA_CHECK), any(), any()); + .setWindow(anyInt(), eq(expectedFrequentAlarmTime), anyLong(), + eq(TAG_QUOTA_CHECK), any(), any()); mCategorizer.mCategoryToUse = WORKING_SET_BUCKET_CATEGORY; mQuotaTracker.maybeScheduleStartAlarmLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG); inOrder.verify(mAlarmManager, timeout(1000).times(1)) - .set(anyInt(), eq(expectedWorkingAlarmTime), eq(TAG_QUOTA_CHECK), any(), any()); + .setWindow(anyInt(), eq(expectedWorkingAlarmTime), anyLong(), + eq(TAG_QUOTA_CHECK), any(), any()); mCategorizer.mCategoryToUse = ACTIVE_BUCKET_CATEGORY; mQuotaTracker.maybeScheduleStartAlarmLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG); inOrder.verify(mAlarmManager, timeout(1000).times(1)) .cancel(any(AlarmManager.OnAlarmListener.class)); inOrder.verify(mAlarmManager, timeout(1000).times(0)) - .set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); + .setWindow(anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); } @Test