Fix QuotaController job spam throttling.

QuotaController was inadvertently updating all Timers for a particular
user whenever any process state crossed the FOREGROUND_SERVICE
threshold, instead of only updating the Timer for the specific UID.

Also adding more data to QuotaController's dump to make future debugging
easier.

Bug: 129117282
Test: atest com.android.server.job.controllers.QuotaControllerTest
Test: atest CtsJobSchedulerTestCases
Change-Id: Ic8d9e6478e61cc62318ae5651f0526e41a71de8d
This commit is contained in:
Kweku Adams
2019-04-16 17:05:30 -07:00
parent 0727797765
commit 7d6a31c154
4 changed files with 399 additions and 85 deletions

View File

@@ -43,6 +43,13 @@ public class SparseSetArray<T> {
return false;
}
/**
* Removes all mappings from this SparseSetArray.
*/
public void clear() {
mData.clear();
}
/**
* @return whether a value exists at index n.
*/

View File

@@ -478,6 +478,54 @@ message StateControllerProto {
}
repeated TrackedJob tracked_jobs = 4;
message ExecutionStats {
option (.android.msg_privacy).dest = DEST_AUTOMATIC;
optional JobStatusDumpProto.Bucket standby_bucket = 1;
// The time after which this record should be considered invalid (out of date), in the
// elapsed realtime timebase.
optional int64 expiration_time_elapsed = 2;
optional int64 window_size_ms = 3;
/** The total amount of time the app ran in its respective bucket window size. */
optional int64 execution_time_in_window_ms = 4;
optional int32 bg_job_count_in_window = 5;
/**
* The total amount of time the app ran in the last
* {@link QuotaController#MAX_PERIOD_MS}.
*/
optional int64 execution_time_in_max_period_ms = 6;
optional int32 bg_job_count_in_max_period = 7;
/**
* The time after which the sum of all the app's sessions plus
* ConstantsProto.QuotaController.in_quota_buffer_ms equals the quota. This is only
* valid if
* execution_time_in_window_ms >=
* ConstantsProto.QuotaController.allowed_time_per_period_ms
* or
* execution_time_in_max_period_ms >=
* ConstantsProto.QuotaController.max_execution_time_ms.
*/
optional int64 quota_cutoff_time_elapsed = 8;
/**
* The time after which job_count_in_allowed_time should be considered invalid, in the
* elapsed realtime timebase.
*/
optional int64 job_count_expiration_time_elapsed = 9;
/**
* The number of jobs that ran in at least the last
* ConstantsProto.QuotaController.allowed_time_per_period_ms.
* It may contain a few stale entries since cleanup won't happen exactly every
* ConstantsProto.QuotaController.allowed_time_per_period_ms.
*/
optional int32 job_count_in_allowed_time = 10;
}
message Package {
option (.android.msg_privacy).dest = DEST_AUTOMATIC;
@@ -517,6 +565,8 @@ message StateControllerProto {
optional Timer timer = 2;
repeated TimingSession saved_sessions = 3;
repeated ExecutionStats execution_stats = 4;
}
repeated PackageStats package_stats = 5;
}

View File

