• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2014 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.pm;
18 
19 import static com.android.server.pm.PackageManagerService.DEBUG_DEXOPT;
20 import static com.android.server.pm.PackageManagerService.PLATFORM_PACKAGE_NAME;
21 
22 import android.annotation.Nullable;
23 import android.app.job.JobInfo;
24 import android.app.job.JobParameters;
25 import android.app.job.JobScheduler;
26 import android.app.job.JobService;
27 import android.content.ComponentName;
28 import android.content.Context;
29 import android.content.Intent;
30 import android.content.IntentFilter;
31 import android.content.pm.PackageInfo;
32 import android.os.BatteryManager;
33 import android.os.Environment;
34 import android.os.ServiceManager;
35 import android.os.SystemProperties;
36 import android.os.UserHandle;
37 import android.os.storage.StorageManager;
38 import android.util.ArraySet;
39 import android.util.Log;
40 
41 import com.android.internal.util.ArrayUtils;
42 import com.android.internal.util.FrameworkStatsLog;
43 import com.android.server.LocalServices;
44 import com.android.server.PinnerService;
45 import com.android.server.pm.dex.DexManager;
46 import com.android.server.pm.dex.DexoptOptions;
47 
48 import java.io.File;
49 import java.nio.file.Paths;
50 import java.util.ArrayList;
51 import java.util.List;
52 import java.util.Set;
53 import java.util.concurrent.TimeUnit;
54 import java.util.concurrent.atomic.AtomicBoolean;
55 import java.util.function.Supplier;
56 
57 /**
58  * {@hide}
59  */
60 public class BackgroundDexOptService extends JobService {
61     private static final String TAG = "BackgroundDexOptService";
62 
63     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
64 
65     private static final int JOB_IDLE_OPTIMIZE = 800;
66     private static final int JOB_POST_BOOT_UPDATE = 801;
67 
68     private static final long IDLE_OPTIMIZATION_PERIOD = DEBUG
69             ? TimeUnit.MINUTES.toMillis(1)
70             : TimeUnit.DAYS.toMillis(1);
71 
72     private static ComponentName sDexoptServiceName = new ComponentName(
73             "android",
74             BackgroundDexOptService.class.getName());
75 
76     // Possible return codes of individual optimization steps.
77 
78     // Optimizations finished. All packages were processed.
79     private static final int OPTIMIZE_PROCESSED = 0;
80     // Optimizations should continue. Issued after checking the scheduler, disk space or battery.
81     private static final int OPTIMIZE_CONTINUE = 1;
82     // Optimizations should be aborted. Job scheduler requested it.
83     private static final int OPTIMIZE_ABORT_BY_JOB_SCHEDULER = 2;
84     // Optimizations should be aborted. No space left on device.
85     private static final int OPTIMIZE_ABORT_NO_SPACE_LEFT = 3;
86 
87     // Used for calculating space threshold for downgrading unused apps.
88     private static final int LOW_THRESHOLD_MULTIPLIER_FOR_DOWNGRADE = 2;
89 
90     /**
91      * Set of failed packages remembered across job runs.
92      */
93     static final ArraySet<String> sFailedPackageNamesPrimary = new ArraySet<String>();
94     static final ArraySet<String> sFailedPackageNamesSecondary = new ArraySet<String>();
95 
96     /**
97      * Atomics set to true if the JobScheduler requests an abort.
98      */
99     private final AtomicBoolean mAbortPostBootUpdate = new AtomicBoolean(false);
100     private final AtomicBoolean mAbortIdleOptimization = new AtomicBoolean(false);
101 
102     /**
103      * Atomic set to true if one job should exit early because another job was started.
104      */
105     private final AtomicBoolean mExitPostBootUpdate = new AtomicBoolean(false);
106 
107     private final File mDataDir = Environment.getDataDirectory();
108     private static final long mDowngradeUnusedAppsThresholdInMillis =
109             getDowngradeUnusedAppsThresholdInMillis();
110 
111     private static List<PackagesUpdatedListener> sPackagesUpdatedListeners = new ArrayList<>();
112 
schedule(Context context)113     public static void schedule(Context context) {
114         if (isBackgroundDexoptDisabled()) {
115             return;
116         }
117 
118         JobScheduler js = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
119 
120         // Schedule a one-off job which scans installed packages and updates
121         // out-of-date oat files.
122         js.schedule(new JobInfo.Builder(JOB_POST_BOOT_UPDATE, sDexoptServiceName)
123                     .setMinimumLatency(TimeUnit.MINUTES.toMillis(1))
124                     .setOverrideDeadline(TimeUnit.MINUTES.toMillis(1))
125                     .build());
126 
127         // Schedule a daily job which scans installed packages and compiles
128         // those with fresh profiling data.
129         js.schedule(new JobInfo.Builder(JOB_IDLE_OPTIMIZE, sDexoptServiceName)
130                     .setRequiresDeviceIdle(true)
131                     .setRequiresCharging(true)
132                     .setPeriodic(IDLE_OPTIMIZATION_PERIOD)
133                     .build());
134 
135         if (DEBUG_DEXOPT) {
136             Log.i(TAG, "Jobs scheduled");
137         }
138     }
139 
notifyPackageChanged(String packageName)140     public static void notifyPackageChanged(String packageName) {
141         // The idle maintanance job skips packages which previously failed to
142         // compile. The given package has changed and may successfully compile
143         // now. Remove it from the list of known failing packages.
144         synchronized (sFailedPackageNamesPrimary) {
145             sFailedPackageNamesPrimary.remove(packageName);
146         }
147         synchronized (sFailedPackageNamesSecondary) {
148             sFailedPackageNamesSecondary.remove(packageName);
149         }
150     }
151 
152     // Returns the current battery level as a 0-100 integer.
getBatteryLevel()153     private int getBatteryLevel() {
154         IntentFilter filter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
155         Intent intent = registerReceiver(null, filter);
156         int level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1);
157         int scale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1);
158         boolean present = intent.getBooleanExtra(BatteryManager.EXTRA_PRESENT, true);
159 
160         if (!present) {
161             // No battery, treat as if 100%, no possibility of draining battery.
162             return 100;
163         }
164 
165         if (level < 0 || scale <= 0) {
166             // Battery data unavailable. This should never happen, so assume the worst.
167             return 0;
168         }
169 
170         return (100 * level / scale);
171     }
172 
getLowStorageThreshold(Context context)173     private long getLowStorageThreshold(Context context) {
174         @SuppressWarnings("deprecation")
175         final long lowThreshold = StorageManager.from(context).getStorageLowBytes(mDataDir);
176         if (lowThreshold == 0) {
177             Log.e(TAG, "Invalid low storage threshold");
178         }
179 
180         return lowThreshold;
181     }
182 
runPostBootUpdate(final JobParameters jobParams, final PackageManagerService pm, final ArraySet<String> pkgs)183     private boolean runPostBootUpdate(final JobParameters jobParams,
184             final PackageManagerService pm, final ArraySet<String> pkgs) {
185         if (mExitPostBootUpdate.get()) {
186             // This job has already been superseded. Do not start it.
187             return false;
188         }
189         new Thread("BackgroundDexOptService_PostBootUpdate") {
190             @Override
191             public void run() {
192                 postBootUpdate(jobParams, pm, pkgs);
193             }
194 
195         }.start();
196         return true;
197     }
198 
postBootUpdate(JobParameters jobParams, PackageManagerService pm, ArraySet<String> pkgs)199     private void postBootUpdate(JobParameters jobParams, PackageManagerService pm,
200             ArraySet<String> pkgs) {
201         // Load low battery threshold from the system config. This is a 0-100 integer.
202         final int lowBatteryThreshold = getResources().getInteger(
203                 com.android.internal.R.integer.config_lowBatteryWarningLevel);
204         final long lowThreshold = getLowStorageThreshold(this);
205 
206         mAbortPostBootUpdate.set(false);
207 
208         ArraySet<String> updatedPackages = new ArraySet<>();
209         for (String pkg : pkgs) {
210             if (mAbortPostBootUpdate.get()) {
211                 // JobScheduler requested an early abort.
212                 return;
213             }
214             if (mExitPostBootUpdate.get()) {
215                 // Different job, which supersedes this one, is running.
216                 break;
217             }
218             if (getBatteryLevel() < lowBatteryThreshold) {
219                 // Rather bail than completely drain the battery.
220                 break;
221             }
222             long usableSpace = mDataDir.getUsableSpace();
223             if (usableSpace < lowThreshold) {
224                 // Rather bail than completely fill up the disk.
225                 Log.w(TAG, "Aborting background dex opt job due to low storage: " +
226                         usableSpace);
227                 break;
228             }
229 
230             if (DEBUG_DEXOPT) {
231                 Log.i(TAG, "Updating package " + pkg);
232             }
233 
234             // Update package if needed. Note that there can be no race between concurrent
235             // jobs because PackageDexOptimizer.performDexOpt is synchronized.
236 
237             // checkProfiles is false to avoid merging profiles during boot which
238             // might interfere with background compilation (b/28612421).
239             // Unfortunately this will also means that "pm.dexopt.boot=speed-profile" will
240             // behave differently than "pm.dexopt.bg-dexopt=speed-profile" but that's a
241             // trade-off worth doing to save boot time work.
242             int result = pm.performDexOptWithStatus(new DexoptOptions(
243                     pkg,
244                     PackageManagerService.REASON_BOOT,
245                     DexoptOptions.DEXOPT_BOOT_COMPLETE));
246             if (result == PackageDexOptimizer.DEX_OPT_PERFORMED)  {
247                 updatedPackages.add(pkg);
248             }
249         }
250         notifyPinService(updatedPackages);
251         notifyPackagesUpdated(updatedPackages);
252         // Ran to completion, so we abandon our timeslice and do not reschedule.
253         jobFinished(jobParams, /* reschedule */ false);
254     }
255 
runIdleOptimization(final JobParameters jobParams, final PackageManagerService pm, final ArraySet<String> pkgs)256     private boolean runIdleOptimization(final JobParameters jobParams,
257             final PackageManagerService pm, final ArraySet<String> pkgs) {
258         new Thread("BackgroundDexOptService_IdleOptimization") {
259             @Override
260             public void run() {
261                 int result = idleOptimization(pm, pkgs, BackgroundDexOptService.this);
262                 if (result == OPTIMIZE_PROCESSED) {
263                     Log.i(TAG, "Idle optimizations completed.");
264                 } else if (result == OPTIMIZE_ABORT_NO_SPACE_LEFT) {
265                     Log.w(TAG, "Idle optimizations aborted because of space constraints.");
266                 } else if (result == OPTIMIZE_ABORT_BY_JOB_SCHEDULER) {
267                     Log.w(TAG, "Idle optimizations aborted by job scheduler.");
268                 } else {
269                     Log.w(TAG, "Idle optimizations ended with unexpected code: " + result);
270                 }
271                 if (result != OPTIMIZE_ABORT_BY_JOB_SCHEDULER) {
272                     // Abandon our timeslice and do not reschedule.
273                     jobFinished(jobParams, /* reschedule */ false);
274                 }
275             }
276         }.start();
277         return true;
278     }
279 
280     // Optimize the given packages and return the optimization result (one of the OPTIMIZE_* codes).
idleOptimization(PackageManagerService pm, ArraySet<String> pkgs, Context context)281     private int idleOptimization(PackageManagerService pm, ArraySet<String> pkgs,
282             Context context) {
283         Log.i(TAG, "Performing idle optimizations");
284         // If post-boot update is still running, request that it exits early.
285         mExitPostBootUpdate.set(true);
286         mAbortIdleOptimization.set(false);
287 
288         long lowStorageThreshold = getLowStorageThreshold(context);
289         int result = idleOptimizePackages(pm, pkgs, lowStorageThreshold);
290         return result;
291     }
292 
293     /**
294      * Get the size of the directory. It uses recursion to go over all files.
295      * @param f
296      * @return
297      */
getDirectorySize(File f)298     private long getDirectorySize(File f) {
299         long size = 0;
300         if (f.isDirectory()) {
301             for (File file: f.listFiles()) {
302                 size += getDirectorySize(file);
303             }
304         } else {
305             size = f.length();
306         }
307         return size;
308     }
309 
310     /**
311      * Get the size of a package.
312      * @param pkg
313      */
getPackageSize(PackageManagerService pm, String pkg)314     private long getPackageSize(PackageManagerService pm, String pkg) {
315         PackageInfo info = pm.getPackageInfo(pkg, 0, UserHandle.USER_SYSTEM);
316         long size = 0;
317         if (info != null && info.applicationInfo != null) {
318             File path = Paths.get(info.applicationInfo.sourceDir).toFile();
319             if (path.isFile()) {
320                 path = path.getParentFile();
321             }
322             size += getDirectorySize(path);
323             if (!ArrayUtils.isEmpty(info.applicationInfo.splitSourceDirs)) {
324                 for (String splitSourceDir : info.applicationInfo.splitSourceDirs) {
325                     path = Paths.get(splitSourceDir).toFile();
326                     if (path.isFile()) {
327                         path = path.getParentFile();
328                     }
329                     size += getDirectorySize(path);
330                 }
331             }
332             return size;
333         }
334         return 0;
335     }
336 
idleOptimizePackages(PackageManagerService pm, ArraySet<String> pkgs, long lowStorageThreshold)337     private int idleOptimizePackages(PackageManagerService pm, ArraySet<String> pkgs,
338             long lowStorageThreshold) {
339         ArraySet<String> updatedPackages = new ArraySet<>();
340         ArraySet<String> updatedPackagesDueToSecondaryDex = new ArraySet<>();
341 
342         try {
343             final boolean supportSecondaryDex = supportSecondaryDex();
344 
345             if (supportSecondaryDex) {
346                 int result = reconcileSecondaryDexFiles(pm.getDexManager());
347                 if (result == OPTIMIZE_ABORT_BY_JOB_SCHEDULER) {
348                     return result;
349                 }
350             }
351 
352             // Only downgrade apps when space is low on device.
353             // Threshold is selected above the lowStorageThreshold so that we can pro-actively clean
354             // up disk before user hits the actual lowStorageThreshold.
355             final long lowStorageThresholdForDowngrade = LOW_THRESHOLD_MULTIPLIER_FOR_DOWNGRADE
356                     * lowStorageThreshold;
357             boolean shouldDowngrade = shouldDowngrade(lowStorageThresholdForDowngrade);
358             Log.d(TAG, "Should Downgrade " + shouldDowngrade);
359             if (shouldDowngrade) {
360                 Set<String> unusedPackages =
361                         pm.getUnusedPackages(mDowngradeUnusedAppsThresholdInMillis);
362                 Log.d(TAG, "Unsused Packages " +  String.join(",", unusedPackages));
363 
364                 if (!unusedPackages.isEmpty()) {
365                     for (String pkg : unusedPackages) {
366                         int abortCode = abortIdleOptimizations(/*lowStorageThreshold*/ -1);
367                         if (abortCode != OPTIMIZE_CONTINUE) {
368                             // Should be aborted by the scheduler.
369                             return abortCode;
370                         }
371                         if (downgradePackage(pm, pkg, /*isForPrimaryDex*/ true)) {
372                             updatedPackages.add(pkg);
373                         }
374                         if (supportSecondaryDex) {
375                             downgradePackage(pm, pkg, /*isForPrimaryDex*/ false);
376                         }
377                     }
378 
379                     pkgs = new ArraySet<>(pkgs);
380                     pkgs.removeAll(unusedPackages);
381                 }
382             }
383 
384             int primaryResult = optimizePackages(pm, pkgs, lowStorageThreshold,
385                     /*isForPrimaryDex*/ true, updatedPackages);
386             if (primaryResult != OPTIMIZE_PROCESSED) {
387                 return primaryResult;
388             }
389 
390             if (!supportSecondaryDex) {
391                 return OPTIMIZE_PROCESSED;
392             }
393 
394             int secondaryResult = optimizePackages(pm, pkgs, lowStorageThreshold,
395                     /*isForPrimaryDex*/ false, updatedPackagesDueToSecondaryDex);
396             return secondaryResult;
397         } finally {
398             // Always let the pinner service know about changes.
399             notifyPinService(updatedPackages);
400             // Only notify IORap the primary dex opt, because we don't want to
401             // invalidate traces unnecessary due to b/161633001 and that it's
402             // better to have a trace than no trace at all.
403             notifyPackagesUpdated(updatedPackages);
404         }
405     }
406 
optimizePackages(PackageManagerService pm, ArraySet<String> pkgs, long lowStorageThreshold, boolean isForPrimaryDex, ArraySet<String> updatedPackages)407     private int optimizePackages(PackageManagerService pm, ArraySet<String> pkgs,
408             long lowStorageThreshold, boolean isForPrimaryDex, ArraySet<String> updatedPackages) {
409         for (String pkg : pkgs) {
410             int abortCode = abortIdleOptimizations(lowStorageThreshold);
411             if (abortCode != OPTIMIZE_CONTINUE) {
412                 // Either aborted by the scheduler or no space left.
413                 return abortCode;
414             }
415 
416             boolean dexOptPerformed = optimizePackage(pm, pkg, isForPrimaryDex);
417             if (dexOptPerformed) {
418                 updatedPackages.add(pkg);
419             }
420         }
421         return OPTIMIZE_PROCESSED;
422     }
423 
424     /**
425      * Try to downgrade the package to a smaller compilation filter.
426      * eg. if the package is in speed-profile the package will be downgraded to verify.
427      * @param pm PackageManagerService
428      * @param pkg The package to be downgraded.
429      * @param isForPrimaryDex. Apps can have several dex file, primary and secondary.
430      * @return true if the package was downgraded.
431      */
downgradePackage(PackageManagerService pm, String pkg, boolean isForPrimaryDex)432     private boolean downgradePackage(PackageManagerService pm, String pkg,
433             boolean isForPrimaryDex) {
434         Log.d(TAG, "Downgrading " + pkg);
435         boolean dex_opt_performed = false;
436         int reason = PackageManagerService.REASON_INACTIVE_PACKAGE_DOWNGRADE;
437         int dexoptFlags = DexoptOptions.DEXOPT_BOOT_COMPLETE
438                 | DexoptOptions.DEXOPT_IDLE_BACKGROUND_JOB
439                 | DexoptOptions.DEXOPT_DOWNGRADE;
440         long package_size_before = getPackageSize(pm, pkg);
441 
442         if (isForPrimaryDex || PLATFORM_PACKAGE_NAME.equals(pkg)) {
443             // This applies for system apps or if packages location is not a directory, i.e.
444             // monolithic install.
445             if (!pm.canHaveOatDir(pkg)) {
446                 // For apps that don't have the oat directory, instead of downgrading,
447                 // remove their compiler artifacts from dalvik cache.
448                 pm.deleteOatArtifactsOfPackage(pkg);
449             } else {
450                 dex_opt_performed = performDexOptPrimary(pm, pkg, reason, dexoptFlags);
451             }
452         } else {
453             dex_opt_performed = performDexOptSecondary(pm, pkg, reason, dexoptFlags);
454         }
455 
456         if (dex_opt_performed) {
457             FrameworkStatsLog.write(FrameworkStatsLog.APP_DOWNGRADED, pkg, package_size_before,
458                     getPackageSize(pm, pkg), /*aggressive=*/ false);
459         }
460         return dex_opt_performed;
461     }
462 
supportSecondaryDex()463     private boolean supportSecondaryDex() {
464         return (SystemProperties.getBoolean("dalvik.vm.dexopt.secondary", false));
465     }
466 
reconcileSecondaryDexFiles(DexManager dm)467     private int reconcileSecondaryDexFiles(DexManager dm) {
468         // TODO(calin): should we blacklist packages for which we fail to reconcile?
469         for (String p : dm.getAllPackagesWithSecondaryDexFiles()) {
470             if (mAbortIdleOptimization.get()) {
471                 return OPTIMIZE_ABORT_BY_JOB_SCHEDULER;
472             }
473             dm.reconcileSecondaryDexFiles(p);
474         }
475         return OPTIMIZE_PROCESSED;
476     }
477 
478     /**
479      *
480      * Optimize package if needed. Note that there can be no race between
481      * concurrent jobs because PackageDexOptimizer.performDexOpt is synchronized.
482      * @param pm An instance of PackageManagerService
483      * @param pkg The package to be downgraded.
484      * @param isForPrimaryDex. Apps can have several dex file, primary and secondary.
485      * @return true if the package was downgraded.
486      */
optimizePackage(PackageManagerService pm, String pkg, boolean isForPrimaryDex)487     private boolean optimizePackage(PackageManagerService pm, String pkg,
488             boolean isForPrimaryDex) {
489         int reason = PackageManagerService.REASON_BACKGROUND_DEXOPT;
490         int dexoptFlags = DexoptOptions.DEXOPT_CHECK_FOR_PROFILES_UPDATES
491                 | DexoptOptions.DEXOPT_BOOT_COMPLETE
492                 | DexoptOptions.DEXOPT_IDLE_BACKGROUND_JOB;
493 
494         // System server share the same code path as primary dex files.
495         // PackageManagerService will select the right optimization path for it.
496         return (isForPrimaryDex || PLATFORM_PACKAGE_NAME.equals(pkg))
497             ? performDexOptPrimary(pm, pkg, reason, dexoptFlags)
498             : performDexOptSecondary(pm, pkg, reason, dexoptFlags);
499     }
500 
performDexOptPrimary(PackageManagerService pm, String pkg, int reason, int dexoptFlags)501     private boolean performDexOptPrimary(PackageManagerService pm, String pkg, int reason,
502             int dexoptFlags) {
503         int result = trackPerformDexOpt(pkg, /*isForPrimaryDex=*/ false,
504                 () -> pm.performDexOptWithStatus(new DexoptOptions(pkg, reason, dexoptFlags)));
505         return result == PackageDexOptimizer.DEX_OPT_PERFORMED;
506     }
507 
performDexOptSecondary(PackageManagerService pm, String pkg, int reason, int dexoptFlags)508     private boolean performDexOptSecondary(PackageManagerService pm, String pkg, int reason,
509             int dexoptFlags) {
510         DexoptOptions dexoptOptions = new DexoptOptions(pkg, reason,
511                 dexoptFlags | DexoptOptions.DEXOPT_ONLY_SECONDARY_DEX);
512         int result = trackPerformDexOpt(pkg, /*isForPrimaryDex=*/ true,
513                 () -> pm.performDexOpt(dexoptOptions)
514                     ? PackageDexOptimizer.DEX_OPT_PERFORMED : PackageDexOptimizer.DEX_OPT_FAILED
515         );
516         return result == PackageDexOptimizer.DEX_OPT_PERFORMED;
517     }
518 
519     /**
520      * Execute the dexopt wrapper and make sure that if performDexOpt wrapper fails
521      * the package is added to the list of failed packages.
522      * Return one of following result:
523      *  {@link PackageDexOptimizer#DEX_OPT_SKIPPED}
524      *  {@link PackageDexOptimizer#DEX_OPT_PERFORMED}
525      *  {@link PackageDexOptimizer#DEX_OPT_FAILED}
526      */
trackPerformDexOpt(String pkg, boolean isForPrimaryDex, Supplier<Integer> performDexOptWrapper)527     private int trackPerformDexOpt(String pkg, boolean isForPrimaryDex,
528             Supplier<Integer> performDexOptWrapper) {
529         ArraySet<String> sFailedPackageNames =
530                 isForPrimaryDex ? sFailedPackageNamesPrimary : sFailedPackageNamesSecondary;
531         synchronized (sFailedPackageNames) {
532             if (sFailedPackageNames.contains(pkg)) {
533                 // Skip previously failing package
534                 return PackageDexOptimizer.DEX_OPT_SKIPPED;
535             }
536             sFailedPackageNames.add(pkg);
537         }
538         int result = performDexOptWrapper.get();
539         if (result != PackageDexOptimizer.DEX_OPT_FAILED) {
540             synchronized (sFailedPackageNames) {
541                 sFailedPackageNames.remove(pkg);
542             }
543         }
544         return result;
545     }
546 
547     // Evaluate whether or not idle optimizations should continue.
abortIdleOptimizations(long lowStorageThreshold)548     private int abortIdleOptimizations(long lowStorageThreshold) {
549         if (mAbortIdleOptimization.get()) {
550             // JobScheduler requested an early abort.
551             return OPTIMIZE_ABORT_BY_JOB_SCHEDULER;
552         }
553         long usableSpace = mDataDir.getUsableSpace();
554         if (usableSpace < lowStorageThreshold) {
555             // Rather bail than completely fill up the disk.
556             Log.w(TAG, "Aborting background dex opt job due to low storage: " + usableSpace);
557             return OPTIMIZE_ABORT_NO_SPACE_LEFT;
558         }
559 
560         return OPTIMIZE_CONTINUE;
561     }
562 
563     // Evaluate whether apps should be downgraded.
shouldDowngrade(long lowStorageThresholdForDowngrade)564     private boolean shouldDowngrade(long lowStorageThresholdForDowngrade) {
565         long usableSpace = mDataDir.getUsableSpace();
566         if (usableSpace < lowStorageThresholdForDowngrade) {
567             return true;
568         }
569 
570         return false;
571     }
572 
573     /**
574      * Execute idle optimizations immediately on packages in packageNames. If packageNames is null,
575      * then execute on all packages.
576      */
runIdleOptimizationsNow(PackageManagerService pm, Context context, @Nullable List<String> packageNames)577     public static boolean runIdleOptimizationsNow(PackageManagerService pm, Context context,
578             @Nullable List<String> packageNames) {
579         // Create a new object to make sure we don't interfere with the scheduled jobs.
580         // Note that this may still run at the same time with the job scheduled by the
581         // JobScheduler but the scheduler will not be able to cancel it.
582         BackgroundDexOptService bdos = new BackgroundDexOptService();
583         ArraySet<String> packagesToOptimize;
584         if (packageNames == null) {
585             packagesToOptimize = pm.getOptimizablePackages();
586         } else {
587             packagesToOptimize = new ArraySet<>(packageNames);
588         }
589         int result = bdos.idleOptimization(pm, packagesToOptimize, context);
590         return result == OPTIMIZE_PROCESSED;
591     }
592 
593     @Override
onStartJob(JobParameters params)594     public boolean onStartJob(JobParameters params) {
595         if (DEBUG_DEXOPT) {
596             Log.i(TAG, "onStartJob");
597         }
598 
599         // NOTE: PackageManagerService.isStorageLow uses a different set of criteria from
600         // the checks above. This check is not "live" - the value is determined by a background
601         // restart with a period of ~1 minute.
602         PackageManagerService pm = (PackageManagerService)ServiceManager.getService("package");
603         if (pm.isStorageLow()) {
604             if (DEBUG_DEXOPT) {
605                 Log.i(TAG, "Low storage, skipping this run");
606             }
607             return false;
608         }
609 
610         final ArraySet<String> pkgs = pm.getOptimizablePackages();
611         if (pkgs.isEmpty()) {
612             if (DEBUG_DEXOPT) {
613                 Log.i(TAG, "No packages to optimize");
614             }
615             return false;
616         }
617 
618         boolean result;
619         if (params.getJobId() == JOB_POST_BOOT_UPDATE) {
620             result = runPostBootUpdate(params, pm, pkgs);
621         } else {
622             result = runIdleOptimization(params, pm, pkgs);
623         }
624 
625         return result;
626     }
627 
628     @Override
onStopJob(JobParameters params)629     public boolean onStopJob(JobParameters params) {
630         if (DEBUG_DEXOPT) {
631             Log.i(TAG, "onStopJob");
632         }
633 
634         if (params.getJobId() == JOB_POST_BOOT_UPDATE) {
635             mAbortPostBootUpdate.set(true);
636 
637             // Do not reschedule.
638             // TODO: We should reschedule if we didn't process all apps, yet.
639             return false;
640         } else {
641             mAbortIdleOptimization.set(true);
642 
643             // Reschedule the run.
644             // TODO: Should this be dependent on the stop reason?
645             return true;
646         }
647     }
648 
notifyPinService(ArraySet<String> updatedPackages)649     private void notifyPinService(ArraySet<String> updatedPackages) {
650         PinnerService pinnerService = LocalServices.getService(PinnerService.class);
651         if (pinnerService != null) {
652             Log.i(TAG, "Pinning optimized code " + updatedPackages);
653             pinnerService.update(updatedPackages, false /* force */);
654         }
655     }
656 
657     public static interface PackagesUpdatedListener {
658         /** Callback when packages have been updated by the bg-dexopt service. */
onPackagesUpdated(ArraySet<String> updatedPackages)659         public void onPackagesUpdated(ArraySet<String> updatedPackages);
660     }
661 
addPackagesUpdatedListener(PackagesUpdatedListener listener)662     public static void addPackagesUpdatedListener(PackagesUpdatedListener listener) {
663         synchronized (sPackagesUpdatedListeners) {
664             sPackagesUpdatedListeners.add(listener);
665         }
666     }
667 
removePackagesUpdatedListener(PackagesUpdatedListener listener)668     public static void removePackagesUpdatedListener(PackagesUpdatedListener listener) {
669         synchronized (sPackagesUpdatedListeners) {
670             sPackagesUpdatedListeners.remove(listener);
671         }
672     }
673 
674     /** Notify all listeners (#addPackagesUpdatedListener) that packages have been updated. */
notifyPackagesUpdated(ArraySet<String> updatedPackages)675     private void notifyPackagesUpdated(ArraySet<String> updatedPackages) {
676         synchronized (sPackagesUpdatedListeners) {
677             for (PackagesUpdatedListener listener : sPackagesUpdatedListeners) {
678                 listener.onPackagesUpdated(updatedPackages);
679             }
680         }
681     }
682 
getDowngradeUnusedAppsThresholdInMillis()683     private static long getDowngradeUnusedAppsThresholdInMillis() {
684         final String sysPropKey = "pm.dexopt.downgrade_after_inactive_days";
685         String sysPropValue = SystemProperties.get(sysPropKey);
686         if (sysPropValue == null || sysPropValue.isEmpty()) {
687             Log.w(TAG, "SysProp " + sysPropKey + " not set");
688             return Long.MAX_VALUE;
689         }
690         return TimeUnit.DAYS.toMillis(Long.parseLong(sysPropValue));
691     }
692 
isBackgroundDexoptDisabled()693     private static boolean isBackgroundDexoptDisabled() {
694         return SystemProperties.getBoolean("pm.dexopt.disable_bg_dexopt" /* key */,
695                 false /* default */);
696     }
697 }
698