Merge "Extract AlarmQueue into its own class."

This commit is contained in:
Kweku Adams
2021-10-06 15:08:54 +00:00
committed by Android (Google) Code Review
7 changed files with 942 additions and 729 deletions

View File

@@ -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<String, ExecutionStats[]> 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<Pair<Package, Long>> {
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<Package, Long> 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<Package> {
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<Package, Long> 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<Package> 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);
}

View File

@@ -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<OngoingEvent> {
@@ -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<String> 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<Pair<Package, Long>> {
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<Package, Long> 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<Package, Long> 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<Package> {
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<Package> 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);
}
}

View File

@@ -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 <K> Any class that will be used as the key. Must have a proper equals() implementation.
* @hide
*/
public abstract class AlarmQueue<K> implements AlarmManager.OnAlarmListener {
private static final String TAG = AlarmQueue.class.getSimpleName();
private static final boolean DEBUG = false;
private static final long NOT_SCHEDULED = -1;
/**
* Internal priority queue for each key's alarm, ordered by the time the alarm should go off.
* The pair is the key and its associated alarm time (in the elapsed realtime timebase).
*/
private static class AlarmPriorityQueue<Q> extends PriorityQueue<Pair<Q, Long>> {
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<K> mAlarmPriorityQueue = new AlarmPriorityQueue<>();
private final String mAlarmTag;
private final String mDumpTitle;
/** Whether to use an exact alarm or an inexact alarm. */
private final boolean mExactAlarm;
/** The minimum amount of time between check alarms. */
@GuardedBy("mLock")
private long mMinTimeBetweenAlarmsMs;
/** The next time the alarm is set to go off, in the elapsed realtime timebase. */
@GuardedBy("mLock")
@ElapsedRealtimeLong
private long mTriggerTimeElapsed = NOT_SCHEDULED;
/**
* @param alarmTag The tag to use when scheduling the alarm with AlarmManager.
* @param dumpTitle The title to use when dumping state.
* @param exactAlarm Whether or not to use an exact alarm. If false, this will use
* an inexact window alarm.
* @param minTimeBetweenAlarmsMs The minimum amount of time that should be between alarms. If
* one alarm will go off too soon after another, the second one
* will be delayed to meet this minimum time.
*/
public AlarmQueue(@NonNull Context context, @NonNull Looper looper, @NonNull String alarmTag,
@NonNull String dumpTitle, boolean exactAlarm, long minTimeBetweenAlarmsMs) {
this(context, looper, alarmTag, dumpTitle, exactAlarm, minTimeBetweenAlarmsMs,
new Injector());
}
@VisibleForTesting
AlarmQueue(@NonNull Context context, @NonNull Looper looper, @NonNull String alarmTag,
@NonNull String dumpTitle, boolean exactAlarm, long minTimeBetweenAlarmsMs,
@NonNull Injector injector) {
mContext = context;
mAlarmTag = alarmTag;
mDumpTitle = dumpTitle.trim();
mExactAlarm = exactAlarm;
mHandler = new Handler(looper);
mInjector = injector;
if (minTimeBetweenAlarmsMs < 0) {
throw new IllegalArgumentException("min time between alarms must be non-negative");
}
mMinTimeBetweenAlarmsMs = minTimeBetweenAlarmsMs;
}
/**
* Add an alarm for the specified key that should go off at the provided time
* (in the elapsed realtime timebase). This will also remove any existing alarm for the key.
*/
public void addAlarm(K key, @ElapsedRealtimeLong long alarmTimeElapsed) {
synchronized (mLock) {
final boolean removed = mAlarmPriorityQueue.removeKey(key);
mAlarmPriorityQueue.offer(new Pair<>(key, alarmTimeElapsed));
if (mTriggerTimeElapsed == NOT_SCHEDULED || removed
|| alarmTimeElapsed < mTriggerTimeElapsed) {
setNextAlarmLocked();
}
}
}
/**
* Get the current minimum time between alarms.
*
* @see #setMinTimeBetweenAlarmsMs(long)
*/
public long getMinTimeBetweenAlarmsMs() {
synchronized (mLock) {
return mMinTimeBetweenAlarmsMs;
}
}
/** Remove the alarm for this specific key. */
public void removeAlarmForKey(K key) {
synchronized (mLock) {
if (mAlarmPriorityQueue.removeKey(key)) {
setNextAlarmLocked();
}
}
}
/** Remove all alarms tied to the specified user. */
public void removeAlarmsForUserId(@UserIdInt int userId) {
boolean removed = false;
synchronized (mLock) {
Pair[] alarms = mAlarmPriorityQueue.toArray(new Pair[mAlarmPriorityQueue.size()]);
for (int i = alarms.length - 1; i >= 0; --i) {
final K key = (K) alarms[i].first;
if (isForUser(key, userId)) {
mAlarmPriorityQueue.remove(alarms[i]);
removed = true;
}
}
if (removed) {
setNextAlarmLocked();
}
}
}
/** Cancel and remove all alarms. */
public void removeAllAlarms() {
synchronized (mLock) {
mAlarmPriorityQueue.clear();
setNextAlarmLocked(0);
}
}
/** Remove all alarms that satisfy the predicate. */
protected void removeAlarmsIf(@NonNull Predicate<K> predicate) {
boolean removed = false;
synchronized (mLock) {
Pair[] alarms = mAlarmPriorityQueue.toArray(new Pair[mAlarmPriorityQueue.size()]);
for (int i = alarms.length - 1; i >= 0; --i) {
final K key = (K) alarms[i].first;
if (predicate.test(key)) {
mAlarmPriorityQueue.remove(alarms[i]);
removed = true;
}
}
if (removed) {
setNextAlarmLocked();
}
}
}
/**
* Update the minimum time that should be between alarms. This helps avoid thrashing when alarms
* are scheduled very closely together and may result in some batching of expired alarms.
*/
public void setMinTimeBetweenAlarmsMs(long minTimeMs) {
if (minTimeMs < 0) {
throw new IllegalArgumentException("min time between alarms must be non-negative");
}
synchronized (mLock) {
mMinTimeBetweenAlarmsMs = minTimeMs;
}
}
/** Return true if the key is for the specified user. */
protected abstract boolean isForUser(@NonNull K key, int userId);
/** Handle all of the alarms that have now expired (their trigger time has passed). */
protected abstract void processExpiredAlarms(@NonNull ArraySet<K> expired);
/** Sets an alarm with {@link AlarmManager} for the earliest alarm in the queue after now. */
@GuardedBy("mLock")
private void setNextAlarmLocked() {
setNextAlarmLocked(mInjector.getElapsedRealtime());
}
/**
* Sets an alarm with {@link AlarmManager} for the earliest alarm in the queue, using
* {@code earliestTriggerElapsed} as a floor.
*/
@GuardedBy("mLock")
private void setNextAlarmLocked(long earliestTriggerElapsed) {
if (mAlarmPriorityQueue.size() == 0) {
mHandler.post(() -> {
// Never call out to AlarmManager with the lock held. This could sit below AM.
final AlarmManager alarmManager = mContext.getSystemService(AlarmManager.class);
if (alarmManager != null) {
// This should only be null at boot time. No concerns around not
// cancelling if we get null here, so no need to retry.
alarmManager.cancel(this);
}
});
mTriggerTimeElapsed = NOT_SCHEDULED;
return;
}
final Pair<K, Long> alarm = mAlarmPriorityQueue.peek();
final long nextTriggerTimeElapsed = Math.max(earliestTriggerElapsed, alarm.second);
// Only schedule the alarm if one of the following is true:
// 1. There isn't one currently scheduled
// 2. The new alarm is significantly earlier than the previous alarm. If it's
// earlier but not significantly so, then we essentially delay the check for some
// apps by up to a minute.
// 3. The alarm is after the current alarm.
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<K> expired = new ArraySet<>();
synchronized (mLock) {
final long nowElapsed = mInjector.getElapsedRealtime();
while (mAlarmPriorityQueue.size() > 0) {
final Pair<K, Long> alarm = mAlarmPriorityQueue.peek();
if (alarm.second <= nowElapsed) {
expired.add(alarm.first);
mAlarmPriorityQueue.remove(alarm);
} else {
break;
}
}
setNextAlarmLocked(nowElapsed + mMinTimeBetweenAlarmsMs);
}
// Don't "call out" with the lock held to avoid potential deadlocks.
if (expired.size() > 0) {
processExpiredAlarms(expired);
}
}
/** Dump internal state. */
public void dump(IndentingPrintWriter pw) {
synchronized (mLock) {
pw.print(mDumpTitle);
pw.println(" alarms:");
pw.increaseIndent();
if (mAlarmPriorityQueue.size() == 0) {
pw.println("NOT WAITING");
} else {
Pair[] alarms = mAlarmPriorityQueue.toArray(new Pair[mAlarmPriorityQueue.size()]);
for (int i = 0; i < alarms.length; ++i) {
final K key = (K) alarms[i].first;
pw.print(key);
pw.print(": ");
pw.print(alarms[i].second);
pw.println();
}
}
pw.decreaseIndent();
}
}
}

View File

@@ -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<QuotaChangeListener> 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<Pair<Uptc, Long>> {
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<Uptc> {
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<Uptc, Long> 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<Uptc> 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);

View File

@@ -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. */

View File

@@ -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<String> 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<String> createAlarmQueue(boolean exactAlarm, long minTimeBetweenAlarmsMs) {
return new AlarmQueue<String>(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<String> expired) {
mExpiredPackages = expired;
}
};
}
@Test
public void testAddingIncreasingAlarms() {
final AlarmQueue<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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
}
}
}

View File

@@ -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