@@ -29,6 +29,7 @@ import android.annotation.UserIdInt;
import android.app.ActivityManager;
import android.app.ActivityManagerInternal;
import android.app.AlarmManager;
import android.app.AppGlobals;
import android.app.IUidObserver;
import android.app.usage.UsageStatsManagerInternal;
import android.app.usage.UsageStatsManagerInternal.AppIdleStateChangeListener;
@@ -49,6 +50,7 @@ import android.util.Log;
import android.util.Slog;
import android.util.SparseArray;
import android.util.SparseBooleanArray;
import android.util.SparseSetArray;
import android.util.proto.ProtoOutputStream;
import com.android.internal.annotations.VisibleForTesting;
@@ -277,9 +279,9 @@ public final class QuotaController extends StateController {
.append(", ")
.append("bgJobCountInMaxPeriod=").append(bgJobCountInMaxPeriod).append(", ")
.append("quotaCutoffTime=").append(quotaCutoffTimeElapsed).append(", ")
.append("jobCountExpirationTime").append(jobCountExpirationTimeElapsed)
.append("jobCountExpirationTime=").append(jobCountExpirationTimeElapsed)
.append(", ")
.append("jobCountInAllowedTime").append(jobCountInAllowedTime)
.append("jobCountInAllowedTime=").append(jobCountInAllowedTime)
.toString();
}
@@ -338,6 +340,9 @@ public final class QuotaController extends StateController {
/** List of UIDs currently in the foreground. */
private final SparseBooleanArray mForegroundUids = new SparseBooleanArray();
/** Cached mapping of UIDs (for all users) to a list of packages in the UID. */
private final SparseSetArray<String> mUidToPackageCache = new SparseSetArray<>();
/**
* List of jobs that started while the UID was in the TOP state. There will be no more than
* 16 ({@link JobSchedulerService#MAX_JOB_CONTEXTS_COUNT}) running at once, so an ArraySet is
@@ -421,6 +426,22 @@ public final class QuotaController extends StateController {
}
};
private final BroadcastReceiver mPackageAddedReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (intent == null) {
return;
}
if (intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) {
return;
}
final int uid = intent.getIntExtra(Intent.EXTRA_UID, -1);
synchronized (mLock) {
mUidToPackageCache.remove(uid);
}
}
};
/**
* The rolling window size for each standby bucket. Within each window, an app will have 10
* minutes to run its jobs.
@@ -469,6 +490,9 @@ public final class QuotaController extends StateController {
mActivityManagerInternal = LocalServices.getService(ActivityManagerInternal.class);
mAlarmManager = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
final IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED);
mContext.registerReceiverAsUser(mPackageAddedReceiver, UserHandle.ALL, filter, null, null);
// Set up the app standby bucketing tracker
UsageStatsManagerInternal usageStats = LocalServices.getService(
UsageStatsManagerInternal.class);
@@ -502,10 +526,15 @@ public final class QuotaController extends StateController {
@Override
public void prepareForExecutionLocked(JobStatus jobStatus) {
if (DEBUG) Slog.d(TAG, "Prepping for " + jobStatus.toShortString());
if (DEBUG) {
Slog.d(TAG, "Prepping for " + jobStatus.toShortString());
}
final int uid = jobStatus.getSourceUid();
if (mActivityManagerInternal.getUidProcessState(uid) <= ActivityManager.PROCESS_STATE_TOP) {
if (DEBUG) {
Slog.d(TAG, jobStatus.toShortString() + " is top started job");
}
mTopStartedJobs.add(jobStatus);
// Top jobs won't count towards quota so there's no need to involve the Timer.
return;
@@ -518,7 +547,7 @@ public final class QuotaController extends StateController {
timer = new Timer(uid, userId, packageName);
mPkgTimers.add(userId, packageName, timer);
}
timer.startTrackingJob(jobStatus);
timer.startTrackingJobLocked(jobStatus);
}
@Override
@@ -645,7 +674,7 @@ public final class QuotaController extends StateController {
if (timer != null) {
if (timer.isActive()) {
Slog.wtf(TAG, "onAppRemovedLocked called before Timer turned off.");
timer.dropEverything();
timer.dropEverythingLocked();
}
mPkgTimers.delete(userId, packageName);
}
@@ -657,6 +686,7 @@ public final class QuotaController extends StateController {
}
mExecutionStatsCache.delete(userId, packageName);
mForegroundUids.delete(uid);
mUidToPackageCache.remove(uid);
}
@Override
@@ -666,6 +696,7 @@ public final class QuotaController extends StateController {
mTimingSessions.delete(userId);
mInQuotaAlarmListeners.delete(userId);
mExecutionStatsCache.delete(userId);
mUidToPackageCache.clear();
}
private boolean isUidInForeground(int uid) {
@@ -678,7 +709,7 @@ public final class QuotaController extends StateController {
}
/** @return true if the job was started while the app was in the TOP state. */
private boolean isTopStartedJob(@NonNull final JobStatus jobStatus) {
private boolean isTopStartedJobLocked(@NonNull final JobStatus jobStatus) {
return mTopStartedJobs.contains(jobStatus);
}
@@ -695,14 +726,14 @@ public final class QuotaController extends StateController {
return jobStatus.getStandbyBucket();
}
private boolean isWithinQuotaLocked(@NonNull final JobStatus jobStatus) {
@VisibleForTesting
boolean isWithinQuotaLocked(@NonNull final JobStatus jobStatus) {
final int standbyBucket = getEffectiveStandbyBucket(jobStatus);
Timer timer = mPkgTimers.get(jobStatus.getSourceUserId(), jobStatus.getSourcePackageName());
// A job is within quota if one of the following is true:
// 1. it was started while the app was in the TOP state
// 2. the app is currently in the foreground
// 3. the app overall is within its quota
return isTopStartedJob(jobStatus)
return isTopStartedJobLocked(jobStatus)
|| isUidInForeground(jobStatus.getSourceUid())
|| isWithinQuotaLocked(
jobStatus.getSourceUserId(), jobStatus.getSourcePackageName(), standbyBucket);
@@ -1081,7 +1112,9 @@ public final class QuotaController extends StateController {
if (earliestEndElapsed == Long.MAX_VALUE) {
// Couldn't find a good time to clean up. Maybe this was called after we deleted all
// timing sessions.
if (DEBUG) Slog.d(TAG, "Didn't find a time to schedule cleanup");
if (DEBUG) {
Slog.d(TAG, "Didn't find a time to schedule cleanup");
}
return;
}
// Need to keep sessions for all apps up to the max period, regardless of their current
@@ -1095,15 +1128,19 @@ public final class QuotaController extends StateController {
mNextCleanupTimeElapsed = nextCleanupElapsed;
mAlarmManager.set(AlarmManager.ELAPSED_REALTIME, nextCleanupElapsed, ALARM_TAG_CLEANUP,
mSessionCleanupAlarmListener, mHandler);
if (DEBUG) Slog.d(TAG, "Scheduled next cleanup for " + mNextCleanupTimeElapsed);
if (DEBUG) {
Slog.d(TAG, "Scheduled next cleanup for " + mNextCleanupTimeElapsed);
}
}
private void handleNewChargingStateLocked() {
final long nowElapsed = sElapsedRealtimeClock.millis();
final boolean isCharging = mChargeTracker.isCharging();
if (DEBUG) Slog.d(TAG, "handleNewChargingStateLocked: " + isCharging);
if (DEBUG) {
Slog.d(TAG, "handleNewChargingStateLocked: " + isCharging);
}
// Deal with Timers first.
mPkgTimers.forEach((t) -> t.onStateChanged(nowElapsed, isCharging));
mPkgTimers.forEach((t) -> t.onStateChangedLocked(nowElapsed, isCharging));
// Now update jobs.
maybeUpdateAllConstraintsLocked();
}
@@ -1140,7 +1177,7 @@ public final class QuotaController extends StateController {
boolean changed = false;
for (int i = jobs.size() - 1; i >= 0; --i) {
final JobStatus js = jobs.valueAt(i);
if (isTopStartedJob(js)) {
if (isTopStartedJobLocked(js)) {
// Job was started while the app was in the TOP state so we should allow it to
// finish.
changed |= js.setQuotaConstraintSatisfied(true);
@@ -1282,7 +1319,9 @@ public final class QuotaController extends StateController {
if (!alarmListener.isWaiting()
|| inQuotaTimeElapsed < alarmListener.getTriggerTimeElapsed() - 3 * MINUTE_IN_MILLIS
|| alarmListener.getTriggerTimeElapsed() < inQuotaTimeElapsed) {
if (DEBUG) Slog.d(TAG, "Scheduling start alarm for " + pkgString);
if (DEBUG) {
Slog.d(TAG, "Scheduling start alarm for " + pkgString);
}
// If the next time this app will have quota is at least 3 minutes before the
// alarm is supposed to go off, reschedule the alarm.
mAlarmManager.set(AlarmManager.ELAPSED_REALTIME, inQuotaTimeElapsed,
@@ -1430,8 +1469,8 @@ public final class QuotaController extends StateController {
mUid = uid;
}
void startTrackingJob(@NonNull JobStatus jobStatus) {
if (isTopStartedJob(jobStatus)) {
void startTrackingJobLocked(@NonNull JobStatus jobStatus) {
if (isTopStartedJobLocked(jobStatus)) {
// We intentionally don't pay attention to fg state changes after a TOP job has
// started.
if (DEBUG) {
@@ -1440,27 +1479,28 @@ public final class QuotaController extends StateController {
}
return;
}
if (DEBUG) Slog.v(TAG, "Starting to track " + jobStatus.toShortString());
synchronized (mLock) {
// Always track jobs, even when charging.
mRunningBgJobs.add(jobStatus);
if (shouldTrackLocked()) {
mBgJobCount++;
incrementJobCount(mPkg.userId, mPkg.packageName, 1);
if (mRunningBgJobs.size() == 1) {
// Started tracking the first job.
mStartTimeElapsed = sElapsedRealtimeClock.millis();
// Starting the timer means that all cached execution stats are now
// incorrect.
invalidateAllExecutionStatsLocked(mPkg.userId, mPkg.packageName);
scheduleCutoff();
}
if (DEBUG) {
Slog.v(TAG, "Starting to track " + jobStatus.toShortString());
}
// Always track jobs, even when charging.
mRunningBgJobs.add(jobStatus);
if (shouldTrackLocked()) {
mBgJobCount++;
incrementJobCount(mPkg.userId, mPkg.packageName, 1);
if (mRunningBgJobs.size() == 1) {
// Started tracking the first job.
mStartTimeElapsed = sElapsedRealtimeClock.millis();
// Starting the timer means that all cached execution stats are now incorrect.
invalidateAllExecutionStatsLocked(mPkg.userId, mPkg.packageName);
scheduleCutoff();
}
}
}
void stopTrackingJob(@NonNull JobStatus jobStatus) {
if (DEBUG) Slog.v(TAG, "Stopping tracking of " + jobStatus.toShortString());
if (DEBUG) {
Slog.v(TAG, "Stopping tracking of " + jobStatus.toShortString());
}
synchronized (mLock) {
if (mRunningBgJobs.size() == 0) {
// maybeStopTrackingJobLocked can be called when an app cancels a job, so a
@@ -1482,7 +1522,7 @@ public final class QuotaController extends StateController {
* Stops tracking all jobs and cancels any pending alarms. This should only be called if
* the Timer is not going to be used anymore.
*/
void dropEverything() {
void dropEverythingLocked() {
mRunningBgJobs.clear();
cancelCutoff();
}
@@ -1531,25 +1571,23 @@ public final class QuotaController extends StateController {
return !mChargeTracker.isCharging() && !mForegroundUids.get(mUid);
}
void onStateChanged(long nowElapsed, boolean isQuotaFree) {
synchronized (mLock) {
if (isQuotaFree) {
emitSessionLocked(nowElapsed);
} else if (shouldTrackLocked()) {
// Start timing from unplug.
if (mRunningBgJobs.size() > 0) {
mStartTimeElapsed = nowElapsed;
// NOTE: this does have the unfortunate consequence that if the device is
// repeatedly plugged in and unplugged, or an app changes foreground state
// very frequently, the job count for a package may be artificially high.
mBgJobCount = mRunningBgJobs.size();
incrementJobCount(mPkg.userId, mPkg.packageName, mBgJobCount);
// Starting the timer means that all cached execution stats are now
// incorrect.
invalidateAllExecutionStatsLocked(mPkg.userId, mPkg.packageName);
// Schedule cutoff since we're now actively tracking for quotas again.
scheduleCutoff();
}
void onStateChangedLocked(long nowElapsed, boolean isQuotaFree) {
if (isQuotaFree) {
emitSessionLocked(nowElapsed);
} else if (!isActive() && shouldTrackLocked()) {
// Start timing from unplug.
if (mRunningBgJobs.size() > 0) {
mStartTimeElapsed = nowElapsed;
// NOTE: this does have the unfortunate consequence that if the device is
// repeatedly plugged in and unplugged, or an app changes foreground state
// very frequently, the job count for a package may be artificially high.
mBgJobCount = mRunningBgJobs.size();
incrementJobCount(mPkg.userId, mPkg.packageName, mBgJobCount);
// Starting the timer means that all cached execution stats are now
// incorrect.
invalidateAllExecutionStatsLocked(mPkg.userId, mPkg.packageName);
// Schedule cutoff since we're now actively tracking for quotas again.
scheduleCutoff();
}
}
}
@@ -1604,7 +1642,6 @@ public final class QuotaController extends StateController {
pw.println(js.toShortString());
}
}
pw.decreaseIndent();
}
@@ -1667,7 +1704,9 @@ public final class QuotaController extends StateController {
@Override
public void onParoleStateChanged(final boolean isParoleOn) {
mInParole = isParoleOn;
if (DEBUG) Slog.i(TAG, "Global parole state now " + (isParoleOn ? "ON" : "OFF"));
if (DEBUG) {
Slog.i(TAG, "Global parole state now " + (isParoleOn ? "ON" : "OFF"));
}
// Update job bookkeeping out of band.
BackgroundThread.getHandler().post(() -> {
synchronized (mLock) {
@@ -1712,7 +1751,9 @@ public final class QuotaController extends StateController {
switch (msg.what) {
case MSG_REACHED_QUOTA: {
Package pkg = (Package) msg.obj;
if (DEBUG) Slog.d(TAG, "Checking if " + pkg + " has reached its quota.");
if (DEBUG) {
Slog.d(TAG, "Checking if " + pkg + " has reached its quota.");
}
long timeRemainingMs = getRemainingExecutionTimeLocked(pkg.userId,
pkg.packageName);
@@ -1737,7 +1778,9 @@ public final class QuotaController extends StateController {
break;
}
case MSG_CLEAN_UP_SESSIONS:
if (DEBUG) Slog.d(TAG, "Cleaning up timing sessions.");
if (DEBUG) {
Slog.d(TAG, "Cleaning up timing sessions.");
}
deleteObsoleteSessionsLocked();
maybeScheduleCleanupAlarmLocked();
@@ -1745,7 +1788,9 @@ public final class QuotaController extends StateController {
case MSG_CHECK_PACKAGE: {
String packageName = (String) msg.obj;
int userId = msg.arg1;
if (DEBUG) Slog.d(TAG, "Checking pkg " + string(userId, packageName));
if (DEBUG) {
Slog.d(TAG, "Checking pkg " + string(userId, packageName));
}
if (maybeUpdateConstraintForPkgLocked(userId, packageName)) {
mStateChangedListener.onControllerStateChanged();
}
@@ -1767,13 +1812,28 @@ public final class QuotaController extends StateController {
isQuotaFree = false;
}
// Update Timers first.
final int userIndex = mPkgTimers.indexOfKey(userId);
if (userIndex != -1) {
final int numPkgs = mPkgTimers.numPackagesForUser(userId);
for (int p = 0; p < numPkgs; ++p) {
Timer t = mPkgTimers.valueAt(userIndex, p);
if (t != null) {
t.onStateChanged(nowElapsed, isQuotaFree);
if (mPkgTimers.indexOfKey(userId) >= 0) {
ArraySet<String> packages = mUidToPackageCache.get(uid);
if (packages == null) {
try {
String[] pkgs = AppGlobals.getPackageManager()
.getPackagesForUid(uid);
if (pkgs != null) {
for (String pkg : pkgs) {
mUidToPackageCache.add(uid, pkg);
}
packages = mUidToPackageCache.get(uid);
}
} catch (RemoteException e) {
Slog.wtf(TAG, "Failed to get package list", e);
}
}
if (packages != null) {
for (int i = packages.size() - 1; i >= 0; --i) {
Timer t = mPkgTimers.get(userId, packages.valueAt(i));
if (t != null) {
t.onStateChangedLocked(nowElapsed, isQuotaFree);
}
}
}
}
@@ -1883,6 +1943,17 @@ public final class QuotaController extends StateController {
pw.println(mForegroundUids.toString());
pw.println();
pw.println("Cached UID->package map:");
pw.increaseIndent();
for (int i = 0; i < mUidToPackageCache.size(); ++i) {
final int uid = mUidToPackageCache.keyAt(i);
pw.print(uid);
pw.print(": ");
pw.println(mUidToPackageCache.get(uid));
}
pw.decreaseIndent();
pw.println();
mTrackedJobs.forEach((jobs) -> {
for (int j = 0; j < jobs.size(); j++) {
final JobStatus js = jobs.valueAt(j);
@@ -1936,6 +2007,29 @@ public final class QuotaController extends StateController {
}
}
}
pw.println("Cached execution stats:");
pw.increaseIndent();
for (int u = 0; u < mExecutionStatsCache.numUsers(); ++u) {
final int userId = mExecutionStatsCache.keyAt(u);
for (int p = 0; p < mExecutionStatsCache.numPackagesForUser(userId); ++p) {
final String pkgName = mExecutionStatsCache.keyAt(u, p);
ExecutionStats[] stats = mExecutionStatsCache.valueAt(u, p);
pw.println(string(userId, pkgName));
pw.increaseIndent();
for (int i = 0; i < stats.length; ++i) {
ExecutionStats executionStats = stats[i];
if (executionStats != null) {
pw.print(JobStatus.bucketName(i));
pw.print(": ");
pw.println(executionStats);
}
}
pw.decreaseIndent();
}
}
pw.decreaseIndent();
}
@Override
@@ -1995,6 +2089,49 @@ public final class QuotaController extends StateController {
}
}
ExecutionStats[] stats = mExecutionStatsCache.get(userId, pkgName);
if (stats != null) {
for (int bucketIndex = 0; bucketIndex < stats.length; ++bucketIndex) {
ExecutionStats es = stats[bucketIndex];
if (es == null) {
continue;
}
final long esToken = proto.start(
StateControllerProto.QuotaController.PackageStats.EXECUTION_STATS);
proto.write(
StateControllerProto.QuotaController.ExecutionStats.STANDBY_BUCKET,
bucketIndex);
proto.write(
StateControllerProto.QuotaController.ExecutionStats.EXPIRATION_TIME_ELAPSED,
es.expirationTimeElapsed);
proto.write(
StateControllerProto.QuotaController.ExecutionStats.WINDOW_SIZE_MS,
es.windowSizeMs);
proto.write(
StateControllerProto.QuotaController.ExecutionStats.EXECUTION_TIME_IN_WINDOW_MS,
es.executionTimeInWindowMs);
proto.write(
StateControllerProto.QuotaController.ExecutionStats.BG_JOB_COUNT_IN_WINDOW,
es.bgJobCountInWindow);
proto.write(
StateControllerProto.QuotaController.ExecutionStats.EXECUTION_TIME_IN_MAX_PERIOD_MS,
es.executionTimeInMaxPeriodMs);
proto.write(
StateControllerProto.QuotaController.ExecutionStats.BG_JOB_COUNT_IN_MAX_PERIOD,
es.bgJobCountInMaxPeriod);
proto.write(
StateControllerProto.QuotaController.ExecutionStats.QUOTA_CUTOFF_TIME_ELAPSED,
es.quotaCutoffTimeElapsed);
proto.write(
StateControllerProto.QuotaController.ExecutionStats.JOB_COUNT_EXPIRATION_TIME_ELAPSED,
es.jobCountExpirationTimeElapsed);
proto.write(
StateControllerProto.QuotaController.ExecutionStats.JOB_COUNT_IN_ALLOWED_TIME,
es.jobCountInAllowedTime);
proto.end(esToken);
}
}
proto.end(psToken);
}
}

View File

@@ -58,6 +58,7 @@ import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.IPackageManager;
import android.content.pm.PackageManagerInternal;
import android.os.BatteryManager;
import android.os.BatteryManagerInternal;
@@ -224,50 +225,55 @@ public class QuotaControllerTest {
}
private void setProcessState(int procState) {
setProcessState(procState, mSourceUid);
}
private void setProcessState(int procState, int uid) {
try {
doReturn(procState).when(mActivityMangerInternal).getUidProcessState(mSourceUid);
doReturn(procState).when(mActivityMangerInternal).getUidProcessState(uid);
SparseBooleanArray foregroundUids = mQuotaController.getForegroundUids();
spyOn(foregroundUids);
mUidObserver.onUidStateChanged(mSourceUid, procState, 0);
mUidObserver.onUidStateChanged(uid, procState, 0);
if (procState <= ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE) {
verify(foregroundUids, timeout(SECOND_IN_MILLIS).times(1))
.put(eq(mSourceUid), eq(true));
assertTrue(foregroundUids.get(mSourceUid));
verify(foregroundUids, timeout(2 * SECOND_IN_MILLIS).times(1))
.put(eq(uid), eq(true));
assertTrue(foregroundUids.get(uid));
} else {
verify(foregroundUids, timeout(SECOND_IN_MILLIS).times(1)).delete(eq(mSourceUid));
assertFalse(foregroundUids.get(mSourceUid));
verify(foregroundUids, timeout(2 * SECOND_IN_MILLIS).times(1)).delete(eq(uid));
assertFalse(foregroundUids.get(uid));
}
} catch (RemoteException e) {
fail("registerUidObserver threw exception: " + e.getMessage());
}
}
private void setStandbyBucket(int bucketIndex) {
int bucket;
private int bucketIndexToUsageStatsBucket(int bucketIndex) {
switch (bucketIndex) {
case ACTIVE_INDEX:
bucket = UsageStatsManager.STANDBY_BUCKET_ACTIVE;
break;
return UsageStatsManager.STANDBY_BUCKET_ACTIVE;
case WORKING_INDEX:
bucket = UsageStatsManager.STANDBY_BUCKET_WORKING_SET;
break;
return UsageStatsManager.STANDBY_BUCKET_WORKING_SET;
case FREQUENT_INDEX:
bucket = UsageStatsManager.STANDBY_BUCKET_FREQUENT;
break;
return UsageStatsManager.STANDBY_BUCKET_FREQUENT;
case RARE_INDEX:
bucket = UsageStatsManager.STANDBY_BUCKET_RARE;
break;
return UsageStatsManager.STANDBY_BUCKET_RARE;
default:
bucket = UsageStatsManager.STANDBY_BUCKET_NEVER;
return UsageStatsManager.STANDBY_BUCKET_NEVER;
}
}
private void setStandbyBucket(int bucketIndex) {
when(mUsageStatsManager.getAppStandbyBucket(eq(SOURCE_PACKAGE), eq(SOURCE_USER_ID),
anyLong())).thenReturn(bucket);
anyLong())).thenReturn(bucketIndexToUsageStatsBucket(bucketIndex));
}
private void setStandbyBucket(int bucketIndex, JobStatus... jobs) {
setStandbyBucket(bucketIndex);
for (JobStatus job : jobs) {
job.setStandbyBucket(bucketIndex);
when(mUsageStatsManager.getAppStandbyBucket(
eq(job.getSourcePackageName()), eq(job.getSourceUserId()), anyLong()))
.thenReturn(bucketIndexToUsageStatsBucket(bucketIndex));
}
}
@@ -283,8 +289,13 @@ public class QuotaControllerTest {
new ComponentName(mContext, "TestQuotaJobService"))
.setMinimumLatency(Math.abs(jobId) + 1)
.build();
return createJobStatus(testTag, SOURCE_PACKAGE, CALLING_UID, jobInfo);
}
private JobStatus createJobStatus(String testTag, String packageName, int callingUid,
JobInfo jobInfo) {
JobStatus js = JobStatus.createFromJobInfo(
jobInfo, CALLING_UID, SOURCE_PACKAGE, SOURCE_USER_ID, testTag);
jobInfo, callingUid, packageName, SOURCE_USER_ID, testTag);
// Make sure tests aren't passing just because the default bucket is likely ACTIVE.
js.setStandbyBucket(FREQUENT_INDEX);
return js;
@@ -934,6 +945,115 @@ public class QuotaControllerTest {
assertFalse(mQuotaController.isWithinQuotaLocked(0, "com.android.test", WORKING_INDEX));
}
@Test
public void testIsWithinQuotaLocked_UnderDuration_UnderJobCount_MultiStateChange_BelowFGS() {
setDischarging();
JobStatus jobStatus = createJobStatus(
"testIsWithinQuotaLocked_UnderDuration_UnderJobCount_MultiStateChange_BelowFGS", 1);
setStandbyBucket(ACTIVE_INDEX, jobStatus);
setProcessState(ActivityManager.PROCESS_STATE_BACKUP);
mQuotaController.maybeStartTrackingJobLocked(jobStatus, null);
mQuotaController.prepareForExecutionLocked(jobStatus);
for (int i = 0; i < 20; ++i) {
advanceElapsedClock(SECOND_IN_MILLIS);
setProcessState(ActivityManager.PROCESS_STATE_SERVICE);
setProcessState(ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND);
}
mQuotaController.maybeStopTrackingJobLocked(jobStatus, null, false);
advanceElapsedClock(15 * SECOND_IN_MILLIS);
mQuotaController.maybeStartTrackingJobLocked(jobStatus, null);
mQuotaController.prepareForExecutionLocked(jobStatus);
for (int i = 0; i < 20; ++i) {
advanceElapsedClock(SECOND_IN_MILLIS);
setProcessState(ActivityManager.PROCESS_STATE_SERVICE);
setProcessState(ActivityManager.PROCESS_STATE_RECEIVER);
}
mQuotaController.maybeStopTrackingJobLocked(jobStatus, null, false);
advanceElapsedClock(10 * MINUTE_IN_MILLIS + 30 * SECOND_IN_MILLIS);
assertEquals(2, mQuotaController.getExecutionStatsLocked(
SOURCE_USER_ID, SOURCE_PACKAGE, ACTIVE_INDEX).jobCountInAllowedTime);
assertTrue(mQuotaController.isWithinQuotaLocked(jobStatus));
}
@Test
public void testIsWithinQuotaLocked_UnderDuration_UnderJobCount_MultiStateChange_SeparateApps()
throws Exception {
setDischarging();
final String unaffectedPkgName = "com.android.unaffected";
final int unaffectedUid = 10987;
JobInfo unaffectedJobInfo = new JobInfo.Builder(1,
new ComponentName(unaffectedPkgName, "foo"))
.build();
JobStatus unaffected = createJobStatus(
"testIsWithinQuotaLocked_UnderDuration_UnderJobCount_MultiStateChange_SeparateApps",
unaffectedPkgName, unaffectedUid, unaffectedJobInfo);
setStandbyBucket(FREQUENT_INDEX, unaffected);
setProcessState(ActivityManager.PROCESS_STATE_SERVICE, unaffectedUid);
final String fgChangerPkgName = "com.android.foreground.changer";
final int fgChangerUid = 10234;
JobInfo fgChangerJobInfo = new JobInfo.Builder(2,
new ComponentName(fgChangerPkgName, "foo"))
.build();
JobStatus fgStateChanger = createJobStatus(
"testIsWithinQuotaLocked_UnderDuration_UnderJobCount_MultiStateChange_SeparateApps",
fgChangerPkgName, fgChangerUid, fgChangerJobInfo);
setStandbyBucket(ACTIVE_INDEX, fgStateChanger);
setProcessState(ActivityManager.PROCESS_STATE_BACKUP, fgChangerUid);
IPackageManager packageManager = AppGlobals.getPackageManager();
spyOn(packageManager);
doReturn(new String[]{unaffectedPkgName})
.when(packageManager).getPackagesForUid(unaffectedUid);
doReturn(new String[]{fgChangerPkgName})
.when(packageManager).getPackagesForUid(fgChangerUid);
mQuotaController.maybeStartTrackingJobLocked(unaffected, null);
mQuotaController.prepareForExecutionLocked(unaffected);
mQuotaController.maybeStartTrackingJobLocked(fgStateChanger, null);
mQuotaController.prepareForExecutionLocked(fgStateChanger);
for (int i = 0; i < 20; ++i) {
advanceElapsedClock(SECOND_IN_MILLIS);
setProcessState(ActivityManager.PROCESS_STATE_TOP, fgChangerUid);
setProcessState(ActivityManager.PROCESS_STATE_TOP_SLEEPING, fgChangerUid);
}
mQuotaController.maybeStopTrackingJobLocked(fgStateChanger, null, false);
advanceElapsedClock(15 * SECOND_IN_MILLIS);
mQuotaController.maybeStartTrackingJobLocked(fgStateChanger, null);
mQuotaController.prepareForExecutionLocked(fgStateChanger);
for (int i = 0; i < 20; ++i) {
advanceElapsedClock(SECOND_IN_MILLIS);
setProcessState(ActivityManager.PROCESS_STATE_TOP, fgChangerUid);
setProcessState(ActivityManager.PROCESS_STATE_TOP_SLEEPING, fgChangerUid);
}
mQuotaController.maybeStopTrackingJobLocked(fgStateChanger, null, false);
mQuotaController.maybeStopTrackingJobLocked(unaffected, null, false);
assertTrue(mQuotaController.isWithinQuotaLocked(unaffected));
assertFalse(mQuotaController.isWithinQuotaLocked(fgStateChanger));
assertEquals(1,
mQuotaController.getTimingSessions(SOURCE_USER_ID, unaffectedPkgName).size());
assertEquals(42,
mQuotaController.getTimingSessions(SOURCE_USER_ID, fgChangerPkgName).size());
for (int i = ACTIVE_INDEX; i < RARE_INDEX; ++i) {
assertEquals(42, mQuotaController.getExecutionStatsLocked(
SOURCE_USER_ID, fgChangerPkgName, i).jobCountInAllowedTime);
assertEquals(1, mQuotaController.getExecutionStatsLocked(
SOURCE_USER_ID, unaffectedPkgName, i).jobCountInAllowedTime);
}
}
@Test
public void testMaybeScheduleCleanupAlarmLocked() {
// No sessions saved yet.