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
17package com.android.server.pm;
18
19import static com.android.server.pm.PackageManagerService.DEBUG_DEXOPT;
20
21import android.annotation.Nullable;
22import android.app.job.JobInfo;
23import android.app.job.JobParameters;
24import android.app.job.JobScheduler;
25import android.app.job.JobService;
26import android.content.ComponentName;
27import android.content.Context;
28import android.content.Intent;
29import android.content.IntentFilter;
30import android.os.BatteryManager;
31import android.os.Environment;
32import android.os.ServiceManager;
33import android.os.SystemProperties;
34import android.os.storage.StorageManager;
35import android.util.ArraySet;
36import android.util.Log;
37
38import com.android.server.pm.dex.DexManager;
39import com.android.server.LocalServices;
40import com.android.server.PinnerService;
41import com.android.server.pm.dex.DexoptOptions;
42
43import java.io.File;
44import java.util.List;
45import java.util.Set;
46import java.util.concurrent.atomic.AtomicBoolean;
47import java.util.concurrent.TimeUnit;
48
49/**
50 * {@hide}
51 */
52public class BackgroundDexOptService extends JobService {
53    private static final String TAG = "BackgroundDexOptService";
54
55    private static final boolean DEBUG = false;
56
57    private static final int JOB_IDLE_OPTIMIZE = 800;
58    private static final int JOB_POST_BOOT_UPDATE = 801;
59
60    private static final long IDLE_OPTIMIZATION_PERIOD = DEBUG
61            ? TimeUnit.MINUTES.toMillis(1)
62            : TimeUnit.DAYS.toMillis(1);
63
64    private static ComponentName sDexoptServiceName = new ComponentName(
65            "android",
66            BackgroundDexOptService.class.getName());
67
68    // Possible return codes of individual optimization steps.
69
70    // Optimizations finished. All packages were processed.
71    private static final int OPTIMIZE_PROCESSED = 0;
72    // Optimizations should continue. Issued after checking the scheduler, disk space or battery.
73    private static final int OPTIMIZE_CONTINUE = 1;
74    // Optimizations should be aborted. Job scheduler requested it.
75    private static final int OPTIMIZE_ABORT_BY_JOB_SCHEDULER = 2;
76    // Optimizations should be aborted. No space left on device.
77    private static final int OPTIMIZE_ABORT_NO_SPACE_LEFT = 3;
78
79    // Used for calculating space threshold for downgrading unused apps.
80    private static final int LOW_THRESHOLD_MULTIPLIER_FOR_DOWNGRADE = 2;
81
82    /**
83     * Set of failed packages remembered across job runs.
84     */
85    static final ArraySet<String> sFailedPackageNamesPrimary = new ArraySet<String>();
86    static final ArraySet<String> sFailedPackageNamesSecondary = new ArraySet<String>();
87
88    /**
89     * Atomics set to true if the JobScheduler requests an abort.
90     */
91    private final AtomicBoolean mAbortPostBootUpdate = new AtomicBoolean(false);
92    private final AtomicBoolean mAbortIdleOptimization = new AtomicBoolean(false);
93
94    /**
95     * Atomic set to true if one job should exit early because another job was started.
96     */
97    private final AtomicBoolean mExitPostBootUpdate = new AtomicBoolean(false);
98
99    private final File mDataDir = Environment.getDataDirectory();
100
101    private static final long mDowngradeUnusedAppsThresholdInMillis =
102            getDowngradeUnusedAppsThresholdInMillis();
103
104    public static void schedule(Context context) {
105        if (isBackgroundDexoptDisabled()) {
106            return;
107        }
108
109        JobScheduler js = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
110
111        // Schedule a one-off job which scans installed packages and updates
112        // out-of-date oat files.
113        js.schedule(new JobInfo.Builder(JOB_POST_BOOT_UPDATE, sDexoptServiceName)
114                    .setMinimumLatency(TimeUnit.MINUTES.toMillis(1))
115                    .setOverrideDeadline(TimeUnit.MINUTES.toMillis(1))
116                    .build());
117
118        // Schedule a daily job which scans installed packages and compiles
119        // those with fresh profiling data.
120        js.schedule(new JobInfo.Builder(JOB_IDLE_OPTIMIZE, sDexoptServiceName)
121                    .setRequiresDeviceIdle(true)
122                    .setRequiresCharging(true)
123                    .setPeriodic(IDLE_OPTIMIZATION_PERIOD)
124                    .build());
125
126        if (DEBUG_DEXOPT) {
127            Log.i(TAG, "Jobs scheduled");
128        }
129    }
130
131    public static void notifyPackageChanged(String packageName) {
132        // The idle maintanance job skips packages which previously failed to
133        // compile. The given package has changed and may successfully compile
134        // now. Remove it from the list of known failing packages.
135        synchronized (sFailedPackageNamesPrimary) {
136            sFailedPackageNamesPrimary.remove(packageName);
137        }
138        synchronized (sFailedPackageNamesSecondary) {
139            sFailedPackageNamesSecondary.remove(packageName);
140        }
141    }
142
143    // Returns the current battery level as a 0-100 integer.
144    private int getBatteryLevel() {
145        IntentFilter filter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
146        Intent intent = registerReceiver(null, filter);
147        int level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1);
148        int scale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1);
149        boolean present = intent.getBooleanExtra(BatteryManager.EXTRA_PRESENT, true);
150
151        if (!present) {
152            // No battery, treat as if 100%, no possibility of draining battery.
153            return 100;
154        }
155
156        if (level < 0 || scale <= 0) {
157            // Battery data unavailable. This should never happen, so assume the worst.
158            return 0;
159        }
160
161        return (100 * level / scale);
162    }
163
164    private long getLowStorageThreshold(Context context) {
165        @SuppressWarnings("deprecation")
166        final long lowThreshold = StorageManager.from(context).getStorageLowBytes(mDataDir);
167        if (lowThreshold == 0) {
168            Log.e(TAG, "Invalid low storage threshold");
169        }
170
171        return lowThreshold;
172    }
173
174    private boolean runPostBootUpdate(final JobParameters jobParams,
175            final PackageManagerService pm, final ArraySet<String> pkgs) {
176        if (mExitPostBootUpdate.get()) {
177            // This job has already been superseded. Do not start it.
178            return false;
179        }
180        new Thread("BackgroundDexOptService_PostBootUpdate") {
181            @Override
182            public void run() {
183                postBootUpdate(jobParams, pm, pkgs);
184            }
185
186        }.start();
187        return true;
188    }
189
190    private void postBootUpdate(JobParameters jobParams, PackageManagerService pm,
191            ArraySet<String> pkgs) {
192        // Load low battery threshold from the system config. This is a 0-100 integer.
193        final int lowBatteryThreshold = getResources().getInteger(
194                com.android.internal.R.integer.config_lowBatteryWarningLevel);
195        final long lowThreshold = getLowStorageThreshold(this);
196
197        mAbortPostBootUpdate.set(false);
198
199        ArraySet<String> updatedPackages = new ArraySet<>();
200        for (String pkg : pkgs) {
201            if (mAbortPostBootUpdate.get()) {
202                // JobScheduler requested an early abort.
203                return;
204            }
205            if (mExitPostBootUpdate.get()) {
206                // Different job, which supersedes this one, is running.
207                break;
208            }
209            if (getBatteryLevel() < lowBatteryThreshold) {
210                // Rather bail than completely drain the battery.
211                break;
212            }
213            long usableSpace = mDataDir.getUsableSpace();
214            if (usableSpace < lowThreshold) {
215                // Rather bail than completely fill up the disk.
216                Log.w(TAG, "Aborting background dex opt job due to low storage: " +
217                        usableSpace);
218                break;
219            }
220
221            if (DEBUG_DEXOPT) {
222                Log.i(TAG, "Updating package " + pkg);
223            }
224
225            // Update package if needed. Note that there can be no race between concurrent
226            // jobs because PackageDexOptimizer.performDexOpt is synchronized.
227
228            // checkProfiles is false to avoid merging profiles during boot which
229            // might interfere with background compilation (b/28612421).
230            // Unfortunately this will also means that "pm.dexopt.boot=speed-profile" will
231            // behave differently than "pm.dexopt.bg-dexopt=speed-profile" but that's a
232            // trade-off worth doing to save boot time work.
233            int result = pm.performDexOptWithStatus(new DexoptOptions(
234                    pkg,
235                    PackageManagerService.REASON_BOOT,
236                    DexoptOptions.DEXOPT_BOOT_COMPLETE));
237            if (result == PackageDexOptimizer.DEX_OPT_PERFORMED)  {
238                updatedPackages.add(pkg);
239            }
240        }
241        notifyPinService(updatedPackages);
242        // Ran to completion, so we abandon our timeslice and do not reschedule.
243        jobFinished(jobParams, /* reschedule */ false);
244    }
245
246    private boolean runIdleOptimization(final JobParameters jobParams,
247            final PackageManagerService pm, final ArraySet<String> pkgs) {
248        new Thread("BackgroundDexOptService_IdleOptimization") {
249            @Override
250            public void run() {
251                int result = idleOptimization(pm, pkgs, BackgroundDexOptService.this);
252                if (result != OPTIMIZE_ABORT_BY_JOB_SCHEDULER) {
253                    Log.w(TAG, "Idle optimizations aborted because of space constraints.");
254                    // If we didn't abort we ran to completion (or stopped because of space).
255                    // Abandon our timeslice and do not reschedule.
256                    jobFinished(jobParams, /* reschedule */ false);
257                }
258            }
259        }.start();
260        return true;
261    }
262
263    // Optimize the given packages and return the optimization result (one of the OPTIMIZE_* codes).
264    private int idleOptimization(PackageManagerService pm, ArraySet<String> pkgs,
265            Context context) {
266        Log.i(TAG, "Performing idle optimizations");
267        // If post-boot update is still running, request that it exits early.
268        mExitPostBootUpdate.set(true);
269        mAbortIdleOptimization.set(false);
270
271        long lowStorageThreshold = getLowStorageThreshold(context);
272        // Optimize primary apks.
273        int result = optimizePackages(pm, pkgs, lowStorageThreshold, /*is_for_primary_dex*/ true,
274                sFailedPackageNamesPrimary);
275
276        if (result == OPTIMIZE_ABORT_BY_JOB_SCHEDULER) {
277            return result;
278        }
279
280        if (SystemProperties.getBoolean("dalvik.vm.dexopt.secondary", false)) {
281            result = reconcileSecondaryDexFiles(pm.getDexManager());
282            if (result == OPTIMIZE_ABORT_BY_JOB_SCHEDULER) {
283                return result;
284            }
285
286            result = optimizePackages(pm, pkgs, lowStorageThreshold, /*is_for_primary_dex*/ false,
287                    sFailedPackageNamesSecondary);
288        }
289        return result;
290    }
291
292    private int optimizePackages(PackageManagerService pm, ArraySet<String> pkgs,
293            long lowStorageThreshold, boolean is_for_primary_dex,
294            ArraySet<String> failedPackageNames) {
295        ArraySet<String> updatedPackages = new ArraySet<>();
296        Set<String> unusedPackages = pm.getUnusedPackages(mDowngradeUnusedAppsThresholdInMillis);
297        // Only downgrade apps when space is low on device.
298        // Threshold is selected above the lowStorageThreshold so that we can pro-actively clean
299        // up disk before user hits the actual lowStorageThreshold.
300        final long lowStorageThresholdForDowngrade = LOW_THRESHOLD_MULTIPLIER_FOR_DOWNGRADE *
301                lowStorageThreshold;
302        boolean shouldDowngrade = shouldDowngrade(lowStorageThresholdForDowngrade);
303        for (String pkg : pkgs) {
304            int abort_code = abortIdleOptimizations(lowStorageThreshold);
305            if (abort_code == OPTIMIZE_ABORT_BY_JOB_SCHEDULER) {
306                return abort_code;
307            }
308
309            synchronized (failedPackageNames) {
310                if (failedPackageNames.contains(pkg)) {
311                    // Skip previously failing package
312                    continue;
313                }
314            }
315
316            int reason;
317            boolean downgrade;
318            // Downgrade unused packages.
319            if (unusedPackages.contains(pkg) && shouldDowngrade) {
320                // This applies for system apps or if packages location is not a directory, i.e.
321                // monolithic install.
322                if (is_for_primary_dex && !pm.canHaveOatDir(pkg)) {
323                    // For apps that don't have the oat directory, instead of downgrading,
324                    // remove their compiler artifacts from dalvik cache.
325                    pm.deleteOatArtifactsOfPackage(pkg);
326                    continue;
327                } else {
328                    reason = PackageManagerService.REASON_INACTIVE_PACKAGE_DOWNGRADE;
329                    downgrade = true;
330                }
331            } else if (abort_code != OPTIMIZE_ABORT_NO_SPACE_LEFT) {
332                reason = PackageManagerService.REASON_BACKGROUND_DEXOPT;
333                downgrade = false;
334            } else {
335                // can't dexopt because of low space.
336                continue;
337            }
338
339            synchronized (failedPackageNames) {
340                // Conservatively add package to the list of failing ones in case
341                // performDexOpt never returns.
342                failedPackageNames.add(pkg);
343            }
344
345            // Optimize package if needed. Note that there can be no race between
346            // concurrent jobs because PackageDexOptimizer.performDexOpt is synchronized.
347            boolean success;
348            int dexoptFlags =
349                    DexoptOptions.DEXOPT_CHECK_FOR_PROFILES_UPDATES |
350                    DexoptOptions.DEXOPT_BOOT_COMPLETE |
351                    (downgrade ? DexoptOptions.DEXOPT_DOWNGRADE : 0) |
352                    DexoptOptions.DEXOPT_IDLE_BACKGROUND_JOB;
353            if (is_for_primary_dex) {
354                int result = pm.performDexOptWithStatus(new DexoptOptions(pkg, reason,
355                        dexoptFlags));
356                success = result != PackageDexOptimizer.DEX_OPT_FAILED;
357                if (result == PackageDexOptimizer.DEX_OPT_PERFORMED) {
358                    updatedPackages.add(pkg);
359                }
360            } else {
361                success = pm.performDexOpt(new DexoptOptions(pkg,
362                        reason, dexoptFlags | DexoptOptions.DEXOPT_ONLY_SECONDARY_DEX));
363            }
364            if (success) {
365                // Dexopt succeeded, remove package from the list of failing ones.
366                synchronized (failedPackageNames) {
367                    failedPackageNames.remove(pkg);
368                }
369            }
370        }
371        notifyPinService(updatedPackages);
372        return OPTIMIZE_PROCESSED;
373    }
374
375    private int reconcileSecondaryDexFiles(DexManager dm) {
376        // TODO(calin): should we blacklist packages for which we fail to reconcile?
377        for (String p : dm.getAllPackagesWithSecondaryDexFiles()) {
378            if (mAbortIdleOptimization.get()) {
379                return OPTIMIZE_ABORT_BY_JOB_SCHEDULER;
380            }
381            dm.reconcileSecondaryDexFiles(p);
382        }
383        return OPTIMIZE_PROCESSED;
384    }
385
386    // Evaluate whether or not idle optimizations should continue.
387    private int abortIdleOptimizations(long lowStorageThreshold) {
388        if (mAbortIdleOptimization.get()) {
389            // JobScheduler requested an early abort.
390            return OPTIMIZE_ABORT_BY_JOB_SCHEDULER;
391        }
392        long usableSpace = mDataDir.getUsableSpace();
393        if (usableSpace < lowStorageThreshold) {
394            // Rather bail than completely fill up the disk.
395            Log.w(TAG, "Aborting background dex opt job due to low storage: " + usableSpace);
396            return OPTIMIZE_ABORT_NO_SPACE_LEFT;
397        }
398
399        return OPTIMIZE_CONTINUE;
400    }
401
402    // Evaluate whether apps should be downgraded.
403    private boolean shouldDowngrade(long lowStorageThresholdForDowngrade) {
404        long usableSpace = mDataDir.getUsableSpace();
405        if (usableSpace < lowStorageThresholdForDowngrade) {
406            return true;
407        }
408
409        return false;
410    }
411
412    /**
413     * Execute idle optimizations immediately on packages in packageNames. If packageNames is null,
414     * then execute on all packages.
415     */
416    public static boolean runIdleOptimizationsNow(PackageManagerService pm, Context context,
417            @Nullable List<String> packageNames) {
418        // Create a new object to make sure we don't interfere with the scheduled jobs.
419        // Note that this may still run at the same time with the job scheduled by the
420        // JobScheduler but the scheduler will not be able to cancel it.
421        BackgroundDexOptService bdos = new BackgroundDexOptService();
422        ArraySet<String> packagesToOptimize;
423        if (packageNames == null) {
424            packagesToOptimize = pm.getOptimizablePackages();
425        } else {
426            packagesToOptimize = new ArraySet<>(packageNames);
427        }
428        int result = bdos.idleOptimization(pm, packagesToOptimize, context);
429        return result == OPTIMIZE_PROCESSED;
430    }
431
432    @Override
433    public boolean onStartJob(JobParameters params) {
434        if (DEBUG_DEXOPT) {
435            Log.i(TAG, "onStartJob");
436        }
437
438        // NOTE: PackageManagerService.isStorageLow uses a different set of criteria from
439        // the checks above. This check is not "live" - the value is determined by a background
440        // restart with a period of ~1 minute.
441        PackageManagerService pm = (PackageManagerService)ServiceManager.getService("package");
442        if (pm.isStorageLow()) {
443            if (DEBUG_DEXOPT) {
444                Log.i(TAG, "Low storage, skipping this run");
445            }
446            return false;
447        }
448
449        final ArraySet<String> pkgs = pm.getOptimizablePackages();
450        if (pkgs.isEmpty()) {
451            if (DEBUG_DEXOPT) {
452                Log.i(TAG, "No packages to optimize");
453            }
454            return false;
455        }
456
457        boolean result;
458        if (params.getJobId() == JOB_POST_BOOT_UPDATE) {
459            result = runPostBootUpdate(params, pm, pkgs);
460        } else {
461            result = runIdleOptimization(params, pm, pkgs);
462        }
463
464        return result;
465    }
466
467    @Override
468    public boolean onStopJob(JobParameters params) {
469        if (DEBUG_DEXOPT) {
470            Log.i(TAG, "onStopJob");
471        }
472
473        if (params.getJobId() == JOB_POST_BOOT_UPDATE) {
474            mAbortPostBootUpdate.set(true);
475
476            // Do not reschedule.
477            // TODO: We should reschedule if we didn't process all apps, yet.
478            return false;
479        } else {
480            mAbortIdleOptimization.set(true);
481
482            // Reschedule the run.
483            // TODO: Should this be dependent on the stop reason?
484            return true;
485        }
486    }
487
488    private void notifyPinService(ArraySet<String> updatedPackages) {
489        PinnerService pinnerService = LocalServices.getService(PinnerService.class);
490        if (pinnerService != null) {
491            Log.i(TAG, "Pinning optimized code " + updatedPackages);
492            pinnerService.update(updatedPackages);
493        }
494    }
495
496    private static long getDowngradeUnusedAppsThresholdInMillis() {
497        final String sysPropKey = "pm.dexopt.downgrade_after_inactive_days";
498        String sysPropValue = SystemProperties.get(sysPropKey);
499        if (sysPropValue == null || sysPropValue.isEmpty()) {
500            Log.w(TAG, "SysProp " + sysPropKey + " not set");
501            return Long.MAX_VALUE;
502        }
503        return TimeUnit.DAYS.toMillis(Long.parseLong(sysPropValue));
504    }
505
506    private static boolean isBackgroundDexoptDisabled() {
507        return SystemProperties.getBoolean("pm.dexopt.disable_bg_dexopt" /* key */,
508                false /* default */);
509    }
510}
511