blob: 7408088b8efcc1f8624090cc9498d0727c877be2 [file] [log] [blame]
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server.job.controllers;
import static android.app.job.JobInfo.getPriorityString;
import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock;
import android.annotation.NonNull;
import android.app.job.JobInfo;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.IndentingPrintWriter;
import android.util.Log;
import android.util.Slog;
import android.util.SparseArrayMap;
import com.android.internal.annotations.GuardedBy;
import com.android.server.AppSchedulingModuleThread;
import com.android.server.LocalServices;
import com.android.server.job.JobSchedulerService;
import com.android.server.tare.EconomicPolicy;
import com.android.server.tare.EconomyManagerInternal;
import com.android.server.tare.EconomyManagerInternal.ActionBill;
import com.android.server.tare.JobSchedulerEconomicPolicy;
import java.util.List;
import java.util.function.Predicate;
/**
* Controller that interfaces with Tare ({@link EconomyManagerInternal} and manages each job's
* ability to run per TARE policies.
*
* @see JobSchedulerEconomicPolicy
*/
public class TareController extends StateController {
private static final String TAG = "JobScheduler.TARE";
private static final boolean DEBUG = JobSchedulerService.DEBUG
|| Log.isLoggable(TAG, Log.DEBUG);
/**
* Bill to use while we're waiting to start a min priority job. If a job isn't running yet,
* don't consider it eligible to run unless it can pay for a job start and at least some
* period of execution time. We don't want min priority jobs to use up all available credits,
* so we make sure to only run them while there are enough credits to run higher priority jobs.
*/
private static final ActionBill BILL_JOB_START_MIN =
new ActionBill(List.of(
new EconomyManagerInternal.AnticipatedAction(
JobSchedulerEconomicPolicy.ACTION_JOB_DEFAULT_START, 1, 0),
new EconomyManagerInternal.AnticipatedAction(
JobSchedulerEconomicPolicy.ACTION_JOB_DEFAULT_RUNNING, 0, 120_000L)
));
/**
* Bill to use when a min priority job is currently running. We don't want min priority jobs
* to use up remaining credits, so we make sure to only run them while there are enough
* credits to run higher priority jobs. We stop the job when the app's credits get too low.
*/
private static final ActionBill BILL_JOB_RUNNING_MIN =
new ActionBill(List.of(
new EconomyManagerInternal.AnticipatedAction(
JobSchedulerEconomicPolicy.ACTION_JOB_DEFAULT_RUNNING, 0, 60_000L)
));
/**
* Bill to use while we're waiting to start a low priority job. If a job isn't running yet,
* don't consider it eligible to run unless it can pay for a job start and at least some
* period of execution time. We don't want low priority jobs to use up all available credits,
* so we make sure to only run them while there are enough credits to run higher priority jobs.
*/
private static final ActionBill BILL_JOB_START_LOW =
new ActionBill(List.of(
new EconomyManagerInternal.AnticipatedAction(
JobSchedulerEconomicPolicy.ACTION_JOB_DEFAULT_START, 1, 0),
new EconomyManagerInternal.AnticipatedAction(
JobSchedulerEconomicPolicy.ACTION_JOB_DEFAULT_RUNNING, 0, 60_000L)
));
/**
* Bill to use when a low priority job is currently running. We don't want low priority jobs
* to use up all available credits, so we make sure to only run them while there are enough
* credits to run higher priority jobs. We stop the job when the app's credits get too low.
*/
private static final ActionBill BILL_JOB_RUNNING_LOW =
new ActionBill(List.of(
new EconomyManagerInternal.AnticipatedAction(
JobSchedulerEconomicPolicy.ACTION_JOB_DEFAULT_RUNNING, 0, 30_000L)
));
/**
* Bill to use while we're waiting to start a job. If a job isn't running yet, don't consider it
* eligible to run unless it can pay for a job start and at least some period of execution time.
*/
private static final ActionBill BILL_JOB_START_DEFAULT =
new ActionBill(List.of(
new EconomyManagerInternal.AnticipatedAction(
JobSchedulerEconomicPolicy.ACTION_JOB_DEFAULT_START, 1, 0),
new EconomyManagerInternal.AnticipatedAction(
JobSchedulerEconomicPolicy.ACTION_JOB_DEFAULT_RUNNING, 0, 30_000L)
));
/**
* Bill to use when a default priority job is currently running. We want to track and make
* sure the app can continue to pay for 1 more second of execution time. We stop the job when
* the app can no longer pay for that time.
*/
private static final ActionBill BILL_JOB_RUNNING_DEFAULT =
new ActionBill(List.of(
new EconomyManagerInternal.AnticipatedAction(
JobSchedulerEconomicPolicy.ACTION_JOB_DEFAULT_RUNNING, 0, 1_000L)
));
/**
* Bill to use while we're waiting to start a job. If a job isn't running yet, don't consider it
* eligible to run unless it can pay for a job start and at least some period of execution time.
*/
private static final ActionBill BILL_JOB_START_HIGH =
new ActionBill(List.of(
new EconomyManagerInternal.AnticipatedAction(
JobSchedulerEconomicPolicy.ACTION_JOB_HIGH_START, 1, 0),
new EconomyManagerInternal.AnticipatedAction(
JobSchedulerEconomicPolicy.ACTION_JOB_HIGH_RUNNING, 0, 30_000L)
));
/**
* Bill to use when a high priority job is currently running. We want to track and make sure
* the app can continue to pay for 1 more second of execution time. We stop the job when the
* app can no longer pay for that time.
*/
private static final ActionBill BILL_JOB_RUNNING_HIGH =
new ActionBill(List.of(
new EconomyManagerInternal.AnticipatedAction(
JobSchedulerEconomicPolicy.ACTION_JOB_HIGH_RUNNING, 0, 1_000L)
));
/**
* Bill to use while we're waiting to start a max priority job. This should only be used for
* requested-EJs that aren't allowed to run as EJs. If a job isn't running yet, don't consider
* it eligible to run unless it can pay for a job start and at least some period of execution
* time.
*/
private static final ActionBill BILL_JOB_START_MAX =
new ActionBill(List.of(
new EconomyManagerInternal.AnticipatedAction(
JobSchedulerEconomicPolicy.ACTION_JOB_MAX_START, 1, 0),
new EconomyManagerInternal.AnticipatedAction(
JobSchedulerEconomicPolicy.ACTION_JOB_MAX_RUNNING, 0, 30_000L)
));
/**
* Bill to use when a max priority job is currently running. This should only be used for
* requested-EJs that aren't allowed to run as EJs. We want to track and make sure
* the app can continue to pay for 1 more second of execution time. We stop the job when the
* app can no longer pay for that time.
*/
private static final ActionBill BILL_JOB_RUNNING_MAX =
new ActionBill(List.of(
new EconomyManagerInternal.AnticipatedAction(
JobSchedulerEconomicPolicy.ACTION_JOB_MAX_RUNNING, 0, 1_000L)
));
/**
* Bill to use while we're waiting to start a job. If a job isn't running yet, don't consider it
* eligible to run unless it can pay for a job start and at least some period of execution time.
*/
private static final ActionBill BILL_JOB_START_MAX_EXPEDITED =
new ActionBill(List.of(
new EconomyManagerInternal.AnticipatedAction(
JobSchedulerEconomicPolicy.ACTION_JOB_MAX_START, 1, 0),
new EconomyManagerInternal.AnticipatedAction(
JobSchedulerEconomicPolicy.ACTION_JOB_MAX_RUNNING, 0, 30_000L)
));
/**
* Bill to use when a max priority EJ is currently running (as an EJ). We want to track and
* make sure the app can continue to pay for 1 more second of execution time. We stop the job
* when the app can no longer pay for that time.
*/
private static final ActionBill BILL_JOB_RUNNING_MAX_EXPEDITED =
new ActionBill(List.of(
new EconomyManagerInternal.AnticipatedAction(
JobSchedulerEconomicPolicy.ACTION_JOB_MAX_RUNNING, 0, 1_000L)
));
/**
* Bill to use while we're waiting to start a job. If a job isn't running yet, don't consider it
* eligible to run unless it can pay for a job start and at least some period of execution time.
*/
private static final ActionBill BILL_JOB_START_HIGH_EXPEDITED =
new ActionBill(List.of(
new EconomyManagerInternal.AnticipatedAction(
JobSchedulerEconomicPolicy.ACTION_JOB_HIGH_START, 1, 0),
new EconomyManagerInternal.AnticipatedAction(
JobSchedulerEconomicPolicy.ACTION_JOB_HIGH_RUNNING, 0, 30_000L)
));
/**
* Bill to use when a high priority EJ is currently running (as an EJ). We want to track and
* make sure the app can continue to pay for 1 more second of execution time. We stop the job
* when the app can no longer pay for that time.
*/
private static final ActionBill BILL_JOB_RUNNING_HIGH_EXPEDITED =
new ActionBill(List.of(
new EconomyManagerInternal.AnticipatedAction(
JobSchedulerEconomicPolicy.ACTION_JOB_HIGH_RUNNING, 0, 1_000L)
));
private final EconomyManagerInternal mEconomyManagerInternal;
private final BackgroundJobsController mBackgroundJobsController;
private final ConnectivityController mConnectivityController;
/**
* Local cache of the ability of each userId-pkg to afford the various bills we're tracking for
* them.
*/
@GuardedBy("mLock")
private final SparseArrayMap<String, ArrayMap<ActionBill, Boolean>> mAffordabilityCache =
new SparseArrayMap<>();
/**
* List of all tracked jobs. Out SparseArrayMap is userId-sourcePkg. The inner mapping is the
* anticipated actions and all the jobs that are applicable to them.
*/
@GuardedBy("mLock")
private final SparseArrayMap<String, ArrayMap<ActionBill, ArraySet<JobStatus>>>
mRegisteredBillsAndJobs = new SparseArrayMap<>();
private final EconomyManagerInternal.AffordabilityChangeListener mAffordabilityChangeListener =
(userId, pkgName, bill, canAfford) -> {
final long nowElapsed = sElapsedRealtimeClock.millis();
if (DEBUG) {
Slog.d(TAG,
userId + ":" + pkgName + " affordability for " + getBillName(bill)
+ " changed to " + canAfford);
}
synchronized (mLock) {
ArrayMap<ActionBill, Boolean> actionAffordability =
mAffordabilityCache.get(userId, pkgName);
if (actionAffordability == null) {
actionAffordability = new ArrayMap<>();
mAffordabilityCache.add(userId, pkgName, actionAffordability);
}
actionAffordability.put(bill, canAfford);
final ArrayMap<ActionBill, ArraySet<JobStatus>> billToJobMap =
mRegisteredBillsAndJobs.get(userId, pkgName);
if (billToJobMap != null) {
final ArraySet<JobStatus> jobs = billToJobMap.get(bill);
if (jobs != null) {
final ArraySet<JobStatus> changedJobs = new ArraySet<>();
for (int i = 0; i < jobs.size(); ++i) {
final JobStatus job = jobs.valueAt(i);
// Use hasEnoughWealth if canAfford is false in case the job has
// other bills it can depend on (eg. EJs being demoted to
// regular jobs).
if (job.setTareWealthConstraintSatisfied(nowElapsed,
canAfford || hasEnoughWealthLocked(job))) {
changedJobs.add(job);
}
if (job.isRequestedExpeditedJob()
&& setExpeditedTareApproved(job, nowElapsed,
canAffordExpeditedBillLocked(job))) {
changedJobs.add(job);
}
}
if (changedJobs.size() > 0) {
mStateChangedListener.onControllerStateChanged(changedJobs);
}
}
}
}
};
/**
* List of jobs that started while the UID was in the TOP state. There will usually be no more
* than {@value JobConcurrencyManager#MAX_STANDARD_JOB_CONCURRENCY} running at once, so an
* ArraySet is fine.
*/
@GuardedBy("mLock")
private final ArraySet<JobStatus> mTopStartedJobs = new ArraySet<>();
@GuardedBy("mLock")
private boolean mIsEnabled;
public TareController(JobSchedulerService service,
@NonNull BackgroundJobsController backgroundJobsController,
@NonNull ConnectivityController connectivityController) {
super(service);
mBackgroundJobsController = backgroundJobsController;
mConnectivityController = connectivityController;
mEconomyManagerInternal = LocalServices.getService(EconomyManagerInternal.class);
mIsEnabled = mConstants.USE_TARE_POLICY;
}
@Override
@GuardedBy("mLock")
public void maybeStartTrackingJobLocked(JobStatus jobStatus, JobStatus lastJob) {
final long nowElapsed = sElapsedRealtimeClock.millis();
if (jobStatus.shouldTreatAsUserInitiatedJob()) {
// User-initiated jobs should always be allowed to run.
jobStatus.setTareWealthConstraintSatisfied(nowElapsed, true);
return;
}
jobStatus.setTareWealthConstraintSatisfied(nowElapsed, hasEnoughWealthLocked(jobStatus));
setExpeditedTareApproved(jobStatus, nowElapsed,
jobStatus.isRequestedExpeditedJob() && canAffordExpeditedBillLocked(jobStatus));
final ArraySet<ActionBill> bills = getPossibleStartBills(jobStatus);
for (int i = 0; i < bills.size(); ++i) {
addJobToBillList(jobStatus, bills.valueAt(i));
}
}
@Override
@GuardedBy("mLock")
public void prepareForExecutionLocked(JobStatus jobStatus) {
if (jobStatus.shouldTreatAsUserInitiatedJob()) {
// TODO(202954395): consider noting execution with the EconomyManager even though it
// won't affect this job
return;
}
final int userId = jobStatus.getSourceUserId();
final String pkgName = jobStatus.getSourcePackageName();
ArrayMap<ActionBill, ArraySet<JobStatus>> billToJobMap =
mRegisteredBillsAndJobs.get(userId, pkgName);
if (billToJobMap == null) {
Slog.e(TAG, "Job is being prepared but doesn't have a pre-existing billToJobMap");
} else {
for (int i = 0; i < billToJobMap.size(); ++i) {
removeJobFromBillList(jobStatus, billToJobMap.keyAt(i));
}
}
final int uid = jobStatus.getSourceUid();
if (mService.getUidBias(uid) == JobInfo.BIAS_TOP_APP) {
if (DEBUG) {
Slog.d(TAG, jobStatus.toShortString() + " is top started job");
}
mTopStartedJobs.add(jobStatus);
// Top jobs won't count towards quota so there's no need to involve the EconomyManager.
} else {
addJobToBillList(jobStatus, getRunningBill(jobStatus));
mEconomyManagerInternal.noteOngoingEventStarted(userId, pkgName,
getRunningActionId(jobStatus), String.valueOf(jobStatus.getJobId()));
}
}
@Override
@GuardedBy("mLock")
public void unprepareFromExecutionLocked(JobStatus jobStatus) {
if (jobStatus.shouldTreatAsUserInitiatedJob()) {
return;
}
final int userId = jobStatus.getSourceUserId();
final String pkgName = jobStatus.getSourcePackageName();
// If this method is called, then jobStatus.madeActive was never updated, so don't use it
// to determine if the EconomyManager was notified.
if (!mTopStartedJobs.remove(jobStatus)) {
// If the job was started while the app was top, then the EconomyManager wasn't notified
// of the job start.
mEconomyManagerInternal.noteOngoingEventStopped(userId, pkgName,
getRunningActionId(jobStatus), String.valueOf(jobStatus.getJobId()));
}
final ArraySet<ActionBill> bills = getPossibleStartBills(jobStatus);
ArrayMap<ActionBill, ArraySet<JobStatus>> billToJobMap =
mRegisteredBillsAndJobs.get(userId, pkgName);
if (billToJobMap == null) {
Slog.e(TAG, "Job was just unprepared but didn't have a pre-existing billToJobMap");
} else {
for (int i = 0; i < billToJobMap.size(); ++i) {
removeJobFromBillList(jobStatus, billToJobMap.keyAt(i));
}
}
for (int i = 0; i < bills.size(); ++i) {
addJobToBillList(jobStatus, bills.valueAt(i));
}
}
@Override
@GuardedBy("mLock")
public void maybeStopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob) {
if (jobStatus.shouldTreatAsUserInitiatedJob()) {
return;
}
final int userId = jobStatus.getSourceUserId();
final String pkgName = jobStatus.getSourcePackageName();
if (!mTopStartedJobs.remove(jobStatus) && jobStatus.madeActive > 0) {
// Only note the job stop if we previously told the EconomyManager that the job started.
// If the job was started while the app was top, then the EconomyManager wasn't notified
// of the job start.
mEconomyManagerInternal.noteOngoingEventStopped(userId, pkgName,
getRunningActionId(jobStatus), String.valueOf(jobStatus.getJobId()));
}
ArrayMap<ActionBill, ArraySet<JobStatus>> billToJobMap =
mRegisteredBillsAndJobs.get(userId, pkgName);
if (billToJobMap != null) {
for (int i = 0; i < billToJobMap.size(); ++i) {
removeJobFromBillList(jobStatus, billToJobMap.keyAt(i));
}
}
}
@Override
@GuardedBy("mLock")
public void onConstantsUpdatedLocked() {
if (mIsEnabled != mConstants.USE_TARE_POLICY) {
mIsEnabled = mConstants.USE_TARE_POLICY;
// Update job bookkeeping out of band.
AppSchedulingModuleThread.getHandler().post(() -> {
synchronized (mLock) {
final long nowElapsed = sElapsedRealtimeClock.millis();
mService.getJobStore().forEachJob((jobStatus) -> {
if (!mIsEnabled) {
jobStatus.setTareWealthConstraintSatisfied(nowElapsed, true);
setExpeditedTareApproved(jobStatus, nowElapsed, true);
} else {
jobStatus.setTareWealthConstraintSatisfied(
nowElapsed, hasEnoughWealthLocked(jobStatus));
setExpeditedTareApproved(jobStatus, nowElapsed,
jobStatus.isRequestedExpeditedJob()
&& canAffordExpeditedBillLocked(jobStatus));
}
});
}
});
}
}
@GuardedBy("mLock")
public boolean canScheduleEJ(@NonNull JobStatus jobStatus) {
if (!mIsEnabled) {
return true;
}
if (jobStatus.getEffectivePriority() == JobInfo.PRIORITY_MAX) {
return canAffordBillLocked(jobStatus, BILL_JOB_START_MAX_EXPEDITED);
}
return canAffordBillLocked(jobStatus, BILL_JOB_START_HIGH_EXPEDITED);
}
/** @return true if the job was started while the app was in the TOP state. */
@GuardedBy("mLock")
private boolean isTopStartedJobLocked(@NonNull final JobStatus jobStatus) {
return mTopStartedJobs.contains(jobStatus);
}
@GuardedBy("mLock")
public long getMaxJobExecutionTimeMsLocked(@NonNull JobStatus jobStatus) {
if (!mIsEnabled) {
return mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS;
}
return mEconomyManagerInternal.getMaxDurationMs(
jobStatus.getSourceUserId(), jobStatus.getSourcePackageName(),
getRunningBill(jobStatus));
}
@GuardedBy("mLock")
private void addJobToBillList(@NonNull JobStatus jobStatus, @NonNull ActionBill bill) {
final int userId = jobStatus.getSourceUserId();
final String pkgName = jobStatus.getSourcePackageName();
ArrayMap<ActionBill, ArraySet<JobStatus>> billToJobMap =
mRegisteredBillsAndJobs.get(userId, pkgName);
if (billToJobMap == null) {
billToJobMap = new ArrayMap<>();
mRegisteredBillsAndJobs.add(userId, pkgName, billToJobMap);
}
ArraySet<JobStatus> jobs = billToJobMap.get(bill);
if (jobs == null) {
jobs = new ArraySet<>();
billToJobMap.put(bill, jobs);
}
if (jobs.add(jobStatus)) {
mEconomyManagerInternal.registerAffordabilityChangeListener(userId, pkgName,
mAffordabilityChangeListener, bill);
}
}
@GuardedBy("mLock")
private void removeJobFromBillList(@NonNull JobStatus jobStatus, @NonNull ActionBill bill) {
final int userId = jobStatus.getSourceUserId();
final String pkgName = jobStatus.getSourcePackageName();
final ArrayMap<ActionBill, ArraySet<JobStatus>> billToJobMap =
mRegisteredBillsAndJobs.get(userId, pkgName);
if (billToJobMap != null) {
final ArraySet<JobStatus> jobs = billToJobMap.get(bill);
if (jobs == null || (jobs.remove(jobStatus) && jobs.size() == 0)) {
mEconomyManagerInternal.unregisterAffordabilityChangeListener(
userId, pkgName, mAffordabilityChangeListener, bill);
// Remove the cached value so we don't accidentally use it when the app
// schedules a new job.
final ArrayMap<ActionBill, Boolean> actionAffordability =
mAffordabilityCache.get(userId, pkgName);
if (actionAffordability != null) {
actionAffordability.remove(bill);
}
}
}
}
@NonNull
private ArraySet<ActionBill> getPossibleStartBills(JobStatus jobStatus) {
// TODO: factor in network cost when available
final ArraySet<ActionBill> bills = new ArraySet<>();
if (jobStatus.isRequestedExpeditedJob()) {
if (jobStatus.getEffectivePriority() == JobInfo.PRIORITY_MAX) {
bills.add(BILL_JOB_START_MAX_EXPEDITED);
} else {
bills.add(BILL_JOB_START_HIGH_EXPEDITED);
}
}
switch (jobStatus.getEffectivePriority()) {
case JobInfo.PRIORITY_MAX:
bills.add(BILL_JOB_START_MAX);
break;
case JobInfo.PRIORITY_HIGH:
bills.add(BILL_JOB_START_HIGH);
break;
case JobInfo.PRIORITY_DEFAULT:
bills.add(BILL_JOB_START_DEFAULT);
break;
case JobInfo.PRIORITY_LOW:
bills.add(BILL_JOB_START_LOW);
break;
case JobInfo.PRIORITY_MIN:
bills.add(BILL_JOB_START_MIN);
break;
default:
Slog.wtf(TAG, "Unexpected priority: "
+ JobInfo.getPriorityString(jobStatus.getEffectivePriority()));
break;
}
return bills;
}
@NonNull
private ActionBill getRunningBill(JobStatus jobStatus) {
// TODO: factor in network cost when available
if (jobStatus.shouldTreatAsExpeditedJob() || jobStatus.startedAsExpeditedJob) {
if (jobStatus.getEffectivePriority() == JobInfo.PRIORITY_MAX) {
return BILL_JOB_RUNNING_MAX_EXPEDITED;
} else {
return BILL_JOB_RUNNING_HIGH_EXPEDITED;
}
}
switch (jobStatus.getEffectivePriority()) {
case JobInfo.PRIORITY_MAX:
return BILL_JOB_RUNNING_MAX;
case JobInfo.PRIORITY_HIGH:
return BILL_JOB_RUNNING_HIGH;
case JobInfo.PRIORITY_LOW:
return BILL_JOB_RUNNING_LOW;
case JobInfo.PRIORITY_MIN:
return BILL_JOB_RUNNING_MIN;
default:
Slog.wtf(TAG, "Got unexpected priority: " + jobStatus.getEffectivePriority());
// Intentional fallthrough
case JobInfo.PRIORITY_DEFAULT:
return BILL_JOB_RUNNING_DEFAULT;
}
}
@EconomicPolicy.AppAction
private static int getRunningActionId(@NonNull JobStatus job) {
switch (job.getEffectivePriority()) {
case JobInfo.PRIORITY_MAX:
return JobSchedulerEconomicPolicy.ACTION_JOB_MAX_RUNNING;
case JobInfo.PRIORITY_HIGH:
return JobSchedulerEconomicPolicy.ACTION_JOB_HIGH_RUNNING;
case JobInfo.PRIORITY_LOW:
return JobSchedulerEconomicPolicy.ACTION_JOB_LOW_RUNNING;
case JobInfo.PRIORITY_MIN:
return JobSchedulerEconomicPolicy.ACTION_JOB_MIN_RUNNING;
default:
Slog.wtf(TAG, "Unknown priority: " + getPriorityString(job.getEffectivePriority()));
// Intentional fallthrough
case JobInfo.PRIORITY_DEFAULT:
return JobSchedulerEconomicPolicy.ACTION_JOB_DEFAULT_RUNNING;
}
}
@GuardedBy("mLock")
private boolean canAffordBillLocked(@NonNull JobStatus jobStatus, @NonNull ActionBill bill) {
if (!mIsEnabled) {
return true;
}
if (mService.getUidBias(jobStatus.getSourceUid()) == JobInfo.BIAS_TOP_APP
|| isTopStartedJobLocked(jobStatus)) {
// Jobs for the top app should always be allowed to run, and any jobs started while
// the app is on top shouldn't consume any credits.
return true;
}
final int userId = jobStatus.getSourceUserId();
final String pkgName = jobStatus.getSourcePackageName();
ArrayMap<ActionBill, Boolean> actionAffordability =
mAffordabilityCache.get(userId, pkgName);
if (actionAffordability == null) {
actionAffordability = new ArrayMap<>();
mAffordabilityCache.add(userId, pkgName, actionAffordability);
}
if (actionAffordability.containsKey(bill)) {
return actionAffordability.get(bill);
}
final boolean canAfford = mEconomyManagerInternal.canPayFor(userId, pkgName, bill);
actionAffordability.put(bill, canAfford);
return canAfford;
}
@GuardedBy("mLock")
private boolean canAffordExpeditedBillLocked(@NonNull JobStatus jobStatus) {
if (!mIsEnabled) {
return true;
}
if (!jobStatus.isRequestedExpeditedJob()) {
return false;
}
if (mService.getUidBias(jobStatus.getSourceUid()) == JobInfo.BIAS_TOP_APP
|| isTopStartedJobLocked(jobStatus)) {
// Jobs for the top app should always be allowed to run, and any jobs started while
// the app is on top shouldn't consume any credits.
return true;
}
if (mService.isCurrentlyRunningLocked(jobStatus)) {
return canAffordBillLocked(jobStatus, getRunningBill(jobStatus));
}
if (jobStatus.getEffectivePriority() == JobInfo.PRIORITY_MAX) {
return canAffordBillLocked(jobStatus, BILL_JOB_START_MAX_EXPEDITED);
}
return canAffordBillLocked(jobStatus, BILL_JOB_START_HIGH_EXPEDITED);
}
@GuardedBy("mLock")
private boolean hasEnoughWealthLocked(@NonNull JobStatus jobStatus) {
if (!mIsEnabled) {
return true;
}
if (jobStatus.shouldTreatAsUserInitiatedJob()) {
// Always allow user-initiated jobs.
return true;
}
if (mService.getUidBias(jobStatus.getSourceUid()) == JobInfo.BIAS_TOP_APP
|| isTopStartedJobLocked(jobStatus)) {
// Jobs for the top app should always be allowed to run, and any jobs started while
// the app is on top shouldn't consume any credits.
return true;
}
if (mService.isCurrentlyRunningLocked(jobStatus)) {
return canAffordBillLocked(jobStatus, getRunningBill(jobStatus));
}
final ArraySet<ActionBill> bills = getPossibleStartBills(jobStatus);
for (int i = 0; i < bills.size(); ++i) {
ActionBill bill = bills.valueAt(i);
if (canAffordBillLocked(jobStatus, bill)) {
return true;
}
}
return false;
}
/**
* If the satisfaction changes, this will tell connectivity & background jobs controller to
* also re-evaluate their state.
*/
private boolean setExpeditedTareApproved(@NonNull JobStatus jobStatus, long nowElapsed,
boolean isApproved) {
if (jobStatus.setExpeditedJobTareApproved(nowElapsed, isApproved)) {
mBackgroundJobsController.evaluateStateLocked(jobStatus);
mConnectivityController.evaluateStateLocked(jobStatus);
if (isApproved && jobStatus.isReady()) {
mStateChangedListener.onRunJobNow(jobStatus);
}
return true;
}
return false;
}
@NonNull
private String getBillName(@NonNull ActionBill bill) {
if (bill.equals(BILL_JOB_START_MAX_EXPEDITED)) {
return "EJ_MAX_START_BILL";
}
if (bill.equals(BILL_JOB_RUNNING_MAX_EXPEDITED)) {
return "EJ_MAX_RUNNING_BILL";
}
if (bill.equals(BILL_JOB_START_HIGH_EXPEDITED)) {
return "EJ_HIGH_START_BILL";
}
if (bill.equals(BILL_JOB_RUNNING_HIGH_EXPEDITED)) {
return "EJ_HIGH_RUNNING_BILL";
}
if (bill.equals(BILL_JOB_START_HIGH)) {
return "HIGH_START_BILL";
}
if (bill.equals(BILL_JOB_RUNNING_HIGH)) {
return "HIGH_RUNNING_BILL";
}
if (bill.equals(BILL_JOB_START_DEFAULT)) {
return "DEFAULT_START_BILL";
}
if (bill.equals(BILL_JOB_RUNNING_DEFAULT)) {
return "DEFAULT_RUNNING_BILL";
}
if (bill.equals(BILL_JOB_START_LOW)) {
return "LOW_START_BILL";
}
if (bill.equals(BILL_JOB_RUNNING_LOW)) {
return "LOW_RUNNING_BILL";
}
if (bill.equals(BILL_JOB_START_MIN)) {
return "MIN_START_BILL";
}
if (bill.equals(BILL_JOB_RUNNING_MIN)) {
return "MIN_RUNNING_BILL";
}
return "UNKNOWN_BILL (" + bill + ")";
}
@Override
public void dumpControllerStateLocked(IndentingPrintWriter pw, Predicate<JobStatus> predicate) {
pw.print("Is enabled: ");
pw.println(mIsEnabled);
pw.println("Affordability cache:");
pw.increaseIndent();
mAffordabilityCache.forEach((userId, pkgName, billMap) -> {
final int numBills = billMap.size();
if (numBills > 0) {
pw.print(userId);
pw.print(":");
pw.print(pkgName);
pw.println(":");
pw.increaseIndent();
for (int i = 0; i < numBills; ++i) {
pw.print(getBillName(billMap.keyAt(i)));
pw.print(": ");
pw.println(billMap.valueAt(i));
}
pw.decreaseIndent();
}
});
pw.decreaseIndent();
}
}