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