blob: 2b7438c862bd0f53444d377ee565c0cb89f5a12b [file] [log] [blame]
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server.job.controllers;
import static android.text.format.DateUtils.HOUR_IN_MILLIS;
import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock;
import static com.android.server.job.JobSchedulerService.sSystemClock;
import android.annotation.CurrentTimeMillisLong;
import android.annotation.ElapsedRealtimeLong;
import android.annotation.NonNull;
import android.app.job.JobInfo;
import android.app.usage.UsageStatsManagerInternal;
import android.app.usage.UsageStatsManagerInternal.EstimatedLaunchTimeChangedListener;
import android.appwidget.AppWidgetManager;
import android.content.Context;
import android.content.pm.UserPackage;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
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.SparseArrayMap;
import android.util.TimeUtils;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.os.SomeArgs;
import com.android.server.AppSchedulingModuleThread;
import com.android.server.LocalServices;
import com.android.server.job.JobSchedulerService;
import com.android.server.utils.AlarmQueue;
import java.util.function.Predicate;
/**
* Controller to delay prefetch jobs until we get close to an expected app launch.
*/
public class PrefetchController extends StateController {
private static final String TAG = "JobScheduler.Prefetch";
private static final boolean DEBUG = JobSchedulerService.DEBUG
|| Log.isLoggable(TAG, Log.DEBUG);
private final PcConstants mPcConstants;
private final PcHandler mHandler;
// Note: when determining prefetch bit satisfaction, we mark the bit as satisfied for apps with
// active widgets assuming that any prefetch jobs are being used for the widget. However, we
// don't have a callback telling us when widget status changes, which is incongruent with the
// aforementioned assumption. This inconsistency _should_ be fine since any jobs scheduled
// before the widget is activated are definitely not for the widget and don't have to be updated
// to "satisfied=true".
private AppWidgetManager mAppWidgetManager;
private final UsageStatsManagerInternal mUsageStatsManagerInternal;
@GuardedBy("mLock")
private final SparseArrayMap<String, ArraySet<JobStatus>> mTrackedJobs = new SparseArrayMap<>();
/**
* Cached set of the estimated next launch times of each app. Time are in the current time
* millis ({@link CurrentTimeMillisLong}) timebase.
*/
@GuardedBy("mLock")
private final SparseArrayMap<String, Long> mEstimatedLaunchTimes = new SparseArrayMap<>();
@GuardedBy("mLock")
private final ArraySet<PrefetchChangedListener> mPrefetchChangedListeners = new ArraySet<>();
private final ThresholdAlarmListener mThresholdAlarmListener;
/**
* The cutoff point to decide if a prefetch job is worth running or not. If the app is expected
* to launch within this amount of time into the future, then we will let a prefetch job run.
*/
@GuardedBy("mLock")
@CurrentTimeMillisLong
private long mLaunchTimeThresholdMs = PcConstants.DEFAULT_LAUNCH_TIME_THRESHOLD_MS;
/**
* The additional time we'll add to a launch time estimate before considering it obsolete and
* try to get a new estimate. This will help make prefetch jobs more viable in case an estimate
* is a few minutes early.
*/
@GuardedBy("mLock")
private long mLaunchTimeAllowanceMs = PcConstants.DEFAULT_LAUNCH_TIME_ALLOWANCE_MS;
/** Called by Prefetch Controller after local cache has been updated */
public interface PrefetchChangedListener {
/** Callback to inform listeners when estimated launch times change. */
void onPrefetchCacheUpdated(ArraySet<JobStatus> jobs, int userId, String pkgName,
long prevEstimatedLaunchTime, long newEstimatedLaunchTime, long nowElapsed);
}
@SuppressWarnings("FieldCanBeLocal")
private final EstimatedLaunchTimeChangedListener mEstimatedLaunchTimeChangedListener =
new EstimatedLaunchTimeChangedListener() {
@Override
public void onEstimatedLaunchTimeChanged(int userId, @NonNull String packageName,
@CurrentTimeMillisLong long newEstimatedLaunchTime) {
final SomeArgs args = SomeArgs.obtain();
args.arg1 = packageName;
args.argi1 = userId;
args.argl1 = newEstimatedLaunchTime;
mHandler.obtainMessage(MSG_PROCESS_UPDATED_ESTIMATED_LAUNCH_TIME, args)
.sendToTarget();
}
};
private static final int MSG_RETRIEVE_ESTIMATED_LAUNCH_TIME = 0;
private static final int MSG_PROCESS_UPDATED_ESTIMATED_LAUNCH_TIME = 1;
private static final int MSG_PROCESS_TOP_STATE_CHANGE = 2;
public PrefetchController(JobSchedulerService service) {
super(service);
mPcConstants = new PcConstants();
mHandler = new PcHandler(AppSchedulingModuleThread.get().getLooper());
mThresholdAlarmListener = new ThresholdAlarmListener(
mContext, AppSchedulingModuleThread.get().getLooper());
mUsageStatsManagerInternal = LocalServices.getService(UsageStatsManagerInternal.class);
mUsageStatsManagerInternal
.registerLaunchTimeChangedListener(mEstimatedLaunchTimeChangedListener);
}
@Override
public void onSystemServicesReady() {
mAppWidgetManager = mContext.getSystemService(AppWidgetManager.class);
}
@Override
@GuardedBy("mLock")
public void maybeStartTrackingJobLocked(JobStatus jobStatus, JobStatus lastJob) {
if (jobStatus.getJob().isPrefetch()) {
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);
}
final long now = sSystemClock.millis();
final long nowElapsed = sElapsedRealtimeClock.millis();
if (jobs.add(jobStatus) && jobs.size() == 1
&& !willBeLaunchedSoonLocked(userId, pkgName, now)) {
updateThresholdAlarmLocked(userId, pkgName, now, nowElapsed);
}
updateConstraintLocked(jobStatus, now, nowElapsed);
}
}
@Override
@GuardedBy("mLock")
public void maybeStopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob) {
final int userId = jobStatus.getSourceUserId();
final String pkgName = jobStatus.getSourcePackageName();
final ArraySet<JobStatus> jobs = mTrackedJobs.get(userId, pkgName);
if (jobs != null && jobs.remove(jobStatus) && jobs.size() == 0) {
mThresholdAlarmListener.removeAlarmForKey(UserPackage.of(userId, pkgName));
}
}
@Override
@GuardedBy("mLock")
public void onAppRemovedLocked(String packageName, int uid) {
if (packageName == null) {
Slog.wtf(TAG, "Told app removed but given null package name.");
return;
}
final int userId = UserHandle.getUserId(uid);
mTrackedJobs.delete(userId, packageName);
mEstimatedLaunchTimes.delete(userId, packageName);
mThresholdAlarmListener.removeAlarmForKey(UserPackage.of(userId, packageName));
}
@Override
@GuardedBy("mLock")
public void onUserRemovedLocked(int userId) {
mTrackedJobs.delete(userId);
mEstimatedLaunchTimes.delete(userId);
mThresholdAlarmListener.removeAlarmsForUserId(userId);
}
@GuardedBy("mLock")
@Override
public void onUidBiasChangedLocked(int uid, int prevBias, int newBias) {
final boolean isNowTop = newBias == JobInfo.BIAS_TOP_APP;
final boolean wasTop = prevBias == JobInfo.BIAS_TOP_APP;
if (isNowTop != wasTop) {
mHandler.obtainMessage(MSG_PROCESS_TOP_STATE_CHANGE, uid, 0).sendToTarget();
}
}
/** Return the app's next estimated launch time. */
@GuardedBy("mLock")
@CurrentTimeMillisLong
public long getNextEstimatedLaunchTimeLocked(@NonNull JobStatus jobStatus) {
final int userId = jobStatus.getSourceUserId();
final String pkgName = jobStatus.getSourcePackageName();
return getNextEstimatedLaunchTimeLocked(userId, pkgName, sSystemClock.millis());
}
@GuardedBy("mLock")
@CurrentTimeMillisLong
private long getNextEstimatedLaunchTimeLocked(int userId, @NonNull String pkgName,
@CurrentTimeMillisLong long now) {
final Long nextEstimatedLaunchTime = mEstimatedLaunchTimes.get(userId, pkgName);
if (nextEstimatedLaunchTime == null
|| nextEstimatedLaunchTime < now - mLaunchTimeAllowanceMs) {
// Don't query usage stats here because it may have to read from disk.
mHandler.obtainMessage(MSG_RETRIEVE_ESTIMATED_LAUNCH_TIME, userId, 0, pkgName)
.sendToTarget();
// Store something in the cache so we don't keep posting retrieval messages.
mEstimatedLaunchTimes.add(userId, pkgName, Long.MAX_VALUE);
return Long.MAX_VALUE;
}
return nextEstimatedLaunchTime;
}
@GuardedBy("mLock")
private boolean maybeUpdateConstraintForPkgLocked(@CurrentTimeMillisLong long now,
@ElapsedRealtimeLong long nowElapsed, int userId, String pkgName) {
final ArraySet<JobStatus> jobs = mTrackedJobs.get(userId, pkgName);
if (jobs == null) {
return false;
}
boolean changed = false;
for (int i = 0; i < jobs.size(); i++) {
final JobStatus js = jobs.valueAt(i);
changed |= updateConstraintLocked(js, now, nowElapsed);
}
return changed;
}
private void maybeUpdateConstraintForUid(int uid) {
synchronized (mLock) {
final ArraySet<String> pkgs = mService.getPackagesForUidLocked(uid);
if (pkgs == null) {
return;
}
final int userId = UserHandle.getUserId(uid);
final ArraySet<JobStatus> changedJobs = new ArraySet<>();
final long now = sSystemClock.millis();
final long nowElapsed = sElapsedRealtimeClock.millis();
for (int p = pkgs.size() - 1; p >= 0; --p) {
final String pkgName = pkgs.valueAt(p);
final ArraySet<JobStatus> jobs = mTrackedJobs.get(userId, pkgName);
if (jobs == null) {
continue;
}
for (int i = 0; i < jobs.size(); i++) {
final JobStatus js = jobs.valueAt(i);
if (updateConstraintLocked(js, now, nowElapsed)) {
changedJobs.add(js);
}
}
}
if (changedJobs.size() > 0) {
mStateChangedListener.onControllerStateChanged(changedJobs);
}
}
}
private void processUpdatedEstimatedLaunchTime(int userId, @NonNull String pkgName,
@CurrentTimeMillisLong long newEstimatedLaunchTime) {
if (DEBUG) {
Slog.d(TAG, "Estimated launch time for " + packageToString(userId, pkgName)
+ " changed to " + newEstimatedLaunchTime
+ " ("
+ TimeUtils.formatDuration(newEstimatedLaunchTime - sSystemClock.millis())
+ " from now)");
}
synchronized (mLock) {
final ArraySet<JobStatus> jobs = mTrackedJobs.get(userId, pkgName);
if (jobs == null) {
if (DEBUG) {
Slog.i(TAG,
"Not caching launch time since we haven't seen any prefetch"
+ " jobs for " + packageToString(userId, pkgName));
}
} else {
// Don't bother caching the value unless the app has scheduled prefetch jobs
// before. This is based on the assumption that if an app has scheduled a
// prefetch job before, then it will probably schedule another one again.
final long prevEstimatedLaunchTime = mEstimatedLaunchTimes.get(userId, pkgName);
mEstimatedLaunchTimes.add(userId, pkgName, newEstimatedLaunchTime);
if (!jobs.isEmpty()) {
final long now = sSystemClock.millis();
final long nowElapsed = sElapsedRealtimeClock.millis();
updateThresholdAlarmLocked(userId, pkgName, now, nowElapsed);
for (int i = 0; i < mPrefetchChangedListeners.size(); i++) {
mPrefetchChangedListeners.valueAt(i).onPrefetchCacheUpdated(
jobs, userId, pkgName, prevEstimatedLaunchTime,
newEstimatedLaunchTime, nowElapsed);
}
if (maybeUpdateConstraintForPkgLocked(now, nowElapsed, userId, pkgName)) {
mStateChangedListener.onControllerStateChanged(jobs);
}
}
}
}
}
@GuardedBy("mLock")
private boolean updateConstraintLocked(@NonNull JobStatus jobStatus,
@CurrentTimeMillisLong long now, @ElapsedRealtimeLong long nowElapsed) {
// Mark a prefetch constraint as satisfied in the following scenarios:
// 1. The app is not open but it will be launched soon
// 2. The app is open and the job is already running (so we let it finish)
// 3. The app is not open but has an active widget (we can't tell if a widget displays
// status/data, so this assumes the prefetch job is to update the data displayed on
// the widget).
final boolean appIsOpen =
mService.getUidBias(jobStatus.getSourceUid()) == JobInfo.BIAS_TOP_APP;
final boolean satisfied;
if (!appIsOpen) {
final int userId = jobStatus.getSourceUserId();
final String pkgName = jobStatus.getSourcePackageName();
satisfied = willBeLaunchedSoonLocked(userId, pkgName, now)
// At the time of implementation, isBoundWidgetPackage() results in a process ID
// check and then a lookup into a map. Calling the method here every time
// is based on the assumption that widgets won't change often and
// AppWidgetManager won't be a bottleneck, so having a local cache won't provide
// huge performance gains. If anything changes, we should reconsider having a
// local cache.
|| (mAppWidgetManager != null
&& mAppWidgetManager.isBoundWidgetPackage(pkgName, userId));
} else {
satisfied = mService.isCurrentlyRunningLocked(jobStatus);
}
return jobStatus.setPrefetchConstraintSatisfied(nowElapsed, satisfied);
}
@GuardedBy("mLock")
private void updateThresholdAlarmLocked(int userId, @NonNull String pkgName,
@CurrentTimeMillisLong long now, @ElapsedRealtimeLong long nowElapsed) {
final ArraySet<JobStatus> jobs = mTrackedJobs.get(userId, pkgName);
if (jobs == null || jobs.size() == 0) {
mThresholdAlarmListener.removeAlarmForKey(UserPackage.of(userId, pkgName));
return;
}
final long nextEstimatedLaunchTime = getNextEstimatedLaunchTimeLocked(userId, pkgName, now);
// Avoid setting an alarm for the end of time.
if (nextEstimatedLaunchTime != Long.MAX_VALUE
&& nextEstimatedLaunchTime - now > mLaunchTimeThresholdMs) {
// Set alarm to be notified when this crosses the threshold.
final long timeToCrossThresholdMs =
nextEstimatedLaunchTime - (now + mLaunchTimeThresholdMs);
mThresholdAlarmListener.addAlarm(UserPackage.of(userId, pkgName),
nowElapsed + timeToCrossThresholdMs);
} else {
mThresholdAlarmListener.removeAlarmForKey(UserPackage.of(userId, pkgName));
}
}
/**
* Returns true if the app is expected to be launched soon, where "soon" is within the next
* {@link #mLaunchTimeThresholdMs} time.
*/
@GuardedBy("mLock")
private boolean willBeLaunchedSoonLocked(int userId, @NonNull String pkgName,
@CurrentTimeMillisLong long now) {
return getNextEstimatedLaunchTimeLocked(userId, pkgName, now)
<= now + mLaunchTimeThresholdMs - mLaunchTimeAllowanceMs;
}
@Override
@GuardedBy("mLock")
public void prepareForUpdatedConstantsLocked() {
mPcConstants.mShouldReevaluateConstraints = false;
}
@Override
@GuardedBy("mLock")
public void processConstantLocked(DeviceConfig.Properties properties, String key) {
mPcConstants.processConstantLocked(properties, key);
}
@Override
@GuardedBy("mLock")
public void onConstantsUpdatedLocked() {
if (mPcConstants.mShouldReevaluateConstraints) {
// Update job bookkeeping out of band.
AppSchedulingModuleThread.getHandler().post(() -> {
final ArraySet<JobStatus> changedJobs = new ArraySet<>();
synchronized (mLock) {
final long nowElapsed = sElapsedRealtimeClock.millis();
final long now = sSystemClock.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);
if (maybeUpdateConstraintForPkgLocked(
now, nowElapsed, userId, packageName)) {
changedJobs.addAll(mTrackedJobs.valueAt(u, p));
}
if (!willBeLaunchedSoonLocked(userId, packageName, now)) {
updateThresholdAlarmLocked(userId, packageName, now, nowElapsed);
}
}
}
}
if (changedJobs.size() > 0) {
mStateChangedListener.onControllerStateChanged(changedJobs);
}
});
}
}
/** Track when apps will cross the "will run soon" threshold. */
private class ThresholdAlarmListener extends AlarmQueue<UserPackage> {
private ThresholdAlarmListener(Context context, Looper looper) {
super(context, looper, "*job.prefetch*", "Prefetch threshold", false,
PcConstants.DEFAULT_LAUNCH_TIME_THRESHOLD_MS / 10);
}
@Override
protected boolean isForUser(@NonNull UserPackage key, int userId) {
return key.userId == userId;
}
@Override
protected void processExpiredAlarms(@NonNull ArraySet<UserPackage> expired) {
final ArraySet<JobStatus> changedJobs = new ArraySet<>();
synchronized (mLock) {
final long now = sSystemClock.millis();
final long nowElapsed = sElapsedRealtimeClock.millis();
for (int i = 0; i < expired.size(); ++i) {
UserPackage p = expired.valueAt(i);
if (!willBeLaunchedSoonLocked(p.userId, p.packageName, now)) {
Slog.e(TAG, "Alarm expired for "
+ packageToString(p.userId, p.packageName) + " at the wrong time");
updateThresholdAlarmLocked(p.userId, p.packageName, now, nowElapsed);
} else if (maybeUpdateConstraintForPkgLocked(
now, nowElapsed, p.userId, p.packageName)) {
changedJobs.addAll(mTrackedJobs.get(p.userId, p.packageName));
}
}
}
if (changedJobs.size() > 0) {
mStateChangedListener.onControllerStateChanged(changedJobs);
}
}
}
void registerPrefetchChangedListener(PrefetchChangedListener listener) {
synchronized (mLock) {
mPrefetchChangedListeners.add(listener);
}
}
void unRegisterPrefetchChangedListener(PrefetchChangedListener listener) {
synchronized (mLock) {
mPrefetchChangedListeners.remove(listener);
}
}
private class PcHandler extends Handler {
PcHandler(Looper looper) {
super(looper);
}
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_RETRIEVE_ESTIMATED_LAUNCH_TIME:
final int userId = msg.arg1;
final String pkgName = (String) msg.obj;
// It's okay to get the time without holding the lock since all updates to
// the local cache go through the handler (and therefore will be sequential).
final long nextEstimatedLaunchTime = mUsageStatsManagerInternal
.getEstimatedPackageLaunchTime(pkgName, userId);
if (DEBUG) {
Slog.d(TAG, "Retrieved launch time for "
+ packageToString(userId, pkgName)
+ " of " + nextEstimatedLaunchTime
+ " (" + TimeUtils.formatDuration(
nextEstimatedLaunchTime - sSystemClock.millis())
+ " from now)");
}
synchronized (mLock) {
final Long curEstimatedLaunchTime =
mEstimatedLaunchTimes.get(userId, pkgName);
if (curEstimatedLaunchTime == null
|| nextEstimatedLaunchTime != curEstimatedLaunchTime) {
processUpdatedEstimatedLaunchTime(
userId, pkgName, nextEstimatedLaunchTime);
}
}
break;
case MSG_PROCESS_UPDATED_ESTIMATED_LAUNCH_TIME:
final SomeArgs args = (SomeArgs) msg.obj;
processUpdatedEstimatedLaunchTime(args.argi1, (String) args.arg1, args.argl1);
args.recycle();
break;
case MSG_PROCESS_TOP_STATE_CHANGE:
final int uid = msg.arg1;
maybeUpdateConstraintForUid(uid);
break;
}
}
}
@VisibleForTesting
class PcConstants {
private boolean mShouldReevaluateConstraints = false;
/** Prefix to use with all constant keys in order to "sub-namespace" the keys. */
private static final String PC_CONSTANT_PREFIX = "pc_";
@VisibleForTesting
static final String KEY_LAUNCH_TIME_THRESHOLD_MS =
PC_CONSTANT_PREFIX + "launch_time_threshold_ms";
@VisibleForTesting
static final String KEY_LAUNCH_TIME_ALLOWANCE_MS =
PC_CONSTANT_PREFIX + "launch_time_allowance_ms";
private static final long DEFAULT_LAUNCH_TIME_THRESHOLD_MS = 7 * HOUR_IN_MILLIS;
private static final long DEFAULT_LAUNCH_TIME_ALLOWANCE_MS = 20 * MINUTE_IN_MILLIS;
/** How much time each app will have to run jobs within their standby bucket window. */
public long LAUNCH_TIME_THRESHOLD_MS = DEFAULT_LAUNCH_TIME_THRESHOLD_MS;
/**
* How much additional time to add to an estimated launch time before considering it
* unusable.
*/
public long LAUNCH_TIME_ALLOWANCE_MS = DEFAULT_LAUNCH_TIME_ALLOWANCE_MS;
@GuardedBy("mLock")
public void processConstantLocked(@NonNull DeviceConfig.Properties properties,
@NonNull String key) {
switch (key) {
case KEY_LAUNCH_TIME_ALLOWANCE_MS:
LAUNCH_TIME_ALLOWANCE_MS =
properties.getLong(key, DEFAULT_LAUNCH_TIME_ALLOWANCE_MS);
// Limit the allowance to the range [0 minutes, 2 hours].
long newLaunchTimeAllowanceMs = Math.min(2 * HOUR_IN_MILLIS,
Math.max(0, LAUNCH_TIME_ALLOWANCE_MS));
if (mLaunchTimeAllowanceMs != newLaunchTimeAllowanceMs) {
mLaunchTimeAllowanceMs = newLaunchTimeAllowanceMs;
mShouldReevaluateConstraints = true;
}
break;
case KEY_LAUNCH_TIME_THRESHOLD_MS:
LAUNCH_TIME_THRESHOLD_MS =
properties.getLong(key, DEFAULT_LAUNCH_TIME_THRESHOLD_MS);
// Limit the threshold to the range [1, 24] hours.
long newLaunchTimeThresholdMs = Math.min(24 * HOUR_IN_MILLIS,
Math.max(HOUR_IN_MILLIS, LAUNCH_TIME_THRESHOLD_MS));
if (mLaunchTimeThresholdMs != newLaunchTimeThresholdMs) {
mLaunchTimeThresholdMs = newLaunchTimeThresholdMs;
mShouldReevaluateConstraints = true;
// Give a leeway of 10% of the launch time threshold between alarms.
mThresholdAlarmListener.setMinTimeBetweenAlarmsMs(
mLaunchTimeThresholdMs / 10);
}
break;
}
}
private void dump(IndentingPrintWriter pw) {
pw.println();
pw.print(PrefetchController.class.getSimpleName());
pw.println(":");
pw.increaseIndent();
pw.print(KEY_LAUNCH_TIME_THRESHOLD_MS, LAUNCH_TIME_THRESHOLD_MS).println();
pw.print(KEY_LAUNCH_TIME_ALLOWANCE_MS, LAUNCH_TIME_ALLOWANCE_MS).println();
pw.decreaseIndent();
}
}
//////////////////////// TESTING HELPERS /////////////////////////////
@VisibleForTesting
long getLaunchTimeAllowanceMs() {
return mLaunchTimeAllowanceMs;
}
@VisibleForTesting
long getLaunchTimeThresholdMs() {
return mLaunchTimeThresholdMs;
}
@VisibleForTesting
@NonNull
PcConstants getPcConstants() {
return mPcConstants;
}
//////////////////////////// DATA DUMP //////////////////////////////
@Override
@GuardedBy("mLock")
public void dumpControllerStateLocked(IndentingPrintWriter pw, Predicate<JobStatus> predicate) {
final long now = sSystemClock.millis();
pw.println("Cached launch times:");
pw.increaseIndent();
for (int u = 0; u < mEstimatedLaunchTimes.numMaps(); ++u) {
final int userId = mEstimatedLaunchTimes.keyAt(u);
for (int p = 0; p < mEstimatedLaunchTimes.numElementsForKey(userId); ++p) {
final String pkgName = mEstimatedLaunchTimes.keyAt(u, p);
final long estimatedLaunchTime = mEstimatedLaunchTimes.valueAt(u, p);
pw.print(packageToString(userId, pkgName));
pw.print(": ");
pw.print(estimatedLaunchTime);
pw.print(" (");
TimeUtils.formatDuration(estimatedLaunchTime - now, pw,
TimeUtils.HUNDRED_DAY_FIELD_LEN);
pw.println(" from now)");
}
}
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());
pw.println();
}
});
pw.println();
mThresholdAlarmListener.dump(pw);
}
@Override
public void dumpConstants(IndentingPrintWriter pw) {
mPcConstants.dump(pw);
}
}