Loading core/java/android/util/SparseSetArray.java +7 −0 Original line number Diff line number Diff line Loading @@ -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. */ Loading core/proto/android/server/jobscheduler.proto +50 −0 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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; } Loading services/core/java/com/android/server/job/controllers/QuotaController.java +202 −65 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading Loading @@ -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(); } Loading Loading @@ -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 Loading Loading @@ -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. Loading Loading @@ -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); Loading Loading @@ -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; Loading @@ -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 Loading Loading @@ -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); } Loading @@ -657,6 +686,7 @@ public final class QuotaController extends StateController { } mExecutionStatsCache.delete(userId, packageName); mForegroundUids.delete(uid); mUidToPackageCache.remove(uid); } @Override Loading @@ -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) { Loading @@ -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); } Loading @@ -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); Loading Loading @@ -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 Loading @@ -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(); } Loading Loading @@ -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); Loading Loading @@ -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, Loading Loading @@ -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) { Loading @@ -1440,8 +1479,9 @@ public final class QuotaController extends StateController { } return; } if (DEBUG) Slog.v(TAG, "Starting to track " + jobStatus.toShortString()); synchronized (mLock) { if (DEBUG) { Slog.v(TAG, "Starting to track " + jobStatus.toShortString()); } // Always track jobs, even when charging. mRunningBgJobs.add(jobStatus); if (shouldTrackLocked()) { Loading @@ -1450,17 +1490,17 @@ public final class QuotaController extends StateController { if (mRunningBgJobs.size() == 1) { // Started tracking the first job. mStartTimeElapsed = sElapsedRealtimeClock.millis(); // Starting the timer means that all cached execution stats are now // incorrect. // 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 Loading @@ -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(); } Loading Loading @@ -1531,11 +1571,10 @@ public final class QuotaController extends StateController { return !mChargeTracker.isCharging() && !mForegroundUids.get(mUid); } void onStateChanged(long nowElapsed, boolean isQuotaFree) { synchronized (mLock) { void onStateChangedLocked(long nowElapsed, boolean isQuotaFree) { if (isQuotaFree) { emitSessionLocked(nowElapsed); } else if (shouldTrackLocked()) { } else if (!isActive() && shouldTrackLocked()) { // Start timing from unplug. if (mRunningBgJobs.size() > 0) { mStartTimeElapsed = nowElapsed; Loading @@ -1552,7 +1591,6 @@ public final class QuotaController extends StateController { } } } } void rescheduleCutoff() { cancelCutoff(); Loading Loading @@ -1604,7 +1642,6 @@ public final class QuotaController extends StateController { pw.println(js.toShortString()); } } pw.decreaseIndent(); } Loading Loading @@ -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) { Loading Loading @@ -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); Loading @@ -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(); Loading @@ -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(); } Loading @@ -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 (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.onStateChanged(nowElapsed, isQuotaFree); t.onStateChangedLocked(nowElapsed, isQuotaFree); } } } } Loading Loading @@ -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); Loading Loading @@ -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 Loading Loading @@ -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); } } Loading Loading
core/java/android/util/SparseSetArray.java +7 −0 Original line number Diff line number Diff line Loading @@ -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. */ Loading
core/proto/android/server/jobscheduler.proto +50 −0 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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; } Loading
services/core/java/com/android/server/job/controllers/QuotaController.java +202 −65 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading Loading @@ -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(); } Loading Loading @@ -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 Loading Loading @@ -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. Loading Loading @@ -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); Loading Loading @@ -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; Loading @@ -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 Loading Loading @@ -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); } Loading @@ -657,6 +686,7 @@ public final class QuotaController extends StateController { } mExecutionStatsCache.delete(userId, packageName); mForegroundUids.delete(uid); mUidToPackageCache.remove(uid); } @Override Loading @@ -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) { Loading @@ -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); } Loading @@ -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); Loading Loading @@ -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 Loading @@ -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(); } Loading Loading @@ -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); Loading Loading @@ -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, Loading Loading @@ -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) { Loading @@ -1440,8 +1479,9 @@ public final class QuotaController extends StateController { } return; } if (DEBUG) Slog.v(TAG, "Starting to track " + jobStatus.toShortString()); synchronized (mLock) { if (DEBUG) { Slog.v(TAG, "Starting to track " + jobStatus.toShortString()); } // Always track jobs, even when charging. mRunningBgJobs.add(jobStatus); if (shouldTrackLocked()) { Loading @@ -1450,17 +1490,17 @@ public final class QuotaController extends StateController { if (mRunningBgJobs.size() == 1) { // Started tracking the first job. mStartTimeElapsed = sElapsedRealtimeClock.millis(); // Starting the timer means that all cached execution stats are now // incorrect. // 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 Loading @@ -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(); } Loading Loading @@ -1531,11 +1571,10 @@ public final class QuotaController extends StateController { return !mChargeTracker.isCharging() && !mForegroundUids.get(mUid); } void onStateChanged(long nowElapsed, boolean isQuotaFree) { synchronized (mLock) { void onStateChangedLocked(long nowElapsed, boolean isQuotaFree) { if (isQuotaFree) { emitSessionLocked(nowElapsed); } else if (shouldTrackLocked()) { } else if (!isActive() && shouldTrackLocked()) { // Start timing from unplug. if (mRunningBgJobs.size() > 0) { mStartTimeElapsed = nowElapsed; Loading @@ -1552,7 +1591,6 @@ public final class QuotaController extends StateController { } } } } void rescheduleCutoff() { cancelCutoff(); Loading Loading @@ -1604,7 +1642,6 @@ public final class QuotaController extends StateController { pw.println(js.toShortString()); } } pw.decreaseIndent(); } Loading Loading @@ -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) { Loading Loading @@ -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); Loading @@ -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(); Loading @@ -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(); } Loading @@ -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 (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.onStateChanged(nowElapsed, isQuotaFree); t.onStateChangedLocked(nowElapsed, isQuotaFree); } } } } Loading Loading @@ -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); Loading Loading @@ -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 Loading Loading @@ -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); } } Loading