diff options
4 files changed, 399 insertions, 85 deletions
diff --git a/core/java/android/util/SparseSetArray.java b/core/java/android/util/SparseSetArray.java index 680e85fa2ba8..c1873d76f46f 100644 --- a/core/java/android/util/SparseSetArray.java +++ b/core/java/android/util/SparseSetArray.java @@ -44,6 +44,13 @@ public class SparseSetArray<T> { } /** + * 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 1e0b0d84e04d..dc2e6d5d2d41 100644 --- a/core/proto/android/server/jobscheduler.proto +++ b/core/proto/android/server/jobscheduler.proto @@ -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; } 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 1820acfaf293..ccd1db496675 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.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); } } 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 08f6a372de86..f492d13f371f 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.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; @@ -935,6 +946,115 @@ public class QuotaControllerTest { } @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(); |