blob: 175c8d1cc4f3313036f9aae961c94055c02ed736 [file] [log] [blame]
/*
* Copyright (C) 2018 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.text.format.DateUtils.HOUR_IN_MILLIS;
import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
import static android.text.format.DateUtils.SECOND_IN_MILLIS;
import static com.android.server.job.JobSchedulerService.ACTIVE_INDEX;
import static com.android.server.job.JobSchedulerService.EXEMPTED_INDEX;
import static com.android.server.job.JobSchedulerService.FREQUENT_INDEX;
import static com.android.server.job.JobSchedulerService.NEVER_INDEX;
import static com.android.server.job.JobSchedulerService.RARE_INDEX;
import static com.android.server.job.JobSchedulerService.RESTRICTED_INDEX;
import static com.android.server.job.JobSchedulerService.WORKING_INDEX;
import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock;
import android.Manifest;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UserIdInt;
import android.app.ActivityManager;
import android.app.AlarmManager;
import android.app.UidObserver;
import android.app.usage.UsageEvents;
import android.app.usage.UsageStatsManagerInternal;
import android.app.usage.UsageStatsManagerInternal.UsageEventListener;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.UserPackage;
import android.os.BatteryManager;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.RemoteException;
import android.os.UserHandle;
import android.provider.DeviceConfig;
import android.util.ArraySet;
import android.util.IndentingPrintWriter;
import android.util.Log;
import android.util.Slog;
import android.util.SparseArray;
import android.util.SparseArrayMap;
import android.util.SparseBooleanArray;
import android.util.SparseLongArray;
import android.util.SparseSetArray;
import android.util.proto.ProtoOutputStream;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.ArrayUtils;
import com.android.server.AppSchedulingModuleThread;
import com.android.server.LocalServices;
import com.android.server.PowerAllowlistInternal;
import com.android.server.job.ConstantsProto;
import com.android.server.job.JobSchedulerService;
import com.android.server.job.StateControllerProto;
import com.android.server.usage.AppStandbyInternal;
import com.android.server.usage.AppStandbyInternal.AppIdleStateChangeListener;
import com.android.server.utils.AlarmQueue;
import dalvik.annotation.optimization.NeverCompile;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Predicate;
/**
* Controller that tracks whether an app has exceeded its standby bucket quota.
*
* With initial defaults, each app in each bucket is given 10 minutes to run within its respective
* time window. Active jobs can run indefinitely, working set jobs can run for 10 minutes within a
* 2 hour window, frequent jobs get to run 10 minutes in an 8 hour window, and rare jobs get to run
* 10 minutes in a 24 hour window. The windows are rolling, so as soon as a job would have some
* quota based on its bucket, it will be eligible to run. When a job's bucket changes, its new
* quota is immediately applied to it.
*
* Job and session count limits are included to prevent abuse/spam. Each bucket has its own limit on
* the number of jobs or sessions that can run within the window. Regardless of bucket, apps will
* not be allowed to run more than 20 jobs within the past 10 minutes.
*
* Jobs are throttled while an app is not in a foreground state. All jobs are allowed to run
* freely when an app enters the foreground state and are restricted when the app leaves the
* foreground state. However, jobs that are started while the app is in the TOP state do not count
* towards any quota and are not restricted regardless of the app's state change.
*
* Jobs will not be throttled when the device is charging. The device is considered to be charging
* once the {@link BatteryManager#ACTION_CHARGING} intent has been broadcast.
*
* Note: all limits are enforced per bucket window unless explicitly stated otherwise.
* All stated values are configurable and subject to change. See {@link QcConstants} for current
* defaults.
*
* Test: atest com.android.server.job.controllers.QuotaControllerTest
*/
public final class QuotaController extends StateController {
private static final String TAG = "JobScheduler.Quota";
private static final boolean DEBUG = JobSchedulerService.DEBUG
|| Log.isLoggable(TAG, Log.DEBUG);
private static final String ALARM_TAG_CLEANUP = "*job.cleanup*";
private static final String ALARM_TAG_QUOTA_CHECK = "*job.quota_check*";
private static final int SYSTEM_APP_CHECK_FLAGS =
PackageManager.MATCH_DIRECT_BOOT_AWARE | PackageManager.MATCH_DIRECT_BOOT_UNAWARE
| PackageManager.GET_PERMISSIONS | PackageManager.MATCH_KNOWN_PACKAGES;
private static int hashLong(long val) {
return (int) (val ^ (val >>> 32));
}
@VisibleForTesting
static class ExecutionStats {
/**
* The time after which this record should be considered invalid (out of date), in the
* elapsed realtime timebase.
*/
public long expirationTimeElapsed;
public long allowedTimePerPeriodMs;
public long windowSizeMs;
public int jobCountLimit;
public int sessionCountLimit;
/** The total amount of time the app ran in its respective bucket window size. */
public long executionTimeInWindowMs;
public int bgJobCountInWindow;
/** The total amount of time the app ran in the last {@link #MAX_PERIOD_MS}. */
public long executionTimeInMaxPeriodMs;
public int bgJobCountInMaxPeriod;
/**
* The number of {@link TimingSession TimingSessions} within the bucket window size.
* This will include sessions that started before the window as long as they end within
* the window.
*/
public int sessionCountInWindow;
/**
* The time after which the app will be under the bucket quota and can start running jobs
* again. This is only valid if
* {@link #executionTimeInWindowMs} >= {@link #mAllowedTimePerPeriodMs},
* {@link #executionTimeInMaxPeriodMs} >= {@link #mMaxExecutionTimeMs},
* {@link #bgJobCountInWindow} >= {@link #jobCountLimit}, or
* {@link #sessionCountInWindow} >= {@link #sessionCountLimit}.
*/
public long inQuotaTimeElapsed;
/**
* The time after which {@link #jobCountInRateLimitingWindow} should be considered invalid,
* in the elapsed realtime timebase.
*/
public long jobRateLimitExpirationTimeElapsed;
/**
* The number of jobs that ran in at least the last {@link #mRateLimitingWindowMs}.
* It may contain a few stale entries since cleanup won't happen exactly every
* {@link #mRateLimitingWindowMs}.
*/
public int jobCountInRateLimitingWindow;
/**
* The time after which {@link #sessionCountInRateLimitingWindow} should be considered
* invalid, in the elapsed realtime timebase.
*/
public long sessionRateLimitExpirationTimeElapsed;
/**
* The number of {@link TimingSession TimingSessions} that ran in at least the last
* {@link #mRateLimitingWindowMs}. It may contain a few stale entries since cleanup won't
* happen exactly every {@link #mRateLimitingWindowMs}. This should only be considered
* valid before elapsed realtime has reached {@link #sessionRateLimitExpirationTimeElapsed}.
*/
public int sessionCountInRateLimitingWindow;
@Override
public String toString() {
return "expirationTime=" + expirationTimeElapsed + ", "
+ "allowedTimePerPeriodMs=" + allowedTimePerPeriodMs + ", "
+ "windowSizeMs=" + windowSizeMs + ", "
+ "jobCountLimit=" + jobCountLimit + ", "
+ "sessionCountLimit=" + sessionCountLimit + ", "
+ "executionTimeInWindow=" + executionTimeInWindowMs + ", "
+ "bgJobCountInWindow=" + bgJobCountInWindow + ", "
+ "executionTimeInMaxPeriod=" + executionTimeInMaxPeriodMs + ", "
+ "bgJobCountInMaxPeriod=" + bgJobCountInMaxPeriod + ", "
+ "sessionCountInWindow=" + sessionCountInWindow + ", "
+ "inQuotaTime=" + inQuotaTimeElapsed + ", "
+ "rateLimitJobCountExpirationTime=" + jobRateLimitExpirationTimeElapsed + ", "
+ "rateLimitJobCountWindow=" + jobCountInRateLimitingWindow + ", "
+ "rateLimitSessionCountExpirationTime="
+ sessionRateLimitExpirationTimeElapsed + ", "
+ "rateLimitSessionCountWindow=" + sessionCountInRateLimitingWindow;
}
@Override
public boolean equals(Object obj) {
if (obj instanceof ExecutionStats) {
ExecutionStats other = (ExecutionStats) obj;
return this.expirationTimeElapsed == other.expirationTimeElapsed
&& this.allowedTimePerPeriodMs == other.allowedTimePerPeriodMs
&& this.windowSizeMs == other.windowSizeMs
&& this.jobCountLimit == other.jobCountLimit
&& this.sessionCountLimit == other.sessionCountLimit
&& this.executionTimeInWindowMs == other.executionTimeInWindowMs
&& this.bgJobCountInWindow == other.bgJobCountInWindow
&& this.executionTimeInMaxPeriodMs == other.executionTimeInMaxPeriodMs
&& this.sessionCountInWindow == other.sessionCountInWindow
&& this.bgJobCountInMaxPeriod == other.bgJobCountInMaxPeriod
&& this.inQuotaTimeElapsed == other.inQuotaTimeElapsed
&& this.jobRateLimitExpirationTimeElapsed
== other.jobRateLimitExpirationTimeElapsed
&& this.jobCountInRateLimitingWindow == other.jobCountInRateLimitingWindow
&& this.sessionRateLimitExpirationTimeElapsed
== other.sessionRateLimitExpirationTimeElapsed
&& this.sessionCountInRateLimitingWindow
== other.sessionCountInRateLimitingWindow;
} else {
return false;
}
}
@Override
public int hashCode() {
int result = 0;
result = 31 * result + hashLong(expirationTimeElapsed);
result = 31 * result + hashLong(allowedTimePerPeriodMs);
result = 31 * result + hashLong(windowSizeMs);
result = 31 * result + hashLong(jobCountLimit);
result = 31 * result + hashLong(sessionCountLimit);
result = 31 * result + hashLong(executionTimeInWindowMs);
result = 31 * result + bgJobCountInWindow;
result = 31 * result + hashLong(executionTimeInMaxPeriodMs);
result = 31 * result + bgJobCountInMaxPeriod;
result = 31 * result + sessionCountInWindow;
result = 31 * result + hashLong(inQuotaTimeElapsed);
result = 31 * result + hashLong(jobRateLimitExpirationTimeElapsed);
result = 31 * result + jobCountInRateLimitingWindow;
result = 31 * result + hashLong(sessionRateLimitExpirationTimeElapsed);
result = 31 * result + sessionCountInRateLimitingWindow;
return result;
}
}
/** List of all tracked jobs keyed by source package-userId combo. */
private final SparseArrayMap<String, ArraySet<JobStatus>> mTrackedJobs = new SparseArrayMap<>();
/** Timer for each package-userId combo. */
private final SparseArrayMap<String, Timer> mPkgTimers = new SparseArrayMap<>();
/** Timer for expedited jobs for each package-userId combo. */
private final SparseArrayMap<String, Timer> mEJPkgTimers = new SparseArrayMap<>();
/** List of all regular timing sessions for a package-userId combo, in chronological order. */
private final SparseArrayMap<String, List<TimedEvent>> mTimingEvents = new SparseArrayMap<>();
/**
* List of all expedited job timing sessions for a package-userId combo, in chronological order.
*/
private final SparseArrayMap<String, List<TimedEvent>> mEJTimingSessions =
new SparseArrayMap<>();
/**
* Queue to track and manage when each package comes back within quota.
*/
@GuardedBy("mLock")
private final InQuotaAlarmQueue mInQuotaAlarmQueue;
/** Cached calculation results for each app, with the standby buckets as the array indices. */
private final SparseArrayMap<String, ExecutionStats[]> mExecutionStatsCache =
new SparseArrayMap<>();
private final SparseArrayMap<String, ShrinkableDebits> mEJStats = new SparseArrayMap<>();
private final SparseArrayMap<String, TopAppTimer> mTopAppTrackers = new SparseArrayMap<>();
/** List of UIDs currently in the foreground. */
private final SparseBooleanArray mForegroundUids = new SparseBooleanArray();
/**
* List of jobs that started while the UID was in the TOP state. There will usually be no more
* than {@value JobConcurrencyManager#MAX_STANDARD_JOB_CONCURRENCY} running at once, so an
* ArraySet is fine.
*/
private final ArraySet<JobStatus> mTopStartedJobs = new ArraySet<>();
/** Current set of UIDs on the temp allowlist. */
private final SparseBooleanArray mTempAllowlistCache = new SparseBooleanArray();
/**
* Mapping of UIDs to when their temp allowlist grace period ends (in the elapsed
* realtime timebase).
*/
private final SparseLongArray mTempAllowlistGraceCache = new SparseLongArray();
/** Current set of UIDs in the {@link ActivityManager#PROCESS_STATE_TOP} state. */
private final SparseBooleanArray mTopAppCache = new SparseBooleanArray();
/**
* Mapping of UIDs to the when their top app grace period ends (in the elapsed realtime
* timebase).
*/
private final SparseLongArray mTopAppGraceCache = new SparseLongArray();
private final AlarmManager mAlarmManager;
private final QcHandler mHandler;
private final QcConstants mQcConstants;
private final BackgroundJobsController mBackgroundJobsController;
private final ConnectivityController mConnectivityController;
@GuardedBy("mLock")
private boolean mIsEnabled;
/** How much time each app will have to run jobs within their standby bucket window. */
private final long[] mAllowedTimePerPeriodMs = new long[]{
QcConstants.DEFAULT_ALLOWED_TIME_PER_PERIOD_ACTIVE_MS,
QcConstants.DEFAULT_ALLOWED_TIME_PER_PERIOD_WORKING_MS,
QcConstants.DEFAULT_ALLOWED_TIME_PER_PERIOD_FREQUENT_MS,
QcConstants.DEFAULT_ALLOWED_TIME_PER_PERIOD_RARE_MS,
0, // NEVER
QcConstants.DEFAULT_ALLOWED_TIME_PER_PERIOD_RESTRICTED_MS,
QcConstants.DEFAULT_ALLOWED_TIME_PER_PERIOD_EXEMPTED_MS
};
/**
* The maximum amount of time an app can have its jobs running within a {@link #MAX_PERIOD_MS}
* window.
*/
private long mMaxExecutionTimeMs = QcConstants.DEFAULT_MAX_EXECUTION_TIME_MS;
/**
* How much time the app should have before transitioning from out-of-quota to in-quota.
* This should not affect processing if the app is already in-quota.
*/
private long mQuotaBufferMs = QcConstants.DEFAULT_IN_QUOTA_BUFFER_MS;
/**
* {@link #mMaxExecutionTimeMs} - {@link #mQuotaBufferMs}. This can be used to determine when an
* app will have enough quota to transition from out-of-quota to in-quota.
*/
private long mMaxExecutionTimeIntoQuotaMs = mMaxExecutionTimeMs - mQuotaBufferMs;
/** The period of time used to rate limit recently run jobs. */
private long mRateLimitingWindowMs = QcConstants.DEFAULT_RATE_LIMITING_WINDOW_MS;
/** The maximum number of jobs that can run within the past {@link #mRateLimitingWindowMs}. */
private int mMaxJobCountPerRateLimitingWindow =
QcConstants.DEFAULT_MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW;
/**
* The maximum number of {@link TimingSession TimingSessions} that can run within the past
* {@link #mRateLimitingWindowMs}.
*/
private int mMaxSessionCountPerRateLimitingWindow =
QcConstants.DEFAULT_MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW;
private long mNextCleanupTimeElapsed = 0;
private final AlarmManager.OnAlarmListener mSessionCleanupAlarmListener =
new AlarmManager.OnAlarmListener() {
@Override
public void onAlarm() {
mHandler.obtainMessage(MSG_CLEAN_UP_SESSIONS).sendToTarget();
}
};
private class QcUidObserver extends UidObserver {
@Override
public void onUidStateChanged(int uid, int procState, long procStateSeq, int capability) {
mHandler.obtainMessage(MSG_UID_PROCESS_STATE_CHANGED, uid, procState).sendToTarget();
}
}
/**
* The rolling window size for each standby bucket. Within each window, an app will have 10
* minutes to run its jobs.
*/
private final long[] mBucketPeriodsMs = new long[]{
QcConstants.DEFAULT_WINDOW_SIZE_ACTIVE_MS,
QcConstants.DEFAULT_WINDOW_SIZE_WORKING_MS,
QcConstants.DEFAULT_WINDOW_SIZE_FREQUENT_MS,
QcConstants.DEFAULT_WINDOW_SIZE_RARE_MS,
0, // NEVER
QcConstants.DEFAULT_WINDOW_SIZE_RESTRICTED_MS,
QcConstants.DEFAULT_WINDOW_SIZE_EXEMPTED_MS
};
/** The maximum period any bucket can have. */
private static final long MAX_PERIOD_MS = 24 * 60 * MINUTE_IN_MILLIS;
/**
* The maximum number of jobs based on its standby bucket. For each max value count in the
* array, the app will not be allowed to run more than that many number of jobs within the
* latest time interval of its rolling window size.
*
* @see #mBucketPeriodsMs
*/
private final int[] mMaxBucketJobCounts = new int[]{
QcConstants.DEFAULT_MAX_JOB_COUNT_ACTIVE,
QcConstants.DEFAULT_MAX_JOB_COUNT_WORKING,
QcConstants.DEFAULT_MAX_JOB_COUNT_FREQUENT,
QcConstants.DEFAULT_MAX_JOB_COUNT_RARE,
0, // NEVER
QcConstants.DEFAULT_MAX_JOB_COUNT_RESTRICTED,
QcConstants.DEFAULT_MAX_JOB_COUNT_EXEMPTED
};
/**
* The maximum number of {@link TimingSession TimingSessions} based on its standby bucket.
* For each max value count in the array, the app will not be allowed to have more than that
* many number of {@link TimingSession TimingSessions} within the latest time interval of its
* rolling window size.
*
* @see #mBucketPeriodsMs
*/
private final int[] mMaxBucketSessionCounts = new int[]{
QcConstants.DEFAULT_MAX_SESSION_COUNT_ACTIVE,
QcConstants.DEFAULT_MAX_SESSION_COUNT_WORKING,
QcConstants.DEFAULT_MAX_SESSION_COUNT_FREQUENT,
QcConstants.DEFAULT_MAX_SESSION_COUNT_RARE,
0, // NEVER
QcConstants.DEFAULT_MAX_SESSION_COUNT_RESTRICTED,
QcConstants.DEFAULT_MAX_SESSION_COUNT_EXEMPTED,
};
/**
* Treat two distinct {@link TimingSession TimingSessions} as the same if they start and end
* within this amount of time of each other.
*/
private long mTimingSessionCoalescingDurationMs =
QcConstants.DEFAULT_TIMING_SESSION_COALESCING_DURATION_MS;
/**
* The rolling window size for each standby bucket. Within each window, an app will have 10
* minutes to run its jobs.
*/
private final long[] mEJLimitsMs = new long[]{
QcConstants.DEFAULT_EJ_LIMIT_ACTIVE_MS,
QcConstants.DEFAULT_EJ_LIMIT_WORKING_MS,
QcConstants.DEFAULT_EJ_LIMIT_FREQUENT_MS,
QcConstants.DEFAULT_EJ_LIMIT_RARE_MS,
0, // NEVER
QcConstants.DEFAULT_EJ_LIMIT_RESTRICTED_MS,
QcConstants.DEFAULT_EJ_LIMIT_EXEMPTED_MS
};
private long mEjLimitAdditionInstallerMs = QcConstants.DEFAULT_EJ_LIMIT_ADDITION_INSTALLER_MS;
private long mEjLimitAdditionSpecialMs = QcConstants.DEFAULT_EJ_LIMIT_ADDITION_SPECIAL_MS;
/**
* The period of time used to calculate expedited job sessions. Apps can only have expedited job
* sessions totalling {@link #mEJLimitsMs}[bucket within this period of time (without factoring
* in any rewards or free EJs).
*/
private long mEJLimitWindowSizeMs = QcConstants.DEFAULT_EJ_WINDOW_SIZE_MS;
/**
* Length of time used to split an app's top time into chunks.
*/
private long mEJTopAppTimeChunkSizeMs = QcConstants.DEFAULT_EJ_TOP_APP_TIME_CHUNK_SIZE_MS;
/**
* How much EJ quota to give back to an app based on the number of top app time chunks it had.
*/
private long mEJRewardTopAppMs = QcConstants.DEFAULT_EJ_REWARD_TOP_APP_MS;
/**
* How much EJ quota to give back to an app based on each non-top user interaction.
*/
private long mEJRewardInteractionMs = QcConstants.DEFAULT_EJ_REWARD_INTERACTION_MS;
/**
* How much EJ quota to give back to an app based on each notification seen event.
*/
private long mEJRewardNotificationSeenMs = QcConstants.DEFAULT_EJ_REWARD_NOTIFICATION_SEEN_MS;
private long mEJGracePeriodTempAllowlistMs =
QcConstants.DEFAULT_EJ_GRACE_PERIOD_TEMP_ALLOWLIST_MS;
private long mEJGracePeriodTopAppMs = QcConstants.DEFAULT_EJ_GRACE_PERIOD_TOP_APP_MS;
private long mQuotaBumpAdditionalDurationMs =
QcConstants.DEFAULT_QUOTA_BUMP_ADDITIONAL_DURATION_MS;
private int mQuotaBumpAdditionalJobCount = QcConstants.DEFAULT_QUOTA_BUMP_ADDITIONAL_JOB_COUNT;
private int mQuotaBumpAdditionalSessionCount =
QcConstants.DEFAULT_QUOTA_BUMP_ADDITIONAL_SESSION_COUNT;
private long mQuotaBumpWindowSizeMs = QcConstants.DEFAULT_QUOTA_BUMP_WINDOW_SIZE_MS;
private int mQuotaBumpLimit = QcConstants.DEFAULT_QUOTA_BUMP_LIMIT;
/**
* List of system apps with the {@link android.Manifest.permission#INSTALL_PACKAGES} permission
* granted for each user.
*/
private final SparseSetArray<String> mSystemInstallers = new SparseSetArray<>();
/** An app has reached its quota. The message should contain a {@link UserPackage} object. */
@VisibleForTesting
static final int MSG_REACHED_QUOTA = 0;
/** Drop any old timing sessions. */
private static final int MSG_CLEAN_UP_SESSIONS = 1;
/** Check if a package is now within its quota. */
private static final int MSG_CHECK_PACKAGE = 2;
/** Process state for a UID has changed. */
private static final int MSG_UID_PROCESS_STATE_CHANGED = 3;
/**
* An app has reached its expedited job quota. The message should contain a {@link UserPackage}
* object.
*/
@VisibleForTesting
static final int MSG_REACHED_EJ_QUOTA = 4;
/**
* Process a new {@link UsageEvents.Event}. The event will be the message's object and the
* userId will the first arg.
*/
private static final int MSG_PROCESS_USAGE_EVENT = 5;
/** A UID's free quota grace period has ended. */
@VisibleForTesting
static final int MSG_END_GRACE_PERIOD = 6;
public QuotaController(@NonNull JobSchedulerService service,
@NonNull BackgroundJobsController backgroundJobsController,
@NonNull ConnectivityController connectivityController) {
super(service);
mHandler = new QcHandler(AppSchedulingModuleThread.get().getLooper());
mAlarmManager = mContext.getSystemService(AlarmManager.class);
mQcConstants = new QcConstants();
mBackgroundJobsController = backgroundJobsController;
mConnectivityController = connectivityController;
mIsEnabled = !mConstants.USE_TARE_POLICY;
mInQuotaAlarmQueue =
new InQuotaAlarmQueue(mContext, AppSchedulingModuleThread.get().getLooper());
// Set up the app standby bucketing tracker
AppStandbyInternal appStandby = LocalServices.getService(AppStandbyInternal.class);
appStandby.addListener(new StandbyTracker());
UsageStatsManagerInternal usmi = LocalServices.getService(UsageStatsManagerInternal.class);
usmi.registerListener(new UsageEventTracker());
PowerAllowlistInternal pai = LocalServices.getService(PowerAllowlistInternal.class);
pai.registerTempAllowlistChangeListener(new TempAllowlistTracker());
try {
ActivityManager.getService().registerUidObserver(new QcUidObserver(),
ActivityManager.UID_OBSERVER_PROCSTATE,
ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE, null);
ActivityManager.getService().registerUidObserver(new QcUidObserver(),
ActivityManager.UID_OBSERVER_PROCSTATE,
ActivityManager.PROCESS_STATE_TOP, null);
} catch (RemoteException e) {
// ignored; both services live in system_server
}
}
@Override
public void onSystemServicesReady() {
synchronized (mLock) {
cacheInstallerPackagesLocked(UserHandle.USER_SYSTEM);
}
}
@Override
@GuardedBy("mLock")
public void maybeStartTrackingJobLocked(JobStatus jobStatus, JobStatus lastJob) {
final long nowElapsed = sElapsedRealtimeClock.millis();
final int userId = jobStatus.getSourceUserId();
final String pkgName = jobStatus.getSourcePackageName();
ArraySet<JobStatus> jobs = mTrackedJobs.get(userId, pkgName);
if (jobs == null) {
jobs = new ArraySet<>();
mTrackedJobs.add(userId, pkgName, jobs);
}
jobs.add(jobStatus);
jobStatus.setTrackingController(JobStatus.TRACKING_QUOTA);
final boolean isWithinQuota = isWithinQuotaLocked(jobStatus);
final boolean isWithinEJQuota =
jobStatus.isRequestedExpeditedJob() && isWithinEJQuotaLocked(jobStatus);
setConstraintSatisfied(jobStatus, nowElapsed, isWithinQuota, isWithinEJQuota);
final boolean outOfEJQuota;
if (jobStatus.isRequestedExpeditedJob()) {
setExpeditedQuotaApproved(jobStatus, nowElapsed, isWithinEJQuota);
outOfEJQuota = !isWithinEJQuota;
} else {
outOfEJQuota = false;
}
if (!isWithinQuota || outOfEJQuota) {
maybeScheduleStartAlarmLocked(userId, pkgName, jobStatus.getEffectiveStandbyBucket());
}
}
@Override
@GuardedBy("mLock")
public void prepareForExecutionLocked(JobStatus jobStatus) {
if (DEBUG) {
Slog.d(TAG, "Prepping for " + jobStatus.toShortString());
}
final int uid = jobStatus.getSourceUid();
if (mTopAppCache.get(uid)) {
if (DEBUG) {
Slog.d(TAG, jobStatus.toShortString() + " is top started job");
}
mTopStartedJobs.add(jobStatus);
// Top jobs won't count towards quota so there's no need to involve the Timer.
return;
} else if (jobStatus.shouldTreatAsUserInitiatedJob()) {
// User-initiated jobs won't count towards quota.
return;
}
final int userId = jobStatus.getSourceUserId();
final String packageName = jobStatus.getSourcePackageName();
final SparseArrayMap<String, Timer> timerMap =
jobStatus.shouldTreatAsExpeditedJob() ? mEJPkgTimers : mPkgTimers;
Timer timer = timerMap.get(userId, packageName);
if (timer == null) {
timer = new Timer(uid, userId, packageName, !jobStatus.shouldTreatAsExpeditedJob());
timerMap.add(userId, packageName, timer);
}
timer.startTrackingJobLocked(jobStatus);
}
@Override
@GuardedBy("mLock")
public void unprepareFromExecutionLocked(JobStatus jobStatus) {
Timer timer = mPkgTimers.get(jobStatus.getSourceUserId(), jobStatus.getSourcePackageName());
if (timer != null) {
timer.stopTrackingJob(jobStatus);
}
if (jobStatus.isRequestedExpeditedJob()) {
timer = mEJPkgTimers.get(jobStatus.getSourceUserId(), jobStatus.getSourcePackageName());
if (timer != null) {
timer.stopTrackingJob(jobStatus);
}
}
mTopStartedJobs.remove(jobStatus);
}
@Override
@GuardedBy("mLock")
public void maybeStopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob) {
if (jobStatus.clearTrackingController(JobStatus.TRACKING_QUOTA)) {
unprepareFromExecutionLocked(jobStatus);
final int userId = jobStatus.getSourceUserId();
final String pkgName = jobStatus.getSourcePackageName();
ArraySet<JobStatus> jobs = mTrackedJobs.get(userId, pkgName);
if (jobs != null && jobs.remove(jobStatus) && jobs.size() == 0) {
mInQuotaAlarmQueue.removeAlarmForKey(UserPackage.of(userId, pkgName));
}
}
}
@Override
public void onAppRemovedLocked(String packageName, int uid) {
if (packageName == null) {
Slog.wtf(TAG, "Told app removed but given null package name.");
return;
}
clearAppStatsLocked(UserHandle.getUserId(uid), packageName);
if (mService.getPackagesForUidLocked(uid) == null) {
// All packages in the UID have been removed. It's safe to remove things based on
// UID alone.
mForegroundUids.delete(uid);
mTempAllowlistCache.delete(uid);
mTempAllowlistGraceCache.delete(uid);
mTopAppCache.delete(uid);
mTopAppGraceCache.delete(uid);
}
}
@Override
public void onUserAddedLocked(int userId) {
cacheInstallerPackagesLocked(userId);
}
@Override
public void onUserRemovedLocked(int userId) {
mTrackedJobs.delete(userId);
mPkgTimers.delete(userId);
mEJPkgTimers.delete(userId);
mTimingEvents.delete(userId);
mEJTimingSessions.delete(userId);
mInQuotaAlarmQueue.removeAlarmsForUserId(userId);
mExecutionStatsCache.delete(userId);
mEJStats.delete(userId);
mSystemInstallers.remove(userId);
mTopAppTrackers.delete(userId);
}
@Override
public void onBatteryStateChangedLocked() {
handleNewChargingStateLocked();
}
/** Drop all historical stats and stop tracking any active sessions for the specified app. */
public void clearAppStatsLocked(int userId, @NonNull String packageName) {
mTrackedJobs.delete(userId, packageName);
Timer timer = mPkgTimers.delete(userId, packageName);
if (timer != null) {
if (timer.isActive()) {
Slog.e(TAG, "clearAppStats called before Timer turned off.");
timer.dropEverythingLocked();
}
}
timer = mEJPkgTimers.delete(userId, packageName);
if (timer != null) {
if (timer.isActive()) {
Slog.e(TAG, "clearAppStats called before EJ Timer turned off.");
timer.dropEverythingLocked();
}
}
mTimingEvents.delete(userId, packageName);
mEJTimingSessions.delete(userId, packageName);
mInQuotaAlarmQueue.removeAlarmForKey(UserPackage.of(userId, packageName));
mExecutionStatsCache.delete(userId, packageName);
mEJStats.delete(userId, packageName);
mTopAppTrackers.delete(userId, packageName);
}
private void cacheInstallerPackagesLocked(int userId) {
final List<PackageInfo> packages = mContext.getPackageManager()
.getInstalledPackagesAsUser(SYSTEM_APP_CHECK_FLAGS, userId);
for (int i = packages.size() - 1; i >= 0; --i) {
final PackageInfo pi = packages.get(i);
final ApplicationInfo ai = pi.applicationInfo;
final int idx = ArrayUtils.indexOf(
pi.requestedPermissions, Manifest.permission.INSTALL_PACKAGES);
if (idx >= 0 && ai != null && PackageManager.PERMISSION_GRANTED
== mContext.checkPermission(Manifest.permission.INSTALL_PACKAGES, -1, ai.uid)) {
mSystemInstallers.add(UserHandle.getUserId(ai.uid), pi.packageName);
}
}
}
private boolean isUidInForeground(int uid) {
if (UserHandle.isCore(uid)) {
return true;
}
synchronized (mLock) {
return mForegroundUids.get(uid);
}
}
/** @return true if the job was started while the app was in the TOP state. */
private boolean isTopStartedJobLocked(@NonNull final JobStatus jobStatus) {
return mTopStartedJobs.contains(jobStatus);
}
/** Returns the maximum amount of time this job could run for. */
@GuardedBy("mLock")
public long getMaxJobExecutionTimeMsLocked(@NonNull final JobStatus jobStatus) {
if (!jobStatus.shouldTreatAsExpeditedJob()) {
// If quota is currently "free", then the job can run for the full amount of time,
// regardless of bucket (hence using charging instead of isQuotaFreeLocked()).
if (mService.isBatteryCharging()
|| mTopAppCache.get(jobStatus.getSourceUid())
|| isTopStartedJobLocked(jobStatus)
|| isUidInForeground(jobStatus.getSourceUid())) {
return mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS;
}
return getTimeUntilQuotaConsumedLocked(
jobStatus.getSourceUserId(), jobStatus.getSourcePackageName());
}
// Expedited job.
if (mService.isBatteryCharging()) {
return mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS;
}
if (jobStatus.getEffectiveStandbyBucket() == EXEMPTED_INDEX) {
return Math.max(mEJLimitsMs[EXEMPTED_INDEX] / 2,
getTimeUntilEJQuotaConsumedLocked(
jobStatus.getSourceUserId(), jobStatus.getSourcePackageName()));
}
if (mTopAppCache.get(jobStatus.getSourceUid()) || isTopStartedJobLocked(jobStatus)) {
return Math.max(mEJLimitsMs[ACTIVE_INDEX] / 2,
getTimeUntilEJQuotaConsumedLocked(
jobStatus.getSourceUserId(), jobStatus.getSourcePackageName()));
}
if (isUidInForeground(jobStatus.getSourceUid())) {
return Math.max(mEJLimitsMs[WORKING_INDEX] / 2,
getTimeUntilEJQuotaConsumedLocked(
jobStatus.getSourceUserId(), jobStatus.getSourcePackageName()));
}
return getTimeUntilEJQuotaConsumedLocked(
jobStatus.getSourceUserId(), jobStatus.getSourcePackageName());
}
private boolean hasTempAllowlistExemptionLocked(int sourceUid, int standbyBucket,
long nowElapsed) {
if (standbyBucket == RESTRICTED_INDEX || standbyBucket == NEVER_INDEX) {
// Don't let RESTRICTED apps get free quota from the temp allowlist.
// TODO: consider granting the exemption to RESTRICTED apps if the temp allowlist allows
// them to start FGS
return false;
}
final long tempAllowlistGracePeriodEndElapsed = mTempAllowlistGraceCache.get(sourceUid);
return mTempAllowlistCache.get(sourceUid)
|| nowElapsed < tempAllowlistGracePeriodEndElapsed;
}
/** @return true if the job is within expedited job quota. */
@GuardedBy("mLock")
public boolean isWithinEJQuotaLocked(@NonNull final JobStatus jobStatus) {
if (!mIsEnabled) {
return true;
}
if (isQuotaFreeLocked(jobStatus.getEffectiveStandbyBucket())) {
return true;
}
// A job is within quota if one of the following is true:
// 1. the app is currently in the foreground
// 2. the app overall is within its quota
// 3. It's on the temp allowlist (or within the grace period)
if (isTopStartedJobLocked(jobStatus) || isUidInForeground(jobStatus.getSourceUid())) {
return true;
}
final long nowElapsed = sElapsedRealtimeClock.millis();
if (hasTempAllowlistExemptionLocked(jobStatus.getSourceUid(),
jobStatus.getEffectiveStandbyBucket(), nowElapsed)) {
return true;
}
final long topAppGracePeriodEndElapsed = mTopAppGraceCache.get(jobStatus.getSourceUid());
final boolean hasTopAppExemption = mTopAppCache.get(jobStatus.getSourceUid())
|| nowElapsed < topAppGracePeriodEndElapsed;
if (hasTopAppExemption) {
return true;
}
return 0 < getRemainingEJExecutionTimeLocked(
jobStatus.getSourceUserId(), jobStatus.getSourcePackageName());
}
@NonNull
@VisibleForTesting
ShrinkableDebits getEJDebitsLocked(final int userId, @NonNull final String packageName) {
ShrinkableDebits debits = mEJStats.get(userId, packageName);
if (debits == null) {
debits = new ShrinkableDebits(
JobSchedulerService.standbyBucketForPackage(
packageName, userId, sElapsedRealtimeClock.millis())
);
mEJStats.add(userId, packageName, debits);
}
return debits;
}
@VisibleForTesting
boolean isWithinQuotaLocked(@NonNull final JobStatus jobStatus) {
if (!mIsEnabled) {
return true;
}
final int standbyBucket = jobStatus.getEffectiveStandbyBucket();
// A job is within quota if one of the following is true:
// 1. it was started while the app was in the TOP state
// 2. the app is currently in the foreground
// 3. the app overall is within its quota
return jobStatus.shouldTreatAsUserInitiatedJob()
|| isTopStartedJobLocked(jobStatus)
|| isUidInForeground(jobStatus.getSourceUid())
|| isWithinQuotaLocked(
jobStatus.getSourceUserId(), jobStatus.getSourcePackageName(), standbyBucket);
}
@GuardedBy("mLock")
private boolean isQuotaFreeLocked(final int standbyBucket) {
// Quota constraint is not enforced while charging.
if (mService.isBatteryCharging()) {
// Restricted jobs require additional constraints when charging, so don't immediately
// mark quota as free when charging.
return standbyBucket != RESTRICTED_INDEX;
}
return false;
}
@VisibleForTesting
@GuardedBy("mLock")
boolean isWithinQuotaLocked(final int userId, @NonNull final String packageName,
final int standbyBucket) {
if (!mIsEnabled) {
return true;
}
if (standbyBucket == NEVER_INDEX) return false;
if (isQuotaFreeLocked(standbyBucket)) return true;
ExecutionStats stats = getExecutionStatsLocked(userId, packageName, standbyBucket);
// TODO: use a higher minimum remaining time for jobs with MINIMUM priority
return getRemainingExecutionTimeLocked(stats) > 0
&& isUnderJobCountQuotaLocked(stats, standbyBucket)
&& isUnderSessionCountQuotaLocked(stats, standbyBucket);
}
private boolean isUnderJobCountQuotaLocked(@NonNull ExecutionStats stats,
final int standbyBucket) {
final long now = sElapsedRealtimeClock.millis();
final boolean isUnderAllowedTimeQuota =
(stats.jobRateLimitExpirationTimeElapsed <= now
|| stats.jobCountInRateLimitingWindow < mMaxJobCountPerRateLimitingWindow);
return isUnderAllowedTimeQuota
&& stats.bgJobCountInWindow < stats.jobCountLimit;
}
private boolean isUnderSessionCountQuotaLocked(@NonNull ExecutionStats stats,
final int standbyBucket) {
final long now = sElapsedRealtimeClock.millis();
final boolean isUnderAllowedTimeQuota = (stats.sessionRateLimitExpirationTimeElapsed <= now
|| stats.sessionCountInRateLimitingWindow < mMaxSessionCountPerRateLimitingWindow);
return isUnderAllowedTimeQuota
&& stats.sessionCountInWindow < stats.sessionCountLimit;
}
@VisibleForTesting
long getRemainingExecutionTimeLocked(@NonNull final JobStatus jobStatus) {
return getRemainingExecutionTimeLocked(jobStatus.getSourceUserId(),
jobStatus.getSourcePackageName(),
jobStatus.getEffectiveStandbyBucket());
}
@VisibleForTesting
long getRemainingExecutionTimeLocked(final int userId, @NonNull final String packageName) {
final int standbyBucket = JobSchedulerService.standbyBucketForPackage(packageName,
userId, sElapsedRealtimeClock.millis());
return getRemainingExecutionTimeLocked(userId, packageName, standbyBucket);
}
/**
* Returns the amount of time, in milliseconds, that this job has remaining to run based on its
* current standby bucket. Time remaining could be negative if the app was moved from a less
* restricted to a more restricted bucket.
*/
private long getRemainingExecutionTimeLocked(final int userId,
@NonNull final String packageName, final int standbyBucket) {
if (standbyBucket == NEVER_INDEX) {
return 0;
}
return getRemainingExecutionTimeLocked(
getExecutionStatsLocked(userId, packageName, standbyBucket));
}
private long getRemainingExecutionTimeLocked(@NonNull ExecutionStats stats) {
return Math.min(stats.allowedTimePerPeriodMs - stats.executionTimeInWindowMs,
mMaxExecutionTimeMs - stats.executionTimeInMaxPeriodMs);
}
@VisibleForTesting
long getRemainingEJExecutionTimeLocked(final int userId, @NonNull final String packageName) {
ShrinkableDebits quota = getEJDebitsLocked(userId, packageName);
if (quota.getStandbyBucketLocked() == NEVER_INDEX) {
return 0;
}
final long limitMs =
getEJLimitMsLocked(userId, packageName, quota.getStandbyBucketLocked());
long remainingMs = limitMs - quota.getTallyLocked();
// Stale sessions may still be factored into tally. Make sure they're removed.
List<TimedEvent> timingSessions = mEJTimingSessions.get(userId, packageName);
final long nowElapsed = sElapsedRealtimeClock.millis();
final long windowStartTimeElapsed = nowElapsed - mEJLimitWindowSizeMs;
if (timingSessions != null) {
while (timingSessions.size() > 0) {
TimingSession ts = (TimingSession) timingSessions.get(0);
if (ts.endTimeElapsed < windowStartTimeElapsed) {
final long duration = ts.endTimeElapsed - ts.startTimeElapsed;
remainingMs += duration;
quota.transactLocked(-duration);
timingSessions.remove(0);
} else if (ts.startTimeElapsed < windowStartTimeElapsed) {
remainingMs += windowStartTimeElapsed - ts.startTimeElapsed;
break;
} else {
// Fully within the window.
break;
}
}
}
TopAppTimer topAppTimer = mTopAppTrackers.get(userId, packageName);
if (topAppTimer != null && topAppTimer.isActive()) {
remainingMs += topAppTimer.getPendingReward(nowElapsed);
}
Timer timer = mEJPkgTimers.get(userId, packageName);
if (timer == null) {
return remainingMs;
}
return remainingMs - timer.getCurrentDuration(sElapsedRealtimeClock.millis());
}
private long getEJLimitMsLocked(final int userId, @NonNull final String packageName,
final int standbyBucket) {
final long baseLimitMs = mEJLimitsMs[standbyBucket];
if (mSystemInstallers.contains(userId, packageName)) {
return baseLimitMs + mEjLimitAdditionInstallerMs;
}
return baseLimitMs;
}
/**
* Returns the amount of time, in milliseconds, until the package would have reached its
* duration quota, assuming it has a job counting towards its quota the entire time. This takes
* into account any {@link TimingSession TimingSessions} that may roll out of the window as the
* job is running.
*/
@VisibleForTesting
long getTimeUntilQuotaConsumedLocked(final int userId, @NonNull final String packageName) {
final long nowElapsed = sElapsedRealtimeClock.millis();
final int standbyBucket = JobSchedulerService.standbyBucketForPackage(
packageName, userId, nowElapsed);
if (standbyBucket == NEVER_INDEX) {
return 0;
}
List<TimedEvent> events = mTimingEvents.get(userId, packageName);
final ExecutionStats stats = getExecutionStatsLocked(userId, packageName, standbyBucket);
if (events == null || events.size() == 0) {
// Regular ACTIVE case. Since the bucket size equals the allowed time, the app jobs can
// essentially run until they reach the maximum limit.
if (stats.windowSizeMs == mAllowedTimePerPeriodMs[standbyBucket]) {
return mMaxExecutionTimeMs;
}
return mAllowedTimePerPeriodMs[standbyBucket];
}
final long startWindowElapsed = nowElapsed - stats.windowSizeMs;
final long startMaxElapsed = nowElapsed - MAX_PERIOD_MS;
final long allowedTimePerPeriodMs = mAllowedTimePerPeriodMs[standbyBucket];
final long allowedTimeRemainingMs = allowedTimePerPeriodMs - stats.executionTimeInWindowMs;
final long maxExecutionTimeRemainingMs =
mMaxExecutionTimeMs - stats.executionTimeInMaxPeriodMs;
// Regular ACTIVE case. Since the bucket size equals the allowed time, the app jobs can
// essentially run until they reach the maximum limit.
if (stats.windowSizeMs == mAllowedTimePerPeriodMs[standbyBucket]) {
return calculateTimeUntilQuotaConsumedLocked(
events, startMaxElapsed, maxExecutionTimeRemainingMs, false);
}
// Need to check both max time and period time in case one is less than the other.
// For example, max time remaining could be less than bucket time remaining, but sessions
// contributing to the max time remaining could phase out enough that we'd want to use the
// bucket value.
return Math.min(
calculateTimeUntilQuotaConsumedLocked(
events, startMaxElapsed, maxExecutionTimeRemainingMs, false),
calculateTimeUntilQuotaConsumedLocked(
events, startWindowElapsed, allowedTimeRemainingMs, true));
}
/**
* Calculates how much time it will take, in milliseconds, until the quota is fully consumed.
*
* @param windowStartElapsed The start of the window, in the elapsed realtime timebase.
* @param deadSpaceMs How much time can be allowed to count towards the quota
*/
private long calculateTimeUntilQuotaConsumedLocked(@NonNull List<TimedEvent> sessions,
final long windowStartElapsed, long deadSpaceMs, boolean allowQuotaBumps) {
long timeUntilQuotaConsumedMs = 0;
long start = windowStartElapsed;
int numQuotaBumps = 0;
final long quotaBumpWindowStartElapsed =
sElapsedRealtimeClock.millis() - mQuotaBumpWindowSizeMs;
final int numSessions = sessions.size();
if (allowQuotaBumps) {
for (int i = numSessions - 1; i >= 0; --i) {
TimedEvent event = sessions.get(i);
if (event instanceof QuotaBump) {
if (event.getEndTimeElapsed() >= quotaBumpWindowStartElapsed
&& numQuotaBumps++ < mQuotaBumpLimit) {
deadSpaceMs += mQuotaBumpAdditionalDurationMs;
} else {
break;
}
}
}
}
for (int i = 0; i < numSessions; ++i) {
TimedEvent event = sessions.get(i);
if (event instanceof QuotaBump) {
continue;
}
TimingSession session = (TimingSession) event;
if (session.endTimeElapsed < windowStartElapsed) {
// Outside of window. Ignore.
continue;
} else if (session.startTimeElapsed <= windowStartElapsed) {
// Overlapping session. Can extend time by portion of session in window.
timeUntilQuotaConsumedMs += session.endTimeElapsed - windowStartElapsed;
start = session.endTimeElapsed;
} else {
// Completely within the window. Can only consider if there's enough dead space
// to get to the start of the session.
long diff = session.startTimeElapsed - start;
if (diff > deadSpaceMs) {
break;
}
timeUntilQuotaConsumedMs += diff
+ (session.endTimeElapsed - session.startTimeElapsed);
deadSpaceMs -= diff;
start = session.endTimeElapsed;
}
}
// Will be non-zero if the loop didn't look at any sessions.
timeUntilQuotaConsumedMs += deadSpaceMs;
if (timeUntilQuotaConsumedMs > mMaxExecutionTimeMs) {
Slog.wtf(TAG, "Calculated quota consumed time too high: " + timeUntilQuotaConsumedMs);
}
return timeUntilQuotaConsumedMs;
}
/**
* Returns the amount of time, in milliseconds, until the package would have reached its
* expedited job quota, assuming it has a job counting towards the quota the entire time and
* the quota isn't replenished at all in that time.
*/
@VisibleForTesting
long getTimeUntilEJQuotaConsumedLocked(final int userId, @NonNull final String packageName) {
final long remainingExecutionTimeMs =
getRemainingEJExecutionTimeLocked(userId, packageName);
List<TimedEvent> sessions = mEJTimingSessions.get(userId, packageName);
if (sessions == null || sessions.size() == 0) {
return remainingExecutionTimeMs;
}
final long nowElapsed = sElapsedRealtimeClock.millis();
ShrinkableDebits quota = getEJDebitsLocked(userId, packageName);
final long limitMs =
getEJLimitMsLocked(userId, packageName, quota.getStandbyBucketLocked());
final long startWindowElapsed = Math.max(0, nowElapsed - mEJLimitWindowSizeMs);
long remainingDeadSpaceMs = remainingExecutionTimeMs;
// Total time looked at where a session wouldn't be phasing out.
long deadSpaceMs = 0;
// Time regained from sessions phasing out
long phasedOutSessionTimeMs = 0;
for (int i = 0; i < sessions.size(); ++i) {
TimingSession session = (TimingSession) sessions.get(i);
if (session.endTimeElapsed < startWindowElapsed) {
// Edge case where a session became stale in the time between the call to
// getRemainingEJExecutionTimeLocked and this line.
remainingDeadSpaceMs += session.endTimeElapsed - session.startTimeElapsed;
sessions.remove(i);
i--;
} else if (session.startTimeElapsed < startWindowElapsed) {
// Session straddles start of window
phasedOutSessionTimeMs = session.endTimeElapsed - startWindowElapsed;
} else {
// Session fully inside window
final long timeBetweenSessions = session.startTimeElapsed
- (i == 0 ? startWindowElapsed : sessions.get(i - 1).getEndTimeElapsed());
final long usedDeadSpaceMs = Math.min(remainingDeadSpaceMs, timeBetweenSessions);
deadSpaceMs += usedDeadSpaceMs;
if (usedDeadSpaceMs == timeBetweenSessions) {
phasedOutSessionTimeMs += session.endTimeElapsed - session.startTimeElapsed;
}
remainingDeadSpaceMs -= usedDeadSpaceMs;
if (remainingDeadSpaceMs <= 0) {
break;
}
}
}
return Math.min(limitMs, deadSpaceMs + phasedOutSessionTimeMs + remainingDeadSpaceMs);
}
/** Returns the execution stats of the app in the most recent window. */
@VisibleForTesting
@NonNull
ExecutionStats getExecutionStatsLocked(final int userId, @NonNull final String packageName,
final int standbyBucket) {
return getExecutionStatsLocked(userId, packageName, standbyBucket, true);
}
@NonNull
private ExecutionStats getExecutionStatsLocked(final int userId,
@NonNull final String packageName, final int standbyBucket,
final boolean refreshStatsIfOld) {
if (standbyBucket == NEVER_INDEX) {
Slog.wtf(TAG, "getExecutionStatsLocked called for a NEVER app.");
return new ExecutionStats();
}
ExecutionStats[] appStats = mExecutionStatsCache.get(userId, packageName);
if (appStats == null) {
appStats = new ExecutionStats[mBucketPeriodsMs.length];
mExecutionStatsCache.add(userId, packageName, appStats);
}
ExecutionStats stats = appStats[standbyBucket];
if (stats == null) {
stats = new ExecutionStats();
appStats[standbyBucket] = stats;
}
if (refreshStatsIfOld) {
final long bucketAllowedTimeMs = mAllowedTimePerPeriodMs[standbyBucket];
final long bucketWindowSizeMs = mBucketPeriodsMs[standbyBucket];
final int jobCountLimit = mMaxBucketJobCounts[standbyBucket];
final int sessionCountLimit = mMaxBucketSessionCounts[standbyBucket];
Timer timer = mPkgTimers.get(userId, packageName);
if ((timer != null && timer.isActive())
|| stats.expirationTimeElapsed <= sElapsedRealtimeClock.millis()
|| stats.allowedTimePerPeriodMs != bucketAllowedTimeMs
|| stats.windowSizeMs != bucketWindowSizeMs
|| stats.jobCountLimit != jobCountLimit
|| stats.sessionCountLimit != sessionCountLimit) {
// The stats are no longer valid.
stats.allowedTimePerPeriodMs = bucketAllowedTimeMs;
stats.windowSizeMs = bucketWindowSizeMs;
stats.jobCountLimit = jobCountLimit;
stats.sessionCountLimit = sessionCountLimit;
updateExecutionStatsLocked(userId, packageName, stats);
}
}
return stats;
}
@VisibleForTesting
void updateExecutionStatsLocked(final int userId, @NonNull final String packageName,
@NonNull ExecutionStats stats) {
stats.executionTimeInWindowMs = 0;
stats.bgJobCountInWindow = 0;
stats.executionTimeInMaxPeriodMs = 0;
stats.bgJobCountInMaxPeriod = 0;
stats.sessionCountInWindow = 0;
if (stats.jobCountLimit == 0 || stats.sessionCountLimit == 0) {
// App won't be in quota until configuration changes.
stats.inQuotaTimeElapsed = Long.MAX_VALUE;
} else {
stats.inQuotaTimeElapsed = 0;
}
final long allowedTimeIntoQuotaMs = stats.allowedTimePerPeriodMs - mQuotaBufferMs;
Timer timer = mPkgTimers.get(userId, packageName);
final long nowElapsed = sElapsedRealtimeClock.millis();
stats.expirationTimeElapsed = nowElapsed + MAX_PERIOD_MS;
if (timer != null && timer.isActive()) {
// Exclude active sessions from the session count so that new jobs aren't prevented
// from starting due to an app hitting the session limit.
stats.executionTimeInWindowMs =
stats.executionTimeInMaxPeriodMs = timer.getCurrentDuration(nowElapsed);
stats.bgJobCountInWindow = stats.bgJobCountInMaxPeriod = timer.getBgJobCount();
// If the timer is active, the value will be stale at the next method call, so
// invalidate now.
stats.expirationTimeElapsed = nowElapsed;
if (stats.executionTimeInWindowMs >= allowedTimeIntoQuotaMs) {
stats.inQuotaTimeElapsed = Math.max(stats.inQuotaTimeElapsed,
nowElapsed - allowedTimeIntoQuotaMs + stats.windowSizeMs);
}
if (stats.executionTimeInMaxPeriodMs >= mMaxExecutionTimeIntoQuotaMs) {
final long inQuotaTime = nowElapsed - mMaxExecutionTimeIntoQuotaMs + MAX_PERIOD_MS;
stats.inQuotaTimeElapsed = Math.max(stats.inQuotaTimeElapsed, inQuotaTime);
}
if (stats.bgJobCountInWindow >= stats.jobCountLimit) {
final long inQuotaTime = nowElapsed + stats.windowSizeMs;
stats.inQuotaTimeElapsed = Math.max(stats.inQuotaTimeElapsed, inQuotaTime);
}
}
List<TimedEvent> events = mTimingEvents.get(userId, packageName);
if (events == null || events.size() == 0) {
return;
}
final long startWindowElapsed = nowElapsed - stats.windowSizeMs;
final long startMaxElapsed = nowElapsed - MAX_PERIOD_MS;
int sessionCountInWindow = 0;
int numQuotaBumps = 0;
final long quotaBumpWindowStartElapsed = nowElapsed - mQuotaBumpWindowSizeMs;
// The minimum time between the start time and the beginning of the events that were
// looked at --> how much time the stats will be valid for.
long emptyTimeMs = Long.MAX_VALUE;
// Sessions are non-overlapping and in order of occurrence, so iterating backwards will get
// the most recent ones.
final int loopStart = events.size() - 1;
// Process QuotaBumps first to ensure the limits are properly adjusted.
for (int i = loopStart; i >= 0; --i) {
TimedEvent event = events.get(i);
if (event.getEndTimeElapsed() < quotaBumpWindowStartElapsed
|| numQuotaBumps >= mQuotaBumpLimit) {
break;
}
if (event instanceof QuotaBump) {
stats.allowedTimePerPeriodMs += mQuotaBumpAdditionalDurationMs;
stats.jobCountLimit += mQuotaBumpAdditionalJobCount;
stats.sessionCountLimit += mQuotaBumpAdditionalSessionCount;
emptyTimeMs = Math.min(emptyTimeMs,
event.getEndTimeElapsed() - quotaBumpWindowStartElapsed);
numQuotaBumps++;
}
}
TimingSession lastSeenTimingSession = null;
for (int i = loopStart; i >= 0; --i) {
TimedEvent event = events.get(i);
if (event instanceof QuotaBump) {
continue;
}
TimingSession session = (TimingSession) event;
// Window management.
if (startWindowElapsed < session.endTimeElapsed) {
final long start;
if (startWindowElapsed < session.startTimeElapsed) {
start = session.startTimeElapsed;
emptyTimeMs =
Math.min(emptyTimeMs, session.startTimeElapsed - startWindowElapsed);
} else {
// The session started before the window but ended within the window. Only
// include the portion that was within the window.
start = startWindowElapsed;
emptyTimeMs = 0;
}
stats.executionTimeInWindowMs += session.endTimeElapsed - start;
stats.bgJobCountInWindow += session.bgJobCount;
if (stats.executionTimeInWindowMs >= allowedTimeIntoQuotaMs) {
stats.inQuotaTimeElapsed = Math.max(stats.inQuotaTimeElapsed,
start + stats.executionTimeInWindowMs - allowedTimeIntoQuotaMs
+ stats.windowSizeMs);
}
if (stats.bgJobCountInWindow >= stats.jobCountLimit) {
final long inQuotaTime = session.endTimeElapsed + stats.windowSizeMs;
stats.inQuotaTimeElapsed = Math.max(stats.inQuotaTimeElapsed, inQuotaTime);
}
// Coalesce sessions if they are very close to each other in time
boolean shouldCoalesce = lastSeenTimingSession != null
&& lastSeenTimingSession.startTimeElapsed - session.endTimeElapsed
<= mTimingSessionCoalescingDurationMs;
if (!shouldCoalesce) {
sessionCountInWindow++;
if (sessionCountInWindow >= stats.sessionCountLimit) {
final long inQuotaTime = session.endTimeElapsed + stats.windowSizeMs;
stats.inQuotaTimeElapsed = Math.max(stats.inQuotaTimeElapsed, inQuotaTime);
}
}
}
// Max period check.
if (startMaxElapsed < session.startTimeElapsed) {
stats.executionTimeInMaxPeriodMs +=
session.endTimeElapsed - session.startTimeElapsed;
stats.bgJobCountInMaxPeriod += session.bgJobCount;
emptyTimeMs = Math.min(emptyTimeMs, session.startTimeElapsed - startMaxElapsed);
if (stats.executionTimeInMaxPeriodMs >= mMaxExecutionTimeIntoQuotaMs) {
stats.inQuotaTimeElapsed = Math.max(stats.inQuotaTimeElapsed,
session.startTimeElapsed + stats.executionTimeInMaxPeriodMs
- mMaxExecutionTimeIntoQuotaMs + MAX_PERIOD_MS);
}
} else if (startMaxElapsed < session.endTimeElapsed) {
// The session started before the window but ended within the window. Only include
// the portion that was within the window.
stats.executionTimeInMaxPeriodMs += session.endTimeElapsed - startMaxElapsed;
stats.bgJobCountInMaxPeriod += session.bgJobCount;
emptyTimeMs = 0;
if (stats.executionTimeInMaxPeriodMs >= mMaxExecutionTimeIntoQuotaMs) {
stats.inQuotaTimeElapsed = Math.max(stats.inQuotaTimeElapsed,
startMaxElapsed + stats.executionTimeInMaxPeriodMs
- mMaxExecutionTimeIntoQuotaMs + MAX_PERIOD_MS);
}
} else {
// This session ended before the window. No point in going any further.
break;
}
lastSeenTimingSession = session;
}
stats.expirationTimeElapsed = nowElapsed + emptyTimeMs;
stats.sessionCountInWindow = sessionCountInWindow;
}
/** Invalidate ExecutionStats for all apps. */
@VisibleForTesting
void invalidateAllExecutionStatsLocked() {
final long nowElapsed = sElapsedRealtimeClock.millis();
mExecutionStatsCache.forEach((appStats) -> {
if (appStats != null) {
for (int i = 0; i < appStats.length; ++i) {
ExecutionStats stats = appStats[i];
if (stats != null) {
stats.expirationTimeElapsed = nowElapsed;
}
}
}
});
}
@VisibleForTesting
void invalidateAllExecutionStatsLocked(final int userId,
@NonNull final String packageName) {
ExecutionStats[] appStats = mExecutionStatsCache.get(userId, packageName);
if (appStats != null) {
final long nowElapsed = sElapsedRealtimeClock.millis();
for (int i = 0; i < appStats.length; ++i) {
ExecutionStats stats = appStats[i];
if (stats != null) {
stats.expirationTimeElapsed = nowElapsed;
}
}
}
}
@VisibleForTesting
void incrementJobCountLocked(final int userId, @NonNull final String packageName, int count) {
final long now = sElapsedRealtimeClock.millis();
ExecutionStats[] appStats = mExecutionStatsCache.get(userId, packageName);
if (appStats == null) {
appStats = new ExecutionStats[mBucketPeriodsMs.length];
mExecutionStatsCache.add(userId, packageName, appStats);
}
for (int i = 0; i < appStats.length; ++i) {
ExecutionStats stats = appStats[i];
if (stats == null) {
stats = new ExecutionStats();
appStats[i] = stats;
}
if (stats.jobRateLimitExpirationTimeElapsed <= now) {
stats.jobRateLimitExpirationTimeElapsed = now + mRateLimitingWindowMs;
stats.jobCountInRateLimitingWindow = 0;
}
stats.jobCountInRateLimitingWindow += count;
}
}
private void incrementTimingSessionCountLocked(final int userId,
@NonNull final String packageName) {
final long now = sElapsedRealtimeClock.millis();
ExecutionStats[] appStats = mExecutionStatsCache.get(userId, packageName);
if (appStats == null) {
appStats = new ExecutionStats[mBucketPeriodsMs.length];
mExecutionStatsCache.add(userId, packageName, appStats);
}
for (int i = 0; i < appStats.length; ++i) {
ExecutionStats stats = appStats[i];
if (stats == null) {
stats = new ExecutionStats();
appStats[i] = stats;
}
if (stats.sessionRateLimitExpirationTimeElapsed <= now) {
stats.sessionRateLimitExpirationTimeElapsed = now + mRateLimitingWindowMs;
stats.sessionCountInRateLimitingWindow = 0;
}
stats.sessionCountInRateLimitingWindow++;
}
}
@VisibleForTesting
void saveTimingSession(final int userId, @NonNull final String packageName,
@NonNull final TimingSession session, boolean isExpedited) {
saveTimingSession(userId, packageName, session, isExpedited, 0);
}
private void saveTimingSession(final int userId, @NonNull final String packageName,
@NonNull final TimingSession session, boolean isExpedited, long debitAdjustment) {
synchronized (mLock) {
final SparseArrayMap<String, List<TimedEvent>> sessionMap =
isExpedited ? mEJTimingSessions : mTimingEvents;
List<TimedEvent> sessions = sessionMap.get(userId, packageName);
if (sessions == null) {
sessions = new ArrayList<>();
sessionMap.add(userId, packageName, sessions);
}
sessions.add(session);
if (isExpedited) {
final ShrinkableDebits quota = getEJDebitsLocked(userId, packageName);
quota.transactLocked(session.endTimeElapsed - session.startTimeElapsed
+ debitAdjustment);
} else {
// Adding a new session means that the current stats are now incorrect.
invalidateAllExecutionStatsLocked(userId, packageName);
maybeScheduleCleanupAlarmLocked();
}
}
}
private void grantRewardForInstantEvent(
final int userId, @NonNull final String packageName, final long credit) {
if (credit == 0) {
return;
}
synchronized (mLock) {
final long nowElapsed = sElapsedRealtimeClock.millis();
final ShrinkableDebits quota = getEJDebitsLocked(userId, packageName);
if (transactQuotaLocked(userId, packageName, nowElapsed, quota, credit)) {
mStateChangedListener.onControllerStateChanged(
maybeUpdateConstraintForPkgLocked(nowElapsed, userId, packageName));
}
}
}
private boolean transactQuotaLocked(final int userId, @NonNull final String packageName,
final long nowElapsed, @NonNull ShrinkableDebits debits, final long credit) {
final long oldTally = debits.getTallyLocked();
final long leftover = debits.transactLocked(-credit);
if (DEBUG) {
Slog.d(TAG, "debits overflowed by " + leftover);
}
boolean changed = oldTally != debits.getTallyLocked();
if (leftover != 0) {
// Only adjust timer if its active.
final Timer ejTimer = mEJPkgTimers.get(userId, packageName);
if (ejTimer != null && ejTimer.isActive()) {
ejTimer.updateDebitAdjustment(nowElapsed, leftover);
changed = true;
}
}
return changed;
}
private final class EarliestEndTimeFunctor implements Consumer<List<TimedEvent>> {
public long earliestEndElapsed = Long.MAX_VALUE;
@Override
public void accept(List<TimedEvent> events) {
if (events != null && events.size() > 0) {
earliestEndElapsed =
Math.min(earliestEndElapsed, events.get(0).getEndTimeElapsed());
}
}
void reset() {
earliestEndElapsed = Long.MAX_VALUE;
}
}
private final EarliestEndTimeFunctor mEarliestEndTimeFunctor = new EarliestEndTimeFunctor();
/** Schedule a cleanup alarm if necessary and there isn't already one scheduled. */
@VisibleForTesting
void maybeScheduleCleanupAlarmLocked() {
final long nowElapsed = sElapsedRealtimeClock.millis();
if (mNextCleanupTimeElapsed > nowElapsed) {
// There's already an alarm scheduled. Just stick with that one. There's no way we'll
// end up scheduling an earlier alarm.
if (DEBUG) {
Slog.v(TAG, "Not scheduling cleanup since there's already one at "
+ mNextCleanupTimeElapsed
+ " (in " + (mNextCleanupTimeElapsed - nowElapsed) + "ms)");
}
return;
}
mEarliestEndTimeFunctor.reset();
mTimingEvents.forEach(mEarliestEndTimeFunctor);
mEJTimingSessions.forEach(mEarliestEndTimeFunctor);
final long earliestEndElapsed = mEarliestEndTimeFunctor.earliestEndElapsed;
if (earliestEndElapsed == Long.MAX_VALUE) {
// Couldn't find a good time to clean up. Maybe this was called after we deleted all
// timing sessions.
if (DEBUG) {
Slog.d(TAG, "Didn't find a time to schedule cleanup");
}
return;
}
// Need to keep sessions for all apps up to the max period, regardless of their current
// standby bucket.
long nextCleanupElapsed = earliestEndElapsed + MAX_PERIOD_MS;
if (nextCleanupElapsed - mNextCleanupTimeElapsed <= 10 * MINUTE_IN_MILLIS) {
// No need to clean up too often. Delay the alarm if the next cleanup would be too soon
// after it.
nextCleanupElapsed = mNextCleanupTimeElapsed + 10 * MINUTE_IN_MILLIS;
}
mNextCleanupTimeElapsed = nextCleanupElapsed;
mAlarmManager.set(AlarmManager.ELAPSED_REALTIME, nextCleanupElapsed, ALARM_TAG_CLEANUP,
mSessionCleanupAlarmListener, mHandler);
if (DEBUG) {
Slog.d(TAG, "Scheduled next cleanup for " + mNextCleanupTimeElapsed);
}
}
private class TimerChargingUpdateFunctor implements Consumer<Timer> {
private long mNowElapsed;
private boolean mIsCharging;
private void setStatus(long nowElapsed, boolean isCharging) {
mNowElapsed = nowElapsed;
mIsCharging = isCharging;
}
@Override
public void accept(Timer timer) {
if (JobSchedulerService.standbyBucketForPackage(timer.mPkg.packageName,
timer.mPkg.userId, mNowElapsed) != RESTRICTED_INDEX) {
// Restricted jobs need additional constraints even when charging, so don't
// immediately say that quota is free.
timer.onStateChangedLocked(mNowElapsed, mIsCharging);
}
}
}
private final TimerChargingUpdateFunctor
mTimerChargingUpdateFunctor = new TimerChargingUpdateFunctor();
private void handleNewChargingStateLocked() {
mTimerChargingUpdateFunctor.setStatus(sElapsedRealtimeClock.millis(),
mService.isBatteryCharging());
if (DEBUG) {
Slog.d(TAG, "handleNewChargingStateLocked: " + mService.isBatteryCharging());
}
// Deal with Timers first.
mEJPkgTimers.forEach(mTimerChargingUpdateFunctor);
mPkgTimers.forEach(mTimerChargingUpdateFunctor);
// Now update jobs out of band so broadcast processing can proceed.
AppSchedulingModuleThread.getHandler().post(() -> {
synchronized (mLock) {
maybeUpdateAllConstraintsLocked();
}
});
}
private void maybeUpdateAllConstraintsLocked() {
final ArraySet<JobStatus> changedJobs = new ArraySet<>();
final long nowElapsed = sElapsedRealtimeClock.millis();
for (int u = 0; u < mTrackedJobs.numMaps(); ++u) {
final int userId = mTrackedJobs.keyAt(u);
for (int p = 0; p < mTrackedJobs.numElementsForKey(userId); ++p) {
final String packageName = mTrackedJobs.keyAt(u, p);
changedJobs.addAll(
maybeUpdateConstraintForPkgLocked(nowElapsed, userId, packageName));
}
}
if (changedJobs.size() > 0) {
mStateChangedListener.onControllerStateChanged(changedJobs);
}
}
/**
* Update the CONSTRAINT_WITHIN_QUOTA bit for all of the Jobs for a given package.
*
* @return the set of jobs whose status changed
*/
@NonNull
private ArraySet<JobStatus> maybeUpdateConstraintForPkgLocked(final long nowElapsed,
final int userId, @NonNull final String packageName) {
ArraySet<JobStatus> jobs = mTrackedJobs.get(userId, packageName);
final ArraySet<JobStatus> changedJobs = new ArraySet<>();
if (jobs == null || jobs.size() == 0) {
return changedJobs;
}
// Quota is the same for all jobs within a package.
final int realStandbyBucket = jobs.valueAt(0).getStandbyBucket();
final boolean realInQuota = isWithinQuotaLocked(userId, packageName, realStandbyBucket);
boolean outOfEJQuota = false;
for (int i = jobs.size() - 1; i >= 0; --i) {
final JobStatus js = jobs.valueAt(i);
final boolean isWithinEJQuota =
js.isRequestedExpeditedJob() && isWithinEJQuotaLocked(js);
if (isTopStartedJobLocked(js)) {
// Job was started while the app was in the TOP state so we should allow it to
// finish.
if (js.setQuotaConstraintSatisfied(nowElapsed, true)) {
changedJobs.add(js);
}
} else if (realStandbyBucket != EXEMPTED_INDEX && realStandbyBucket != ACTIVE_INDEX
&& realStandbyBucket == js.getEffectiveStandbyBucket()) {
// An app in the ACTIVE bucket may be out of quota while the job could be in quota
// for some reason. Therefore, avoid setting the real value here and check each job
// individually.
if (setConstraintSatisfied(js, nowElapsed, realInQuota, isWithinEJQuota)) {
changedJobs.add(js);
}
} else {
// This job is somehow exempted. Need to determine its own quota status.
if (setConstraintSatisfied(js, nowElapsed,
isWithinQuotaLocked(js), isWithinEJQuota)) {
changedJobs.add(js);
}
}
if (js.isRequestedExpeditedJob()) {
if (setExpeditedQuotaApproved(js, nowElapsed, isWithinEJQuota)) {
changedJobs.add(js);
}
outOfEJQuota |= !isWithinEJQuota;
}
}
if (!realInQuota || outOfEJQuota) {
// Don't want to use the effective standby bucket here since that bump the bucket to
// ACTIVE for one of the jobs, which doesn't help with other jobs that aren't
// exempted.
maybeScheduleStartAlarmLocked(userId, packageName, realStandbyBucket);
} else {
mInQuotaAlarmQueue.removeAlarmForKey(UserPackage.of(userId, packageName));
}
return changedJobs;
}
private class UidConstraintUpdater implements Consumer<JobStatus> {
private final SparseArrayMap<String, Integer> mToScheduleStartAlarms =
new SparseArrayMap<>();
public final ArraySet<JobStatus> changedJobs = new ArraySet<>();
long mUpdateTimeElapsed = 0;
void prepare() {
mUpdateTimeElapsed = sElapsedRealtimeClock.millis();
changedJobs.clear();
}
@Override
public void accept(JobStatus jobStatus) {
final boolean isWithinEJQuota;
if (jobStatus.isRequestedExpeditedJob()) {
isWithinEJQuota = isWithinEJQuotaLocked(jobStatus);
} else {
isWithinEJQuota = false;
}
if (setConstraintSatisfied(jobStatus, mUpdateTimeElapsed,
isWithinQuotaLocked(jobStatus), isWithinEJQuota)) {
changedJobs.add(jobStatus);
}
if (setExpeditedQuotaApproved(jobStatus, mUpdateTimeElapsed, isWithinEJQuota)) {
changedJobs.add(jobStatus);
}
final int userId = jobStatus.getSourceUserId();
final String packageName = jobStatus.getSourcePackageName();
final int realStandbyBucket = jobStatus.getStandbyBucket();
if (isWithinEJQuota
&& isWithinQuotaLocked(userId, packageName, realStandbyBucket)) {
// TODO(141645789): we probably shouldn't cancel the alarm until we've verified
// that all jobs for the userId-package are within quota.
mInQuotaAlarmQueue.removeAlarmForKey(UserPackage.of(userId, packageName));
} else {
mToScheduleStartAlarms.add(userId, packageName, realStandbyBucket);
}
}
void postProcess() {
for (int u = 0; u < mToScheduleStartAlarms.numMaps(); ++u) {
final int userId = mToScheduleStartAlarms.keyAt(u);
for (int p = 0; p < mToScheduleStartAlarms.numElementsForKey(userId); ++p) {
final String packageName = mToScheduleStartAlarms.keyAt(u, p);
final int standbyBucket = mToScheduleStartAlarms.get(userId, packageName);
maybeScheduleStartAlarmLocked(userId, packageName, standbyBucket);
}
}
}
void reset() {
mToScheduleStartAlarms.clear();
}
}
private final UidConstraintUpdater mUpdateUidConstraints = new UidConstraintUpdater();
@GuardedBy("mLock")
@NonNull
private ArraySet<JobStatus> maybeUpdateConstraintForUidLocked(final int uid) {
mUpdateUidConstraints.prepare();
mService.getJobStore().forEachJobForSourceUid(uid, mUpdateUidConstraints);
mUpdateUidConstraints.postProcess();
mUpdateUidConstraints.reset();
return mUpdateUidConstraints.changedJobs;
}
/**
* Maybe schedule a non-wakeup alarm for the next time this package will have quota to run
* again. This should only be called if the package is already out of quota.
*/
@VisibleForTesting
void maybeScheduleStartAlarmLocked(final int userId, @NonNull final String packageName,
final int standbyBucket) {
if (standbyBucket == NEVER_INDEX) {
return;
}
ArraySet<JobStatus> jobs = mTrackedJobs.get(userId, packageName);
if (jobs == null || jobs.size() == 0) {
Slog.e(TAG, "maybeScheduleStartAlarmLocked called for "
+ packageToString(userId, packageName) + " that has no jobs");
mInQuotaAlarmQueue.removeAlarmForKey(UserPackage.of(userId, packageName));
return;
}
ExecutionStats stats = getExecutionStatsLocked(userId, packageName, standbyBucket);
final boolean isUnderJobCountQuota = isUnderJobCountQuotaLocked(stats, standbyBucket);
final boolean isUnderTimingSessionCountQuota = isUnderSessionCountQuotaLocked(stats,
standbyBucket);
final long remainingEJQuota = getRemainingEJExecutionTimeLocked(userId, packageName);
final boolean inRegularQuota =
stats.executionTimeInWindowMs < mAllowedTimePerPeriodMs[standbyBucket]
&& stats.executionTimeInMaxPeriodMs < mMaxExecutionTimeMs
&& isUnderJobCountQuota
&& isUnderTimingSessionCountQuota;
if (inRegularQuota && remainingEJQuota > 0) {
// Already in quota. Why was this method called?
if (DEBUG) {
Slog.e(TAG, "maybeScheduleStartAlarmLocked called for "
+ packageToString(userId, packageName)
+ " even though it already has "
+ getRemainingExecutionTimeLocked(userId, packageName, standbyBucket)
+ "ms in its quota.");
}
mInQuotaAlarmQueue.removeAlarmForKey(UserPackage.of(userId, packageName));
mHandler.obtainMessage(MSG_CHECK_PACKAGE, userId, 0, packageName).sendToTarget();
return;
}
long inRegularQuotaTimeElapsed = Long.MAX_VALUE;
long inEJQuotaTimeElapsed = Long.MAX_VALUE;
if (!inRegularQuota) {
// The time this app will have quota again.
long inQuotaTimeElapsed = stats.inQuotaTimeElapsed;
if (!isUnderJobCountQuota && stats.bgJobCountInWindow < stats.jobCountLimit) {
// App hit the rate limit.
inQuotaTimeElapsed =
Math.max(inQuotaTimeElapsed, stats.jobRateLimitExpirationTimeElapsed);
}
if (!isUnderTimingSessionCountQuota
&& stats.sessionCountInWindow < stats.sessionCountLimit) {
// App hit the rate limit.
inQuotaTimeElapsed =
Math.max(inQuotaTimeElapsed, stats.sessionRateLimitExpirationTimeElapsed);
}
inRegularQuotaTimeElapsed = inQuotaTimeElapsed;
}
if (remainingEJQuota <= 0) {
final long limitMs =
getEJLimitMsLocked(userId, packageName, standbyBucket) - mQuotaBufferMs;
long sumMs = 0;
final Timer ejTimer = mEJPkgTimers.get(userId, packageName);
if (ejTimer != null && ejTimer.isActive()) {
final long nowElapsed = sElapsedRealtimeClock.millis();
sumMs += ejTimer.getCurrentDuration(nowElapsed);
if (sumMs >= limitMs) {
inEJQuotaTimeElapsed = (nowElapsed - limitMs) + mEJLimitWindowSizeMs;
}
}
List<TimedEvent> timingSessions = mEJTimingSessions.get(userId, packageName);
if (timingSessions != null) {
for (int i = timingSessions.size() - 1; i >= 0; --i) {
TimingSession ts = (TimingSession) timingSessions.get(i);
final long durationMs = ts.endTimeElapsed - ts.startTimeElapsed;
sumMs += durationMs;
if (sumMs >= limitMs) {
inEJQuotaTimeElapsed =
ts.startTimeElapsed + (sumMs - limitMs) + mEJLimitWindowSizeMs;
break;
}
}
} else if ((ejTimer == null || !ejTimer.isActive()) && inRegularQuota) {
// In some strange cases, an app may end be in the NEVER bucket but could have run
// some regular jobs. This results in no EJ timing sessions and QC having a bad
// time.
Slog.wtf(TAG, packageToString(userId, packageName)
+ " has 0 EJ quota without running anything");
return;
}
}
long inQuotaTimeElapsed = Math.min(inRegularQuotaTimeElapsed, inEJQuotaTimeElapsed);
if (inQuotaTimeElapsed <= sElapsedRealtimeClock.millis()) {
final long nowElapsed = sElapsedRealtimeClock.millis();
Slog.wtf(TAG,
"In quota time is " + (nowElapsed - inQuotaTimeElapsed) + "ms old. Now="
+ nowElapsed + ", inQuotaTime=" + inQuotaTimeElapsed + ": " + stats);
inQuotaTimeElapsed = nowElapsed + 5 * MINUTE_IN_MILLIS;
}
mInQuotaAlarmQueue.addAlarm(UserPackage.of(userId, packageName), inQuotaTimeElapsed);
}
private boolean setConstraintSatisfied(@NonNull JobStatus jobStatus, long nowElapsed,
boolean isWithinQuota, boolean isWithinEjQuota) {
final boolean isSatisfied;
if (jobStatus.startedAsExpeditedJob) {
// If the job started as an EJ, then we should only consider EJ quota for the constraint
// satisfaction.
isSatisfied = isWithinEjQuota;
} else if (mService.isCurrentlyRunningLocked(jobStatus)) {
// Job is running but didn't start as an EJ, so only the regular quota should be
// considered.
isSatisfied = isWithinQuota;
} else {
isSatisfied = isWithinEjQuota || isWithinQuota;
}
if (!isSatisfied && jobStatus.getWhenStandbyDeferred() == 0) {
// Mark that the job is being deferred due to buckets.
jobStatus.setWhenStandbyDeferred(nowElapsed);
}
return jobStatus.setQuotaConstraintSatisfied(nowElapsed, isSatisfied);
}
/**
* If the satisfaction changes, this will tell connectivity & background jobs controller to
* also re-evaluate their state.
*/
private boolean setExpeditedQuotaApproved(@NonNull JobStatus jobStatus, long nowElapsed,
boolean isWithinQuota) {
if (jobStatus.setExpeditedJobQuotaApproved(nowElapsed, isWithinQuota)) {
mBackgroundJobsController.evaluateStateLocked(jobStatus);
mConnectivityController.evaluateStateLocked(jobStatus);
if (isWithinQuota && jobStatus.isReady()) {
mStateChangedListener.onRunJobNow(jobStatus);
}
return true;
}
return false;
}
@VisibleForTesting
interface TimedEvent {
long getEndTimeElapsed();
void dump(IndentingPrintWriter pw);
}
@VisibleForTesting
static final class TimingSession implements TimedEvent {
// Start timestamp in elapsed realtime timebase.
public final long startTimeElapsed;
// End timestamp in elapsed realtime timebase.
public final long endTimeElapsed;
// How many background jobs ran during this session.
public final int bgJobCount;
private final int mHashCode;
TimingSession(long startElapsed, long endElapsed, int bgJobCount) {
this.startTimeElapsed = startElapsed;
this.endTimeElapsed = endElapsed;
this.bgJobCount = bgJobCount;
int hashCode = 0;
hashCode = 31 * hashCode + hashLong(startTimeElapsed);
hashCode = 31 * hashCode + hashLong(endTimeElapsed);
hashCode = 31 * hashCode + bgJobCount;
mHashCode = hashCode;
}
@Override
public long getEndTimeElapsed() {
return endTimeElapsed;
}
@Override
public String toString() {
return "TimingSession{" + startTimeElapsed + "->" + endTimeElapsed + ", " + bgJobCount
+ "}";
}
@Override
public boolean equals(Object obj) {
if (obj instanceof TimingSession) {
TimingSession other = (TimingSession) obj;
return startTimeElapsed == other.startTimeElapsed
&& endTimeElapsed == other.endTimeElapsed
&& bgJobCount == other.bgJobCount;
} else {
return false;
}
}
@Override
public int hashCode() {
return mHashCode;
}
@Override
public void dump(IndentingPrintWriter pw) {
pw.print(startTimeElapsed);
pw.print(" -> ");
pw.print(endTimeElapsed);
pw.print(" (");
pw.print(endTimeElapsed - startTimeElapsed);
pw.print("), ");
pw.print(bgJobCount);
pw.print(" bg jobs.");
pw.println();
}
public void dump(@NonNull ProtoOutputStream proto, long fieldId) {
final long token = proto.start(fieldId);
proto.write(StateControllerProto.QuotaController.TimingSession.START_TIME_ELAPSED,
startTimeElapsed);
proto.write(StateControllerProto.QuotaController.TimingSession.END_TIME_ELAPSED,
endTimeElapsed);
proto.write(StateControllerProto.QuotaController.TimingSession.BG_JOB_COUNT,
bgJobCount);
proto.end(token);
}
}
@VisibleForTesting
static final class QuotaBump implements TimedEvent {
// Event timestamp in elapsed realtime timebase.
public final long eventTimeElapsed;
QuotaBump(long eventElapsed) {
this.eventTimeElapsed = eventElapsed;
}
@Override
public long getEndTimeElapsed() {
return eventTimeElapsed;
}
@Override
public void dump(IndentingPrintWriter pw) {
pw.print("Quota bump @ ");
pw.print(eventTimeElapsed);
pw.println();
}
}
@VisibleForTesting
static final class ShrinkableDebits {
/** The amount of quota remaining. Can be negative if limit changes. */
private long mDebitTally;
private int mStandbyBucket;
ShrinkableDebits(int standbyBucket) {
mDebitTally = 0;
mStandbyBucket = standbyBucket;
}
long getTallyLocked() {
return mDebitTally;
}
/**
* Negative if the tally should decrease (therefore increasing available quota);
* or positive if the tally should increase (therefore decreasing available quota).
*/
long transactLocked(final long amount) {
final long leftover = amount < 0 && Math.abs(amount) > mDebitTally
? mDebitTally + amount : 0;
mDebitTally = Math.max(0, mDebitTally + amount);
return leftover;
}
void setStandbyBucketLocked(int standbyBucket) {
mStandbyBucket = standbyBucket;
}
int getStandbyBucketLocked() {
return mStandbyBucket;
}
@Override
public String toString() {
return "ShrinkableDebits { debit tally: "
+ mDebitTally + ", bucket: " + mStandbyBucket
+ " }";
}
void dumpLocked(IndentingPrintWriter pw) {
pw.println(toString());
}
}
private final class Timer {
private final UserPackage mPkg;
private final int mUid;
private final boolean mRegularJobTimer;
// List of jobs currently running for this app that started when the app wasn't in the
// foreground.
private final ArraySet<JobStatus> mRunningBgJobs = new ArraySet<>();
private long mStartTimeElapsed;
private int mBgJobCount;
private long mDebitAdjustment;
Timer(int uid, int userId, String packageName, boolean regularJobTimer) {
mPkg = UserPackage.of(userId, packageName);
mUid = uid;
mRegularJobTimer = regularJobTimer;
}
void startTrackingJobLocked(@NonNull JobStatus jobStatus) {
if (jobStatus.shouldTreatAsUserInitiatedJob()) {
if (DEBUG) {
Slog.v(TAG, "Timer ignoring " + jobStatus.toShortString()
+ " because it's user-initiated");
}
return;
}
if (isTopStartedJobLocked(jobStatus)) {
// We intentionally don't pay attention to fg state changes after a TOP job has
// started.
if (DEBUG) {
Slog.v(TAG,
"Timer ignoring " + jobStatus.toShortString() + " because isTop");
}
return;
}
if (DEBUG) {
Slog.v(TAG, "Starting to track " + jobStatus.toShortString());
}
// Always maintain list of running jobs, even when quota is free.
if (mRunningBgJobs.add(jobStatus) && shouldTrackLocked()) {
mBgJobCount++;
if (mRegularJobTimer) {
incrementJobCountLocked(mPkg.userId, mPkg.packageName, 1);
}
if (mRunningBgJobs.size() == 1) {
// Started tracking the first job.
mStartTimeElapsed = sElapsedRealtimeClock.millis();
mDebitAdjustment = 0;
if (mRegularJobTimer) {
// Starting the timer means that all cached execution stats are now
// incorrect.
invalidateAllExecutionStatsLocked(mPkg.userId, mPkg.packageName);
}
scheduleCutoff();
}
}
}
void stopTrackingJob(@NonNull JobStatus jobStatus) {
if (DEBUG) {
Slog.v(TAG, "Stopping tracking of " + jobStatus.toShortString());
}
synchronized (mLock) {
if (mRunningBgJobs.size() == 0) {
// maybeStopTrackingJobLocked can be called when an app cancels a job, so a
// timer may not be running when it's asked to stop tracking a job.
if (DEBUG) {
Slog.d(TAG, "Timer isn't tracking any jobs but still told to stop");
}
return;
}
final long nowElapsed = sElapsedRealtimeClock.millis();
final int standbyBucket = JobSchedulerService.standbyBucketForPackage(
mPkg.packageName, mPkg.userId, nowElapsed);
if (mRunningBgJobs.remove(jobStatus) && mRunningBgJobs.size() == 0
&& !isQuotaFreeLocked(standbyBucket)) {
emitSessionLocked(nowElapsed);
cancelCutoff();
}
}
}
void updateDebitAdjustment(long nowElapsed, long debit) {
// Make sure we don't have a credit larger than the expected session.
mDebitAdjustment = Math.max(mDebitAdjustment + debit, mStartTimeElapsed - nowElapsed);
}
/**
* Stops tracking all jobs and cancels any pending alarms. This should only be called if
* the Timer is not going to be used anymore.
*/
void dropEverythingLocked() {
mRunningBgJobs.clear();
cancelCutoff();
}
@GuardedBy("mLock")
private void emitSessionLocked(long nowElapsed) {
if (mBgJobCount <= 0) {
// Nothing to emit.
return;
}
TimingSession ts = new TimingSession(mStartTimeElapsed, nowElapsed, mBgJobCount);
saveTimingSession(mPkg.userId, mPkg.packageName, ts, !mRegularJobTimer,
mDebitAdjustment);
mBgJobCount = 0;
// Don't reset the tracked jobs list as we need to keep tracking the current number
// of jobs.
// However, cancel the currently scheduled cutoff since it's not currently useful.
cancelCutoff();
if (mRegularJobTimer) {
incrementTimingSessionCountLocked(mPkg.userId, mPkg.packageName);
}
}
/**
* Returns true if the Timer is actively tracking, as opposed to passively ref counting
* during charging.
*/
public boolean isActive() {
synchronized (mLock) {
return mBgJobCount > 0;
}
}
boolean isRunning(JobStatus jobStatus) {
return mRunningBgJobs.contains(jobStatus);
}
long getCurrentDuration(long nowElapsed) {
synchronized (mLock) {
return !isActive() ? 0 : nowElapsed - mStartTimeElapsed + mDebitAdjustment;
}
}
int getBgJobCount() {
synchronized (mLock) {
return mBgJobCount;
}
}
@GuardedBy("mLock")
private boolean shouldTrackLocked() {
final long nowElapsed = sElapsedRealtimeClock.millis();
final int standbyBucket = JobSchedulerService.standbyBucketForPackage(mPkg.packageName,
mPkg.userId, nowElapsed);
final boolean hasTempAllowlistExemption = !mRegularJobTimer
&& hasTempAllowlistExemptionLocked(mUid, standbyBucket, nowElapsed);
final long topAppGracePeriodEndElapsed = mTopAppGraceCache.get(mUid);
final boolean hasTopAppExemption = !mRegularJobTimer
&& (mTopAppCache.get(mUid) || nowElapsed < topAppGracePeriodEndElapsed);
if (DEBUG) {
Slog.d(TAG, "quotaFree=" + isQuotaFreeLocked(standbyBucket)
+ " isFG=" + mForegroundUids.get(mUid)
+ " tempEx=" + hasTempAllowlistExemption
+ " topEx=" + hasTopAppExemption);
}
return !isQuotaFreeLocked(standbyBucket)
&& !mForegroundUids.get(mUid) && !hasTempAllowlistExemption
&& !hasTopAppExemption;
}
void onStateChangedLocked(long nowElapsed, boolean isQuotaFree) {
if (isQuotaFree) {
emitSessionLocked(nowElapsed);
} else if (!isActive() && shouldTrackLocked()) {
// Start timing from unplug.
if (mRunningBgJobs.size() > 0) {
mStartTimeElapsed = nowElapsed;
mDebitAdjustment = 0;
// NOTE: this does have the unfortunate consequence that if the device is
// repeatedly plugged in and unplugged, or an app changes foreground state
// very frequently, the job count for a package may be artificially high.
mBgJobCount = mRunningBgJobs.size();
if (mRegularJobTimer) {
incrementJobCountLocked(mPkg.userId, mPkg.packageName, mBgJobCount);
// Starting the timer means that all cached execution stats are now
// incorrect.
invalidateAllExecutionStatsLocked(mPkg.userId, mPkg.packageName);
}
// Schedule cutoff since we're now actively tracking for quotas again.
scheduleCutoff();
}
}
}
void rescheduleCutoff() {
cancelCutoff();
scheduleCutoff();
}
private void scheduleCutoff() {
// Each package can only be in one standby bucket, so we only need to have one
// message per timer. We only need to reschedule when restarting timer or when
// standby bucket changes.
synchronized (mLock) {
if (!isActive()) {
return;
}
Message msg = mHandler.obtainMessage(
mRegularJobTimer ? MSG_REACHED_QUOTA : MSG_REACHED_EJ_QUOTA, mPkg);
final long timeRemainingMs = mRegularJobTimer
? getTimeUntilQuotaConsumedLocked(mPkg.userId, mPkg.packageName)
: getTimeUntilEJQuotaConsumedLocked(mPkg.userId, mPkg.packageName);
if (DEBUG) {
Slog.i(TAG,
(mRegularJobTimer ? "Regular job" : "EJ") + " for " + mPkg + " has "
+ timeRemainingMs + "ms left.");
}
// If the job was running the entire time, then the system would be up, so it's
// fine to use uptime millis for these messages.
mHandler.sendMessageDelayed(msg, timeRemainingMs);
}
}
private void cancelCutoff() {
mHandler.removeMessages(
mRegularJobTimer ? MSG_REACHED_QUOTA : MSG_REACHED_EJ_QUOTA, mPkg);
}
public void dump(IndentingPrintWriter pw, Predicate<JobStatus> predicate) {
pw.print("Timer<");
pw.print(mRegularJobTimer ? "REG" : "EJ");
pw.print(">{");
pw.print(mPkg);
pw.print("} ");
if (isActive()) {
pw.print("started at ");
pw.print(mStartTimeElapsed);
pw.print(" (");
pw.print(sElapsedRealtimeClock.millis() - mStartTimeElapsed);
pw.print("ms ago)");
} else {
pw.print("NOT active");
}
pw.print(", ");
pw.print(mBgJobCount);
pw.print(" running bg jobs");
if (!mRegularJobTimer) {
pw.print(" (debit adj=");
pw.print(mDebitAdjustment);
pw.print(")");
}
pw.println();
pw.increaseIndent();
for (int i = 0; i < mRunningBgJobs.size(); i++) {
JobStatus js = mRunningBgJobs.valueAt(i);
if (predicate.test(js)) {
pw.println(js.toShortString());
}
}
pw.decreaseIndent();
}
public void dump(ProtoOutputStream proto, long fieldId, Predicate<JobStatus> predicate) {
final long token = proto.start(fieldId);
proto.write(StateControllerProto.QuotaController.Timer.IS_ACTIVE, isActive());
proto.write(StateControllerProto.QuotaController.Timer.START_TIME_ELAPSED,
mStartTimeElapsed);
proto.write(StateControllerProto.QuotaController.Timer.BG_JOB_COUNT, mBgJobCount);
for (int i = 0; i < mRunningBgJobs.size(); i++) {
JobStatus js = mRunningBgJobs.valueAt(i);
if (predicate.test(js)) {
js.writeToShortProto(proto,
StateControllerProto.QuotaController.Timer.RUNNING_JOBS);
}
}
proto.end(token);
}
}
private final class TopAppTimer {
private final UserPackage mPkg;
// List of jobs currently running for this app that started when the app wasn't in the
// foreground.
private final SparseArray<UsageEvents.Event> mActivities = new SparseArray<>();
private long mStartTimeElapsed;
TopAppTimer(int userId, String packageName) {
mPkg = UserPackage.of(userId, packageName);
}
private int calculateTimeChunks(final long nowElapsed) {
final long totalTopTimeMs = nowElapsed - mStartTimeElapsed;
int numTimeChunks = (int) (totalTopTimeMs / mEJTopAppTimeChunkSizeMs);
final long remainderMs = totalTopTimeMs % mEJTopAppTimeChunkSizeMs;
if (remainderMs >= SECOND_IN_MILLIS) {
// "Round up"
numTimeChunks++;
}
return numTimeChunks;
}
long getPendingReward(final long nowElapsed) {
return mEJRewardTopAppMs * calculateTimeChunks(nowElapsed);
}
void processEventLocked(@NonNull UsageEvents.Event event) {
final long nowElapsed = sElapsedRealtimeClock.millis();
switch (event.getEventType()) {
case UsageEvents.Event.ACTIVITY_RESUMED:
if (mActivities.size() == 0) {
mStartTimeElapsed = nowElapsed;
}
mActivities.put(event.mInstanceId, event);
break;
case UsageEvents.Event.ACTIVITY_PAUSED:
case UsageEvents.Event.ACTIVITY_STOPPED:
case UsageEvents.Event.ACTIVITY_DESTROYED:
final UsageEvents.Event existingEvent =
mActivities.removeReturnOld(event.mInstanceId);
if (existingEvent != null && mActivities.size() == 0) {
final long pendingReward = getPendingReward(nowElapsed);
if (DEBUG) {
Slog.d(TAG, "Crediting " + mPkg + " " + pendingReward + "ms"
+ " for " + calculateTimeChunks(nowElapsed) + " time chunks");
}
final ShrinkableDebits debits =
getEJDebitsLocked(mPkg.userId, mPkg.packageName);
if (transactQuotaLocked(mPkg.userId, mPkg.packageName,
nowElapsed, debits, pendingReward)) {
mStateChangedListener.onControllerStateChanged(
maybeUpdateConstraintForPkgLocked(nowElapsed,
mPkg.userId, mPkg.packageName));
}
}
break;
}
}
boolean isActive() {
synchronized (mLock) {
return mActivities.size() > 0;
}
}
public void dump(IndentingPrintWriter pw) {
pw.print("TopAppTimer{");
pw.print(mPkg);
pw.print("} ");
if (isActive()) {
pw.print("started at ");
pw.print(mStartTimeElapsed);
pw.print(" (");
pw.print(sElapsedRealtimeClock.millis() - mStartTimeElapsed);
pw.print("ms ago)");
} else {
pw.print("NOT active");
}
pw.println();
pw.increaseIndent();
for (int i = 0; i < mActivities.size(); i++) {
UsageEvents.Event event = mActivities.valueAt(i);
pw.println(event.getClassName());
}
pw.decreaseIndent();
}
public void dump(ProtoOutputStream proto, long fieldId) {
final long token = proto.start(fieldId);
proto.write(StateControllerProto.QuotaController.TopAppTimer.IS_ACTIVE, isActive());
proto.write(StateControllerProto.QuotaController.TopAppTimer.START_TIME_ELAPSED,
mStartTimeElapsed);
proto.write(StateControllerProto.QuotaController.TopAppTimer.ACTIVITY_COUNT,
mActivities.size());
// TODO: maybe dump activities/events
proto.end(token);
}
}
/**
* Tracking of app assignments to standby buckets
*/
final class StandbyTracker extends AppIdleStateChangeListener {
@Override
public void onAppIdleStateChanged(final String packageName, final @UserIdInt int userId,
boolean idle, int bucket, int reason) {
// Update job bookkeeping out of band.
AppSchedulingModuleThread.getHandler().post(() -> {
final int bucketIndex = JobSchedulerService.standbyBucketToBucketIndex(bucket);
updateStandbyBucket(userId, packageName, bucketIndex);
});
}
@Override
public void triggerTemporaryQuotaBump(String packageName, @UserIdInt int userId) {
synchronized (mLock) {
List<TimedEvent> events = mTimingEvents.get(userId, packageName);
if (events == null || events.size() == 0) {
// If the app hasn't run any jobs, there's no point giving it a quota bump.
return;
}
events.add(new QuotaBump(sElapsedRealtimeClock.millis()));
invalidateAllExecutionStatsLocked(userId, packageName);
}
// Update jobs out of band.
mHandler.obtainMessage(MSG_CHECK_PACKAGE, userId, 0, packageName).sendToTarget();
}
}
@VisibleForTesting
void updateStandbyBucket(
final int userId, final @NonNull String packageName, final int bucketIndex) {
if (DEBUG) {
Slog.i(TAG, "Moving pkg " + packageToString(userId, packageName)
+ " to bucketIndex " + bucketIndex);
}
List<JobStatus> restrictedChanges = new ArrayList<>();
synchronized (mLock) {
ShrinkableDebits debits = mEJStats.get(userId, packageName);
if (debits != null) {
debits.setStandbyBucketLocked(bucketIndex);
}
ArraySet<JobStatus> jobs = mTrackedJobs.get(userId, packageName);
if (jobs == null || jobs.size() == 0) {
// Nothing further to do.
return;
}
for (int i = jobs.size() - 1; i >= 0; i--) {
JobStatus js = jobs.valueAt(i);
// Effective standby bucket can change after this in some situations so
// use the real bucket so that the job is tracked by the controllers.
if ((bucketIndex == RESTRICTED_INDEX || js.getStandbyBucket() == RESTRICTED_INDEX)
&& bucketIndex != js.getStandbyBucket()) {
restrictedChanges.add(js);
}
js.setStandbyBucket(bucketIndex);
}
Timer timer = mPkgTimers.get(userId, packageName);
if (timer != null && timer.isActive()) {
timer.rescheduleCutoff();
}
timer = mEJPkgTimers.get(userId, packageName);
if (timer != null && timer.isActive()) {
timer.rescheduleCutoff();
}
mStateChangedListener.onControllerStateChanged(
maybeUpdateConstraintForPkgLocked(
sElapsedRealtimeClock.millis(), userId, packageName));
}
if (restrictedChanges.size() > 0) {
mStateChangedListener.onRestrictedBucketChanged(restrictedChanges);
}
}
final class UsageEventTracker implements UsageEventListener {
/**
* Callback to inform listeners of a new event.
*/
@Override
public void onUsageEvent(int userId, @NonNull UsageEvents.Event event) {
mHandler.obtainMessage(MSG_PROCESS_USAGE_EVENT, userId, 0, event).sendToTarget();
}
}
final class TempAllowlistTracker implements PowerAllowlistInternal.TempAllowlistChangeListener {
@Override
public void onAppAdded(int uid) {
synchronized (mLock) {
final long nowElapsed = sElapsedRealtimeClock.millis();
mTempAllowlistCache.put(uid, true);
final ArraySet<String> packages = mService.getPackagesForUidLocked(uid);
if (packages != null) {
final int userId = UserHandle.getUserId(uid);
for (int i = packages.size() - 1; i >= 0; --i) {
Timer t = mEJPkgTimers.get(userId, packages.valueAt(i));
if (t != null) {
t.onStateChangedLocked(nowElapsed, true);
}
}
final ArraySet<JobStatus> changedJobs = maybeUpdateConstraintForUidLocked(uid);
if (changedJobs.size() > 0) {
mStateChangedListener.onControllerStateChanged(changedJobs);
}
}
}
}
@Override
public void onAppRemoved(int uid) {
synchronized (mLock) {
final long nowElapsed = sElapsedRealtimeClock.millis();
final long endElapsed = nowElapsed + mEJGracePeriodTempAllowlistMs;
mTempAllowlistCache.delete(uid);
mTempAllowlistGraceCache.put(uid, endElapsed);
Message msg = mHandler.obtainMessage(MSG_END_GRACE_PERIOD, uid, 0);
mHandler.sendMessageDelayed(msg, mEJGracePeriodTempAllowlistMs);
}
}
}
private static final class TimedEventTooOldPredicate implements Predicate<TimedEvent> {
private long mNowElapsed;
private void updateNow() {
mNowElapsed = sElapsedRealtimeClock.millis();
}
@Override
public boolean test(TimedEvent ts) {
return ts.getEndTimeElapsed() <= mNowElapsed - MAX_PERIOD_MS;
}
}
private final TimedEventTooOldPredicate mTimedEventTooOld = new TimedEventTooOldPredicate();
private final Consumer<List<TimedEvent>> mDeleteOldEventsFunctor = events -> {
if (events != null) {
// Remove everything older than MAX_PERIOD_MS time ago.
events.removeIf(mTimedEventTooOld);
}
};
@VisibleForTesting
void deleteObsoleteSessionsLocked() {
mTimedEventTooOld.updateNow();
// Regular sessions
mTimingEvents.forEach(mDeleteOldEventsFunctor);
// EJ sessions
for (int uIdx = 0; uIdx < mEJTimingSessions.numMaps(); ++uIdx) {
final int userId = mEJTimingSessions.keyAt(uIdx);
for (int pIdx = 0; pIdx < mEJTimingSessions.numElementsForKey(userId); ++pIdx) {
final String packageName = mEJTimingSessions.keyAt(uIdx, pIdx);
final ShrinkableDebits debits = getEJDebitsLocked(userId, packageName);
final List<TimedEvent> sessions = mEJTimingSessions.get(userId, packageName);
if (sessions == null) {
continue;
}
while (sessions.size() > 0) {
final TimingSession ts = (TimingSession) sessions.get(0);
if (mTimedEventTooOld.test(ts)) {
// Stale sessions may still be factored into tally. Remove them.
final long duration = ts.endTimeElapsed - ts.startTimeElapsed;
debits.transactLocked(-duration);
sessions.remove(0);
} else {
break;
}
}
}
}
}
private class QcHandler extends Handler {
QcHandler(Looper looper) {
super(looper);
}
@Override
public void handleMessage(Message msg) {
synchronized (mLock) {
switch (msg.what) {
case MSG_REACHED_QUOTA: {
UserPackage pkg = (UserPackage) msg.obj;
if (DEBUG) {
Slog.d(TAG, "Checking if " + pkg + " has reached its quota.");
}
long timeRemainingMs = getRemainingExecutionTimeLocked(pkg.userId,
pkg.packageName);
if (timeRemainingMs <= 50) {
// Less than 50 milliseconds left. Start process of shutting down jobs.
if (DEBUG) Slog.d(TAG, pkg + " has reached its quota.");
mStateChangedListener.onControllerStateChanged(
maybeUpdateConstraintForPkgLocked(
sElapsedRealtimeClock.millis(),
pkg.userId, pkg.packageName));
} else {
// This could potentially happen if an old session phases out while a
// job is currently running.
// Reschedule message
Message rescheduleMsg = obtainMessage(MSG_REACHED_QUOTA, pkg);
timeRemainingMs = getTimeUntilQuotaConsumedLocked(pkg.userId,
pkg.packageName);
if (DEBUG) {
Slog.d(TAG, pkg + " has " + timeRemainingMs + "ms left.");
}
sendMessageDelayed(rescheduleMsg, timeRemainingMs);
}
break;
}
case MSG_REACHED_EJ_QUOTA: {
UserPackage pkg = (UserPackage) msg.obj;
if (DEBUG) {
Slog.d(TAG, "Checking if " + pkg + " has reached its EJ quota.");
}
long timeRemainingMs = getRemainingEJExecutionTimeLocked(
pkg.userId, pkg.packageName);
if (timeRemainingMs <= 0) {
if (DEBUG) Slog.d(TAG, pkg + " has reached its EJ quota.");
mStateChangedListener.onControllerStateChanged(
maybeUpdateConstraintForPkgLocked(
sElapsedRealtimeClock.millis(),
pkg.userId, pkg.packageName));
} else {
// This could potentially happen if an old session phases out while a
// job is currently running.
// Reschedule message
Message rescheduleMsg = obtainMessage(MSG_REACHED_EJ_QUOTA, pkg);
timeRemainingMs = getTimeUntilEJQuotaConsumedLocked(
pkg.userId, pkg.packageName);
if (DEBUG) {
Slog.d(TAG, pkg + " has " + timeRemainingMs + "ms left for EJ");
}
sendMessageDelayed(rescheduleMsg, timeRemainingMs);
}
break;
}
case MSG_CLEAN_UP_SESSIONS:
if (DEBUG) {
Slog.d(TAG, "Cleaning up timing sessions.");
}
deleteObsoleteSessionsLocked();
maybeScheduleCleanupAlarmLocked();
break;
case MSG_CHECK_PACKAGE: {
String packageName = (String) msg.obj;
int userId = msg.arg1;
if (DEBUG) {
Slog.d(TAG, "Checking pkg " + packageToString(userId, packageName));
}
mStateChangedListener.onControllerStateChanged(
maybeUpdateConstraintForPkgLocked(sElapsedRealtimeClock.millis(),
userId, packageName));
break;
}
case MSG_UID_PROCESS_STATE_CHANGED: {
final int uid = msg.arg1;
final int procState = msg.arg2;
final int userId = UserHandle.getUserId(uid);
final long nowElapsed = sElapsedRealtimeClock.millis();
synchronized (mLock) {
boolean isQuotaFree;
if (procState <= ActivityManager.PROCESS_STATE_TOP) {
mTopAppCache.put(uid, true);
mTopAppGraceCache.delete(uid);
if (mForegroundUids.get(uid)) {
// Went from FGS to TOP. We don't need to reprocess timers or
// jobs.
break;
}
mForegroundUids.put(uid, true);
isQuotaFree = true;
} else {
final boolean reprocess;
if (procState <= ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE) {
reprocess = !mForegroundUids.get(uid);
mForegroundUids.put(uid, true);
isQuotaFree = true;
} else {
reprocess = true;
mForegroundUids.delete(uid);
isQuotaFree = false;
}
if (mTopAppCache.get(uid)) {
final long endElapsed = nowElapsed + mEJGracePeriodTopAppMs;
mTopAppCache.delete(uid);
mTopAppGraceCache.put(uid, endElapsed);
sendMessageDelayed(obtainMessage(MSG_END_GRACE_PERIOD, uid, 0),
mEJGracePeriodTopAppMs);
}
if (!reprocess) {
break;
}
}
// Update Timers first.
if (mPkgTimers.indexOfKey(userId) >= 0
|| mEJPkgTimers.indexOfKey(userId) >= 0) {
final ArraySet<String> packages =
mService.getPackagesForUidLocked(uid);
if (packages != null) {
for (int i = packages.size() - 1; i >= 0; --i) {
Timer t = mEJPkgTimers.get(userId, packages.valueAt(i));
if (t != null) {
t.onStateChangedLocked(nowElapsed, isQuotaFree);
}
t = mPkgTimers.get(userId, packages.valueAt(i));
if (t != null) {
t.onStateChangedLocked(nowElapsed, isQuotaFree);
}
}
}
}
final ArraySet<JobStatus> changedJobs =
maybeUpdateConstraintForUidLocked(uid);
if (changedJobs.size() > 0) {
mStateChangedListener.onControllerStateChanged(changedJobs);
}
}
break;
}
case MSG_PROCESS_USAGE_EVENT: {
final int userId = msg.arg1;
final UsageEvents.Event event = (UsageEvents.Event) msg.obj;
final String pkgName = event.getPackageName();
if (DEBUG) {
Slog.d(TAG, "Processing event " + event.getEventType()
+ " for " + packageToString(userId, pkgName));
}
switch (event.getEventType()) {
case UsageEvents.Event.ACTIVITY_RESUMED:
case UsageEvents.Event.ACTIVITY_PAUSED:
case UsageEvents.Event.ACTIVITY_STOPPED:
case UsageEvents.Event.ACTIVITY_DESTROYED:
synchronized (mLock) {
TopAppTimer timer = mTopAppTrackers.get(userId, pkgName);
if (timer == null) {
timer = new TopAppTimer(userId, pkgName);
mTopAppTrackers.add(userId, pkgName, timer);
}
timer.processEventLocked(event);
}
break;
case UsageEvents.Event.USER_INTERACTION:
case UsageEvents.Event.CHOOSER_ACTION:
case UsageEvents.Event.NOTIFICATION_INTERRUPTION:
// Don't need to include SHORTCUT_INVOCATION. The app will be
// launched through it (if it's not already on top).
grantRewardForInstantEvent(
userId, pkgName, mEJRewardInteractionMs);
break;
case UsageEvents.Event.NOTIFICATION_SEEN:
// Intentionally don't give too much for notification seen.
// Interactions will award more.
grantRewardForInstantEvent(
userId, pkgName, mEJRewardNotificationSeenMs);
break;
}
break;
}
case MSG_END_GRACE_PERIOD: {
final int uid = msg.arg1;
synchronized (mLock) {
if (mTempAllowlistCache.get(uid) || mTopAppCache.get(uid)) {
// App added back to the temp allowlist or became top again
// during the grace period.
if (DEBUG) {
Slog.d(TAG, uid + " is still allowed");
}
break;
}
final long nowElapsed = sElapsedRealtimeClock.millis();
if (nowElapsed < mTempAllowlistGraceCache.get(uid)
|| nowElapsed < mTopAppGraceCache.get(uid)) {
// One of the grace periods is still in effect.
if (DEBUG) {
Slog.d(TAG, uid + " is still in grace period");
}
break;
}
if (DEBUG) {
Slog.d(TAG, uid + " is now out of grace period");
}
mTempAllowlistGraceCache.delete(uid);
mTopAppGraceCache.delete(uid);
final ArraySet<String> packages = mService.getPackagesForUidLocked(uid);
if (packages != null) {
final int userId = UserHandle.getUserId(uid);
for (int i = packages.size() - 1; i >= 0; --i) {
Timer t = mEJPkgTimers.get(userId, packages.valueAt(i));
if (t != null) {
t.onStateChangedLocked(nowElapsed, false);
}
}
final ArraySet<JobStatus> changedJobs =
maybeUpdateConstraintForUidLocked(uid);
if (changedJobs.size() > 0) {
mStateChangedListener.onControllerStateChanged(changedJobs);
}
}
}
break;
}
}
}
}
}
/** Track when UPTCs are expected to come back into quota. */
private class InQuotaAlarmQueue extends AlarmQueue<UserPackage> {
private InQuotaAlarmQueue(Context context, Looper looper) {
super(context, looper, ALARM_TAG_QUOTA_CHECK, "In quota", false,
QcConstants.DEFAULT_MIN_QUOTA_CHECK_DELAY_MS);
}
@Override
protected boolean isForUser(@NonNull UserPackage key, int userId) {
return key.userId == userId;
}
@Override
protected void processExpiredAlarms(@NonNull ArraySet<UserPackage> expired) {
for (int i = 0; i < expired.size(); ++i) {
UserPackage p = expired.valueAt(i);
mHandler.obtainMessage(MSG_CHECK_PACKAGE, p.userId, 0, p.packageName)
.sendToTarget();
}
}
}
@Override
public void prepareForUpdatedConstantsLocked() {
mQcConstants.mShouldReevaluateConstraints = false;
mQcConstants.mRateLimitingConstantsUpdated = false;
mQcConstants.mExecutionPeriodConstantsUpdated = false;
mQcConstants.mEJLimitConstantsUpdated = false;
mQcConstants.mQuotaBumpConstantsUpdated = false;
}
@Override
public void processConstantLocked(DeviceConfig.Properties properties, String key) {
mQcConstants.processConstantLocked(properties, key);
}
@Override
public void onConstantsUpdatedLocked() {
if (mQcConstants.mShouldReevaluateConstraints || mIsEnabled == mConstants.USE_TARE_POLICY) {
mIsEnabled = !mConstants.USE_TARE_POLICY;
// Update job bookkeeping out of band.
AppSchedulingModuleThread.getHandler().post(() -> {
synchronized (mLock) {
invalidateAllExecutionStatsLocked();
maybeUpdateAllConstraintsLocked();
}
});
}
}
@VisibleForTesting
class QcConstants {
private boolean mShouldReevaluateConstraints = false;
private boolean mRateLimitingConstantsUpdated = false;
private boolean mExecutionPeriodConstantsUpdated = false;
private boolean mEJLimitConstantsUpdated = false;
private boolean mQuotaBumpConstantsUpdated = false;
/** Prefix to use with all constant keys in order to "sub-namespace" the keys. */
private static final String QC_CONSTANT_PREFIX = "qc_";
/**
* Previously used keys:
* * allowed_time_per_period_ms -- No longer used after splitting by bucket
*/
@VisibleForTesting
static final String KEY_ALLOWED_TIME_PER_PERIOD_EXEMPTED_MS =
QC_CONSTANT_PREFIX + "allowed_time_per_period_exempted_ms";
@VisibleForTesting
static final String KEY_ALLOWED_TIME_PER_PERIOD_ACTIVE_MS =
QC_CONSTANT_PREFIX + "allowed_time_per_period_active_ms";
@VisibleForTesting
static final String KEY_ALLOWED_TIME_PER_PERIOD_WORKING_MS =
QC_CONSTANT_PREFIX + "allowed_time_per_period_working_ms";
@VisibleForTesting
static final String KEY_ALLOWED_TIME_PER_PERIOD_FREQUENT_MS =
QC_CONSTANT_PREFIX + "allowed_time_per_period_frequent_ms";
@VisibleForTesting
static final String KEY_ALLOWED_TIME_PER_PERIOD_RARE_MS =
QC_CONSTANT_PREFIX + "allowed_time_per_period_rare_ms";
@VisibleForTesting
static final String KEY_ALLOWED_TIME_PER_PERIOD_RESTRICTED_MS =
QC_CONSTANT_PREFIX + "allowed_time_per_period_restricted_ms";
@VisibleForTesting
static final String KEY_IN_QUOTA_BUFFER_MS =
QC_CONSTANT_PREFIX + "in_quota_buffer_ms";
@VisibleForTesting
static final String KEY_WINDOW_SIZE_EXEMPTED_MS =
QC_CONSTANT_PREFIX + "window_size_exempted_ms";
@VisibleForTesting
static final String KEY_WINDOW_SIZE_ACTIVE_MS =
QC_CONSTANT_PREFIX + "window_size_active_ms";
@VisibleForTesting
static final String KEY_WINDOW_SIZE_WORKING_MS =
QC_CONSTANT_PREFIX + "window_size_working_ms";
@VisibleForTesting
static final String KEY_WINDOW_SIZE_FREQUENT_MS =
QC_CONSTANT_PREFIX + "window_size_frequent_ms";
@VisibleForTesting
static final String KEY_WINDOW_SIZE_RARE_MS =
QC_CONSTANT_PREFIX + "window_size_rare_ms";
@VisibleForTesting
static final String KEY_WINDOW_SIZE_RESTRICTED_MS =
QC_CONSTANT_PREFIX + "window_size_restricted_ms";
@VisibleForTesting
static final String KEY_MAX_EXECUTION_TIME_MS =
QC_CONSTANT_PREFIX + "max_execution_time_ms";
@VisibleForTesting
static final String KEY_MAX_JOB_COUNT_EXEMPTED =
QC_CONSTANT_PREFIX + "max_job_count_exempted";
@VisibleForTesting
static final String KEY_MAX_JOB_COUNT_ACTIVE =
QC_CONSTANT_PREFIX + "max_job_count_active";
@VisibleForTesting
static final String KEY_MAX_JOB_COUNT_WORKING =
QC_CONSTANT_PREFIX + "max_job_count_working";
@VisibleForTesting
static final String KEY_MAX_JOB_COUNT_FREQUENT =
QC_CONSTANT_PREFIX + "max_job_count_frequent";
@VisibleForTesting
static final String KEY_MAX_JOB_COUNT_RARE =
QC_CONSTANT_PREFIX + "max_job_count_rare";
@VisibleForTesting
static final String KEY_MAX_JOB_COUNT_RESTRICTED =
QC_CONSTANT_PREFIX + "max_job_count_restricted";
@VisibleForTesting
static final String KEY_RATE_LIMITING_WINDOW_MS =
QC_CONSTANT_PREFIX + "rate_limiting_window_ms";
@VisibleForTesting
static final String KEY_MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW =
QC_CONSTANT_PREFIX + "max_job_count_per_rate_limiting_window";
@VisibleForTesting
static final String KEY_MAX_SESSION_COUNT_EXEMPTED =
QC_CONSTANT_PREFIX + "max_session_count_exempted";
@VisibleForTesting
static final String KEY_MAX_SESSION_COUNT_ACTIVE =
QC_CONSTANT_PREFIX + "max_session_count_active";
@VisibleForTesting
static final String KEY_MAX_SESSION_COUNT_WORKING =
QC_CONSTANT_PREFIX + "max_session_count_working";
@VisibleForTesting
static final String KEY_MAX_SESSION_COUNT_FREQUENT =
QC_CONSTANT_PREFIX + "max_session_count_frequent";
@VisibleForTesting
static final String KEY_MAX_SESSION_COUNT_RARE =
QC_CONSTANT_PREFIX + "max_session_count_rare";
@VisibleForTesting
static final String KEY_MAX_SESSION_COUNT_RESTRICTED =
QC_CONSTANT_PREFIX + "max_session_count_restricted";
@VisibleForTesting
static final String KEY_MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW =
QC_CONSTANT_PREFIX + "max_session_count_per_rate_limiting_window";
@VisibleForTesting
static final String KEY_TIMING_SESSION_COALESCING_DURATION_MS =
QC_CONSTANT_PREFIX + "timing_session_coalescing_duration_ms";
@VisibleForTesting
static final String KEY_MIN_QUOTA_CHECK_DELAY_MS =
QC_CONSTANT_PREFIX + "min_quota_check_delay_ms";
@VisibleForTesting
static final String KEY_EJ_LIMIT_EXEMPTED_MS =
QC_CONSTANT_PREFIX + "ej_limit_exempted_ms";
@VisibleForTesting
static final String KEY_EJ_LIMIT_ACTIVE_MS =
QC_CONSTANT_PREFIX + "ej_limit_active_ms";
@VisibleForTesting
static final String KEY_EJ_LIMIT_WORKING_MS =
QC_CONSTANT_PREFIX + "ej_limit_working_ms";
@VisibleForTesting
static final String KEY_EJ_LIMIT_FREQUENT_MS =
QC_CONSTANT_PREFIX + "ej_limit_frequent_ms";
@VisibleForTesting
static final String KEY_EJ_LIMIT_RARE_MS =
QC_CONSTANT_PREFIX + "ej_limit_rare_ms";
@VisibleForTesting
static final String KEY_EJ_LIMIT_RESTRICTED_MS =
QC_CONSTANT_PREFIX + "ej_limit_restricted_ms";
@VisibleForTesting
static final String KEY_EJ_LIMIT_ADDITION_SPECIAL_MS =
QC_CONSTANT_PREFIX + "ej_limit_addition_special_ms";
@VisibleForTesting
static final String KEY_EJ_LIMIT_ADDITION_INSTALLER_MS =
QC_CONSTANT_PREFIX + "ej_limit_addition_installer_ms";
@VisibleForTesting
static final String KEY_EJ_WINDOW_SIZE_MS =
QC_CONSTANT_PREFIX + "ej_window_size_ms";
@VisibleForTesting
static final String KEY_EJ_TOP_APP_TIME_CHUNK_SIZE_MS =
QC_CONSTANT_PREFIX + "ej_top_app_time_chunk_size_ms";
@VisibleForTesting
static final String KEY_EJ_REWARD_TOP_APP_MS =
QC_CONSTANT_PREFIX + "ej_reward_top_app_ms";
@VisibleForTesting
static final String KEY_EJ_REWARD_INTERACTION_MS =
QC_CONSTANT_PREFIX + "ej_reward_interaction_ms";
@VisibleForTesting
static final String KEY_EJ_REWARD_NOTIFICATION_SEEN_MS =
QC_CONSTANT_PREFIX + "ej_reward_notification_seen_ms";
@VisibleForTesting
static final String KEY_EJ_GRACE_PERIOD_TEMP_ALLOWLIST_MS =
QC_CONSTANT_PREFIX + "ej_grace_period_temp_allowlist_ms";
@VisibleForTesting
static final String KEY_EJ_GRACE_PERIOD_TOP_APP_MS =
QC_CONSTANT_PREFIX + "ej_grace_period_top_app_ms";
@VisibleForTesting
static final String KEY_QUOTA_BUMP_ADDITIONAL_DURATION_MS =
QC_CONSTANT_PREFIX + "quota_bump_additional_duration_ms";
@VisibleForTesting
static final String KEY_QUOTA_BUMP_ADDITIONAL_JOB_COUNT =
QC_CONSTANT_PREFIX + "quota_bump_additional_job_count";
@VisibleForTesting
static final String KEY_QUOTA_BUMP_ADDITIONAL_SESSION_COUNT =
QC_CONSTANT_PREFIX + "quota_bump_additional_session_count";
@VisibleForTesting
static final String KEY_QUOTA_BUMP_WINDOW_SIZE_MS =
QC_CONSTANT_PREFIX + "quota_bump_window_size_ms";
@VisibleForTesting
static final String KEY_QUOTA_BUMP_LIMIT =
QC_CONSTANT_PREFIX + "quota_bump_limit";
private static final long DEFAULT_ALLOWED_TIME_PER_PERIOD_EXEMPTED_MS =
10 * 60 * 1000L; // 10 minutes
private static final long DEFAULT_ALLOWED_TIME_PER_PERIOD_ACTIVE_MS =
10 * 60 * 1000L; // 10 minutes
private static final long DEFAULT_ALLOWED_TIME_PER_PERIOD_WORKING_MS =
10 * 60 * 1000L; // 10 minutes
private static final long DEFAULT_ALLOWED_TIME_PER_PERIOD_FREQUENT_MS =
10 * 60 * 1000L; // 10 minutes
private static final long DEFAULT_ALLOWED_TIME_PER_PERIOD_RARE_MS =
10 * 60 * 1000L; // 10 minutes
private static final long DEFAULT_ALLOWED_TIME_PER_PERIOD_RESTRICTED_MS =
10 * 60 * 1000L; // 10 minutes
private static final long DEFAULT_IN_QUOTA_BUFFER_MS =
30 * 1000L; // 30 seconds
private static final long DEFAULT_WINDOW_SIZE_EXEMPTED_MS =
DEFAULT_ALLOWED_TIME_PER_PERIOD_EXEMPTED_MS; // EXEMPT apps can run jobs at any time
private static final long DEFAULT_WINDOW_SIZE_ACTIVE_MS =
DEFAULT_ALLOWED_TIME_PER_PERIOD_ACTIVE_MS; // ACTIVE apps can run jobs at any time
private static final long DEFAULT_WINDOW_SIZE_WORKING_MS =
2 * 60 * 60 * 1000L; // 2 hours
private static final long DEFAULT_WINDOW_SIZE_FREQUENT_MS =
8 * 60 * 60 * 1000L; // 8 hours
private static final long DEFAULT_WINDOW_SIZE_RARE_MS =
24 * 60 * 60 * 1000L; // 24 hours
private static final long DEFAULT_WINDOW_SIZE_RESTRICTED_MS =
24 * 60 * 60 * 1000L; // 24 hours
private static final long DEFAULT_MAX_EXECUTION_TIME_MS =
4 * HOUR_IN_MILLIS;
private static final long DEFAULT_RATE_LIMITING_WINDOW_MS =
MINUTE_IN_MILLIS;
private static final int DEFAULT_MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW = 20;
private static final int DEFAULT_MAX_JOB_COUNT_EXEMPTED =
75; // 75/window = 450/hr = 1/session
private static final int DEFAULT_MAX_JOB_COUNT_ACTIVE = DEFAULT_MAX_JOB_COUNT_EXEMPTED;
private static final int DEFAULT_MAX_JOB_COUNT_WORKING = // 120/window = 60/hr = 12/session
(int) (60.0 * DEFAULT_WINDOW_SIZE_WORKING_MS / HOUR_IN_MILLIS);
private static final int DEFAULT_MAX_JOB_COUNT_FREQUENT = // 200/window = 25/hr = 25/session
(int) (25.0 * DEFAULT_WINDOW_SIZE_FREQUENT_MS / HOUR_IN_MILLIS);
private static final int DEFAULT_MAX_JOB_COUNT_RARE = // 48/window = 2/hr = 16/session
(int) (2.0 * DEFAULT_WINDOW_SIZE_RARE_MS / HOUR_IN_MILLIS);
private static final int DEFAULT_MAX_JOB_COUNT_RESTRICTED = 10;
private static final int DEFAULT_MAX_SESSION_COUNT_EXEMPTED =
75; // 450/hr
private static final int DEFAULT_MAX_SESSION_COUNT_ACTIVE =
DEFAULT_MAX_SESSION_COUNT_EXEMPTED;
private static final int DEFAULT_MAX_SESSION_COUNT_WORKING =
10; // 5/hr
private static final int DEFAULT_MAX_SESSION_COUNT_FREQUENT =
8; // 1/hr
private static final int DEFAULT_MAX_SESSION_COUNT_RARE =
3; // .125/hr
private static final int DEFAULT_MAX_SESSION_COUNT_RESTRICTED = 1; // 1/day
private static final int DEFAULT_MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW = 20;
private static final long DEFAULT_TIMING_SESSION_COALESCING_DURATION_MS = 5000; // 5 seconds
private static final long DEFAULT_MIN_QUOTA_CHECK_DELAY_MS = MINUTE_IN_MILLIS;
private static final long DEFAULT_EJ_LIMIT_EXEMPTED_MS = 45 * MINUTE_IN_MILLIS;
private static final long DEFAULT_EJ_LIMIT_ACTIVE_MS = 30 * MINUTE_IN_MILLIS;
private static final long DEFAULT_EJ_LIMIT_WORKING_MS = DEFAULT_EJ_LIMIT_ACTIVE_MS;
private static final long DEFAULT_EJ_LIMIT_FREQUENT_MS = 10 * MINUTE_IN_MILLIS;
private static final long DEFAULT_EJ_LIMIT_RARE_MS = DEFAULT_EJ_LIMIT_FREQUENT_MS;
private static final long DEFAULT_EJ_LIMIT_RESTRICTED_MS = 5 * MINUTE_IN_MILLIS;
private static final long DEFAULT_EJ_LIMIT_ADDITION_SPECIAL_MS = 15 * MINUTE_IN_MILLIS;
private static final long DEFAULT_EJ_LIMIT_ADDITION_INSTALLER_MS = 30 * MINUTE_IN_MILLIS;
private static final long DEFAULT_EJ_WINDOW_SIZE_MS = 24 * HOUR_IN_MILLIS;
private static final long DEFAULT_EJ_TOP_APP_TIME_CHUNK_SIZE_MS = 30 * SECOND_IN_MILLIS;
private static final long DEFAULT_EJ_REWARD_TOP_APP_MS = 10 * SECOND_IN_MILLIS;
private static final long DEFAULT_EJ_REWARD_INTERACTION_MS = 15 * SECOND_IN_MILLIS;
private static final long DEFAULT_EJ_REWARD_NOTIFICATION_SEEN_MS = 0;
private static final long DEFAULT_EJ_GRACE_PERIOD_TEMP_ALLOWLIST_MS = 3 * MINUTE_IN_MILLIS;
private static final long DEFAULT_EJ_GRACE_PERIOD_TOP_APP_MS = 1 * MINUTE_IN_MILLIS;
private static final long DEFAULT_QUOTA_BUMP_ADDITIONAL_DURATION_MS = 1 * MINUTE_IN_MILLIS;
private static final int DEFAULT_QUOTA_BUMP_ADDITIONAL_JOB_COUNT = 2;
private static final int DEFAULT_QUOTA_BUMP_ADDITIONAL_SESSION_COUNT = 1;
private static final long DEFAULT_QUOTA_BUMP_WINDOW_SIZE_MS = 8 * HOUR_IN_MILLIS;
private static final int DEFAULT_QUOTA_BUMP_LIMIT = 8;
/**
* How much time each app in the exempted bucket will have to run jobs within their standby
* bucket window.
*/
public long ALLOWED_TIME_PER_PERIOD_EXEMPTED_MS =
DEFAULT_ALLOWED_TIME_PER_PERIOD_EXEMPTED_MS;
/**
* How much time each app in the active bucket will have to run jobs within their standby
* bucket window.
*/
public long ALLOWED_TIME_PER_PERIOD_ACTIVE_MS = DEFAULT_ALLOWED_TIME_PER_PERIOD_ACTIVE_MS;
/**
* How much time each app in the working set bucket will have to run jobs within their
* standby bucket window.
*/
public long ALLOWED_TIME_PER_PERIOD_WORKING_MS = DEFAULT_ALLOWED_TIME_PER_PERIOD_WORKING_MS;
/**
* How much time each app in the frequent bucket will have to run jobs within their standby
* bucket window.
*/
public long ALLOWED_TIME_PER_PERIOD_FREQUENT_MS =
DEFAULT_ALLOWED_TIME_PER_PERIOD_FREQUENT_MS;
/**
* How much time each app in the rare bucket will have to run jobs within their standby
* bucket window.
*/
public long ALLOWED_TIME_PER_PERIOD_RARE_MS = DEFAULT_ALLOWED_TIME_PER_PERIOD_RARE_MS;
/**
* How much time each app in the restricted bucket will have to run jobs within their
* standby bucket window.
*/
public long ALLOWED_TIME_PER_PERIOD_RESTRICTED_MS =
DEFAULT_ALLOWED_TIME_PER_PERIOD_RESTRICTED_MS;
/**
* How much time the package should have before transitioning from out-of-quota to in-quota.
* This should not affect processing if the package is already in-quota.
*/
public long IN_QUOTA_BUFFER_MS = DEFAULT_IN_QUOTA_BUFFER_MS;
/**
* The quota window size of the particular standby bucket. Apps in this standby bucket are
* expected to run only {@link #ALLOWED_TIME_PER_PERIOD_EXEMPTED_MS} within the past
* WINDOW_SIZE_MS.
*/
public long WINDOW_SIZE_EXEMPTED_MS = DEFAULT_WINDOW_SIZE_EXEMPTED_MS;
/**
* The quota window size of the particular standby bucket. Apps in this standby bucket are
* expected to run only {@link #ALLOWED_TIME_PER_PERIOD_ACTIVE_MS} within the past
* WINDOW_SIZE_MS.
*/
public long WINDOW_SIZE_ACTIVE_MS = DEFAULT_WINDOW_SIZE_ACTIVE_MS;
/**
* The quota window size of the particular standby bucket. Apps in this standby bucket are
* expected to run only {@link #ALLOWED_TIME_PER_PERIOD_WORKING_MS} within the past
* WINDOW_SIZE_MS.
*/
public long WINDOW_SIZE_WORKING_MS = DEFAULT_WINDOW_SIZE_WORKING_MS;
/**
* The quota window size of the particular standby bucket. Apps in this standby bucket are
* expected to run only {@link #ALLOWED_TIME_PER_PERIOD_FREQUENT_MS} within the past
* WINDOW_SIZE_MS.
*/
public long WINDOW_SIZE_FREQUENT_MS = DEFAULT_WINDOW_SIZE_FREQUENT_MS;
/**
* The quota window size of the particular standby bucket. Apps in this standby bucket are
* expected to run only {@link #ALLOWED_TIME_PER_PERIOD_RARE_MS} within the past
* WINDOW_SIZE_MS.
*/
public long WINDOW_SIZE_RARE_MS = DEFAULT_WINDOW_SIZE_RARE_MS;
/**
* The quota window size of the particular standby bucket. Apps in this standby bucket are
* expected to run only {@link #ALLOWED_TIME_PER_PERIOD_RESTRICTED_MS} within the past
* WINDOW_SIZE_MS.
*/
public long WINDOW_SIZE_RESTRICTED_MS = DEFAULT_WINDOW_SIZE_RESTRICTED_MS;
/**
* The maximum amount of time an app can have its jobs running within a 24 hour window.
*/
public long MAX_EXECUTION_TIME_MS = DEFAULT_MAX_EXECUTION_TIME_MS;
/**
* The maximum number of jobs an app can run within this particular standby bucket's
* window size.
*/
public int MAX_JOB_COUNT_EXEMPTED = DEFAULT_MAX_JOB_COUNT_EXEMPTED;
/**
* The maximum number of jobs an app can run within this particular standby bucket's
* window size.
*/
public int MAX_JOB_COUNT_ACTIVE = DEFAULT_MAX_JOB_COUNT_ACTIVE;
/**
* The maximum number of jobs an app can run within this particular standby bucket's
* window size.
*/
public int MAX_JOB_COUNT_WORKING = DEFAULT_MAX_JOB_COUNT_WORKING;
/**
* The maximum number of jobs an app can run within this particular standby bucket's
* window size.
*/
public int MAX_JOB_COUNT_FREQUENT = DEFAULT_MAX_JOB_COUNT_FREQUENT;
/**
* The maximum number of jobs an app can run within this particular standby bucket's
* window size.
*/
public int MAX_JOB_COUNT_RARE = DEFAULT_MAX_JOB_COUNT_RARE;
/**
* The maximum number of jobs an app can run within this particular standby bucket's
* window size.
*/
public int MAX_JOB_COUNT_RESTRICTED = DEFAULT_MAX_JOB_COUNT_RESTRICTED;
/** The period of time used to rate limit recently run jobs. */
public long RATE_LIMITING_WINDOW_MS = DEFAULT_RATE_LIMITING_WINDOW_MS;
/**
* The maximum number of jobs that can run within the past {@link #RATE_LIMITING_WINDOW_MS}.
*/
public int MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW =
DEFAULT_MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW;
/**
* The maximum number of {@link TimingSession TimingSessions} an app can run within this
* particular standby bucket's window size.
*/
public int MAX_SESSION_COUNT_EXEMPTED = DEFAULT_MAX_SESSION_COUNT_EXEMPTED;
/**
* The maximum number of {@link TimingSession TimingSessions} an app can run within this
* particular standby bucket's window size.
*/
public int MAX_SESSION_COUNT_ACTIVE = DEFAULT_MAX_SESSION_COUNT_ACTIVE;
/**
* The maximum number of {@link TimingSession TimingSessions} an app can run within this
* particular standby bucket's window size.
*/
public int MAX_SESSION_COUNT_WORKING = DEFAULT_MAX_SESSION_COUNT_WORKING;
/**
* The maximum number of {@link TimingSession TimingSessions} an app can run within this
* particular standby bucket's window size.
*/
public int MAX_SESSION_COUNT_FREQUENT = DEFAULT_MAX_SESSION_COUNT_FREQUENT;
/**
* The maximum number of {@link TimingSession TimingSessions} an app can run within this
* particular standby bucket's window size.
*/
public int MAX_SESSION_COUNT_RARE = DEFAULT_MAX_SESSION_COUNT_RARE;
/**
* The maximum number of {@link TimingSession TimingSessions} an app can run within this
* particular standby bucket's window size.
*/
public int MAX_SESSION_COUNT_RESTRICTED = DEFAULT_MAX_SESSION_COUNT_RESTRICTED;
/**
* The maximum number of {@link TimingSession TimingSessions} that can run within the past
* {@link #RATE_LIMITING_WINDOW_MS}.
*/
public int MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW =
DEFAULT_MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW;
/**
* Treat two distinct {@link TimingSession TimingSessions} as the same if they start and
* end within this amount of time of each other.
*/
public long TIMING_SESSION_COALESCING_DURATION_MS =
DEFAULT_TIMING_SESSION_COALESCING_DURATION_MS;
/** The minimum amount of time between quota check alarms. */
public long MIN_QUOTA_CHECK_DELAY_MS = DEFAULT_MIN_QUOTA_CHECK_DELAY_MS;
// Safeguards
/** The minimum number of jobs that any bucket will be allowed to run within its window. */
private static final int MIN_BUCKET_JOB_COUNT = 10;
/**
* The minimum number of {@link TimingSession TimingSessions} that any bucket will be
* allowed to run within its window.
*/
private static final int MIN_BUCKET_SESSION_COUNT = 1;
/** The minimum value that {@link #MAX_EXECUTION_TIME_MS} can have. */
private static final long MIN_MAX_EXECUTION_TIME_MS = 60 * MINUTE_IN_MILLIS;
/** The minimum value that {@link #MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW} can have. */
private static final int MIN_MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW = 10;
/** The minimum value that {@link #MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW} can have. */
private static final int MIN_MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW = 10;
/** The minimum value that {@link #RATE_LIMITING_WINDOW_MS} can have. */
private static final long MIN_RATE_LIMITING_WINDOW_MS = 30 * SECOND_IN_MILLIS;
/**
* The total expedited job session limit of the particular standby bucket. Apps in this
* standby bucket can only have expedited job sessions totalling EJ_LIMIT (without factoring
* in any rewards or free EJs).
*/
public long EJ_LIMIT_EXEMPTED_MS = DEFAULT_EJ_LIMIT_EXEMPTED_MS;
/**
* The total expedited job session limit of the particular standby bucket. Apps in this
* standby bucket can only have expedited job sessions totalling EJ_LIMIT (without factoring
* in any rewards or free EJs).
*/
public long EJ_LIMIT_ACTIVE_MS = DEFAULT_EJ_LIMIT_ACTIVE_MS;
/**
* The total expedited job session limit of the particular standby bucket. Apps in this
* standby bucket can only have expedited job sessions totalling EJ_LIMIT (without factoring
* in any rewards or free EJs).
*/
public long EJ_LIMIT_WORKING_MS = DEFAULT_EJ_LIMIT_WORKING_MS;
/**
* The total expedited job session limit of the particular standby bucket. Apps in this
* standby bucket can only have expedited job sessions totalling EJ_LIMIT (without factoring
* in any rewards or free EJs).
*/
public long EJ_LIMIT_FREQUENT_MS = DEFAULT_EJ_LIMIT_FREQUENT_MS;
/**
* The total expedited job session limit of the particular standby bucket. Apps in this
* standby bucket can only have expedited job sessions totalling EJ_LIMIT (without factoring
* in any rewards or free EJs).
*/
public long EJ_LIMIT_RARE_MS = DEFAULT_EJ_LIMIT_RARE_MS;
/**
* The total expedited job session limit of the particular standby bucket. Apps in this
* standby bucket can only have expedited job sessions totalling EJ_LIMIT (without factoring
* in any rewards or free EJs).
*/
public long EJ_LIMIT_RESTRICTED_MS = DEFAULT_EJ_LIMIT_RESTRICTED_MS;
/**
* How much additional EJ quota special, critical apps should get.
*/
public long EJ_LIMIT_ADDITION_SPECIAL_MS = DEFAULT_EJ_LIMIT_ADDITION_SPECIAL_MS;
/**
* How much additional EJ quota system installers (with the INSTALL_PACKAGES permission)
* should get.
*/
public long EJ_LIMIT_ADDITION_INSTALLER_MS = DEFAULT_EJ_LIMIT_ADDITION_INSTALLER_MS;
/**
* The period of time used to calculate expedited job sessions. Apps can only have expedited
* job sessions totalling EJ_LIMIT_<bucket>_MS within this period of time (without factoring
* in any rewards or free EJs).
*/
public long EJ_WINDOW_SIZE_MS = DEFAULT_EJ_WINDOW_SIZE_MS;
/**
* Length of time used to split an app's top time into chunks.
*/
public long EJ_TOP_APP_TIME_CHUNK_SIZE_MS = DEFAULT_EJ_TOP_APP_TIME_CHUNK_SIZE_MS;
/**
* How much EJ quota to give back to an app based on the number of top app time chunks it
* had.
*/
public long EJ_REWARD_TOP_APP_MS = DEFAULT_EJ_REWARD_TOP_APP_MS;
/**
* How much EJ quota to give back to an app based on each non-top user interaction.
*/
public long EJ_REWARD_INTERACTION_MS = DEFAULT_EJ_REWARD_INTERACTION_MS;
/**
* How much EJ quota to give back to an app based on each notification seen event.
*/
public long EJ_REWARD_NOTIFICATION_SEEN_MS = DEFAULT_EJ_REWARD_NOTIFICATION_SEEN_MS;
/**
* How much additional grace period to add to the end of an app's temp allowlist
* duration.
*/
public long EJ_GRACE_PERIOD_TEMP_ALLOWLIST_MS = DEFAULT_EJ_GRACE_PERIOD_TEMP_ALLOWLIST_MS;
/**
* How much additional grace period to give an app when it leaves the TOP state.
*/
public long EJ_GRACE_PERIOD_TOP_APP_MS = DEFAULT_EJ_GRACE_PERIOD_TOP_APP_MS;
/**
* How much additional session duration to give an app for each accepted quota bump.
*/
public long QUOTA_BUMP_ADDITIONAL_DURATION_MS = DEFAULT_QUOTA_BUMP_ADDITIONAL_DURATION_MS;
/**
* How many additional regular jobs to give an app for each accepted quota bump.
*/
public int QUOTA_BUMP_ADDITIONAL_JOB_COUNT = DEFAULT_QUOTA_BUMP_ADDITIONAL_JOB_COUNT;
/**
* How many additional sessions to give an app for each accepted quota bump.
*/
public int QUOTA_BUMP_ADDITIONAL_SESSION_COUNT =
DEFAULT_QUOTA_BUMP_ADDITIONAL_SESSION_COUNT;
/**
* The rolling window size within which to accept and apply quota bump events.
*/
public long QUOTA_BUMP_WINDOW_SIZE_MS = DEFAULT_QUOTA_BUMP_WINDOW_SIZE_MS;
/**
* The maximum number of quota bumps to accept and apply within the
* {@link #QUOTA_BUMP_WINDOW_SIZE_MS window}.
*/
public int QUOTA_BUMP_LIMIT = DEFAULT_QUOTA_BUMP_LIMIT;
public void processConstantLocked(@NonNull DeviceConfig.Properties properties,
@NonNull String key) {
switch (key) {
case KEY_ALLOWED_TIME_PER_PERIOD_EXEMPTED_MS:
case KEY_ALLOWED_TIME_PER_PERIOD_ACTIVE_MS:
case KEY_ALLOWED_TIME_PER_PERIOD_WORKING_MS:
case KEY_ALLOWED_TIME_PER_PERIOD_FREQUENT_MS:
case KEY_ALLOWED_TIME_PER_PERIOD_RARE_MS:
case KEY_ALLOWED_TIME_PER_PERIOD_RESTRICTED_MS:
case KEY_IN_QUOTA_BUFFER_MS:
case KEY_MAX_EXECUTION_TIME_MS:
case KEY_WINDOW_SIZE_ACTIVE_MS:
case KEY_WINDOW_SIZE_WORKING_MS:
case KEY_WINDOW_SIZE_FREQUENT_MS:
case KEY_WINDOW_SIZE_RARE_MS:
case KEY_WINDOW_SIZE_RESTRICTED_MS:
updateExecutionPeriodConstantsLocked();
break;
case KEY_RATE_LIMITING_WINDOW_MS:
case KEY_MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW:
case KEY_MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW:
updateRateLimitingConstantsLocked();
break;
case KEY_EJ_LIMIT_ACTIVE_MS:
case KEY_EJ_LIMIT_WORKING_MS:
case KEY_EJ_LIMIT_FREQUENT_MS:
case KEY_EJ_LIMIT_RARE_MS:
case KEY_EJ_LIMIT_RESTRICTED_MS:
case KEY_EJ_LIMIT_ADDITION_SPECIAL_MS:
case KEY_EJ_LIMIT_ADDITION_INSTALLER_MS:
case KEY_EJ_WINDOW_SIZE_MS:
updateEJLimitConstantsLocked();
break;
case KEY_QUOTA_BUMP_ADDITIONAL_DURATION_MS:
case KEY_QUOTA_BUMP_ADDITIONAL_JOB_COUNT:
case KEY_QUOTA_BUMP_ADDITIONAL_SESSION_COUNT:
case KEY_QUOTA_BUMP_WINDOW_SIZE_MS:
case KEY_QUOTA_BUMP_LIMIT:
updateQuotaBumpConstantsLocked();
break;
case KEY_MAX_JOB_COUNT_EXEMPTED:
MAX_JOB_COUNT_EXEMPTED = properties.getInt(key, DEFAULT_MAX_JOB_COUNT_EXEMPTED);
int newExemptedMaxJobCount =
Math.max(MIN_BUCKET_JOB_COUNT, MAX_JOB_COUNT_EXEMPTED);
if (mMaxBucketJobCounts[EXEMPTED_INDEX] != newExemptedMaxJobCount) {
mMaxBucketJobCounts[EXEMPTED_INDEX] = newExemptedMaxJobCount;
mShouldReevaluateConstraints = true;
}
break;
case KEY_MAX_JOB_COUNT_ACTIVE:
MAX_JOB_COUNT_ACTIVE = properties.getInt(key, DEFAULT_MAX_JOB_COUNT_ACTIVE);
int newActiveMaxJobCount = Math.max(MIN_BUCKET_JOB_COUNT, MAX_JOB_COUNT_ACTIVE);
if (mMaxBucketJobCounts[ACTIVE_INDEX] != newActiveMaxJobCount) {
mMaxBucketJobCounts[ACTIVE_INDEX] = newActiveMaxJobCount;
mShouldReevaluateConstraints = true;
}
break;
case KEY_MAX_JOB_COUNT_WORKING:
MAX_JOB_COUNT_WORKING = properties.getInt(key, DEFAULT_MAX_JOB_COUNT_WORKING);
int newWorkingMaxJobCount = Math.max(MIN_BUCKET_JOB_COUNT,
MAX_JOB_COUNT_WORKING);
if (mMaxBucketJobCounts[WORKING_INDEX] != newWorkingMaxJobCount) {
mMaxBucketJobCounts[WORKING_INDEX] = newWorkingMaxJobCount;
mShouldReevaluateConstraints = true;
}
break;
case KEY_MAX_JOB_COUNT_FREQUENT:
MAX_JOB_COUNT_FREQUENT = properties.getInt(key, DEFAULT_MAX_JOB_COUNT_FREQUENT);
int newFrequentMaxJobCount = Math.max(MIN_BUCKET_JOB_COUNT,
MAX_JOB_COUNT_FREQUENT);
if (mMaxBucketJobCounts[FREQUENT_INDEX] != newFrequentMaxJobCount) {
mMaxBucketJobCounts[FREQUENT_INDEX] = newFrequentMaxJobCount;
mShouldReevaluateConstraints = true;
}
break;
case KEY_MAX_JOB_COUNT_RARE:
MAX_JOB_COUNT_RARE = properties.getInt(key, DEFAULT_MAX_JOB_COUNT_RARE);
int newRareMaxJobCount = Math.max(MIN_BUCKET_JOB_COUNT, MAX_JOB_COUNT_RARE);
if (mMaxBucketJobCounts[RARE_INDEX] != newRareMaxJobCount) {
mMaxBucketJobCounts[RARE_INDEX] = newRareMaxJobCount;
mShouldReevaluateConstraints = true;
}
break;
case KEY_MAX_JOB_COUNT_RESTRICTED:
MAX_JOB_COUNT_RESTRICTED =
properties.getInt(key, DEFAULT_MAX_JOB_COUNT_RESTRICTED);
int newRestrictedMaxJobCount =
Math.max(MIN_BUCKET_JOB_COUNT, MAX_JOB_COUNT_RESTRICTED);
if (mMaxBucketJobCounts[RESTRICTED_INDEX] != newRestrictedMaxJobCount) {
mMaxBucketJobCounts[RESTRICTED_INDEX] = newRestrictedMaxJobCount;
mShouldReevaluateConstraints = true;
}
break;
case KEY_MAX_SESSION_COUNT_EXEMPTED:
MAX_SESSION_COUNT_EXEMPTED =
properties.getInt(key, DEFAULT_MAX_SESSION_COUNT_EXEMPTED);
int newExemptedMaxSessionCount =
Math.max(MIN_BUCKET_SESSION_COUNT, MAX_SESSION_COUNT_EXEMPTED);
if (mMaxBucketSessionCounts[EXEMPTED_INDEX] != newExemptedMaxSessionCount) {
mMaxBucketSessionCounts[EXEMPTED_INDEX] = newExemptedMaxSessionCount;
mShouldReevaluateConstraints = true;
}
break;
case KEY_MAX_SESSION_COUNT_ACTIVE:
MAX_SESSION_COUNT_ACTIVE =
properties.getInt(key, DEFAULT_MAX_SESSION_COUNT_ACTIVE);
int newActiveMaxSessionCount =
Math.max(MIN_BUCKET_SESSION_COUNT, MAX_SESSION_COUNT_ACTIVE);
if (mMaxBucketSessionCounts[ACTIVE_INDEX] != newActiveMaxSessionCount) {
mMaxBucketSessionCounts[ACTIVE_INDEX] = newActiveMaxSessionCount;
mShouldReevaluateConstraints = true;
}
break;
case KEY_MAX_SESSION_COUNT_WORKING:
MAX_SESSION_COUNT_WORKING =
properties.getInt(key, DEFAULT_MAX_SESSION_COUNT_WORKING);
int newWorkingMaxSessionCount =
Math.max(MIN_BUCKET_SESSION_COUNT, MAX_SESSION_COUNT_WORKING);
if (mMaxBucketSessionCounts[WORKING_INDEX] != newWorkingMaxSessionCount) {
mMaxBucketSessionCounts[WORKING_INDEX] = newWorkingMaxSessionCount;
mShouldReevaluateConstraints = true;
}
break;
case KEY_MAX_SESSION_COUNT_FREQUENT:
MAX_SESSION_COUNT_FREQUENT =
properties.getInt(key, DEFAULT_MAX_SESSION_COUNT_FREQUENT);
int newFrequentMaxSessionCount =
Math.max(MIN_BUCKET_SESSION_COUNT, MAX_SESSION_COUNT_FREQUENT);
if (mMaxBucketSessionCounts[FREQUENT_INDEX] != newFrequentMaxSessionCount) {
mMaxBucketSessionCounts[FREQUENT_INDEX] = newFrequentMaxSessionCount;
mShouldReevaluateConstraints = true;
}
break;
case KEY_MAX_SESSION_COUNT_RARE:
MAX_SESSION_COUNT_RARE = properties.getInt(key, DEFAULT_MAX_SESSION_COUNT_RARE);
int newRareMaxSessionCount =
Math.max(MIN_BUCKET_SESSION_COUNT, MAX_SESSION_COUNT_RARE);
if (mMaxBucketSessionCounts[RARE_INDEX] != newRareMaxSessionCount) {
mMaxBucketSessionCounts[RARE_INDEX] = newRareMaxSessionCount;
mShouldReevaluateConstraints = true;
}
break;
case KEY_MAX_SESSION_COUNT_RESTRICTED:
MAX_SESSION_COUNT_RESTRICTED =
properties.getInt(key, DEFAULT_MAX_SESSION_COUNT_RESTRICTED);
int newRestrictedMaxSessionCount = Math.max(0, MAX_SESSION_COUNT_RESTRICTED);
if (mMaxBucketSessionCounts[RESTRICTED_INDEX] != newRestrictedMaxSessionCount) {
mMaxBucketSessionCounts[RESTRICTED_INDEX] = newRestrictedMaxSessionCount;
mShouldReevaluateConstraints = true;
}
break;
case KEY_TIMING_SESSION_COALESCING_DURATION_MS:
TIMING_SESSION_COALESCING_DURATION_MS =
properties.getLong(key, DEFAULT_TIMING_SESSION_COALESCING_DURATION_MS);
long newSessionCoalescingDurationMs = Math.min(15 * MINUTE_IN_MILLIS,
Math.max(0, TIMING_SESSION_COALESCING_DURATION_MS));
if (mTimingSessionCoalescingDurationMs != newSessionCoalescingDurationMs) {
mTimingSessionCoalescingDurationMs = newSessionCoalescingDurationMs;
mShouldReevaluateConstraints = true;
}
break;
case KEY_MIN_QUOTA_CHECK_DELAY_MS:
MIN_QUOTA_CHECK_DELAY_MS =
properties.getLong(key, DEFAULT_MIN_QUOTA_CHECK_DELAY_MS);
// We don't need to re-evaluate execution stats or constraint status for this.
// Limit the delay to the range [0, 15] minutes.
mInQuotaAlarmQueue.setMinTimeBetweenAlarmsMs(
Math.min(15 * MINUTE_IN_MILLIS, Math.max(0, MIN_QUOTA_CHECK_DELAY_MS)));
break;
case KEY_EJ_TOP_APP_TIME_CHUNK_SIZE_MS:
// We don't need to re-evaluate execution stats or constraint status for this.
EJ_TOP_APP_TIME_CHUNK_SIZE_MS =
properties.getLong(key, DEFAULT_EJ_TOP_APP_TIME_CHUNK_SIZE_MS);
// Limit chunking to be in the range [1 millisecond, 15 minutes] per event.
long newChunkSizeMs = Math.min(15 * MINUTE_IN_MILLIS,
Math.max(1, EJ_TOP_APP_TIME_CHUNK_SIZE_MS));
if (mEJTopAppTimeChunkSizeMs != newChunkSizeMs) {
mEJTopAppTimeChunkSizeMs = newChunkSizeMs;
if (mEJTopAppTimeChunkSizeMs < mEJRewardTopAppMs) {
// Not making chunk sizes and top rewards to be the upper/lower
// limits of the other to allow trying different policies. Just log
// the discrepancy.
Slog.w(TAG, "EJ top app time chunk less than reward: "
+ mEJTopAppTimeChunkSizeMs + " vs " + mEJRewardTopAppMs);
}
}
break;
case KEY_EJ_REWARD_TOP_APP_MS:
// We don't need to re-evaluate execution stats or constraint status for this.
EJ_REWARD_TOP_APP_MS =
properties.getLong(key, DEFAULT_EJ_REWARD_TOP_APP_MS);
// Limit top reward to be in the range [10 seconds, 15 minutes] per event.
long newTopReward = Math.min(15 * MINUTE_IN_MILLIS,
Math.max(10 * SECOND_IN_MILLIS, EJ_REWARD_TOP_APP_MS));
if (mEJRewardTopAppMs != newTopReward) {
mEJRewardTopAppMs = newTopReward;
if (mEJTopAppTimeChunkSizeMs < mEJRewardTopAppMs) {
// Not making chunk sizes and top rewards to be the upper/lower
// limits of the other to allow trying different policies. Just log
// the discrepancy.
Slog.w(TAG, "EJ top app time chunk less than reward: "
+ mEJTopAppTimeChunkSizeMs + " vs " + mEJRewardTopAppMs);
}
}
break;
case KEY_EJ_REWARD_INTERACTION_MS:
// We don't need to re-evaluate execution stats or constraint status for this.
EJ_REWARD_INTERACTION_MS =
properties.getLong(key, DEFAULT_EJ_REWARD_INTERACTION_MS);
// Limit interaction reward to be in the range [5 seconds, 15 minutes] per
// event.
mEJRewardInteractionMs = Math.min(15 * MINUTE_IN_MILLIS,
Math.max(5 * SECOND_IN_MILLIS, EJ_REWARD_INTERACTION_MS));
break;
case KEY_EJ_REWARD_NOTIFICATION_SEEN_MS:
// We don't need to re-evaluate execution stats or constraint status for this.
EJ_REWARD_NOTIFICATION_SEEN_MS =
properties.getLong(key, DEFAULT_EJ_REWARD_NOTIFICATION_SEEN_MS);
// Limit notification seen reward to be in the range [0, 5] minutes per event.
mEJRewardNotificationSeenMs = Math.min(5 * MINUTE_IN_MILLIS,
Math.max(0, EJ_REWARD_NOTIFICATION_SEEN_MS));
break;
case KEY_EJ_GRACE_PERIOD_TEMP_ALLOWLIST_MS:
// We don't need to re-evaluate execution stats or constraint status for this.
EJ_GRACE_PERIOD_TEMP_ALLOWLIST_MS =
properties.getLong(key, DEFAULT_EJ_GRACE_PERIOD_TEMP_ALLOWLIST_MS);
// Limit grace period to be in the range [0 minutes, 1 hour].
mEJGracePeriodTempAllowlistMs = Math.min(HOUR_IN_MILLIS,
Math.max(0, EJ_GRACE_PERIOD_TEMP_ALLOWLIST_MS));
break;
case KEY_EJ_GRACE_PERIOD_TOP_APP_MS:
// We don't need to re-evaluate execution stats or constraint status for this.
EJ_GRACE_PERIOD_TOP_APP_MS =
properties.getLong(key, DEFAULT_EJ_GRACE_PERIOD_TOP_APP_MS);
// Limit grace period to be in the range [0 minutes, 1 hour].
mEJGracePeriodTopAppMs = Math.min(HOUR_IN_MILLIS,
Math.max(0, EJ_GRACE_PERIOD_TOP_APP_MS));
break;
}
}
private void updateExecutionPeriodConstantsLocked() {
if (mExecutionPeriodConstantsUpdated) {
return;
}
mExecutionPeriodConstantsUpdated = true;
// Query the values as an atomic set.
final DeviceConfig.Properties properties = DeviceConfig.getProperties(
DeviceConfig.NAMESPACE_JOB_SCHEDULER,
KEY_ALLOWED_TIME_PER_PERIOD_EXEMPTED_MS, KEY_ALLOWED_TIME_PER_PERIOD_ACTIVE_MS,
KEY_ALLOWED_TIME_PER_PERIOD_WORKING_MS, KEY_ALLOWED_TIME_PER_PERIOD_FREQUENT_MS,
KEY_ALLOWED_TIME_PER_PERIOD_RARE_MS, KEY_ALLOWED_TIME_PER_PERIOD_RESTRICTED_MS,
KEY_IN_QUOTA_BUFFER_MS,
KEY_MAX_EXECUTION_TIME_MS,
KEY_WINDOW_SIZE_EXEMPTED_MS, KEY_WINDOW_SIZE_ACTIVE_MS,
KEY_WINDOW_SIZE_WORKING_MS,
KEY_WINDOW_SIZE_FREQUENT_MS, KEY_WINDOW_SIZE_RARE_MS,
KEY_WINDOW_SIZE_RESTRICTED_MS);
ALLOWED_TIME_PER_PERIOD_EXEMPTED_MS =
properties.getLong(KEY_ALLOWED_TIME_PER_PERIOD_EXEMPTED_MS,
DEFAULT_ALLOWED_TIME_PER_PERIOD_EXEMPTED_MS);
ALLOWED_TIME_PER_PERIOD_ACTIVE_MS =
properties.getLong(KEY_ALLOWED_TIME_PER_PERIOD_ACTIVE_MS,
DEFAULT_ALLOWED_TIME_PER_PERIOD_ACTIVE_MS);
ALLOWED_TIME_PER_PERIOD_WORKING_MS =
properties.getLong(KEY_ALLOWED_TIME_PER_PERIOD_WORKING_MS,
DEFAULT_ALLOWED_TIME_PER_PERIOD_WORKING_MS);
ALLOWED_TIME_PER_PERIOD_FREQUENT_MS =
properties.getLong(KEY_ALLOWED_TIME_PER_PERIOD_FREQUENT_MS,
DEFAULT_ALLOWED_TIME_PER_PERIOD_FREQUENT_MS);
ALLOWED_TIME_PER_PERIOD_RARE_MS =
properties.getLong(KEY_ALLOWED_TIME_PER_PERIOD_RARE_MS,
DEFAULT_ALLOWED_TIME_PER_PERIOD_RARE_MS);
ALLOWED_TIME_PER_PERIOD_RESTRICTED_MS =
properties.getLong(KEY_ALLOWED_TIME_PER_PERIOD_RESTRICTED_MS,
DEFAULT_ALLOWED_TIME_PER_PERIOD_RESTRICTED_MS);
IN_QUOTA_BUFFER_MS = properties.getLong(KEY_IN_QUOTA_BUFFER_MS,
DEFAULT_IN_QUOTA_BUFFER_MS);
MAX_EXECUTION_TIME_MS = properties.getLong(KEY_MAX_EXECUTION_TIME_MS,
DEFAULT_MAX_EXECUTION_TIME_MS);
WINDOW_SIZE_EXEMPTED_MS = properties.getLong(KEY_WINDOW_SIZE_EXEMPTED_MS,
DEFAULT_WINDOW_SIZE_EXEMPTED_MS);
WINDOW_SIZE_ACTIVE_MS = properties.getLong(KEY_WINDOW_SIZE_ACTIVE_MS,
DEFAULT_WINDOW_SIZE_ACTIVE_MS);
WINDOW_SIZE_WORKING_MS =
properties.getLong(KEY_WINDOW_SIZE_WORKING_MS, DEFAULT_WINDOW_SIZE_WORKING_MS);
WINDOW_SIZE_FREQUENT_MS =
properties.getLong(KEY_WINDOW_SIZE_FREQUENT_MS,
DEFAULT_WINDOW_SIZE_FREQUENT_MS);
WINDOW_SIZE_RARE_MS = properties.getLong(KEY_WINDOW_SIZE_RARE_MS,
DEFAULT_WINDOW_SIZE_RARE_MS);
WINDOW_SIZE_RESTRICTED_MS =
properties.getLong(KEY_WINDOW_SIZE_RESTRICTED_MS,
DEFAULT_WINDOW_SIZE_RESTRICTED_MS);
long newMaxExecutionTimeMs = Math.max(MIN_MAX_EXECUTION_TIME_MS,
Math.min(MAX_PERIOD_MS, MAX_EXECUTION_TIME_MS));
if (mMaxExecutionTimeMs != newMaxExecutionTimeMs) {
mMaxExecutionTimeMs = newMaxExecutionTimeMs;
mMaxExecutionTimeIntoQuotaMs = mMaxExecutionTimeMs - mQuotaBufferMs;
mShouldReevaluateConstraints = true;
}
long minAllowedTimeMs = Long.MAX_VALUE;
long newAllowedTimeExemptedMs = Math.min(mMaxExecutionTimeMs,
Math.max(MINUTE_IN_MILLIS, ALLOWED_TIME_PER_PERIOD_EXEMPTED_MS));
minAllowedTimeMs = Math.min(minAllowedTimeMs, newAllowedTimeExemptedMs);
if (mAllowedTimePerPeriodMs[EXEMPTED_INDEX] != newAllowedTimeExemptedMs) {
mAllowedTimePerPeriodMs[EXEMPTED_INDEX] = newAllowedTimeExemptedMs;
mShouldReevaluateConstraints = true;
}
long newAllowedTimeActiveMs = Math.min(mMaxExecutionTimeMs,
Math.max(MINUTE_IN_MILLIS, ALLOWED_TIME_PER_PERIOD_ACTIVE_MS));
minAllowedTimeMs = Math.min(minAllowedTimeMs, newAllowedTimeActiveMs);
if (mAllowedTimePerPeriodMs[ACTIVE_INDEX] != newAllowedTimeActiveMs) {
mAllowedTimePerPeriodMs[ACTIVE_INDEX] = newAllowedTimeActiveMs;
mShouldReevaluateConstraints = true;
}
long newAllowedTimeWorkingMs = Math.min(mMaxExecutionTimeMs,
Math.max(MINUTE_IN_MILLIS, ALLOWED_TIME_PER_PERIOD_WORKING_MS));
minAllowedTimeMs = Math.min(minAllowedTimeMs, newAllowedTimeWorkingMs);
if (mAllowedTimePerPeriodMs[WORKING_INDEX] != newAllowedTimeWorkingMs) {
mAllowedTimePerPeriodMs[WORKING_INDEX] = newAllowedTimeWorkingMs;
mShouldReevaluateConstraints = true;
}
long newAllowedTimeFrequentMs = Math.min(mMaxExecutionTimeMs,
Math.max(MINUTE_IN_MILLIS, ALLOWED_TIME_PER_PERIOD_FREQUENT_MS));
minAllowedTimeMs = Math.min(minAllowedTimeMs, newAllowedTimeFrequentMs);
if (mAllowedTimePerPeriodMs[FREQUENT_INDEX] != newAllowedTimeFrequentMs) {
mAllowedTimePerPeriodMs[FREQUENT_INDEX] = newAllowedTimeFrequentMs;
mShouldReevaluateConstraints = true;
}
long newAllowedTimeRareMs = Math.min(mMaxExecutionTimeMs,
Math.max(MINUTE_IN_MILLIS, ALLOWED_TIME_PER_PERIOD_RARE_MS));
minAllowedTimeMs = Math.min(minAllowedTimeMs, newAllowedTimeRareMs);
if (mAllowedTimePerPeriodMs[RARE_INDEX] != newAllowedTimeRareMs) {
mAllowedTimePerPeriodMs[RARE_INDEX] = newAllowedTimeRareMs;
mShouldReevaluateConstraints = true;
}
long newAllowedTimeRestrictedMs = Math.min(mMaxExecutionTimeMs,
Math.max(MINUTE_IN_MILLIS, ALLOWED_TIME_PER_PERIOD_RESTRICTED_MS));
minAllowedTimeMs = Math.min(minAllowedTimeMs, newAllowedTimeRestrictedMs);
if (mAllowedTimePerPeriodMs[RESTRICTED_INDEX] != newAllowedTimeRestrictedMs) {
mAllowedTimePerPeriodMs[RESTRICTED_INDEX] = newAllowedTimeRestrictedMs;
mShouldReevaluateConstraints = true;
}
// Make sure quota buffer is non-negative, not greater than allowed time per period,
// and no more than 5 minutes.
long newQuotaBufferMs = Math.max(0, Math.min(minAllowedTimeMs,
Math.min(5 * MINUTE_IN_MILLIS, IN_QUOTA_BUFFER_MS)));
if (mQuotaBufferMs != newQuotaBufferMs) {
mQuotaBufferMs = newQuotaBufferMs;
mMaxExecutionTimeIntoQuotaMs = mMaxExecutionTimeMs - mQuotaBufferMs;
mShouldReevaluateConstraints = true;
}
long newExemptedPeriodMs = Math.max(mAllowedTimePerPeriodMs[EXEMPTED_INDEX],
Math.min(MAX_PERIOD_MS, WINDOW_SIZE_EXEMPTED_MS));
if (mBucketPeriodsMs[EXEMPTED_INDEX] != newExemptedPeriodMs) {
mBucketPeriodsMs[EXEMPTED_INDEX] = newExemptedPeriodMs;
mShouldReevaluateConstraints = true;
}
long newActivePeriodMs = Math.max(mAllowedTimePerPeriodMs[ACTIVE_INDEX],
Math.min(MAX_PERIOD_MS, WINDOW_SIZE_ACTIVE_MS));
if (mBucketPeriodsMs[ACTIVE_INDEX] != newActivePeriodMs) {
mBucketPeriodsMs[ACTIVE_INDEX] = newActivePeriodMs;
mShouldReevaluateConstraints = true;
}
long newWorkingPeriodMs = Math.max(mAllowedTimePerPeriodMs[WORKING_INDEX],
Math.min(MAX_PERIOD_MS, WINDOW_SIZE_WORKING_MS));
if (mBucketPeriodsMs[WORKING_INDEX] != newWorkingPeriodMs) {
mBucketPeriodsMs[WORKING_INDEX] = newWorkingPeriodMs;
mShouldReevaluateConstraints = true;
}
long newFrequentPeriodMs = Math.max(mAllowedTimePerPeriodMs[FREQUENT_INDEX],
Math.min(MAX_PERIOD_MS, WINDOW_SIZE_FREQUENT_MS));
if (mBucketPeriodsMs[FREQUENT_INDEX] != newFrequentPeriodMs) {
mBucketPeriodsMs[FREQUENT_INDEX] = newFrequentPeriodMs;
mShouldReevaluateConstraints = true;
}
long newRarePeriodMs = Math.max(mAllowedTimePerPeriodMs[RARE_INDEX],
Math.min(MAX_PERIOD_MS, WINDOW_SIZE_RARE_MS));
if (mBucketPeriodsMs[RARE_INDEX] != newRarePeriodMs) {
mBucketPeriodsMs[RARE_INDEX] = newRarePeriodMs;
mShouldReevaluateConstraints = true;
}
// Fit in the range [allowed time (10 mins), 1 week].
long newRestrictedPeriodMs = Math.max(mAllowedTimePerPeriodMs[RESTRICTED_INDEX],
Math.min(7 * 24 * 60 * MINUTE_IN_MILLIS, WINDOW_SIZE_RESTRICTED_MS));
if (mBucketPeriodsMs[RESTRICTED_INDEX] != newRestrictedPeriodMs) {
mBucketPeriodsMs[RESTRICTED_INDEX] = newRestrictedPeriodMs;
mShouldReevaluateConstraints = true;
}
}
private void updateRateLimitingConstantsLocked() {
if (mRateLimitingConstantsUpdated) {
return;
}
mRateLimitingConstantsUpdated = true;
// Query the values as an atomic set.
final DeviceConfig.Properties properties = DeviceConfig.getProperties(
DeviceConfig.NAMESPACE_JOB_SCHEDULER,
KEY_RATE_LIMITING_WINDOW_MS, KEY_MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW,
KEY_MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW);
RATE_LIMITING_WINDOW_MS =
properties.getLong(KEY_RATE_LIMITING_WINDOW_MS,
DEFAULT_RATE_LIMITING_WINDOW_MS);
MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW =
properties.getInt(KEY_MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW,
DEFAULT_MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW);
MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW =
properties.getInt(KEY_MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW,
DEFAULT_MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW);
long newRateLimitingWindowMs = Math.min(MAX_PERIOD_MS,
Math.max(MIN_RATE_LIMITING_WINDOW_MS, RATE_LIMITING_WINDOW_MS));
if (mRateLimitingWindowMs != newRateLimitingWindowMs) {
mRateLimitingWindowMs = newRateLimitingWindowMs;
mShouldReevaluateConstraints = true;
}
int newMaxJobCountPerRateLimitingWindow = Math.max(
MIN_MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW,
MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW);
if (mMaxJobCountPerRateLimitingWindow != newMaxJobCountPerRateLimitingWindow) {
mMaxJobCountPerRateLimitingWindow = newMaxJobCountPerRateLimitingWindow;
mShouldReevaluateConstraints = true;
}
int newMaxSessionCountPerRateLimitPeriod = Math.max(
MIN_MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW,
MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW);
if (mMaxSessionCountPerRateLimitingWindow != newMaxSessionCountPerRateLimitPeriod) {
mMaxSessionCountPerRateLimitingWindow = newMaxSessionCountPerRateLimitPeriod;
mShouldReevaluateConstraints = true;
}
}
private void updateEJLimitConstantsLocked() {
if (mEJLimitConstantsUpdated) {
return;
}
mEJLimitConstantsUpdated = true;
// Query the values as an atomic set.
final DeviceConfig.Properties properties = DeviceConfig.getProperties(
DeviceConfig.NAMESPACE_JOB_SCHEDULER,
KEY_EJ_LIMIT_EXEMPTED_MS,
KEY_EJ_LIMIT_ACTIVE_MS, KEY_EJ_LIMIT_WORKING_MS,
KEY_EJ_LIMIT_FREQUENT_MS, KEY_EJ_LIMIT_RARE_MS,
KEY_EJ_LIMIT_RESTRICTED_MS, KEY_EJ_LIMIT_ADDITION_SPECIAL_MS,
KEY_EJ_LIMIT_ADDITION_INSTALLER_MS,
KEY_EJ_WINDOW_SIZE_MS);
EJ_LIMIT_EXEMPTED_MS = properties.getLong(
KEY_EJ_LIMIT_EXEMPTED_MS, DEFAULT_EJ_LIMIT_EXEMPTED_MS);
EJ_LIMIT_ACTIVE_MS = properties.getLong(
KEY_EJ_LIMIT_ACTIVE_MS, DEFAULT_EJ_LIMIT_ACTIVE_MS);
EJ_LIMIT_WORKING_MS = properties.getLong(
KEY_EJ_LIMIT_WORKING_MS, DEFAULT_EJ_LIMIT_WORKING_MS);
EJ_LIMIT_FREQUENT_MS = properties.getLong(
KEY_EJ_LIMIT_FREQUENT_MS, DEFAULT_EJ_LIMIT_FREQUENT_MS);
EJ_LIMIT_RARE_MS = properties.getLong(
KEY_EJ_LIMIT_RARE_MS, DEFAULT_EJ_LIMIT_RARE_MS);
EJ_LIMIT_RESTRICTED_MS = properties.getLong(
KEY_EJ_LIMIT_RESTRICTED_MS, DEFAULT_EJ_LIMIT_RESTRICTED_MS);
EJ_LIMIT_ADDITION_INSTALLER_MS = properties.getLong(
KEY_EJ_LIMIT_ADDITION_INSTALLER_MS, DEFAULT_EJ_LIMIT_ADDITION_INSTALLER_MS);
EJ_LIMIT_ADDITION_SPECIAL_MS = properties.getLong(
KEY_EJ_LIMIT_ADDITION_SPECIAL_MS, DEFAULT_EJ_LIMIT_ADDITION_SPECIAL_MS);
EJ_WINDOW_SIZE_MS = properties.getLong(
KEY_EJ_WINDOW_SIZE_MS, DEFAULT_EJ_WINDOW_SIZE_MS);
// The window must be in the range [1 hour, 24 hours].
long newWindowSizeMs = Math.max(HOUR_IN_MILLIS,
Math.min(MAX_PERIOD_MS, EJ_WINDOW_SIZE_MS));
if (mEJLimitWindowSizeMs != newWindowSizeMs) {
mEJLimitWindowSizeMs = newWindowSizeMs;
mShouldReevaluateConstraints = true;
}
// The limit must be in the range [15 minutes, window size].
long newExemptLimitMs = Math.max(15 * MINUTE_IN_MILLIS,
Math.min(newWindowSizeMs, EJ_LIMIT_EXEMPTED_MS));
if (mEJLimitsMs[EXEMPTED_INDEX] != newExemptLimitMs) {
mEJLimitsMs[EXEMPTED_INDEX] = newExemptLimitMs;
mShouldReevaluateConstraints = true;
}
// The limit must be in the range [15 minutes, exempted limit].
long newActiveLimitMs = Math.max(15 * MINUTE_IN_MILLIS,
Math.min(newExemptLimitMs, EJ_LIMIT_ACTIVE_MS));
if (mEJLimitsMs[ACTIVE_INDEX] != newActiveLimitMs) {
mEJLimitsMs[ACTIVE_INDEX] = newActiveLimitMs;
mShouldReevaluateConstraints = true;
}
// The limit must be in the range [15 minutes, active limit].
long newWorkingLimitMs = Math.max(15 * MINUTE_IN_MILLIS,
Math.min(newActiveLimitMs, EJ_LIMIT_WORKING_MS));
if (mEJLimitsMs[WORKING_INDEX] != newWorkingLimitMs) {
mEJLimitsMs[WORKING_INDEX] = newWorkingLimitMs;
mShouldReevaluateConstraints = true;
}
// The limit must be in the range [10 minutes, working limit].
long newFrequentLimitMs = Math.max(10 * MINUTE_IN_MILLIS,
Math.min(newWorkingLimitMs, EJ_LIMIT_FREQUENT_MS));
if (mEJLimitsMs[FREQUENT_INDEX] != newFrequentLimitMs) {
mEJLimitsMs[FREQUENT_INDEX] = newFrequentLimitMs;
mShouldReevaluateConstraints = true;
}
// The limit must be in the range [10 minutes, frequent limit].
long newRareLimitMs = Math.max(10 * MINUTE_IN_MILLIS,
Math.min(newFrequentLimitMs, EJ_LIMIT_RARE_MS));
if (mEJLimitsMs[RARE_INDEX] != newRareLimitMs) {
mEJLimitsMs[RARE_INDEX] = newRareLimitMs;
mShouldReevaluateConstraints = true;
}
// The limit must be in the range [5 minutes, rare limit].
long newRestrictedLimitMs = Math.max(5 * MINUTE_IN_MILLIS,
Math.min(newRareLimitMs, EJ_LIMIT_RESTRICTED_MS));
if (mEJLimitsMs[RESTRICTED_INDEX] != newRestrictedLimitMs) {
mEJLimitsMs[RESTRICTED_INDEX] = newRestrictedLimitMs;
mShouldReevaluateConstraints = true;
}
// The additions must be in the range [0 minutes, window size - active limit].
long newAdditionInstallerMs = Math.max(0,
Math.min(newWindowSizeMs - newActiveLimitMs, EJ_LIMIT_ADDITION_INSTALLER_MS));
if (mEjLimitAdditionInstallerMs != newAdditionInstallerMs) {
mEjLimitAdditionInstallerMs = newAdditionInstallerMs;
mShouldReevaluateConstraints = true;
}
long newAdditionSpecialMs = Math.max(0,
Math.min(newWindowSizeMs - newActiveLimitMs, EJ_LIMIT_ADDITION_SPECIAL_MS));
if (mEjLimitAdditionSpecialMs != newAdditionSpecialMs) {
mEjLimitAdditionSpecialMs = newAdditionSpecialMs;
mShouldReevaluateConstraints = true;
}
}
private void updateQuotaBumpConstantsLocked() {
if (mQuotaBumpConstantsUpdated) {
return;
}
mQuotaBumpConstantsUpdated = true;
// Query the values as an atomic set.
final DeviceConfig.Properties properties = DeviceConfig.getProperties(
DeviceConfig.NAMESPACE_JOB_SCHEDULER,
KEY_QUOTA_BUMP_ADDITIONAL_DURATION_MS,
KEY_QUOTA_BUMP_ADDITIONAL_JOB_COUNT, KEY_QUOTA_BUMP_ADDITIONAL_SESSION_COUNT,
KEY_QUOTA_BUMP_WINDOW_SIZE_MS, KEY_QUOTA_BUMP_LIMIT);
QUOTA_BUMP_ADDITIONAL_DURATION_MS = properties.getLong(
KEY_QUOTA_BUMP_ADDITIONAL_DURATION_MS,
DEFAULT_QUOTA_BUMP_ADDITIONAL_DURATION_MS);
QUOTA_BUMP_ADDITIONAL_JOB_COUNT = properties.getInt(
KEY_QUOTA_BUMP_ADDITIONAL_JOB_COUNT, DEFAULT_QUOTA_BUMP_ADDITIONAL_JOB_COUNT);
QUOTA_BUMP_ADDITIONAL_SESSION_COUNT = properties.getInt(
KEY_QUOTA_BUMP_ADDITIONAL_SESSION_COUNT,
DEFAULT_QUOTA_BUMP_ADDITIONAL_SESSION_COUNT);
QUOTA_BUMP_WINDOW_SIZE_MS = properties.getLong(
KEY_QUOTA_BUMP_WINDOW_SIZE_MS, DEFAULT_QUOTA_BUMP_WINDOW_SIZE_MS);
QUOTA_BUMP_LIMIT = properties.getInt(
KEY_QUOTA_BUMP_LIMIT, DEFAULT_QUOTA_BUMP_LIMIT);
// The window must be in the range [1 hour, 24 hours].
long newWindowSizeMs = Math.max(HOUR_IN_MILLIS,
Math.min(MAX_PERIOD_MS, QUOTA_BUMP_WINDOW_SIZE_MS));
if (mQuotaBumpWindowSizeMs != newWindowSizeMs) {
mQuotaBumpWindowSizeMs = newWindowSizeMs;
mShouldReevaluateConstraints = true;
}
// The limit must be nonnegative.
int newLimit = Math.max(0, QUOTA_BUMP_LIMIT);
if (mQuotaBumpLimit != newLimit) {
mQuotaBumpLimit = newLimit;
mShouldReevaluateConstraints = true;
}
// The job count must be nonnegative.
int newJobAddition = Math.max(0, QUOTA_BUMP_ADDITIONAL_JOB_COUNT);
if (mQuotaBumpAdditionalJobCount != newJobAddition) {
mQuotaBumpAdditionalJobCount = newJobAddition;
mShouldReevaluateConstraints = true;
}
// The session count must be nonnegative.
int newSessionAddition = Math.max(0, QUOTA_BUMP_ADDITIONAL_SESSION_COUNT);
if (mQuotaBumpAdditionalSessionCount != newSessionAddition) {
mQuotaBumpAdditionalSessionCount = newSessionAddition;
mShouldReevaluateConstraints = true;
}
// The additional duration must be in the range [0, 10 minutes].
long newAdditionalDuration = Math.max(0,
Math.min(10 * MINUTE_IN_MILLIS, QUOTA_BUMP_ADDITIONAL_DURATION_MS));
if (mQuotaBumpAdditionalDurationMs != newAdditionalDuration) {
mQuotaBumpAdditionalDurationMs = newAdditionalDuration;
mShouldReevaluateConstraints = true;
}
}
private void dump(IndentingPrintWriter pw) {
pw.println();
pw.println("QuotaController:");
pw.increaseIndent();
pw.print(KEY_ALLOWED_TIME_PER_PERIOD_EXEMPTED_MS, ALLOWED_TIME_PER_PERIOD_EXEMPTED_MS)
.println();
pw.print(KEY_ALLOWED_TIME_PER_PERIOD_ACTIVE_MS, ALLOWED_TIME_PER_PERIOD_ACTIVE_MS)
.println();
pw.print(KEY_ALLOWED_TIME_PER_PERIOD_WORKING_MS, ALLOWED_TIME_PER_PERIOD_WORKING_MS)
.println();
pw.print(KEY_ALLOWED_TIME_PER_PERIOD_FREQUENT_MS, ALLOWED_TIME_PER_PERIOD_FREQUENT_MS)
.println();
pw.print(KEY_ALLOWED_TIME_PER_PERIOD_RARE_MS, ALLOWED_TIME_PER_PERIOD_RARE_MS)
.println();
pw.print(KEY_ALLOWED_TIME_PER_PERIOD_RESTRICTED_MS,
ALLOWED_TIME_PER_PERIOD_RESTRICTED_MS).println();
pw.print(KEY_IN_QUOTA_BUFFER_MS, IN_QUOTA_BUFFER_MS).println();
pw.print(KEY_WINDOW_SIZE_EXEMPTED_MS, WINDOW_SIZE_EXEMPTED_MS).println();
pw.print(KEY_WINDOW_SIZE_ACTIVE_MS, WINDOW_SIZE_ACTIVE_MS).println();
pw.print(KEY_WINDOW_SIZE_WORKING_MS, WINDOW_SIZE_WORKING_MS).println();
pw.print(KEY_WINDOW_SIZE_FREQUENT_MS, WINDOW_SIZE_FREQUENT_MS).println();
pw.print(KEY_WINDOW_SIZE_RARE_MS, WINDOW_SIZE_RARE_MS).println();
pw.print(KEY_WINDOW_SIZE_RESTRICTED_MS, WINDOW_SIZE_RESTRICTED_MS).println();
pw.print(KEY_MAX_EXECUTION_TIME_MS, MAX_EXECUTION_TIME_MS).println();
pw.print(KEY_MAX_JOB_COUNT_EXEMPTED, MAX_JOB_COUNT_EXEMPTED).println();
pw.print(KEY_MAX_JOB_COUNT_ACTIVE, MAX_JOB_COUNT_ACTIVE).println();
pw.print(KEY_MAX_JOB_COUNT_WORKING, MAX_JOB_COUNT_WORKING).println();
pw.print(KEY_MAX_JOB_COUNT_FREQUENT, MAX_JOB_COUNT_FREQUENT).println();
pw.print(KEY_MAX_JOB_COUNT_RARE, MAX_JOB_COUNT_RARE).println();
pw.print(KEY_MAX_JOB_COUNT_RESTRICTED, MAX_JOB_COUNT_RESTRICTED).println();
pw.print(KEY_RATE_LIMITING_WINDOW_MS, RATE_LIMITING_WINDOW_MS).println();
pw.print(KEY_MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW,
MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW).println();
pw.print(KEY_MAX_SESSION_COUNT_EXEMPTED, MAX_SESSION_COUNT_EXEMPTED).println();
pw.print(KEY_MAX_SESSION_COUNT_ACTIVE, MAX_SESSION_COUNT_ACTIVE).println();
pw.print(KEY_MAX_SESSION_COUNT_WORKING, MAX_SESSION_COUNT_WORKING).println();
pw.print(KEY_MAX_SESSION_COUNT_FREQUENT, MAX_SESSION_COUNT_FREQUENT).println();
pw.print(KEY_MAX_SESSION_COUNT_RARE, MAX_SESSION_COUNT_RARE).println();
pw.print(KEY_MAX_SESSION_COUNT_RESTRICTED, MAX_SESSION_COUNT_RESTRICTED).println();
pw.print(KEY_MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW,
MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW).println();
pw.print(KEY_TIMING_SESSION_COALESCING_DURATION_MS,
TIMING_SESSION_COALESCING_DURATION_MS).println();
pw.print(KEY_MIN_QUOTA_CHECK_DELAY_MS, MIN_QUOTA_CHECK_DELAY_MS).println();
pw.print(KEY_EJ_LIMIT_EXEMPTED_MS, EJ_LIMIT_EXEMPTED_MS).println();
pw.print(KEY_EJ_LIMIT_ACTIVE_MS, EJ_LIMIT_ACTIVE_MS).println();
pw.print(KEY_EJ_LIMIT_WORKING_MS, EJ_LIMIT_WORKING_MS).println();
pw.print(KEY_EJ_LIMIT_FREQUENT_MS, EJ_LIMIT_FREQUENT_MS).println();
pw.print(KEY_EJ_LIMIT_RARE_MS, EJ_LIMIT_RARE_MS).println();
pw.print(KEY_EJ_LIMIT_RESTRICTED_MS, EJ_LIMIT_RESTRICTED_MS).println();
pw.print(KEY_EJ_LIMIT_ADDITION_INSTALLER_MS, EJ_LIMIT_ADDITION_INSTALLER_MS).println();
pw.print(KEY_EJ_LIMIT_ADDITION_SPECIAL_MS, EJ_LIMIT_ADDITION_SPECIAL_MS).println();
pw.print(KEY_EJ_WINDOW_SIZE_MS, EJ_WINDOW_SIZE_MS).println();
pw.print(KEY_EJ_TOP_APP_TIME_CHUNK_SIZE_MS, EJ_TOP_APP_TIME_CHUNK_SIZE_MS).println();
pw.print(KEY_EJ_REWARD_TOP_APP_MS, EJ_REWARD_TOP_APP_MS).println();
pw.print(KEY_EJ_REWARD_INTERACTION_MS, EJ_REWARD_INTERACTION_MS).println();
pw.print(KEY_EJ_REWARD_NOTIFICATION_SEEN_MS, EJ_REWARD_NOTIFICATION_SEEN_MS).println();
pw.print(KEY_EJ_GRACE_PERIOD_TEMP_ALLOWLIST_MS,
EJ_GRACE_PERIOD_TEMP_ALLOWLIST_MS).println();
pw.print(KEY_EJ_GRACE_PERIOD_TOP_APP_MS, EJ_GRACE_PERIOD_TOP_APP_MS).println();
pw.print(KEY_QUOTA_BUMP_ADDITIONAL_DURATION_MS,
QUOTA_BUMP_ADDITIONAL_DURATION_MS).println();
pw.print(KEY_QUOTA_BUMP_ADDITIONAL_JOB_COUNT,
QUOTA_BUMP_ADDITIONAL_JOB_COUNT).println();
pw.print(KEY_QUOTA_BUMP_ADDITIONAL_SESSION_COUNT,
QUOTA_BUMP_ADDITIONAL_SESSION_COUNT).println();
pw.print(KEY_QUOTA_BUMP_WINDOW_SIZE_MS, QUOTA_BUMP_WINDOW_SIZE_MS).println();
pw.print(KEY_QUOTA_BUMP_LIMIT, QUOTA_BUMP_LIMIT).println();
pw.decreaseIndent();
}
private void dump(ProtoOutputStream proto) {
final long qcToken = proto.start(ConstantsProto.QUOTA_CONTROLLER);
proto.write(ConstantsProto.QuotaController.IN_QUOTA_BUFFER_MS, IN_QUOTA_BUFFER_MS);
proto.write(ConstantsProto.QuotaController.ACTIVE_WINDOW_SIZE_MS,
WINDOW_SIZE_ACTIVE_MS);
proto.write(ConstantsProto.QuotaController.WORKING_WINDOW_SIZE_MS,
WINDOW_SIZE_WORKING_MS);
proto.write(ConstantsProto.QuotaController.FREQUENT_WINDOW_SIZE_MS,
WINDOW_SIZE_FREQUENT_MS);
proto.write(ConstantsProto.QuotaController.RARE_WINDOW_SIZE_MS, WINDOW_SIZE_RARE_MS);
proto.write(ConstantsProto.QuotaController.RESTRICTED_WINDOW_SIZE_MS,
WINDOW_SIZE_RESTRICTED_MS);
proto.write(ConstantsProto.QuotaController.MAX_EXECUTION_TIME_MS,
MAX_EXECUTION_TIME_MS);
proto.write(ConstantsProto.QuotaController.MAX_JOB_COUNT_ACTIVE, MAX_JOB_COUNT_ACTIVE);
proto.write(ConstantsProto.QuotaController.MAX_JOB_COUNT_WORKING,
MAX_JOB_COUNT_WORKING);
proto.write(ConstantsProto.QuotaController.MAX_JOB_COUNT_FREQUENT,
MAX_JOB_COUNT_FREQUENT);
proto.write(ConstantsProto.QuotaController.MAX_JOB_COUNT_RARE, MAX_JOB_COUNT_RARE);
proto.write(ConstantsProto.QuotaController.MAX_JOB_COUNT_RESTRICTED,
MAX_JOB_COUNT_RESTRICTED);
proto.write(ConstantsProto.QuotaController.RATE_LIMITING_WINDOW_MS,
RATE_LIMITING_WINDOW_MS);
proto.write(ConstantsProto.QuotaController.MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW,
MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW);
proto.write(ConstantsProto.QuotaController.MAX_SESSION_COUNT_ACTIVE,
MAX_SESSION_COUNT_ACTIVE);
proto.write(ConstantsProto.QuotaController.MAX_SESSION_COUNT_WORKING,
MAX_SESSION_COUNT_WORKING);
proto.write(ConstantsProto.QuotaController.MAX_SESSION_COUNT_FREQUENT,
MAX_SESSION_COUNT_FREQUENT);
proto.write(ConstantsProto.QuotaController.MAX_SESSION_COUNT_RARE,
MAX_SESSION_COUNT_RARE);
proto.write(ConstantsProto.QuotaController.MAX_SESSION_COUNT_RESTRICTED,
MAX_SESSION_COUNT_RESTRICTED);
proto.write(ConstantsProto.QuotaController.MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW,
MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW);
proto.write(ConstantsProto.QuotaController.TIMING_SESSION_COALESCING_DURATION_MS,
TIMING_SESSION_COALESCING_DURATION_MS);
proto.write(ConstantsProto.QuotaController.MIN_QUOTA_CHECK_DELAY_MS,
MIN_QUOTA_CHECK_DELAY_MS);
proto.write(ConstantsProto.QuotaController.EXPEDITED_JOB_LIMIT_ACTIVE_MS,
EJ_LIMIT_ACTIVE_MS);
proto.write(ConstantsProto.QuotaController.EXPEDITED_JOB_LIMIT_WORKING_MS,
EJ_LIMIT_WORKING_MS);
proto.write(ConstantsProto.QuotaController.EXPEDITED_JOB_LIMIT_FREQUENT_MS,
EJ_LIMIT_FREQUENT_MS);
proto.write(ConstantsProto.QuotaController.EXPEDITED_JOB_LIMIT_RARE_MS,
EJ_LIMIT_RARE_MS);
proto.write(ConstantsProto.QuotaController.EXPEDITED_JOB_LIMIT_RESTRICTED_MS,
EJ_LIMIT_RESTRICTED_MS);
proto.write(ConstantsProto.QuotaController.EXPEDITED_JOB_WINDOW_SIZE_MS,
EJ_WINDOW_SIZE_MS);
proto.write(ConstantsProto.QuotaController.EXPEDITED_JOB_TOP_APP_TIME_CHUNK_SIZE_MS,
EJ_TOP_APP_TIME_CHUNK_SIZE_MS);
proto.write(ConstantsProto.QuotaController.EXPEDITED_JOB_REWARD_TOP_APP_MS,
EJ_REWARD_TOP_APP_MS);
proto.write(ConstantsProto.QuotaController.EXPEDITED_JOB_REWARD_INTERACTION_MS,
EJ_REWARD_INTERACTION_MS);
proto.write(ConstantsProto.QuotaController.EXPEDITED_JOB_REWARD_NOTIFICATION_SEEN_MS,
EJ_REWARD_NOTIFICATION_SEEN_MS);
proto.end(qcToken);
}
}
//////////////////////// TESTING HELPERS /////////////////////////////
@VisibleForTesting
long[] getAllowedTimePerPeriodMs() {
return mAllowedTimePerPeriodMs;
}
@VisibleForTesting
@NonNull
int[] getBucketMaxJobCounts() {
return mMaxBucketJobCounts;
}
@VisibleForTesting
@NonNull
int[] getBucketMaxSessionCounts() {
return mMaxBucketSessionCounts;
}
@VisibleForTesting
@NonNull
long[] getBucketWindowSizes() {
return mBucketPeriodsMs;
}
@VisibleForTesting
@NonNull
SparseBooleanArray getForegroundUids() {
return mForegroundUids;
}
@VisibleForTesting
@NonNull
Handler getHandler() {
return mHandler;
}
@VisibleForTesting
long getEJGracePeriodTempAllowlistMs() {
return mEJGracePeriodTempAllowlistMs;
}
@VisibleForTesting
long getEJGracePeriodTopAppMs() {
return mEJGracePeriodTopAppMs;
}
@VisibleForTesting
@NonNull
long[] getEJLimitsMs() {
return mEJLimitsMs;
}
@VisibleForTesting
long getEjLimitAdditionInstallerMs() {
return mEjLimitAdditionInstallerMs;
}
@VisibleForTesting
long getEjLimitAdditionSpecialMs() {
return mEjLimitAdditionSpecialMs;
}
@VisibleForTesting
@NonNull
long getEJLimitWindowSizeMs() {
return mEJLimitWindowSizeMs;
}
@VisibleForTesting
@NonNull
long getEJRewardInteractionMs() {
return mEJRewardInteractionMs;
}
@VisibleForTesting
@NonNull
long getEJRewardNotificationSeenMs() {
return mEJRewardNotificationSeenMs;
}
@VisibleForTesting
@NonNull
long getEJRewardTopAppMs() {
return mEJRewardTopAppMs;
}
@VisibleForTesting
@Nullable
List<TimedEvent> getEJTimingSessions(int userId, String packageName) {
return mEJTimingSessions.get(userId, packageName);
}
@VisibleForTesting
@NonNull
long getEJTopAppTimeChunkSizeMs() {
return mEJTopAppTimeChunkSizeMs;
}
@VisibleForTesting
long getInQuotaBufferMs() {
return mQuotaBufferMs;
}
@VisibleForTesting
long getMaxExecutionTimeMs() {
return mMaxExecutionTimeMs;
}
@VisibleForTesting
int getMaxJobCountPerRateLimitingWindow() {
return mMaxJobCountPerRateLimitingWindow;
}
@VisibleForTesting
int getMaxSessionCountPerRateLimitingWindow() {
return mMaxSessionCountPerRateLimitingWindow;
}
@VisibleForTesting
long getMinQuotaCheckDelayMs() {
return mInQuotaAlarmQueue.getMinTimeBetweenAlarmsMs();
}
@VisibleForTesting
long getRateLimitingWindowMs() {
return mRateLimitingWindowMs;
}
@VisibleForTesting
long getTimingSessionCoalescingDurationMs() {
return mTimingSessionCoalescingDurationMs;
}
@VisibleForTesting
@Nullable
List<TimedEvent> getTimingSessions(int userId, String packageName) {
return mTimingEvents.get(userId, packageName);
}
@VisibleForTesting
@NonNull
QcConstants getQcConstants() {
return mQcConstants;
}
@VisibleForTesting
long getQuotaBumpAdditionDurationMs() {
return mQuotaBumpAdditionalDurationMs;
}
@VisibleForTesting
int getQuotaBumpAdditionJobCount() {
return mQuotaBumpAdditionalJobCount;
}
@VisibleForTesting
int getQuotaBumpAdditionSessionCount() {
return mQuotaBumpAdditionalSessionCount;
}
@VisibleForTesting
int getQuotaBumpLimit() {
return mQuotaBumpLimit;
}
@VisibleForTesting
long getQuotaBumpWindowSizeMs() {
return mQuotaBumpWindowSizeMs;
}
//////////////////////////// DATA DUMP //////////////////////////////
@NeverCompile // Avoid size overhead of debugging code.
@Override
public void dumpControllerStateLocked(final IndentingPrintWriter pw,
final Predicate<JobStatus> predicate) {
pw.println("Is enabled: " + mIsEnabled);
pw.println("Current elapsed time: " + sElapsedRealtimeClock.millis());
pw.println();
pw.print("Foreground UIDs: ");
pw.println(mForegroundUids.toString());
pw.println();
pw.print("Cached top apps: ");
pw.println(mTopAppCache.toString());
pw.print("Cached top app grace period: ");
pw.println(mTopAppGraceCache.toString());
pw.print("Cached temp allowlist: ");
pw.println(mTempAllowlistCache.toString());
pw.print("Cached temp allowlist grace period: ");
pw.println(mTempAllowlistGraceCache.toString());
pw.println();
pw.println("Special apps:");
pw.increaseIndent();
pw.print("System installers={");
for (int si = 0; si < mSystemInstallers.size(); ++si) {
if (si > 0) {
pw.print(", ");
}
pw.print(mSystemInstallers.keyAt(si));
pw.print("->");
pw.print(mSystemInstallers.get(si));
}
pw.println("}");
pw.decreaseIndent();
pw.println();
mTrackedJobs.forEach((jobs) -> {
for (int j = 0; j < jobs.size(); j++) {
final JobStatus js = jobs.valueAt(j);
if (!predicate.test(js)) {
continue;
}
pw.print("#");
js.printUniqueId(pw);
pw.print(" from ");
UserHandle.formatUid(pw, js.getSourceUid());
if (mTopStartedJobs.contains(js)) {
pw.print(" (TOP)");
}
pw.println();
pw.increaseIndent();
pw.print(JobStatus.bucketName(js.getEffectiveStandbyBucket()));
pw.print(", ");
if (js.shouldTreatAsExpeditedJob()) {
pw.print("within EJ quota");
} else if (js.startedAsExpeditedJob) {
pw.print("out of EJ quota");
} else if (js.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA)) {
pw.print("within regular quota");
} else {
pw.print("not within quota");
}
pw.print(", ");
if (js.shouldTreatAsExpeditedJob()) {
pw.print(getRemainingEJExecutionTimeLocked(
js.getSourceUserId(), js.getSourcePackageName()));
pw.print("ms remaining in EJ quota");
} else if (js.startedAsExpeditedJob) {
pw.print("should be stopped after min execution time");
} else {
pw.print(getRemainingExecutionTimeLocked(js));
pw.print("ms remaining in quota");
}
pw.println();
pw.decreaseIndent();
}
});
pw.println();
for (int u = 0; u < mPkgTimers.numMaps(); ++u) {
final int userId = mPkgTimers.keyAt(u);
for (int p = 0; p < mPkgTimers.numElementsForKey(userId); ++p) {
final String pkgName = mPkgTimers.keyAt(u, p);
mPkgTimers.valueAt(u, p).dump(pw, predicate);
pw.println();
List<TimedEvent> events = mTimingEvents.get(userId, pkgName);
if (events != null) {
pw.increaseIndent();
pw.println("Saved events:");
pw.increaseIndent();
for (int j = events.size() - 1; j >= 0; j--) {
TimedEvent event = events.get(j);
event.dump(pw);
}
pw.decreaseIndent();
pw.decreaseIndent();
pw.println();
}
}
}
pw.println();
for (int u = 0; u < mEJPkgTimers.numMaps(); ++u) {
final int userId = mEJPkgTimers.keyAt(u);
for (int p = 0; p < mEJPkgTimers.numElementsForKey(userId); ++p) {
final String pkgName = mEJPkgTimers.keyAt(u, p);
mEJPkgTimers.valueAt(u, p).dump(pw, predicate);
pw.println();
List<TimedEvent> sessions = mEJTimingSessions.get(userId, pkgName);
if (sessions != null) {
pw.increaseIndent();
pw.println("Saved sessions:");
pw.increaseIndent();
for (int j = sessions.size() - 1; j >= 0; j--) {
TimedEvent session = sessions.get(j);
session.dump(pw);
}
pw.decreaseIndent();
pw.decreaseIndent();
pw.println();
}
}
}
pw.println();
mTopAppTrackers.forEach((timer) -> timer.dump(pw));
pw.println();
pw.println("Cached execution stats:");
pw.increaseIndent();
for (int u = 0; u < mExecutionStatsCache.numMaps(); ++u) {
final int userId = mExecutionStatsCache.keyAt(u);
for (int p = 0; p < mExecutionStatsCache.numElementsForKey(userId); ++p) {
final String pkgName = mExecutionStatsCache.keyAt(u, p);
ExecutionStats[] stats = mExecutionStatsCache.valueAt(u, p);
pw.println(packageToString(userId, pkgName));
pw.increaseIndent();
for (int i = 0; i < stats.length; ++i) {
ExecutionStats executionStats = stats[i];
if (executionStats != null) {
pw.print(JobStatus.bucketName(i));
pw.print(": ");
pw.println(executionStats);
}
}
pw.decreaseIndent();
}
}
pw.decreaseIndent();
pw.println();
pw.println("EJ debits:");
pw.increaseIndent();
for (int u = 0; u < mEJStats.numMaps(); ++u) {
final int userId = mEJStats.keyAt(u);
for (int p = 0; p < mEJStats.numElementsForKey(userId); ++p) {
final String pkgName = mEJStats.keyAt(u, p);
ShrinkableDebits debits = mEJStats.valueAt(u, p);
pw.print(packageToString(userId, pkgName));
pw.print(": ");
debits.dumpLocked(pw);
}
}
pw.decreaseIndent();
pw.println();
mInQuotaAlarmQueue.dump(pw);
pw.decreaseIndent();
}
@Override
public void dumpControllerStateLocked(ProtoOutputStream proto, long fieldId,
Predicate<JobStatus> predicate) {
final long token = proto.start(fieldId);
final long mToken = proto.start(StateControllerProto.QUOTA);
proto.write(StateControllerProto.QuotaController.IS_CHARGING,
mService.isBatteryCharging());
proto.write(StateControllerProto.QuotaController.ELAPSED_REALTIME,
sElapsedRealtimeClock.millis());
for (int i = 0; i < mForegroundUids.size(); ++i) {
proto.write(StateControllerProto.QuotaController.FOREGROUND_UIDS,
mForegroundUids.keyAt(i));
}
mTrackedJobs.forEach((jobs) -> {
for (int j = 0; j < jobs.size(); j++) {
final JobStatus js = jobs.valueAt(j);
if (!predicate.test(js)) {
continue;
}
final long jsToken = proto.start(StateControllerProto.QuotaController.TRACKED_JOBS);
js.writeToShortProto(proto, StateControllerProto.QuotaController.TrackedJob.INFO);
proto.write(StateControllerProto.QuotaController.TrackedJob.SOURCE_UID,
js.getSourceUid());
proto.write(
StateControllerProto.QuotaController.TrackedJob.EFFECTIVE_STANDBY_BUCKET,
js.getEffectiveStandbyBucket());
proto.write(StateControllerProto.QuotaController.TrackedJob.IS_TOP_STARTED_JOB,
mTopStartedJobs.contains(js));
proto.write(StateControllerProto.QuotaController.TrackedJob.HAS_QUOTA,
js.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA));
proto.write(StateControllerProto.QuotaController.TrackedJob.REMAINING_QUOTA_MS,
getRemainingExecutionTimeLocked(js));
proto.write(
StateControllerProto.QuotaController.TrackedJob.IS_REQUESTED_FOREGROUND_JOB,
js.isRequestedExpeditedJob());
proto.write(
StateControllerProto.QuotaController.TrackedJob.IS_WITHIN_FG_JOB_QUOTA,
js.isExpeditedQuotaApproved());
proto.end(jsToken);
}
});
for (int u = 0; u < mPkgTimers.numMaps(); ++u) {
final int userId = mPkgTimers.keyAt(u);
for (int p = 0; p < mPkgTimers.numElementsForKey(userId); ++p) {
final String pkgName = mPkgTimers.keyAt(u, p);
final long psToken = proto.start(
StateControllerProto.QuotaController.PACKAGE_STATS);
mPkgTimers.valueAt(u, p).dump(proto,
StateControllerProto.QuotaController.PackageStats.TIMER, predicate);
final Timer ejTimer = mEJPkgTimers.get(userId, pkgName);
if (ejTimer != null) {
ejTimer.dump(proto,
StateControllerProto.QuotaController.PackageStats.FG_JOB_TIMER,
predicate);
}
List<TimedEvent> events = mTimingEvents.get(userId, pkgName);
if (events != null) {
for (int j = events.size() - 1; j >= 0; j--) {
TimedEvent event = events.get(j);
if (!(event instanceof TimingSession)) {
continue;
}
TimingSession session = (TimingSession) event;
session.dump(proto,
StateControllerProto.QuotaController.PackageStats.SAVED_SESSIONS);
}
}
ExecutionStats[] stats = mExecutionStatsCache.get(userId, pkgName);
if (stats != null) {
for (int bucketIndex = 0; bucketIndex < stats.length; ++bucketIndex) {
ExecutionStats es = stats[bucketIndex];
if (es == null) {
continue;
}
final long esToken = proto.start(
StateControllerProto.QuotaController.PackageStats.EXECUTION_STATS);
proto.write(
StateControllerProto.QuotaController.ExecutionStats.STANDBY_BUCKET,
bucketIndex);
proto.write(
StateControllerProto.QuotaController.ExecutionStats.EXPIRATION_TIME_ELAPSED,
es.expirationTimeElapsed);
proto.write(
StateControllerProto.QuotaController.ExecutionStats.WINDOW_SIZE_MS,
es.windowSizeMs);
proto.write(
StateControllerProto.QuotaController.ExecutionStats.JOB_COUNT_LIMIT,
es.jobCountLimit);
proto.write(
StateControllerProto.QuotaController.ExecutionStats.SESSION_COUNT_LIMIT,
es.sessionCountLimit);
proto.write(
StateControllerProto.QuotaController.ExecutionStats.EXECUTION_TIME_IN_WINDOW_MS,
es.executionTimeInWindowMs);
proto.write(
StateControllerProto.QuotaController.ExecutionStats.BG_JOB_COUNT_IN_WINDOW,
es.bgJobCountInWindow);
proto.write(
StateControllerProto.QuotaController.ExecutionStats.EXECUTION_TIME_IN_MAX_PERIOD_MS,
es.executionTimeInMaxPeriodMs);
proto.write(
StateControllerProto.QuotaController.ExecutionStats.BG_JOB_COUNT_IN_MAX_PERIOD,
es.bgJobCountInMaxPeriod);
proto.write(
StateControllerProto.QuotaController.ExecutionStats.SESSION_COUNT_IN_WINDOW,
es.sessionCountInWindow);
proto.write(
StateControllerProto.QuotaController.ExecutionStats.IN_QUOTA_TIME_ELAPSED,
es.inQuotaTimeElapsed);
proto.write(
StateControllerProto.QuotaController.ExecutionStats.JOB_COUNT_EXPIRATION_TIME_ELAPSED,
es.jobRateLimitExpirationTimeElapsed);
proto.write(
StateControllerProto.QuotaController.ExecutionStats.JOB_COUNT_IN_RATE_LIMITING_WINDOW,
es.jobCountInRateLimitingWindow);
proto.write(
StateControllerProto.QuotaController.ExecutionStats.SESSION_COUNT_EXPIRATION_TIME_ELAPSED,
es.sessionRateLimitExpirationTimeElapsed);
proto.write(
StateControllerProto.QuotaController.ExecutionStats.SESSION_COUNT_IN_RATE_LIMITING_WINDOW,
es.sessionCountInRateLimitingWindow);
proto.end(esToken);
}
}
proto.end(psToken);
}
}
proto.end(mToken);
proto.end(token);
}
@Override
public void dumpConstants(IndentingPrintWriter pw) {
mQcConstants.dump(pw);
}
@Override
public void dumpConstants(ProtoOutputStream proto) {
mQcConstants.dump(proto);
}
}