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 
341         try {
342             final boolean supportSecondaryDex = supportSecondaryDex();
343 
344             if (supportSecondaryDex) {
345                 int result = reconcileSecondaryDexFiles(pm.getDexManager());
346                 if (result == OPTIMIZE_ABORT_BY_JOB_SCHEDULER) {
347                     return result;
348                 }
349             }
350 
351             // Only downgrade apps when space is low on device.
352             // Threshold is selected above the lowStorageThreshold so that we can pro-actively clean
353             // up disk before user hits the actual lowStorageThreshold.
354             final long lowStorageThresholdForDowngrade = LOW_THRESHOLD_MULTIPLIER_FOR_DOWNGRADE
355                     * lowStorageThreshold;
356             boolean shouldDowngrade = shouldDowngrade(lowStorageThresholdForDowngrade);
357             Log.d(TAG, "Should Downgrade " + shouldDowngrade);
358             if (shouldDowngrade) {
359                 Set<String> unusedPackages =
360                         pm.getUnusedPackages(mDowngradeUnusedAppsThresholdInMillis);
361                 Log.d(TAG, "Unsused Packages " +  String.join(",", unusedPackages));
362 
363                 if (!unusedPackages.isEmpty()) {
364                     for (String pkg : unusedPackages) {
365                         int abortCode = abortIdleOptimizations(/*lowStorageThreshold*/ -1);
366                         if (abortCode != OPTIMIZE_CONTINUE) {
367                             // Should be aborted by the scheduler.
368                             return abortCode;
369                         }
370                         if (downgradePackage(pm, pkg, /*isForPrimaryDex*/ true)) {
371                             updatedPackages.add(pkg);
372                         }
373                         if (supportSecondaryDex) {
374                             downgradePackage(pm, pkg, /*isForPrimaryDex*/ false);
375                         }
376                     }
377 
378                     pkgs = new ArraySet<>(pkgs);
379                     pkgs.removeAll(unusedPackages);
380                 }
381             }
382 
383             int primaryResult = optimizePackages(pm, pkgs, lowStorageThreshold,
384                     /*isForPrimaryDex*/ true, updatedPackages);
385             if (primaryResult != OPTIMIZE_PROCESSED) {
386                 return primaryResult;
387             }
388 
389             if (!supportSecondaryDex) {
390                 return OPTIMIZE_PROCESSED;
391             }
392 
393             int secondaryResult = optimizePackages(pm, pkgs, lowStorageThreshold,
394                     /*isForPrimaryDex*/ false, updatedPackages);
395             return secondaryResult;
396         } finally {
397             // Always let the pinner service know about changes.
398             notifyPinService(updatedPackages);
399             notifyPackagesUpdated(updatedPackages);
400         }
401     }
402 
optimizePackages(PackageManagerService pm, ArraySet<String> pkgs, long lowStorageThreshold, boolean isForPrimaryDex, ArraySet<String> updatedPackages)403     private int optimizePackages(PackageManagerService pm, ArraySet<String> pkgs,
404             long lowStorageThreshold, boolean isForPrimaryDex, ArraySet<String> updatedPackages) {
405         for (String pkg : pkgs) {
406             int abortCode = abortIdleOptimizations(lowStorageThreshold);
407             if (abortCode != OPTIMIZE_CONTINUE) {
408                 // Either aborted by the scheduler or no space left.
409                 return abortCode;
410             }
411 
412             boolean dexOptPerformed = optimizePackage(pm, pkg, isForPrimaryDex);
413             if (dexOptPerformed) {
414                 updatedPackages.add(pkg);
415             }
416         }
417         return OPTIMIZE_PROCESSED;
418     }
419 
420     /**
421      * Try to downgrade the package to a smaller compilation filter.
422      * eg. if the package is in speed-profile the package will be downgraded to verify.
423      * @param pm PackageManagerService
424      * @param pkg The package to be downgraded.
425      * @param isForPrimaryDex. Apps can have several dex file, primary and secondary.
426      * @return true if the package was downgraded.
427      */
downgradePackage(PackageManagerService pm, String pkg, boolean isForPrimaryDex)428     private boolean downgradePackage(PackageManagerService pm, String pkg,
429             boolean isForPrimaryDex) {
430         Log.d(TAG, "Downgrading " + pkg);
431         boolean dex_opt_performed = false;
432         int reason = PackageManagerService.REASON_INACTIVE_PACKAGE_DOWNGRADE;
433         int dexoptFlags = DexoptOptions.DEXOPT_BOOT_COMPLETE
434                 | DexoptOptions.DEXOPT_IDLE_BACKGROUND_JOB
435                 | DexoptOptions.DEXOPT_DOWNGRADE;
436         long package_size_before = getPackageSize(pm, pkg);
437 
438         if (isForPrimaryDex || PLATFORM_PACKAGE_NAME.equals(pkg)) {
439             // This applies for system apps or if packages location is not a directory, i.e.
440             // monolithic install.
441             if (!pm.canHaveOatDir(pkg)) {
442                 // For apps that don't have the oat directory, instead of downgrading,
443                 // remove their compiler artifacts from dalvik cache.
444                 pm.deleteOatArtifactsOfPackage(pkg);
445             } else {
446                 dex_opt_performed = performDexOptPrimary(pm, pkg, reason, dexoptFlags);
447             }
448         } else {
449             dex_opt_performed = performDexOptSecondary(pm, pkg, reason, dexoptFlags);
450         }
451 
452         if (dex_opt_performed) {
453             FrameworkStatsLog.write(FrameworkStatsLog.APP_DOWNGRADED, pkg, package_size_before,
454                     getPackageSize(pm, pkg), /*aggressive=*/ false);
455         }
456         return dex_opt_performed;
457     }
458 
supportSecondaryDex()459     private boolean supportSecondaryDex() {
460         return (SystemProperties.getBoolean("dalvik.vm.dexopt.secondary", false));
461     }
462 
reconcileSecondaryDexFiles(DexManager dm)463     private int reconcileSecondaryDexFiles(DexManager dm) {
464         // TODO(calin): should we blacklist packages for which we fail to reconcile?
465         for (String p : dm.getAllPackagesWithSecondaryDexFiles()) {
466             if (mAbortIdleOptimization.get()) {
467                 return OPTIMIZE_ABORT_BY_JOB_SCHEDULER;
468             }
469             dm.reconcileSecondaryDexFiles(p);
470         }
471         return OPTIMIZE_PROCESSED;
472     }
473 
474     /**
475      *
476      * Optimize package if needed. Note that there can be no race between
477      * concurrent jobs because PackageDexOptimizer.performDexOpt is synchronized.
478      * @param pm An instance of PackageManagerService
479      * @param pkg The package to be downgraded.
480      * @param isForPrimaryDex. Apps can have several dex file, primary and secondary.
481      * @return true if the package was downgraded.
482      */
optimizePackage(PackageManagerService pm, String pkg, boolean isForPrimaryDex)483     private boolean optimizePackage(PackageManagerService pm, String pkg,
484             boolean isForPrimaryDex) {
485         int reason = PackageManagerService.REASON_BACKGROUND_DEXOPT;
486         int dexoptFlags = DexoptOptions.DEXOPT_CHECK_FOR_PROFILES_UPDATES
487                 | DexoptOptions.DEXOPT_BOOT_COMPLETE
488                 | DexoptOptions.DEXOPT_IDLE_BACKGROUND_JOB;
489 
490         // System server share the same code path as primary dex files.
491         // PackageManagerService will select the right optimization path for it.
492         return (isForPrimaryDex || PLATFORM_PACKAGE_NAME.equals(pkg))
493             ? performDexOptPrimary(pm, pkg, reason, dexoptFlags)
494             : performDexOptSecondary(pm, pkg, reason, dexoptFlags);
495     }
496 
performDexOptPrimary(PackageManagerService pm, String pkg, int reason, int dexoptFlags)497     private boolean performDexOptPrimary(PackageManagerService pm, String pkg, int reason,
498             int dexoptFlags) {
499         int result = trackPerformDexOpt(pkg, /*isForPrimaryDex=*/ false,
500                 () -> pm.performDexOptWithStatus(new DexoptOptions(pkg, reason, dexoptFlags)));
501         return result == PackageDexOptimizer.DEX_OPT_PERFORMED;
502     }
503 
performDexOptSecondary(PackageManagerService pm, String pkg, int reason, int dexoptFlags)504     private boolean performDexOptSecondary(PackageManagerService pm, String pkg, int reason,
505             int dexoptFlags) {
506         DexoptOptions dexoptOptions = new DexoptOptions(pkg, reason,
507                 dexoptFlags | DexoptOptions.DEXOPT_ONLY_SECONDARY_DEX);
508         int result = trackPerformDexOpt(pkg, /*isForPrimaryDex=*/ true,
509                 () -> pm.performDexOpt(dexoptOptions)
510                     ? PackageDexOptimizer.DEX_OPT_PERFORMED : PackageDexOptimizer.DEX_OPT_FAILED
511         );
512         return result == PackageDexOptimizer.DEX_OPT_PERFORMED;
513     }
514 
515     /**
516      * Execute the dexopt wrapper and make sure that if performDexOpt wrapper fails
517      * the package is added to the list of failed packages.
518      * Return one of following result:
519      *  {@link PackageDexOptimizer#DEX_OPT_SKIPPED}
520      *  {@link PackageDexOptimizer#DEX_OPT_PERFORMED}
521      *  {@link PackageDexOptimizer#DEX_OPT_FAILED}
522      */
trackPerformDexOpt(String pkg, boolean isForPrimaryDex, Supplier<Integer> performDexOptWrapper)523     private int trackPerformDexOpt(String pkg, boolean isForPrimaryDex,
524             Supplier<Integer> performDexOptWrapper) {
525         ArraySet<String> sFailedPackageNames =
526                 isForPrimaryDex ? sFailedPackageNamesPrimary : sFailedPackageNamesSecondary;
527         synchronized (sFailedPackageNames) {
528             if (sFailedPackageNames.contains(pkg)) {
529                 // Skip previously failing package
530                 return PackageDexOptimizer.DEX_OPT_SKIPPED;
531             }
532             sFailedPackageNames.add(pkg);
533         }
534         int result = performDexOptWrapper.get();
535         if (result != PackageDexOptimizer.DEX_OPT_FAILED) {
536             synchronized (sFailedPackageNames) {
537                 sFailedPackageNames.remove(pkg);
538             }
539         }
540         return result;
541     }
542 
543     // Evaluate whether or not idle optimizations should continue.
abortIdleOptimizations(long lowStorageThreshold)544     private int abortIdleOptimizations(long lowStorageThreshold) {
545         if (mAbortIdleOptimization.get()) {
546             // JobScheduler requested an early abort.
547             return OPTIMIZE_ABORT_BY_JOB_SCHEDULER;
548         }
549         long usableSpace = mDataDir.getUsableSpace();
550         if (usableSpace < lowStorageThreshold) {
551             // Rather bail than completely fill up the disk.
552             Log.w(TAG, "Aborting background dex opt job due to low storage: " + usableSpace);
553             return OPTIMIZE_ABORT_NO_SPACE_LEFT;
554         }
555 
556         return OPTIMIZE_CONTINUE;
557     }
558 
559     // Evaluate whether apps should be downgraded.
shouldDowngrade(long lowStorageThresholdForDowngrade)560     private boolean shouldDowngrade(long lowStorageThresholdForDowngrade) {
561         long usableSpace = mDataDir.getUsableSpace();
562         if (usableSpace < lowStorageThresholdForDowngrade) {
563             return true;
564         }
565 
566         return false;
567     }
568 
569     /**
570      * Execute idle optimizations immediately on packages in packageNames. If packageNames is null,
571      * then execute on all packages.
572      */
runIdleOptimizationsNow(PackageManagerService pm, Context context, @Nullable List<String> packageNames)573     public static boolean runIdleOptimizationsNow(PackageManagerService pm, Context context,
574             @Nullable List<String> packageNames) {
575         // Create a new object to make sure we don't interfere with the scheduled jobs.
576         // Note that this may still run at the same time with the job scheduled by the
577         // JobScheduler but the scheduler will not be able to cancel it.
578         BackgroundDexOptService bdos = new BackgroundDexOptService();
579         ArraySet<String> packagesToOptimize;
580         if (packageNames == null) {
581             packagesToOptimize = pm.getOptimizablePackages();
582         } else {
583             packagesToOptimize = new ArraySet<>(packageNames);
584         }
585         int result = bdos.idleOptimization(pm, packagesToOptimize, context);
586         return result == OPTIMIZE_PROCESSED;
587     }
588 
589     @Override
onStartJob(JobParameters params)590     public boolean onStartJob(JobParameters params) {
591         if (DEBUG_DEXOPT) {
592             Log.i(TAG, "onStartJob");
593         }
594 
595         // NOTE: PackageManagerService.isStorageLow uses a different set of criteria from
596         // the checks above. This check is not "live" - the value is determined by a background
597         // restart with a period of ~1 minute.
598         PackageManagerService pm = (PackageManagerService)ServiceManager.getService("package");
599         if (pm.isStorageLow()) {
600             if (DEBUG_DEXOPT) {
601                 Log.i(TAG, "Low storage, skipping this run");
602             }
603             return false;
604         }
605 
606         final ArraySet<String> pkgs = pm.getOptimizablePackages();
607         if (pkgs.isEmpty()) {
608             if (DEBUG_DEXOPT) {
609                 Log.i(TAG, "No packages to optimize");
610             }
611             return false;
612         }
613 
614         boolean result;
615         if (params.getJobId() == JOB_POST_BOOT_UPDATE) {
616             result = runPostBootUpdate(params, pm, pkgs);
617         } else {
618             result = runIdleOptimization(params, pm, pkgs);
619         }
620 
621         return result;
622     }
623 
624     @Override
onStopJob(JobParameters params)625     public boolean onStopJob(JobParameters params) {
626         if (DEBUG_DEXOPT) {
627             Log.i(TAG, "onStopJob");
628         }
629 
630         if (params.getJobId() == JOB_POST_BOOT_UPDATE) {
631             mAbortPostBootUpdate.set(true);
632 
633             // Do not reschedule.
634             // TODO: We should reschedule if we didn't process all apps, yet.
635             return false;
636         } else {
637             mAbortIdleOptimization.set(true);
638 
639             // Reschedule the run.
640             // TODO: Should this be dependent on the stop reason?
641             return true;
642         }
643     }
644 
notifyPinService(ArraySet<String> updatedPackages)645     private void notifyPinService(ArraySet<String> updatedPackages) {
646         PinnerService pinnerService = LocalServices.getService(PinnerService.class);
647         if (pinnerService != null) {
648             Log.i(TAG, "Pinning optimized code " + updatedPackages);
649             pinnerService.update(updatedPackages, false /* force */);
650         }
651     }
652 
653     public static interface PackagesUpdatedListener {
654         /** Callback when packages have been updated by the bg-dexopt service. */
onPackagesUpdated(ArraySet<String> updatedPackages)655         public void onPackagesUpdated(ArraySet<String> updatedPackages);
656     }
657 
addPackagesUpdatedListener(PackagesUpdatedListener listener)658     public static void addPackagesUpdatedListener(PackagesUpdatedListener listener) {
659         synchronized (sPackagesUpdatedListeners) {
660             sPackagesUpdatedListeners.add(listener);
661         }
662     }
663 
removePackagesUpdatedListener(PackagesUpdatedListener listener)664     public static void removePackagesUpdatedListener(PackagesUpdatedListener listener) {
665         synchronized (sPackagesUpdatedListeners) {
666             sPackagesUpdatedListeners.remove(listener);
667         }
668     }
669 
670     /** Notify all listeners (#addPackagesUpdatedListener) that packages have been updated. */
notifyPackagesUpdated(ArraySet<String> updatedPackages)671     private void notifyPackagesUpdated(ArraySet<String> updatedPackages) {
672         synchronized (sPackagesUpdatedListeners) {
673             for (PackagesUpdatedListener listener : sPackagesUpdatedListeners) {
674                 listener.onPackagesUpdated(updatedPackages);
675             }
676         }
677     }
678 
getDowngradeUnusedAppsThresholdInMillis()679     private static long getDowngradeUnusedAppsThresholdInMillis() {
680         final String sysPropKey = "pm.dexopt.downgrade_after_inactive_days";
681         String sysPropValue = SystemProperties.get(sysPropKey);
682         if (sysPropValue == null || sysPropValue.isEmpty()) {
683             Log.w(TAG, "SysProp " + sysPropKey + " not set");
684             return Long.MAX_VALUE;
685         }
686         return TimeUnit.DAYS.toMillis(Long.parseLong(sysPropValue));
687     }
688 
isBackgroundDexoptDisabled()689     private static boolean isBackgroundDexoptDisabled() {
690         return SystemProperties.getBoolean("pm.dexopt.disable_bg_dexopt" /* key */,
691                 false /* default */);
692     }
693 }
694