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