1 /*
2  * Copyright (C) 2018 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.server.job.controllers;
18 
19 import static android.text.format.DateUtils.HOUR_IN_MILLIS;
20 import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
21 import static android.text.format.DateUtils.SECOND_IN_MILLIS;
22 
23 import static com.android.server.job.JobSchedulerService.ACTIVE_INDEX;
24 import static com.android.server.job.JobSchedulerService.FREQUENT_INDEX;
25 import static com.android.server.job.JobSchedulerService.NEVER_INDEX;
26 import static com.android.server.job.JobSchedulerService.RARE_INDEX;
27 import static com.android.server.job.JobSchedulerService.RESTRICTED_INDEX;
28 import static com.android.server.job.JobSchedulerService.WORKING_INDEX;
29 import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock;
30 
31 import android.annotation.NonNull;
32 import android.annotation.Nullable;
33 import android.annotation.UserIdInt;
34 import android.app.ActivityManager;
35 import android.app.ActivityManagerInternal;
36 import android.app.AlarmManager;
37 import android.app.AppGlobals;
38 import android.app.IUidObserver;
39 import android.content.BroadcastReceiver;
40 import android.content.ContentResolver;
41 import android.content.Context;
42 import android.content.Intent;
43 import android.content.IntentFilter;
44 import android.database.ContentObserver;
45 import android.net.Uri;
46 import android.os.BatteryManager;
47 import android.os.BatteryManagerInternal;
48 import android.os.Handler;
49 import android.os.Looper;
50 import android.os.Message;
51 import android.os.RemoteException;
52 import android.os.UserHandle;
53 import android.provider.Settings;
54 import android.util.ArraySet;
55 import android.util.KeyValueListParser;
56 import android.util.Log;
57 import android.util.Pair;
58 import android.util.Slog;
59 import android.util.SparseArrayMap;
60 import android.util.SparseBooleanArray;
61 import android.util.SparseSetArray;
62 import android.util.proto.ProtoOutputStream;
63 
64 import com.android.internal.annotations.GuardedBy;
65 import com.android.internal.annotations.VisibleForTesting;
66 import com.android.internal.os.BackgroundThread;
67 import com.android.internal.util.IndentingPrintWriter;
68 import com.android.server.LocalServices;
69 import com.android.server.job.ConstantsProto;
70 import com.android.server.job.JobSchedulerService;
71 import com.android.server.job.JobServiceContext;
72 import com.android.server.job.StateControllerProto;
73 import com.android.server.usage.AppStandbyInternal;
74 import com.android.server.usage.AppStandbyInternal.AppIdleStateChangeListener;
75 
76 import java.util.ArrayList;
77 import java.util.List;
78 import java.util.Objects;
79 import java.util.PriorityQueue;
80 import java.util.function.Consumer;
81 import java.util.function.Predicate;
82 
83 /**
84  * Controller that tracks whether an app has exceeded its standby bucket quota.
85  *
86  * With initial defaults, each app in each bucket is given 10 minutes to run within its respective
87  * time window. Active jobs can run indefinitely, working set jobs can run for 10 minutes within a
88  * 2 hour window, frequent jobs get to run 10 minutes in an 8 hour window, and rare jobs get to run
89  * 10 minutes in a 24 hour window. The windows are rolling, so as soon as a job would have some
90  * quota based on its bucket, it will be eligible to run. When a job's bucket changes, its new
91  * quota is immediately applied to it.
92  *
93  * Job and session count limits are included to prevent abuse/spam. Each bucket has its own limit on
94  * the number of jobs or sessions that can run within the window. Regardless of bucket, apps will
95  * not be allowed to run more than 20 jobs within the past 10 minutes.
96  *
97  * Jobs are throttled while an app is not in a foreground state. All jobs are allowed to run
98  * freely when an app enters the foreground state and are restricted when the app leaves the
99  * foreground state. However, jobs that are started while the app is in the TOP state do not count
100  * towards any quota and are not restricted regardless of the app's state change.
101  *
102  * Jobs will not be throttled when the device is charging. The device is considered to be charging
103  * once the {@link BatteryManager#ACTION_CHARGING} intent has been broadcast.
104  *
105  * Note: all limits are enforced per bucket window unless explicitly stated otherwise.
106  * All stated values are configurable and subject to change. See {@link QcConstants} for current
107  * defaults.
108  *
109  * Test: atest com.android.server.job.controllers.QuotaControllerTest
110  */
111 public final class QuotaController extends StateController {
112     private static final String TAG = "JobScheduler.Quota";
113     private static final boolean DEBUG = JobSchedulerService.DEBUG
114             || Log.isLoggable(TAG, Log.DEBUG);
115 
116     private static final String ALARM_TAG_CLEANUP = "*job.cleanup*";
117     private static final String ALARM_TAG_QUOTA_CHECK = "*job.quota_check*";
118 
119     /**
120      * Standardize the output of userId-packageName combo.
121      */
string(int userId, String packageName)122     private static String string(int userId, String packageName) {
123         return "<" + userId + ">" + packageName;
124     }
125 
126     private static final class Package {
127         public final String packageName;
128         public final int userId;
129 
Package(int userId, String packageName)130         Package(int userId, String packageName) {
131             this.userId = userId;
132             this.packageName = packageName;
133         }
134 
135         @Override
toString()136         public String toString() {
137             return string(userId, packageName);
138         }
139 
dumpDebug(ProtoOutputStream proto, long fieldId)140         public void dumpDebug(ProtoOutputStream proto, long fieldId) {
141             final long token = proto.start(fieldId);
142 
143             proto.write(StateControllerProto.QuotaController.Package.USER_ID, userId);
144             proto.write(StateControllerProto.QuotaController.Package.NAME, packageName);
145 
146             proto.end(token);
147         }
148 
149         @Override
equals(Object obj)150         public boolean equals(Object obj) {
151             if (obj instanceof Package) {
152                 Package other = (Package) obj;
153                 return userId == other.userId && Objects.equals(packageName, other.packageName);
154             } else {
155                 return false;
156             }
157         }
158 
159         @Override
hashCode()160         public int hashCode() {
161             return packageName.hashCode() + userId;
162         }
163     }
164 
hashLong(long val)165     private static int hashLong(long val) {
166         return (int) (val ^ (val >>> 32));
167     }
168 
169     @VisibleForTesting
170     static class ExecutionStats {
171         /**
172          * The time after which this record should be considered invalid (out of date), in the
173          * elapsed realtime timebase.
174          */
175         public long expirationTimeElapsed;
176 
177         public long windowSizeMs;
178         public int jobCountLimit;
179         public int sessionCountLimit;
180 
181         /** The total amount of time the app ran in its respective bucket window size. */
182         public long executionTimeInWindowMs;
183         public int bgJobCountInWindow;
184 
185         /** The total amount of time the app ran in the last {@link #MAX_PERIOD_MS}. */
186         public long executionTimeInMaxPeriodMs;
187         public int bgJobCountInMaxPeriod;
188 
189         /**
190          * The number of {@link TimingSession}s within the bucket window size. This will include
191          * sessions that started before the window as long as they end within the window.
192          */
193         public int sessionCountInWindow;
194 
195         /**
196          * The time after which the app will be under the bucket quota and can start running jobs
197          * again. This is only valid if
198          * {@link #executionTimeInWindowMs} >= {@link #mAllowedTimePerPeriodMs},
199          * {@link #executionTimeInMaxPeriodMs} >= {@link #mMaxExecutionTimeMs},
200          * {@link #bgJobCountInWindow} >= {@link #jobCountLimit}, or
201          * {@link #sessionCountInWindow} >= {@link #sessionCountLimit}.
202          */
203         public long inQuotaTimeElapsed;
204 
205         /**
206          * The time after which {@link #jobCountInRateLimitingWindow} should be considered invalid,
207          * in the elapsed realtime timebase.
208          */
209         public long jobRateLimitExpirationTimeElapsed;
210 
211         /**
212          * The number of jobs that ran in at least the last {@link #mRateLimitingWindowMs}.
213          * It may contain a few stale entries since cleanup won't happen exactly every
214          * {@link #mRateLimitingWindowMs}.
215          */
216         public int jobCountInRateLimitingWindow;
217 
218         /**
219          * The time after which {@link #sessionCountInRateLimitingWindow} should be considered
220          * invalid, in the elapsed realtime timebase.
221          */
222         public long sessionRateLimitExpirationTimeElapsed;
223 
224         /**
225          * The number of {@link TimingSession}s that ran in at least the last
226          * {@link #mRateLimitingWindowMs}. It may contain a few stale entries since cleanup won't
227          * happen exactly every {@link #mRateLimitingWindowMs}. This should only be considered
228          * valid before elapsed realtime has reached {@link #sessionRateLimitExpirationTimeElapsed}.
229          */
230         public int sessionCountInRateLimitingWindow;
231 
232         @Override
toString()233         public String toString() {
234             return "expirationTime=" + expirationTimeElapsed + ", "
235                     + "windowSizeMs=" + windowSizeMs + ", "
236                     + "jobCountLimit=" + jobCountLimit + ", "
237                     + "sessionCountLimit=" + sessionCountLimit + ", "
238                     + "executionTimeInWindow=" + executionTimeInWindowMs + ", "
239                     + "bgJobCountInWindow=" + bgJobCountInWindow + ", "
240                     + "executionTimeInMaxPeriod=" + executionTimeInMaxPeriodMs + ", "
241                     + "bgJobCountInMaxPeriod=" + bgJobCountInMaxPeriod + ", "
242                     + "sessionCountInWindow=" + sessionCountInWindow + ", "
243                     + "inQuotaTime=" + inQuotaTimeElapsed + ", "
244                     + "rateLimitJobCountExpirationTime=" + jobRateLimitExpirationTimeElapsed + ", "
245                     + "rateLimitJobCountWindow=" + jobCountInRateLimitingWindow + ", "
246                     + "rateLimitSessionCountExpirationTime="
247                     + sessionRateLimitExpirationTimeElapsed + ", "
248                     + "rateLimitSessionCountWindow=" + sessionCountInRateLimitingWindow;
249         }
250 
251         @Override
equals(Object obj)252         public boolean equals(Object obj) {
253             if (obj instanceof ExecutionStats) {
254                 ExecutionStats other = (ExecutionStats) obj;
255                 return this.expirationTimeElapsed == other.expirationTimeElapsed
256                         && this.windowSizeMs == other.windowSizeMs
257                         && this.jobCountLimit == other.jobCountLimit
258                         && this.sessionCountLimit == other.sessionCountLimit
259                         && this.executionTimeInWindowMs == other.executionTimeInWindowMs
260                         && this.bgJobCountInWindow == other.bgJobCountInWindow
261                         && this.executionTimeInMaxPeriodMs == other.executionTimeInMaxPeriodMs
262                         && this.sessionCountInWindow == other.sessionCountInWindow
263                         && this.bgJobCountInMaxPeriod == other.bgJobCountInMaxPeriod
264                         && this.inQuotaTimeElapsed == other.inQuotaTimeElapsed
265                         && this.jobRateLimitExpirationTimeElapsed
266                                 == other.jobRateLimitExpirationTimeElapsed
267                         && this.jobCountInRateLimitingWindow == other.jobCountInRateLimitingWindow
268                         && this.sessionRateLimitExpirationTimeElapsed
269                                 == other.sessionRateLimitExpirationTimeElapsed
270                         && this.sessionCountInRateLimitingWindow
271                                 == other.sessionCountInRateLimitingWindow;
272             } else {
273                 return false;
274             }
275         }
276 
277         @Override
hashCode()278         public int hashCode() {
279             int result = 0;
280             result = 31 * result + hashLong(expirationTimeElapsed);
281             result = 31 * result + hashLong(windowSizeMs);
282             result = 31 * result + hashLong(jobCountLimit);
283             result = 31 * result + hashLong(sessionCountLimit);
284             result = 31 * result + hashLong(executionTimeInWindowMs);
285             result = 31 * result + bgJobCountInWindow;
286             result = 31 * result + hashLong(executionTimeInMaxPeriodMs);
287             result = 31 * result + bgJobCountInMaxPeriod;
288             result = 31 * result + sessionCountInWindow;
289             result = 31 * result + hashLong(inQuotaTimeElapsed);
290             result = 31 * result + hashLong(jobRateLimitExpirationTimeElapsed);
291             result = 31 * result + jobCountInRateLimitingWindow;
292             result = 31 * result + hashLong(sessionRateLimitExpirationTimeElapsed);
293             result = 31 * result + sessionCountInRateLimitingWindow;
294             return result;
295         }
296     }
297 
298     /** List of all tracked jobs keyed by source package-userId combo. */
299     private final SparseArrayMap<ArraySet<JobStatus>> mTrackedJobs = new SparseArrayMap<>();
300 
301     /** Timer for each package-userId combo. */
302     private final SparseArrayMap<Timer> mPkgTimers = new SparseArrayMap<>();
303 
304     /** List of all timing sessions for a package-userId combo, in chronological order. */
305     private final SparseArrayMap<List<TimingSession>> mTimingSessions = new SparseArrayMap<>();
306 
307     /**
308      * Listener to track and manage when each package comes back within quota.
309      */
310     @GuardedBy("mLock")
311     private final InQuotaAlarmListener mInQuotaAlarmListener = new InQuotaAlarmListener();
312 
313     /** Cached calculation results for each app, with the standby buckets as the array indices. */
314     private final SparseArrayMap<ExecutionStats[]> mExecutionStatsCache = new SparseArrayMap<>();
315 
316     /** List of UIDs currently in the foreground. */
317     private final SparseBooleanArray mForegroundUids = new SparseBooleanArray();
318 
319     /** Cached mapping of UIDs (for all users) to a list of packages in the UID. */
320     private final SparseSetArray<String> mUidToPackageCache = new SparseSetArray<>();
321 
322     /**
323      * List of jobs that started while the UID was in the TOP state. There will be no more than
324      * 16 ({@link JobSchedulerService#MAX_JOB_CONTEXTS_COUNT}) running at once, so an ArraySet is
325      * fine.
326      */
327     private final ArraySet<JobStatus> mTopStartedJobs = new ArraySet<>();
328 
329     private final ActivityManagerInternal mActivityManagerInternal;
330     private final AlarmManager mAlarmManager;
331     private final ChargingTracker mChargeTracker;
332     private final Handler mHandler;
333     private final QcConstants mQcConstants;
334 
335     /** How much time each app will have to run jobs within their standby bucket window. */
336     private long mAllowedTimePerPeriodMs = QcConstants.DEFAULT_ALLOWED_TIME_PER_PERIOD_MS;
337 
338     /**
339      * The maximum amount of time an app can have its jobs running within a {@link #MAX_PERIOD_MS}
340      * window.
341      */
342     private long mMaxExecutionTimeMs = QcConstants.DEFAULT_MAX_EXECUTION_TIME_MS;
343 
344     /**
345      * How much time the app should have before transitioning from out-of-quota to in-quota.
346      * This should not affect processing if the app is already in-quota.
347      */
348     private long mQuotaBufferMs = QcConstants.DEFAULT_IN_QUOTA_BUFFER_MS;
349 
350     /**
351      * {@link #mAllowedTimePerPeriodMs} - {@link #mQuotaBufferMs}. This can be used to determine
352      * when an app will have enough quota to transition from out-of-quota to in-quota.
353      */
354     private long mAllowedTimeIntoQuotaMs = mAllowedTimePerPeriodMs - mQuotaBufferMs;
355 
356     /**
357      * {@link #mMaxExecutionTimeMs} - {@link #mQuotaBufferMs}. This can be used to determine when an
358      * app will have enough quota to transition from out-of-quota to in-quota.
359      */
360     private long mMaxExecutionTimeIntoQuotaMs = mMaxExecutionTimeMs - mQuotaBufferMs;
361 
362     /** The period of time used to rate limit recently run jobs. */
363     private long mRateLimitingWindowMs = QcConstants.DEFAULT_RATE_LIMITING_WINDOW_MS;
364 
365     /** The maximum number of jobs that can run within the past {@link #mRateLimitingWindowMs}. */
366     private int mMaxJobCountPerRateLimitingWindow =
367             QcConstants.DEFAULT_MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW;
368 
369     /**
370      * The maximum number of {@link TimingSession}s that can run within the past {@link
371      * #mRateLimitingWindowMs}.
372      */
373     private int mMaxSessionCountPerRateLimitingWindow =
374             QcConstants.DEFAULT_MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW;
375 
376     private long mNextCleanupTimeElapsed = 0;
377     private final AlarmManager.OnAlarmListener mSessionCleanupAlarmListener =
378             new AlarmManager.OnAlarmListener() {
379                 @Override
380                 public void onAlarm() {
381                     mHandler.obtainMessage(MSG_CLEAN_UP_SESSIONS).sendToTarget();
382                 }
383             };
384 
385     private final IUidObserver mUidObserver = new IUidObserver.Stub() {
386         @Override
387         public void onUidStateChanged(int uid, int procState, long procStateSeq, int capability) {
388             mHandler.obtainMessage(MSG_UID_PROCESS_STATE_CHANGED, uid, procState).sendToTarget();
389         }
390 
391         @Override
392         public void onUidGone(int uid, boolean disabled) {
393         }
394 
395         @Override
396         public void onUidActive(int uid) {
397         }
398 
399         @Override
400         public void onUidIdle(int uid, boolean disabled) {
401         }
402 
403         @Override
404         public void onUidCachedChanged(int uid, boolean cached) {
405         }
406     };
407 
408     private final BroadcastReceiver mPackageAddedReceiver = new BroadcastReceiver() {
409         @Override
410         public void onReceive(Context context, Intent intent) {
411             if (intent == null) {
412                 return;
413             }
414             if (intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) {
415                 return;
416             }
417             final int uid = intent.getIntExtra(Intent.EXTRA_UID, -1);
418             synchronized (mLock) {
419                 mUidToPackageCache.remove(uid);
420             }
421         }
422     };
423 
424     /**
425      * The rolling window size for each standby bucket. Within each window, an app will have 10
426      * minutes to run its jobs.
427      */
428     private final long[] mBucketPeriodsMs = new long[]{
429             QcConstants.DEFAULT_WINDOW_SIZE_ACTIVE_MS,
430             QcConstants.DEFAULT_WINDOW_SIZE_WORKING_MS,
431             QcConstants.DEFAULT_WINDOW_SIZE_FREQUENT_MS,
432             QcConstants.DEFAULT_WINDOW_SIZE_RARE_MS,
433             0, // NEVER
434             QcConstants.DEFAULT_WINDOW_SIZE_RESTRICTED_MS
435     };
436 
437     /** The maximum period any bucket can have. */
438     private static final long MAX_PERIOD_MS = 24 * 60 * MINUTE_IN_MILLIS;
439 
440     /**
441      * The maximum number of jobs based on its standby bucket. For each max value count in the
442      * array, the app will not be allowed to run more than that many number of jobs within the
443      * latest time interval of its rolling window size.
444      *
445      * @see #mBucketPeriodsMs
446      */
447     private final int[] mMaxBucketJobCounts = new int[]{
448             QcConstants.DEFAULT_MAX_JOB_COUNT_ACTIVE,
449             QcConstants.DEFAULT_MAX_JOB_COUNT_WORKING,
450             QcConstants.DEFAULT_MAX_JOB_COUNT_FREQUENT,
451             QcConstants.DEFAULT_MAX_JOB_COUNT_RARE,
452             0, // NEVER
453             QcConstants.DEFAULT_MAX_JOB_COUNT_RESTRICTED
454     };
455 
456     /**
457      * The maximum number of {@link TimingSession}s based on its standby bucket. For each max value
458      * count in the array, the app will not be allowed to have more than that many number of
459      * {@link TimingSession}s within the latest time interval of its rolling window size.
460      *
461      * @see #mBucketPeriodsMs
462      */
463     private final int[] mMaxBucketSessionCounts = new int[]{
464             QcConstants.DEFAULT_MAX_SESSION_COUNT_ACTIVE,
465             QcConstants.DEFAULT_MAX_SESSION_COUNT_WORKING,
466             QcConstants.DEFAULT_MAX_SESSION_COUNT_FREQUENT,
467             QcConstants.DEFAULT_MAX_SESSION_COUNT_RARE,
468             0, // NEVER
469             QcConstants.DEFAULT_MAX_SESSION_COUNT_RESTRICTED,
470     };
471 
472     /**
473      * Treat two distinct {@link TimingSession}s as the same if they start and end within this
474      * amount of time of each other.
475      */
476     private long mTimingSessionCoalescingDurationMs =
477             QcConstants.DEFAULT_TIMING_SESSION_COALESCING_DURATION_MS;
478 
479     /** An app has reached its quota. The message should contain a {@link Package} object. */
480     private static final int MSG_REACHED_QUOTA = 0;
481     /** Drop any old timing sessions. */
482     private static final int MSG_CLEAN_UP_SESSIONS = 1;
483     /** Check if a package is now within its quota. */
484     private static final int MSG_CHECK_PACKAGE = 2;
485     /** Process state for a UID has changed. */
486     private static final int MSG_UID_PROCESS_STATE_CHANGED = 3;
487 
QuotaController(JobSchedulerService service)488     public QuotaController(JobSchedulerService service) {
489         super(service);
490         mHandler = new QcHandler(mContext.getMainLooper());
491         mChargeTracker = new ChargingTracker();
492         mChargeTracker.startTracking();
493         mActivityManagerInternal = LocalServices.getService(ActivityManagerInternal.class);
494         mAlarmManager = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
495         mQcConstants = new QcConstants(mHandler);
496 
497         final IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED);
498         mContext.registerReceiverAsUser(mPackageAddedReceiver, UserHandle.ALL, filter, null, null);
499 
500         // Set up the app standby bucketing tracker
501         AppStandbyInternal appStandby = LocalServices.getService(AppStandbyInternal.class);
502         appStandby.addListener(new StandbyTracker());
503 
504         try {
505             ActivityManager.getService().registerUidObserver(mUidObserver,
506                     ActivityManager.UID_OBSERVER_PROCSTATE,
507                     ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE, null);
508         } catch (RemoteException e) {
509             // ignored; both services live in system_server
510         }
511     }
512 
513     @Override
onSystemServicesReady()514     public void onSystemServicesReady() {
515         mQcConstants.start(mContext.getContentResolver());
516     }
517 
518     @Override
maybeStartTrackingJobLocked(JobStatus jobStatus, JobStatus lastJob)519     public void maybeStartTrackingJobLocked(JobStatus jobStatus, JobStatus lastJob) {
520         final int userId = jobStatus.getSourceUserId();
521         final String pkgName = jobStatus.getSourcePackageName();
522         ArraySet<JobStatus> jobs = mTrackedJobs.get(userId, pkgName);
523         if (jobs == null) {
524             jobs = new ArraySet<>();
525             mTrackedJobs.add(userId, pkgName, jobs);
526         }
527         jobs.add(jobStatus);
528         jobStatus.setTrackingController(JobStatus.TRACKING_QUOTA);
529         final boolean isWithinQuota = isWithinQuotaLocked(jobStatus);
530         setConstraintSatisfied(jobStatus, isWithinQuota);
531         if (!isWithinQuota) {
532             maybeScheduleStartAlarmLocked(userId, pkgName, jobStatus.getEffectiveStandbyBucket());
533         }
534     }
535 
536     @Override
prepareForExecutionLocked(JobStatus jobStatus)537     public void prepareForExecutionLocked(JobStatus jobStatus) {
538         if (DEBUG) {
539             Slog.d(TAG, "Prepping for " + jobStatus.toShortString());
540         }
541 
542         final int uid = jobStatus.getSourceUid();
543         if (mActivityManagerInternal.getUidProcessState(uid) <= ActivityManager.PROCESS_STATE_TOP) {
544             if (DEBUG) {
545                 Slog.d(TAG, jobStatus.toShortString() + " is top started job");
546             }
547             mTopStartedJobs.add(jobStatus);
548             // Top jobs won't count towards quota so there's no need to involve the Timer.
549             return;
550         }
551 
552         final int userId = jobStatus.getSourceUserId();
553         final String packageName = jobStatus.getSourcePackageName();
554         Timer timer = mPkgTimers.get(userId, packageName);
555         if (timer == null) {
556             timer = new Timer(uid, userId, packageName);
557             mPkgTimers.add(userId, packageName, timer);
558         }
559         timer.startTrackingJobLocked(jobStatus);
560     }
561 
562     @Override
maybeStopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob, boolean forUpdate)563     public void maybeStopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob,
564             boolean forUpdate) {
565         if (jobStatus.clearTrackingController(JobStatus.TRACKING_QUOTA)) {
566             Timer timer = mPkgTimers.get(jobStatus.getSourceUserId(),
567                     jobStatus.getSourcePackageName());
568             if (timer != null) {
569                 timer.stopTrackingJob(jobStatus);
570             }
571             ArraySet<JobStatus> jobs = mTrackedJobs.get(jobStatus.getSourceUserId(),
572                     jobStatus.getSourcePackageName());
573             if (jobs != null) {
574                 jobs.remove(jobStatus);
575             }
576             mTopStartedJobs.remove(jobStatus);
577         }
578     }
579 
580     @Override
onAppRemovedLocked(String packageName, int uid)581     public void onAppRemovedLocked(String packageName, int uid) {
582         if (packageName == null) {
583             Slog.wtf(TAG, "Told app removed but given null package name.");
584             return;
585         }
586         clearAppStats(UserHandle.getUserId(uid), packageName);
587         mForegroundUids.delete(uid);
588         mUidToPackageCache.remove(uid);
589     }
590 
591     @Override
onUserRemovedLocked(int userId)592     public void onUserRemovedLocked(int userId) {
593         mTrackedJobs.delete(userId);
594         mPkgTimers.delete(userId);
595         mTimingSessions.delete(userId);
596         mInQuotaAlarmListener.removeAlarmsLocked(userId);
597         mExecutionStatsCache.delete(userId);
598         mUidToPackageCache.clear();
599     }
600 
601     /** Drop all historical stats and stop tracking any active sessions for the specified app. */
clearAppStats(int userId, @NonNull String packageName)602     public void clearAppStats(int userId, @NonNull String packageName) {
603         mTrackedJobs.delete(userId, packageName);
604         Timer timer = mPkgTimers.get(userId, packageName);
605         if (timer != null) {
606             if (timer.isActive()) {
607                 Slog.e(TAG, "clearAppStats called before Timer turned off.");
608                 timer.dropEverythingLocked();
609             }
610             mPkgTimers.delete(userId, packageName);
611         }
612         mTimingSessions.delete(userId, packageName);
613         mInQuotaAlarmListener.removeAlarmLocked(userId, packageName);
614         mExecutionStatsCache.delete(userId, packageName);
615     }
616 
isUidInForeground(int uid)617     private boolean isUidInForeground(int uid) {
618         if (UserHandle.isCore(uid)) {
619             return true;
620         }
621         synchronized (mLock) {
622             return mForegroundUids.get(uid);
623         }
624     }
625 
626     /** @return true if the job was started while the app was in the TOP state. */
isTopStartedJobLocked(@onNull final JobStatus jobStatus)627     private boolean isTopStartedJobLocked(@NonNull final JobStatus jobStatus) {
628         return mTopStartedJobs.contains(jobStatus);
629     }
630 
631     /** Returns the maximum amount of time this job could run for. */
getMaxJobExecutionTimeMsLocked(@onNull final JobStatus jobStatus)632     public long getMaxJobExecutionTimeMsLocked(@NonNull final JobStatus jobStatus) {
633         // If quota is currently "free", then the job can run for the full amount of time.
634         if (mChargeTracker.isCharging()
635                 || isTopStartedJobLocked(jobStatus)
636                 || isUidInForeground(jobStatus.getSourceUid())) {
637             return JobServiceContext.EXECUTING_TIMESLICE_MILLIS;
638         }
639         return getRemainingExecutionTimeLocked(jobStatus);
640     }
641 
642     @VisibleForTesting
isWithinQuotaLocked(@onNull final JobStatus jobStatus)643     boolean isWithinQuotaLocked(@NonNull final JobStatus jobStatus) {
644         final int standbyBucket = jobStatus.getEffectiveStandbyBucket();
645         // A job is within quota if one of the following is true:
646         //   1. it was started while the app was in the TOP state
647         //   2. the app is currently in the foreground
648         //   3. the app overall is within its quota
649         return isTopStartedJobLocked(jobStatus)
650                 || isUidInForeground(jobStatus.getSourceUid())
651                 || isWithinQuotaLocked(
652                 jobStatus.getSourceUserId(), jobStatus.getSourcePackageName(), standbyBucket);
653     }
654 
655     @VisibleForTesting
isWithinQuotaLocked(final int userId, @NonNull final String packageName, final int standbyBucket)656     boolean isWithinQuotaLocked(final int userId, @NonNull final String packageName,
657             final int standbyBucket) {
658         if (standbyBucket == NEVER_INDEX) return false;
659 
660         // Quota constraint is not enforced while charging.
661         if (mChargeTracker.isCharging()) {
662             // Restricted jobs require additional constraints when charging, so don't immediately
663             // mark quota as free when charging.
664             if (standbyBucket != RESTRICTED_INDEX) {
665                 return true;
666             }
667         }
668 
669         ExecutionStats stats = getExecutionStatsLocked(userId, packageName, standbyBucket);
670         return getRemainingExecutionTimeLocked(stats) > 0
671                 && isUnderJobCountQuotaLocked(stats, standbyBucket)
672                 && isUnderSessionCountQuotaLocked(stats, standbyBucket);
673     }
674 
isUnderJobCountQuotaLocked(@onNull ExecutionStats stats, final int standbyBucket)675     private boolean isUnderJobCountQuotaLocked(@NonNull ExecutionStats stats,
676             final int standbyBucket) {
677         final long now = sElapsedRealtimeClock.millis();
678         final boolean isUnderAllowedTimeQuota =
679                 (stats.jobRateLimitExpirationTimeElapsed <= now
680                         || stats.jobCountInRateLimitingWindow < mMaxJobCountPerRateLimitingWindow);
681         return isUnderAllowedTimeQuota
682                 && (stats.bgJobCountInWindow < mMaxBucketJobCounts[standbyBucket]);
683     }
684 
isUnderSessionCountQuotaLocked(@onNull ExecutionStats stats, final int standbyBucket)685     private boolean isUnderSessionCountQuotaLocked(@NonNull ExecutionStats stats,
686             final int standbyBucket) {
687         final long now = sElapsedRealtimeClock.millis();
688         final boolean isUnderAllowedTimeQuota = (stats.sessionRateLimitExpirationTimeElapsed <= now
689                 || stats.sessionCountInRateLimitingWindow < mMaxSessionCountPerRateLimitingWindow);
690         return isUnderAllowedTimeQuota
691                 && stats.sessionCountInWindow < mMaxBucketSessionCounts[standbyBucket];
692     }
693 
694     @VisibleForTesting
getRemainingExecutionTimeLocked(@onNull final JobStatus jobStatus)695     long getRemainingExecutionTimeLocked(@NonNull final JobStatus jobStatus) {
696         return getRemainingExecutionTimeLocked(jobStatus.getSourceUserId(),
697                 jobStatus.getSourcePackageName(),
698                 jobStatus.getEffectiveStandbyBucket());
699     }
700 
701     @VisibleForTesting
getRemainingExecutionTimeLocked(final int userId, @NonNull final String packageName)702     long getRemainingExecutionTimeLocked(final int userId, @NonNull final String packageName) {
703         final int standbyBucket = JobSchedulerService.standbyBucketForPackage(packageName,
704                 userId, sElapsedRealtimeClock.millis());
705         return getRemainingExecutionTimeLocked(userId, packageName, standbyBucket);
706     }
707 
708     /**
709      * Returns the amount of time, in milliseconds, that this job has remaining to run based on its
710      * current standby bucket. Time remaining could be negative if the app was moved from a less
711      * restricted to a more restricted bucket.
712      */
getRemainingExecutionTimeLocked(final int userId, @NonNull final String packageName, final int standbyBucket)713     private long getRemainingExecutionTimeLocked(final int userId,
714             @NonNull final String packageName, final int standbyBucket) {
715         if (standbyBucket == NEVER_INDEX) {
716             return 0;
717         }
718         return getRemainingExecutionTimeLocked(
719                 getExecutionStatsLocked(userId, packageName, standbyBucket));
720     }
721 
getRemainingExecutionTimeLocked(@onNull ExecutionStats stats)722     private long getRemainingExecutionTimeLocked(@NonNull ExecutionStats stats) {
723         return Math.min(mAllowedTimePerPeriodMs - stats.executionTimeInWindowMs,
724                 mMaxExecutionTimeMs - stats.executionTimeInMaxPeriodMs);
725     }
726 
727     /**
728      * Returns the amount of time, in milliseconds, until the package would have reached its
729      * duration quota, assuming it has a job counting towards its quota the entire time. This takes
730      * into account any {@link TimingSession}s that may roll out of the window as the job is
731      * running.
732      */
733     @VisibleForTesting
getTimeUntilQuotaConsumedLocked(final int userId, @NonNull final String packageName)734     long getTimeUntilQuotaConsumedLocked(final int userId, @NonNull final String packageName) {
735         final long nowElapsed = sElapsedRealtimeClock.millis();
736         final int standbyBucket = JobSchedulerService.standbyBucketForPackage(
737                 packageName, userId, nowElapsed);
738         if (standbyBucket == NEVER_INDEX) {
739             return 0;
740         }
741         List<TimingSession> sessions = mTimingSessions.get(userId, packageName);
742         if (sessions == null || sessions.size() == 0) {
743             return mAllowedTimePerPeriodMs;
744         }
745 
746         final ExecutionStats stats = getExecutionStatsLocked(userId, packageName, standbyBucket);
747         final long startWindowElapsed = nowElapsed - stats.windowSizeMs;
748         final long startMaxElapsed = nowElapsed - MAX_PERIOD_MS;
749         final long allowedTimeRemainingMs = mAllowedTimePerPeriodMs - stats.executionTimeInWindowMs;
750         final long maxExecutionTimeRemainingMs =
751                 mMaxExecutionTimeMs - stats.executionTimeInMaxPeriodMs;
752 
753         // Regular ACTIVE case. Since the bucket size equals the allowed time, the app jobs can
754         // essentially run until they reach the maximum limit.
755         if (stats.windowSizeMs == mAllowedTimePerPeriodMs) {
756             return calculateTimeUntilQuotaConsumedLocked(
757                     sessions, startMaxElapsed, maxExecutionTimeRemainingMs);
758         }
759 
760         // Need to check both max time and period time in case one is less than the other.
761         // For example, max time remaining could be less than bucket time remaining, but sessions
762         // contributing to the max time remaining could phase out enough that we'd want to use the
763         // bucket value.
764         return Math.min(
765                 calculateTimeUntilQuotaConsumedLocked(
766                         sessions, startMaxElapsed, maxExecutionTimeRemainingMs),
767                 calculateTimeUntilQuotaConsumedLocked(
768                         sessions, startWindowElapsed, allowedTimeRemainingMs));
769     }
770 
771     /**
772      * Calculates how much time it will take, in milliseconds, until the quota is fully consumed.
773      *
774      * @param windowStartElapsed The start of the window, in the elapsed realtime timebase.
775      * @param deadSpaceMs        How much time can be allowed to count towards the quota
776      */
calculateTimeUntilQuotaConsumedLocked(@onNull List<TimingSession> sessions, final long windowStartElapsed, long deadSpaceMs)777     private long calculateTimeUntilQuotaConsumedLocked(@NonNull List<TimingSession> sessions,
778             final long windowStartElapsed, long deadSpaceMs) {
779         long timeUntilQuotaConsumedMs = 0;
780         long start = windowStartElapsed;
781         for (int i = 0; i < sessions.size(); ++i) {
782             TimingSession session = sessions.get(i);
783 
784             if (session.endTimeElapsed < windowStartElapsed) {
785                 // Outside of window. Ignore.
786                 continue;
787             } else if (session.startTimeElapsed <= windowStartElapsed) {
788                 // Overlapping session. Can extend time by portion of session in window.
789                 timeUntilQuotaConsumedMs += session.endTimeElapsed - windowStartElapsed;
790                 start = session.endTimeElapsed;
791             } else {
792                 // Completely within the window. Can only consider if there's enough dead space
793                 // to get to the start of the session.
794                 long diff = session.startTimeElapsed - start;
795                 if (diff > deadSpaceMs) {
796                     break;
797                 }
798                 timeUntilQuotaConsumedMs += diff
799                         + (session.endTimeElapsed - session.startTimeElapsed);
800                 deadSpaceMs -= diff;
801                 start = session.endTimeElapsed;
802             }
803         }
804         // Will be non-zero if the loop didn't look at any sessions.
805         timeUntilQuotaConsumedMs += deadSpaceMs;
806         if (timeUntilQuotaConsumedMs > mMaxExecutionTimeMs) {
807             Slog.wtf(TAG, "Calculated quota consumed time too high: " + timeUntilQuotaConsumedMs);
808         }
809         return timeUntilQuotaConsumedMs;
810     }
811 
812     /** Returns the execution stats of the app in the most recent window. */
813     @VisibleForTesting
814     @NonNull
getExecutionStatsLocked(final int userId, @NonNull final String packageName, final int standbyBucket)815     ExecutionStats getExecutionStatsLocked(final int userId, @NonNull final String packageName,
816             final int standbyBucket) {
817         return getExecutionStatsLocked(userId, packageName, standbyBucket, true);
818     }
819 
820     @NonNull
getExecutionStatsLocked(final int userId, @NonNull final String packageName, final int standbyBucket, final boolean refreshStatsIfOld)821     private ExecutionStats getExecutionStatsLocked(final int userId,
822             @NonNull final String packageName, final int standbyBucket,
823             final boolean refreshStatsIfOld) {
824         if (standbyBucket == NEVER_INDEX) {
825             Slog.wtf(TAG, "getExecutionStatsLocked called for a NEVER app.");
826             return new ExecutionStats();
827         }
828         ExecutionStats[] appStats = mExecutionStatsCache.get(userId, packageName);
829         if (appStats == null) {
830             appStats = new ExecutionStats[mBucketPeriodsMs.length];
831             mExecutionStatsCache.add(userId, packageName, appStats);
832         }
833         ExecutionStats stats = appStats[standbyBucket];
834         if (stats == null) {
835             stats = new ExecutionStats();
836             appStats[standbyBucket] = stats;
837         }
838         if (refreshStatsIfOld) {
839             final long bucketWindowSizeMs = mBucketPeriodsMs[standbyBucket];
840             final int jobCountLimit = mMaxBucketJobCounts[standbyBucket];
841             final int sessionCountLimit = mMaxBucketSessionCounts[standbyBucket];
842             Timer timer = mPkgTimers.get(userId, packageName);
843             if ((timer != null && timer.isActive())
844                     || stats.expirationTimeElapsed <= sElapsedRealtimeClock.millis()
845                     || stats.windowSizeMs != bucketWindowSizeMs
846                     || stats.jobCountLimit != jobCountLimit
847                     || stats.sessionCountLimit != sessionCountLimit) {
848                 // The stats are no longer valid.
849                 stats.windowSizeMs = bucketWindowSizeMs;
850                 stats.jobCountLimit = jobCountLimit;
851                 stats.sessionCountLimit = sessionCountLimit;
852                 updateExecutionStatsLocked(userId, packageName, stats);
853             }
854         }
855 
856         return stats;
857     }
858 
859     @VisibleForTesting
updateExecutionStatsLocked(final int userId, @NonNull final String packageName, @NonNull ExecutionStats stats)860     void updateExecutionStatsLocked(final int userId, @NonNull final String packageName,
861             @NonNull ExecutionStats stats) {
862         stats.executionTimeInWindowMs = 0;
863         stats.bgJobCountInWindow = 0;
864         stats.executionTimeInMaxPeriodMs = 0;
865         stats.bgJobCountInMaxPeriod = 0;
866         stats.sessionCountInWindow = 0;
867         if (stats.jobCountLimit == 0 || stats.sessionCountLimit == 0) {
868             // App won't be in quota until configuration changes.
869             stats.inQuotaTimeElapsed = Long.MAX_VALUE;
870         } else {
871             stats.inQuotaTimeElapsed = 0;
872         }
873 
874         Timer timer = mPkgTimers.get(userId, packageName);
875         final long nowElapsed = sElapsedRealtimeClock.millis();
876         stats.expirationTimeElapsed = nowElapsed + MAX_PERIOD_MS;
877         if (timer != null && timer.isActive()) {
878             // Exclude active sessions from the session count so that new jobs aren't prevented
879             // from starting due to an app hitting the session limit.
880             stats.executionTimeInWindowMs =
881                     stats.executionTimeInMaxPeriodMs = timer.getCurrentDuration(nowElapsed);
882             stats.bgJobCountInWindow = stats.bgJobCountInMaxPeriod = timer.getBgJobCount();
883             // If the timer is active, the value will be stale at the next method call, so
884             // invalidate now.
885             stats.expirationTimeElapsed = nowElapsed;
886             if (stats.executionTimeInWindowMs >= mAllowedTimeIntoQuotaMs) {
887                 stats.inQuotaTimeElapsed = Math.max(stats.inQuotaTimeElapsed,
888                         nowElapsed - mAllowedTimeIntoQuotaMs + stats.windowSizeMs);
889             }
890             if (stats.executionTimeInMaxPeriodMs >= mMaxExecutionTimeIntoQuotaMs) {
891                 stats.inQuotaTimeElapsed = Math.max(stats.inQuotaTimeElapsed,
892                         nowElapsed - mMaxExecutionTimeIntoQuotaMs + MAX_PERIOD_MS);
893             }
894             if (stats.bgJobCountInWindow >= stats.jobCountLimit) {
895                 stats.inQuotaTimeElapsed = Math.max(stats.inQuotaTimeElapsed,
896                         nowElapsed + stats.windowSizeMs);
897             }
898         }
899 
900         List<TimingSession> sessions = mTimingSessions.get(userId, packageName);
901         if (sessions == null || sessions.size() == 0) {
902             return;
903         }
904 
905         final long startWindowElapsed = nowElapsed - stats.windowSizeMs;
906         final long startMaxElapsed = nowElapsed - MAX_PERIOD_MS;
907         int sessionCountInWindow = 0;
908         // The minimum time between the start time and the beginning of the sessions that were
909         // looked at --> how much time the stats will be valid for.
910         long emptyTimeMs = Long.MAX_VALUE;
911         // Sessions are non-overlapping and in order of occurrence, so iterating backwards will get
912         // the most recent ones.
913         final int loopStart = sessions.size() - 1;
914         for (int i = loopStart; i >= 0; --i) {
915             TimingSession session = sessions.get(i);
916 
917             // Window management.
918             if (startWindowElapsed < session.endTimeElapsed) {
919                 final long start;
920                 if (startWindowElapsed < session.startTimeElapsed) {
921                     start = session.startTimeElapsed;
922                     emptyTimeMs =
923                             Math.min(emptyTimeMs, session.startTimeElapsed - startWindowElapsed);
924                 } else {
925                     // The session started before the window but ended within the window. Only
926                     // include the portion that was within the window.
927                     start = startWindowElapsed;
928                     emptyTimeMs = 0;
929                 }
930 
931                 stats.executionTimeInWindowMs += session.endTimeElapsed - start;
932                 stats.bgJobCountInWindow += session.bgJobCount;
933                 if (stats.executionTimeInWindowMs >= mAllowedTimeIntoQuotaMs) {
934                     stats.inQuotaTimeElapsed = Math.max(stats.inQuotaTimeElapsed,
935                             start + stats.executionTimeInWindowMs - mAllowedTimeIntoQuotaMs
936                                     + stats.windowSizeMs);
937                 }
938                 if (stats.bgJobCountInWindow >= stats.jobCountLimit) {
939                     stats.inQuotaTimeElapsed = Math.max(stats.inQuotaTimeElapsed,
940                             session.endTimeElapsed + stats.windowSizeMs);
941                 }
942                 if (i == loopStart
943                         || (sessions.get(i + 1).startTimeElapsed - session.endTimeElapsed)
944                                 > mTimingSessionCoalescingDurationMs) {
945                     // Coalesce sessions if they are very close to each other in time
946                     sessionCountInWindow++;
947 
948                     if (sessionCountInWindow >= stats.sessionCountLimit) {
949                         stats.inQuotaTimeElapsed = Math.max(stats.inQuotaTimeElapsed,
950                                 session.endTimeElapsed + stats.windowSizeMs);
951                     }
952                 }
953             }
954 
955             // Max period check.
956             if (startMaxElapsed < session.startTimeElapsed) {
957                 stats.executionTimeInMaxPeriodMs +=
958                         session.endTimeElapsed - session.startTimeElapsed;
959                 stats.bgJobCountInMaxPeriod += session.bgJobCount;
960                 emptyTimeMs = Math.min(emptyTimeMs, session.startTimeElapsed - startMaxElapsed);
961                 if (stats.executionTimeInMaxPeriodMs >= mMaxExecutionTimeIntoQuotaMs) {
962                     stats.inQuotaTimeElapsed = Math.max(stats.inQuotaTimeElapsed,
963                             session.startTimeElapsed + stats.executionTimeInMaxPeriodMs
964                                     - mMaxExecutionTimeIntoQuotaMs + MAX_PERIOD_MS);
965                 }
966             } else if (startMaxElapsed < session.endTimeElapsed) {
967                 // The session started before the window but ended within the window. Only include
968                 // the portion that was within the window.
969                 stats.executionTimeInMaxPeriodMs += session.endTimeElapsed - startMaxElapsed;
970                 stats.bgJobCountInMaxPeriod += session.bgJobCount;
971                 emptyTimeMs = 0;
972                 if (stats.executionTimeInMaxPeriodMs >= mMaxExecutionTimeIntoQuotaMs) {
973                     stats.inQuotaTimeElapsed = Math.max(stats.inQuotaTimeElapsed,
974                             startMaxElapsed + stats.executionTimeInMaxPeriodMs
975                                     - mMaxExecutionTimeIntoQuotaMs + MAX_PERIOD_MS);
976                 }
977             } else {
978                 // This session ended before the window. No point in going any further.
979                 break;
980             }
981         }
982         stats.expirationTimeElapsed = nowElapsed + emptyTimeMs;
983         stats.sessionCountInWindow = sessionCountInWindow;
984     }
985 
986     /** Invalidate ExecutionStats for all apps. */
987     @VisibleForTesting
invalidateAllExecutionStatsLocked()988     void invalidateAllExecutionStatsLocked() {
989         final long nowElapsed = sElapsedRealtimeClock.millis();
990         mExecutionStatsCache.forEach((appStats) -> {
991             if (appStats != null) {
992                 for (int i = 0; i < appStats.length; ++i) {
993                     ExecutionStats stats = appStats[i];
994                     if (stats != null) {
995                         stats.expirationTimeElapsed = nowElapsed;
996                     }
997                 }
998             }
999         });
1000     }
1001 
1002     @VisibleForTesting
invalidateAllExecutionStatsLocked(final int userId, @NonNull final String packageName)1003     void invalidateAllExecutionStatsLocked(final int userId,
1004             @NonNull final String packageName) {
1005         ExecutionStats[] appStats = mExecutionStatsCache.get(userId, packageName);
1006         if (appStats != null) {
1007             final long nowElapsed = sElapsedRealtimeClock.millis();
1008             for (int i = 0; i < appStats.length; ++i) {
1009                 ExecutionStats stats = appStats[i];
1010                 if (stats != null) {
1011                     stats.expirationTimeElapsed = nowElapsed;
1012                 }
1013             }
1014         }
1015     }
1016 
1017     @VisibleForTesting
incrementJobCount(final int userId, @NonNull final String packageName, int count)1018     void incrementJobCount(final int userId, @NonNull final String packageName, int count) {
1019         final long now = sElapsedRealtimeClock.millis();
1020         ExecutionStats[] appStats = mExecutionStatsCache.get(userId, packageName);
1021         if (appStats == null) {
1022             appStats = new ExecutionStats[mBucketPeriodsMs.length];
1023             mExecutionStatsCache.add(userId, packageName, appStats);
1024         }
1025         for (int i = 0; i < appStats.length; ++i) {
1026             ExecutionStats stats = appStats[i];
1027             if (stats == null) {
1028                 stats = new ExecutionStats();
1029                 appStats[i] = stats;
1030             }
1031             if (stats.jobRateLimitExpirationTimeElapsed <= now) {
1032                 stats.jobRateLimitExpirationTimeElapsed = now + mRateLimitingWindowMs;
1033                 stats.jobCountInRateLimitingWindow = 0;
1034             }
1035             stats.jobCountInRateLimitingWindow += count;
1036         }
1037     }
1038 
incrementTimingSessionCount(final int userId, @NonNull final String packageName)1039     private void incrementTimingSessionCount(final int userId, @NonNull final String packageName) {
1040         final long now = sElapsedRealtimeClock.millis();
1041         ExecutionStats[] appStats = mExecutionStatsCache.get(userId, packageName);
1042         if (appStats == null) {
1043             appStats = new ExecutionStats[mBucketPeriodsMs.length];
1044             mExecutionStatsCache.add(userId, packageName, appStats);
1045         }
1046         for (int i = 0; i < appStats.length; ++i) {
1047             ExecutionStats stats = appStats[i];
1048             if (stats == null) {
1049                 stats = new ExecutionStats();
1050                 appStats[i] = stats;
1051             }
1052             if (stats.sessionRateLimitExpirationTimeElapsed <= now) {
1053                 stats.sessionRateLimitExpirationTimeElapsed = now + mRateLimitingWindowMs;
1054                 stats.sessionCountInRateLimitingWindow = 0;
1055             }
1056             stats.sessionCountInRateLimitingWindow++;
1057         }
1058     }
1059 
1060     @VisibleForTesting
saveTimingSession(final int userId, @NonNull final String packageName, @NonNull final TimingSession session)1061     void saveTimingSession(final int userId, @NonNull final String packageName,
1062             @NonNull final TimingSession session) {
1063         synchronized (mLock) {
1064             List<TimingSession> sessions = mTimingSessions.get(userId, packageName);
1065             if (sessions == null) {
1066                 sessions = new ArrayList<>();
1067                 mTimingSessions.add(userId, packageName, sessions);
1068             }
1069             sessions.add(session);
1070             // Adding a new session means that the current stats are now incorrect.
1071             invalidateAllExecutionStatsLocked(userId, packageName);
1072 
1073             maybeScheduleCleanupAlarmLocked();
1074         }
1075     }
1076 
1077     private final class EarliestEndTimeFunctor implements Consumer<List<TimingSession>> {
1078         public long earliestEndElapsed = Long.MAX_VALUE;
1079 
1080         @Override
accept(List<TimingSession> sessions)1081         public void accept(List<TimingSession> sessions) {
1082             if (sessions != null && sessions.size() > 0) {
1083                 earliestEndElapsed = Math.min(earliestEndElapsed, sessions.get(0).endTimeElapsed);
1084             }
1085         }
1086 
reset()1087         void reset() {
1088             earliestEndElapsed = Long.MAX_VALUE;
1089         }
1090     }
1091 
1092     private final EarliestEndTimeFunctor mEarliestEndTimeFunctor = new EarliestEndTimeFunctor();
1093 
1094     /** Schedule a cleanup alarm if necessary and there isn't already one scheduled. */
1095     @VisibleForTesting
maybeScheduleCleanupAlarmLocked()1096     void maybeScheduleCleanupAlarmLocked() {
1097         if (mNextCleanupTimeElapsed > sElapsedRealtimeClock.millis()) {
1098             // There's already an alarm scheduled. Just stick with that one. There's no way we'll
1099             // end up scheduling an earlier alarm.
1100             if (DEBUG) {
1101                 Slog.v(TAG, "Not scheduling cleanup since there's already one at "
1102                         + mNextCleanupTimeElapsed + " (in " + (mNextCleanupTimeElapsed
1103                         - sElapsedRealtimeClock.millis()) + "ms)");
1104             }
1105             return;
1106         }
1107         mEarliestEndTimeFunctor.reset();
1108         mTimingSessions.forEach(mEarliestEndTimeFunctor);
1109         final long earliestEndElapsed = mEarliestEndTimeFunctor.earliestEndElapsed;
1110         if (earliestEndElapsed == Long.MAX_VALUE) {
1111             // Couldn't find a good time to clean up. Maybe this was called after we deleted all
1112             // timing sessions.
1113             if (DEBUG) {
1114                 Slog.d(TAG, "Didn't find a time to schedule cleanup");
1115             }
1116             return;
1117         }
1118         // Need to keep sessions for all apps up to the max period, regardless of their current
1119         // standby bucket.
1120         long nextCleanupElapsed = earliestEndElapsed + MAX_PERIOD_MS;
1121         if (nextCleanupElapsed - mNextCleanupTimeElapsed <= 10 * MINUTE_IN_MILLIS) {
1122             // No need to clean up too often. Delay the alarm if the next cleanup would be too soon
1123             // after it.
1124             nextCleanupElapsed += 10 * MINUTE_IN_MILLIS;
1125         }
1126         mNextCleanupTimeElapsed = nextCleanupElapsed;
1127         mAlarmManager.set(AlarmManager.ELAPSED_REALTIME, nextCleanupElapsed, ALARM_TAG_CLEANUP,
1128                 mSessionCleanupAlarmListener, mHandler);
1129         if (DEBUG) {
1130             Slog.d(TAG, "Scheduled next cleanup for " + mNextCleanupTimeElapsed);
1131         }
1132     }
1133 
1134     private class TimerChargingUpdateFunctor implements Consumer<Timer> {
1135         private long mNowElapsed;
1136         private boolean mIsCharging;
1137 
setStatus(long nowElapsed, boolean isCharging)1138         private void setStatus(long nowElapsed, boolean isCharging) {
1139             mNowElapsed = nowElapsed;
1140             mIsCharging = isCharging;
1141         }
1142 
1143         @Override
accept(Timer timer)1144         public void accept(Timer timer) {
1145             if (JobSchedulerService.standbyBucketForPackage(timer.mPkg.packageName,
1146                     timer.mPkg.userId, mNowElapsed) != RESTRICTED_INDEX) {
1147                 // Restricted jobs need additional constraints even when charging, so don't
1148                 // immediately say that quota is free.
1149                 timer.onStateChangedLocked(mNowElapsed, mIsCharging);
1150             }
1151         }
1152     }
1153 
1154     private final TimerChargingUpdateFunctor
1155             mTimerChargingUpdateFunctor = new TimerChargingUpdateFunctor();
1156 
handleNewChargingStateLocked()1157     private void handleNewChargingStateLocked() {
1158         mTimerChargingUpdateFunctor.setStatus(sElapsedRealtimeClock.millis(),
1159                 mChargeTracker.isCharging());
1160         if (DEBUG) {
1161             Slog.d(TAG, "handleNewChargingStateLocked: " + mChargeTracker.isCharging());
1162         }
1163         // Deal with Timers first.
1164         mPkgTimers.forEach(mTimerChargingUpdateFunctor);
1165         // Now update jobs.
1166         maybeUpdateAllConstraintsLocked();
1167     }
1168 
maybeUpdateAllConstraintsLocked()1169     private void maybeUpdateAllConstraintsLocked() {
1170         boolean changed = false;
1171         for (int u = 0; u < mTrackedJobs.numMaps(); ++u) {
1172             final int userId = mTrackedJobs.keyAt(u);
1173             for (int p = 0; p < mTrackedJobs.numElementsForKey(userId); ++p) {
1174                 final String packageName = mTrackedJobs.keyAt(u, p);
1175                 changed |= maybeUpdateConstraintForPkgLocked(userId, packageName);
1176             }
1177         }
1178         if (changed) {
1179             mStateChangedListener.onControllerStateChanged();
1180         }
1181     }
1182 
1183     /**
1184      * Update the CONSTRAINT_WITHIN_QUOTA bit for all of the Jobs for a given package.
1185      *
1186      * @return true if at least one job had its bit changed
1187      */
maybeUpdateConstraintForPkgLocked(final int userId, @NonNull final String packageName)1188     private boolean maybeUpdateConstraintForPkgLocked(final int userId,
1189             @NonNull final String packageName) {
1190         ArraySet<JobStatus> jobs = mTrackedJobs.get(userId, packageName);
1191         if (jobs == null || jobs.size() == 0) {
1192             return false;
1193         }
1194 
1195         // Quota is the same for all jobs within a package.
1196         final int realStandbyBucket = jobs.valueAt(0).getStandbyBucket();
1197         final boolean realInQuota = isWithinQuotaLocked(userId, packageName, realStandbyBucket);
1198         boolean changed = false;
1199         for (int i = jobs.size() - 1; i >= 0; --i) {
1200             final JobStatus js = jobs.valueAt(i);
1201             if (isTopStartedJobLocked(js)) {
1202                 // Job was started while the app was in the TOP state so we should allow it to
1203                 // finish.
1204                 changed |= js.setQuotaConstraintSatisfied(true);
1205             } else if (realStandbyBucket != ACTIVE_INDEX
1206                     && realStandbyBucket == js.getEffectiveStandbyBucket()) {
1207                 // An app in the ACTIVE bucket may be out of quota while the job could be in quota
1208                 // for some reason. Therefore, avoid setting the real value here and check each job
1209                 // individually.
1210                 changed |= setConstraintSatisfied(js, realInQuota);
1211             } else {
1212                 // This job is somehow exempted. Need to determine its own quota status.
1213                 changed |= setConstraintSatisfied(js, isWithinQuotaLocked(js));
1214             }
1215         }
1216         if (!realInQuota) {
1217             // Don't want to use the effective standby bucket here since that bump the bucket to
1218             // ACTIVE for one of the jobs, which doesn't help with other jobs that aren't
1219             // exempted.
1220             maybeScheduleStartAlarmLocked(userId, packageName, realStandbyBucket);
1221         } else {
1222             mInQuotaAlarmListener.removeAlarmLocked(userId, packageName);
1223         }
1224         return changed;
1225     }
1226 
1227     private class UidConstraintUpdater implements Consumer<JobStatus> {
1228         private final SparseArrayMap<Integer> mToScheduleStartAlarms = new SparseArrayMap<>();
1229         public boolean wasJobChanged;
1230 
1231         @Override
accept(JobStatus jobStatus)1232         public void accept(JobStatus jobStatus) {
1233             wasJobChanged |= setConstraintSatisfied(jobStatus, isWithinQuotaLocked(jobStatus));
1234             final int userId = jobStatus.getSourceUserId();
1235             final String packageName = jobStatus.getSourcePackageName();
1236             final int realStandbyBucket = jobStatus.getStandbyBucket();
1237             if (isWithinQuotaLocked(userId, packageName, realStandbyBucket)) {
1238                 mInQuotaAlarmListener.removeAlarmLocked(userId, packageName);
1239             } else {
1240                 mToScheduleStartAlarms.add(userId, packageName, realStandbyBucket);
1241             }
1242         }
1243 
postProcess()1244         void postProcess() {
1245             for (int u = 0; u < mToScheduleStartAlarms.numMaps(); ++u) {
1246                 final int userId = mToScheduleStartAlarms.keyAt(u);
1247                 for (int p = 0; p < mToScheduleStartAlarms.numElementsForKey(userId); ++p) {
1248                     final String packageName = mToScheduleStartAlarms.keyAt(u, p);
1249                     final int standbyBucket = mToScheduleStartAlarms.get(userId, packageName);
1250                     maybeScheduleStartAlarmLocked(userId, packageName, standbyBucket);
1251                 }
1252             }
1253         }
1254 
reset()1255         void reset() {
1256             wasJobChanged = false;
1257             mToScheduleStartAlarms.clear();
1258         }
1259     }
1260 
1261     private final UidConstraintUpdater mUpdateUidConstraints = new UidConstraintUpdater();
1262 
maybeUpdateConstraintForUidLocked(final int uid)1263     private boolean maybeUpdateConstraintForUidLocked(final int uid) {
1264         mService.getJobStore().forEachJobForSourceUid(uid, mUpdateUidConstraints);
1265 
1266         mUpdateUidConstraints.postProcess();
1267         boolean changed = mUpdateUidConstraints.wasJobChanged;
1268         mUpdateUidConstraints.reset();
1269         return changed;
1270     }
1271 
1272     /**
1273      * Maybe schedule a non-wakeup alarm for the next time this package will have quota to run
1274      * again. This should only be called if the package is already out of quota.
1275      */
1276     @VisibleForTesting
maybeScheduleStartAlarmLocked(final int userId, @NonNull final String packageName, final int standbyBucket)1277     void maybeScheduleStartAlarmLocked(final int userId, @NonNull final String packageName,
1278             final int standbyBucket) {
1279         if (standbyBucket == NEVER_INDEX) {
1280             return;
1281         }
1282 
1283         final String pkgString = string(userId, packageName);
1284         ExecutionStats stats = getExecutionStatsLocked(userId, packageName, standbyBucket);
1285         final boolean isUnderJobCountQuota = isUnderJobCountQuotaLocked(stats, standbyBucket);
1286         final boolean isUnderTimingSessionCountQuota = isUnderSessionCountQuotaLocked(stats,
1287                 standbyBucket);
1288 
1289         if (stats.executionTimeInWindowMs < mAllowedTimePerPeriodMs
1290                 && stats.executionTimeInMaxPeriodMs < mMaxExecutionTimeMs
1291                 && isUnderJobCountQuota
1292                 && isUnderTimingSessionCountQuota) {
1293             // Already in quota. Why was this method called?
1294             if (DEBUG) {
1295                 Slog.e(TAG, "maybeScheduleStartAlarmLocked called for " + pkgString
1296                         + " even though it already has "
1297                         + getRemainingExecutionTimeLocked(userId, packageName, standbyBucket)
1298                         + "ms in its quota.");
1299             }
1300             mInQuotaAlarmListener.removeAlarmLocked(userId, packageName);
1301             mHandler.obtainMessage(MSG_CHECK_PACKAGE, userId, 0, packageName).sendToTarget();
1302             return;
1303         }
1304 
1305         // The time this app will have quota again.
1306         long inQuotaTimeElapsed = stats.inQuotaTimeElapsed;
1307         if (!isUnderJobCountQuota && stats.bgJobCountInWindow < stats.jobCountLimit) {
1308             // App hit the rate limit.
1309             inQuotaTimeElapsed = Math.max(inQuotaTimeElapsed,
1310                     stats.jobRateLimitExpirationTimeElapsed);
1311         }
1312         if (!isUnderTimingSessionCountQuota
1313                 && stats.sessionCountInWindow < stats.sessionCountLimit) {
1314             // App hit the rate limit.
1315             inQuotaTimeElapsed = Math.max(inQuotaTimeElapsed,
1316                     stats.sessionRateLimitExpirationTimeElapsed);
1317         }
1318         if (inQuotaTimeElapsed <= sElapsedRealtimeClock.millis()) {
1319             final long nowElapsed = sElapsedRealtimeClock.millis();
1320             Slog.wtf(TAG,
1321                     "In quota time is " + (nowElapsed - inQuotaTimeElapsed) + "ms old. Now="
1322                             + nowElapsed + ", inQuotaTime=" + inQuotaTimeElapsed + ": " + stats);
1323             inQuotaTimeElapsed = nowElapsed + 5 * MINUTE_IN_MILLIS;
1324         }
1325         mInQuotaAlarmListener.addAlarmLocked(userId, packageName, inQuotaTimeElapsed);
1326     }
1327 
setConstraintSatisfied(@onNull JobStatus jobStatus, boolean isWithinQuota)1328     private boolean setConstraintSatisfied(@NonNull JobStatus jobStatus, boolean isWithinQuota) {
1329         if (!isWithinQuota && jobStatus.getWhenStandbyDeferred() == 0) {
1330             // Mark that the job is being deferred due to buckets.
1331             jobStatus.setWhenStandbyDeferred(sElapsedRealtimeClock.millis());
1332         }
1333         return jobStatus.setQuotaConstraintSatisfied(isWithinQuota);
1334     }
1335 
1336     private final class ChargingTracker extends BroadcastReceiver {
1337         /**
1338          * Track whether we're charging. This has a slightly different definition than that of
1339          * BatteryController.
1340          */
1341         private boolean mCharging;
1342 
ChargingTracker()1343         ChargingTracker() {
1344         }
1345 
startTracking()1346         public void startTracking() {
1347             IntentFilter filter = new IntentFilter();
1348 
1349             // Charging/not charging.
1350             filter.addAction(BatteryManager.ACTION_CHARGING);
1351             filter.addAction(BatteryManager.ACTION_DISCHARGING);
1352             mContext.registerReceiver(this, filter);
1353 
1354             // Initialise tracker state.
1355             BatteryManagerInternal batteryManagerInternal =
1356                     LocalServices.getService(BatteryManagerInternal.class);
1357             mCharging = batteryManagerInternal.isPowered(BatteryManager.BATTERY_PLUGGED_ANY);
1358         }
1359 
isCharging()1360         public boolean isCharging() {
1361             return mCharging;
1362         }
1363 
1364         @Override
onReceive(Context context, Intent intent)1365         public void onReceive(Context context, Intent intent) {
1366             synchronized (mLock) {
1367                 final String action = intent.getAction();
1368                 if (BatteryManager.ACTION_CHARGING.equals(action)) {
1369                     if (DEBUG) {
1370                         Slog.d(TAG, "Received charging intent, fired @ "
1371                                 + sElapsedRealtimeClock.millis());
1372                     }
1373                     mCharging = true;
1374                     handleNewChargingStateLocked();
1375                 } else if (BatteryManager.ACTION_DISCHARGING.equals(action)) {
1376                     if (DEBUG) {
1377                         Slog.d(TAG, "Disconnected from power.");
1378                     }
1379                     mCharging = false;
1380                     handleNewChargingStateLocked();
1381                 }
1382             }
1383         }
1384     }
1385 
1386     @VisibleForTesting
1387     static final class TimingSession {
1388         // Start timestamp in elapsed realtime timebase.
1389         public final long startTimeElapsed;
1390         // End timestamp in elapsed realtime timebase.
1391         public final long endTimeElapsed;
1392         // How many background jobs ran during this session.
1393         public final int bgJobCount;
1394 
1395         private final int mHashCode;
1396 
TimingSession(long startElapsed, long endElapsed, int bgJobCount)1397         TimingSession(long startElapsed, long endElapsed, int bgJobCount) {
1398             this.startTimeElapsed = startElapsed;
1399             this.endTimeElapsed = endElapsed;
1400             this.bgJobCount = bgJobCount;
1401 
1402             int hashCode = 0;
1403             hashCode = 31 * hashCode + hashLong(startTimeElapsed);
1404             hashCode = 31 * hashCode + hashLong(endTimeElapsed);
1405             hashCode = 31 * hashCode + bgJobCount;
1406             mHashCode = hashCode;
1407         }
1408 
1409         @Override
toString()1410         public String toString() {
1411             return "TimingSession{" + startTimeElapsed + "->" + endTimeElapsed + ", " + bgJobCount
1412                     + "}";
1413         }
1414 
1415         @Override
equals(Object obj)1416         public boolean equals(Object obj) {
1417             if (obj instanceof TimingSession) {
1418                 TimingSession other = (TimingSession) obj;
1419                 return startTimeElapsed == other.startTimeElapsed
1420                         && endTimeElapsed == other.endTimeElapsed
1421                         && bgJobCount == other.bgJobCount;
1422             } else {
1423                 return false;
1424             }
1425         }
1426 
1427         @Override
hashCode()1428         public int hashCode() {
1429             return mHashCode;
1430         }
1431 
dump(IndentingPrintWriter pw)1432         public void dump(IndentingPrintWriter pw) {
1433             pw.print(startTimeElapsed);
1434             pw.print(" -> ");
1435             pw.print(endTimeElapsed);
1436             pw.print(" (");
1437             pw.print(endTimeElapsed - startTimeElapsed);
1438             pw.print("), ");
1439             pw.print(bgJobCount);
1440             pw.print(" bg jobs.");
1441             pw.println();
1442         }
1443 
dump(@onNull ProtoOutputStream proto, long fieldId)1444         public void dump(@NonNull ProtoOutputStream proto, long fieldId) {
1445             final long token = proto.start(fieldId);
1446 
1447             proto.write(StateControllerProto.QuotaController.TimingSession.START_TIME_ELAPSED,
1448                     startTimeElapsed);
1449             proto.write(StateControllerProto.QuotaController.TimingSession.END_TIME_ELAPSED,
1450                     endTimeElapsed);
1451             proto.write(StateControllerProto.QuotaController.TimingSession.BG_JOB_COUNT,
1452                     bgJobCount);
1453 
1454             proto.end(token);
1455         }
1456     }
1457 
1458     private final class Timer {
1459         private final Package mPkg;
1460         private final int mUid;
1461 
1462         // List of jobs currently running for this app that started when the app wasn't in the
1463         // foreground.
1464         private final ArraySet<JobStatus> mRunningBgJobs = new ArraySet<>();
1465         private long mStartTimeElapsed;
1466         private int mBgJobCount;
1467 
Timer(int uid, int userId, String packageName)1468         Timer(int uid, int userId, String packageName) {
1469             mPkg = new Package(userId, packageName);
1470             mUid = uid;
1471         }
1472 
startTrackingJobLocked(@onNull JobStatus jobStatus)1473         void startTrackingJobLocked(@NonNull JobStatus jobStatus) {
1474             if (isTopStartedJobLocked(jobStatus)) {
1475                 // We intentionally don't pay attention to fg state changes after a TOP job has
1476                 // started.
1477                 if (DEBUG) {
1478                     Slog.v(TAG,
1479                             "Timer ignoring " + jobStatus.toShortString() + " because isTop");
1480                 }
1481                 return;
1482             }
1483             if (DEBUG) {
1484                 Slog.v(TAG, "Starting to track " + jobStatus.toShortString());
1485             }
1486             // Always track jobs, even when charging.
1487             mRunningBgJobs.add(jobStatus);
1488             if (shouldTrackLocked()) {
1489                 mBgJobCount++;
1490                 incrementJobCount(mPkg.userId, mPkg.packageName, 1);
1491                 if (mRunningBgJobs.size() == 1) {
1492                     // Started tracking the first job.
1493                     mStartTimeElapsed = sElapsedRealtimeClock.millis();
1494                     // Starting the timer means that all cached execution stats are now incorrect.
1495                     invalidateAllExecutionStatsLocked(mPkg.userId, mPkg.packageName);
1496                     scheduleCutoff();
1497                 }
1498             }
1499         }
1500 
stopTrackingJob(@onNull JobStatus jobStatus)1501         void stopTrackingJob(@NonNull JobStatus jobStatus) {
1502             if (DEBUG) {
1503                 Slog.v(TAG, "Stopping tracking of " + jobStatus.toShortString());
1504             }
1505             synchronized (mLock) {
1506                 if (mRunningBgJobs.size() == 0) {
1507                     // maybeStopTrackingJobLocked can be called when an app cancels a job, so a
1508                     // timer may not be running when it's asked to stop tracking a job.
1509                     if (DEBUG) {
1510                         Slog.d(TAG, "Timer isn't tracking any jobs but still told to stop");
1511                     }
1512                     return;
1513                 }
1514                 if (mRunningBgJobs.remove(jobStatus)
1515                         && !mChargeTracker.isCharging() && mRunningBgJobs.size() == 0) {
1516                     emitSessionLocked(sElapsedRealtimeClock.millis());
1517                     cancelCutoff();
1518                 }
1519             }
1520         }
1521 
1522         /**
1523          * Stops tracking all jobs and cancels any pending alarms. This should only be called if
1524          * the Timer is not going to be used anymore.
1525          */
dropEverythingLocked()1526         void dropEverythingLocked() {
1527             mRunningBgJobs.clear();
1528             cancelCutoff();
1529         }
1530 
emitSessionLocked(long nowElapsed)1531         private void emitSessionLocked(long nowElapsed) {
1532             if (mBgJobCount <= 0) {
1533                 // Nothing to emit.
1534                 return;
1535             }
1536             TimingSession ts = new TimingSession(mStartTimeElapsed, nowElapsed, mBgJobCount);
1537             saveTimingSession(mPkg.userId, mPkg.packageName, ts);
1538             mBgJobCount = 0;
1539             // Don't reset the tracked jobs list as we need to keep tracking the current number
1540             // of jobs.
1541             // However, cancel the currently scheduled cutoff since it's not currently useful.
1542             cancelCutoff();
1543             incrementTimingSessionCount(mPkg.userId, mPkg.packageName);
1544         }
1545 
1546         /**
1547          * Returns true if the Timer is actively tracking, as opposed to passively ref counting
1548          * during charging.
1549          */
isActive()1550         public boolean isActive() {
1551             synchronized (mLock) {
1552                 return mBgJobCount > 0;
1553             }
1554         }
1555 
isRunning(JobStatus jobStatus)1556         boolean isRunning(JobStatus jobStatus) {
1557             return mRunningBgJobs.contains(jobStatus);
1558         }
1559 
getCurrentDuration(long nowElapsed)1560         long getCurrentDuration(long nowElapsed) {
1561             synchronized (mLock) {
1562                 return !isActive() ? 0 : nowElapsed - mStartTimeElapsed;
1563             }
1564         }
1565 
getBgJobCount()1566         int getBgJobCount() {
1567             synchronized (mLock) {
1568                 return mBgJobCount;
1569             }
1570         }
1571 
shouldTrackLocked()1572         private boolean shouldTrackLocked() {
1573             final int standbyBucket = JobSchedulerService.standbyBucketForPackage(mPkg.packageName,
1574                     mPkg.userId, sElapsedRealtimeClock.millis());
1575             return (standbyBucket == RESTRICTED_INDEX || !mChargeTracker.isCharging())
1576                     && !mForegroundUids.get(mUid);
1577         }
1578 
onStateChangedLocked(long nowElapsed, boolean isQuotaFree)1579         void onStateChangedLocked(long nowElapsed, boolean isQuotaFree) {
1580             if (isQuotaFree) {
1581                 emitSessionLocked(nowElapsed);
1582             } else if (!isActive() && shouldTrackLocked()) {
1583                 // Start timing from unplug.
1584                 if (mRunningBgJobs.size() > 0) {
1585                     mStartTimeElapsed = nowElapsed;
1586                     // NOTE: this does have the unfortunate consequence that if the device is
1587                     // repeatedly plugged in and unplugged, or an app changes foreground state
1588                     // very frequently, the job count for a package may be artificially high.
1589                     mBgJobCount = mRunningBgJobs.size();
1590                     incrementJobCount(mPkg.userId, mPkg.packageName, mBgJobCount);
1591                     // Starting the timer means that all cached execution stats are now
1592                     // incorrect.
1593                     invalidateAllExecutionStatsLocked(mPkg.userId, mPkg.packageName);
1594                     // Schedule cutoff since we're now actively tracking for quotas again.
1595                     scheduleCutoff();
1596                 }
1597             }
1598         }
1599 
rescheduleCutoff()1600         void rescheduleCutoff() {
1601             cancelCutoff();
1602             scheduleCutoff();
1603         }
1604 
scheduleCutoff()1605         private void scheduleCutoff() {
1606             // Each package can only be in one standby bucket, so we only need to have one
1607             // message per timer. We only need to reschedule when restarting timer or when
1608             // standby bucket changes.
1609             synchronized (mLock) {
1610                 if (!isActive()) {
1611                     return;
1612                 }
1613                 Message msg = mHandler.obtainMessage(MSG_REACHED_QUOTA, mPkg);
1614                 final long timeRemainingMs = getTimeUntilQuotaConsumedLocked(mPkg.userId,
1615                         mPkg.packageName);
1616                 if (DEBUG) {
1617                     Slog.i(TAG, "Job for " + mPkg + " has " + timeRemainingMs + "ms left.");
1618                 }
1619                 // If the job was running the entire time, then the system would be up, so it's
1620                 // fine to use uptime millis for these messages.
1621                 mHandler.sendMessageDelayed(msg, timeRemainingMs);
1622             }
1623         }
1624 
cancelCutoff()1625         private void cancelCutoff() {
1626             mHandler.removeMessages(MSG_REACHED_QUOTA, mPkg);
1627         }
1628 
dump(IndentingPrintWriter pw, Predicate<JobStatus> predicate)1629         public void dump(IndentingPrintWriter pw, Predicate<JobStatus> predicate) {
1630             pw.print("Timer{");
1631             pw.print(mPkg);
1632             pw.print("} ");
1633             if (isActive()) {
1634                 pw.print("started at ");
1635                 pw.print(mStartTimeElapsed);
1636                 pw.print(" (");
1637                 pw.print(sElapsedRealtimeClock.millis() - mStartTimeElapsed);
1638                 pw.print("ms ago)");
1639             } else {
1640                 pw.print("NOT active");
1641             }
1642             pw.print(", ");
1643             pw.print(mBgJobCount);
1644             pw.print(" running bg jobs");
1645             pw.println();
1646             pw.increaseIndent();
1647             for (int i = 0; i < mRunningBgJobs.size(); i++) {
1648                 JobStatus js = mRunningBgJobs.valueAt(i);
1649                 if (predicate.test(js)) {
1650                     pw.println(js.toShortString());
1651                 }
1652             }
1653             pw.decreaseIndent();
1654         }
1655 
dump(ProtoOutputStream proto, long fieldId, Predicate<JobStatus> predicate)1656         public void dump(ProtoOutputStream proto, long fieldId, Predicate<JobStatus> predicate) {
1657             final long token = proto.start(fieldId);
1658 
1659             mPkg.dumpDebug(proto, StateControllerProto.QuotaController.Timer.PKG);
1660             proto.write(StateControllerProto.QuotaController.Timer.IS_ACTIVE, isActive());
1661             proto.write(StateControllerProto.QuotaController.Timer.START_TIME_ELAPSED,
1662                     mStartTimeElapsed);
1663             proto.write(StateControllerProto.QuotaController.Timer.BG_JOB_COUNT, mBgJobCount);
1664             for (int i = 0; i < mRunningBgJobs.size(); i++) {
1665                 JobStatus js = mRunningBgJobs.valueAt(i);
1666                 if (predicate.test(js)) {
1667                     js.writeToShortProto(proto,
1668                             StateControllerProto.QuotaController.Timer.RUNNING_JOBS);
1669                 }
1670             }
1671 
1672             proto.end(token);
1673         }
1674     }
1675 
1676     /**
1677      * Tracking of app assignments to standby buckets
1678      */
1679     final class StandbyTracker extends AppIdleStateChangeListener {
1680 
1681         @Override
onAppIdleStateChanged(final String packageName, final @UserIdInt int userId, boolean idle, int bucket, int reason)1682         public void onAppIdleStateChanged(final String packageName, final @UserIdInt int userId,
1683                 boolean idle, int bucket, int reason) {
1684             // Update job bookkeeping out of band.
1685             BackgroundThread.getHandler().post(() -> {
1686                 final int bucketIndex = JobSchedulerService.standbyBucketToBucketIndex(bucket);
1687                 if (DEBUG) {
1688                     Slog.i(TAG, "Moving pkg " + string(userId, packageName) + " to bucketIndex "
1689                             + bucketIndex);
1690                 }
1691                 List<JobStatus> restrictedChanges = new ArrayList<>();
1692                 synchronized (mLock) {
1693                     ArraySet<JobStatus> jobs = mTrackedJobs.get(userId, packageName);
1694                     if (jobs == null || jobs.size() == 0) {
1695                         return;
1696                     }
1697                     for (int i = jobs.size() - 1; i >= 0; i--) {
1698                         JobStatus js = jobs.valueAt(i);
1699                         // Effective standby bucket can change after this in some situations so
1700                         // use the real bucket so that the job is tracked by the controllers.
1701                         if ((bucketIndex == RESTRICTED_INDEX
1702                                 || js.getStandbyBucket() == RESTRICTED_INDEX)
1703                                 && bucketIndex != js.getStandbyBucket()) {
1704                             restrictedChanges.add(js);
1705                         }
1706                         js.setStandbyBucket(bucketIndex);
1707                     }
1708                     Timer timer = mPkgTimers.get(userId, packageName);
1709                     if (timer != null && timer.isActive()) {
1710                         timer.rescheduleCutoff();
1711                     }
1712                     if (maybeUpdateConstraintForPkgLocked(userId, packageName)) {
1713                         mStateChangedListener.onControllerStateChanged();
1714                     }
1715                 }
1716                 if (restrictedChanges.size() > 0) {
1717                     mStateChangedListener.onRestrictedBucketChanged(restrictedChanges);
1718                 }
1719             });
1720         }
1721     }
1722 
1723     private final class DeleteTimingSessionsFunctor implements Consumer<List<TimingSession>> {
1724         private final Predicate<TimingSession> mTooOld = new Predicate<TimingSession>() {
1725             public boolean test(TimingSession ts) {
1726                 return ts.endTimeElapsed <= sElapsedRealtimeClock.millis() - MAX_PERIOD_MS;
1727             }
1728         };
1729 
1730         @Override
accept(List<TimingSession> sessions)1731         public void accept(List<TimingSession> sessions) {
1732             if (sessions != null) {
1733                 // Remove everything older than MAX_PERIOD_MS time ago.
1734                 sessions.removeIf(mTooOld);
1735             }
1736         }
1737     }
1738 
1739     private final DeleteTimingSessionsFunctor mDeleteOldSessionsFunctor =
1740             new DeleteTimingSessionsFunctor();
1741 
1742     @VisibleForTesting
deleteObsoleteSessionsLocked()1743     void deleteObsoleteSessionsLocked() {
1744         mTimingSessions.forEach(mDeleteOldSessionsFunctor);
1745     }
1746 
1747     private class QcHandler extends Handler {
QcHandler(Looper looper)1748         QcHandler(Looper looper) {
1749             super(looper);
1750         }
1751 
1752         @Override
handleMessage(Message msg)1753         public void handleMessage(Message msg) {
1754             synchronized (mLock) {
1755                 switch (msg.what) {
1756                     case MSG_REACHED_QUOTA: {
1757                         Package pkg = (Package) msg.obj;
1758                         if (DEBUG) {
1759                             Slog.d(TAG, "Checking if " + pkg + " has reached its quota.");
1760                         }
1761 
1762                         long timeRemainingMs = getRemainingExecutionTimeLocked(pkg.userId,
1763                                 pkg.packageName);
1764                         if (timeRemainingMs <= 50) {
1765                             // Less than 50 milliseconds left. Start process of shutting down jobs.
1766                             if (DEBUG) Slog.d(TAG, pkg + " has reached its quota.");
1767                             if (maybeUpdateConstraintForPkgLocked(pkg.userId, pkg.packageName)) {
1768                                 mStateChangedListener.onControllerStateChanged();
1769                             }
1770                         } else {
1771                             // This could potentially happen if an old session phases out while a
1772                             // job is currently running.
1773                             // Reschedule message
1774                             Message rescheduleMsg = obtainMessage(MSG_REACHED_QUOTA, pkg);
1775                             timeRemainingMs = getTimeUntilQuotaConsumedLocked(pkg.userId,
1776                                     pkg.packageName);
1777                             if (DEBUG) {
1778                                 Slog.d(TAG, pkg + " has " + timeRemainingMs + "ms left.");
1779                             }
1780                             sendMessageDelayed(rescheduleMsg, timeRemainingMs);
1781                         }
1782                         break;
1783                     }
1784                     case MSG_CLEAN_UP_SESSIONS:
1785                         if (DEBUG) {
1786                             Slog.d(TAG, "Cleaning up timing sessions.");
1787                         }
1788                         deleteObsoleteSessionsLocked();
1789                         maybeScheduleCleanupAlarmLocked();
1790 
1791                         break;
1792                     case MSG_CHECK_PACKAGE: {
1793                         String packageName = (String) msg.obj;
1794                         int userId = msg.arg1;
1795                         if (DEBUG) {
1796                             Slog.d(TAG, "Checking pkg " + string(userId, packageName));
1797                         }
1798                         if (maybeUpdateConstraintForPkgLocked(userId, packageName)) {
1799                             mStateChangedListener.onControllerStateChanged();
1800                         }
1801                         break;
1802                     }
1803                     case MSG_UID_PROCESS_STATE_CHANGED: {
1804                         final int uid = msg.arg1;
1805                         final int procState = msg.arg2;
1806                         final int userId = UserHandle.getUserId(uid);
1807                         final long nowElapsed = sElapsedRealtimeClock.millis();
1808 
1809                         synchronized (mLock) {
1810                             boolean isQuotaFree;
1811                             if (procState <= ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE) {
1812                                 mForegroundUids.put(uid, true);
1813                                 isQuotaFree = true;
1814                             } else {
1815                                 mForegroundUids.delete(uid);
1816                                 isQuotaFree = false;
1817                             }
1818                             // Update Timers first.
1819                             if (mPkgTimers.indexOfKey(userId) >= 0) {
1820                                 ArraySet<String> packages = mUidToPackageCache.get(uid);
1821                                 if (packages == null) {
1822                                     try {
1823                                         String[] pkgs = AppGlobals.getPackageManager()
1824                                                 .getPackagesForUid(uid);
1825                                         if (pkgs != null) {
1826                                             for (String pkg : pkgs) {
1827                                                 mUidToPackageCache.add(uid, pkg);
1828                                             }
1829                                             packages = mUidToPackageCache.get(uid);
1830                                         }
1831                                     } catch (RemoteException e) {
1832                                         Slog.wtf(TAG, "Failed to get package list", e);
1833                                     }
1834                                 }
1835                                 if (packages != null) {
1836                                     for (int i = packages.size() - 1; i >= 0; --i) {
1837                                         Timer t = mPkgTimers.get(userId, packages.valueAt(i));
1838                                         if (t != null) {
1839                                             t.onStateChangedLocked(nowElapsed, isQuotaFree);
1840                                         }
1841                                     }
1842                                 }
1843                             }
1844                             if (maybeUpdateConstraintForUidLocked(uid)) {
1845                                 mStateChangedListener.onControllerStateChanged();
1846                             }
1847                         }
1848                         break;
1849                     }
1850                 }
1851             }
1852         }
1853     }
1854 
1855     static class AlarmQueue extends PriorityQueue<Pair<Package, Long>> {
AlarmQueue()1856         AlarmQueue() {
1857             super(1, (o1, o2) -> (int) (o1.second - o2.second));
1858         }
1859 
1860         /**
1861          * Remove any instances of the Package from the queue.
1862          *
1863          * @return true if an instance was removed, false otherwise.
1864          */
remove(@onNull Package pkg)1865         boolean remove(@NonNull Package pkg) {
1866             boolean removed = false;
1867             Pair[] alarms = toArray(new Pair[size()]);
1868             for (int i = alarms.length - 1; i >= 0; --i) {
1869                 if (pkg.equals(alarms[i].first)) {
1870                     remove(alarms[i]);
1871                     removed = true;
1872                 }
1873             }
1874             return removed;
1875         }
1876     }
1877 
1878     /** Track when UPTCs are expected to come back into quota. */
1879     private class InQuotaAlarmListener implements AlarmManager.OnAlarmListener {
1880         @GuardedBy("mLock")
1881         private final AlarmQueue mAlarmQueue = new AlarmQueue();
1882         /** The next time the alarm is set to go off, in the elapsed realtime timebase. */
1883         @GuardedBy("mLock")
1884         private long mTriggerTimeElapsed = 0;
1885         /** The minimum amount of time between quota check alarms. */
1886         @GuardedBy("mLock")
1887         private long mMinQuotaCheckDelayMs = QcConstants.DEFAULT_MIN_QUOTA_CHECK_DELAY_MS;
1888 
1889         @GuardedBy("mLock")
addAlarmLocked(int userId, @NonNull String pkgName, long inQuotaTimeElapsed)1890         void addAlarmLocked(int userId, @NonNull String pkgName, long inQuotaTimeElapsed) {
1891             final Package pkg = new Package(userId, pkgName);
1892             mAlarmQueue.remove(pkg);
1893             mAlarmQueue.offer(new Pair<>(pkg, inQuotaTimeElapsed));
1894             setNextAlarmLocked();
1895         }
1896 
1897         @GuardedBy("mLock")
setMinQuotaCheckDelayMs(long minDelayMs)1898         void setMinQuotaCheckDelayMs(long minDelayMs) {
1899             mMinQuotaCheckDelayMs = minDelayMs;
1900         }
1901 
1902         @GuardedBy("mLock")
removeAlarmLocked(@onNull Package pkg)1903         void removeAlarmLocked(@NonNull Package pkg) {
1904             if (mAlarmQueue.remove(pkg)) {
1905                 setNextAlarmLocked();
1906             }
1907         }
1908 
1909         @GuardedBy("mLock")
removeAlarmLocked(int userId, @NonNull String packageName)1910         void removeAlarmLocked(int userId, @NonNull String packageName) {
1911             removeAlarmLocked(new Package(userId, packageName));
1912         }
1913 
1914         @GuardedBy("mLock")
removeAlarmsLocked(int userId)1915         void removeAlarmsLocked(int userId) {
1916             boolean removed = false;
1917             Pair[] alarms = mAlarmQueue.toArray(new Pair[mAlarmQueue.size()]);
1918             for (int i = alarms.length - 1; i >= 0; --i) {
1919                 final Package pkg = (Package) alarms[i].first;
1920                 if (userId == pkg.userId) {
1921                     mAlarmQueue.remove(alarms[i]);
1922                     removed = true;
1923                 }
1924             }
1925             if (removed) {
1926                 setNextAlarmLocked();
1927             }
1928         }
1929 
1930         @GuardedBy("mLock")
setNextAlarmLocked()1931         private void setNextAlarmLocked() {
1932             setNextAlarmLocked(sElapsedRealtimeClock.millis());
1933         }
1934 
1935         @GuardedBy("mLock")
setNextAlarmLocked(long earliestTriggerElapsed)1936         private void setNextAlarmLocked(long earliestTriggerElapsed) {
1937             if (mAlarmQueue.size() > 0) {
1938                 final Pair<Package, Long> alarm = mAlarmQueue.peek();
1939                 final long nextTriggerTimeElapsed = Math.max(earliestTriggerElapsed, alarm.second);
1940                 // Only schedule the alarm if one of the following is true:
1941                 // 1. There isn't one currently scheduled
1942                 // 2. The new alarm is significantly earlier than the previous alarm. If it's
1943                 // earlier but not significantly so, then we essentially delay the job a few extra
1944                 // minutes.
1945                 // 3. The alarm is after the current alarm.
1946                 if (mTriggerTimeElapsed == 0
1947                         || nextTriggerTimeElapsed < mTriggerTimeElapsed - 3 * MINUTE_IN_MILLIS
1948                         || mTriggerTimeElapsed < nextTriggerTimeElapsed) {
1949                     if (DEBUG) {
1950                         Slog.d(TAG, "Scheduling start alarm at " + nextTriggerTimeElapsed
1951                                 + " for app " + alarm.first);
1952                     }
1953                     mAlarmManager.set(AlarmManager.ELAPSED_REALTIME, nextTriggerTimeElapsed,
1954                             ALARM_TAG_QUOTA_CHECK, this, mHandler);
1955                     mTriggerTimeElapsed = nextTriggerTimeElapsed;
1956                 }
1957             } else {
1958                 mAlarmManager.cancel(this);
1959                 mTriggerTimeElapsed = 0;
1960             }
1961         }
1962 
1963         @Override
onAlarm()1964         public void onAlarm() {
1965             synchronized (mLock) {
1966                 while (mAlarmQueue.size() > 0) {
1967                     final Pair<Package, Long> alarm = mAlarmQueue.peek();
1968                     if (alarm.second <= sElapsedRealtimeClock.millis()) {
1969                         mHandler.obtainMessage(MSG_CHECK_PACKAGE, alarm.first.userId, 0,
1970                                 alarm.first.packageName).sendToTarget();
1971                         mAlarmQueue.remove(alarm);
1972                     } else {
1973                         break;
1974                     }
1975                 }
1976                 setNextAlarmLocked(sElapsedRealtimeClock.millis() + mMinQuotaCheckDelayMs);
1977             }
1978         }
1979 
1980         @GuardedBy("mLock")
dumpLocked(IndentingPrintWriter pw)1981         void dumpLocked(IndentingPrintWriter pw) {
1982             pw.println("In quota alarms:");
1983             pw.increaseIndent();
1984 
1985             if (mAlarmQueue.size() == 0) {
1986                 pw.println("NOT WAITING");
1987             } else {
1988                 Pair[] alarms = mAlarmQueue.toArray(new Pair[mAlarmQueue.size()]);
1989                 for (int i = 0; i < alarms.length; ++i) {
1990                     final Package pkg = (Package) alarms[i].first;
1991                     pw.print(pkg);
1992                     pw.print(": ");
1993                     pw.print(alarms[i].second);
1994                     pw.println();
1995                 }
1996             }
1997 
1998             pw.decreaseIndent();
1999         }
2000 
2001         @GuardedBy("mLock")
dumpLocked(ProtoOutputStream proto, long fieldId)2002         void dumpLocked(ProtoOutputStream proto, long fieldId) {
2003             final long token = proto.start(fieldId);
2004 
2005             proto.write(
2006                     StateControllerProto.QuotaController.InQuotaAlarmListener.TRIGGER_TIME_ELAPSED,
2007                     mTriggerTimeElapsed);
2008 
2009             Pair[] alarms = mAlarmQueue.toArray(new Pair[mAlarmQueue.size()]);
2010             for (int i = 0; i < alarms.length; ++i) {
2011                 final long aToken = proto.start(
2012                         StateControllerProto.QuotaController.InQuotaAlarmListener.ALARMS);
2013 
2014                 final Package pkg = (Package) alarms[i].first;
2015                 pkg.dumpDebug(proto,
2016                         StateControllerProto.QuotaController.InQuotaAlarmListener.Alarm.PKG);
2017                 proto.write(
2018                         StateControllerProto.QuotaController.InQuotaAlarmListener.Alarm.IN_QUOTA_TIME_ELAPSED,
2019                         (Long) alarms[i].second);
2020 
2021                 proto.end(aToken);
2022             }
2023 
2024             proto.end(token);
2025         }
2026     }
2027 
2028     @VisibleForTesting
2029     class QcConstants extends ContentObserver {
2030         private ContentResolver mResolver;
2031         private final KeyValueListParser mParser = new KeyValueListParser(',');
2032 
2033         private static final String KEY_ALLOWED_TIME_PER_PERIOD_MS = "allowed_time_per_period_ms";
2034         private static final String KEY_IN_QUOTA_BUFFER_MS = "in_quota_buffer_ms";
2035         private static final String KEY_WINDOW_SIZE_ACTIVE_MS = "window_size_active_ms";
2036         private static final String KEY_WINDOW_SIZE_WORKING_MS = "window_size_working_ms";
2037         private static final String KEY_WINDOW_SIZE_FREQUENT_MS = "window_size_frequent_ms";
2038         private static final String KEY_WINDOW_SIZE_RARE_MS = "window_size_rare_ms";
2039         private static final String KEY_WINDOW_SIZE_RESTRICTED_MS = "window_size_restricted_ms";
2040         private static final String KEY_MAX_EXECUTION_TIME_MS = "max_execution_time_ms";
2041         private static final String KEY_MAX_JOB_COUNT_ACTIVE = "max_job_count_active";
2042         private static final String KEY_MAX_JOB_COUNT_WORKING = "max_job_count_working";
2043         private static final String KEY_MAX_JOB_COUNT_FREQUENT = "max_job_count_frequent";
2044         private static final String KEY_MAX_JOB_COUNT_RARE = "max_job_count_rare";
2045         private static final String KEY_MAX_JOB_COUNT_RESTRICTED = "max_job_count_restricted";
2046         private static final String KEY_RATE_LIMITING_WINDOW_MS = "rate_limiting_window_ms";
2047         private static final String KEY_MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW =
2048                 "max_job_count_per_rate_limiting_window";
2049         private static final String KEY_MAX_SESSION_COUNT_ACTIVE = "max_session_count_active";
2050         private static final String KEY_MAX_SESSION_COUNT_WORKING = "max_session_count_working";
2051         private static final String KEY_MAX_SESSION_COUNT_FREQUENT = "max_session_count_frequent";
2052         private static final String KEY_MAX_SESSION_COUNT_RARE = "max_session_count_rare";
2053         private static final String KEY_MAX_SESSION_COUNT_RESTRICTED =
2054                 "max_session_count_restricted";
2055         private static final String KEY_MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW =
2056                 "max_session_count_per_rate_limiting_window";
2057         private static final String KEY_TIMING_SESSION_COALESCING_DURATION_MS =
2058                 "timing_session_coalescing_duration_ms";
2059         private static final String KEY_MIN_QUOTA_CHECK_DELAY_MS = "min_quota_check_delay_ms";
2060 
2061         private static final long DEFAULT_ALLOWED_TIME_PER_PERIOD_MS =
2062                 10 * 60 * 1000L; // 10 minutes
2063         private static final long DEFAULT_IN_QUOTA_BUFFER_MS =
2064                 30 * 1000L; // 30 seconds
2065         private static final long DEFAULT_WINDOW_SIZE_ACTIVE_MS =
2066                 DEFAULT_ALLOWED_TIME_PER_PERIOD_MS; // ACTIVE apps can run jobs at any time
2067         private static final long DEFAULT_WINDOW_SIZE_WORKING_MS =
2068                 2 * 60 * 60 * 1000L; // 2 hours
2069         private static final long DEFAULT_WINDOW_SIZE_FREQUENT_MS =
2070                 8 * 60 * 60 * 1000L; // 8 hours
2071         private static final long DEFAULT_WINDOW_SIZE_RARE_MS =
2072                 24 * 60 * 60 * 1000L; // 24 hours
2073         private static final long DEFAULT_WINDOW_SIZE_RESTRICTED_MS =
2074                 24 * 60 * 60 * 1000L; // 24 hours
2075         private static final long DEFAULT_MAX_EXECUTION_TIME_MS =
2076                 4 * HOUR_IN_MILLIS;
2077         private static final long DEFAULT_RATE_LIMITING_WINDOW_MS =
2078                 MINUTE_IN_MILLIS;
2079         private static final int DEFAULT_MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW = 20;
2080         private static final int DEFAULT_MAX_JOB_COUNT_ACTIVE =
2081                 75; // 75/window = 450/hr = 1/session
2082         private static final int DEFAULT_MAX_JOB_COUNT_WORKING = // 120/window = 60/hr = 12/session
2083                 (int) (60.0 * DEFAULT_WINDOW_SIZE_WORKING_MS / HOUR_IN_MILLIS);
2084         private static final int DEFAULT_MAX_JOB_COUNT_FREQUENT = // 200/window = 25/hr = 25/session
2085                 (int) (25.0 * DEFAULT_WINDOW_SIZE_FREQUENT_MS / HOUR_IN_MILLIS);
2086         private static final int DEFAULT_MAX_JOB_COUNT_RARE = // 48/window = 2/hr = 16/session
2087                 (int) (2.0 * DEFAULT_WINDOW_SIZE_RARE_MS / HOUR_IN_MILLIS);
2088         private static final int DEFAULT_MAX_JOB_COUNT_RESTRICTED = 10;
2089         private static final int DEFAULT_MAX_SESSION_COUNT_ACTIVE =
2090                 75; // 450/hr
2091         private static final int DEFAULT_MAX_SESSION_COUNT_WORKING =
2092                 10; // 5/hr
2093         private static final int DEFAULT_MAX_SESSION_COUNT_FREQUENT =
2094                 8; // 1/hr
2095         private static final int DEFAULT_MAX_SESSION_COUNT_RARE =
2096                 3; // .125/hr
2097         private static final int DEFAULT_MAX_SESSION_COUNT_RESTRICTED = 1; // 1/day
2098         private static final int DEFAULT_MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW = 20;
2099         private static final long DEFAULT_TIMING_SESSION_COALESCING_DURATION_MS = 5000; // 5 seconds
2100         private static final long DEFAULT_MIN_QUOTA_CHECK_DELAY_MS = MINUTE_IN_MILLIS;
2101 
2102         /** How much time each app will have to run jobs within their standby bucket window. */
2103         public long ALLOWED_TIME_PER_PERIOD_MS = DEFAULT_ALLOWED_TIME_PER_PERIOD_MS;
2104 
2105         /**
2106          * How much time the package should have before transitioning from out-of-quota to in-quota.
2107          * This should not affect processing if the package is already in-quota.
2108          */
2109         public long IN_QUOTA_BUFFER_MS = DEFAULT_IN_QUOTA_BUFFER_MS;
2110 
2111         /**
2112          * The quota window size of the particular standby bucket. Apps in this standby bucket are
2113          * expected to run only {@link #ALLOWED_TIME_PER_PERIOD_MS} within the past
2114          * WINDOW_SIZE_MS.
2115          */
2116         public long WINDOW_SIZE_ACTIVE_MS = DEFAULT_WINDOW_SIZE_ACTIVE_MS;
2117 
2118         /**
2119          * The quota window size of the particular standby bucket. Apps in this standby bucket are
2120          * expected to run only {@link #ALLOWED_TIME_PER_PERIOD_MS} within the past
2121          * WINDOW_SIZE_MS.
2122          */
2123         public long WINDOW_SIZE_WORKING_MS = DEFAULT_WINDOW_SIZE_WORKING_MS;
2124 
2125         /**
2126          * The quota window size of the particular standby bucket. Apps in this standby bucket are
2127          * expected to run only {@link #ALLOWED_TIME_PER_PERIOD_MS} within the past
2128          * WINDOW_SIZE_MS.
2129          */
2130         public long WINDOW_SIZE_FREQUENT_MS = DEFAULT_WINDOW_SIZE_FREQUENT_MS;
2131 
2132         /**
2133          * The quota window size of the particular standby bucket. Apps in this standby bucket are
2134          * expected to run only {@link #ALLOWED_TIME_PER_PERIOD_MS} within the past
2135          * WINDOW_SIZE_MS.
2136          */
2137         public long WINDOW_SIZE_RARE_MS = DEFAULT_WINDOW_SIZE_RARE_MS;
2138 
2139         /**
2140          * The quota window size of the particular standby bucket. Apps in this standby bucket are
2141          * expected to run only {@link #ALLOWED_TIME_PER_PERIOD_MS} within the past
2142          * WINDOW_SIZE_MS.
2143          */
2144         public long WINDOW_SIZE_RESTRICTED_MS = DEFAULT_WINDOW_SIZE_RESTRICTED_MS;
2145 
2146         /**
2147          * The maximum amount of time an app can have its jobs running within a 24 hour window.
2148          */
2149         public long MAX_EXECUTION_TIME_MS = DEFAULT_MAX_EXECUTION_TIME_MS;
2150 
2151         /**
2152          * The maximum number of jobs an app can run within this particular standby bucket's
2153          * window size.
2154          */
2155         public int MAX_JOB_COUNT_ACTIVE = DEFAULT_MAX_JOB_COUNT_ACTIVE;
2156 
2157         /**
2158          * The maximum number of jobs an app can run within this particular standby bucket's
2159          * window size.
2160          */
2161         public int MAX_JOB_COUNT_WORKING = DEFAULT_MAX_JOB_COUNT_WORKING;
2162 
2163         /**
2164          * The maximum number of jobs an app can run within this particular standby bucket's
2165          * window size.
2166          */
2167         public int MAX_JOB_COUNT_FREQUENT = DEFAULT_MAX_JOB_COUNT_FREQUENT;
2168 
2169         /**
2170          * The maximum number of jobs an app can run within this particular standby bucket's
2171          * window size.
2172          */
2173         public int MAX_JOB_COUNT_RARE = DEFAULT_MAX_JOB_COUNT_RARE;
2174 
2175         /**
2176          * The maximum number of jobs an app can run within this particular standby bucket's
2177          * window size.
2178          */
2179         public int MAX_JOB_COUNT_RESTRICTED = DEFAULT_MAX_JOB_COUNT_RESTRICTED;
2180 
2181         /** The period of time used to rate limit recently run jobs. */
2182         public long RATE_LIMITING_WINDOW_MS = DEFAULT_RATE_LIMITING_WINDOW_MS;
2183 
2184         /**
2185          * The maximum number of jobs that can run within the past {@link #RATE_LIMITING_WINDOW_MS}.
2186          */
2187         public int MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW =
2188                 DEFAULT_MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW;
2189 
2190         /**
2191          * The maximum number of {@link TimingSession}s an app can run within this particular
2192          * standby bucket's window size.
2193          */
2194         public int MAX_SESSION_COUNT_ACTIVE = DEFAULT_MAX_SESSION_COUNT_ACTIVE;
2195 
2196         /**
2197          * The maximum number of {@link TimingSession}s an app can run within this particular
2198          * standby bucket's window size.
2199          */
2200         public int MAX_SESSION_COUNT_WORKING = DEFAULT_MAX_SESSION_COUNT_WORKING;
2201 
2202         /**
2203          * The maximum number of {@link TimingSession}s an app can run within this particular
2204          * standby bucket's window size.
2205          */
2206         public int MAX_SESSION_COUNT_FREQUENT = DEFAULT_MAX_SESSION_COUNT_FREQUENT;
2207 
2208         /**
2209          * The maximum number of {@link TimingSession}s an app can run within this particular
2210          * standby bucket's window size.
2211          */
2212         public int MAX_SESSION_COUNT_RARE = DEFAULT_MAX_SESSION_COUNT_RARE;
2213 
2214         /**
2215          * The maximum number of {@link TimingSession}s an app can run within this particular
2216          * standby bucket's window size.
2217          */
2218         public int MAX_SESSION_COUNT_RESTRICTED = DEFAULT_MAX_SESSION_COUNT_RESTRICTED;
2219 
2220         /**
2221          * The maximum number of {@link TimingSession}s that can run within the past
2222          * {@link #ALLOWED_TIME_PER_PERIOD_MS}.
2223          */
2224         public int MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW =
2225                 DEFAULT_MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW;
2226 
2227         /**
2228          * Treat two distinct {@link TimingSession}s as the same if they start and end within this
2229          * amount of time of each other.
2230          */
2231         public long TIMING_SESSION_COALESCING_DURATION_MS =
2232                 DEFAULT_TIMING_SESSION_COALESCING_DURATION_MS;
2233 
2234         /** The minimum amount of time between quota check alarms. */
2235         public long MIN_QUOTA_CHECK_DELAY_MS = DEFAULT_MIN_QUOTA_CHECK_DELAY_MS;
2236 
2237         // Safeguards
2238 
2239         /** The minimum number of jobs that any bucket will be allowed to run within its window. */
2240         private static final int MIN_BUCKET_JOB_COUNT = 10;
2241 
2242         /**
2243          * The minimum number of {@link TimingSession}s that any bucket will be allowed to run
2244          * within its window.
2245          */
2246         private static final int MIN_BUCKET_SESSION_COUNT = 1;
2247 
2248         /** The minimum value that {@link #MAX_EXECUTION_TIME_MS} can have. */
2249         private static final long MIN_MAX_EXECUTION_TIME_MS = 60 * MINUTE_IN_MILLIS;
2250 
2251         /** The minimum value that {@link #MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW} can have. */
2252         private static final int MIN_MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW = 10;
2253 
2254         /** The minimum value that {@link #MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW} can have. */
2255         private static final int MIN_MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW = 10;
2256 
2257         /** The minimum value that {@link #RATE_LIMITING_WINDOW_MS} can have. */
2258         private static final long MIN_RATE_LIMITING_WINDOW_MS = 30 * SECOND_IN_MILLIS;
2259 
QcConstants(Handler handler)2260         QcConstants(Handler handler) {
2261             super(handler);
2262         }
2263 
start(ContentResolver resolver)2264         private void start(ContentResolver resolver) {
2265             mResolver = resolver;
2266             mResolver.registerContentObserver(Settings.Global.getUriFor(
2267                     Settings.Global.JOB_SCHEDULER_QUOTA_CONTROLLER_CONSTANTS), false, this);
2268             onChange(true, null);
2269         }
2270 
2271         @Override
onChange(boolean selfChange, Uri uri)2272         public void onChange(boolean selfChange, Uri uri) {
2273             final String constants = Settings.Global.getString(
2274                     mResolver, Settings.Global.JOB_SCHEDULER_QUOTA_CONTROLLER_CONSTANTS);
2275 
2276             try {
2277                 mParser.setString(constants);
2278             } catch (Exception e) {
2279                 // Failed to parse the settings string, log this and move on with defaults.
2280                 Slog.e(TAG, "Bad jobscheduler quota controller settings", e);
2281             }
2282 
2283             ALLOWED_TIME_PER_PERIOD_MS = mParser.getDurationMillis(
2284                     KEY_ALLOWED_TIME_PER_PERIOD_MS, DEFAULT_ALLOWED_TIME_PER_PERIOD_MS);
2285             IN_QUOTA_BUFFER_MS = mParser.getDurationMillis(
2286                     KEY_IN_QUOTA_BUFFER_MS, DEFAULT_IN_QUOTA_BUFFER_MS);
2287             WINDOW_SIZE_ACTIVE_MS = mParser.getDurationMillis(
2288                     KEY_WINDOW_SIZE_ACTIVE_MS, DEFAULT_WINDOW_SIZE_ACTIVE_MS);
2289             WINDOW_SIZE_WORKING_MS = mParser.getDurationMillis(
2290                     KEY_WINDOW_SIZE_WORKING_MS, DEFAULT_WINDOW_SIZE_WORKING_MS);
2291             WINDOW_SIZE_FREQUENT_MS = mParser.getDurationMillis(
2292                     KEY_WINDOW_SIZE_FREQUENT_MS, DEFAULT_WINDOW_SIZE_FREQUENT_MS);
2293             WINDOW_SIZE_RARE_MS = mParser.getDurationMillis(
2294                     KEY_WINDOW_SIZE_RARE_MS, DEFAULT_WINDOW_SIZE_RARE_MS);
2295             WINDOW_SIZE_RESTRICTED_MS = mParser.getDurationMillis(
2296                     KEY_WINDOW_SIZE_RESTRICTED_MS, DEFAULT_WINDOW_SIZE_RESTRICTED_MS);
2297             MAX_EXECUTION_TIME_MS = mParser.getDurationMillis(
2298                     KEY_MAX_EXECUTION_TIME_MS, DEFAULT_MAX_EXECUTION_TIME_MS);
2299             MAX_JOB_COUNT_ACTIVE = mParser.getInt(
2300                     KEY_MAX_JOB_COUNT_ACTIVE, DEFAULT_MAX_JOB_COUNT_ACTIVE);
2301             MAX_JOB_COUNT_WORKING = mParser.getInt(
2302                     KEY_MAX_JOB_COUNT_WORKING, DEFAULT_MAX_JOB_COUNT_WORKING);
2303             MAX_JOB_COUNT_FREQUENT = mParser.getInt(
2304                     KEY_MAX_JOB_COUNT_FREQUENT, DEFAULT_MAX_JOB_COUNT_FREQUENT);
2305             MAX_JOB_COUNT_RARE = mParser.getInt(
2306                     KEY_MAX_JOB_COUNT_RARE, DEFAULT_MAX_JOB_COUNT_RARE);
2307             MAX_JOB_COUNT_RESTRICTED = mParser.getInt(
2308                     KEY_MAX_JOB_COUNT_RESTRICTED, DEFAULT_MAX_JOB_COUNT_RESTRICTED);
2309             RATE_LIMITING_WINDOW_MS = mParser.getLong(
2310                     KEY_RATE_LIMITING_WINDOW_MS, DEFAULT_RATE_LIMITING_WINDOW_MS);
2311             MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW = mParser.getInt(
2312                     KEY_MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW,
2313                     DEFAULT_MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW);
2314             MAX_SESSION_COUNT_ACTIVE = mParser.getInt(
2315                     KEY_MAX_SESSION_COUNT_ACTIVE, DEFAULT_MAX_SESSION_COUNT_ACTIVE);
2316             MAX_SESSION_COUNT_WORKING = mParser.getInt(
2317                     KEY_MAX_SESSION_COUNT_WORKING, DEFAULT_MAX_SESSION_COUNT_WORKING);
2318             MAX_SESSION_COUNT_FREQUENT = mParser.getInt(
2319                     KEY_MAX_SESSION_COUNT_FREQUENT, DEFAULT_MAX_SESSION_COUNT_FREQUENT);
2320             MAX_SESSION_COUNT_RARE = mParser.getInt(
2321                     KEY_MAX_SESSION_COUNT_RARE, DEFAULT_MAX_SESSION_COUNT_RARE);
2322             MAX_SESSION_COUNT_RESTRICTED = mParser.getInt(
2323                     KEY_MAX_SESSION_COUNT_RESTRICTED, DEFAULT_MAX_SESSION_COUNT_RESTRICTED);
2324             MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW = mParser.getInt(
2325                     KEY_MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW,
2326                     DEFAULT_MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW);
2327             TIMING_SESSION_COALESCING_DURATION_MS = mParser.getLong(
2328                     KEY_TIMING_SESSION_COALESCING_DURATION_MS,
2329                     DEFAULT_TIMING_SESSION_COALESCING_DURATION_MS);
2330             MIN_QUOTA_CHECK_DELAY_MS = mParser.getDurationMillis(KEY_MIN_QUOTA_CHECK_DELAY_MS,
2331                     DEFAULT_MIN_QUOTA_CHECK_DELAY_MS);
2332 
2333             updateConstants();
2334         }
2335 
2336         @VisibleForTesting
updateConstants()2337         void updateConstants() {
2338             synchronized (mLock) {
2339                 boolean changed = false;
2340 
2341                 long newMaxExecutionTimeMs = Math.max(MIN_MAX_EXECUTION_TIME_MS,
2342                         Math.min(MAX_PERIOD_MS, MAX_EXECUTION_TIME_MS));
2343                 if (mMaxExecutionTimeMs != newMaxExecutionTimeMs) {
2344                     mMaxExecutionTimeMs = newMaxExecutionTimeMs;
2345                     mMaxExecutionTimeIntoQuotaMs = mMaxExecutionTimeMs - mQuotaBufferMs;
2346                     changed = true;
2347                 }
2348                 long newAllowedTimeMs = Math.min(mMaxExecutionTimeMs,
2349                         Math.max(MINUTE_IN_MILLIS, ALLOWED_TIME_PER_PERIOD_MS));
2350                 if (mAllowedTimePerPeriodMs != newAllowedTimeMs) {
2351                     mAllowedTimePerPeriodMs = newAllowedTimeMs;
2352                     mAllowedTimeIntoQuotaMs = mAllowedTimePerPeriodMs - mQuotaBufferMs;
2353                     changed = true;
2354                 }
2355                 // Make sure quota buffer is non-negative, not greater than allowed time per period,
2356                 // and no more than 5 minutes.
2357                 long newQuotaBufferMs = Math.max(0, Math.min(mAllowedTimePerPeriodMs,
2358                         Math.min(5 * MINUTE_IN_MILLIS, IN_QUOTA_BUFFER_MS)));
2359                 if (mQuotaBufferMs != newQuotaBufferMs) {
2360                     mQuotaBufferMs = newQuotaBufferMs;
2361                     mAllowedTimeIntoQuotaMs = mAllowedTimePerPeriodMs - mQuotaBufferMs;
2362                     mMaxExecutionTimeIntoQuotaMs = mMaxExecutionTimeMs - mQuotaBufferMs;
2363                     changed = true;
2364                 }
2365                 long newActivePeriodMs = Math.max(mAllowedTimePerPeriodMs,
2366                         Math.min(MAX_PERIOD_MS, WINDOW_SIZE_ACTIVE_MS));
2367                 if (mBucketPeriodsMs[ACTIVE_INDEX] != newActivePeriodMs) {
2368                     mBucketPeriodsMs[ACTIVE_INDEX] = newActivePeriodMs;
2369                     changed = true;
2370                 }
2371                 long newWorkingPeriodMs = Math.max(mAllowedTimePerPeriodMs,
2372                         Math.min(MAX_PERIOD_MS, WINDOW_SIZE_WORKING_MS));
2373                 if (mBucketPeriodsMs[WORKING_INDEX] != newWorkingPeriodMs) {
2374                     mBucketPeriodsMs[WORKING_INDEX] = newWorkingPeriodMs;
2375                     changed = true;
2376                 }
2377                 long newFrequentPeriodMs = Math.max(mAllowedTimePerPeriodMs,
2378                         Math.min(MAX_PERIOD_MS, WINDOW_SIZE_FREQUENT_MS));
2379                 if (mBucketPeriodsMs[FREQUENT_INDEX] != newFrequentPeriodMs) {
2380                     mBucketPeriodsMs[FREQUENT_INDEX] = newFrequentPeriodMs;
2381                     changed = true;
2382                 }
2383                 long newRarePeriodMs = Math.max(mAllowedTimePerPeriodMs,
2384                         Math.min(MAX_PERIOD_MS, WINDOW_SIZE_RARE_MS));
2385                 if (mBucketPeriodsMs[RARE_INDEX] != newRarePeriodMs) {
2386                     mBucketPeriodsMs[RARE_INDEX] = newRarePeriodMs;
2387                     changed = true;
2388                 }
2389                 // Fit in the range [allowed time (10 mins), 1 week].
2390                 long newRestrictedPeriodMs = Math.max(mAllowedTimePerPeriodMs,
2391                         Math.min(7 * 24 * 60 * MINUTE_IN_MILLIS, WINDOW_SIZE_RESTRICTED_MS));
2392                 if (mBucketPeriodsMs[RESTRICTED_INDEX] != newRestrictedPeriodMs) {
2393                     mBucketPeriodsMs[RESTRICTED_INDEX] = newRestrictedPeriodMs;
2394                     changed = true;
2395                 }
2396                 long newRateLimitingWindowMs = Math.min(MAX_PERIOD_MS,
2397                         Math.max(MIN_RATE_LIMITING_WINDOW_MS, RATE_LIMITING_WINDOW_MS));
2398                 if (mRateLimitingWindowMs != newRateLimitingWindowMs) {
2399                     mRateLimitingWindowMs = newRateLimitingWindowMs;
2400                     changed = true;
2401                 }
2402                 int newMaxJobCountPerRateLimitingWindow = Math.max(
2403                         MIN_MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW,
2404                         MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW);
2405                 if (mMaxJobCountPerRateLimitingWindow != newMaxJobCountPerRateLimitingWindow) {
2406                     mMaxJobCountPerRateLimitingWindow = newMaxJobCountPerRateLimitingWindow;
2407                     changed = true;
2408                 }
2409                 int newActiveMaxJobCount = Math.max(MIN_BUCKET_JOB_COUNT, MAX_JOB_COUNT_ACTIVE);
2410                 if (mMaxBucketJobCounts[ACTIVE_INDEX] != newActiveMaxJobCount) {
2411                     mMaxBucketJobCounts[ACTIVE_INDEX] = newActiveMaxJobCount;
2412                     changed = true;
2413                 }
2414                 int newWorkingMaxJobCount = Math.max(MIN_BUCKET_JOB_COUNT, MAX_JOB_COUNT_WORKING);
2415                 if (mMaxBucketJobCounts[WORKING_INDEX] != newWorkingMaxJobCount) {
2416                     mMaxBucketJobCounts[WORKING_INDEX] = newWorkingMaxJobCount;
2417                     changed = true;
2418                 }
2419                 int newFrequentMaxJobCount = Math.max(MIN_BUCKET_JOB_COUNT, MAX_JOB_COUNT_FREQUENT);
2420                 if (mMaxBucketJobCounts[FREQUENT_INDEX] != newFrequentMaxJobCount) {
2421                     mMaxBucketJobCounts[FREQUENT_INDEX] = newFrequentMaxJobCount;
2422                     changed = true;
2423                 }
2424                 int newRareMaxJobCount = Math.max(MIN_BUCKET_JOB_COUNT, MAX_JOB_COUNT_RARE);
2425                 if (mMaxBucketJobCounts[RARE_INDEX] != newRareMaxJobCount) {
2426                     mMaxBucketJobCounts[RARE_INDEX] = newRareMaxJobCount;
2427                     changed = true;
2428                 }
2429                 int newRestrictedMaxJobCount = Math.max(MIN_BUCKET_JOB_COUNT,
2430                         MAX_JOB_COUNT_RESTRICTED);
2431                 if (mMaxBucketJobCounts[RESTRICTED_INDEX] != newRestrictedMaxJobCount) {
2432                     mMaxBucketJobCounts[RESTRICTED_INDEX] = newRestrictedMaxJobCount;
2433                     changed = true;
2434                 }
2435                 int newMaxSessionCountPerRateLimitPeriod = Math.max(
2436                         MIN_MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW,
2437                         MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW);
2438                 if (mMaxSessionCountPerRateLimitingWindow != newMaxSessionCountPerRateLimitPeriod) {
2439                     mMaxSessionCountPerRateLimitingWindow = newMaxSessionCountPerRateLimitPeriod;
2440                     changed = true;
2441                 }
2442                 int newActiveMaxSessionCount =
2443                         Math.max(MIN_BUCKET_SESSION_COUNT, MAX_SESSION_COUNT_ACTIVE);
2444                 if (mMaxBucketSessionCounts[ACTIVE_INDEX] != newActiveMaxSessionCount) {
2445                     mMaxBucketSessionCounts[ACTIVE_INDEX] = newActiveMaxSessionCount;
2446                     changed = true;
2447                 }
2448                 int newWorkingMaxSessionCount =
2449                         Math.max(MIN_BUCKET_SESSION_COUNT, MAX_SESSION_COUNT_WORKING);
2450                 if (mMaxBucketSessionCounts[WORKING_INDEX] != newWorkingMaxSessionCount) {
2451                     mMaxBucketSessionCounts[WORKING_INDEX] = newWorkingMaxSessionCount;
2452                     changed = true;
2453                 }
2454                 int newFrequentMaxSessionCount =
2455                         Math.max(MIN_BUCKET_SESSION_COUNT, MAX_SESSION_COUNT_FREQUENT);
2456                 if (mMaxBucketSessionCounts[FREQUENT_INDEX] != newFrequentMaxSessionCount) {
2457                     mMaxBucketSessionCounts[FREQUENT_INDEX] = newFrequentMaxSessionCount;
2458                     changed = true;
2459                 }
2460                 int newRareMaxSessionCount =
2461                         Math.max(MIN_BUCKET_SESSION_COUNT, MAX_SESSION_COUNT_RARE);
2462                 if (mMaxBucketSessionCounts[RARE_INDEX] != newRareMaxSessionCount) {
2463                     mMaxBucketSessionCounts[RARE_INDEX] = newRareMaxSessionCount;
2464                     changed = true;
2465                 }
2466                 int newRestrictedMaxSessionCount = Math.max(0, MAX_SESSION_COUNT_RESTRICTED);
2467                 if (mMaxBucketSessionCounts[RESTRICTED_INDEX] != newRestrictedMaxSessionCount) {
2468                     mMaxBucketSessionCounts[RESTRICTED_INDEX] = newRestrictedMaxSessionCount;
2469                     changed = true;
2470                 }
2471                 long newSessionCoalescingDurationMs = Math.min(15 * MINUTE_IN_MILLIS,
2472                         Math.max(0, TIMING_SESSION_COALESCING_DURATION_MS));
2473                 if (mTimingSessionCoalescingDurationMs != newSessionCoalescingDurationMs) {
2474                     mTimingSessionCoalescingDurationMs = newSessionCoalescingDurationMs;
2475                     changed = true;
2476                 }
2477                 // Don't set changed to true for this one since we don't need to re-evaluate
2478                 // execution stats or constraint status. Limit the delay to the range [0, 15]
2479                 // minutes.
2480                 mInQuotaAlarmListener.setMinQuotaCheckDelayMs(
2481                         Math.min(15 * MINUTE_IN_MILLIS, Math.max(0, MIN_QUOTA_CHECK_DELAY_MS)));
2482 
2483                 if (changed) {
2484                     // Update job bookkeeping out of band.
2485                     BackgroundThread.getHandler().post(() -> {
2486                         synchronized (mLock) {
2487                             invalidateAllExecutionStatsLocked();
2488                             maybeUpdateAllConstraintsLocked();
2489                         }
2490                     });
2491                 }
2492             }
2493         }
2494 
dump(IndentingPrintWriter pw)2495         private void dump(IndentingPrintWriter pw) {
2496             pw.println();
2497             pw.println("QuotaController:");
2498             pw.increaseIndent();
2499             pw.printPair(KEY_ALLOWED_TIME_PER_PERIOD_MS, ALLOWED_TIME_PER_PERIOD_MS).println();
2500             pw.printPair(KEY_IN_QUOTA_BUFFER_MS, IN_QUOTA_BUFFER_MS).println();
2501             pw.printPair(KEY_WINDOW_SIZE_ACTIVE_MS, WINDOW_SIZE_ACTIVE_MS).println();
2502             pw.printPair(KEY_WINDOW_SIZE_WORKING_MS, WINDOW_SIZE_WORKING_MS).println();
2503             pw.printPair(KEY_WINDOW_SIZE_FREQUENT_MS, WINDOW_SIZE_FREQUENT_MS).println();
2504             pw.printPair(KEY_WINDOW_SIZE_RARE_MS, WINDOW_SIZE_RARE_MS).println();
2505             pw.printPair(KEY_WINDOW_SIZE_RESTRICTED_MS, WINDOW_SIZE_RESTRICTED_MS).println();
2506             pw.printPair(KEY_MAX_EXECUTION_TIME_MS, MAX_EXECUTION_TIME_MS).println();
2507             pw.printPair(KEY_MAX_JOB_COUNT_ACTIVE, MAX_JOB_COUNT_ACTIVE).println();
2508             pw.printPair(KEY_MAX_JOB_COUNT_WORKING, MAX_JOB_COUNT_WORKING).println();
2509             pw.printPair(KEY_MAX_JOB_COUNT_FREQUENT, MAX_JOB_COUNT_FREQUENT).println();
2510             pw.printPair(KEY_MAX_JOB_COUNT_RARE, MAX_JOB_COUNT_RARE).println();
2511             pw.printPair(KEY_MAX_JOB_COUNT_RESTRICTED, MAX_JOB_COUNT_RESTRICTED).println();
2512             pw.printPair(KEY_RATE_LIMITING_WINDOW_MS, RATE_LIMITING_WINDOW_MS).println();
2513             pw.printPair(KEY_MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW,
2514                     MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW).println();
2515             pw.printPair(KEY_MAX_SESSION_COUNT_ACTIVE, MAX_SESSION_COUNT_ACTIVE).println();
2516             pw.printPair(KEY_MAX_SESSION_COUNT_WORKING, MAX_SESSION_COUNT_WORKING).println();
2517             pw.printPair(KEY_MAX_SESSION_COUNT_FREQUENT, MAX_SESSION_COUNT_FREQUENT).println();
2518             pw.printPair(KEY_MAX_SESSION_COUNT_RARE, MAX_SESSION_COUNT_RARE).println();
2519             pw.printPair(KEY_MAX_SESSION_COUNT_RESTRICTED, MAX_SESSION_COUNT_RESTRICTED).println();
2520             pw.printPair(KEY_MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW,
2521                     MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW).println();
2522             pw.printPair(KEY_TIMING_SESSION_COALESCING_DURATION_MS,
2523                     TIMING_SESSION_COALESCING_DURATION_MS).println();
2524             pw.printPair(KEY_MIN_QUOTA_CHECK_DELAY_MS, MIN_QUOTA_CHECK_DELAY_MS).println();
2525             pw.decreaseIndent();
2526         }
2527 
dump(ProtoOutputStream proto)2528         private void dump(ProtoOutputStream proto) {
2529             final long qcToken = proto.start(ConstantsProto.QUOTA_CONTROLLER);
2530             proto.write(ConstantsProto.QuotaController.ALLOWED_TIME_PER_PERIOD_MS,
2531                     ALLOWED_TIME_PER_PERIOD_MS);
2532             proto.write(ConstantsProto.QuotaController.IN_QUOTA_BUFFER_MS, IN_QUOTA_BUFFER_MS);
2533             proto.write(ConstantsProto.QuotaController.ACTIVE_WINDOW_SIZE_MS,
2534                     WINDOW_SIZE_ACTIVE_MS);
2535             proto.write(ConstantsProto.QuotaController.WORKING_WINDOW_SIZE_MS,
2536                     WINDOW_SIZE_WORKING_MS);
2537             proto.write(ConstantsProto.QuotaController.FREQUENT_WINDOW_SIZE_MS,
2538                     WINDOW_SIZE_FREQUENT_MS);
2539             proto.write(ConstantsProto.QuotaController.RARE_WINDOW_SIZE_MS, WINDOW_SIZE_RARE_MS);
2540             proto.write(ConstantsProto.QuotaController.RESTRICTED_WINDOW_SIZE_MS,
2541                     WINDOW_SIZE_RESTRICTED_MS);
2542             proto.write(ConstantsProto.QuotaController.MAX_EXECUTION_TIME_MS,
2543                     MAX_EXECUTION_TIME_MS);
2544             proto.write(ConstantsProto.QuotaController.MAX_JOB_COUNT_ACTIVE, MAX_JOB_COUNT_ACTIVE);
2545             proto.write(ConstantsProto.QuotaController.MAX_JOB_COUNT_WORKING,
2546                     MAX_JOB_COUNT_WORKING);
2547             proto.write(ConstantsProto.QuotaController.MAX_JOB_COUNT_FREQUENT,
2548                     MAX_JOB_COUNT_FREQUENT);
2549             proto.write(ConstantsProto.QuotaController.MAX_JOB_COUNT_RARE, MAX_JOB_COUNT_RARE);
2550             proto.write(ConstantsProto.QuotaController.MAX_JOB_COUNT_RESTRICTED,
2551                     MAX_JOB_COUNT_RESTRICTED);
2552             proto.write(ConstantsProto.QuotaController.RATE_LIMITING_WINDOW_MS,
2553                     RATE_LIMITING_WINDOW_MS);
2554             proto.write(ConstantsProto.QuotaController.MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW,
2555                     MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW);
2556             proto.write(ConstantsProto.QuotaController.MAX_SESSION_COUNT_ACTIVE,
2557                     MAX_SESSION_COUNT_ACTIVE);
2558             proto.write(ConstantsProto.QuotaController.MAX_SESSION_COUNT_WORKING,
2559                     MAX_SESSION_COUNT_WORKING);
2560             proto.write(ConstantsProto.QuotaController.MAX_SESSION_COUNT_FREQUENT,
2561                     MAX_SESSION_COUNT_FREQUENT);
2562             proto.write(ConstantsProto.QuotaController.MAX_SESSION_COUNT_RARE,
2563                     MAX_SESSION_COUNT_RARE);
2564             proto.write(ConstantsProto.QuotaController.MAX_SESSION_COUNT_RESTRICTED,
2565                     MAX_SESSION_COUNT_RESTRICTED);
2566             proto.write(ConstantsProto.QuotaController.MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW,
2567                     MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW);
2568             proto.write(ConstantsProto.QuotaController.TIMING_SESSION_COALESCING_DURATION_MS,
2569                     TIMING_SESSION_COALESCING_DURATION_MS);
2570             proto.write(ConstantsProto.QuotaController.MIN_QUOTA_CHECK_DELAY_MS,
2571                     MIN_QUOTA_CHECK_DELAY_MS);
2572             proto.end(qcToken);
2573         }
2574     }
2575 
2576     //////////////////////// TESTING HELPERS /////////////////////////////
2577 
2578     @VisibleForTesting
getAllowedTimePerPeriodMs()2579     long getAllowedTimePerPeriodMs() {
2580         return mAllowedTimePerPeriodMs;
2581     }
2582 
2583     @VisibleForTesting
2584     @NonNull
getBucketMaxJobCounts()2585     int[] getBucketMaxJobCounts() {
2586         return mMaxBucketJobCounts;
2587     }
2588 
2589     @VisibleForTesting
2590     @NonNull
getBucketMaxSessionCounts()2591     int[] getBucketMaxSessionCounts() {
2592         return mMaxBucketSessionCounts;
2593     }
2594 
2595     @VisibleForTesting
2596     @NonNull
getBucketWindowSizes()2597     long[] getBucketWindowSizes() {
2598         return mBucketPeriodsMs;
2599     }
2600 
2601     @VisibleForTesting
2602     @NonNull
getForegroundUids()2603     SparseBooleanArray getForegroundUids() {
2604         return mForegroundUids;
2605     }
2606 
2607     @VisibleForTesting
2608     @NonNull
getHandler()2609     Handler getHandler() {
2610         return mHandler;
2611     }
2612 
2613     @VisibleForTesting
getInQuotaBufferMs()2614     long getInQuotaBufferMs() {
2615         return mQuotaBufferMs;
2616     }
2617 
2618     @VisibleForTesting
getMaxExecutionTimeMs()2619     long getMaxExecutionTimeMs() {
2620         return mMaxExecutionTimeMs;
2621     }
2622 
2623     @VisibleForTesting
getMaxJobCountPerRateLimitingWindow()2624     int getMaxJobCountPerRateLimitingWindow() {
2625         return mMaxJobCountPerRateLimitingWindow;
2626     }
2627 
2628     @VisibleForTesting
getMaxSessionCountPerRateLimitingWindow()2629     int getMaxSessionCountPerRateLimitingWindow() {
2630         return mMaxSessionCountPerRateLimitingWindow;
2631     }
2632 
2633     @VisibleForTesting
getRateLimitingWindowMs()2634     long getRateLimitingWindowMs() {
2635         return mRateLimitingWindowMs;
2636     }
2637 
2638     @VisibleForTesting
getTimingSessionCoalescingDurationMs()2639     long getTimingSessionCoalescingDurationMs() {
2640         return mTimingSessionCoalescingDurationMs;
2641     }
2642 
2643     @VisibleForTesting
2644     @Nullable
getTimingSessions(int userId, String packageName)2645     List<TimingSession> getTimingSessions(int userId, String packageName) {
2646         return mTimingSessions.get(userId, packageName);
2647     }
2648 
2649     @VisibleForTesting
2650     @NonNull
getQcConstants()2651     QcConstants getQcConstants() {
2652         return mQcConstants;
2653     }
2654 
2655     //////////////////////////// DATA DUMP //////////////////////////////
2656 
2657     @Override
dumpControllerStateLocked(final IndentingPrintWriter pw, final Predicate<JobStatus> predicate)2658     public void dumpControllerStateLocked(final IndentingPrintWriter pw,
2659             final Predicate<JobStatus> predicate) {
2660         pw.println("Is charging: " + mChargeTracker.isCharging());
2661         pw.println("Current elapsed time: " + sElapsedRealtimeClock.millis());
2662         pw.println();
2663 
2664         pw.print("Foreground UIDs: ");
2665         pw.println(mForegroundUids.toString());
2666         pw.println();
2667 
2668         pw.println("Cached UID->package map:");
2669         pw.increaseIndent();
2670         for (int i = 0; i < mUidToPackageCache.size(); ++i) {
2671             final int uid = mUidToPackageCache.keyAt(i);
2672             pw.print(uid);
2673             pw.print(": ");
2674             pw.println(mUidToPackageCache.get(uid));
2675         }
2676         pw.decreaseIndent();
2677         pw.println();
2678 
2679         mTrackedJobs.forEach((jobs) -> {
2680             for (int j = 0; j < jobs.size(); j++) {
2681                 final JobStatus js = jobs.valueAt(j);
2682                 if (!predicate.test(js)) {
2683                     continue;
2684                 }
2685                 pw.print("#");
2686                 js.printUniqueId(pw);
2687                 pw.print(" from ");
2688                 UserHandle.formatUid(pw, js.getSourceUid());
2689                 if (mTopStartedJobs.contains(js)) {
2690                     pw.print(" (TOP)");
2691                 }
2692                 pw.println();
2693 
2694                 pw.increaseIndent();
2695                 pw.print(JobStatus.bucketName(js.getEffectiveStandbyBucket()));
2696                 pw.print(", ");
2697                 if (js.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA)) {
2698                     pw.print("within quota");
2699                 } else {
2700                     pw.print("not within quota");
2701                 }
2702                 pw.print(", ");
2703                 pw.print(getRemainingExecutionTimeLocked(js));
2704                 pw.print("ms remaining in quota");
2705                 pw.decreaseIndent();
2706                 pw.println();
2707             }
2708         });
2709 
2710         pw.println();
2711         for (int u = 0; u < mPkgTimers.numMaps(); ++u) {
2712             final int userId = mPkgTimers.keyAt(u);
2713             for (int p = 0; p < mPkgTimers.numElementsForKey(userId); ++p) {
2714                 final String pkgName = mPkgTimers.keyAt(u, p);
2715                 mPkgTimers.valueAt(u, p).dump(pw, predicate);
2716                 pw.println();
2717                 List<TimingSession> sessions = mTimingSessions.get(userId, pkgName);
2718                 if (sessions != null) {
2719                     pw.increaseIndent();
2720                     pw.println("Saved sessions:");
2721                     pw.increaseIndent();
2722                     for (int j = sessions.size() - 1; j >= 0; j--) {
2723                         TimingSession session = sessions.get(j);
2724                         session.dump(pw);
2725                     }
2726                     pw.decreaseIndent();
2727                     pw.decreaseIndent();
2728                     pw.println();
2729                 }
2730             }
2731         }
2732 
2733         pw.println("Cached execution stats:");
2734         pw.increaseIndent();
2735         for (int u = 0; u < mExecutionStatsCache.numMaps(); ++u) {
2736             final int userId = mExecutionStatsCache.keyAt(u);
2737             for (int p = 0; p < mExecutionStatsCache.numElementsForKey(userId); ++p) {
2738                 final String pkgName = mExecutionStatsCache.keyAt(u, p);
2739                 ExecutionStats[] stats = mExecutionStatsCache.valueAt(u, p);
2740 
2741                 pw.println(string(userId, pkgName));
2742                 pw.increaseIndent();
2743                 for (int i = 0; i < stats.length; ++i) {
2744                     ExecutionStats executionStats = stats[i];
2745                     if (executionStats != null) {
2746                         pw.print(JobStatus.bucketName(i));
2747                         pw.print(": ");
2748                         pw.println(executionStats);
2749                     }
2750                 }
2751                 pw.decreaseIndent();
2752             }
2753         }
2754         pw.decreaseIndent();
2755 
2756         pw.println();
2757         mInQuotaAlarmListener.dumpLocked(pw);
2758         pw.decreaseIndent();
2759     }
2760 
2761     @Override
dumpControllerStateLocked(ProtoOutputStream proto, long fieldId, Predicate<JobStatus> predicate)2762     public void dumpControllerStateLocked(ProtoOutputStream proto, long fieldId,
2763             Predicate<JobStatus> predicate) {
2764         final long token = proto.start(fieldId);
2765         final long mToken = proto.start(StateControllerProto.QUOTA);
2766 
2767         proto.write(StateControllerProto.QuotaController.IS_CHARGING, mChargeTracker.isCharging());
2768         proto.write(StateControllerProto.QuotaController.ELAPSED_REALTIME,
2769                 sElapsedRealtimeClock.millis());
2770 
2771         for (int i = 0; i < mForegroundUids.size(); ++i) {
2772             proto.write(StateControllerProto.QuotaController.FOREGROUND_UIDS,
2773                     mForegroundUids.keyAt(i));
2774         }
2775 
2776         for (int i = 0; i < mUidToPackageCache.size(); ++i) {
2777             final long upToken = proto.start(
2778                     StateControllerProto.QuotaController.UID_TO_PACKAGE_CACHE);
2779 
2780             final int uid = mUidToPackageCache.keyAt(i);
2781             ArraySet<String> packages = mUidToPackageCache.get(uid);
2782 
2783             proto.write(StateControllerProto.QuotaController.UidPackageMapping.UID, uid);
2784             for (int j = 0; j < packages.size(); ++j) {
2785                 proto.write(StateControllerProto.QuotaController.UidPackageMapping.PACKAGE_NAMES,
2786                         packages.valueAt(j));
2787             }
2788 
2789             proto.end(upToken);
2790         }
2791 
2792         mTrackedJobs.forEach((jobs) -> {
2793             for (int j = 0; j < jobs.size(); j++) {
2794                 final JobStatus js = jobs.valueAt(j);
2795                 if (!predicate.test(js)) {
2796                     continue;
2797                 }
2798                 final long jsToken = proto.start(StateControllerProto.QuotaController.TRACKED_JOBS);
2799                 js.writeToShortProto(proto, StateControllerProto.QuotaController.TrackedJob.INFO);
2800                 proto.write(StateControllerProto.QuotaController.TrackedJob.SOURCE_UID,
2801                         js.getSourceUid());
2802                 proto.write(
2803                         StateControllerProto.QuotaController.TrackedJob.EFFECTIVE_STANDBY_BUCKET,
2804                         js.getEffectiveStandbyBucket());
2805                 proto.write(StateControllerProto.QuotaController.TrackedJob.IS_TOP_STARTED_JOB,
2806                         mTopStartedJobs.contains(js));
2807                 proto.write(StateControllerProto.QuotaController.TrackedJob.HAS_QUOTA,
2808                         js.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA));
2809                 proto.write(StateControllerProto.QuotaController.TrackedJob.REMAINING_QUOTA_MS,
2810                         getRemainingExecutionTimeLocked(js));
2811                 proto.end(jsToken);
2812             }
2813         });
2814 
2815         for (int u = 0; u < mPkgTimers.numMaps(); ++u) {
2816             final int userId = mPkgTimers.keyAt(u);
2817             for (int p = 0; p < mPkgTimers.numElementsForKey(userId); ++p) {
2818                 final String pkgName = mPkgTimers.keyAt(u, p);
2819                 final long psToken = proto.start(
2820                         StateControllerProto.QuotaController.PACKAGE_STATS);
2821                 mPkgTimers.valueAt(u, p).dump(proto,
2822                         StateControllerProto.QuotaController.PackageStats.TIMER, predicate);
2823 
2824                 List<TimingSession> sessions = mTimingSessions.get(userId, pkgName);
2825                 if (sessions != null) {
2826                     for (int j = sessions.size() - 1; j >= 0; j--) {
2827                         TimingSession session = sessions.get(j);
2828                         session.dump(proto,
2829                                 StateControllerProto.QuotaController.PackageStats.SAVED_SESSIONS);
2830                     }
2831                 }
2832 
2833                 ExecutionStats[] stats = mExecutionStatsCache.get(userId, pkgName);
2834                 if (stats != null) {
2835                     for (int bucketIndex = 0; bucketIndex < stats.length; ++bucketIndex) {
2836                         ExecutionStats es = stats[bucketIndex];
2837                         if (es == null) {
2838                             continue;
2839                         }
2840                         final long esToken = proto.start(
2841                                 StateControllerProto.QuotaController.PackageStats.EXECUTION_STATS);
2842                         proto.write(
2843                                 StateControllerProto.QuotaController.ExecutionStats.STANDBY_BUCKET,
2844                                 bucketIndex);
2845                         proto.write(
2846                                 StateControllerProto.QuotaController.ExecutionStats.EXPIRATION_TIME_ELAPSED,
2847                                 es.expirationTimeElapsed);
2848                         proto.write(
2849                                 StateControllerProto.QuotaController.ExecutionStats.WINDOW_SIZE_MS,
2850                                 es.windowSizeMs);
2851                         proto.write(
2852                                 StateControllerProto.QuotaController.ExecutionStats.JOB_COUNT_LIMIT,
2853                                 es.jobCountLimit);
2854                         proto.write(
2855                                 StateControllerProto.QuotaController.ExecutionStats.SESSION_COUNT_LIMIT,
2856                                 es.sessionCountLimit);
2857                         proto.write(
2858                                 StateControllerProto.QuotaController.ExecutionStats.EXECUTION_TIME_IN_WINDOW_MS,
2859                                 es.executionTimeInWindowMs);
2860                         proto.write(
2861                                 StateControllerProto.QuotaController.ExecutionStats.BG_JOB_COUNT_IN_WINDOW,
2862                                 es.bgJobCountInWindow);
2863                         proto.write(
2864                                 StateControllerProto.QuotaController.ExecutionStats.EXECUTION_TIME_IN_MAX_PERIOD_MS,
2865                                 es.executionTimeInMaxPeriodMs);
2866                         proto.write(
2867                                 StateControllerProto.QuotaController.ExecutionStats.BG_JOB_COUNT_IN_MAX_PERIOD,
2868                                 es.bgJobCountInMaxPeriod);
2869                         proto.write(
2870                                 StateControllerProto.QuotaController.ExecutionStats.SESSION_COUNT_IN_WINDOW,
2871                                 es.sessionCountInWindow);
2872                         proto.write(
2873                                 StateControllerProto.QuotaController.ExecutionStats.IN_QUOTA_TIME_ELAPSED,
2874                                 es.inQuotaTimeElapsed);
2875                         proto.write(
2876                                 StateControllerProto.QuotaController.ExecutionStats.JOB_COUNT_EXPIRATION_TIME_ELAPSED,
2877                                 es.jobRateLimitExpirationTimeElapsed);
2878                         proto.write(
2879                                 StateControllerProto.QuotaController.ExecutionStats.JOB_COUNT_IN_RATE_LIMITING_WINDOW,
2880                                 es.jobCountInRateLimitingWindow);
2881                         proto.write(
2882                                 StateControllerProto.QuotaController.ExecutionStats.SESSION_COUNT_EXPIRATION_TIME_ELAPSED,
2883                                 es.sessionRateLimitExpirationTimeElapsed);
2884                         proto.write(
2885                                 StateControllerProto.QuotaController.ExecutionStats.SESSION_COUNT_IN_RATE_LIMITING_WINDOW,
2886                                 es.sessionCountInRateLimitingWindow);
2887                         proto.end(esToken);
2888                     }
2889                 }
2890 
2891                 proto.end(psToken);
2892             }
2893         }
2894 
2895         mInQuotaAlarmListener.dumpLocked(proto,
2896                 StateControllerProto.QuotaController.IN_QUOTA_ALARM_LISTENER);
2897 
2898         proto.end(mToken);
2899         proto.end(token);
2900     }
2901 
2902     @Override
dumpConstants(IndentingPrintWriter pw)2903     public void dumpConstants(IndentingPrintWriter pw) {
2904         mQcConstants.dump(pw);
2905     }
2906 
2907     @Override
dumpConstants(ProtoOutputStream proto)2908     public void dumpConstants(ProtoOutputStream proto) {
2909         mQcConstants.dump(proto);
2910     }
2911 }
2912