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