blob: f8d6b5643e717fd8c82b3c5699b22fd38e4617fe [file] [log] [blame]
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server.art;
import static com.android.server.art.DexUseManagerLocal.DetailedSecondaryDexInfo;
import static com.android.server.art.DexUseManagerLocal.SecondaryDexInfo;
import static com.android.server.art.PrimaryDexUtils.DetailedPrimaryDexInfo;
import static com.android.server.art.PrimaryDexUtils.PrimaryDexInfo;
import static com.android.server.art.ReasonMapping.BatchDexoptReason;
import static com.android.server.art.ReasonMapping.BootReason;
import static com.android.server.art.Utils.Abi;
import static com.android.server.art.Utils.InitProfileResult;
import static com.android.server.art.model.ArtFlags.GetStatusFlags;
import static com.android.server.art.model.ArtFlags.ScheduleStatus;
import static com.android.server.art.model.Config.Callback;
import static com.android.server.art.model.DexoptStatus.DexContainerFileDexoptStatus;
import android.annotation.CallbackExecutor;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SuppressLint;
import android.annotation.SystemApi;
import android.annotation.SystemService;
import android.app.job.JobInfo;
import android.apphibernation.AppHibernationManager;
import android.content.Context;
import android.os.Binder;
import android.os.Build;
import android.os.CancellationSignal;
import android.os.ParcelFileDescriptor;
import android.os.Process;
import android.os.RemoteException;
import android.os.ServiceSpecificException;
import android.os.SystemProperties;
import android.os.UserHandle;
import android.os.UserManager;
import android.os.storage.StorageManager;
import android.text.TextUtils;
import android.util.Log;
import android.util.Pair;
import androidx.annotation.RequiresApi;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.LocalManagerRegistry;
import com.android.server.art.model.ArtFlags;
import com.android.server.art.model.BatchDexoptParams;
import com.android.server.art.model.Config;
import com.android.server.art.model.DeleteResult;
import com.android.server.art.model.DetailedDexInfo;
import com.android.server.art.model.DexoptParams;
import com.android.server.art.model.DexoptResult;
import com.android.server.art.model.DexoptStatus;
import com.android.server.art.model.OperationProgress;
import com.android.server.pm.PackageManagerLocal;
import com.android.server.pm.pkg.AndroidPackage;
import com.android.server.pm.pkg.AndroidPackageSplit;
import com.android.server.pm.pkg.PackageState;
import dalvik.system.DexFile;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* This class provides a system API for functionality provided by the ART module.
*
* Note: Although this class is the entry point of ART services, this class is not a {@link
* SystemService}, and it does not publish a binder. Instead, it is a module loaded by the
* system_server process, registered in {@link LocalManagerRegistry}. {@link LocalManagerRegistry}
* specifies that in-process module interfaces should be named with the suffix {@code ManagerLocal}
* for consistency.
*
* @hide
*/
@SystemApi(client = SystemApi.Client.SYSTEM_SERVER)
public final class ArtManagerLocal {
/** @hide */
public static final String TAG = "ArtService";
private static final String[] CLASSPATHS_FOR_BOOT_IMAGE_PROFILE = {
"BOOTCLASSPATH", "SYSTEMSERVERCLASSPATH", "STANDALONE_SYSTEMSERVER_JARS"};
/** @hide */
@VisibleForTesting public static final long DOWNGRADE_THRESHOLD_ABOVE_LOW_BYTES = 500_000_000;
@NonNull private final Injector mInjector;
@Deprecated
public ArtManagerLocal() {
mInjector = new Injector(this, null /* context */);
}
/**
* Creates an instance.
*
* Only {@code SystemServer} should create an instance and register it in {@link
* LocalManagerRegistry}. Other API users should obtain the instance from {@link
* LocalManagerRegistry}.
*
* @param context the system server context
* @throws NullPointerException if required dependencies are missing
*/
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public ArtManagerLocal(@NonNull Context context) {
mInjector = new Injector(this, context);
}
/** @hide */
@VisibleForTesting
public ArtManagerLocal(@NonNull Injector injector) {
mInjector = injector;
}
/**
* Handles ART Service commands, which is a subset of `cmd package` commands.
*
* Note: This method is not an override of {@link Binder#handleShellCommand} because ART
* services does not publish a binder. Instead, it handles the commands forwarded by the
* `package` service. The semantics of the parameters are the same as {@link
* Binder#handleShellCommand}.
*
* @return zero on success, non-zero on internal error (e.g., I/O error)
* @throws SecurityException if the caller is not root
* @throws IllegalArgumentException if the arguments are illegal
* @see ArtShellCommand#printHelp(PrintWriter)
*/
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public int handleShellCommand(@NonNull Binder target, @NonNull ParcelFileDescriptor in,
@NonNull ParcelFileDescriptor out, @NonNull ParcelFileDescriptor err,
@NonNull String[] args) {
return new ArtShellCommand(this, mInjector.getPackageManagerLocal())
.exec(target, in.getFileDescriptor(), out.getFileDescriptor(),
err.getFileDescriptor(), args);
}
/** Prints ART Service shell command help. */
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public void printShellCommandHelp(@NonNull PrintWriter pw) {
ArtShellCommand.printHelp(pw);
}
/**
* Deletes dexopt artifacts of a package, including the artifacts for primary dex files and the
* ones for secondary dex files. This includes VDEX, ODEX, and ART files.
*
* @throws IllegalArgumentException if the package is not found or the flags are illegal
* @throws IllegalStateException if the operation encounters an error that should never happen
* (e.g., an internal logic error).
*/
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
@NonNull
public DeleteResult deleteDexoptArtifacts(
@NonNull PackageManagerLocal.FilteredSnapshot snapshot, @NonNull String packageName) {
PackageState pkgState = Utils.getPackageStateOrThrow(snapshot, packageName);
AndroidPackage pkg = Utils.getPackageOrThrow(pkgState);
try {
long freedBytes = 0;
boolean isInDalvikCache = Utils.isInDalvikCache(pkgState, mInjector.getArtd());
for (PrimaryDexInfo dexInfo : PrimaryDexUtils.getDexInfo(pkg)) {
if (!dexInfo.hasCode()) {
continue;
}
for (Abi abi : Utils.getAllAbis(pkgState)) {
freedBytes += mInjector.getArtd().deleteArtifacts(AidlUtils.buildArtifactsPath(
dexInfo.dexPath(), abi.isa(), isInDalvikCache));
}
}
for (SecondaryDexInfo dexInfo :
mInjector.getDexUseManager().getSecondaryDexInfo(packageName)) {
for (Abi abi : Utils.getAllAbisForNames(dexInfo.abiNames(), pkgState)) {
freedBytes += mInjector.getArtd().deleteArtifacts(AidlUtils.buildArtifactsPath(
dexInfo.dexPath(), abi.isa(), false /* isInDalvikCache */));
}
}
return DeleteResult.create(freedBytes);
} catch (RemoteException e) {
Utils.logArtdException(e);
return DeleteResult.create(0 /* freedBytes */);
}
}
/**
* Returns the dexopt status of a package.
*
* Uses the default flags ({@link ArtFlags#defaultGetStatusFlags()}).
*
* @throws IllegalArgumentException if the package is not found or the flags are illegal
* @throws IllegalStateException if the operation encounters an error that should never happen
* (e.g., an internal logic error).
*/
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
@NonNull
public DexoptStatus getDexoptStatus(
@NonNull PackageManagerLocal.FilteredSnapshot snapshot, @NonNull String packageName) {
return getDexoptStatus(snapshot, packageName, ArtFlags.defaultGetStatusFlags());
}
/**
* Same as above, but allows to specify flags.
*
* @see #getDexoptStatus(PackageManagerLocal.FilteredSnapshot, String)
*/
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
@NonNull
public DexoptStatus getDexoptStatus(@NonNull PackageManagerLocal.FilteredSnapshot snapshot,
@NonNull String packageName, @GetStatusFlags int flags) {
if ((flags & ArtFlags.FLAG_FOR_PRIMARY_DEX) == 0
&& (flags & ArtFlags.FLAG_FOR_SECONDARY_DEX) == 0) {
throw new IllegalArgumentException("Nothing to check");
}
PackageState pkgState = Utils.getPackageStateOrThrow(snapshot, packageName);
AndroidPackage pkg = Utils.getPackageOrThrow(pkgState);
List<Pair<DetailedDexInfo, Abi>> dexAndAbis = new ArrayList<>();
if ((flags & ArtFlags.FLAG_FOR_PRIMARY_DEX) != 0) {
for (DetailedPrimaryDexInfo dexInfo :
PrimaryDexUtils.getDetailedDexInfo(pkgState, pkg)) {
if (!dexInfo.hasCode()) {
continue;
}
for (Abi abi : Utils.getAllAbis(pkgState)) {
dexAndAbis.add(Pair.create(dexInfo, abi));
}
}
}
if ((flags & ArtFlags.FLAG_FOR_SECONDARY_DEX) != 0) {
for (SecondaryDexInfo dexInfo :
mInjector.getDexUseManager().getSecondaryDexInfo(packageName)) {
for (Abi abi : Utils.getAllAbisForNames(dexInfo.abiNames(), pkgState)) {
dexAndAbis.add(Pair.create(dexInfo, abi));
}
}
}
try {
List<DexContainerFileDexoptStatus> statuses = new ArrayList<>();
for (Pair<DetailedDexInfo, Abi> pair : dexAndAbis) {
DetailedDexInfo dexInfo = pair.first;
Abi abi = pair.second;
try {
GetDexoptStatusResult result = mInjector.getArtd().getDexoptStatus(
dexInfo.dexPath(), abi.isa(), dexInfo.classLoaderContext());
statuses.add(DexContainerFileDexoptStatus.create(dexInfo.dexPath(),
dexInfo instanceof DetailedPrimaryDexInfo, abi.isPrimaryAbi(),
abi.name(), result.compilerFilter, result.compilationReason,
result.locationDebugString));
} catch (ServiceSpecificException e) {
statuses.add(DexContainerFileDexoptStatus.create(dexInfo.dexPath(),
dexInfo instanceof DetailedPrimaryDexInfo, abi.isPrimaryAbi(),
abi.name(), "error", "error", e.getMessage()));
}
}
return DexoptStatus.create(statuses);
} catch (RemoteException e) {
Utils.logArtdException(e);
List<DexContainerFileDexoptStatus> statuses = new ArrayList<>();
for (Pair<DetailedDexInfo, Abi> pair : dexAndAbis) {
DetailedDexInfo dexInfo = pair.first;
Abi abi = pair.second;
statuses.add(DexContainerFileDexoptStatus.create(dexInfo.dexPath(),
dexInfo instanceof DetailedPrimaryDexInfo, abi.isPrimaryAbi(), abi.name(),
"error", "error", e.getMessage()));
}
return DexoptStatus.create(statuses);
}
}
/**
* Clear the profiles that are collected locally for the given package, including the profiles
* for primary and secondary dex files. More specifically, it clears reference profiles and
* current profiles. External profiles (e.g., cloud profiles) will be kept.
*
* @throws IllegalArgumentException if the package is not found or the flags are illegal
* @throws IllegalStateException if the operation encounters an error that should never happen
* (e.g., an internal logic error).
*/
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
@NonNull
public void clearAppProfiles(
@NonNull PackageManagerLocal.FilteredSnapshot snapshot, @NonNull String packageName) {
PackageState pkgState = Utils.getPackageStateOrThrow(snapshot, packageName);
AndroidPackage pkg = Utils.getPackageOrThrow(pkgState);
try {
for (PrimaryDexInfo dexInfo : PrimaryDexUtils.getDexInfo(pkg)) {
if (!dexInfo.hasCode()) {
continue;
}
mInjector.getArtd().deleteProfile(
PrimaryDexUtils.buildRefProfilePath(pkgState, dexInfo));
for (ProfilePath profile : PrimaryDexUtils.getCurProfiles(
mInjector.getUserManager(), pkgState, dexInfo)) {
mInjector.getArtd().deleteProfile(profile);
}
}
// This only deletes the profiles of known secondary dex files. If there are unknown
// secondary dex files, their profiles will be deleted by `cleanup`.
for (SecondaryDexInfo dexInfo :
mInjector.getDexUseManager().getSecondaryDexInfo(packageName)) {
mInjector.getArtd().deleteProfile(
AidlUtils.buildProfilePathForSecondaryRef(dexInfo.dexPath()));
mInjector.getArtd().deleteProfile(
AidlUtils.buildProfilePathForSecondaryCur(dexInfo.dexPath()));
}
} catch (RemoteException e) {
Utils.logArtdException(e);
}
}
/**
* Dexopts a package. The time this operation takes ranges from a few milliseconds to several
* minutes, depending on the params and the code size of the package.
*
* When this operation ends (either completed or cancelled), callbacks added by {@link
* #addDexoptDoneCallback(Executor, DexoptDoneCallback)} are called.
*
* @throws IllegalArgumentException if the package is not found or the params are illegal
* @throws IllegalStateException if the operation encounters an error that should never happen
* (e.g., an internal logic error).
*/
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
@NonNull
public DexoptResult dexoptPackage(@NonNull PackageManagerLocal.FilteredSnapshot snapshot,
@NonNull String packageName, @NonNull DexoptParams params) {
var cancellationSignal = new CancellationSignal();
return dexoptPackage(snapshot, packageName, params, cancellationSignal);
}
/**
* Same as above, but supports cancellation.
*
* @see #dexoptPackage(PackageManagerLocal.FilteredSnapshot, String, DexoptParams)
*/
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
@NonNull
public DexoptResult dexoptPackage(@NonNull PackageManagerLocal.FilteredSnapshot snapshot,
@NonNull String packageName, @NonNull DexoptParams params,
@NonNull CancellationSignal cancellationSignal) {
return mInjector.getDexoptHelper().dexopt(
snapshot, List.of(packageName), params, cancellationSignal, Runnable::run);
}
/**
* Resets the dexopt state of the package as if the package is newly installed.
*
* More specifically, it clears reference profiles, current profiles, and any code compiled from
* those local profiles. If there is an external profile (e.g., a cloud profile), the code
* compiled from that profile will be kept.
*
* For secondary dex files, it also clears all dexopt artifacts.
*
* @hide
*/
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
@NonNull
public DexoptResult resetDexoptStatus(@NonNull PackageManagerLocal.FilteredSnapshot snapshot,
@NonNull String packageName, @NonNull CancellationSignal cancellationSignal) {
// We must delete the artifacts for primary dex files beforehand rather than relying on
// `dexoptPackage` to replace them because:
// - If dexopt is not needed after the deletion, then we shouldn't run dexopt at all. For
// example, when we have a DM file that contains a VDEX file but doesn't contain a cloud
// profile, this happens. Note that this is more about correctness rather than
// performance.
// - We don't want the existing artifacts to affect dexopt. For example, the existing VDEX
// file should not be an input VDEX.
//
// We delete the artifacts for secondary dex files and `dexoptPackage` won't re-generate
// them because `dexoptPackage` for `REASON_INSTALL` is for primary dex only. This is
// intentional because secondary dex files are supposed to be unknown at install time.
deleteDexoptArtifacts(snapshot, packageName);
clearAppProfiles(snapshot, packageName);
// Re-generate artifacts for primary dex files if needed.
return dexoptPackage(snapshot, packageName,
new DexoptParams.Builder(ReasonMapping.REASON_INSTALL).build(), cancellationSignal);
}
/**
* Runs batch dexopt for the given reason.
*
* This is called by ART Service automatically during boot / background dexopt.
*
* The list of packages and options are determined by {@code reason}, and can be overridden by
* {@link #setBatchDexoptStartCallback(Executor, BatchDexoptStartCallback)}.
*
* The dexopt is done in a thread pool. The number of packages being dexopted
* simultaneously can be configured by system property {@code pm.dexopt.<reason>.concurrency}
* (e.g., {@code pm.dexopt.bg-dexopt.concurrency=4}), and the number of threads for each {@code
* dex2oat} invocation can be configured by system property {@code dalvik.vm.*dex2oat-threads}
* (e.g., {@code dalvik.vm.background-dex2oat-threads=4}). I.e., the maximum number of
* concurrent threads is the product of the two system properties. Note that the physical core
* usage is always bound by {@code dalvik.vm.*dex2oat-cpu-set} regardless of the number of
* threads.
*
* When this operation ends (either completed or cancelled), callbacks added by {@link
* #addDexoptDoneCallback(Executor, DexoptDoneCallback)} are called.
*
* If the storage is nearly low, and {@code reason} is {@link ReasonMapping#REASON_BG_DEXOPT},
* it may also downgrade some inactive packages to a less optimized compiler filter, specified
* by the system property {@code pm.dexopt.inactive} (typically "verify"), to free up some
* space. This feature is only enabled when the system property {@code
* pm.dexopt.downgrade_after_inactive_days} is set. The space threshold to trigger this feature
* is the Storage Manager's low space threshold plus {@link
* #DOWNGRADE_THRESHOLD_ABOVE_LOW_BYTES}. The concurrency can be configured by system property
* {@code pm.dexopt.bg-dexopt.concurrency}. The packages in the list provided by
* {@link BatchDexoptStartCallback} for {@link ReasonMapping#REASON_BG_DEXOPT} are never
* downgraded.
*
* @param snapshot the snapshot from {@link PackageManagerLocal} to operate on
* @param reason determines the default list of packages and options
* @param cancellationSignal provides the ability to cancel this operation
* @param processCallbackExecutor the executor to call {@code progressCallback}
* @param progressCallbacks a mapping from an integer, in {@link ArtFlags.BatchDexoptPass}, to
* the callback that is called repeatedly whenever there is an update on the progress
* @return a mapping from an integer, in {@link ArtFlags.BatchDexoptPass}, to the dexopt result.
* @throws IllegalStateException if the operation encounters an error that should never happen
* (e.g., an internal logic error), or the callback set by {@link
* #setBatchDexoptStartCallback(Executor, BatchDexoptStartCallback)} provides invalid
* params.
*
* @hide
*/
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
@NonNull
public Map<Integer, DexoptResult> dexoptPackages(
@NonNull PackageManagerLocal.FilteredSnapshot snapshot,
@NonNull @BatchDexoptReason String reason,
@NonNull CancellationSignal cancellationSignal,
@Nullable @CallbackExecutor Executor progressCallbackExecutor,
@Nullable Map<Integer, Consumer<OperationProgress>> progressCallbacks) {
List<String> defaultPackages =
Collections.unmodifiableList(getDefaultPackages(snapshot, reason));
DexoptParams defaultDexoptParams = new DexoptParams.Builder(reason).build();
var builder = new BatchDexoptParams.Builder(defaultPackages, defaultDexoptParams);
Callback<BatchDexoptStartCallback, Void> callback =
mInjector.getConfig().getBatchDexoptStartCallback();
if (callback != null) {
Utils.executeAndWait(callback.executor(), () -> {
callback.get().onBatchDexoptStart(
snapshot, reason, defaultPackages, builder, cancellationSignal);
});
}
BatchDexoptParams params = builder.build();
Utils.check(params.getDexoptParams().getReason().equals(reason));
ExecutorService dexoptExecutor =
Executors.newFixedThreadPool(ReasonMapping.getConcurrencyForReason(reason));
Map<Integer, DexoptResult> dexoptResults = new HashMap<>();
try {
if (reason.equals(ReasonMapping.REASON_BG_DEXOPT)) {
DexoptResult downgradeResult = maybeDowngradePackages(snapshot,
new HashSet<>(params.getPackages()) /* excludedPackages */,
cancellationSignal, dexoptExecutor, progressCallbackExecutor,
progressCallbacks != null ? progressCallbacks.get(ArtFlags.PASS_DOWNGRADE)
: null);
if (downgradeResult != null) {
dexoptResults.put(ArtFlags.PASS_DOWNGRADE, downgradeResult);
}
}
Log.i(TAG,
"Dexopting " + params.getPackages().size() + " packages with reason=" + reason);
DexoptResult mainResult = mInjector.getDexoptHelper().dexopt(snapshot,
params.getPackages(), params.getDexoptParams(), cancellationSignal,
dexoptExecutor, progressCallbackExecutor,
progressCallbacks != null ? progressCallbacks.get(ArtFlags.PASS_MAIN) : null);
dexoptResults.put(ArtFlags.PASS_MAIN, mainResult);
if (reason.equals(ReasonMapping.REASON_BG_DEXOPT)) {
DexoptResult supplementaryResult = maybeDexoptPackagesSupplementaryPass(snapshot,
mainResult, params.getDexoptParams(), cancellationSignal, dexoptExecutor,
progressCallbackExecutor,
progressCallbacks != null
? progressCallbacks.get(ArtFlags.PASS_SUPPLEMENTARY)
: null);
if (supplementaryResult != null) {
dexoptResults.put(ArtFlags.PASS_SUPPLEMENTARY, supplementaryResult);
}
}
return dexoptResults;
} finally {
dexoptExecutor.shutdown();
}
}
/**
* Overrides the default params for {@link #dexoptPackages}. This method is thread-safe.
*
* This method gives users the opportunity to change the behavior of {@link #dexoptPackages},
* which is called by ART Service automatically during boot / background dexopt.
*
* If this method is not called, the default list of packages and options determined by {@code
* reason} will be used.
*/
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public void setBatchDexoptStartCallback(@NonNull @CallbackExecutor Executor executor,
@NonNull BatchDexoptStartCallback callback) {
mInjector.getConfig().setBatchDexoptStartCallback(executor, callback);
}
/**
* Clears the callback set by {@link
* #setBatchDexoptStartCallback(Executor, BatchDexoptStartCallback)}. This method is
* thread-safe.
*/
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public void clearBatchDexoptStartCallback() {
mInjector.getConfig().clearBatchDexoptStartCallback();
}
/**
* Schedules a background dexopt job. Does nothing if the job is already scheduled.
*
* Use this method if you want the system to automatically determine the best time to run
* dexopt.
*
* The job will be run by the job scheduler. The job scheduling configuration can be overridden
* by {@link
* #setScheduleBackgroundDexoptJobCallback(Executor, ScheduleBackgroundDexoptJobCallback)}. By
* default, it runs periodically (at most once a day) when all the following constraints are
* meet.
*
* <ul>
* <li>The device is idling. (see {@link JobInfo.Builder#setRequiresDeviceIdle(boolean)})
* <li>The device is charging. (see {@link JobInfo.Builder#setRequiresCharging(boolean)})
* <li>The battery level is not low.
* (see {@link JobInfo.Builder#setRequiresBatteryNotLow(boolean)})
* </ul>
*
* When the job is running, it may be cancelled by the job scheduler immediately whenever one of
* the constraints above is no longer met or cancelled by the {@link
* #cancelBackgroundDexoptJob()} API. The job scheduler retries it in the next <i>maintenance
* window</i>. For information about <i>maintenance window</i>, see
* https://developer.android.com/training/monitoring-device-state/doze-standby.
*
* See {@link #dexoptPackages} for how to customize the behavior of the job.
*
* When the job ends (either completed or cancelled), the result is sent to the callbacks added
* by {@link #addDexoptDoneCallback(Executor, DexoptDoneCallback)} with the
* reason {@link ReasonMapping#REASON_BG_DEXOPT}.
*
* @throws RuntimeException if called during boot before the job scheduler service has started.
*/
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public @ScheduleStatus int scheduleBackgroundDexoptJob() {
return mInjector.getBackgroundDexoptJob().schedule();
}
/**
* Unschedules the background dexopt job scheduled by {@link #scheduleBackgroundDexoptJob()}.
* Does nothing if the job is not scheduled.
*
* Use this method if you no longer want the system to automatically run dexopt.
*
* If the job is already started by the job scheduler and is running, it will be cancelled
* immediately, and the result sent to the callbacks added by {@link
* #addDexoptDoneCallback(Executor, DexoptDoneCallback)} will contain {@link
* DexoptResult#DEXOPT_CANCELLED}. Note that a job started by {@link
* #startBackgroundDexoptJob()} will not be cancelled by this method.
*/
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public void unscheduleBackgroundDexoptJob() {
mInjector.getBackgroundDexoptJob().unschedule();
}
/**
* Overrides the configuration of the background dexopt job. This method is thread-safe.
*/
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public void setScheduleBackgroundDexoptJobCallback(@NonNull @CallbackExecutor Executor executor,
@NonNull ScheduleBackgroundDexoptJobCallback callback) {
mInjector.getConfig().setScheduleBackgroundDexoptJobCallback(executor, callback);
}
/**
* Clears the callback set by {@link
* #setScheduleBackgroundDexoptJobCallback(Executor, ScheduleBackgroundDexoptJobCallback)}. This
* method is thread-safe.
*/
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public void clearScheduleBackgroundDexoptJobCallback() {
mInjector.getConfig().clearScheduleBackgroundDexoptJobCallback();
}
/**
* Manually starts a background dexopt job. Does nothing if a job is already started by this
* method or by the job scheduler. This method is not blocking.
*
* Unlike the job started by job scheduler, the job started by this method does not respect
* constraints described in {@link #scheduleBackgroundDexoptJob()}, and hence will not be
* cancelled when they aren't met.
*
* See {@link #dexoptPackages} for how to customize the behavior of the job.
*
* When the job ends (either completed or cancelled), the result is sent to the callbacks added
* by {@link #addDexoptDoneCallback(Executor, DexoptDoneCallback)} with the
* reason {@link ReasonMapping#REASON_BG_DEXOPT}.
*/
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public void startBackgroundDexoptJob() {
mInjector.getBackgroundDexoptJob().start();
}
/**
* Same as above, but also returns a {@link CompletableFuture}.
*
* @hide
*/
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
@NonNull
public CompletableFuture<BackgroundDexoptJob.Result> startBackgroundDexoptJobAndReturnFuture() {
return mInjector.getBackgroundDexoptJob().start();
}
/**
* Returns the running background dexopt job, or null of no background dexopt job is running.
*
* @hide
*/
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
@Nullable
public CompletableFuture<BackgroundDexoptJob.Result> getRunningBackgroundDexoptJob() {
return mInjector.getBackgroundDexoptJob().get();
}
/**
* Cancels the running background dexopt job started by the job scheduler or by {@link
* #startBackgroundDexoptJob()}. Does nothing if the job is not running. This method is not
* blocking.
*
* The result sent to the callbacks added by {@link
* #addDexoptDoneCallback(Executor, DexoptDoneCallback)} will contain {@link
* DexoptResult#DEXOPT_CANCELLED}.
*/
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public void cancelBackgroundDexoptJob() {
mInjector.getBackgroundDexoptJob().cancel();
}
/**
* Adds a global listener that listens to any result of dexopting package(s), no matter run
* manually or automatically. Calling this method multiple times with different callbacks is
* allowed. Callbacks are executed in the same order as the one in which they were added. This
* method is thread-safe.
*
* @param onlyIncludeUpdates if true, the results passed to the callback will only contain
* packages that have any update, and the callback won't be called with results that
* don't have any update.
* @throws IllegalStateException if the same callback instance is already added
*/
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public void addDexoptDoneCallback(boolean onlyIncludeUpdates,
@NonNull @CallbackExecutor Executor executor, @NonNull DexoptDoneCallback callback) {
mInjector.getConfig().addDexoptDoneCallback(onlyIncludeUpdates, executor, callback);
}
/**
* Removes the listener added by {@link
* #addDexoptDoneCallback(Executor, DexoptDoneCallback)}. Does nothing if the
* callback was not added. This method is thread-safe.
*/
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public void removeDexoptDoneCallback(@NonNull DexoptDoneCallback callback) {
mInjector.getConfig().removeDexoptDoneCallback(callback);
}
/**
* Snapshots the profile of the given app split. The profile snapshot is the aggregation of all
* existing profiles of the app split (all current user profiles and the reference profile).
*
* @param snapshot the snapshot from {@link PackageManagerLocal} to operate on
* @param packageName the name of the app that owns the profile
* @param splitName see {@link AndroidPackageSplit#getName()}
* @return the file descriptor of the snapshot. It doesn't have any path associated with it. The
* caller is responsible for closing it. Note that the content may be empty.
* @throws IllegalArgumentException if the package or the split is not found
* @throws IllegalStateException if the operation encounters an error that should never happen
* (e.g., an internal logic error).
* @throws SnapshotProfileException if the operation encounters an error that the caller should
* handle (e.g., an I/O error, a sub-process crash).
*/
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
@NonNull
public ParcelFileDescriptor snapshotAppProfile(
@NonNull PackageManagerLocal.FilteredSnapshot snapshot, @NonNull String packageName,
@Nullable String splitName) throws SnapshotProfileException {
var options = new MergeProfileOptions();
options.forceMerge = true;
return snapshotOrDumpAppProfile(snapshot, packageName, splitName, options);
}
/**
* Same as above, but outputs in text format.
*
* @hide
*/
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
@NonNull
public ParcelFileDescriptor dumpAppProfile(
@NonNull PackageManagerLocal.FilteredSnapshot snapshot, @NonNull String packageName,
@Nullable String splitName, boolean dumpClassesAndMethods)
throws SnapshotProfileException {
var options = new MergeProfileOptions();
options.dumpOnly = !dumpClassesAndMethods;
options.dumpClassesAndMethods = dumpClassesAndMethods;
return snapshotOrDumpAppProfile(snapshot, packageName, splitName, options);
}
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
@NonNull
private ParcelFileDescriptor snapshotOrDumpAppProfile(
@NonNull PackageManagerLocal.FilteredSnapshot snapshot, @NonNull String packageName,
@Nullable String splitName, @NonNull MergeProfileOptions options)
throws SnapshotProfileException {
try {
PackageState pkgState = Utils.getPackageStateOrThrow(snapshot, packageName);
AndroidPackage pkg = Utils.getPackageOrThrow(pkgState);
PrimaryDexInfo dexInfo = PrimaryDexUtils.getDexInfoBySplitName(pkg, splitName);
List<ProfilePath> profiles = new ArrayList<>();
InitProfileResult result = Utils.getOrInitReferenceProfile(mInjector.getArtd(),
dexInfo.dexPath(), PrimaryDexUtils.buildRefProfilePath(pkgState, dexInfo),
PrimaryDexUtils.getExternalProfiles(dexInfo),
PrimaryDexUtils.buildOutputProfile(pkgState, dexInfo, Process.SYSTEM_UID,
Process.SYSTEM_UID, false /* isPublic */));
if (!result.externalProfileErrors().isEmpty()) {
Log.e(TAG,
"Error occurred when initializing from external profiles: "
+ result.externalProfileErrors());
}
ProfilePath refProfile = result.profile();
if (refProfile != null) {
profiles.add(refProfile);
}
profiles.addAll(
PrimaryDexUtils.getCurProfiles(mInjector.getUserManager(), pkgState, dexInfo));
OutputProfile output = PrimaryDexUtils.buildOutputProfile(pkgState, dexInfo,
Process.SYSTEM_UID, Process.SYSTEM_UID, false /* isPublic */);
try {
return mergeProfilesAndGetFd(profiles, output, List.of(dexInfo.dexPath()), options);
} finally {
if (refProfile != null && refProfile.getTag() == ProfilePath.tmpProfilePath) {
mInjector.getArtd().deleteProfile(refProfile);
}
}
} catch (RemoteException e) {
throw new SnapshotProfileException(e);
}
}
/**
* Snapshots the boot image profile
* (https://source.android.com/docs/core/bootloader/boot-image-profiles). The profile snapshot
* is the aggregation of all existing profiles on the device (all current user profiles and
* reference profiles) of all apps and the system server filtered by applicable classpaths.
*
* @param snapshot the snapshot from {@link PackageManagerLocal} to operate on
* @return the file descriptor of the snapshot. It doesn't have any path associated with it. The
* caller is responsible for closing it. Note that the content may be empty.
* @throws IllegalStateException if the operation encounters an error that should never happen
* (e.g., an internal logic error).
* @throws SnapshotProfileException if the operation encounters an error that the caller should
* handle (e.g., an I/O error, a sub-process crash).
*/
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
@NonNull
public ParcelFileDescriptor snapshotBootImageProfile(
@NonNull PackageManagerLocal.FilteredSnapshot snapshot)
throws SnapshotProfileException {
if (!Constants.isBootImageProfilingEnabled()) {
throw new SnapshotProfileException("Boot image profiling not enabled");
}
List<ProfilePath> profiles = new ArrayList<>();
// System server profiles.
profiles.add(AidlUtils.buildProfilePathForPrimaryRef(
Utils.PLATFORM_PACKAGE_NAME, PrimaryDexUtils.PROFILE_PRIMARY));
for (UserHandle handle :
mInjector.getUserManager().getUserHandles(true /* excludeDying */)) {
profiles.add(AidlUtils.buildProfilePathForPrimaryCur(handle.getIdentifier(),
Utils.PLATFORM_PACKAGE_NAME, PrimaryDexUtils.PROFILE_PRIMARY));
}
// App profiles.
snapshot.getPackageStates().forEach((packageName, appPkgState) -> {
// Hibernating apps can still provide useful profile contents, so skip the hibernation
// check.
if (Utils.canDexoptPackage(appPkgState, null /* appHibernationManager */)) {
AndroidPackage appPkg = Utils.getPackageOrThrow(appPkgState);
for (PrimaryDexInfo appDexInfo : PrimaryDexUtils.getDexInfo(appPkg)) {
if (!appDexInfo.hasCode()) {
continue;
}
profiles.add(PrimaryDexUtils.buildRefProfilePath(appPkgState, appDexInfo));
profiles.addAll(PrimaryDexUtils.getCurProfiles(
mInjector.getUserManager(), appPkgState, appDexInfo));
}
}
});
OutputProfile output = AidlUtils.buildOutputProfileForPrimary(Utils.PLATFORM_PACKAGE_NAME,
PrimaryDexUtils.PROFILE_PRIMARY, Process.SYSTEM_UID, Process.SYSTEM_UID,
false /* isPublic */);
List<String> dexPaths = Arrays.stream(CLASSPATHS_FOR_BOOT_IMAGE_PROFILE)
.map(envVar -> Constants.getenv(envVar))
.filter(classpath -> !TextUtils.isEmpty(classpath))
.flatMap(classpath -> Arrays.stream(classpath.split(":")))
.collect(Collectors.toList());
var options = new MergeProfileOptions();
options.forceMerge = true;
options.forBootImage = true;
return mergeProfilesAndGetFd(profiles, output, dexPaths, options);
}
/**
* Notifies ART Service that this is a boot that falls into one of the categories listed in
* {@link BootReason}. The current behavior is that ART Service goes through all recently used
* packages and dexopts those that are not dexopted. This might change in the future.
*
* This method is blocking. It takes about 30 seconds to a few minutes. During execution, {@code
* progressCallback} is repeatedly called whenever there is an update on the progress.
*
* See {@link #dexoptPackages} for how to customize the behavior.
*/
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public void onBoot(@NonNull @BootReason String bootReason,
@Nullable @CallbackExecutor Executor progressCallbackExecutor,
@Nullable Consumer<OperationProgress> progressCallback) {
try (var snapshot = mInjector.getPackageManagerLocal().withFilteredSnapshot()) {
dexoptPackages(snapshot, bootReason, new CancellationSignal(), progressCallbackExecutor,
progressCallback != null ? Map.of(ArtFlags.PASS_MAIN, progressCallback) : null);
}
}
/**
* Dumps the dexopt state of all packages in text format for debugging purposes.
*
* There are no stability guarantees for the output format.
*
* @throws IllegalStateException if the operation encounters an error that should never happen
* (e.g., an internal logic error).
*/
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public void dump(
@NonNull PrintWriter pw, @NonNull PackageManagerLocal.FilteredSnapshot snapshot) {
new DumpHelper(this).dump(pw, snapshot);
}
/**
* Dumps the dexopt state of the given package in text format for debugging purposes.
*
* There are no stability guarantees for the output format.
*
* @throws IllegalArgumentException if the package is not found
* @throws IllegalStateException if the operation encounters an error that should never happen
* (e.g., an internal logic error).
*/
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public void dumpPackage(@NonNull PrintWriter pw,
@NonNull PackageManagerLocal.FilteredSnapshot snapshot, @NonNull String packageName) {
new DumpHelper(this).dumpPackage(
pw, snapshot, Utils.getPackageStateOrThrow(snapshot, packageName));
}
/**
* Cleans up obsolete profiles and artifacts.
*
* This is done in a mark-and-sweep approach.
*
* @return The amount of the disk space freed by the cleanup, in bytes.
* @hide
*/
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public long cleanup(@NonNull PackageManagerLocal.FilteredSnapshot snapshot) {
mInjector.getDexUseManager().cleanup();
try {
// For every primary dex container file or secondary dex container file of every app, if
// it has code, we keep the following types of files:
// - The reference profile and the current profiles, regardless of the hibernation state
// of the app.
// - The dexopt artifacts, if they are up-to-date and the app is not hibernating.
// - Only the VDEX part of the dexopt artifacts, if the dexopt artifacts are outdated
// but the VDEX part is still usable and the app is not hibernating.
List<ProfilePath> profilesToKeep = new ArrayList<>();
List<ArtifactsPath> artifactsToKeep = new ArrayList<>();
List<VdexPath> vdexFilesToKeep = new ArrayList<>();
for (PackageState pkgState : snapshot.getPackageStates().values()) {
if (!Utils.canDexoptPackage(pkgState, null /* appHibernationManager */)) {
continue;
}
AndroidPackage pkg = Utils.getPackageOrThrow(pkgState);
boolean isInDalvikCache = Utils.isInDalvikCache(pkgState, mInjector.getArtd());
boolean keepArtifacts = !Utils.shouldSkipDexoptDueToHibernation(
pkgState, mInjector.getAppHibernationManager());
for (DetailedPrimaryDexInfo dexInfo :
PrimaryDexUtils.getDetailedDexInfo(pkgState, pkg)) {
if (!dexInfo.hasCode()) {
continue;
}
profilesToKeep.add(PrimaryDexUtils.buildRefProfilePath(pkgState, dexInfo));
profilesToKeep.addAll(PrimaryDexUtils.getCurProfiles(
mInjector.getUserManager(), pkgState, dexInfo));
if (keepArtifacts) {
for (Abi abi : Utils.getAllAbis(pkgState)) {
maybeKeepArtifacts(artifactsToKeep, vdexFilesToKeep, pkgState, dexInfo,
abi, isInDalvikCache);
}
}
}
for (DetailedSecondaryDexInfo dexInfo :
mInjector.getDexUseManager().getFilteredDetailedSecondaryDexInfo(
pkgState.getPackageName())) {
profilesToKeep.add(
AidlUtils.buildProfilePathForSecondaryRef(dexInfo.dexPath()));
profilesToKeep.add(
AidlUtils.buildProfilePathForSecondaryCur(dexInfo.dexPath()));
if (keepArtifacts) {
for (Abi abi : Utils.getAllAbisForNames(dexInfo.abiNames(), pkgState)) {
maybeKeepArtifacts(artifactsToKeep, vdexFilesToKeep, pkgState, dexInfo,
abi, false /* isInDalvikCache */);
}
}
}
}
return mInjector.getArtd().cleanup(profilesToKeep, artifactsToKeep, vdexFilesToKeep);
} catch (RemoteException e) {
Utils.logArtdException(e);
return 0;
}
}
/**
* Checks if the artifacts are up-to-date, and maybe adds them to {@code artifactsToKeep} or
* {@code vdexFilesToKeep} based on the result.
*/
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
private void maybeKeepArtifacts(@NonNull List<ArtifactsPath> artifactsToKeep,
@NonNull List<VdexPath> vdexFilesToKeep, @NonNull PackageState pkgState,
@NonNull DetailedDexInfo dexInfo, @NonNull Abi abi, boolean isInDalvikCache)
throws RemoteException {
try {
GetDexoptStatusResult result = mInjector.getArtd().getDexoptStatus(
dexInfo.dexPath(), abi.isa(), dexInfo.classLoaderContext());
if (DexFile.isValidCompilerFilter(result.compilerFilter)) {
// TODO(b/263579377): This is a bit inaccurate. We may be keeping the artifacts in
// dalvik-cache while OatFileAssistant actually picks the ones not in dalvik-cache.
// However, this isn't a big problem because it is an edge case and it only causes
// us to delete less rather than deleting more.
ArtifactsPath artifacts =
AidlUtils.buildArtifactsPath(dexInfo.dexPath(), abi.isa(), isInDalvikCache);
if (result.compilationReason.equals(ArtConstants.REASON_VDEX)) {
// Only the VDEX file is usable.
vdexFilesToKeep.add(VdexPath.artifactsPath(artifacts));
} else {
artifactsToKeep.add(artifacts);
}
}
} catch (ServiceSpecificException e) {
// Don't add the artifacts to the lists. They should be cleaned up.
Log.e(TAG,
String.format("Failed to get dexopt status [packageName = %s, dexPath = %s, "
+ "isa = %s, classLoaderContext = %s]",
pkgState.getPackageName(), dexInfo.dexPath(), abi.isa(),
dexInfo.classLoaderContext()),
e);
}
}
/**
* Should be used by {@link BackgroundDexoptJobService} ONLY.
*
* @hide
*/
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
@NonNull
BackgroundDexoptJob getBackgroundDexoptJob() {
return mInjector.getBackgroundDexoptJob();
}
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
@Nullable
private DexoptResult maybeDowngradePackages(
@NonNull PackageManagerLocal.FilteredSnapshot snapshot,
@NonNull Set<String> excludedPackages, @NonNull CancellationSignal cancellationSignal,
@NonNull Executor executor,
@Nullable @CallbackExecutor Executor progressCallbackExecutor,
@Nullable Consumer<OperationProgress> progressCallback) {
if (shouldDowngrade()) {
List<String> packages = getDefaultPackages(snapshot, ReasonMapping.REASON_INACTIVE)
.stream()
.filter(pkg -> !excludedPackages.contains(pkg))
.collect(Collectors.toList());
if (!packages.isEmpty()) {
Log.i(TAG, "Storage is low. Downgrading " + packages.size() + " inactive packages");
DexoptParams params =
new DexoptParams.Builder(ReasonMapping.REASON_INACTIVE).build();
return mInjector.getDexoptHelper().dexopt(snapshot, packages, params,
cancellationSignal, executor, progressCallbackExecutor, progressCallback);
} else {
Log.i(TAG,
"Storage is low, but downgrading is disabled or there's nothing to "
+ "downgrade");
}
}
return null;
}
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
private boolean shouldDowngrade() {
try {
return mInjector.getStorageManager().getAllocatableBytes(StorageManager.UUID_DEFAULT)
< DOWNGRADE_THRESHOLD_ABOVE_LOW_BYTES;
} catch (IOException e) {
Log.e(TAG, "Failed to check storage. Assuming storage not low", e);
return false;
}
}
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
@Nullable
private DexoptResult maybeDexoptPackagesSupplementaryPass(
@NonNull PackageManagerLocal.FilteredSnapshot snapshot,
@NonNull DexoptResult mainResult, @NonNull DexoptParams mainParams,
@NonNull CancellationSignal cancellationSignal, @NonNull Executor dexoptExecutor,
@Nullable @CallbackExecutor Executor progressCallbackExecutor,
@Nullable Consumer<OperationProgress> progressCallback) {
if ((mainParams.getFlags() & ArtFlags.FLAG_FORCE_MERGE_PROFILE) != 0) {
return null;
}
// Only pick packages that used a profile-guided filter and were skipped in the main pass.
// This is a very coarse filter to reduce unnecessary iterations on a best-effort basis.
// Packages included in the list may still be skipped by dexopter if the profiles don't have
// any change.
List<String> packageNames =
mainResult.getPackageDexoptResults()
.stream()
.filter(packageResult
-> packageResult.getDexContainerFileDexoptResults()
.stream()
.anyMatch(fileResult
-> DexFile.isProfileGuidedCompilerFilter(
fileResult.getActualCompilerFilter())
&& fileResult.getStatus()
== DexoptResult.DEXOPT_SKIPPED))
.map(packageResult -> packageResult.getPackageName())
.collect(Collectors.toList());
DexoptParams dexoptParams = mainParams.toBuilder()
.setFlags(ArtFlags.FLAG_FORCE_MERGE_PROFILE,
ArtFlags.FLAG_FORCE_MERGE_PROFILE)
.build();
Log.i(TAG,
"Dexopting " + packageNames.size() + " packages with reason="
+ dexoptParams.getReason() + " (supplementary pass)");
return mInjector.getDexoptHelper().dexopt(snapshot, packageNames, dexoptParams,
cancellationSignal, dexoptExecutor, progressCallbackExecutor, progressCallback);
}
/** Returns the list of packages to process for the given reason. */
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
@NonNull
private List<String> getDefaultPackages(@NonNull PackageManagerLocal.FilteredSnapshot snapshot,
@NonNull /* @BatchDexoptReason|REASON_INACTIVE */ String reason) {
var appHibernationManager = mInjector.getAppHibernationManager();
// Filter out hibernating packages even if the reason is REASON_INACTIVE. This is because
// artifacts for hibernating packages are already deleted.
Stream<PackageState> packages = snapshot.getPackageStates().values().stream().filter(
pkgState -> Utils.canDexoptPackage(pkgState, appHibernationManager));
switch (reason) {
case ReasonMapping.REASON_BOOT_AFTER_MAINLINE_UPDATE:
packages = packages.filter(pkgState
-> mInjector.isSystemUiPackage(pkgState.getPackageName())
|| mInjector.isLauncherPackage(pkgState.getPackageName()));
break;
case ReasonMapping.REASON_INACTIVE:
packages = filterAndSortByLastActiveTime(
packages, false /* keepRecent */, false /* descending */);
break;
default:
// Actually, the sorting is only needed for background dexopt, but we do it for all
// cases for simplicity.
packages = filterAndSortByLastActiveTime(
packages, true /* keepRecent */, true /* descending */);
}
return packages.map(PackageState::getPackageName).collect(Collectors.toList());
}
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
@NonNull
private Stream<PackageState> filterAndSortByLastActiveTime(
@NonNull Stream<PackageState> packages, boolean keepRecent, boolean descending) {
// "pm.dexopt.downgrade_after_inactive_days" is repurposed to also determine whether to
// dexopt a package.
long inactiveMs = TimeUnit.DAYS.toMillis(SystemProperties.getInt(
"pm.dexopt.downgrade_after_inactive_days", Integer.MAX_VALUE /* def */));
long currentTimeMs = mInjector.getCurrentTimeMillis();
long thresholdTimeMs = currentTimeMs - inactiveMs;
return packages
.map(pkgState
-> Pair.create(pkgState,
Utils.getPackageLastActiveTime(pkgState,
mInjector.getDexUseManager(), mInjector.getUserManager())))
.filter(keepRecent ? (pair -> pair.second > thresholdTimeMs)
: (pair -> pair.second <= thresholdTimeMs))
.sorted(descending ? Comparator.comparingLong(pair -> - pair.second)
: Comparator.comparingLong(pair -> pair.second))
.map(pair -> pair.first);
}
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
@NonNull
private ParcelFileDescriptor mergeProfilesAndGetFd(@NonNull List<ProfilePath> profiles,
@NonNull OutputProfile output, @NonNull List<String> dexPaths,
@NonNull MergeProfileOptions options) throws SnapshotProfileException {
try {
boolean hasContent = false;
try {
hasContent = mInjector.getArtd().mergeProfiles(
profiles, null /* referenceProfile */, output, dexPaths, options);
} catch (ServiceSpecificException e) {
throw new SnapshotProfileException(e);
}
String path;
Path emptyFile = null;
if (hasContent) {
path = output.profilePath.tmpPath;
} else {
// We cannot use /dev/null because `ParcelFileDescriptor` have an API `getStatSize`,
// which expects the file to be a regular file or a link, and apps may call that
// API.
emptyFile =
Files.createTempFile(Paths.get(mInjector.getTempDir()), "empty", ".tmp");
path = emptyFile.toString();
}
ParcelFileDescriptor fd;
try {
fd = ParcelFileDescriptor.open(new File(path), ParcelFileDescriptor.MODE_READ_ONLY);
} catch (FileNotFoundException e) {
throw new IllegalStateException(
String.format("Failed to open profile snapshot '%s'", path), e);
}
// The deletion is done on the open file so that only the FD keeps a reference to the
// file.
if (hasContent) {
mInjector.getArtd().deleteProfile(ProfilePath.tmpProfilePath(output.profilePath));
} else {
Files.delete(emptyFile);
}
return fd;
} catch (IOException | RemoteException e) {
throw new SnapshotProfileException(e);
}
}
/** @hide */
@SystemApi(client = SystemApi.Client.SYSTEM_SERVER)
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public interface BatchDexoptStartCallback {
/**
* Mutates {@code builder} to override the default params for {@link #dexoptPackages}. It
* must ignore unknown reasons because more reasons may be added in the future.
*
* This is called before the start of any automatic package dexopt (i.e., not
* including package dexopt initiated by the {@link #dexoptPackage} API call).
*
* If {@code builder.setPackages} is not called, {@code defaultPackages} will be used as the
* list of packages to dexopt.
*
* If {@code builder.setDexoptParams} is not called, the default params built from {@code
* new DexoptParams.Builder(reason)} will to used as the params for dexopting each
* package.
*
* Additionally, {@code cancellationSignal.cancel()} can be called to cancel this operation.
* If this operation is initiated by the job scheduler and the {@code reason} is {@link
* ReasonMapping#REASON_BG_DEXOPT}, the job will be retried in the next <i>maintenance
* window</i>. For information about <i>maintenance window</i>, see
* https://developer.android.com/training/monitoring-device-state/doze-standby.
*
* Changing the reason is not allowed. Doing so will result in {@link IllegalStateException}
* when {@link #dexoptPackages} is called.
*/
void onBatchDexoptStart(@NonNull PackageManagerLocal.FilteredSnapshot snapshot,
@NonNull @BatchDexoptReason String reason, @NonNull List<String> defaultPackages,
@NonNull BatchDexoptParams.Builder builder,
@NonNull CancellationSignal cancellationSignal);
}
/** @hide */
@SystemApi(client = SystemApi.Client.SYSTEM_SERVER)
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public interface ScheduleBackgroundDexoptJobCallback {
/**
* Mutates {@code builder} to override the configuration of the background dexopt job.
*
* The default configuration described in {@link
* ArtManagerLocal#scheduleBackgroundDexoptJob()} is passed to the callback as the {@code
* builder} argument.
*
* Setting {@link JobInfo.Builder#setRequiresStorageNotLow(boolean)} is not allowed. Doing
* so will result in {@link IllegalStateException} when {@link
* #scheduleBackgroundDexoptJob()} is called. ART Service has its own storage check, which
* skips package dexopt when the storage is low. The storage check is enabled by
* default for background dexopt jobs. {@link
* #setBatchDexoptStartCallback(Executor, BatchDexoptStartCallback)} can be used to disable
* the storage check by clearing the {@link ArtFlags#FLAG_SKIP_IF_STORAGE_LOW} flag.
*/
void onOverrideJobInfo(@NonNull JobInfo.Builder builder);
}
/** @hide */
@SystemApi(client = SystemApi.Client.SYSTEM_SERVER)
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public interface DexoptDoneCallback {
void onDexoptDone(@NonNull DexoptResult result);
}
/**
* Represents an error that happens when snapshotting profiles.
*
* @hide
*/
@SystemApi(client = SystemApi.Client.SYSTEM_SERVER)
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public static class SnapshotProfileException extends Exception {
/** @hide */
public SnapshotProfileException(@NonNull Throwable cause) {
super(cause);
}
/** @hide */
public SnapshotProfileException(@NonNull String message) {
super(message);
}
}
/**
* Injector pattern for testing purpose.
*
* @hide
*/
@VisibleForTesting
public static class Injector {
@Nullable private final ArtManagerLocal mArtManagerLocal;
@Nullable private final Context mContext;
@Nullable private final PackageManagerLocal mPackageManagerLocal;
@Nullable private final Config mConfig;
@Nullable private BackgroundDexoptJob mBgDexoptJob = null;
// TODO(jiakaiz): Remove @SuppressLint and check `Build.VERSION.SDK_INT >=
// Build.VERSION_CODES.UPSIDE_DOWN_CAKE` once the SDK is finalized.
@SuppressLint("NewApi")
Injector(@NonNull ArtManagerLocal artManagerLocal, @Nullable Context context) {
mArtManagerLocal = artManagerLocal;
mContext = context;
if (context != null) {
// We only need them on Android U and above, where a context is passed.
mPackageManagerLocal = Objects.requireNonNull(
LocalManagerRegistry.getManager(PackageManagerLocal.class));
mConfig = new Config();
// Call the getters for the dependencies that aren't optional, to ensure correct
// initialization order.
getDexoptHelper();
getUserManager();
getDexUseManager();
getStorageManager();
ArtModuleServiceInitializer.getArtModuleServiceManager();
} else {
mPackageManagerLocal = null;
mConfig = null;
}
}
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
@NonNull
public Context getContext() {
return Objects.requireNonNull(mContext);
}
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
@NonNull
public PackageManagerLocal getPackageManagerLocal() {
return Objects.requireNonNull(mPackageManagerLocal);
}
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
@NonNull
public IArtd getArtd() {
return Utils.getArtd();
}
/** Returns a new {@link DexoptHelper} instance. */
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
@NonNull
public DexoptHelper getDexoptHelper() {
return new DexoptHelper(getContext(), getConfig());
}
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
@NonNull
public Config getConfig() {
return mConfig;
}
/** Returns the registered {@link AppHibernationManager} instance. */
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
@NonNull
public AppHibernationManager getAppHibernationManager() {
return Objects.requireNonNull(mContext.getSystemService(AppHibernationManager.class));
}
/**
* Returns the {@link BackgroundDexoptJob} instance.
*
* @throws RuntimeException if called during boot before the job scheduler service has
* started.
*/
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
@NonNull
public synchronized BackgroundDexoptJob getBackgroundDexoptJob() {
if (mBgDexoptJob == null) {
mBgDexoptJob = new BackgroundDexoptJob(mContext, mArtManagerLocal, mConfig);
}
return mBgDexoptJob;
}
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
@NonNull
public UserManager getUserManager() {
return Objects.requireNonNull(mContext.getSystemService(UserManager.class));
}
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
@NonNull
public DexUseManagerLocal getDexUseManager() {
return Objects.requireNonNull(
LocalManagerRegistry.getManager(DexUseManagerLocal.class));
}
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public boolean isSystemUiPackage(@NonNull String packageName) {
return Utils.isSystemUiPackage(mContext, packageName);
}
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public boolean isLauncherPackage(@NonNull String packageName) {
return Utils.isLauncherPackage(mContext, packageName);
}
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public long getCurrentTimeMillis() {
return System.currentTimeMillis();
}
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
@NonNull
public StorageManager getStorageManager() {
return Objects.requireNonNull(mContext.getSystemService(StorageManager.class));
}
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
@NonNull
public String getTempDir() {
// This is a path that system_server is known to have full access to.
return "/data/system";
}
}
}