• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2021 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.server.job.controllers;
18 
19 import static android.text.format.DateUtils.HOUR_IN_MILLIS;
20 import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
21 
22 import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock;
23 import static com.android.server.job.JobSchedulerService.sSystemClock;
24 import static com.android.server.job.controllers.Package.packageToString;
25 
26 import android.annotation.CurrentTimeMillisLong;
27 import android.annotation.ElapsedRealtimeLong;
28 import android.annotation.NonNull;
29 import android.app.job.JobInfo;
30 import android.app.usage.UsageStatsManagerInternal;
31 import android.app.usage.UsageStatsManagerInternal.EstimatedLaunchTimeChangedListener;
32 import android.appwidget.AppWidgetManager;
33 import android.content.Context;
34 import android.os.Handler;
35 import android.os.Looper;
36 import android.os.Message;
37 import android.os.UserHandle;
38 import android.provider.DeviceConfig;
39 import android.util.ArraySet;
40 import android.util.IndentingPrintWriter;
41 import android.util.Log;
42 import android.util.Slog;
43 import android.util.SparseArrayMap;
44 import android.util.TimeUtils;
45 
46 import com.android.internal.annotations.GuardedBy;
47 import com.android.internal.annotations.VisibleForTesting;
48 import com.android.internal.os.SomeArgs;
49 import com.android.server.JobSchedulerBackgroundThread;
50 import com.android.server.LocalServices;
51 import com.android.server.job.JobSchedulerService;
52 import com.android.server.utils.AlarmQueue;
53 
54 import java.util.function.Predicate;
55 
56 /**
57  * Controller to delay prefetch jobs until we get close to an expected app launch.
58  */
59 public class PrefetchController extends StateController {
60     private static final String TAG = "JobScheduler.Prefetch";
61     private static final boolean DEBUG = JobSchedulerService.DEBUG
62             || Log.isLoggable(TAG, Log.DEBUG);
63 
64     private final PcConstants mPcConstants;
65     private final PcHandler mHandler;
66 
67     // Note: when determining prefetch bit satisfaction, we mark the bit as satisfied for apps with
68     // active widgets assuming that any prefetch jobs are being used for the widget. However, we
69     // don't have a callback telling us when widget status changes, which is incongruent with the
70     // aforementioned assumption. This inconsistency _should_ be fine since any jobs scheduled
71     // before the widget is activated are definitely not for the widget and don't have to be updated
72     // to "satisfied=true".
73     private AppWidgetManager mAppWidgetManager;
74     private final UsageStatsManagerInternal mUsageStatsManagerInternal;
75 
76     @GuardedBy("mLock")
77     private final SparseArrayMap<String, ArraySet<JobStatus>> mTrackedJobs = new SparseArrayMap<>();
78     /**
79      * Cached set of the estimated next launch times of each app. Time are in the current time
80      * millis ({@link CurrentTimeMillisLong}) timebase.
81      */
82     @GuardedBy("mLock")
83     private final SparseArrayMap<String, Long> mEstimatedLaunchTimes = new SparseArrayMap<>();
84     private final ThresholdAlarmListener mThresholdAlarmListener;
85 
86     /**
87      * The cutoff point to decide if a prefetch job is worth running or not. If the app is expected
88      * to launch within this amount of time into the future, then we will let a prefetch job run.
89      */
90     @GuardedBy("mLock")
91     @CurrentTimeMillisLong
92     private long mLaunchTimeThresholdMs = PcConstants.DEFAULT_LAUNCH_TIME_THRESHOLD_MS;
93 
94     /**
95      * The additional time we'll add to a launch time estimate before considering it obsolete and
96      * try to get a new estimate. This will help make prefetch jobs more viable in case an estimate
97      * is a few minutes early.
98      */
99     @GuardedBy("mLock")
100     private long mLaunchTimeAllowanceMs = PcConstants.DEFAULT_LAUNCH_TIME_ALLOWANCE_MS;
101 
102     @SuppressWarnings("FieldCanBeLocal")
103     private final EstimatedLaunchTimeChangedListener mEstimatedLaunchTimeChangedListener =
104             new EstimatedLaunchTimeChangedListener() {
105                 @Override
106                 public void onEstimatedLaunchTimeChanged(int userId, @NonNull String packageName,
107                         @CurrentTimeMillisLong long newEstimatedLaunchTime) {
108                     final SomeArgs args = SomeArgs.obtain();
109                     args.arg1 = packageName;
110                     args.argi1 = userId;
111                     args.argl1 = newEstimatedLaunchTime;
112                     mHandler.obtainMessage(MSG_PROCESS_UPDATED_ESTIMATED_LAUNCH_TIME, args)
113                             .sendToTarget();
114                 }
115             };
116 
117     private static final int MSG_RETRIEVE_ESTIMATED_LAUNCH_TIME = 0;
118     private static final int MSG_PROCESS_UPDATED_ESTIMATED_LAUNCH_TIME = 1;
119     private static final int MSG_PROCESS_TOP_STATE_CHANGE = 2;
120 
PrefetchController(JobSchedulerService service)121     public PrefetchController(JobSchedulerService service) {
122         super(service);
123         mPcConstants = new PcConstants();
124         mHandler = new PcHandler(mContext.getMainLooper());
125         mThresholdAlarmListener = new ThresholdAlarmListener(
126                 mContext, JobSchedulerBackgroundThread.get().getLooper());
127         mUsageStatsManagerInternal = LocalServices.getService(UsageStatsManagerInternal.class);
128 
129         mUsageStatsManagerInternal
130                 .registerLaunchTimeChangedListener(mEstimatedLaunchTimeChangedListener);
131     }
132 
133     @Override
onSystemServicesReady()134     public void onSystemServicesReady() {
135         mAppWidgetManager = mContext.getSystemService(AppWidgetManager.class);
136     }
137 
138     @Override
139     @GuardedBy("mLock")
maybeStartTrackingJobLocked(JobStatus jobStatus, JobStatus lastJob)140     public void maybeStartTrackingJobLocked(JobStatus jobStatus, JobStatus lastJob) {
141         if (jobStatus.getJob().isPrefetch()) {
142             final int userId = jobStatus.getSourceUserId();
143             final String pkgName = jobStatus.getSourcePackageName();
144             ArraySet<JobStatus> jobs = mTrackedJobs.get(userId, pkgName);
145             if (jobs == null) {
146                 jobs = new ArraySet<>();
147                 mTrackedJobs.add(userId, pkgName, jobs);
148             }
149             final long now = sSystemClock.millis();
150             final long nowElapsed = sElapsedRealtimeClock.millis();
151             if (jobs.add(jobStatus) && jobs.size() == 1
152                     && !willBeLaunchedSoonLocked(userId, pkgName, now)) {
153                 updateThresholdAlarmLocked(userId, pkgName, now, nowElapsed);
154             }
155             updateConstraintLocked(jobStatus, now, nowElapsed);
156         }
157     }
158 
159     @Override
160     @GuardedBy("mLock")
maybeStopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob, boolean forUpdate)161     public void maybeStopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob,
162             boolean forUpdate) {
163         final int userId = jobStatus.getSourceUserId();
164         final String pkgName = jobStatus.getSourcePackageName();
165         final ArraySet<JobStatus> jobs = mTrackedJobs.get(userId, pkgName);
166         if (jobs != null && jobs.remove(jobStatus) && jobs.size() == 0) {
167             mThresholdAlarmListener.removeAlarmForKey(new Package(userId, pkgName));
168         }
169     }
170 
171     @Override
172     @GuardedBy("mLock")
onAppRemovedLocked(String packageName, int uid)173     public void onAppRemovedLocked(String packageName, int uid) {
174         if (packageName == null) {
175             Slog.wtf(TAG, "Told app removed but given null package name.");
176             return;
177         }
178         final int userId = UserHandle.getUserId(uid);
179         mTrackedJobs.delete(userId, packageName);
180         mEstimatedLaunchTimes.delete(userId, packageName);
181         mThresholdAlarmListener.removeAlarmForKey(new Package(userId, packageName));
182     }
183 
184     @Override
185     @GuardedBy("mLock")
onUserRemovedLocked(int userId)186     public void onUserRemovedLocked(int userId) {
187         mTrackedJobs.delete(userId);
188         mEstimatedLaunchTimes.delete(userId);
189         mThresholdAlarmListener.removeAlarmsForUserId(userId);
190     }
191 
192     @GuardedBy("mLock")
193     @Override
onUidBiasChangedLocked(int uid, int prevBias, int newBias)194     public void onUidBiasChangedLocked(int uid, int prevBias, int newBias) {
195         final boolean isNowTop = newBias == JobInfo.BIAS_TOP_APP;
196         final boolean wasTop = prevBias == JobInfo.BIAS_TOP_APP;
197         if (isNowTop != wasTop) {
198             mHandler.obtainMessage(MSG_PROCESS_TOP_STATE_CHANGE, uid, 0).sendToTarget();
199         }
200     }
201 
202     /** Return the app's next estimated launch time. */
203     @GuardedBy("mLock")
204     @CurrentTimeMillisLong
getNextEstimatedLaunchTimeLocked(@onNull JobStatus jobStatus)205     public long getNextEstimatedLaunchTimeLocked(@NonNull JobStatus jobStatus) {
206         final int userId = jobStatus.getSourceUserId();
207         final String pkgName = jobStatus.getSourcePackageName();
208         return getNextEstimatedLaunchTimeLocked(userId, pkgName, sSystemClock.millis());
209     }
210 
211     @GuardedBy("mLock")
212     @CurrentTimeMillisLong
getNextEstimatedLaunchTimeLocked(int userId, @NonNull String pkgName, @CurrentTimeMillisLong long now)213     private long getNextEstimatedLaunchTimeLocked(int userId, @NonNull String pkgName,
214             @CurrentTimeMillisLong long now) {
215         final Long nextEstimatedLaunchTime = mEstimatedLaunchTimes.get(userId, pkgName);
216         if (nextEstimatedLaunchTime == null
217                 || nextEstimatedLaunchTime < now - mLaunchTimeAllowanceMs) {
218             // Don't query usage stats here because it may have to read from disk.
219             mHandler.obtainMessage(MSG_RETRIEVE_ESTIMATED_LAUNCH_TIME, userId, 0, pkgName)
220                     .sendToTarget();
221             // Store something in the cache so we don't keep posting retrieval messages.
222             mEstimatedLaunchTimes.add(userId, pkgName, Long.MAX_VALUE);
223             return Long.MAX_VALUE;
224         }
225         return nextEstimatedLaunchTime;
226     }
227 
228     @GuardedBy("mLock")
maybeUpdateConstraintForPkgLocked(@urrentTimeMillisLong long now, @ElapsedRealtimeLong long nowElapsed, int userId, String pkgName)229     private boolean maybeUpdateConstraintForPkgLocked(@CurrentTimeMillisLong long now,
230             @ElapsedRealtimeLong long nowElapsed, int userId, String pkgName) {
231         final ArraySet<JobStatus> jobs = mTrackedJobs.get(userId, pkgName);
232         if (jobs == null) {
233             return false;
234         }
235         boolean changed = false;
236         for (int i = 0; i < jobs.size(); i++) {
237             final JobStatus js = jobs.valueAt(i);
238             changed |= updateConstraintLocked(js, now, nowElapsed);
239         }
240         return changed;
241     }
242 
maybeUpdateConstraintForUid(int uid)243     private void maybeUpdateConstraintForUid(int uid) {
244         synchronized (mLock) {
245             final ArraySet<String> pkgs = mService.getPackagesForUidLocked(uid);
246             if (pkgs == null) {
247                 return;
248             }
249             final int userId = UserHandle.getUserId(uid);
250             final ArraySet<JobStatus> changedJobs = new ArraySet<>();
251             final long now = sSystemClock.millis();
252             final long nowElapsed = sElapsedRealtimeClock.millis();
253             for (int p = pkgs.size() - 1; p >= 0; --p) {
254                 final String pkgName = pkgs.valueAt(p);
255                 final ArraySet<JobStatus> jobs = mTrackedJobs.get(userId, pkgName);
256                 if (jobs == null) {
257                     continue;
258                 }
259                 for (int i = 0; i < jobs.size(); i++) {
260                     final JobStatus js = jobs.valueAt(i);
261                     if (updateConstraintLocked(js, now, nowElapsed)) {
262                         changedJobs.add(js);
263                     }
264                 }
265             }
266             if (changedJobs.size() > 0) {
267                 mStateChangedListener.onControllerStateChanged(changedJobs);
268             }
269         }
270     }
271 
processUpdatedEstimatedLaunchTime(int userId, @NonNull String pkgName, @CurrentTimeMillisLong long newEstimatedLaunchTime)272     private void processUpdatedEstimatedLaunchTime(int userId, @NonNull String pkgName,
273             @CurrentTimeMillisLong long newEstimatedLaunchTime) {
274         if (DEBUG) {
275             Slog.d(TAG, "Estimated launch time for " + packageToString(userId, pkgName)
276                     + " changed to " + newEstimatedLaunchTime
277                     + " ("
278                     + TimeUtils.formatDuration(newEstimatedLaunchTime - sSystemClock.millis())
279                     + " from now)");
280         }
281 
282         synchronized (mLock) {
283             final ArraySet<JobStatus> jobs = mTrackedJobs.get(userId, pkgName);
284             if (jobs == null) {
285                 if (DEBUG) {
286                     Slog.i(TAG,
287                             "Not caching launch time since we haven't seen any prefetch"
288                                     + " jobs for " + packageToString(userId, pkgName));
289                 }
290             } else {
291                 // Don't bother caching the value unless the app has scheduled prefetch jobs
292                 // before. This is based on the assumption that if an app has scheduled a
293                 // prefetch job before, then it will probably schedule another one again.
294                 mEstimatedLaunchTimes.add(userId, pkgName, newEstimatedLaunchTime);
295 
296                 if (!jobs.isEmpty()) {
297                     final long now = sSystemClock.millis();
298                     final long nowElapsed = sElapsedRealtimeClock.millis();
299                     updateThresholdAlarmLocked(userId, pkgName, now, nowElapsed);
300                     if (maybeUpdateConstraintForPkgLocked(now, nowElapsed, userId, pkgName)) {
301                         mStateChangedListener.onControllerStateChanged(jobs);
302                     }
303                 }
304             }
305         }
306     }
307 
308     @GuardedBy("mLock")
updateConstraintLocked(@onNull JobStatus jobStatus, @CurrentTimeMillisLong long now, @ElapsedRealtimeLong long nowElapsed)309     private boolean updateConstraintLocked(@NonNull JobStatus jobStatus,
310             @CurrentTimeMillisLong long now, @ElapsedRealtimeLong long nowElapsed) {
311         // Mark a prefetch constraint as satisfied in the following scenarios:
312         //   1. The app is not open but it will be launched soon
313         //   2. The app is open and the job is already running (so we let it finish)
314         //   3. The app is not open but has an active widget (we can't tell if a widget displays
315         //      status/data, so this assumes the prefetch job is to update the data displayed on
316         //      the widget).
317         final boolean appIsOpen =
318                 mService.getUidBias(jobStatus.getSourceUid()) == JobInfo.BIAS_TOP_APP;
319         final boolean satisfied;
320         if (!appIsOpen) {
321             final int userId = jobStatus.getSourceUserId();
322             final String pkgName = jobStatus.getSourcePackageName();
323             satisfied = willBeLaunchedSoonLocked(userId, pkgName, now)
324                     // At the time of implementation, isBoundWidgetPackage() results in a process ID
325                     // check and then a lookup into a map. Calling the method here every time
326                     // is based on the assumption that widgets won't change often and
327                     // AppWidgetManager won't be a bottleneck, so having a local cache won't provide
328                     // huge performance gains. If anything changes, we should reconsider having a
329                     // local cache.
330                     || (mAppWidgetManager != null
331                             && mAppWidgetManager.isBoundWidgetPackage(pkgName, userId));
332         } else {
333             satisfied = mService.isCurrentlyRunningLocked(jobStatus);
334         }
335         return jobStatus.setPrefetchConstraintSatisfied(nowElapsed, satisfied);
336     }
337 
338     @GuardedBy("mLock")
updateThresholdAlarmLocked(int userId, @NonNull String pkgName, @CurrentTimeMillisLong long now, @ElapsedRealtimeLong long nowElapsed)339     private void updateThresholdAlarmLocked(int userId, @NonNull String pkgName,
340             @CurrentTimeMillisLong long now, @ElapsedRealtimeLong long nowElapsed) {
341         final ArraySet<JobStatus> jobs = mTrackedJobs.get(userId, pkgName);
342         if (jobs == null || jobs.size() == 0) {
343             mThresholdAlarmListener.removeAlarmForKey(new Package(userId, pkgName));
344             return;
345         }
346 
347         final long nextEstimatedLaunchTime = getNextEstimatedLaunchTimeLocked(userId, pkgName, now);
348         // Avoid setting an alarm for the end of time.
349         if (nextEstimatedLaunchTime != Long.MAX_VALUE
350                 && nextEstimatedLaunchTime - now > mLaunchTimeThresholdMs) {
351             // Set alarm to be notified when this crosses the threshold.
352             final long timeToCrossThresholdMs =
353                     nextEstimatedLaunchTime - (now + mLaunchTimeThresholdMs);
354             mThresholdAlarmListener.addAlarm(new Package(userId, pkgName),
355                     nowElapsed + timeToCrossThresholdMs);
356         } else {
357             mThresholdAlarmListener.removeAlarmForKey(new Package(userId, pkgName));
358         }
359     }
360 
361     /**
362      * Returns true if the app is expected to be launched soon, where "soon" is within the next
363      * {@link #mLaunchTimeThresholdMs} time.
364      */
365     @GuardedBy("mLock")
willBeLaunchedSoonLocked(int userId, @NonNull String pkgName, @CurrentTimeMillisLong long now)366     private boolean willBeLaunchedSoonLocked(int userId, @NonNull String pkgName,
367             @CurrentTimeMillisLong long now) {
368         return getNextEstimatedLaunchTimeLocked(userId, pkgName, now)
369                 <= now + mLaunchTimeThresholdMs - mLaunchTimeAllowanceMs;
370     }
371 
372     @Override
373     @GuardedBy("mLock")
prepareForUpdatedConstantsLocked()374     public void prepareForUpdatedConstantsLocked() {
375         mPcConstants.mShouldReevaluateConstraints = false;
376     }
377 
378     @Override
379     @GuardedBy("mLock")
processConstantLocked(DeviceConfig.Properties properties, String key)380     public void processConstantLocked(DeviceConfig.Properties properties, String key) {
381         mPcConstants.processConstantLocked(properties, key);
382     }
383 
384     @Override
385     @GuardedBy("mLock")
onConstantsUpdatedLocked()386     public void onConstantsUpdatedLocked() {
387         if (mPcConstants.mShouldReevaluateConstraints) {
388             // Update job bookkeeping out of band.
389             JobSchedulerBackgroundThread.getHandler().post(() -> {
390                 final ArraySet<JobStatus> changedJobs = new ArraySet<>();
391                 synchronized (mLock) {
392                     final long nowElapsed = sElapsedRealtimeClock.millis();
393                     final long now = sSystemClock.millis();
394                     for (int u = 0; u < mTrackedJobs.numMaps(); ++u) {
395                         final int userId = mTrackedJobs.keyAt(u);
396                         for (int p = 0; p < mTrackedJobs.numElementsForKey(userId); ++p) {
397                             final String packageName = mTrackedJobs.keyAt(u, p);
398                             if (maybeUpdateConstraintForPkgLocked(
399                                     now, nowElapsed, userId, packageName)) {
400                                 changedJobs.addAll(mTrackedJobs.valueAt(u, p));
401                             }
402                             if (!willBeLaunchedSoonLocked(userId, packageName, now)) {
403                                 updateThresholdAlarmLocked(userId, packageName, now, nowElapsed);
404                             }
405                         }
406                     }
407                 }
408                 if (changedJobs.size() > 0) {
409                     mStateChangedListener.onControllerStateChanged(changedJobs);
410                 }
411             });
412         }
413     }
414 
415     /** Track when apps will cross the "will run soon" threshold. */
416     private class ThresholdAlarmListener extends AlarmQueue<Package> {
ThresholdAlarmListener(Context context, Looper looper)417         private ThresholdAlarmListener(Context context, Looper looper) {
418             super(context, looper, "*job.prefetch*", "Prefetch threshold", false,
419                     PcConstants.DEFAULT_LAUNCH_TIME_THRESHOLD_MS / 10);
420         }
421 
422         @Override
isForUser(@onNull Package key, int userId)423         protected boolean isForUser(@NonNull Package key, int userId) {
424             return key.userId == userId;
425         }
426 
427         @Override
processExpiredAlarms(@onNull ArraySet<Package> expired)428         protected void processExpiredAlarms(@NonNull ArraySet<Package> expired) {
429             final ArraySet<JobStatus> changedJobs = new ArraySet<>();
430             synchronized (mLock) {
431                 final long now = sSystemClock.millis();
432                 final long nowElapsed = sElapsedRealtimeClock.millis();
433                 for (int i = 0; i < expired.size(); ++i) {
434                     Package p = expired.valueAt(i);
435                     if (!willBeLaunchedSoonLocked(p.userId, p.packageName, now)) {
436                         Slog.e(TAG, "Alarm expired for "
437                                 + packageToString(p.userId, p.packageName) + " at the wrong time");
438                         updateThresholdAlarmLocked(p.userId, p.packageName, now, nowElapsed);
439                     } else if (maybeUpdateConstraintForPkgLocked(
440                             now, nowElapsed, p.userId, p.packageName)) {
441                         changedJobs.addAll(mTrackedJobs.get(p.userId, p.packageName));
442                     }
443                 }
444             }
445             if (changedJobs.size() > 0) {
446                 mStateChangedListener.onControllerStateChanged(changedJobs);
447             }
448         }
449     }
450 
451     private class PcHandler extends Handler {
PcHandler(Looper looper)452         PcHandler(Looper looper) {
453             super(looper);
454         }
455 
456         @Override
handleMessage(Message msg)457         public void handleMessage(Message msg) {
458             switch (msg.what) {
459                 case MSG_RETRIEVE_ESTIMATED_LAUNCH_TIME:
460                     final int userId = msg.arg1;
461                     final String pkgName = (String) msg.obj;
462                     // It's okay to get the time without holding the lock since all updates to
463                     // the local cache go through the handler (and therefore will be sequential).
464                     final long nextEstimatedLaunchTime = mUsageStatsManagerInternal
465                             .getEstimatedPackageLaunchTime(pkgName, userId);
466                     if (DEBUG) {
467                         Slog.d(TAG, "Retrieved launch time for "
468                                 + packageToString(userId, pkgName)
469                                 + " of " + nextEstimatedLaunchTime
470                                 + " (" + TimeUtils.formatDuration(
471                                         nextEstimatedLaunchTime - sSystemClock.millis())
472                                 + " from now)");
473                     }
474                     synchronized (mLock) {
475                         final Long curEstimatedLaunchTime =
476                                 mEstimatedLaunchTimes.get(userId, pkgName);
477                         if (curEstimatedLaunchTime == null
478                                 || nextEstimatedLaunchTime != curEstimatedLaunchTime) {
479                             processUpdatedEstimatedLaunchTime(
480                                     userId, pkgName, nextEstimatedLaunchTime);
481                         }
482                     }
483                     break;
484 
485                 case MSG_PROCESS_UPDATED_ESTIMATED_LAUNCH_TIME:
486                     final SomeArgs args = (SomeArgs) msg.obj;
487                     processUpdatedEstimatedLaunchTime(args.argi1, (String) args.arg1, args.argl1);
488                     args.recycle();
489                     break;
490 
491                 case MSG_PROCESS_TOP_STATE_CHANGE:
492                     final int uid = msg.arg1;
493                     maybeUpdateConstraintForUid(uid);
494                     break;
495             }
496         }
497     }
498 
499     @VisibleForTesting
500     class PcConstants {
501         private boolean mShouldReevaluateConstraints = false;
502 
503         /** Prefix to use with all constant keys in order to "sub-namespace" the keys. */
504         private static final String PC_CONSTANT_PREFIX = "pc_";
505 
506         @VisibleForTesting
507         static final String KEY_LAUNCH_TIME_THRESHOLD_MS =
508                 PC_CONSTANT_PREFIX + "launch_time_threshold_ms";
509         @VisibleForTesting
510         static final String KEY_LAUNCH_TIME_ALLOWANCE_MS =
511                 PC_CONSTANT_PREFIX + "launch_time_allowance_ms";
512 
513         private static final long DEFAULT_LAUNCH_TIME_THRESHOLD_MS = 7 * HOUR_IN_MILLIS;
514         private static final long DEFAULT_LAUNCH_TIME_ALLOWANCE_MS = 20 * MINUTE_IN_MILLIS;
515 
516         /** How much time each app will have to run jobs within their standby bucket window. */
517         public long LAUNCH_TIME_THRESHOLD_MS = DEFAULT_LAUNCH_TIME_THRESHOLD_MS;
518 
519         /**
520          * How much additional time to add to an estimated launch time before considering it
521          * unusable.
522          */
523         public long LAUNCH_TIME_ALLOWANCE_MS = DEFAULT_LAUNCH_TIME_ALLOWANCE_MS;
524 
525         @GuardedBy("mLock")
processConstantLocked(@onNull DeviceConfig.Properties properties, @NonNull String key)526         public void processConstantLocked(@NonNull DeviceConfig.Properties properties,
527                 @NonNull String key) {
528             switch (key) {
529                 case KEY_LAUNCH_TIME_ALLOWANCE_MS:
530                     LAUNCH_TIME_ALLOWANCE_MS =
531                             properties.getLong(key, DEFAULT_LAUNCH_TIME_ALLOWANCE_MS);
532                     // Limit the allowance to the range [0 minutes, 2 hours].
533                     long newLaunchTimeAllowanceMs = Math.min(2 * HOUR_IN_MILLIS,
534                             Math.max(0, LAUNCH_TIME_ALLOWANCE_MS));
535                     if (mLaunchTimeAllowanceMs != newLaunchTimeAllowanceMs) {
536                         mLaunchTimeAllowanceMs = newLaunchTimeAllowanceMs;
537                         mShouldReevaluateConstraints = true;
538                     }
539                     break;
540                 case KEY_LAUNCH_TIME_THRESHOLD_MS:
541                     LAUNCH_TIME_THRESHOLD_MS =
542                             properties.getLong(key, DEFAULT_LAUNCH_TIME_THRESHOLD_MS);
543                     // Limit the threshold to the range [1, 24] hours.
544                     long newLaunchTimeThresholdMs = Math.min(24 * HOUR_IN_MILLIS,
545                             Math.max(HOUR_IN_MILLIS, LAUNCH_TIME_THRESHOLD_MS));
546                     if (mLaunchTimeThresholdMs != newLaunchTimeThresholdMs) {
547                         mLaunchTimeThresholdMs = newLaunchTimeThresholdMs;
548                         mShouldReevaluateConstraints = true;
549                         // Give a leeway of 10% of the launch time threshold between alarms.
550                         mThresholdAlarmListener.setMinTimeBetweenAlarmsMs(
551                                 mLaunchTimeThresholdMs / 10);
552                     }
553                     break;
554             }
555         }
556 
dump(IndentingPrintWriter pw)557         private void dump(IndentingPrintWriter pw) {
558             pw.println();
559             pw.print(PrefetchController.class.getSimpleName());
560             pw.println(":");
561             pw.increaseIndent();
562 
563             pw.print(KEY_LAUNCH_TIME_THRESHOLD_MS, LAUNCH_TIME_THRESHOLD_MS).println();
564             pw.print(KEY_LAUNCH_TIME_ALLOWANCE_MS, LAUNCH_TIME_ALLOWANCE_MS).println();
565 
566             pw.decreaseIndent();
567         }
568     }
569 
570     //////////////////////// TESTING HELPERS /////////////////////////////
571 
572     @VisibleForTesting
getLaunchTimeAllowanceMs()573     long getLaunchTimeAllowanceMs() {
574         return mLaunchTimeAllowanceMs;
575     }
576 
577     @VisibleForTesting
getLaunchTimeThresholdMs()578     long getLaunchTimeThresholdMs() {
579         return mLaunchTimeThresholdMs;
580     }
581 
582     @VisibleForTesting
583     @NonNull
getPcConstants()584     PcConstants getPcConstants() {
585         return mPcConstants;
586     }
587 
588     //////////////////////////// DATA DUMP //////////////////////////////
589 
590     @Override
591     @GuardedBy("mLock")
dumpControllerStateLocked(IndentingPrintWriter pw, Predicate<JobStatus> predicate)592     public void dumpControllerStateLocked(IndentingPrintWriter pw, Predicate<JobStatus> predicate) {
593         final long now = sSystemClock.millis();
594 
595         pw.println("Cached launch times:");
596         pw.increaseIndent();
597         for (int u = 0; u < mEstimatedLaunchTimes.numMaps(); ++u) {
598             final int userId = mEstimatedLaunchTimes.keyAt(u);
599             for (int p = 0; p < mEstimatedLaunchTimes.numElementsForKey(userId); ++p) {
600                 final String pkgName = mEstimatedLaunchTimes.keyAt(u, p);
601                 final long estimatedLaunchTime = mEstimatedLaunchTimes.valueAt(u, p);
602 
603                 pw.print(packageToString(userId, pkgName));
604                 pw.print(": ");
605                 pw.print(estimatedLaunchTime);
606                 pw.print(" (");
607                 TimeUtils.formatDuration(estimatedLaunchTime - now, pw,
608                         TimeUtils.HUNDRED_DAY_FIELD_LEN);
609                 pw.println(" from now)");
610             }
611         }
612         pw.decreaseIndent();
613 
614         pw.println();
615         mTrackedJobs.forEach((jobs) -> {
616             for (int j = 0; j < jobs.size(); j++) {
617                 final JobStatus js = jobs.valueAt(j);
618                 if (!predicate.test(js)) {
619                     continue;
620                 }
621                 pw.print("#");
622                 js.printUniqueId(pw);
623                 pw.print(" from ");
624                 UserHandle.formatUid(pw, js.getSourceUid());
625                 pw.println();
626             }
627         });
628 
629         pw.println();
630         mThresholdAlarmListener.dump(pw);
631     }
632 
633     @Override
dumpConstants(IndentingPrintWriter pw)634     public void dumpConstants(IndentingPrintWriter pw) {
635         mPcConstants.dump(pw);
636     }
637 }
638