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
diff --git a/core/java/android/util/SparseSetArray.java b/core/java/android/util/SparseSetArray.java
index 680e85f..c1873d7 100644
--- a/core/java/android/util/SparseSetArray.java
+++ b/core/java/android/util/SparseSetArray.java
@@ -44,6 +44,13 @@
}
/**
+ * Removes all mappings from this SparseSetArray.
+ */
+ public void clear() {
+ mData.clear();
+ }
+
+ /**
* @return whether a value exists at index n.
*/
public boolean contains(int n, T value) {
diff --git a/core/proto/android/server/jobscheduler.proto b/core/proto/android/server/jobscheduler.proto
index 1e0b0d8..dc2e6d5 100644
--- a/core/proto/android/server/jobscheduler.proto
+++ b/core/proto/android/server/jobscheduler.proto
@@ -478,6 +478,54 @@
}
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 @@
optional Timer timer = 2;
repeated TimingSession saved_sessions = 3;
+
+ repeated ExecutionStats execution_stats = 4;
}
repeated PackageStats package_stats = 5;
}
diff --git a/services/core/java/com/android/server/job/controllers/QuotaController.java b/services/core/java/com/android/server/job/controllers/QuotaController.java
index 1820acf..ccd1db4 100644
--- a/services/core/java/com/android/server/job/controllers/QuotaController.java
+++ b/services/core/java/com/android/server/job/controllers/QuotaController.java
@@ -29,6 +29,7 @@
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.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 @@
.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 @@
/** 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 @@
}
};
+ 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 @@
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 @@
@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 @@
timer = new Timer(uid, userId, packageName);
mPkgTimers.add(userId, packageName, timer);
}
- timer.startTrackingJob(jobStatus);
+ timer.startTrackingJobLocked(jobStatus);
}
@Override
@@ -645,7 +674,7 @@
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 @@
}
mExecutionStatsCache.delete(userId, packageName);
mForegroundUids.delete(uid);
+ mUidToPackageCache.remove(uid);
}
@Override
@@ -666,6 +696,7 @@
mTimingSessions.delete(userId);
mInQuotaAlarmListeners.delete(userId);
mExecutionStatsCache.delete(userId);
+ mUidToPackageCache.clear();
}
private boolean isUidInForeground(int uid) {
@@ -678,7 +709,7 @@
}
/** @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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
}
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 @@
* 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 @@
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 @@
pw.println(js.toShortString());
}
}
-
pw.decreaseIndent();
}
@@ -1667,7 +1704,9 @@
@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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
}
}
}
+
+ 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 @@
}
}
+ 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);
}
}
diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java
index 08f6a37..f492d13 100644
--- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java
@@ -58,6 +58,7 @@
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 @@
}
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 @@
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;
@@ -935,6 +946,115 @@
}
@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.
mQuotaController.maybeScheduleCleanupAlarmLocked();