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