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