1 /*
2  * Copyright (C) 2021 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.app.job.JobInfo.getPriorityString;
20 
21 import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock;
22 
23 import android.annotation.NonNull;
24 import android.app.job.JobInfo;
25 import android.util.ArrayMap;
26 import android.util.ArraySet;
27 import android.util.IndentingPrintWriter;
28 import android.util.Log;
29 import android.util.Slog;
30 import android.util.SparseArrayMap;
31 
32 import com.android.internal.annotations.GuardedBy;
33 import com.android.server.AppSchedulingModuleThread;
34 import com.android.server.LocalServices;
35 import com.android.server.job.JobSchedulerService;
36 import com.android.server.tare.EconomicPolicy;
37 import com.android.server.tare.EconomyManagerInternal;
38 import com.android.server.tare.EconomyManagerInternal.ActionBill;
39 import com.android.server.tare.JobSchedulerEconomicPolicy;
40 
41 import java.util.List;
42 import java.util.function.Predicate;
43 
44 /**
45  * Controller that interfaces with Tare ({@link EconomyManagerInternal} and manages each job's
46  * ability to run per TARE policies.
47  *
48  * @see JobSchedulerEconomicPolicy
49  */
50 public class TareController extends StateController {
51     private static final String TAG = "JobScheduler.TARE";
52     private static final boolean DEBUG = JobSchedulerService.DEBUG
53             || Log.isLoggable(TAG, Log.DEBUG);
54 
55     /**
56      * Bill to use while we're waiting to start a min priority job. If a job isn't running yet,
57      * don't consider it eligible to run unless it can pay for a job start and at least some
58      * period of execution time. We don't want min priority jobs to use up all available credits,
59      * so we make sure to only run them while there are enough credits to run higher priority jobs.
60      */
61     private static final ActionBill BILL_JOB_START_MIN =
62             new ActionBill(List.of(
63                     new EconomyManagerInternal.AnticipatedAction(
64                             JobSchedulerEconomicPolicy.ACTION_JOB_DEFAULT_START, 1, 0),
65                     new EconomyManagerInternal.AnticipatedAction(
66                             JobSchedulerEconomicPolicy.ACTION_JOB_DEFAULT_RUNNING, 0, 120_000L)
67             ));
68 
69     /**
70      * Bill to use when a min priority job is currently running. We don't want min priority jobs
71      * to use up remaining credits, so we make sure to only run them while there are enough
72      * credits to run higher priority jobs. We stop the job when the app's credits get too low.
73      */
74     private static final ActionBill BILL_JOB_RUNNING_MIN =
75             new ActionBill(List.of(
76                     new EconomyManagerInternal.AnticipatedAction(
77                             JobSchedulerEconomicPolicy.ACTION_JOB_DEFAULT_RUNNING, 0, 60_000L)
78             ));
79 
80     /**
81      * Bill to use while we're waiting to start a low priority job. If a job isn't running yet,
82      * don't consider it eligible to run unless it can pay for a job start and at least some
83      * period of execution time. We don't want low priority jobs to use up all available credits,
84      * so we make sure to only run them while there are enough credits to run higher priority jobs.
85      */
86     private static final ActionBill BILL_JOB_START_LOW =
87             new ActionBill(List.of(
88                     new EconomyManagerInternal.AnticipatedAction(
89                             JobSchedulerEconomicPolicy.ACTION_JOB_DEFAULT_START, 1, 0),
90                     new EconomyManagerInternal.AnticipatedAction(
91                             JobSchedulerEconomicPolicy.ACTION_JOB_DEFAULT_RUNNING, 0, 60_000L)
92             ));
93 
94     /**
95      * Bill to use when a low priority job is currently running. We don't want low priority jobs
96      * to use up all available credits, so we make sure to only run them while there are enough
97      * credits to run higher priority jobs. We stop the job when the app's credits get too low.
98      */
99     private static final ActionBill BILL_JOB_RUNNING_LOW =
100             new ActionBill(List.of(
101                     new EconomyManagerInternal.AnticipatedAction(
102                             JobSchedulerEconomicPolicy.ACTION_JOB_DEFAULT_RUNNING, 0, 30_000L)
103             ));
104 
105     /**
106      * Bill to use while we're waiting to start a job. If a job isn't running yet, don't consider it
107      * eligible to run unless it can pay for a job start and at least some period of execution time.
108      */
109     private static final ActionBill BILL_JOB_START_DEFAULT =
110             new ActionBill(List.of(
111                     new EconomyManagerInternal.AnticipatedAction(
112                             JobSchedulerEconomicPolicy.ACTION_JOB_DEFAULT_START, 1, 0),
113                     new EconomyManagerInternal.AnticipatedAction(
114                             JobSchedulerEconomicPolicy.ACTION_JOB_DEFAULT_RUNNING, 0, 30_000L)
115             ));
116 
117     /**
118      * Bill to use when a default priority job is currently running. We want to track and make
119      * sure the app can continue to pay for 1 more second of execution time. We stop the job when
120      * the app can no longer pay for that time.
121      */
122     private static final ActionBill BILL_JOB_RUNNING_DEFAULT =
123             new ActionBill(List.of(
124                     new EconomyManagerInternal.AnticipatedAction(
125                             JobSchedulerEconomicPolicy.ACTION_JOB_DEFAULT_RUNNING, 0, 1_000L)
126             ));
127 
128     /**
129      * Bill to use while we're waiting to start a job. If a job isn't running yet, don't consider it
130      * eligible to run unless it can pay for a job start and at least some period of execution time.
131      */
132     private static final ActionBill BILL_JOB_START_HIGH =
133             new ActionBill(List.of(
134                     new EconomyManagerInternal.AnticipatedAction(
135                             JobSchedulerEconomicPolicy.ACTION_JOB_HIGH_START, 1, 0),
136                     new EconomyManagerInternal.AnticipatedAction(
137                             JobSchedulerEconomicPolicy.ACTION_JOB_HIGH_RUNNING, 0, 30_000L)
138             ));
139 
140     /**
141      * Bill to use when a high priority job is currently running. We want to track and make sure
142      * the app can continue to pay for 1 more second of execution time. We stop the job when the
143      * app can no longer pay for that time.
144      */
145     private static final ActionBill BILL_JOB_RUNNING_HIGH =
146             new ActionBill(List.of(
147                     new EconomyManagerInternal.AnticipatedAction(
148                             JobSchedulerEconomicPolicy.ACTION_JOB_HIGH_RUNNING, 0, 1_000L)
149             ));
150 
151 
152     /**
153      * Bill to use while we're waiting to start a max priority job. This should only be used for
154      * requested-EJs that aren't allowed to run as EJs. If a job isn't running yet, don't consider
155      * it eligible to run unless it can pay for a job start and at least some period of execution
156      * time.
157      */
158     private static final ActionBill BILL_JOB_START_MAX =
159             new ActionBill(List.of(
160                     new EconomyManagerInternal.AnticipatedAction(
161                             JobSchedulerEconomicPolicy.ACTION_JOB_MAX_START, 1, 0),
162                     new EconomyManagerInternal.AnticipatedAction(
163                             JobSchedulerEconomicPolicy.ACTION_JOB_MAX_RUNNING, 0, 30_000L)
164             ));
165 
166     /**
167      * Bill to use when a max priority job is currently running. This should only be used for
168      * requested-EJs that aren't allowed to run as EJs. We want to track and make sure
169      * the app can continue to pay for 1 more second of execution time. We stop the job when the
170      * app can no longer pay for that time.
171      */
172     private static final ActionBill BILL_JOB_RUNNING_MAX =
173             new ActionBill(List.of(
174                     new EconomyManagerInternal.AnticipatedAction(
175                             JobSchedulerEconomicPolicy.ACTION_JOB_MAX_RUNNING, 0, 1_000L)
176             ));
177 
178     /**
179      * Bill to use while we're waiting to start a job. If a job isn't running yet, don't consider it
180      * eligible to run unless it can pay for a job start and at least some period of execution time.
181      */
182     private static final ActionBill BILL_JOB_START_MAX_EXPEDITED =
183             new ActionBill(List.of(
184                     new EconomyManagerInternal.AnticipatedAction(
185                             JobSchedulerEconomicPolicy.ACTION_JOB_MAX_START, 1, 0),
186                     new EconomyManagerInternal.AnticipatedAction(
187                             JobSchedulerEconomicPolicy.ACTION_JOB_MAX_RUNNING, 0, 30_000L)
188             ));
189 
190     /**
191      * Bill to use when a max priority EJ is currently running (as an EJ). We want to track and
192      * make sure the app can continue to pay for 1 more second of execution time. We stop the job
193      * when the app can no longer pay for that time.
194      */
195     private static final ActionBill BILL_JOB_RUNNING_MAX_EXPEDITED =
196             new ActionBill(List.of(
197                     new EconomyManagerInternal.AnticipatedAction(
198                             JobSchedulerEconomicPolicy.ACTION_JOB_MAX_RUNNING, 0, 1_000L)
199             ));
200 
201     /**
202      * Bill to use while we're waiting to start a job. If a job isn't running yet, don't consider it
203      * eligible to run unless it can pay for a job start and at least some period of execution time.
204      */
205     private static final ActionBill BILL_JOB_START_HIGH_EXPEDITED =
206             new ActionBill(List.of(
207                     new EconomyManagerInternal.AnticipatedAction(
208                             JobSchedulerEconomicPolicy.ACTION_JOB_HIGH_START, 1, 0),
209                     new EconomyManagerInternal.AnticipatedAction(
210                             JobSchedulerEconomicPolicy.ACTION_JOB_HIGH_RUNNING, 0, 30_000L)
211             ));
212 
213     /**
214      * Bill to use when a high priority EJ is currently running (as an EJ). We want to track and
215      * make sure the app can continue to pay for 1 more second of execution time. We stop the job
216      * when the app can no longer pay for that time.
217      */
218     private static final ActionBill BILL_JOB_RUNNING_HIGH_EXPEDITED =
219             new ActionBill(List.of(
220                     new EconomyManagerInternal.AnticipatedAction(
221                             JobSchedulerEconomicPolicy.ACTION_JOB_HIGH_RUNNING, 0, 1_000L)
222             ));
223 
224     private final EconomyManagerInternal mEconomyManagerInternal;
225 
226     private final BackgroundJobsController mBackgroundJobsController;
227     private final ConnectivityController mConnectivityController;
228 
229     /**
230      * Local cache of the ability of each userId-pkg to afford the various bills we're tracking for
231      * them.
232      */
233     @GuardedBy("mLock")
234     private final SparseArrayMap<String, ArrayMap<ActionBill, Boolean>> mAffordabilityCache =
235             new SparseArrayMap<>();
236 
237     /**
238      * List of all tracked jobs. Out SparseArrayMap is userId-sourcePkg. The inner mapping is the
239      * anticipated actions and all the jobs that are applicable to them.
240      */
241     @GuardedBy("mLock")
242     private final SparseArrayMap<String, ArrayMap<ActionBill, ArraySet<JobStatus>>>
243             mRegisteredBillsAndJobs = new SparseArrayMap<>();
244 
245     private final EconomyManagerInternal.AffordabilityChangeListener mAffordabilityChangeListener =
246             (userId, pkgName, bill, canAfford) -> {
247                 final long nowElapsed = sElapsedRealtimeClock.millis();
248                 if (DEBUG) {
249                     Slog.d(TAG,
250                             userId + ":" + pkgName + " affordability for " + getBillName(bill)
251                                     + " changed to " + canAfford);
252                 }
253                 synchronized (mLock) {
254                     ArrayMap<ActionBill, Boolean> actionAffordability =
255                             mAffordabilityCache.get(userId, pkgName);
256                     if (actionAffordability == null) {
257                         actionAffordability = new ArrayMap<>();
258                         mAffordabilityCache.add(userId, pkgName, actionAffordability);
259                     }
260                     actionAffordability.put(bill, canAfford);
261 
262                     final ArrayMap<ActionBill, ArraySet<JobStatus>> billToJobMap =
263                             mRegisteredBillsAndJobs.get(userId, pkgName);
264                     if (billToJobMap != null) {
265                         final ArraySet<JobStatus> jobs = billToJobMap.get(bill);
266                         if (jobs != null) {
267                             final ArraySet<JobStatus> changedJobs = new ArraySet<>();
268                             for (int i = 0; i < jobs.size(); ++i) {
269                                 final JobStatus job = jobs.valueAt(i);
270                                 // Use hasEnoughWealth if canAfford is false in case the job has
271                                 // other bills it can depend on (eg. EJs being demoted to
272                                 // regular jobs).
273                                 if (job.setTareWealthConstraintSatisfied(nowElapsed,
274                                         canAfford || hasEnoughWealthLocked(job))) {
275                                     changedJobs.add(job);
276                                 }
277                                 if (job.isRequestedExpeditedJob()
278                                         && setExpeditedTareApproved(job, nowElapsed,
279                                         canAffordExpeditedBillLocked(job))) {
280                                     changedJobs.add(job);
281                                 }
282                             }
283                             if (changedJobs.size() > 0) {
284                                 mStateChangedListener.onControllerStateChanged(changedJobs);
285                             }
286                         }
287                     }
288                 }
289             };
290 
291     /**
292      * List of jobs that started while the UID was in the TOP state. There will usually be no more
293      * than {@value JobConcurrencyManager#MAX_STANDARD_JOB_CONCURRENCY} running at once, so an
294      * ArraySet is fine.
295      */
296     @GuardedBy("mLock")
297     private final ArraySet<JobStatus> mTopStartedJobs = new ArraySet<>();
298 
299     @GuardedBy("mLock")
300     private boolean mIsEnabled;
301 
TareController(JobSchedulerService service, @NonNull BackgroundJobsController backgroundJobsController, @NonNull ConnectivityController connectivityController)302     public TareController(JobSchedulerService service,
303             @NonNull BackgroundJobsController backgroundJobsController,
304             @NonNull ConnectivityController connectivityController) {
305         super(service);
306         mBackgroundJobsController = backgroundJobsController;
307         mConnectivityController = connectivityController;
308         mEconomyManagerInternal = LocalServices.getService(EconomyManagerInternal.class);
309         mIsEnabled = mConstants.USE_TARE_POLICY;
310     }
311 
312     @Override
313     @GuardedBy("mLock")
maybeStartTrackingJobLocked(JobStatus jobStatus, JobStatus lastJob)314     public void maybeStartTrackingJobLocked(JobStatus jobStatus, JobStatus lastJob) {
315         final long nowElapsed = sElapsedRealtimeClock.millis();
316         if (jobStatus.shouldTreatAsUserInitiatedJob()) {
317             // User-initiated jobs should always be allowed to run.
318             jobStatus.setTareWealthConstraintSatisfied(nowElapsed, true);
319             return;
320         }
321         jobStatus.setTareWealthConstraintSatisfied(nowElapsed, hasEnoughWealthLocked(jobStatus));
322         setExpeditedTareApproved(jobStatus, nowElapsed,
323                 jobStatus.isRequestedExpeditedJob() && canAffordExpeditedBillLocked(jobStatus));
324 
325         final ArraySet<ActionBill> bills = getPossibleStartBills(jobStatus);
326         for (int i = 0; i < bills.size(); ++i) {
327             addJobToBillList(jobStatus, bills.valueAt(i));
328         }
329     }
330 
331     @Override
332     @GuardedBy("mLock")
prepareForExecutionLocked(JobStatus jobStatus)333     public void prepareForExecutionLocked(JobStatus jobStatus) {
334         if (jobStatus.shouldTreatAsUserInitiatedJob()) {
335             // TODO(202954395): consider noting execution with the EconomyManager even though it
336             //                  won't affect this job
337             return;
338         }
339         final int userId = jobStatus.getSourceUserId();
340         final String pkgName = jobStatus.getSourcePackageName();
341         ArrayMap<ActionBill, ArraySet<JobStatus>> billToJobMap =
342                 mRegisteredBillsAndJobs.get(userId, pkgName);
343         if (billToJobMap == null) {
344             Slog.e(TAG, "Job is being prepared but doesn't have a pre-existing billToJobMap");
345         } else {
346             for (int i = 0; i < billToJobMap.size(); ++i) {
347                 removeJobFromBillList(jobStatus, billToJobMap.keyAt(i));
348             }
349         }
350 
351         final int uid = jobStatus.getSourceUid();
352         if (mService.getUidBias(uid) == JobInfo.BIAS_TOP_APP) {
353             if (DEBUG) {
354                 Slog.d(TAG, jobStatus.toShortString() + " is top started job");
355             }
356             mTopStartedJobs.add(jobStatus);
357             // Top jobs won't count towards quota so there's no need to involve the EconomyManager.
358         } else {
359             addJobToBillList(jobStatus, getRunningBill(jobStatus));
360             mEconomyManagerInternal.noteOngoingEventStarted(userId, pkgName,
361                     getRunningActionId(jobStatus), String.valueOf(jobStatus.getJobId()));
362         }
363     }
364 
365     @Override
366     @GuardedBy("mLock")
unprepareFromExecutionLocked(JobStatus jobStatus)367     public void unprepareFromExecutionLocked(JobStatus jobStatus) {
368         if (jobStatus.shouldTreatAsUserInitiatedJob()) {
369             return;
370         }
371         final int userId = jobStatus.getSourceUserId();
372         final String pkgName = jobStatus.getSourcePackageName();
373         // If this method is called, then jobStatus.madeActive was never updated, so don't use it
374         // to determine if the EconomyManager was notified.
375         if (!mTopStartedJobs.remove(jobStatus)) {
376             // If the job was started while the app was top, then the EconomyManager wasn't notified
377             // of the job start.
378             mEconomyManagerInternal.noteOngoingEventStopped(userId, pkgName,
379                     getRunningActionId(jobStatus), String.valueOf(jobStatus.getJobId()));
380         }
381 
382         final ArraySet<ActionBill> bills = getPossibleStartBills(jobStatus);
383         ArrayMap<ActionBill, ArraySet<JobStatus>> billToJobMap =
384                 mRegisteredBillsAndJobs.get(userId, pkgName);
385         if (billToJobMap == null) {
386             Slog.e(TAG, "Job was just unprepared but didn't have a pre-existing billToJobMap");
387         } else {
388             for (int i = 0; i < billToJobMap.size(); ++i) {
389                 removeJobFromBillList(jobStatus, billToJobMap.keyAt(i));
390             }
391         }
392         for (int i = 0; i < bills.size(); ++i) {
393             addJobToBillList(jobStatus, bills.valueAt(i));
394         }
395     }
396 
397     @Override
398     @GuardedBy("mLock")
maybeStopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob)399     public void maybeStopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob) {
400         if (jobStatus.shouldTreatAsUserInitiatedJob()) {
401             return;
402         }
403         final int userId = jobStatus.getSourceUserId();
404         final String pkgName = jobStatus.getSourcePackageName();
405         if (!mTopStartedJobs.remove(jobStatus) && jobStatus.madeActive > 0) {
406             // Only note the job stop if we previously told the EconomyManager that the job started.
407             // If the job was started while the app was top, then the EconomyManager wasn't notified
408             // of the job start.
409             mEconomyManagerInternal.noteOngoingEventStopped(userId, pkgName,
410                     getRunningActionId(jobStatus), String.valueOf(jobStatus.getJobId()));
411         }
412         ArrayMap<ActionBill, ArraySet<JobStatus>> billToJobMap =
413                 mRegisteredBillsAndJobs.get(userId, pkgName);
414         if (billToJobMap != null) {
415             for (int i = 0; i < billToJobMap.size(); ++i) {
416                 removeJobFromBillList(jobStatus, billToJobMap.keyAt(i));
417             }
418         }
419     }
420 
421     @Override
422     @GuardedBy("mLock")
onConstantsUpdatedLocked()423     public void onConstantsUpdatedLocked() {
424         if (mIsEnabled != mConstants.USE_TARE_POLICY) {
425             mIsEnabled = mConstants.USE_TARE_POLICY;
426             // Update job bookkeeping out of band.
427             AppSchedulingModuleThread.getHandler().post(() -> {
428                 synchronized (mLock) {
429                     final long nowElapsed = sElapsedRealtimeClock.millis();
430                     mService.getJobStore().forEachJob((jobStatus) -> {
431                         if (!mIsEnabled) {
432                             jobStatus.setTareWealthConstraintSatisfied(nowElapsed, true);
433                             setExpeditedTareApproved(jobStatus, nowElapsed, true);
434                         } else {
435                             jobStatus.setTareWealthConstraintSatisfied(
436                                     nowElapsed, hasEnoughWealthLocked(jobStatus));
437                             setExpeditedTareApproved(jobStatus, nowElapsed,
438                                     jobStatus.isRequestedExpeditedJob()
439                                             && canAffordExpeditedBillLocked(jobStatus));
440                         }
441                     });
442                 }
443             });
444         }
445     }
446 
447     @GuardedBy("mLock")
canScheduleEJ(@onNull JobStatus jobStatus)448     public boolean canScheduleEJ(@NonNull JobStatus jobStatus) {
449         if (!mIsEnabled) {
450             return true;
451         }
452         if (jobStatus.getEffectivePriority() == JobInfo.PRIORITY_MAX) {
453             return canAffordBillLocked(jobStatus, BILL_JOB_START_MAX_EXPEDITED);
454         }
455         return canAffordBillLocked(jobStatus, BILL_JOB_START_HIGH_EXPEDITED);
456     }
457 
458     /** @return true if the job was started while the app was in the TOP state. */
459     @GuardedBy("mLock")
isTopStartedJobLocked(@onNull final JobStatus jobStatus)460     private boolean isTopStartedJobLocked(@NonNull final JobStatus jobStatus) {
461         return mTopStartedJobs.contains(jobStatus);
462     }
463 
464     @GuardedBy("mLock")
getMaxJobExecutionTimeMsLocked(@onNull JobStatus jobStatus)465     public long getMaxJobExecutionTimeMsLocked(@NonNull JobStatus jobStatus) {
466         if (!mIsEnabled) {
467             return mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS;
468         }
469         return mEconomyManagerInternal.getMaxDurationMs(
470                 jobStatus.getSourceUserId(), jobStatus.getSourcePackageName(),
471                 getRunningBill(jobStatus));
472     }
473 
474     @GuardedBy("mLock")
addJobToBillList(@onNull JobStatus jobStatus, @NonNull ActionBill bill)475     private void addJobToBillList(@NonNull JobStatus jobStatus, @NonNull ActionBill bill) {
476         final int userId = jobStatus.getSourceUserId();
477         final String pkgName = jobStatus.getSourcePackageName();
478         ArrayMap<ActionBill, ArraySet<JobStatus>> billToJobMap =
479                 mRegisteredBillsAndJobs.get(userId, pkgName);
480         if (billToJobMap == null) {
481             billToJobMap = new ArrayMap<>();
482             mRegisteredBillsAndJobs.add(userId, pkgName, billToJobMap);
483         }
484         ArraySet<JobStatus> jobs = billToJobMap.get(bill);
485         if (jobs == null) {
486             jobs = new ArraySet<>();
487             billToJobMap.put(bill, jobs);
488         }
489         if (jobs.add(jobStatus)) {
490             mEconomyManagerInternal.registerAffordabilityChangeListener(userId, pkgName,
491                     mAffordabilityChangeListener, bill);
492         }
493     }
494 
495     @GuardedBy("mLock")
removeJobFromBillList(@onNull JobStatus jobStatus, @NonNull ActionBill bill)496     private void removeJobFromBillList(@NonNull JobStatus jobStatus, @NonNull ActionBill bill) {
497         final int userId = jobStatus.getSourceUserId();
498         final String pkgName = jobStatus.getSourcePackageName();
499         final ArrayMap<ActionBill, ArraySet<JobStatus>> billToJobMap =
500                 mRegisteredBillsAndJobs.get(userId, pkgName);
501         if (billToJobMap != null) {
502             final ArraySet<JobStatus> jobs = billToJobMap.get(bill);
503             if (jobs == null || (jobs.remove(jobStatus) && jobs.size() == 0)) {
504                 mEconomyManagerInternal.unregisterAffordabilityChangeListener(
505                         userId, pkgName, mAffordabilityChangeListener, bill);
506                 // Remove the cached value so we don't accidentally use it when the app
507                 // schedules a new job.
508                 final ArrayMap<ActionBill, Boolean> actionAffordability =
509                         mAffordabilityCache.get(userId, pkgName);
510                 if (actionAffordability != null) {
511                     actionAffordability.remove(bill);
512                 }
513             }
514         }
515     }
516 
517     @NonNull
getPossibleStartBills(JobStatus jobStatus)518     private ArraySet<ActionBill> getPossibleStartBills(JobStatus jobStatus) {
519         // TODO: factor in network cost when available
520         final ArraySet<ActionBill> bills = new ArraySet<>();
521         if (jobStatus.isRequestedExpeditedJob()) {
522             if (jobStatus.getEffectivePriority() == JobInfo.PRIORITY_MAX) {
523                 bills.add(BILL_JOB_START_MAX_EXPEDITED);
524             } else {
525                 bills.add(BILL_JOB_START_HIGH_EXPEDITED);
526             }
527         }
528         switch (jobStatus.getEffectivePriority()) {
529             case JobInfo.PRIORITY_MAX:
530                 bills.add(BILL_JOB_START_MAX);
531                 break;
532             case JobInfo.PRIORITY_HIGH:
533                 bills.add(BILL_JOB_START_HIGH);
534                 break;
535             case JobInfo.PRIORITY_DEFAULT:
536                 bills.add(BILL_JOB_START_DEFAULT);
537                 break;
538             case JobInfo.PRIORITY_LOW:
539                 bills.add(BILL_JOB_START_LOW);
540                 break;
541             case JobInfo.PRIORITY_MIN:
542                 bills.add(BILL_JOB_START_MIN);
543                 break;
544             default:
545                 Slog.wtf(TAG, "Unexpected priority: "
546                         + JobInfo.getPriorityString(jobStatus.getEffectivePriority()));
547                 break;
548         }
549         return bills;
550     }
551 
552     @NonNull
getRunningBill(JobStatus jobStatus)553     private ActionBill getRunningBill(JobStatus jobStatus) {
554         // TODO: factor in network cost when available
555         if (jobStatus.shouldTreatAsExpeditedJob() || jobStatus.startedAsExpeditedJob) {
556             if (jobStatus.getEffectivePriority() == JobInfo.PRIORITY_MAX) {
557                 return BILL_JOB_RUNNING_MAX_EXPEDITED;
558             } else {
559                 return BILL_JOB_RUNNING_HIGH_EXPEDITED;
560             }
561         }
562         switch (jobStatus.getEffectivePriority()) {
563             case JobInfo.PRIORITY_MAX:
564                 return BILL_JOB_RUNNING_MAX;
565             case JobInfo.PRIORITY_HIGH:
566                 return BILL_JOB_RUNNING_HIGH;
567             case JobInfo.PRIORITY_LOW:
568                 return BILL_JOB_RUNNING_LOW;
569             case JobInfo.PRIORITY_MIN:
570                 return BILL_JOB_RUNNING_MIN;
571             default:
572                 Slog.wtf(TAG, "Got unexpected priority: " + jobStatus.getEffectivePriority());
573                 // Intentional fallthrough
574             case JobInfo.PRIORITY_DEFAULT:
575                 return BILL_JOB_RUNNING_DEFAULT;
576         }
577     }
578 
579     @EconomicPolicy.AppAction
getRunningActionId(@onNull JobStatus job)580     private static int getRunningActionId(@NonNull JobStatus job) {
581         switch (job.getEffectivePriority()) {
582             case JobInfo.PRIORITY_MAX:
583                 return JobSchedulerEconomicPolicy.ACTION_JOB_MAX_RUNNING;
584             case JobInfo.PRIORITY_HIGH:
585                 return JobSchedulerEconomicPolicy.ACTION_JOB_HIGH_RUNNING;
586             case JobInfo.PRIORITY_LOW:
587                 return JobSchedulerEconomicPolicy.ACTION_JOB_LOW_RUNNING;
588             case JobInfo.PRIORITY_MIN:
589                 return JobSchedulerEconomicPolicy.ACTION_JOB_MIN_RUNNING;
590             default:
591                 Slog.wtf(TAG, "Unknown priority: " + getPriorityString(job.getEffectivePriority()));
592                 // Intentional fallthrough
593             case JobInfo.PRIORITY_DEFAULT:
594                 return JobSchedulerEconomicPolicy.ACTION_JOB_DEFAULT_RUNNING;
595         }
596     }
597 
598     @GuardedBy("mLock")
canAffordBillLocked(@onNull JobStatus jobStatus, @NonNull ActionBill bill)599     private boolean canAffordBillLocked(@NonNull JobStatus jobStatus, @NonNull ActionBill bill) {
600         if (!mIsEnabled) {
601             return true;
602         }
603         if (mService.getUidBias(jobStatus.getSourceUid()) == JobInfo.BIAS_TOP_APP
604                 || isTopStartedJobLocked(jobStatus)) {
605             // Jobs for the top app should always be allowed to run, and any jobs started while
606             // the app is on top shouldn't consume any credits.
607             return true;
608         }
609         final int userId = jobStatus.getSourceUserId();
610         final String pkgName = jobStatus.getSourcePackageName();
611         ArrayMap<ActionBill, Boolean> actionAffordability =
612                 mAffordabilityCache.get(userId, pkgName);
613         if (actionAffordability == null) {
614             actionAffordability = new ArrayMap<>();
615             mAffordabilityCache.add(userId, pkgName, actionAffordability);
616         }
617 
618         if (actionAffordability.containsKey(bill)) {
619             return actionAffordability.get(bill);
620         }
621 
622         final boolean canAfford = mEconomyManagerInternal.canPayFor(userId, pkgName, bill);
623         actionAffordability.put(bill, canAfford);
624         return canAfford;
625     }
626 
627     @GuardedBy("mLock")
canAffordExpeditedBillLocked(@onNull JobStatus jobStatus)628     private boolean canAffordExpeditedBillLocked(@NonNull JobStatus jobStatus) {
629         if (!mIsEnabled) {
630             return true;
631         }
632         if (!jobStatus.isRequestedExpeditedJob()) {
633             return false;
634         }
635         if (mService.getUidBias(jobStatus.getSourceUid()) == JobInfo.BIAS_TOP_APP
636                 || isTopStartedJobLocked(jobStatus)) {
637             // Jobs for the top app should always be allowed to run, and any jobs started while
638             // the app is on top shouldn't consume any credits.
639             return true;
640         }
641         if (mService.isCurrentlyRunningLocked(jobStatus)) {
642             return canAffordBillLocked(jobStatus, getRunningBill(jobStatus));
643         }
644 
645         if (jobStatus.getEffectivePriority() == JobInfo.PRIORITY_MAX) {
646             return canAffordBillLocked(jobStatus, BILL_JOB_START_MAX_EXPEDITED);
647         }
648         return canAffordBillLocked(jobStatus, BILL_JOB_START_HIGH_EXPEDITED);
649     }
650 
651     @GuardedBy("mLock")
hasEnoughWealthLocked(@onNull JobStatus jobStatus)652     private boolean hasEnoughWealthLocked(@NonNull JobStatus jobStatus) {
653         if (!mIsEnabled) {
654             return true;
655         }
656         if (jobStatus.shouldTreatAsUserInitiatedJob()) {
657             // Always allow user-initiated jobs.
658             return true;
659         }
660         if (mService.getUidBias(jobStatus.getSourceUid()) == JobInfo.BIAS_TOP_APP
661                 || isTopStartedJobLocked(jobStatus)) {
662             // Jobs for the top app should always be allowed to run, and any jobs started while
663             // the app is on top shouldn't consume any credits.
664             return true;
665         }
666         if (mService.isCurrentlyRunningLocked(jobStatus)) {
667             return canAffordBillLocked(jobStatus, getRunningBill(jobStatus));
668         }
669 
670         final ArraySet<ActionBill> bills = getPossibleStartBills(jobStatus);
671         for (int i = 0; i < bills.size(); ++i) {
672             ActionBill bill = bills.valueAt(i);
673             if (canAffordBillLocked(jobStatus, bill)) {
674                 return true;
675             }
676         }
677         return false;
678     }
679 
680     /**
681      * If the satisfaction changes, this will tell connectivity & background jobs controller to
682      * also re-evaluate their state.
683      */
setExpeditedTareApproved(@onNull JobStatus jobStatus, long nowElapsed, boolean isApproved)684     private boolean setExpeditedTareApproved(@NonNull JobStatus jobStatus, long nowElapsed,
685             boolean isApproved) {
686         if (jobStatus.setExpeditedJobTareApproved(nowElapsed, isApproved)) {
687             mBackgroundJobsController.evaluateStateLocked(jobStatus);
688             mConnectivityController.evaluateStateLocked(jobStatus);
689             if (isApproved && jobStatus.isReady()) {
690                 mStateChangedListener.onRunJobNow(jobStatus);
691             }
692             return true;
693         }
694         return false;
695     }
696 
697     @NonNull
getBillName(@onNull ActionBill bill)698     private String getBillName(@NonNull ActionBill bill) {
699         if (bill.equals(BILL_JOB_START_MAX_EXPEDITED)) {
700             return "EJ_MAX_START_BILL";
701         }
702         if (bill.equals(BILL_JOB_RUNNING_MAX_EXPEDITED)) {
703             return "EJ_MAX_RUNNING_BILL";
704         }
705         if (bill.equals(BILL_JOB_START_HIGH_EXPEDITED)) {
706             return "EJ_HIGH_START_BILL";
707         }
708         if (bill.equals(BILL_JOB_RUNNING_HIGH_EXPEDITED)) {
709             return "EJ_HIGH_RUNNING_BILL";
710         }
711         if (bill.equals(BILL_JOB_START_HIGH)) {
712             return "HIGH_START_BILL";
713         }
714         if (bill.equals(BILL_JOB_RUNNING_HIGH)) {
715             return "HIGH_RUNNING_BILL";
716         }
717         if (bill.equals(BILL_JOB_START_DEFAULT)) {
718             return "DEFAULT_START_BILL";
719         }
720         if (bill.equals(BILL_JOB_RUNNING_DEFAULT)) {
721             return "DEFAULT_RUNNING_BILL";
722         }
723         if (bill.equals(BILL_JOB_START_LOW)) {
724             return "LOW_START_BILL";
725         }
726         if (bill.equals(BILL_JOB_RUNNING_LOW)) {
727             return "LOW_RUNNING_BILL";
728         }
729         if (bill.equals(BILL_JOB_START_MIN)) {
730             return "MIN_START_BILL";
731         }
732         if (bill.equals(BILL_JOB_RUNNING_MIN)) {
733             return "MIN_RUNNING_BILL";
734         }
735         return "UNKNOWN_BILL (" + bill + ")";
736     }
737 
738     @Override
dumpControllerStateLocked(IndentingPrintWriter pw, Predicate<JobStatus> predicate)739     public void dumpControllerStateLocked(IndentingPrintWriter pw, Predicate<JobStatus> predicate) {
740         pw.print("Is enabled: ");
741         pw.println(mIsEnabled);
742 
743         pw.println("Affordability cache:");
744         pw.increaseIndent();
745         mAffordabilityCache.forEach((userId, pkgName, billMap) -> {
746             final int numBills = billMap.size();
747             if (numBills > 0) {
748                 pw.print(userId);
749                 pw.print(":");
750                 pw.print(pkgName);
751                 pw.println(":");
752 
753                 pw.increaseIndent();
754                 for (int i = 0; i < numBills; ++i) {
755                     pw.print(getBillName(billMap.keyAt(i)));
756                     pw.print(": ");
757                     pw.println(billMap.valueAt(i));
758                 }
759                 pw.decreaseIndent();
760             }
761         });
762         pw.decreaseIndent();
763     }
764 }
765