blob: 58204891293ca4dfdf97c63ed1703a2cec58c07e [file] [log] [blame]
/*
* Copyright (C) 2016 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.pm.dex;
import static com.android.server.pm.InstructionSets.getAppDexInstructionSets;
import static com.android.server.pm.PackageManagerService.PLATFORM_PACKAGE_NAME;
import static com.android.server.pm.dex.PackageDexUsage.DexUseInfo;
import static com.android.server.pm.dex.PackageDexUsage.PackageUseInfo;
import static java.util.function.Function.identity;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.IPackageManager;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackagePartitions;
import android.os.BatteryManager;
import android.os.FileUtils;
import android.os.PowerManager;
import android.os.RemoteException;
import android.os.SystemProperties;
import android.os.UserHandle;
import android.os.storage.StorageManager;
import android.util.Log;
import android.util.Slog;
import android.util.jar.StrictJarFile;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.pm.Installer;
import com.android.server.pm.Installer.InstallerException;
import com.android.server.pm.PackageDexOptimizer;
import com.android.server.pm.PackageManagerService;
import com.android.server.pm.PackageManagerServiceUtils;
import dalvik.system.VMRuntime;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.zip.ZipEntry;
/**
* This class keeps track of how dex files are used.
* Every time it gets a notification about a dex file being loaded it tracks
* its owning package and records it in PackageDexUsage (package-dex-usage.list).
*
* TODO(calin): Extract related dexopt functionality from PackageManagerService
* into this class.
*/
public class DexManager {
private static final String TAG = "DexManager";
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
private static final String PROPERTY_NAME_PM_DEXOPT_PRIV_APPS_OOB = "pm.dexopt.priv-apps-oob";
private static final String PROPERTY_NAME_PM_DEXOPT_PRIV_APPS_OOB_LIST =
"pm.dexopt.priv-apps-oob-list";
// System server cannot load executable code outside system partitions.
// However it can load verification data - thus we pick the "verify" compiler filter.
private static final String SYSTEM_SERVER_COMPILER_FILTER = "verify";
// The suffix we add to the package name when the loading happens in an isolated process.
// Note that the double dot creates and "invalid" package name which makes it clear that this
// is an artificially constructed name.
private static final String ISOLATED_PROCESS_PACKAGE_SUFFIX = "..isolated";
private final Context mContext;
// Maps package name to code locations.
// It caches the code locations for the installed packages. This allows for
// faster lookups (no locks) when finding what package owns the dex file.
@GuardedBy("mPackageCodeLocationsCache")
private final Map<String, PackageCodeLocations> mPackageCodeLocationsCache;
// PackageDexUsage handles the actual I/O operations. It is responsible to
// encode and save the dex usage data.
private final PackageDexUsage mPackageDexUsage;
// DynamicCodeLogger handles recording of dynamic code loading - which is similar to
// PackageDexUsage but records a different aspect of the data.
// (It additionally includes DEX files loaded with unsupported class loaders, and doesn't
// record class loaders or ISAs.)
private final DynamicCodeLogger mDynamicCodeLogger;
private final IPackageManager mPackageManager;
private final PackageDexOptimizer mPackageDexOptimizer;
private final Object mInstallLock;
@GuardedBy("mInstallLock")
private final Installer mInstaller;
private BatteryManager mBatteryManager = null;
private PowerManager mPowerManager = null;
// An integer percentage value used to determine when the device is considered to be on low
// power for compilation purposes.
private final int mCriticalBatteryLevel;
// Possible outcomes of a dex search.
private static int DEX_SEARCH_NOT_FOUND = 0; // dex file not found
private static int DEX_SEARCH_FOUND_PRIMARY = 1; // dex file is the primary/base apk
private static int DEX_SEARCH_FOUND_SPLIT = 2; // dex file is a split apk
private static int DEX_SEARCH_FOUND_SECONDARY = 3; // dex file is a secondary dex
public DexManager(Context context, IPackageManager pms, PackageDexOptimizer pdo,
Installer installer, Object installLock) {
mContext = context;
mPackageCodeLocationsCache = new HashMap<>();
mPackageDexUsage = new PackageDexUsage();
mPackageManager = pms;
mPackageDexOptimizer = pdo;
mInstaller = installer;
mInstallLock = installLock;
mDynamicCodeLogger = new DynamicCodeLogger(pms, installer);
// This is currently checked to handle tests that pass in a null context.
// TODO(b/174783329): Modify the tests to pass in a mocked Context, PowerManager,
// and BatteryManager.
if (mContext != null) {
mPowerManager = mContext.getSystemService(PowerManager.class);
if (mPowerManager == null) {
Slog.wtf(TAG, "Power Manager is unavailable at time of Dex Manager start");
}
mCriticalBatteryLevel = mContext.getResources().getInteger(
com.android.internal.R.integer.config_criticalBatteryWarningLevel);
} else {
// This value will never be used as the Battery Manager null check will fail first.
mCriticalBatteryLevel = 0;
}
}
public DynamicCodeLogger getDynamicCodeLogger() {
return mDynamicCodeLogger;
}
/**
* Notify about dex files loads.
* Note that this method is invoked when apps load dex files and it should
* return as fast as possible.
*
* @param loadingAppInfo the package performing the load
* @param classLoaderContextMap a map from file paths to dex files that have been loaded to
* the class loader context that was used to load them.
* @param loaderIsa the ISA of the app loading the dex files
* @param loaderUserId the user id which runs the code loading the dex files
* @param loaderIsIsolatedProcess whether or not the loading process is isolated.
*/
public void notifyDexLoad(ApplicationInfo loadingAppInfo,
Map<String, String> classLoaderContextMap, String loaderIsa, int loaderUserId,
boolean loaderIsIsolatedProcess) {
try {
notifyDexLoadInternal(loadingAppInfo, classLoaderContextMap, loaderIsa,
loaderUserId, loaderIsIsolatedProcess);
} catch (Exception e) {
Slog.w(TAG, "Exception while notifying dex load for package " +
loadingAppInfo.packageName, e);
}
}
@VisibleForTesting
/*package*/ void notifyDexLoadInternal(ApplicationInfo loadingAppInfo,
Map<String, String> classLoaderContextMap, String loaderIsa,
int loaderUserId, boolean loaderIsIsolatedProcess) {
if (classLoaderContextMap == null) {
return;
}
if (classLoaderContextMap.isEmpty()) {
Slog.wtf(TAG, "Bad call to notifyDexLoad: class loaders list is empty");
return;
}
if (!PackageManagerServiceUtils.checkISA(loaderIsa)) {
Slog.w(TAG, "Loading dex files " + classLoaderContextMap.keySet()
+ " in unsupported ISA: " + loaderIsa + "?");
return;
}
// If this load is coming from an isolated process we need to be able to prevent profile
// based optimizations. This is because isolated processes are sandboxed and can only read
// world readable files, so they need world readable optimization files. An
// example of such a package is webview.
//
// In order to prevent profile optimization we pretend that the load is coming from a
// different package, and so we assign a artificial name to the loading package making it
// clear that it comes from an isolated process. This blends well with the entire
// usedByOthers logic without needing to special handle isolated process in all dexopt
// layers.
String loadingPackageAmendedName = loadingAppInfo.packageName;
if (loaderIsIsolatedProcess) {
loadingPackageAmendedName += ISOLATED_PROCESS_PACKAGE_SUFFIX;
}
for (Map.Entry<String, String> mapping : classLoaderContextMap.entrySet()) {
String dexPath = mapping.getKey();
// Find the owning package name.
DexSearchResult searchResult = getDexPackage(loadingAppInfo, dexPath, loaderUserId);
if (DEBUG) {
Slog.i(TAG, loadingPackageAmendedName
+ " loads from " + searchResult + " : " + loaderUserId + " : " + dexPath);
}
if (searchResult.mOutcome != DEX_SEARCH_NOT_FOUND) {
// TODO(calin): extend isUsedByOtherApps check to detect the cases where
// different apps share the same runtime. In that case we should not mark the dex
// file as isUsedByOtherApps. Currently this is a safe approximation.
boolean isUsedByOtherApps =
!loadingPackageAmendedName.equals(searchResult.mOwningPackageName);
boolean primaryOrSplit = searchResult.mOutcome == DEX_SEARCH_FOUND_PRIMARY ||
searchResult.mOutcome == DEX_SEARCH_FOUND_SPLIT;
if (primaryOrSplit && !isUsedByOtherApps
&& !isPlatformPackage(searchResult.mOwningPackageName)) {
// If the dex file is the primary apk (or a split) and not isUsedByOtherApps
// do not record it. This case does not bring any new usable information
// and can be safely skipped.
// Note this is just an optimization that makes things easier to read in the
// package-dex-use file since we don't need to pollute it with redundant info.
// However, we always record system server packages.
continue;
}
if (!primaryOrSplit) {
// Record loading of a DEX file from an app data directory.
mDynamicCodeLogger.recordDex(loaderUserId, dexPath,
searchResult.mOwningPackageName, loadingAppInfo.packageName);
}
String classLoaderContext = mapping.getValue();
// Overwrite the class loader context for system server (instead of merging it).
// We expect system server jars to only change contexts in between OTAs and to
// otherwise be stable.
// Instead of implementing a complex clear-context logic post OTA, it is much
// simpler to always override the context for system server. This way, the context
// will always be up to date and we will avoid merging which could lead to the
// the context being marked as variable and thus making dexopt non-optimal.
boolean overwriteCLC = isPlatformPackage(searchResult.mOwningPackageName);
if (classLoaderContext != null
&& VMRuntime.isValidClassLoaderContext(classLoaderContext)) {
// Record dex file usage. If the current usage is a new pattern (e.g. new
// secondary, or UsedByOtherApps), record will return true and we trigger an
// async write to disk to make sure we don't loose the data in case of a reboot.
if (mPackageDexUsage.record(searchResult.mOwningPackageName,
dexPath, loaderUserId, loaderIsa, primaryOrSplit,
loadingPackageAmendedName, classLoaderContext, overwriteCLC)) {
mPackageDexUsage.maybeWriteAsync();
}
}
} else {
// If we can't find the owner of the dex we simply do not track it. The impact is
// that the dex file will not be considered for offline optimizations.
if (DEBUG) {
Slog.i(TAG, "Could not find owning package for dex file: " + dexPath);
}
}
}
}
/**
* Check if the dexPath belongs to system server.
* System server can load code from different location, so we cast a wide-net here, and
* assume that if the paths is on any of the registered system partitions then it can be loaded
* by system server.
*/
private boolean isSystemServerDexPathSupportedForOdex(String dexPath) {
ArrayList<PackagePartitions.SystemPartition> partitions =
PackagePartitions.getOrderedPartitions(identity());
// First check the apex partition as it's not part of the SystemPartitions.
if (dexPath.startsWith("/apex/")) {
return true;
}
for (int i = 0; i < partitions.size(); i++) {
if (partitions.get(i).containsPath(dexPath)) {
return true;
}
}
return false;
}
/**
* Read the dex usage from disk and populate the code cache locations.
* @param existingPackages a map containing information about what packages
* are available to what users. Only packages in this list will be
* recognized during notifyDexLoad().
*/
public void load(Map<Integer, List<PackageInfo>> existingPackages) {
try {
loadInternal(existingPackages);
} catch (Exception e) {
mPackageDexUsage.clear();
mDynamicCodeLogger.clear();
Slog.w(TAG, "Exception while loading. Starting with a fresh state.", e);
}
}
/**
* Notifies that a new package was installed for {@code userId}.
* {@code userId} must not be {@code UserHandle.USER_ALL}.
*
* @throws IllegalArgumentException if {@code userId} is {@code UserHandle.USER_ALL}.
*/
public void notifyPackageInstalled(PackageInfo pi, int userId) {
if (userId == UserHandle.USER_ALL) {
throw new IllegalArgumentException(
"notifyPackageInstalled called with USER_ALL");
}
cachePackageInfo(pi, userId);
}
/**
* Notifies that package {@code packageName} was updated.
* This will clear the UsedByOtherApps mark if it exists.
*/
public void notifyPackageUpdated(String packageName, String baseCodePath,
String[] splitCodePaths) {
cachePackageCodeLocation(packageName, baseCodePath, splitCodePaths, null, /*userId*/ -1);
// In case there was an update, write the package use info to disk async.
// Note that we do the writing here and not in PackageDexUsage in order to be
// consistent with other methods in DexManager (e.g. reconcileSecondaryDexFiles performs
// multiple updates in PackageDexUsage before writing it).
if (mPackageDexUsage.clearUsedByOtherApps(packageName)) {
mPackageDexUsage.maybeWriteAsync();
}
}
/**
* Notifies that the user {@code userId} data for package {@code packageName}
* was destroyed. This will remove all usage info associated with the package
* for the given user.
* {@code userId} is allowed to be {@code UserHandle.USER_ALL} in which case
* all usage information for the package will be removed.
*/
public void notifyPackageDataDestroyed(String packageName, int userId) {
// In case there was an update, write the package use info to disk async.
// Note that we do the writing here and not in the lower level classes in order to be
// consistent with other methods in DexManager (e.g. reconcileSecondaryDexFiles performs
// multiple updates in PackageDexUsage before writing it).
if (userId == UserHandle.USER_ALL) {
if (mPackageDexUsage.removePackage(packageName)) {
mPackageDexUsage.maybeWriteAsync();
}
mDynamicCodeLogger.removePackage(packageName);
} else {
if (mPackageDexUsage.removeUserPackage(packageName, userId)) {
mPackageDexUsage.maybeWriteAsync();
}
mDynamicCodeLogger.removeUserPackage(packageName, userId);
}
}
/**
* Caches the code location from the given package info.
*/
private void cachePackageInfo(PackageInfo pi, int userId) {
ApplicationInfo ai = pi.applicationInfo;
String[] dataDirs = new String[] {ai.dataDir, ai.deviceProtectedDataDir,
ai.credentialProtectedDataDir};
cachePackageCodeLocation(pi.packageName, ai.sourceDir, ai.splitSourceDirs,
dataDirs, userId);
}
private void cachePackageCodeLocation(String packageName, String baseCodePath,
String[] splitCodePaths, String[] dataDirs, int userId) {
synchronized (mPackageCodeLocationsCache) {
PackageCodeLocations pcl = putIfAbsent(mPackageCodeLocationsCache, packageName,
new PackageCodeLocations(packageName, baseCodePath, splitCodePaths));
// TODO(calin): We are forced to extend the scope of this synchronization because
// the values of the cache (PackageCodeLocations) are updated in place.
// Make PackageCodeLocations immutable to simplify the synchronization reasoning.
pcl.updateCodeLocation(baseCodePath, splitCodePaths);
if (dataDirs != null) {
for (String dataDir : dataDirs) {
// The set of data dirs includes deviceProtectedDataDir and
// credentialProtectedDataDir which might be null for shared
// libraries. Currently we don't track these but be lenient
// and check in case we ever decide to store their usage data.
if (dataDir != null) {
pcl.mergeAppDataDirs(dataDir, userId);
}
}
}
}
}
private void loadInternal(Map<Integer, List<PackageInfo>> existingPackages) {
Map<String, Set<Integer>> packageToUsersMap = new HashMap<>();
Map<String, Set<String>> packageToCodePaths = new HashMap<>();
// Cache the code locations for the installed packages. This allows for
// faster lookups (no locks) when finding what package owns the dex file.
for (Map.Entry<Integer, List<PackageInfo>> entry : existingPackages.entrySet()) {
List<PackageInfo> packageInfoList = entry.getValue();
int userId = entry.getKey();
for (PackageInfo pi : packageInfoList) {
// Cache the code locations.
cachePackageInfo(pi, userId);
// Cache two maps:
// - from package name to the set of user ids who installed the package.
// - from package name to the set of code paths.
// We will use it to sync the data and remove obsolete entries from
// mPackageDexUsage.
Set<Integer> users = putIfAbsent(
packageToUsersMap, pi.packageName, new HashSet<>());
users.add(userId);
Set<String> codePaths = putIfAbsent(
packageToCodePaths, pi.packageName, new HashSet<>());
codePaths.add(pi.applicationInfo.sourceDir);
if (pi.applicationInfo.splitSourceDirs != null) {
Collections.addAll(codePaths, pi.applicationInfo.splitSourceDirs);
}
}
}
try {
mPackageDexUsage.read();
List<String> packagesToKeepDataAbout = new ArrayList<>();
mPackageDexUsage.syncData(
packageToUsersMap, packageToCodePaths, packagesToKeepDataAbout);
} catch (Exception e) {
mPackageDexUsage.clear();
Slog.w(TAG, "Exception while loading package dex usage. "
+ "Starting with a fresh state.", e);
}
try {
mDynamicCodeLogger.readAndSync(packageToUsersMap);
} catch (Exception e) {
mDynamicCodeLogger.clear();
Slog.w(TAG, "Exception while loading package dynamic code usage. "
+ "Starting with a fresh state.", e);
}
}
/**
* Get the package dex usage for the given package name.
* If there is no usage info the method will return a default {@code PackageUseInfo} with
* no data about secondary dex files and marked as not being used by other apps.
*
* Note that no use info means the package was not used or it was used but not by other apps.
* Also, note that right now we might prune packages which are not used by other apps.
* TODO(calin): maybe we should not (prune) so we can have an accurate view when we try
* to access the package use.
*/
public PackageUseInfo getPackageUseInfoOrDefault(String packageName) {
// We do not record packages that have no secondary dex files or that are not used by other
// apps. This is an optimization to reduce the amount of data that needs to be written to
// disk (apps will not usually be shared so this trims quite a bit the number we record).
//
// To make this behaviour transparent to the callers which need use information on packages,
// DexManager will return this DEFAULT instance from
// {@link DexManager#getPackageUseInfoOrDefault}. It has no data about secondary dex files
// and is marked as not being used by other apps. This reflects the intended behaviour when
// we don't find the package in the underlying data file.
PackageUseInfo useInfo = mPackageDexUsage.getPackageUseInfo(packageName);
return useInfo == null ? new PackageUseInfo(packageName) : useInfo;
}
/**
* Return whether or not the manager has usage information on the give package.
*
* Note that no use info means the package was not used or it was used but not by other apps.
* Also, note that right now we might prune packages which are not used by other apps.
* TODO(calin): maybe we should not (prune) so we can have an accurate view when we try
* to access the package use.
*/
@VisibleForTesting
/*package*/ boolean hasInfoOnPackage(String packageName) {
return mPackageDexUsage.getPackageUseInfo(packageName) != null;
}
/**
* Perform dexopt on with the given {@code options} on the secondary dex files.
* @return true if all secondary dex files were processed successfully (compiled or skipped
* because they don't need to be compiled)..
*/
public boolean dexoptSecondaryDex(DexoptOptions options) {
if (isPlatformPackage(options.getPackageName())) {
// We could easily redirect to #dexoptSystemServer in this case. But there should be
// no-one calling this method directly for system server.
// As such we prefer to abort in this case.
Slog.wtf(TAG, "System server jars should be optimized with dexoptSystemServer");
return false;
}
PackageDexOptimizer pdo = getPackageDexOptimizer(options);
String packageName = options.getPackageName();
PackageUseInfo useInfo = getPackageUseInfoOrDefault(packageName);
if (useInfo.getDexUseInfoMap().isEmpty()) {
if (DEBUG) {
Slog.d(TAG, "No secondary dex use for package:" + packageName);
}
// Nothing to compile, return true.
return true;
}
boolean success = true;
for (Map.Entry<String, DexUseInfo> entry : useInfo.getDexUseInfoMap().entrySet()) {
String dexPath = entry.getKey();
DexUseInfo dexUseInfo = entry.getValue();
PackageInfo pkg;
try {
pkg = mPackageManager.getPackageInfo(packageName, /*flags*/0,
dexUseInfo.getOwnerUserId());
} catch (RemoteException e) {
throw new AssertionError(e);
}
// It may be that the package gets uninstalled while we try to compile its
// secondary dex files. If that's the case, just ignore.
// Note that we don't break the entire loop because the package might still be
// installed for other users.
if (pkg == null) {
Slog.d(TAG, "Could not find package when compiling secondary dex " + packageName
+ " for user " + dexUseInfo.getOwnerUserId());
mPackageDexUsage.removeUserPackage(packageName, dexUseInfo.getOwnerUserId());
continue;
}
int result = pdo.dexOptSecondaryDexPath(pkg.applicationInfo, dexPath,
dexUseInfo, options);
success = success && (result != PackageDexOptimizer.DEX_OPT_FAILED);
}
return success;
}
/**
* Performs dexopt on system server dex files.
*
* <p>Verfifies that the package name is {@link PackageManagerService#PLATFORM_PACKAGE_NAME}.
*
* @return
* <p>PackageDexOptimizer.DEX_OPT_SKIPPED if dexopt was skipped because no system server
* files were recorded or if no dexopt was needed.
* <p>PackageDexOptimizer.DEX_OPT_FAILED if any dexopt operation failed.
* <p>PackageDexOptimizer.DEX_OPT_PERFORMED if all dexopt operations succeeded.
*/
public int dexoptSystemServer(DexoptOptions options) {
if (!isPlatformPackage(options.getPackageName())) {
Slog.wtf(TAG, "Non system server package used when trying to dexopt system server:"
+ options.getPackageName());
return PackageDexOptimizer.DEX_OPT_FAILED;
}
// Override compiler filter for system server to the expected one.
//
// We could let the caller do this every time the invoke PackageManagerServer#dexopt.
// However, there are a few places were this will need to be done which creates
// redundancy and the danger of overlooking the config (and thus generating code that will
// waste storage and time).
DexoptOptions overriddenOptions = options.overrideCompilerFilter(
SYSTEM_SERVER_COMPILER_FILTER);
PackageDexOptimizer pdo = getPackageDexOptimizer(overriddenOptions);
String packageName = overriddenOptions.getPackageName();
PackageUseInfo useInfo = getPackageUseInfoOrDefault(packageName);
if (useInfo.getDexUseInfoMap().isEmpty()) {
if (DEBUG) {
Slog.d(TAG, "No dex files recorded for system server");
}
// Nothing to compile, return true.
return PackageDexOptimizer.DEX_OPT_SKIPPED;
}
boolean usageUpdated = false;
int result = PackageDexOptimizer.DEX_OPT_SKIPPED;
for (Map.Entry<String, DexUseInfo> entry : useInfo.getDexUseInfoMap().entrySet()) {
String dexPath = entry.getKey();
DexUseInfo dexUseInfo = entry.getValue();
if (!Files.exists(Paths.get(dexPath))) {
if (DEBUG) {
Slog.w(TAG, "A dex file previously loaded by System Server does not exist "
+ " anymore: " + dexPath);
}
usageUpdated = mPackageDexUsage.removeDexFile(
packageName, dexPath, dexUseInfo.getOwnerUserId()) || usageUpdated;
continue;
}
if (dexUseInfo.isUnsupportedClassLoaderContext()
|| dexUseInfo.isVariableClassLoaderContext()) {
String debugMsg = dexUseInfo.isUnsupportedClassLoaderContext()
? "unsupported"
: "variable";
Slog.w(TAG, "Skipping dexopt for system server path loaded with " + debugMsg
+ " class loader context: " + dexPath);
continue;
}
int newResult = pdo.dexoptSystemServerPath(dexPath, dexUseInfo, overriddenOptions);
// The end result is:
// - FAILED if any path failed,
// - PERFORMED if at least one path needed compilation,
// - SKIPPED when all paths are up to date
if ((result != PackageDexOptimizer.DEX_OPT_FAILED)
&& (newResult != PackageDexOptimizer.DEX_OPT_SKIPPED)) {
result = newResult;
}
}
if (usageUpdated) {
mPackageDexUsage.maybeWriteAsync();
}
return result;
}
/**
* Select the dex optimizer based on the force parameter.
* Forced compilation is done through ForcedUpdatePackageDexOptimizer which will adjust
* the necessary dexopt flags to make sure that compilation is not skipped. This avoid
* passing the force flag through the multitude of layers.
* Note: The force option is rarely used (cmdline input for testing, mostly), so it's OK to
* allocate an object here.
*/
private PackageDexOptimizer getPackageDexOptimizer(DexoptOptions options) {
return options.isForce()
? new PackageDexOptimizer.ForcedUpdatePackageDexOptimizer(mPackageDexOptimizer)
: mPackageDexOptimizer;
}
/**
* Reconcile the information we have about the secondary dex files belonging to
* {@code packagName} and the actual dex files. For all dex files that were
* deleted, update the internal records and delete any generated oat files.
*/
public void reconcileSecondaryDexFiles(String packageName) {
PackageUseInfo useInfo = getPackageUseInfoOrDefault(packageName);
if (useInfo.getDexUseInfoMap().isEmpty()) {
if (DEBUG) {
Slog.d(TAG, "No secondary dex use for package:" + packageName);
}
// Nothing to reconcile.
return;
}
boolean updated = false;
for (Map.Entry<String, DexUseInfo> entry : useInfo.getDexUseInfoMap().entrySet()) {
String dexPath = entry.getKey();
DexUseInfo dexUseInfo = entry.getValue();
PackageInfo pkg = null;
try {
// Note that we look for the package in the PackageManager just to be able
// to get back the real app uid and its storage kind. These are only used
// to perform extra validation in installd.
// TODO(calin): maybe a bit overkill.
pkg = mPackageManager.getPackageInfo(packageName, /*flags*/0,
dexUseInfo.getOwnerUserId());
} catch (RemoteException ignore) {
// Can't happen, DexManager is local.
}
if (pkg == null) {
// It may be that the package was uninstalled while we process the secondary
// dex files.
Slog.d(TAG, "Could not find package when compiling secondary dex " + packageName
+ " for user " + dexUseInfo.getOwnerUserId());
// Update the usage and continue, another user might still have the package.
updated = mPackageDexUsage.removeUserPackage(
packageName, dexUseInfo.getOwnerUserId()) || updated;
continue;
}
// Special handle system server files.
// We don't need an installd call because we have permissions to check if the file
// exists.
if (isPlatformPackage(packageName)) {
if (!Files.exists(Paths.get(dexPath))) {
if (DEBUG) {
Slog.w(TAG, "A dex file previously loaded by System Server does not exist "
+ " anymore: " + dexPath);
}
updated = mPackageDexUsage.removeUserPackage(
packageName, dexUseInfo.getOwnerUserId()) || updated;
}
continue;
}
// This is a regular application.
ApplicationInfo info = pkg.applicationInfo;
int flags = 0;
if (info.deviceProtectedDataDir != null &&
FileUtils.contains(info.deviceProtectedDataDir, dexPath)) {
flags |= StorageManager.FLAG_STORAGE_DE;
} else if (info.credentialProtectedDataDir!= null &&
FileUtils.contains(info.credentialProtectedDataDir, dexPath)) {
flags |= StorageManager.FLAG_STORAGE_CE;
} else {
Slog.e(TAG, "Could not infer CE/DE storage for path " + dexPath);
updated = mPackageDexUsage.removeDexFile(
packageName, dexPath, dexUseInfo.getOwnerUserId()) || updated;
continue;
}
boolean dexStillExists = true;
synchronized(mInstallLock) {
try {
String[] isas = dexUseInfo.getLoaderIsas().toArray(new String[0]);
dexStillExists = mInstaller.reconcileSecondaryDexFile(dexPath, packageName,
info.uid, isas, info.volumeUuid, flags);
} catch (InstallerException e) {
Slog.e(TAG, "Got InstallerException when reconciling dex " + dexPath +
" : " + e.getMessage());
}
}
if (!dexStillExists) {
updated = mPackageDexUsage.removeDexFile(
packageName, dexPath, dexUseInfo.getOwnerUserId()) || updated;
}
}
if (updated) {
mPackageDexUsage.maybeWriteAsync();
}
}
// TODO(calin): questionable API in the presence of class loaders context. Needs amends as the
// compilation happening here will use a pessimistic context.
public RegisterDexModuleResult registerDexModule(ApplicationInfo info, String dexPath,
boolean isSharedModule, int userId) {
// Find the owning package record.
DexSearchResult searchResult = getDexPackage(info, dexPath, userId);
if (searchResult.mOutcome == DEX_SEARCH_NOT_FOUND) {
return new RegisterDexModuleResult(false, "Package not found");
}
if (!info.packageName.equals(searchResult.mOwningPackageName)) {
return new RegisterDexModuleResult(false, "Dex path does not belong to package");
}
if (searchResult.mOutcome == DEX_SEARCH_FOUND_PRIMARY ||
searchResult.mOutcome == DEX_SEARCH_FOUND_SPLIT) {
return new RegisterDexModuleResult(false, "Main apks cannot be registered");
}
// We found the package. Now record the usage for all declared ISAs.
boolean update = false;
// If this is a shared module set the loading package to an arbitrary package name
// so that we can mark that module as usedByOthers.
String loadingPackage = isSharedModule ? ".shared.module" : searchResult.mOwningPackageName;
for (String isa : getAppDexInstructionSets(info.primaryCpuAbi, info.secondaryCpuAbi)) {
boolean newUpdate = mPackageDexUsage.record(searchResult.mOwningPackageName,
dexPath, userId, isa, /*primaryOrSplit*/ false,
loadingPackage,
PackageDexUsage.VARIABLE_CLASS_LOADER_CONTEXT,
/*overwriteCLC=*/ false);
update |= newUpdate;
}
if (update) {
mPackageDexUsage.maybeWriteAsync();
}
DexUseInfo dexUseInfo = mPackageDexUsage.getPackageUseInfo(searchResult.mOwningPackageName)
.getDexUseInfoMap().get(dexPath);
// Try to optimize the package according to the install reason.
DexoptOptions options = new DexoptOptions(info.packageName,
PackageManagerService.REASON_INSTALL, /*flags*/0);
int result = mPackageDexOptimizer.dexOptSecondaryDexPath(info, dexPath, dexUseInfo,
options);
// If we fail to optimize the package log an error but don't propagate the error
// back to the app. The app cannot do much about it and the background job
// will rety again when it executes.
// TODO(calin): there might be some value to return the error here but it may
// cause red herrings since that doesn't mean the app cannot use the module.
if (result != PackageDexOptimizer.DEX_OPT_FAILED) {
Slog.e(TAG, "Failed to optimize dex module " + dexPath);
}
return new RegisterDexModuleResult(true, "Dex module registered successfully");
}
/**
* Return all packages that contain records of secondary dex files.
*/
public Set<String> getAllPackagesWithSecondaryDexFiles() {
return mPackageDexUsage.getAllPackagesWithSecondaryDexFiles();
}
/**
* Retrieves the package which owns the given dexPath.
*/
private DexSearchResult getDexPackage(
ApplicationInfo loadingAppInfo, String dexPath, int userId) {
// First, check if the package which loads the dex file actually owns it.
// Most of the time this will be true and we can return early.
PackageCodeLocations loadingPackageCodeLocations =
new PackageCodeLocations(loadingAppInfo, userId);
int outcome = loadingPackageCodeLocations.searchDex(dexPath, userId);
if (outcome != DEX_SEARCH_NOT_FOUND) {
// TODO(calin): evaluate if we bother to detect symlinks at the dexPath level.
return new DexSearchResult(loadingPackageCodeLocations.mPackageName, outcome);
}
// The loadingPackage does not own the dex file.
// Perform a reverse look-up in the cache to detect if any package has ownership.
// Note that we can have false negatives if the cache falls out of date.
synchronized (mPackageCodeLocationsCache) {
for (PackageCodeLocations pcl : mPackageCodeLocationsCache.values()) {
outcome = pcl.searchDex(dexPath, userId);
if (outcome != DEX_SEARCH_NOT_FOUND) {
return new DexSearchResult(pcl.mPackageName, outcome);
}
}
}
// We could not find the owning package amongst regular apps.
// If the loading package is system server, see if the dex file resides
// on any of the potentially system server owning location and if so,
// assuming system server ownership.
//
// Note: We don't have any way to detect which code paths are actually
// owned by system server. We can only assume that such paths are on
// system partitions.
if (isPlatformPackage(loadingAppInfo.packageName)) {
if (isSystemServerDexPathSupportedForOdex(dexPath)) {
// We record system server dex files as secondary dex files.
// The reason is that we only record the class loader context for secondary dex
// files and we expect that all primary apks are loaded with an empty class loader.
// System server dex files may be loaded in non-empty class loader so we need to
// keep track of their context.
return new DexSearchResult(PLATFORM_PACKAGE_NAME, DEX_SEARCH_FOUND_SECONDARY);
} else {
Slog.wtf(TAG, "System server loads dex files outside paths supported for odex: "
+ dexPath);
}
}
if (DEBUG) {
// TODO(calin): Consider checking for /data/data symlink.
// /data/data/ symlinks /data/user/0/ and there's nothing stopping apps
// to load dex files through it.
try {
String dexPathReal = PackageManagerServiceUtils.realpath(new File(dexPath));
if (!dexPath.equals(dexPathReal)) {
Slog.d(TAG, "Dex loaded with symlink. dexPath=" +
dexPath + " dexPathReal=" + dexPathReal);
}
} catch (IOException e) {
// Ignore
}
}
// Cache miss. The cache is updated during installs and uninstalls,
// so if we get here we're pretty sure the dex path does not exist.
return new DexSearchResult(null, DEX_SEARCH_NOT_FOUND);
}
/** Returns true if this is the platform package .*/
private static boolean isPlatformPackage(String packageName) {
return PLATFORM_PACKAGE_NAME.equals(packageName);
}
private static <K,V> V putIfAbsent(Map<K,V> map, K key, V newValue) {
V existingValue = map.putIfAbsent(key, newValue);
return existingValue == null ? newValue : existingValue;
}
/**
* Writes the in-memory package dex usage to disk right away.
*/
public void writePackageDexUsageNow() {
mPackageDexUsage.writeNow();
mDynamicCodeLogger.writeNow();
}
/**
* Returns whether the given package is in the list of privilaged apps that should run out of
* box. This only makes sense if the feature is enabled. Note that when the the OOB list is
* empty, all priv apps will run in OOB mode.
*/
public static boolean isPackageSelectedToRunOob(String packageName) {
return isPackageSelectedToRunOob(Arrays.asList(packageName));
}
/**
* Returns whether any of the given packages are in the list of privilaged apps that should run
* out of box. This only makes sense if the feature is enabled. Note that when the the OOB list
* is empty, all priv apps will run in OOB mode.
*/
public static boolean isPackageSelectedToRunOob(Collection<String> packageNamesInSameProcess) {
return isPackageSelectedToRunOobInternal(
SystemProperties.getBoolean(PROPERTY_NAME_PM_DEXOPT_PRIV_APPS_OOB, false),
SystemProperties.get(PROPERTY_NAME_PM_DEXOPT_PRIV_APPS_OOB_LIST, "ALL"),
packageNamesInSameProcess);
}
@VisibleForTesting
/* package */ static boolean isPackageSelectedToRunOobInternal(boolean isEnabled,
String whitelist, Collection<String> packageNamesInSameProcess) {
if (!isEnabled) {
return false;
}
if ("ALL".equals(whitelist)) {
return true;
}
for (String oobPkgName : whitelist.split(",")) {
if (packageNamesInSameProcess.contains(oobPkgName)) {
return true;
}
}
return false;
}
/**
* Generates log if the archive located at {@code fileName} has uncompressed dex file that can
* be direclty mapped.
*/
public static boolean auditUncompressedDexInApk(String fileName) {
StrictJarFile jarFile = null;
try {
jarFile = new StrictJarFile(fileName,
false /*verify*/, false /*signatureSchemeRollbackProtectionsEnforced*/);
Iterator<ZipEntry> it = jarFile.iterator();
boolean allCorrect = true;
while (it.hasNext()) {
ZipEntry entry = it.next();
if (entry.getName().endsWith(".dex")) {
if (entry.getMethod() != ZipEntry.STORED) {
allCorrect = false;
Slog.w(TAG, "APK " + fileName + " has compressed dex code " +
entry.getName());
} else if ((entry.getDataOffset() & 0x3) != 0) {
allCorrect = false;
Slog.w(TAG, "APK " + fileName + " has unaligned dex code " +
entry.getName());
}
}
}
return allCorrect;
} catch (IOException ignore) {
Slog.wtf(TAG, "Error when parsing APK " + fileName);
return false;
} finally {
try {
if (jarFile != null) {
jarFile.close();
}
} catch (IOException ignore) {}
}
}
/**
* Translates install scenarios into compilation reasons. This process can be influenced
* by the state of the device.
*/
public int getCompilationReasonForInstallScenario(int installScenario) {
// Compute the compilation reason from the installation scenario.
boolean resourcesAreCritical = areBatteryThermalOrMemoryCritical();
switch (installScenario) {
case PackageManager.INSTALL_SCENARIO_DEFAULT: {
return PackageManagerService.REASON_INSTALL;
}
case PackageManager.INSTALL_SCENARIO_FAST: {
return PackageManagerService.REASON_INSTALL_FAST;
}
case PackageManager.INSTALL_SCENARIO_BULK: {
if (resourcesAreCritical) {
return PackageManagerService.REASON_INSTALL_BULK_DOWNGRADED;
} else {
return PackageManagerService.REASON_INSTALL_BULK;
}
}
case PackageManager.INSTALL_SCENARIO_BULK_SECONDARY: {
if (resourcesAreCritical) {
return PackageManagerService.REASON_INSTALL_BULK_SECONDARY_DOWNGRADED;
} else {
return PackageManagerService.REASON_INSTALL_BULK_SECONDARY;
}
}
default: {
throw new IllegalArgumentException("Invalid installation scenario");
}
}
}
/**
* Fetches the battery manager object and caches it if it hasn't been fetched already.
*/
private BatteryManager getBatteryManager() {
if (mBatteryManager == null && mContext != null) {
mBatteryManager = mContext.getSystemService(BatteryManager.class);
}
return mBatteryManager;
}
/**
* Returns true if the battery level, device temperature, or memory usage are considered to be
* in a critical state.
*/
private boolean areBatteryThermalOrMemoryCritical() {
BatteryManager batteryManager = getBatteryManager();
boolean isBtmCritical = (batteryManager != null
&& batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_STATUS)
== BatteryManager.BATTERY_STATUS_DISCHARGING
&& batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY)
<= mCriticalBatteryLevel)
|| (mPowerManager != null
&& mPowerManager.getCurrentThermalStatus()
>= PowerManager.THERMAL_STATUS_SEVERE);
return isBtmCritical;
}
/**
* Deletes all the optimizations files generated by ART.
* This is best effort, and the method will log but not throw errors
* for individual deletes
*
* @param packageInfo the package information.
* @return the number of freed bytes or -1 if there was an error in the process.
*/
public long deleteOptimizedFiles(ArtPackageInfo packageInfo) {
long freedBytes = 0;
boolean hadErrors = false;
for (String codePath : packageInfo.getCodePaths()) {
for (String isa : packageInfo.getInstructionSets()) {
try {
freedBytes += mInstaller.deleteOdex(codePath, isa, packageInfo.getOatDir());
} catch (InstallerException e) {
Log.e(TAG, "Failed deleting oat files for " + codePath, e);
hadErrors = true;
}
}
}
return hadErrors ? -1 : freedBytes;
}
public static class RegisterDexModuleResult {
public RegisterDexModuleResult() {
this(false, null);
}
public RegisterDexModuleResult(boolean success, String message) {
this.success = success;
this.message = message;
}
public final boolean success;
public final String message;
}
/**
* Convenience class to store the different locations where a package might
* own code.
*/
private static class PackageCodeLocations {
private final String mPackageName;
private String mBaseCodePath;
private final Set<String> mSplitCodePaths;
// Maps user id to the application private directory.
private final Map<Integer, Set<String>> mAppDataDirs;
public PackageCodeLocations(ApplicationInfo ai, int userId) {
this(ai.packageName, ai.sourceDir, ai.splitSourceDirs);
mergeAppDataDirs(ai.dataDir, userId);
}
public PackageCodeLocations(String packageName, String baseCodePath,
String[] splitCodePaths) {
mPackageName = packageName;
mSplitCodePaths = new HashSet<>();
mAppDataDirs = new HashMap<>();
updateCodeLocation(baseCodePath, splitCodePaths);
}
public void updateCodeLocation(String baseCodePath, String[] splitCodePaths) {
mBaseCodePath = baseCodePath;
mSplitCodePaths.clear();
if (splitCodePaths != null) {
for (String split : splitCodePaths) {
mSplitCodePaths.add(split);
}
}
}
public void mergeAppDataDirs(String dataDir, int userId) {
Set<String> dataDirs = putIfAbsent(mAppDataDirs, userId, new HashSet<>());
dataDirs.add(dataDir);
}
public int searchDex(String dexPath, int userId) {
// First check that this package is installed or active for the given user.
// A missing data dir means the package is not installed.
Set<String> userDataDirs = mAppDataDirs.get(userId);
if (userDataDirs == null) {
return DEX_SEARCH_NOT_FOUND;
}
if (mBaseCodePath.equals(dexPath)) {
return DEX_SEARCH_FOUND_PRIMARY;
}
if (mSplitCodePaths.contains(dexPath)) {
return DEX_SEARCH_FOUND_SPLIT;
}
for (String dataDir : userDataDirs) {
if (dexPath.startsWith(dataDir)) {
return DEX_SEARCH_FOUND_SECONDARY;
}
}
return DEX_SEARCH_NOT_FOUND;
}
}
/**
* Convenience class to store ownership search results.
*/
private class DexSearchResult {
private String mOwningPackageName;
private int mOutcome;
public DexSearchResult(String owningPackageName, int outcome) {
this.mOwningPackageName = owningPackageName;
this.mOutcome = outcome;
}
@Override
public String toString() {
return mOwningPackageName + "-" + mOutcome;
}
}
}