Loading core/proto/android/server/jobscheduler.proto +9 −4 Original line number Diff line number Diff line Loading @@ -403,18 +403,23 @@ message StateControllerProto { optional bool is_charging = 1; optional bool is_in_parole = 2; // List of UIDs currently in the foreground. repeated int32 foreground_uids = 3; message TrackedJob { option (.android.msg_privacy).dest = DEST_AUTOMATIC; optional JobStatusShortInfoProto info = 1; optional int32 source_uid = 2; optional JobStatusDumpProto.Bucket effective_standby_bucket = 3; optional bool has_quota = 4; // If the job started while the app was in the TOP state. optional bool is_top_started_job = 4; optional bool has_quota = 5; // The amount of time that this job has remaining in its quota. This // can be negative if the job is out of quota. optional int64 remaining_quota_ms = 5; optional int64 remaining_quota_ms = 6; } repeated TrackedJob tracked_jobs = 3; repeated TrackedJob tracked_jobs = 4; message Package { option (.android.msg_privacy).dest = DEST_AUTOMATIC; Loading Loading @@ -456,7 +461,7 @@ message StateControllerProto { repeated TimingSession saved_sessions = 3; } repeated PackageStats package_stats = 4; repeated PackageStats package_stats = 5; } message StorageController { option (.android.msg_privacy).dest = DEST_AUTOMATIC; Loading services/core/java/com/android/server/job/controllers/QuotaController.java +229 −19 Original line number Diff line number Diff line Loading @@ -26,7 +26,10 @@ import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.UserIdInt; import android.app.ActivityManager; import android.app.ActivityManagerInternal; import android.app.AlarmManager; import android.app.IUidObserver; import android.app.usage.UsageStatsManagerInternal; import android.app.usage.UsageStatsManagerInternal.AppIdleStateChangeListener; import android.content.BroadcastReceiver; Loading @@ -38,12 +41,14 @@ import android.os.BatteryManagerInternal; import android.os.Handler; import android.os.Looper; import android.os.Message; import android.os.RemoteException; import android.os.UserHandle; import android.util.ArrayMap; import android.util.ArraySet; import android.util.Log; import android.util.Slog; import android.util.SparseArray; import android.util.SparseBooleanArray; import android.util.proto.ProtoOutputStream; import com.android.internal.annotations.VisibleForTesting; Loading @@ -69,6 +74,11 @@ import java.util.function.Predicate; * bucket, it will be eligible to run. When a job's bucket changes, its new quota is immediately * applied to it. * * Jobs are throttled while an app is not in a foreground state. All jobs are allowed to run * freely when an app enters the foreground state and are restricted when the app leaves the * foreground state. However, jobs that are started while the app is in the TOP state are not * restricted regardless of the app's state change. * * Test: atest com.android.server.job.controllers.QuotaControllerTest */ public final class QuotaController extends StateController { Loading Loading @@ -97,6 +107,12 @@ public final class QuotaController extends StateController { data.put(packageName, obj); } public void clear() { for (int i = 0; i < mData.size(); ++i) { mData.valueAt(i).clear(); } } /** Removes all the data for the user, if there was any. */ public void delete(int userId) { mData.delete(userId); Loading @@ -119,6 +135,11 @@ public final class QuotaController extends StateController { return null; } /** @see SparseArray#indexOfKey */ public int indexOfKey(int userId) { return mData.indexOfKey(userId); } /** Returns the userId at the given index. */ public int keyAt(int index) { return mData.keyAt(index); Loading Loading @@ -294,6 +315,17 @@ public final class QuotaController extends StateController { /** Cached calculation results for each app, with the standby buckets as the array indices. */ private final UserPackageMap<ExecutionStats[]> mExecutionStatsCache = new UserPackageMap<>(); /** List of UIDs currently in the foreground. */ private final SparseBooleanArray mForegroundUids = new SparseBooleanArray(); /** * 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 * fine. */ private final ArraySet<JobStatus> mTopStartedJobs = new ArraySet<>(); private final ActivityManagerInternal mActivityManagerInternal; private final AlarmManager mAlarmManager; private final ChargingTracker mChargeTracker; private final Handler mHandler; Loading Loading @@ -343,6 +375,29 @@ public final class QuotaController extends StateController { } }; private final IUidObserver mUidObserver = new IUidObserver.Stub() { @Override public void onUidStateChanged(int uid, int procState, long procStateSeq) { mHandler.obtainMessage(MSG_UID_PROCESS_STATE_CHANGED, uid, procState).sendToTarget(); } @Override public void onUidGone(int uid, boolean disabled) { } @Override public void onUidActive(int uid) { } @Override public void onUidIdle(int uid, boolean disabled) { } @Override public void onUidCachedChanged(int uid, boolean cached) { } }; /** * The rolling window size for each standby bucket. Within each window, an app will have 10 * minutes to run its jobs. Loading @@ -363,12 +418,15 @@ public final class QuotaController extends StateController { private static final int MSG_CLEAN_UP_SESSIONS = 1; /** Check if a package is now within its quota. */ private static final int MSG_CHECK_PACKAGE = 2; /** Process state for a UID has changed. */ private static final int MSG_UID_PROCESS_STATE_CHANGED = 3; public QuotaController(JobSchedulerService service) { super(service); mHandler = new QcHandler(mContext.getMainLooper()); mChargeTracker = new ChargingTracker(); mChargeTracker.startTracking(); mActivityManagerInternal = LocalServices.getService(ActivityManagerInternal.class); mAlarmManager = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE); // Set up the app standby bucketing tracker Loading @@ -376,6 +434,14 @@ public final class QuotaController extends StateController { UsageStatsManagerInternal.class); usageStats.addAppIdleStateChangeListener(new StandbyTracker()); try { ActivityManager.getService().registerUidObserver(mUidObserver, ActivityManager.UID_OBSERVER_PROCSTATE, ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE, null); } catch (RemoteException e) { // ignored; both services live in system_server } onConstantsUpdatedLocked(); } Loading @@ -399,11 +465,15 @@ public final class QuotaController extends StateController { if (DEBUG) Slog.d(TAG, "Prepping for " + jobStatus.toShortString()); final int userId = jobStatus.getSourceUserId(); final String packageName = jobStatus.getSourcePackageName(); final int uid = jobStatus.getSourceUid(); Timer timer = mPkgTimers.get(userId, packageName); if (timer == null) { timer = new Timer(userId, packageName); timer = new Timer(uid, userId, packageName); mPkgTimers.add(userId, packageName, timer); } if (mActivityManagerInternal.getUidProcessState(uid) == ActivityManager.PROCESS_STATE_TOP) { mTopStartedJobs.add(jobStatus); } timer.startTrackingJob(jobStatus); } Loading @@ -421,6 +491,7 @@ public final class QuotaController extends StateController { if (jobs != null) { jobs.remove(jobStatus); } mTopStartedJobs.remove(jobStatus); } } Loading Loading @@ -511,6 +582,7 @@ public final class QuotaController extends StateController { mInQuotaAlarmListeners.delete(userId, packageName); } mExecutionStatsCache.delete(userId, packageName); mForegroundUids.delete(uid); } @Override Loading @@ -522,6 +594,20 @@ public final class QuotaController extends StateController { mExecutionStatsCache.delete(userId); } private boolean isUidInForeground(int uid) { if (UserHandle.isCore(uid)) { return true; } synchronized (mLock) { return mForegroundUids.get(uid); } } /** @return true if the job was started while the app was in the TOP state. */ private boolean isTopStartedJob(@NonNull final JobStatus jobStatus) { return mTopStartedJobs.contains(jobStatus); } /** * Returns an appropriate standby bucket for the job, taking into account any standby * exemptions. Loading @@ -537,8 +623,14 @@ public final class QuotaController extends StateController { private boolean isWithinQuotaLocked(@NonNull final JobStatus jobStatus) { final int standbyBucket = getEffectiveStandbyBucket(jobStatus); // Jobs for the active app should always be able to run. return jobStatus.uidActive || isWithinQuotaLocked( 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) || isUidInForeground(jobStatus.getSourceUid()) || isWithinQuotaLocked( jobStatus.getSourceUserId(), jobStatus.getSourcePackageName(), standbyBucket); } Loading Loading @@ -800,7 +892,7 @@ public final class QuotaController extends StateController { final boolean isCharging = mChargeTracker.isCharging(); if (DEBUG) Slog.d(TAG, "handleNewChargingStateLocked: " + isCharging); // Deal with Timers first. mPkgTimers.forEach((t) -> t.onChargingChanged(nowElapsed, isCharging)); mPkgTimers.forEach((t) -> t.onStateChanged(nowElapsed, isCharging)); // Now update jobs. maybeUpdateAllConstraintsLocked(); } Loading Loading @@ -837,10 +929,15 @@ 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 (js.uidActive) { // Jobs for the active app should always be able to run. if (isTopStartedJob(js)) { // Job was started while the app was in the TOP state so we should allow it to // finish. changed |= js.setQuotaConstraintSatisfied(true); } else if (realStandbyBucket == getEffectiveStandbyBucket(js)) { } else if (realStandbyBucket != ACTIVE_INDEX && realStandbyBucket == getEffectiveStandbyBucket(js)) { // An app in the ACTIVE bucket may be out of quota while the job could be in quota // for some reason. Therefore, avoid setting the real value here and check each job // individually. changed |= js.setQuotaConstraintSatisfied(realInQuota); } else { // This job is somehow exempted. Need to determine its own quota status. Loading @@ -854,7 +951,7 @@ public final class QuotaController extends StateController { maybeScheduleStartAlarmLocked(userId, packageName, realStandbyBucket); } else { QcAlarmListener alarmListener = mInQuotaAlarmListeners.get(userId, packageName); if (alarmListener != null) { if (alarmListener != null && alarmListener.isWaiting()) { mAlarmManager.cancel(alarmListener); // Set the trigger time to 0 so that the alarm doesn't think it's still waiting. alarmListener.setTriggerTime(0); Loading @@ -863,6 +960,56 @@ public final class QuotaController extends StateController { return changed; } private class UidConstraintUpdater implements Consumer<JobStatus> { private final UserPackageMap<Integer> mToScheduleStartAlarms = new UserPackageMap<>(); public boolean wasJobChanged; @Override public void accept(JobStatus jobStatus) { wasJobChanged |= jobStatus.setQuotaConstraintSatisfied(isWithinQuotaLocked(jobStatus)); final int userId = jobStatus.getSourceUserId(); final String packageName = jobStatus.getSourcePackageName(); final int realStandbyBucket = jobStatus.getStandbyBucket(); if (isWithinQuotaLocked(userId, packageName, realStandbyBucket)) { QcAlarmListener alarmListener = mInQuotaAlarmListeners.get(userId, packageName); if (alarmListener != null && alarmListener.isWaiting()) { mAlarmManager.cancel(alarmListener); // Set the trigger time to 0 so that the alarm doesn't think it's still waiting. alarmListener.setTriggerTime(0); } } else { mToScheduleStartAlarms.add(userId, packageName, realStandbyBucket); } } void postProcess() { for (int u = 0; u < mToScheduleStartAlarms.numUsers(); ++u) { final int userId = mToScheduleStartAlarms.keyAt(u); for (int p = 0; p < mToScheduleStartAlarms.numPackagesForUser(userId); ++p) { final String packageName = mToScheduleStartAlarms.keyAt(u, p); final int standbyBucket = mToScheduleStartAlarms.get(userId, packageName); maybeScheduleStartAlarmLocked(userId, packageName, standbyBucket); } } } void reset() { wasJobChanged = false; mToScheduleStartAlarms.clear(); } } private final UidConstraintUpdater mUpdateUidConstraints = new UidConstraintUpdater(); private boolean maybeUpdateConstraintForUidLocked(final int uid) { mService.getJobStore().forEachJobForSourceUid(uid, mUpdateUidConstraints); mUpdateUidConstraints.postProcess(); boolean changed = mUpdateUidConstraints.wasJobChanged; mUpdateUidConstraints.reset(); return changed; } /** * Maybe schedule a non-wakeup alarm for the next time this package will have quota to run * again. This should only be called if the package is already out of quota. Loading Loading @@ -1052,6 +1199,7 @@ public final class QuotaController extends StateController { private final class Timer { private final Package mPkg; private final int mUid; // List of jobs currently running for this app that started when the app wasn't in the // foreground. Loading @@ -1059,16 +1207,18 @@ public final class QuotaController extends StateController { private long mStartTimeElapsed; private int mBgJobCount; Timer(int userId, String packageName) { Timer(int uid, int userId, String packageName) { mPkg = new Package(userId, packageName); mUid = uid; } void startTrackingJob(@NonNull JobStatus jobStatus) { if (jobStatus.uidActive) { // We intentionally don't pay attention to fg state changes after a job has started. if (isTopStartedJob(jobStatus)) { // We intentionally don't pay attention to fg state changes after a TOP job has // started. if (DEBUG) { Slog.v(TAG, "Timer ignoring " + jobStatus.toShortString() + " because uidActive"); "Timer ignoring " + jobStatus.toShortString() + " because isTop"); } return; } Loading @@ -1076,7 +1226,7 @@ public final class QuotaController extends StateController { synchronized (mLock) { // Always track jobs, even when charging. mRunningBgJobs.add(jobStatus); if (!mChargeTracker.isCharging()) { if (shouldTrackLocked()) { mBgJobCount++; if (mRunningBgJobs.size() == 1) { // Started tracking the first job. Loading Loading @@ -1142,6 +1292,10 @@ public final class QuotaController extends StateController { } } boolean isRunning(JobStatus jobStatus) { return mRunningBgJobs.contains(jobStatus); } long getCurrentDuration(long nowElapsed) { synchronized (mLock) { return !isActive() ? 0 : nowElapsed - mStartTimeElapsed; Loading @@ -1154,17 +1308,21 @@ public final class QuotaController extends StateController { } } void onChargingChanged(long nowElapsed, boolean isCharging) { private boolean shouldTrackLocked() { return !mChargeTracker.isCharging() && !mForegroundUids.get(mUid); } void onStateChanged(long nowElapsed, boolean isQuotaFree) { synchronized (mLock) { if (isCharging) { if (isQuotaFree) { emitSessionLocked(nowElapsed); } else { } 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, the job count for a package may be // artificially high. // 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(); // Starting the timer means that all cached execution stats are now // incorrect. Loading Loading @@ -1371,6 +1529,38 @@ public final class QuotaController extends StateController { } break; } case MSG_UID_PROCESS_STATE_CHANGED: { final int uid = msg.arg1; final int procState = msg.arg2; final int userId = UserHandle.getUserId(uid); final long nowElapsed = sElapsedRealtimeClock.millis(); synchronized (mLock) { boolean isQuotaFree; if (procState <= ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE) { mForegroundUids.put(uid, true); isQuotaFree = true; } else { mForegroundUids.delete(uid); 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 (maybeUpdateConstraintForUidLocked(uid)) { mStateChangedListener.onControllerStateChanged(); } } break; } } } } Loading Loading @@ -1418,6 +1608,12 @@ public final class QuotaController extends StateController { return mBucketPeriodsMs; } @VisibleForTesting @NonNull SparseBooleanArray getForegroundUids() { return mForegroundUids; } @VisibleForTesting @NonNull Handler getHandler() { Loading Loading @@ -1450,6 +1646,10 @@ public final class QuotaController extends StateController { pw.println("In parole: " + mInParole); pw.println(); pw.print("Foreground UIDs: "); pw.println(mForegroundUids.toString()); pw.println(); mTrackedJobs.forEach((jobs) -> { for (int j = 0; j < jobs.size(); j++) { final JobStatus js = jobs.valueAt(j); Loading @@ -1460,6 +1660,9 @@ public final class QuotaController extends StateController { js.printUniqueId(pw); pw.print(" from "); UserHandle.formatUid(pw, js.getSourceUid()); if (mTopStartedJobs.contains(js)) { pw.print(" (TOP)"); } pw.println(); pw.increaseIndent(); Loading Loading @@ -1511,6 +1714,11 @@ public final class QuotaController extends StateController { proto.write(StateControllerProto.QuotaController.IS_CHARGING, mChargeTracker.isCharging()); proto.write(StateControllerProto.QuotaController.IS_IN_PAROLE, mInParole); for (int i = 0; i < mForegroundUids.size(); ++i) { proto.write(StateControllerProto.QuotaController.FOREGROUND_UIDS, mForegroundUids.keyAt(i)); } mTrackedJobs.forEach((jobs) -> { for (int j = 0; j < jobs.size(); j++) { final JobStatus js = jobs.valueAt(j); Loading @@ -1526,6 +1734,8 @@ public final class QuotaController extends StateController { proto.write( StateControllerProto.QuotaController.TrackedJob.EFFECTIVE_STANDBY_BUCKET, getEffectiveStandbyBucket(js)); proto.write(StateControllerProto.QuotaController.TrackedJob.IS_TOP_STARTED_JOB, mTopStartedJobs.contains(js)); proto.write(StateControllerProto.QuotaController.TrackedJob.HAS_QUOTA, js.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA)); proto.write(StateControllerProto.QuotaController.TrackedJob.REMAINING_QUOTA_MS, Loading Loading
core/proto/android/server/jobscheduler.proto +9 −4 Original line number Diff line number Diff line Loading @@ -403,18 +403,23 @@ message StateControllerProto { optional bool is_charging = 1; optional bool is_in_parole = 2; // List of UIDs currently in the foreground. repeated int32 foreground_uids = 3; message TrackedJob { option (.android.msg_privacy).dest = DEST_AUTOMATIC; optional JobStatusShortInfoProto info = 1; optional int32 source_uid = 2; optional JobStatusDumpProto.Bucket effective_standby_bucket = 3; optional bool has_quota = 4; // If the job started while the app was in the TOP state. optional bool is_top_started_job = 4; optional bool has_quota = 5; // The amount of time that this job has remaining in its quota. This // can be negative if the job is out of quota. optional int64 remaining_quota_ms = 5; optional int64 remaining_quota_ms = 6; } repeated TrackedJob tracked_jobs = 3; repeated TrackedJob tracked_jobs = 4; message Package { option (.android.msg_privacy).dest = DEST_AUTOMATIC; Loading Loading @@ -456,7 +461,7 @@ message StateControllerProto { repeated TimingSession saved_sessions = 3; } repeated PackageStats package_stats = 4; repeated PackageStats package_stats = 5; } message StorageController { option (.android.msg_privacy).dest = DEST_AUTOMATIC; Loading
services/core/java/com/android/server/job/controllers/QuotaController.java +229 −19 Original line number Diff line number Diff line Loading @@ -26,7 +26,10 @@ import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.UserIdInt; import android.app.ActivityManager; import android.app.ActivityManagerInternal; import android.app.AlarmManager; import android.app.IUidObserver; import android.app.usage.UsageStatsManagerInternal; import android.app.usage.UsageStatsManagerInternal.AppIdleStateChangeListener; import android.content.BroadcastReceiver; Loading @@ -38,12 +41,14 @@ import android.os.BatteryManagerInternal; import android.os.Handler; import android.os.Looper; import android.os.Message; import android.os.RemoteException; import android.os.UserHandle; import android.util.ArrayMap; import android.util.ArraySet; import android.util.Log; import android.util.Slog; import android.util.SparseArray; import android.util.SparseBooleanArray; import android.util.proto.ProtoOutputStream; import com.android.internal.annotations.VisibleForTesting; Loading @@ -69,6 +74,11 @@ import java.util.function.Predicate; * bucket, it will be eligible to run. When a job's bucket changes, its new quota is immediately * applied to it. * * Jobs are throttled while an app is not in a foreground state. All jobs are allowed to run * freely when an app enters the foreground state and are restricted when the app leaves the * foreground state. However, jobs that are started while the app is in the TOP state are not * restricted regardless of the app's state change. * * Test: atest com.android.server.job.controllers.QuotaControllerTest */ public final class QuotaController extends StateController { Loading Loading @@ -97,6 +107,12 @@ public final class QuotaController extends StateController { data.put(packageName, obj); } public void clear() { for (int i = 0; i < mData.size(); ++i) { mData.valueAt(i).clear(); } } /** Removes all the data for the user, if there was any. */ public void delete(int userId) { mData.delete(userId); Loading @@ -119,6 +135,11 @@ public final class QuotaController extends StateController { return null; } /** @see SparseArray#indexOfKey */ public int indexOfKey(int userId) { return mData.indexOfKey(userId); } /** Returns the userId at the given index. */ public int keyAt(int index) { return mData.keyAt(index); Loading Loading @@ -294,6 +315,17 @@ public final class QuotaController extends StateController { /** Cached calculation results for each app, with the standby buckets as the array indices. */ private final UserPackageMap<ExecutionStats[]> mExecutionStatsCache = new UserPackageMap<>(); /** List of UIDs currently in the foreground. */ private final SparseBooleanArray mForegroundUids = new SparseBooleanArray(); /** * 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 * fine. */ private final ArraySet<JobStatus> mTopStartedJobs = new ArraySet<>(); private final ActivityManagerInternal mActivityManagerInternal; private final AlarmManager mAlarmManager; private final ChargingTracker mChargeTracker; private final Handler mHandler; Loading Loading @@ -343,6 +375,29 @@ public final class QuotaController extends StateController { } }; private final IUidObserver mUidObserver = new IUidObserver.Stub() { @Override public void onUidStateChanged(int uid, int procState, long procStateSeq) { mHandler.obtainMessage(MSG_UID_PROCESS_STATE_CHANGED, uid, procState).sendToTarget(); } @Override public void onUidGone(int uid, boolean disabled) { } @Override public void onUidActive(int uid) { } @Override public void onUidIdle(int uid, boolean disabled) { } @Override public void onUidCachedChanged(int uid, boolean cached) { } }; /** * The rolling window size for each standby bucket. Within each window, an app will have 10 * minutes to run its jobs. Loading @@ -363,12 +418,15 @@ public final class QuotaController extends StateController { private static final int MSG_CLEAN_UP_SESSIONS = 1; /** Check if a package is now within its quota. */ private static final int MSG_CHECK_PACKAGE = 2; /** Process state for a UID has changed. */ private static final int MSG_UID_PROCESS_STATE_CHANGED = 3; public QuotaController(JobSchedulerService service) { super(service); mHandler = new QcHandler(mContext.getMainLooper()); mChargeTracker = new ChargingTracker(); mChargeTracker.startTracking(); mActivityManagerInternal = LocalServices.getService(ActivityManagerInternal.class); mAlarmManager = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE); // Set up the app standby bucketing tracker Loading @@ -376,6 +434,14 @@ public final class QuotaController extends StateController { UsageStatsManagerInternal.class); usageStats.addAppIdleStateChangeListener(new StandbyTracker()); try { ActivityManager.getService().registerUidObserver(mUidObserver, ActivityManager.UID_OBSERVER_PROCSTATE, ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE, null); } catch (RemoteException e) { // ignored; both services live in system_server } onConstantsUpdatedLocked(); } Loading @@ -399,11 +465,15 @@ public final class QuotaController extends StateController { if (DEBUG) Slog.d(TAG, "Prepping for " + jobStatus.toShortString()); final int userId = jobStatus.getSourceUserId(); final String packageName = jobStatus.getSourcePackageName(); final int uid = jobStatus.getSourceUid(); Timer timer = mPkgTimers.get(userId, packageName); if (timer == null) { timer = new Timer(userId, packageName); timer = new Timer(uid, userId, packageName); mPkgTimers.add(userId, packageName, timer); } if (mActivityManagerInternal.getUidProcessState(uid) == ActivityManager.PROCESS_STATE_TOP) { mTopStartedJobs.add(jobStatus); } timer.startTrackingJob(jobStatus); } Loading @@ -421,6 +491,7 @@ public final class QuotaController extends StateController { if (jobs != null) { jobs.remove(jobStatus); } mTopStartedJobs.remove(jobStatus); } } Loading Loading @@ -511,6 +582,7 @@ public final class QuotaController extends StateController { mInQuotaAlarmListeners.delete(userId, packageName); } mExecutionStatsCache.delete(userId, packageName); mForegroundUids.delete(uid); } @Override Loading @@ -522,6 +594,20 @@ public final class QuotaController extends StateController { mExecutionStatsCache.delete(userId); } private boolean isUidInForeground(int uid) { if (UserHandle.isCore(uid)) { return true; } synchronized (mLock) { return mForegroundUids.get(uid); } } /** @return true if the job was started while the app was in the TOP state. */ private boolean isTopStartedJob(@NonNull final JobStatus jobStatus) { return mTopStartedJobs.contains(jobStatus); } /** * Returns an appropriate standby bucket for the job, taking into account any standby * exemptions. Loading @@ -537,8 +623,14 @@ public final class QuotaController extends StateController { private boolean isWithinQuotaLocked(@NonNull final JobStatus jobStatus) { final int standbyBucket = getEffectiveStandbyBucket(jobStatus); // Jobs for the active app should always be able to run. return jobStatus.uidActive || isWithinQuotaLocked( 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) || isUidInForeground(jobStatus.getSourceUid()) || isWithinQuotaLocked( jobStatus.getSourceUserId(), jobStatus.getSourcePackageName(), standbyBucket); } Loading Loading @@ -800,7 +892,7 @@ public final class QuotaController extends StateController { final boolean isCharging = mChargeTracker.isCharging(); if (DEBUG) Slog.d(TAG, "handleNewChargingStateLocked: " + isCharging); // Deal with Timers first. mPkgTimers.forEach((t) -> t.onChargingChanged(nowElapsed, isCharging)); mPkgTimers.forEach((t) -> t.onStateChanged(nowElapsed, isCharging)); // Now update jobs. maybeUpdateAllConstraintsLocked(); } Loading Loading @@ -837,10 +929,15 @@ 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 (js.uidActive) { // Jobs for the active app should always be able to run. if (isTopStartedJob(js)) { // Job was started while the app was in the TOP state so we should allow it to // finish. changed |= js.setQuotaConstraintSatisfied(true); } else if (realStandbyBucket == getEffectiveStandbyBucket(js)) { } else if (realStandbyBucket != ACTIVE_INDEX && realStandbyBucket == getEffectiveStandbyBucket(js)) { // An app in the ACTIVE bucket may be out of quota while the job could be in quota // for some reason. Therefore, avoid setting the real value here and check each job // individually. changed |= js.setQuotaConstraintSatisfied(realInQuota); } else { // This job is somehow exempted. Need to determine its own quota status. Loading @@ -854,7 +951,7 @@ public final class QuotaController extends StateController { maybeScheduleStartAlarmLocked(userId, packageName, realStandbyBucket); } else { QcAlarmListener alarmListener = mInQuotaAlarmListeners.get(userId, packageName); if (alarmListener != null) { if (alarmListener != null && alarmListener.isWaiting()) { mAlarmManager.cancel(alarmListener); // Set the trigger time to 0 so that the alarm doesn't think it's still waiting. alarmListener.setTriggerTime(0); Loading @@ -863,6 +960,56 @@ public final class QuotaController extends StateController { return changed; } private class UidConstraintUpdater implements Consumer<JobStatus> { private final UserPackageMap<Integer> mToScheduleStartAlarms = new UserPackageMap<>(); public boolean wasJobChanged; @Override public void accept(JobStatus jobStatus) { wasJobChanged |= jobStatus.setQuotaConstraintSatisfied(isWithinQuotaLocked(jobStatus)); final int userId = jobStatus.getSourceUserId(); final String packageName = jobStatus.getSourcePackageName(); final int realStandbyBucket = jobStatus.getStandbyBucket(); if (isWithinQuotaLocked(userId, packageName, realStandbyBucket)) { QcAlarmListener alarmListener = mInQuotaAlarmListeners.get(userId, packageName); if (alarmListener != null && alarmListener.isWaiting()) { mAlarmManager.cancel(alarmListener); // Set the trigger time to 0 so that the alarm doesn't think it's still waiting. alarmListener.setTriggerTime(0); } } else { mToScheduleStartAlarms.add(userId, packageName, realStandbyBucket); } } void postProcess() { for (int u = 0; u < mToScheduleStartAlarms.numUsers(); ++u) { final int userId = mToScheduleStartAlarms.keyAt(u); for (int p = 0; p < mToScheduleStartAlarms.numPackagesForUser(userId); ++p) { final String packageName = mToScheduleStartAlarms.keyAt(u, p); final int standbyBucket = mToScheduleStartAlarms.get(userId, packageName); maybeScheduleStartAlarmLocked(userId, packageName, standbyBucket); } } } void reset() { wasJobChanged = false; mToScheduleStartAlarms.clear(); } } private final UidConstraintUpdater mUpdateUidConstraints = new UidConstraintUpdater(); private boolean maybeUpdateConstraintForUidLocked(final int uid) { mService.getJobStore().forEachJobForSourceUid(uid, mUpdateUidConstraints); mUpdateUidConstraints.postProcess(); boolean changed = mUpdateUidConstraints.wasJobChanged; mUpdateUidConstraints.reset(); return changed; } /** * Maybe schedule a non-wakeup alarm for the next time this package will have quota to run * again. This should only be called if the package is already out of quota. Loading Loading @@ -1052,6 +1199,7 @@ public final class QuotaController extends StateController { private final class Timer { private final Package mPkg; private final int mUid; // List of jobs currently running for this app that started when the app wasn't in the // foreground. Loading @@ -1059,16 +1207,18 @@ public final class QuotaController extends StateController { private long mStartTimeElapsed; private int mBgJobCount; Timer(int userId, String packageName) { Timer(int uid, int userId, String packageName) { mPkg = new Package(userId, packageName); mUid = uid; } void startTrackingJob(@NonNull JobStatus jobStatus) { if (jobStatus.uidActive) { // We intentionally don't pay attention to fg state changes after a job has started. if (isTopStartedJob(jobStatus)) { // We intentionally don't pay attention to fg state changes after a TOP job has // started. if (DEBUG) { Slog.v(TAG, "Timer ignoring " + jobStatus.toShortString() + " because uidActive"); "Timer ignoring " + jobStatus.toShortString() + " because isTop"); } return; } Loading @@ -1076,7 +1226,7 @@ public final class QuotaController extends StateController { synchronized (mLock) { // Always track jobs, even when charging. mRunningBgJobs.add(jobStatus); if (!mChargeTracker.isCharging()) { if (shouldTrackLocked()) { mBgJobCount++; if (mRunningBgJobs.size() == 1) { // Started tracking the first job. Loading Loading @@ -1142,6 +1292,10 @@ public final class QuotaController extends StateController { } } boolean isRunning(JobStatus jobStatus) { return mRunningBgJobs.contains(jobStatus); } long getCurrentDuration(long nowElapsed) { synchronized (mLock) { return !isActive() ? 0 : nowElapsed - mStartTimeElapsed; Loading @@ -1154,17 +1308,21 @@ public final class QuotaController extends StateController { } } void onChargingChanged(long nowElapsed, boolean isCharging) { private boolean shouldTrackLocked() { return !mChargeTracker.isCharging() && !mForegroundUids.get(mUid); } void onStateChanged(long nowElapsed, boolean isQuotaFree) { synchronized (mLock) { if (isCharging) { if (isQuotaFree) { emitSessionLocked(nowElapsed); } else { } 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, the job count for a package may be // artificially high. // 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(); // Starting the timer means that all cached execution stats are now // incorrect. Loading Loading @@ -1371,6 +1529,38 @@ public final class QuotaController extends StateController { } break; } case MSG_UID_PROCESS_STATE_CHANGED: { final int uid = msg.arg1; final int procState = msg.arg2; final int userId = UserHandle.getUserId(uid); final long nowElapsed = sElapsedRealtimeClock.millis(); synchronized (mLock) { boolean isQuotaFree; if (procState <= ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE) { mForegroundUids.put(uid, true); isQuotaFree = true; } else { mForegroundUids.delete(uid); 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 (maybeUpdateConstraintForUidLocked(uid)) { mStateChangedListener.onControllerStateChanged(); } } break; } } } } Loading Loading @@ -1418,6 +1608,12 @@ public final class QuotaController extends StateController { return mBucketPeriodsMs; } @VisibleForTesting @NonNull SparseBooleanArray getForegroundUids() { return mForegroundUids; } @VisibleForTesting @NonNull Handler getHandler() { Loading Loading @@ -1450,6 +1646,10 @@ public final class QuotaController extends StateController { pw.println("In parole: " + mInParole); pw.println(); pw.print("Foreground UIDs: "); pw.println(mForegroundUids.toString()); pw.println(); mTrackedJobs.forEach((jobs) -> { for (int j = 0; j < jobs.size(); j++) { final JobStatus js = jobs.valueAt(j); Loading @@ -1460,6 +1660,9 @@ public final class QuotaController extends StateController { js.printUniqueId(pw); pw.print(" from "); UserHandle.formatUid(pw, js.getSourceUid()); if (mTopStartedJobs.contains(js)) { pw.print(" (TOP)"); } pw.println(); pw.increaseIndent(); Loading Loading @@ -1511,6 +1714,11 @@ public final class QuotaController extends StateController { proto.write(StateControllerProto.QuotaController.IS_CHARGING, mChargeTracker.isCharging()); proto.write(StateControllerProto.QuotaController.IS_IN_PAROLE, mInParole); for (int i = 0; i < mForegroundUids.size(); ++i) { proto.write(StateControllerProto.QuotaController.FOREGROUND_UIDS, mForegroundUids.keyAt(i)); } mTrackedJobs.forEach((jobs) -> { for (int j = 0; j < jobs.size(); j++) { final JobStatus js = jobs.valueAt(j); Loading @@ -1526,6 +1734,8 @@ public final class QuotaController extends StateController { proto.write( StateControllerProto.QuotaController.TrackedJob.EFFECTIVE_STANDBY_BUCKET, getEffectiveStandbyBucket(js)); proto.write(StateControllerProto.QuotaController.TrackedJob.IS_TOP_STARTED_JOB, mTopStartedJobs.contains(js)); proto.write(StateControllerProto.QuotaController.TrackedJob.HAS_QUOTA, js.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA)); proto.write(StateControllerProto.QuotaController.TrackedJob.REMAINING_QUOTA_MS, Loading