Merge "Extract AlarmQueue into its own class."
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
366
services/core/java/com/android/server/utils/AlarmQueue.java
Normal file
366
services/core/java/com/android/server/utils/AlarmQueue.java
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user