Merge changes I5b94a8f0,Ie6cdd846,Ie6b5f5e6,Ib962edf6,I9d779b2d, ...

* changes:
  Add an extra debug flag to BackgroundDexOptimizer
  Some refactoring in BackgroundDexOptService.
  Do not try to resolve realpath in DexManager.
  Log DexManager realpath errors only in debug mode.
  Record data about dex files use on disk
  Add logic for recording dex files use on disk
This commit is contained in:
Treehugger Robot
2017-01-11 02:15:28 +00:00
committed by Gerrit Code Review
8 changed files with 1604 additions and 101 deletions

View File

@@ -60,12 +60,12 @@ public abstract class AbstractStatsBase<T> {
return new AtomicFile(fname);
}
void writeNow(final T data) {
protected void writeNow(final T data) {
writeImpl(data);
mLastTimeWritten.set(SystemClock.elapsedRealtime());
}
boolean maybeWriteAsync(final T data) {
protected boolean maybeWriteAsync(final T data) {
if (SystemClock.elapsedRealtime() - mLastTimeWritten.get() < WRITE_INTERVAL_MS
&& !PackageManagerService.DEBUG_DEXOPT) {
return false;
@@ -105,7 +105,7 @@ public abstract class AbstractStatsBase<T> {
protected abstract void writeInternal(T data);
void read(T data) {
protected void read(T data) {
if (mLock) {
synchronized (data) {
synchronized (mFileLock) {

View File

@@ -42,12 +42,18 @@ import java.util.concurrent.TimeUnit;
* {@hide}
*/
public class BackgroundDexOptService extends JobService {
static final String TAG = "BackgroundDexOptService";
private static final String TAG = "BackgroundDexOptService";
static final long RETRY_LATENCY = 4 * AlarmManager.INTERVAL_HOUR;
private static final boolean DEBUG = false;
static final int JOB_IDLE_OPTIMIZE = 800;
static final int JOB_POST_BOOT_UPDATE = 801;
private static final long RETRY_LATENCY = 4 * AlarmManager.INTERVAL_HOUR;
private static final int JOB_IDLE_OPTIMIZE = 800;
private static final int JOB_POST_BOOT_UPDATE = 801;
private static final long IDLE_OPTIMIZATION_PERIOD = DEBUG
? TimeUnit.MINUTES.toMillis(1)
: TimeUnit.DAYS.toMillis(1);
private static ComponentName sDexoptServiceName = new ComponentName(
"android",
@@ -69,7 +75,7 @@ public class BackgroundDexOptService extends JobService {
*/
final AtomicBoolean mExitPostBootUpdate = new AtomicBoolean(false);
private final File dataDir = Environment.getDataDirectory();
private final File mDataDir = Environment.getDataDirectory();
public static void schedule(Context context) {
JobScheduler js = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
@@ -86,7 +92,7 @@ public class BackgroundDexOptService extends JobService {
js.schedule(new JobInfo.Builder(JOB_IDLE_OPTIMIZE, sDexoptServiceName)
.setRequiresDeviceIdle(true)
.setRequiresCharging(true)
.setPeriodic(TimeUnit.DAYS.toMillis(1))
.setPeriodic(IDLE_OPTIMIZATION_PERIOD)
.build());
if (DEBUG_DEXOPT) {
@@ -120,7 +126,7 @@ public class BackgroundDexOptService extends JobService {
private long getLowStorageThreshold() {
@SuppressWarnings("deprecation")
final long lowThreshold = StorageManager.from(this).getStorageLowBytes(dataDir);
final long lowThreshold = StorageManager.from(this).getStorageLowBytes(mDataDir);
if (lowThreshold == 0) {
Log.e(TAG, "Invalid low storage threshold");
}
@@ -134,114 +140,127 @@ public class BackgroundDexOptService extends JobService {
// This job has already been superseded. Do not start it.
return false;
}
// Load low battery threshold from the system config. This is a 0-100 integer.
final int lowBatteryThreshold = getResources().getInteger(
com.android.internal.R.integer.config_lowBatteryWarningLevel);
final long lowThreshold = getLowStorageThreshold();
mAbortPostBootUpdate.set(false);
new Thread("BackgroundDexOptService_PostBootUpdate") {
@Override
public void run() {
for (String pkg : pkgs) {
if (mAbortPostBootUpdate.get()) {
// JobScheduler requested an early abort.
return;
}
if (mExitPostBootUpdate.get()) {
// Different job, which supersedes this one, is running.
break;
}
if (getBatteryLevel() < lowBatteryThreshold) {
// Rather bail than completely drain the battery.
break;
}
long usableSpace = dataDir.getUsableSpace();
if (usableSpace < lowThreshold) {
// Rather bail than completely fill up the disk.
Log.w(TAG, "Aborting background dex opt job due to low storage: " +
usableSpace);
break;
}
postBootUpdate(jobParams, pm, pkgs);
}
if (DEBUG_DEXOPT) {
Log.i(TAG, "Updating package " + pkg);
}
}.start();
return true;
}
// Update package if needed. Note that there can be no race between concurrent
// jobs because PackageDexOptimizer.performDexOpt is synchronized.
private void postBootUpdate(JobParameters jobParams, PackageManagerService pm,
ArraySet<String> pkgs) {
// Load low battery threshold from the system config. This is a 0-100 integer.
final int lowBatteryThreshold = getResources().getInteger(
com.android.internal.R.integer.config_lowBatteryWarningLevel);
final long lowThreshold = getLowStorageThreshold();
// checkProfiles is false to avoid merging profiles during boot which
// might interfere with background compilation (b/28612421).
// Unfortunately this will also means that "pm.dexopt.boot=speed-profile" will
// behave differently than "pm.dexopt.bg-dexopt=speed-profile" but that's a
// trade-off worth doing to save boot time work.
pm.performDexOpt(pkg,
/* checkProfiles */ false,
PackageManagerService.REASON_BOOT,
/* force */ false);
}
// Ran to completion, so we abandon our timeslice and do not reschedule.
jobFinished(jobParams, /* reschedule */ false);
mAbortPostBootUpdate.set(false);
for (String pkg : pkgs) {
if (mAbortPostBootUpdate.get()) {
// JobScheduler requested an early abort.
return;
}
if (mExitPostBootUpdate.get()) {
// Different job, which supersedes this one, is running.
break;
}
if (getBatteryLevel() < lowBatteryThreshold) {
// Rather bail than completely drain the battery.
break;
}
long usableSpace = mDataDir.getUsableSpace();
if (usableSpace < lowThreshold) {
// Rather bail than completely fill up the disk.
Log.w(TAG, "Aborting background dex opt job due to low storage: " +
usableSpace);
break;
}
if (DEBUG_DEXOPT) {
Log.i(TAG, "Updating package " + pkg);
}
// Update package if needed. Note that there can be no race between concurrent
// jobs because PackageDexOptimizer.performDexOpt is synchronized.
// checkProfiles is false to avoid merging profiles during boot which
// might interfere with background compilation (b/28612421).
// Unfortunately this will also means that "pm.dexopt.boot=speed-profile" will
// behave differently than "pm.dexopt.bg-dexopt=speed-profile" but that's a
// trade-off worth doing to save boot time work.
pm.performDexOpt(pkg,
/* checkProfiles */ false,
PackageManagerService.REASON_BOOT,
/* force */ false);
}
// Ran to completion, so we abandon our timeslice and do not reschedule.
jobFinished(jobParams, /* reschedule */ false);
}
private boolean runIdleOptimization(final JobParameters jobParams,
final PackageManagerService pm, final ArraySet<String> pkgs) {
new Thread("BackgroundDexOptService_IdleOptimization") {
@Override
public void run() {
idleOptimization(jobParams, pm, pkgs);
}
}.start();
return true;
}
private boolean runIdleOptimization(final JobParameters jobParams,
final PackageManagerService pm, final ArraySet<String> pkgs) {
private void idleOptimization(JobParameters jobParams, PackageManagerService pm,
ArraySet<String> pkgs) {
Log.i(TAG, "Performing idle optimizations");
// If post-boot update is still running, request that it exits early.
mExitPostBootUpdate.set(true);
mAbortIdleOptimization.set(false);
final long lowThreshold = getLowStorageThreshold();
new Thread("BackgroundDexOptService_IdleOptimization") {
@Override
public void run() {
for (String pkg : pkgs) {
if (mAbortIdleOptimization.get()) {
// JobScheduler requested an early abort.
return;
}
if (sFailedPackageNames.contains(pkg)) {
// Skip previously failing package
continue;
}
long usableSpace = dataDir.getUsableSpace();
if (usableSpace < lowThreshold) {
// Rather bail than completely fill up the disk.
Log.w(TAG, "Aborting background dex opt job due to low storage: " +
usableSpace);
break;
}
// Conservatively add package to the list of failing ones in case performDexOpt
// never returns.
synchronized (sFailedPackageNames) {
sFailedPackageNames.add(pkg);
}
// Optimize package if needed. Note that there can be no race between
// concurrent jobs because PackageDexOptimizer.performDexOpt is synchronized.
if (pm.performDexOpt(pkg,
/* checkProfiles */ true,
PackageManagerService.REASON_BACKGROUND_DEXOPT,
/* force */ false)) {
// Dexopt succeeded, remove package from the list of failing ones.
synchronized (sFailedPackageNames) {
sFailedPackageNames.remove(pkg);
}
}
}
// Ran to completion, so we abandon our timeslice and do not reschedule.
jobFinished(jobParams, /* reschedule */ false);
for (String pkg : pkgs) {
if (mAbortIdleOptimization.get()) {
// JobScheduler requested an early abort.
return;
}
}.start();
return true;
synchronized (sFailedPackageNames) {
if (sFailedPackageNames.contains(pkg)) {
// Skip previously failing package
continue;
}
}
long usableSpace = mDataDir.getUsableSpace();
if (usableSpace < lowThreshold) {
// Rather bail than completely fill up the disk.
Log.w(TAG, "Aborting background dex opt job due to low storage: " +
usableSpace);
break;
}
// Conservatively add package to the list of failing ones in case performDexOpt
// never returns.
synchronized (sFailedPackageNames) {
sFailedPackageNames.add(pkg);
}
// Optimize package if needed. Note that there can be no race between
// concurrent jobs because PackageDexOptimizer.performDexOpt is synchronized.
if (pm.performDexOpt(pkg,
/* checkProfiles */ true,
PackageManagerService.REASON_BACKGROUND_DEXOPT,
/* force */ false)) {
// Dexopt succeeded, remove package from the list of failing ones.
synchronized (sFailedPackageNames) {
sFailedPackageNames.remove(pkg);
}
}
}
// Ran to completion, so we abandon our timeslice and do not reschedule.
jobFinished(jobParams, /* reschedule */ false);
}
@Override

View File

@@ -253,6 +253,7 @@ import com.android.server.pm.Installer.InstallerException;
import com.android.server.pm.PermissionsState.PermissionState;
import com.android.server.pm.Settings.DatabaseVersion;
import com.android.server.pm.Settings.VersionInfo;
import com.android.server.pm.dex.DexManager;
import com.android.server.storage.DeviceStorageMonitorInternal;
import dalvik.system.CloseGuard;
@@ -295,6 +296,7 @@ import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashSet;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
@@ -712,6 +714,9 @@ public class PackageManagerService extends IPackageManager.Stub {
final PackageInstallerService mInstallerService;
private final PackageDexOptimizer mPackageDexOptimizer;
// DexManager handles the usage of dex files (e.g. secondary files, whether or not a package
// is used by other apps).
private final DexManager mDexManager;
private AtomicInteger mNextMoveId = new AtomicInteger();
private final MoveCallbacks mMoveCallbacks;
@@ -2116,6 +2121,7 @@ public class PackageManagerService extends IPackageManager.Stub {
mInstaller = installer;
mPackageDexOptimizer = new PackageDexOptimizer(installer, mInstallLock, context,
"*dexopt*");
mDexManager = new DexManager();
mMoveCallbacks = new MoveCallbacks(FgThread.get().getLooper());
mOnPermissionChangeListeners = new OnPermissionChangeListeners(
@@ -2709,6 +2715,21 @@ public class PackageManagerService extends IPackageManager.Stub {
}
mEphemeralApplicationRegistry = new EphemeralApplicationRegistry(this);
// Read and update the usage of dex files.
// Do this at the end of PM init so that all the packages have their
// data directory reconciled.
// At this point we know the code paths of the packages, so we can validate
// the disk file and build the internal cache.
// The usage file is expected to be small so loading and verifying it
// should take a fairly small time compare to the other activities (e.g. package
// scanning).
final Map<Integer, List<PackageInfo>> userPackages = new HashMap<>();
final int[] currentUserIds = UserManagerService.getInstance().getUserIds();
for (int userId : currentUserIds) {
userPackages.put(userId, getInstalledPackages(/*flags*/ 0, userId).getList());
}
mDexManager.load(userPackages);
} // synchronized (mPackages)
} // synchronized (mInstallLock)
@@ -7365,7 +7386,14 @@ public class PackageManagerService extends IPackageManager.Stub {
@Override
public void notifyDexLoad(String loadingPackageName, List<String> dexPaths, String loaderIsa) {
// TODO(calin): b/32871170
int userId = UserHandle.getCallingUserId();
ApplicationInfo ai = getApplicationInfo(loadingPackageName, /*flags*/ 0, userId);
if (ai == null) {
Slog.w(TAG, "Loading a package that does not exist for the calling user. package="
+ loadingPackageName + ", user=" + userId);
return;
}
mDexManager.notifyDexLoad(ai, dexPaths, loaderIsa, userId);
}
// TODO: this is not used nor needed. Delete it.

View File

@@ -24,11 +24,13 @@ import android.app.AppGlobals;
import android.content.Intent;
import android.content.pm.PackageParser;
import android.content.pm.ResolveInfo;
import android.os.Build;
import android.os.RemoteException;
import android.os.UserHandle;
import android.system.ErrnoException;
import android.util.ArraySet;
import android.util.Log;
import dalvik.system.VMRuntime;
import libcore.io.Libcore;
import java.io.File;
@@ -197,4 +199,17 @@ public class PackageManagerServiceUtils {
}
return sb.toString();
}
/**
* Verifies that the given string {@code isa} is a valid supported isa on
* the running device.
*/
public static boolean checkISA(String isa) {
for (String abi : Build.SUPPORTED_ABIS) {
if (VMRuntime.getInstructionSet(abi).equals(isa)) {
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,329 @@
/*
* 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 android.content.pm.PackageInfo;
import android.content.pm.PackageParser;
import android.content.pm.ApplicationInfo;
import android.util.Slog;
import com.android.server.pm.PackageManagerServiceUtils;
import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
/**
* 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 = false;
// 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.
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;
// 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() {
mPackageCodeLocationsCache = new HashMap<>();
mPackageDexUsage = new PackageDexUsage();
}
/**
* 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 loadingPackage the package performing the load
* @param dexPaths the list of dex files being loaded
* @param loaderIsa the ISA of the app loading the dex files
* @param loaderUserId the user id which runs the code loading the dex files
*/
public void notifyDexLoad(ApplicationInfo loadingAppInfo, List<String> dexPaths,
String loaderIsa, int loaderUserId) {
try {
notifyDexLoadInternal(loadingAppInfo, dexPaths, loaderIsa, loaderUserId);
} catch (Exception e) {
Slog.w(TAG, "Exception while notifying dex load for package " +
loadingAppInfo.packageName, e);
}
}
private void notifyDexLoadInternal(ApplicationInfo loadingAppInfo, List<String> dexPaths,
String loaderIsa, int loaderUserId) {
if (!PackageManagerServiceUtils.checkISA(loaderIsa)) {
Slog.w(TAG, "Loading dex files " + dexPaths + " in unsupported ISA: " +
loaderIsa + "?");
return;
}
for (String dexPath : dexPaths) {
// Find the owning package name.
DexSearchResult searchResult = getDexPackage(loadingAppInfo, dexPath, loaderUserId);
if (DEBUG) {
Slog.i(TAG, loadingAppInfo.packageName
+ " 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 = !loadingAppInfo.packageName.equals(
searchResult.mOwningPackageName);
boolean primaryOrSplit = searchResult.mOutcome == DEX_SEARCH_FOUND_PRIMARY ||
searchResult.mOutcome == DEX_SEARCH_FOUND_SPLIT;
if (primaryOrSplit && !isUsedByOtherApps) {
// 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.
continue;
}
// Record dex file usage. If the current usage is a new pattern (e.g. new secondary,
// or UsedBytOtherApps), 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, isUsedByOtherApps, primaryOrSplit)) {
mPackageDexUsage.maybeWriteAsync();
}
} else {
// This can happen in a few situations:
// - bogus dex loads
// - recent installs/uninstalls that we didn't detect.
// - new installed splits
// 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.
// TODO(calin): add hooks for install/uninstall notifications to
// capture new or obsolete packages.
if (DEBUG) {
Slog.i(TAG, "Could not find owning package for dex file: " + dexPath);
}
}
}
}
/**
* 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();
Slog.w(TAG, "Exception while loading package dex usage. " +
"Starting with a fresh state.", e);
}
}
private void loadInternal(Map<Integer, List<PackageInfo>> existingPackages) {
Map<String, Set<Integer>> packageToUsersMap = 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.
PackageCodeLocations pcl = mPackageCodeLocationsCache.get(pi.packageName);
if (pcl != null) {
pcl.mergeAppDataDirs(pi.applicationInfo, userId);
} else {
mPackageCodeLocationsCache.put(pi.packageName,
new PackageCodeLocations(pi.applicationInfo, userId));
}
// Cache a map from package name to the set of user ids who installed the package.
// 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);
}
}
mPackageDexUsage.read();
mPackageDexUsage.syncData(packageToUsersMap);
}
/**
* Get the package dex usage for the given package name.
* @return the package data or null if there is no data available for this package.
*/
public PackageDexUsage.PackageUseInfo getPackageUseInfo(String packageName) {
return mPackageDexUsage.getPackageUseInfo(packageName);
}
/**
* Retrieves the package which owns the given dexPath.
*/
private DexSearchResult getDexPackage(
ApplicationInfo loadingAppInfo, String dexPath, int userId) {
// Ignore framework code.
// TODO(calin): is there a better way to detect it?
if (dexPath.startsWith("/system/framework/")) {
new DexSearchResult("framework", DEX_SEARCH_NOT_FOUND);
}
// 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.
for (PackageCodeLocations pcl : mPackageCodeLocationsCache.values()) {
outcome = pcl.searchDex(dexPath, userId);
if (outcome != DEX_SEARCH_NOT_FOUND) {
return new DexSearchResult(pcl.mPackageName, outcome);
}
}
// Cache miss. Return not found for the moment.
//
// TODO(calin): this may be because of a newly installed package, an update
// or a new added user. We can either perform a full look up again or register
// observers to be notified of package/user updates.
return new DexSearchResult(null, DEX_SEARCH_NOT_FOUND);
}
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;
}
/**
* Convenience class to store the different locations where a package might
* own code.
*/
private static class PackageCodeLocations {
private final String mPackageName;
private final 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) {
mPackageName = ai.packageName;
mBaseCodePath = ai.sourceDir;
mSplitCodePaths = new HashSet<>();
if (ai.splitSourceDirs != null) {
for (String split : ai.splitSourceDirs) {
mSplitCodePaths.add(split);
}
}
mAppDataDirs = new HashMap<>();
mergeAppDataDirs(ai, userId);
}
public void mergeAppDataDirs(ApplicationInfo ai, int userId) {
Set<String> dataDirs = putIfAbsent(mAppDataDirs, userId, new HashSet<>());
dataDirs.add(ai.dataDir);
}
public int searchDex(String dexPath, int userId) {
// First check that this package is installed or active for the given user.
// If we don't have a data dir it means this user is trying to load something
// unavailable for them.
Set<String> userDataDirs = mAppDataDirs.get(userId);
if (userDataDirs == null) {
Slog.w(TAG, "Trying to load a dex path which does not exist for the current " +
"user. dexPath=" + dexPath + ", userId=" + userId);
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;
}
}
// TODO(calin): What if we get a symlink? e.g. data dir may be a symlink,
// /data/data/ -> /data/user/0/.
if (DEBUG) {
try {
String dexPathReal = PackageManagerServiceUtils.realpath(new File(dexPath));
if (dexPathReal != dexPath) {
Slog.d(TAG, "Dex loaded with symlink. dexPath=" +
dexPath + " dexPathReal=" + dexPathReal);
}
} catch (IOException e) {
// Ignore
}
}
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;
}
}
}

View File

@@ -0,0 +1,512 @@
/*
* 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 android.util.AtomicFile;
import android.util.Slog;
import android.os.Build;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.util.FastPrintWriter;
import com.android.server.pm.AbstractStatsBase;
import com.android.server.pm.PackageManagerServiceUtils;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.InputStreamReader;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.StringWriter;
import java.io.Writer;
import java.util.Iterator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import dalvik.system.VMRuntime;
import libcore.io.IoUtils;
/**
* Stat file which store usage information about dex files.
*/
public class PackageDexUsage extends AbstractStatsBase<Void> {
private final static String TAG = "PackageDexUsage";
private final static int PACKAGE_DEX_USAGE_VERSION = 1;
private final static String PACKAGE_DEX_USAGE_VERSION_HEADER =
"PACKAGE_MANAGER__PACKAGE_DEX_USAGE__";
private final static String SPLIT_CHAR = ",";
private final static String DEX_LINE_CHAR = "#";
// Map which structures the information we have on a package.
// Maps package name to package data (which stores info about UsedByOtherApps and
// secondary dex files.).
// Access to this map needs synchronized.
@GuardedBy("mPackageUseInfoMap")
private Map<String, PackageUseInfo> mPackageUseInfoMap;
public PackageDexUsage() {
super("package-dex-usage.list", "PackageDexUsage_DiskWriter", /*lock*/ false);
mPackageUseInfoMap = new HashMap<>();
}
/**
* Record a dex file load.
*
* Note this is called when apps load dex files and as such it should return
* as fast as possible.
*
* @param loadingPackage the package performing the load
* @param dexPath the path of the dex files being loaded
* @param ownerUserId the user id which runs the code loading the dex files
* @param loaderIsa the ISA of the app loading the dex files
* @param isUsedByOtherApps whether or not this dex file was not loaded by its owning package
* @param primaryOrSplit whether or not the dex file is a primary/split dex. True indicates
* the file is either primary or a split. False indicates the file is secondary dex.
* @return true if the dex load constitutes new information, or false if this information
* has been seen before.
*/
public boolean record(String owningPackageName, String dexPath, int ownerUserId,
String loaderIsa, boolean isUsedByOtherApps, boolean primaryOrSplit) {
if (!PackageManagerServiceUtils.checkISA(loaderIsa)) {
throw new IllegalArgumentException("loaderIsa " + loaderIsa + " is unsupported");
}
synchronized (mPackageUseInfoMap) {
PackageUseInfo packageUseInfo = mPackageUseInfoMap.get(owningPackageName);
if (packageUseInfo == null) {
// This is the first time we see the package.
packageUseInfo = new PackageUseInfo();
if (primaryOrSplit) {
// If we have a primary or a split apk, set isUsedByOtherApps.
// We do not need to record the loaderIsa or the owner because we compile
// primaries for all users and all ISAs.
packageUseInfo.mIsUsedByOtherApps = isUsedByOtherApps;
} else {
// For secondary dex files record the loaderISA and the owner. We'll need
// to know under which user to compile and for what ISA.
packageUseInfo.mDexUseInfoMap.put(
dexPath, new DexUseInfo(isUsedByOtherApps, ownerUserId, loaderIsa));
}
mPackageUseInfoMap.put(owningPackageName, packageUseInfo);
return true;
} else {
// We already have data on this package. Amend it.
if (primaryOrSplit) {
// We have a possible update on the primary apk usage. Merge
// isUsedByOtherApps information and return if there was an update.
return packageUseInfo.merge(isUsedByOtherApps);
} else {
DexUseInfo newData = new DexUseInfo(
isUsedByOtherApps, ownerUserId, loaderIsa);
DexUseInfo existingData = packageUseInfo.mDexUseInfoMap.get(dexPath);
if (existingData == null) {
// It's the first time we see this dex file.
packageUseInfo.mDexUseInfoMap.put(dexPath, newData);
return true;
} else {
if (ownerUserId != existingData.mOwnerUserId) {
// Oups, this should never happen, the DexManager who calls this should
// do the proper checks and not call record if the user does not own the
// dex path.
// Secondary dex files are stored in the app user directory. A change in
// owningUser for the same path means that something went wrong at some
// higher level, and the loaderUser was allowed to cross
// user-boundaries and access data from what we know to be the owner
// user.
throw new IllegalArgumentException("Trying to change ownerUserId for "
+ " dex path " + dexPath + " from " + existingData.mOwnerUserId
+ " to " + ownerUserId);
}
// Merge the information into the existing data.
// Returns true if there was an update.
return existingData.merge(newData);
}
}
}
}
}
/**
* Convenience method for sync reads which does not force the user to pass a useless
* (Void) null.
*/
public void read() {
read((Void) null);
}
/**
* Convenience method for async writes which does not force the user to pass a useless
* (Void) null.
*/
public void maybeWriteAsync() {
maybeWriteAsync((Void) null);
}
@Override
protected void writeInternal(Void data) {
AtomicFile file = getFile();
FileOutputStream f = null;
try {
f = file.startWrite();
OutputStreamWriter osw = new OutputStreamWriter(f);
write(osw);
osw.flush();
file.finishWrite(f);
} catch (IOException e) {
if (f != null) {
file.failWrite(f);
}
Slog.e(TAG, "Failed to write usage for dex files", e);
}
}
/**
* File format:
*
* file_magic_version
* package_name_1
* #dex_file_path_1_1
* user_1_1, used_by_other_app_1_1, user_isa_1_1_1, user_isa_1_1_2
* #dex_file_path_1_2
* user_1_2, used_by_other_app_1_2, user_isa_1_2_1, user_isa_1_2_2
* ...
* package_name_2
* #dex_file_path_2_1
* user_2_1, used_by_other_app_2_1, user_isa_2_1_1, user_isa_2_1_2
* #dex_file_path_2_2,
* user_2_2, used_by_other_app_2_2, user_isa_2_2_1, user_isa_2_2_2
* ...
*/
/* package */ void write(Writer out) {
// Make a clone to avoid locking while writing to disk.
Map<String, PackageUseInfo> packageUseInfoMapClone = clonePackageUseInfoMap();
FastPrintWriter fpw = new FastPrintWriter(out);
// Write the header.
fpw.print(PACKAGE_DEX_USAGE_VERSION_HEADER);
fpw.println(PACKAGE_DEX_USAGE_VERSION);
for (Map.Entry<String, PackageUseInfo> pEntry : packageUseInfoMapClone.entrySet()) {
// Write the package line.
String packageName = pEntry.getKey();
PackageUseInfo packageUseInfo = pEntry.getValue();
fpw.println(String.join(SPLIT_CHAR, packageName,
writeBoolean(packageUseInfo.mIsUsedByOtherApps)));
// Write dex file lines.
for (Map.Entry<String, DexUseInfo> dEntry : packageUseInfo.mDexUseInfoMap.entrySet()) {
String dexPath = dEntry.getKey();
DexUseInfo dexUseInfo = dEntry.getValue();
fpw.println(DEX_LINE_CHAR + dexPath);
fpw.print(String.join(SPLIT_CHAR, Integer.toString(dexUseInfo.mOwnerUserId),
writeBoolean(dexUseInfo.mIsUsedByOtherApps)));
for (String isa : dexUseInfo.mLoaderIsas) {
fpw.print(SPLIT_CHAR + isa);
}
fpw.println();
}
}
fpw.flush();
}
@Override
protected void readInternal(Void data) {
AtomicFile file = getFile();
BufferedReader in = null;
try {
in = new BufferedReader(new InputStreamReader(file.openRead()));
read(in);
} catch (FileNotFoundException expected) {
// The file may not be there. E.g. When we first take the OTA with this feature.
} catch (IOException e) {
Slog.w(TAG, "Failed to parse package dex usage.", e);
} finally {
IoUtils.closeQuietly(in);
}
}
/* package */ void read(Reader reader) throws IOException {
Map<String, PackageUseInfo> data = new HashMap<>();
BufferedReader in = new BufferedReader(reader);
// Read header, do version check.
String versionLine = in.readLine();
if (versionLine == null) {
throw new IllegalStateException("No version line found.");
} else {
if (!versionLine.startsWith(PACKAGE_DEX_USAGE_VERSION_HEADER)) {
// TODO(calin): the caller is responsible to clear the file.
throw new IllegalStateException("Invalid version line: " + versionLine);
}
int version = Integer.parseInt(
versionLine.substring(PACKAGE_DEX_USAGE_VERSION_HEADER.length()));
if (version != PACKAGE_DEX_USAGE_VERSION) {
throw new IllegalStateException("Unexpected version: " + version);
}
}
String s = null;
String currentPakage = null;
PackageUseInfo currentPakageData = null;
Set<String> supportedIsas = new HashSet<>();
for (String abi : Build.SUPPORTED_ABIS) {
supportedIsas.add(VMRuntime.getInstructionSet(abi));
}
while ((s = in.readLine()) != null) {
if (s.startsWith(DEX_LINE_CHAR)) {
// This is the start of the the dex lines.
// We expect two lines for each dex entry:
// #dexPaths
// onwerUserId,isUsedByOtherApps,isa1,isa2
if (currentPakage == null) {
throw new IllegalStateException(
"Malformed PackageDexUsage file. Expected package line before dex line.");
}
// First line is the dex path.
String dexPath = s.substring(DEX_LINE_CHAR.length());
// Next line is the dex data.
s = in.readLine();
if (s == null) {
throw new IllegalStateException("Could not fine dexUseInfo for line: " + s);
}
// We expect at least 3 elements (isUsedByOtherApps, userId, isa).
String[] elems = s.split(SPLIT_CHAR);
if (elems.length < 3) {
throw new IllegalStateException("Invalid PackageDexUsage line: " + s);
}
int ownerUserId = Integer.parseInt(elems[0]);
boolean isUsedByOtherApps = readBoolean(elems[1]);
DexUseInfo dexUseInfo = new DexUseInfo(isUsedByOtherApps, ownerUserId);
for (int i = 2; i < elems.length; i++) {
String isa = elems[i];
if (supportedIsas.contains(isa)) {
dexUseInfo.mLoaderIsas.add(elems[i]);
} else {
// Should never happen unless someone crafts the file manually.
// In theory it could if we drop a supported ISA after an OTA but we don't
// do that.
Slog.wtf(TAG, "Unsupported ISA when parsing PackageDexUsage: " + isa);
}
}
if (supportedIsas.isEmpty()) {
Slog.wtf(TAG, "Ignore dexPath when parsing PackageDexUsage because of " +
"unsupported isas. dexPath=" + dexPath);
continue;
}
currentPakageData.mDexUseInfoMap.put(dexPath, dexUseInfo);
} else {
// This is a package line.
// We expect it to be: `packageName,isUsedByOtherApps`.
String[] elems = s.split(SPLIT_CHAR);
if (elems.length != 2) {
throw new IllegalStateException("Invalid PackageDexUsage line: " + s);
}
currentPakage = elems[0];
currentPakageData = new PackageUseInfo();
currentPakageData.mIsUsedByOtherApps = readBoolean(elems[1]);
data.put(currentPakage, currentPakageData);
}
}
synchronized (mPackageUseInfoMap) {
mPackageUseInfoMap.clear();
mPackageUseInfoMap.putAll(data);
}
}
/**
* Syncs the existing data with the set of available packages by removing obsolete entries.
*/
public void syncData(Map<String, Set<Integer>> packageToUsersMap) {
synchronized (mPackageUseInfoMap) {
Iterator<Map.Entry<String, PackageUseInfo>> pIt =
mPackageUseInfoMap.entrySet().iterator();
while (pIt.hasNext()) {
Map.Entry<String, PackageUseInfo> pEntry = pIt.next();
String packageName = pEntry.getKey();
PackageUseInfo packageUseInfo = pEntry.getValue();
Set<Integer> users = packageToUsersMap.get(packageName);
if (users == null) {
// The package doesn't exist anymore, remove the record.
pIt.remove();
} else {
// The package exists but we can prune the entries associated with non existing
// users.
Iterator<Map.Entry<String, DexUseInfo>> dIt =
packageUseInfo.mDexUseInfoMap.entrySet().iterator();
while (dIt.hasNext()) {
DexUseInfo dexUseInfo = dIt.next().getValue();
if (!users.contains(dexUseInfo.mOwnerUserId)) {
// User was probably removed. Delete its dex usage info.
dIt.remove();
}
}
if (!packageUseInfo.mIsUsedByOtherApps
&& packageUseInfo.mDexUseInfoMap.isEmpty()) {
// The package is not used by other apps and we removed all its dex files
// records. Remove the entire package record as well.
pIt.remove();
}
}
}
}
}
public PackageUseInfo getPackageUseInfo(String packageName) {
synchronized (mPackageUseInfoMap) {
return mPackageUseInfoMap.get(packageName);
}
}
public void clear() {
synchronized (mPackageUseInfoMap) {
mPackageUseInfoMap.clear();
}
}
// Creates a deep copy of the class' mPackageUseInfoMap.
private Map<String, PackageUseInfo> clonePackageUseInfoMap() {
Map<String, PackageUseInfo> clone = new HashMap<>();
synchronized (mPackageUseInfoMap) {
for (Map.Entry<String, PackageUseInfo> e : mPackageUseInfoMap.entrySet()) {
clone.put(e.getKey(), new PackageUseInfo(e.getValue()));
}
}
return clone;
}
private String writeBoolean(boolean bool) {
return bool ? "1" : "0";
}
private boolean readBoolean(String bool) {
if ("0".equals(bool)) return false;
if ("1".equals(bool)) return true;
throw new IllegalArgumentException("Unknown bool encoding: " + bool);
}
private boolean contains(int[] array, int elem) {
for (int i = 0; i < array.length; i++) {
if (elem == array[i]) {
return true;
}
}
return false;
}
public String dump() {
StringWriter sw = new StringWriter();
write(sw);
return sw.toString();
}
/**
* Stores data on how a package and its dex files are used.
*/
public static class PackageUseInfo {
// This flag is for the primary and split apks. It is set to true whenever one of them
// is loaded by another app.
private boolean mIsUsedByOtherApps;
// Map dex paths to their data (isUsedByOtherApps, owner id, loader isa).
private final Map<String, DexUseInfo> mDexUseInfoMap;
public PackageUseInfo() {
mIsUsedByOtherApps = false;
mDexUseInfoMap = new HashMap<>();
}
// Creates a deep copy of the `other`.
public PackageUseInfo(PackageUseInfo other) {
mIsUsedByOtherApps = other.mIsUsedByOtherApps;
mDexUseInfoMap = new HashMap<>();
for (Map.Entry<String, DexUseInfo> e : other.mDexUseInfoMap.entrySet()) {
mDexUseInfoMap.put(e.getKey(), new DexUseInfo(e.getValue()));
}
}
private boolean merge(boolean isUsedByOtherApps) {
boolean oldIsUsedByOtherApps = mIsUsedByOtherApps;
mIsUsedByOtherApps = mIsUsedByOtherApps || isUsedByOtherApps;
return oldIsUsedByOtherApps != this.mIsUsedByOtherApps;
}
public boolean isUsedByOtherApps() {
return mIsUsedByOtherApps;
}
public Map<String, DexUseInfo> getDexUseInfoMap() {
return mDexUseInfoMap;
}
}
/**
* Stores data about a loaded dex files.
*/
public static class DexUseInfo {
private boolean mIsUsedByOtherApps;
private final int mOwnerUserId;
private final Set<String> mLoaderIsas;
public DexUseInfo(boolean isUsedByOtherApps, int ownerUserId) {
this(isUsedByOtherApps, ownerUserId, null);
}
public DexUseInfo(boolean isUsedByOtherApps, int ownerUserId, String loaderIsa) {
mIsUsedByOtherApps = isUsedByOtherApps;
mOwnerUserId = ownerUserId;
mLoaderIsas = new HashSet<>();
if (loaderIsa != null) {
mLoaderIsas.add(loaderIsa);
}
}
// Creates a deep copy of the `other`.
public DexUseInfo(DexUseInfo other) {
mIsUsedByOtherApps = other.mIsUsedByOtherApps;
mOwnerUserId = other.mOwnerUserId;
mLoaderIsas = new HashSet<>(other.mLoaderIsas);
}
private boolean merge(DexUseInfo dexUseInfo) {
boolean oldIsUsedByOtherApps = mIsUsedByOtherApps;
mIsUsedByOtherApps = mIsUsedByOtherApps || dexUseInfo.mIsUsedByOtherApps;
boolean updateIsas = mLoaderIsas.addAll(dexUseInfo.mLoaderIsas);
return updateIsas || (oldIsUsedByOtherApps != mIsUsedByOtherApps);
}
public boolean isUsedByOtherApps() {
return mIsUsedByOtherApps;
}
public int getOwnerUserId() {
return mOwnerUserId;
}
public Set<String> getLoaderIsas() {
return mLoaderIsas;
}
}
}

View File

@@ -0,0 +1,282 @@
/*
* 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 android.os.Build;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.support.test.filters.SmallTest;
import android.support.test.runner.AndroidJUnit4;
import dalvik.system.VMRuntime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static com.android.server.pm.dex.PackageDexUsage.PackageUseInfo;
import static com.android.server.pm.dex.PackageDexUsage.DexUseInfo;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class DexManagerTests {
private DexManager mDexManager;
private TestData mFooUser0;
private TestData mBarUser0;
private TestData mBarUser1;
private TestData mInvalidIsa;
private TestData mDoesNotExist;
private int mUser0;
private int mUser1;
@Before
public void setup() {
mUser0 = 0;
mUser1 = 1;
String isa = VMRuntime.getInstructionSet(Build.SUPPORTED_ABIS[0]);
String foo = "foo";
String bar = "bar";
mFooUser0 = new TestData(foo, isa, mUser0);
mBarUser0 = new TestData(bar, isa, mUser0);
mBarUser1 = new TestData(bar, isa, mUser1);
mInvalidIsa = new TestData("INVALID", "INVALID_ISA", mUser0);
mDoesNotExist = new TestData("DOES.NOT.EXIST", isa, mUser1);
mDexManager = new DexManager();
// Foo and Bar are available to user0.
// Only Bar is available to user1;
Map<Integer, List<PackageInfo>> existingPackages = new HashMap<>();
existingPackages.put(mUser0, Arrays.asList(mFooUser0.mPackageInfo, mBarUser0.mPackageInfo));
existingPackages.put(mUser1, Arrays.asList(mBarUser1.mPackageInfo));
mDexManager.load(existingPackages);
}
@Test
public void testNotifyPrimaryUse() {
// The main dex file and splits are re-loaded by the app.
notifyDexLoad(mFooUser0, mFooUser0.getBaseAndSplitDexPaths(), mUser0);
// Package is not used by others, so we should get nothing back.
assertNull(getPackageUseInfo(mFooUser0));
}
@Test
public void testNotifyPrimaryForeignUse() {
// Foo loads Bar main apks.
notifyDexLoad(mFooUser0, mBarUser0.getBaseAndSplitDexPaths(), mUser0);
// Bar is used by others now and should be in our records
PackageUseInfo pui = getPackageUseInfo(mBarUser0);
assertNotNull(pui);
assertTrue(pui.isUsedByOtherApps());
assertTrue(pui.getDexUseInfoMap().isEmpty());
}
@Test
public void testNotifySecondary() {
// Foo loads its own secondary files.
List<String> fooSecondaries = mFooUser0.getSecondaryDexPaths();
notifyDexLoad(mFooUser0, fooSecondaries, mUser0);
PackageUseInfo pui = getPackageUseInfo(mFooUser0);
assertNotNull(pui);
assertFalse(pui.isUsedByOtherApps());
assertEquals(fooSecondaries.size(), pui.getDexUseInfoMap().size());
assertSecondaryUse(mFooUser0, pui, fooSecondaries, /*isUsedByOtherApps*/false, mUser0);
}
@Test
public void testNotifySecondaryForeign() {
// Foo loads bar secondary files.
List<String> barSecondaries = mBarUser0.getSecondaryDexPaths();
notifyDexLoad(mFooUser0, barSecondaries, mUser0);
PackageUseInfo pui = getPackageUseInfo(mBarUser0);
assertNotNull(pui);
assertFalse(pui.isUsedByOtherApps());
assertEquals(barSecondaries.size(), pui.getDexUseInfoMap().size());
assertSecondaryUse(mFooUser0, pui, barSecondaries, /*isUsedByOtherApps*/true, mUser0);
}
@Test
public void testNotifySequence() {
// Foo loads its own secondary files.
List<String> fooSecondaries = mFooUser0.getSecondaryDexPaths();
notifyDexLoad(mFooUser0, fooSecondaries, mUser0);
// Foo loads Bar own secondary files.
List<String> barSecondaries = mBarUser0.getSecondaryDexPaths();
notifyDexLoad(mFooUser0, barSecondaries, mUser0);
// Foo loads Bar primary files.
notifyDexLoad(mFooUser0, mBarUser0.getBaseAndSplitDexPaths(), mUser0);
// Bar loads its own secondary files.
notifyDexLoad(mBarUser0, barSecondaries, mUser0);
// Bar loads some own secondary files which foo didn't load.
List<String> barSecondariesForOwnUse = mBarUser0.getSecondaryDexPathsForOwnUse();
notifyDexLoad(mBarUser0, barSecondariesForOwnUse, mUser0);
// Check bar usage. Should be used by other app (for primary and barSecondaries).
PackageUseInfo pui = getPackageUseInfo(mBarUser0);
assertNotNull(pui);
assertTrue(pui.isUsedByOtherApps());
assertEquals(barSecondaries.size() + barSecondariesForOwnUse.size(),
pui.getDexUseInfoMap().size());
assertSecondaryUse(mFooUser0, pui, barSecondaries, /*isUsedByOtherApps*/true, mUser0);
assertSecondaryUse(mFooUser0, pui, barSecondariesForOwnUse,
/*isUsedByOtherApps*/false, mUser0);
// Check foo usage. Should not be used by other app.
pui = getPackageUseInfo(mFooUser0);
assertNotNull(pui);
assertFalse(pui.isUsedByOtherApps());
assertEquals(fooSecondaries.size(), pui.getDexUseInfoMap().size());
assertSecondaryUse(mFooUser0, pui, fooSecondaries, /*isUsedByOtherApps*/false, mUser0);
}
@Test
public void testPackageUseInfoNotFound() {
// Assert we don't get back data we did not previously record.
assertNull(getPackageUseInfo(mFooUser0));
}
@Test
public void testInvalidIsa() {
// Notifying with an invalid ISA should be ignored.
notifyDexLoad(mInvalidIsa, mInvalidIsa.getSecondaryDexPaths(), mUser0);
assertNull(getPackageUseInfo(mInvalidIsa));
}
@Test
public void testNotExistingPackate() {
// Notifying about the load of a package which was previously not
// register in DexManager#load should be ignored.
notifyDexLoad(mDoesNotExist, mDoesNotExist.getBaseAndSplitDexPaths(), mUser0);
assertNull(getPackageUseInfo(mDoesNotExist));
}
@Test
public void testCrossUserAttempt() {
// Bar from User1 tries to load secondary dex files from User0 Bar.
// Request should be ignored.
notifyDexLoad(mBarUser1, mBarUser0.getSecondaryDexPaths(), mUser1);
assertNull(getPackageUseInfo(mBarUser1));
}
@Test
public void testPackageNotInstalledForUser() {
// User1 tries to load Foo which is installed for User0 but not for User1.
// Note that the PackageManagerService already filters this out but we
// still check that nothing goes unexpected in DexManager.
notifyDexLoad(mBarUser0, mFooUser0.getBaseAndSplitDexPaths(), mUser1);
assertNull(getPackageUseInfo(mBarUser1));
}
private void assertSecondaryUse(TestData testData, PackageUseInfo pui,
List<String> secondaries, boolean isUsedByOtherApps, int ownerUserId) {
for (String dex : secondaries) {
DexUseInfo dui = pui.getDexUseInfoMap().get(dex);
assertNotNull(dui);
assertEquals(isUsedByOtherApps, dui.isUsedByOtherApps());
assertEquals(ownerUserId, dui.getOwnerUserId());
assertEquals(1, dui.getLoaderIsas().size());
assertTrue(dui.getLoaderIsas().contains(testData.mLoaderIsa));
}
}
private void notifyDexLoad(TestData testData, List<String> dexPaths, int loaderUserId) {
mDexManager.notifyDexLoad(testData.mPackageInfo.applicationInfo, dexPaths,
testData.mLoaderIsa, loaderUserId);
}
private PackageUseInfo getPackageUseInfo(TestData testData) {
return mDexManager.getPackageUseInfo(testData.mPackageInfo.packageName);
}
private static PackageInfo getMockPackageInfo(String packageName, int userId) {
PackageInfo pi = new PackageInfo();
pi.packageName = packageName;
pi.applicationInfo = getMockApplicationInfo(packageName, userId);
return pi;
}
private static ApplicationInfo getMockApplicationInfo(String packageName, int userId) {
ApplicationInfo ai = new ApplicationInfo();
String codeDir = "/data/app/" + packageName;
ai.setBaseCodePath(codeDir + "/base.dex");
ai.setSplitCodePaths(new String[] {codeDir + "/split-1.dex", codeDir + "/split-2.dex"});
ai.dataDir = "/data/user/" + userId + "/" + packageName;
ai.packageName = packageName;
return ai;
}
private static class TestData {
private final PackageInfo mPackageInfo;
private final String mLoaderIsa;
private TestData(String packageName, String loaderIsa, int userId) {
mPackageInfo = getMockPackageInfo(packageName, userId);
mLoaderIsa = loaderIsa;
}
private String getPackageName() {
return mPackageInfo.packageName;
}
List<String> getSecondaryDexPaths() {
List<String> paths = new ArrayList<>();
paths.add(mPackageInfo.applicationInfo.dataDir + "/secondary1.dex");
paths.add(mPackageInfo.applicationInfo.dataDir + "/secondary2.dex");
paths.add(mPackageInfo.applicationInfo.dataDir + "/secondary3.dex");
return paths;
}
List<String> getSecondaryDexPathsForOwnUse() {
List<String> paths = new ArrayList<>();
paths.add(mPackageInfo.applicationInfo.dataDir + "/secondary4.dex");
paths.add(mPackageInfo.applicationInfo.dataDir + "/secondary5.dex");
return paths;
}
List<String> getBaseAndSplitDexPaths() {
List<String> paths = new ArrayList<>();
paths.add(mPackageInfo.applicationInfo.sourceDir);
for (String split : mPackageInfo.applicationInfo.splitSourceDirs) {
paths.add(split);
}
return paths;
}
}
}

View File

@@ -0,0 +1,318 @@
/*
* 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 android.os.Build;
import android.support.test.filters.SmallTest;
import android.support.test.runner.AndroidJUnit4;
import dalvik.system.VMRuntime;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static com.android.server.pm.dex.PackageDexUsage.PackageUseInfo;
import static com.android.server.pm.dex.PackageDexUsage.DexUseInfo;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class PackageDexUsageTests {
private PackageDexUsage mPackageDexUsage;
private TestData mFooBaseUser0;
private TestData mFooSplit1User0;
private TestData mFooSplit2UsedByOtherApps0;
private TestData mFooSecondary1User0;
private TestData mFooSecondary1User1;
private TestData mFooSecondary2UsedByOtherApps0;
private TestData mInvalidIsa;
private TestData mBarBaseUser0;
private TestData mBarSecondary1User0;
private TestData mBarSecondary2User1;
@Before
public void setup() {
mPackageDexUsage = new PackageDexUsage();
String fooPackageName = "com.google.foo";
String fooCodeDir = "/data/app/com.google.foo/";
String fooDataDir = "/data/user/0/com.google.foo/";
String isa = VMRuntime.getInstructionSet(Build.SUPPORTED_ABIS[0]);
mFooBaseUser0 = new TestData(fooPackageName,
fooCodeDir + "base.apk", 0, isa, false, true);
mFooSplit1User0 = new TestData(fooPackageName,
fooCodeDir + "split-1.apk", 0, isa, false, true);
mFooSplit2UsedByOtherApps0 = new TestData(fooPackageName,
fooCodeDir + "split-2.apk", 0, isa, true, true);
mFooSecondary1User0 = new TestData(fooPackageName,
fooDataDir + "sec-1.dex", 0, isa, false, false);
mFooSecondary1User1 = new TestData(fooPackageName,
fooDataDir + "sec-1.dex", 1, isa, false, false);
mFooSecondary2UsedByOtherApps0 = new TestData(fooPackageName,
fooDataDir + "sec-2.dex", 0, isa, true, false);
mInvalidIsa = new TestData(fooPackageName,
fooCodeDir + "base.apk", 0, "INVALID_ISA", false, true);
String barPackageName = "com.google.bar";
String barCodeDir = "/data/app/com.google.bar/";
String barDataDir = "/data/user/0/com.google.bar/";
String barDataDir1 = "/data/user/1/com.google.bar/";
mBarBaseUser0 = new TestData(barPackageName,
barCodeDir + "base.apk", 0, isa, false, true);
mBarSecondary1User0 = new TestData(barPackageName,
barDataDir + "sec-1.dex", 0, isa, false, false);
mBarSecondary2User1 = new TestData(barPackageName,
barDataDir1 + "sec-2.dex", 1, isa, false, false);
}
@Test
public void testRecordPrimary() {
// Assert new information.
assertTrue(record(mFooBaseUser0));
assertPackageDexUsage(mFooBaseUser0);
writeAndReadBack();
assertPackageDexUsage(mFooBaseUser0);
}
@Test
public void testRecordSplit() {
// Assert new information.
assertTrue(record(mFooSplit1User0));
assertPackageDexUsage(mFooSplit1User0);
writeAndReadBack();
assertPackageDexUsage(mFooSplit1User0);
}
@Test
public void testRecordSplitPrimarySequence() {
// Assert new information.
assertTrue(record(mFooBaseUser0));
// Assert no new information.
assertFalse(record(mFooSplit1User0));
assertPackageDexUsage(mFooBaseUser0);
writeAndReadBack();
assertPackageDexUsage(mFooBaseUser0);
// Write Split2 which is used by other apps.
// Assert new information.
assertTrue(record(mFooSplit2UsedByOtherApps0));
assertPackageDexUsage(mFooSplit2UsedByOtherApps0);
writeAndReadBack();
assertPackageDexUsage(mFooSplit2UsedByOtherApps0);
}
@Test
public void testRecordSecondary() {
assertTrue(record(mFooSecondary1User0));
assertPackageDexUsage(null, mFooSecondary1User0);
writeAndReadBack();
assertPackageDexUsage(null, mFooSecondary1User0);
// Recording again does not add more data.
assertFalse(record(mFooSecondary1User0));
assertPackageDexUsage(null, mFooSecondary1User0);
}
@Test
public void testRecordBaseAndSecondarySequence() {
// Write split.
assertTrue(record(mFooSplit2UsedByOtherApps0));
// Write secondary.
assertTrue(record(mFooSecondary1User0));
// Check.
assertPackageDexUsage(mFooSplit2UsedByOtherApps0, mFooSecondary1User0);
writeAndReadBack();
assertPackageDexUsage(mFooSplit2UsedByOtherApps0, mFooSecondary1User0);
// Write another secondary.
assertTrue(record(mFooSecondary2UsedByOtherApps0));
// Check.
assertPackageDexUsage(
mFooSplit2UsedByOtherApps0, mFooSecondary1User0, mFooSecondary2UsedByOtherApps0);
writeAndReadBack();
assertPackageDexUsage(
mFooSplit2UsedByOtherApps0, mFooSecondary1User0, mFooSecondary2UsedByOtherApps0);
}
@Test
public void testMultiplePackages() {
assertTrue(record(mFooBaseUser0));
assertTrue(record(mFooSecondary1User0));
assertTrue(record(mFooSecondary2UsedByOtherApps0));
assertTrue(record(mBarBaseUser0));
assertTrue(record(mBarSecondary1User0));
assertTrue(record(mBarSecondary2User1));
assertPackageDexUsage(mFooBaseUser0, mFooSecondary1User0, mFooSecondary2UsedByOtherApps0);
assertPackageDexUsage(mBarBaseUser0, mBarSecondary1User0, mBarSecondary2User1);
writeAndReadBack();
assertPackageDexUsage(mFooBaseUser0, mFooSecondary1User0, mFooSecondary2UsedByOtherApps0);
assertPackageDexUsage(mBarBaseUser0, mBarSecondary1User0, mBarSecondary2User1);
}
@Test
public void testPackageNotFound() {
assertNull(mPackageDexUsage.getPackageUseInfo("missing.package"));
}
@Test
public void testAttemptToChangeOwner() {
assertTrue(record(mFooSecondary1User0));
try {
record(mFooSecondary1User1);
fail("Expected exception");
} catch (IllegalArgumentException e) {
// expected
}
}
@Test
public void testInvalidIsa() {
try {
record(mInvalidIsa);
fail("Expected exception");
} catch (IllegalArgumentException e) {
// expected
}
}
@Test
public void testReadWriteEmtpy() {
// Expect no exceptions when writing/reading without data.
writeAndReadBack();
}
@Test
public void testSyncData() {
// Write some records.
assertTrue(record(mFooBaseUser0));
assertTrue(record(mFooSecondary1User0));
assertTrue(record(mFooSecondary2UsedByOtherApps0));
assertTrue(record(mBarBaseUser0));
assertTrue(record(mBarSecondary1User0));
assertTrue(record(mBarSecondary2User1));
// Verify all is good.
assertPackageDexUsage(mFooBaseUser0, mFooSecondary1User0, mFooSecondary2UsedByOtherApps0);
assertPackageDexUsage(mBarBaseUser0, mBarSecondary1User0, mBarSecondary2User1);
writeAndReadBack();
assertPackageDexUsage(mFooBaseUser0, mFooSecondary1User0, mFooSecondary2UsedByOtherApps0);
assertPackageDexUsage(mBarBaseUser0, mBarSecondary1User0, mBarSecondary2User1);
// Simulate that only user 1 is available.
Map<String, Set<Integer>> packageToUsersMap = new HashMap<>();
packageToUsersMap.put(mBarSecondary2User1.mPackageName,
new HashSet<>(Arrays.asList(mBarSecondary2User1.mOwnerUserId)));
mPackageDexUsage.syncData(packageToUsersMap);
// Assert that only user 1 files are there.
assertPackageDexUsage(mBarBaseUser0, mBarSecondary2User1);
assertNull(mPackageDexUsage.getPackageUseInfo(mFooBaseUser0.mPackageName));
}
private void assertPackageDexUsage(TestData primary, TestData... secondaries) {
String packageName = primary == null ? secondaries[0].mPackageName : primary.mPackageName;
boolean primaryUsedByOtherApps = primary == null ? false : primary.mUsedByOtherApps;
PackageUseInfo pInfo = mPackageDexUsage.getPackageUseInfo(packageName);
// Check package use info
assertNotNull(pInfo);
assertEquals(primaryUsedByOtherApps, pInfo.isUsedByOtherApps());
Map<String, DexUseInfo> dexUseInfoMap = pInfo.getDexUseInfoMap();
assertEquals(secondaries.length, dexUseInfoMap.size());
// Check dex use info
for (TestData testData : secondaries) {
DexUseInfo dInfo = dexUseInfoMap.get(testData.mDexFile);
assertNotNull(dInfo);
assertEquals(testData.mUsedByOtherApps, dInfo.isUsedByOtherApps());
assertEquals(testData.mOwnerUserId, dInfo.getOwnerUserId());
assertEquals(1, dInfo.getLoaderIsas().size());
assertTrue(dInfo.getLoaderIsas().contains(testData.mLoaderIsa));
}
}
private boolean record(TestData testData) {
return mPackageDexUsage.record(testData.mPackageName, testData.mDexFile,
testData.mOwnerUserId, testData.mLoaderIsa, testData.mUsedByOtherApps,
testData.mPrimaryOrSplit);
}
private void writeAndReadBack() {
try {
StringWriter writer = new StringWriter();
mPackageDexUsage.write(writer);
mPackageDexUsage = new PackageDexUsage();
mPackageDexUsage.read(new StringReader(writer.toString()));
} catch (IOException e) {
fail("Unexpected IOException: " + e.getMessage());
}
}
private static class TestData {
private final String mPackageName;
private final String mDexFile;
private final int mOwnerUserId;
private final String mLoaderIsa;
private final boolean mUsedByOtherApps;
private final boolean mPrimaryOrSplit;
private TestData(String packageName, String dexFile, int ownerUserId,
String loaderIsa, boolean isUsedByOtherApps, boolean primaryOrSplit) {
mPackageName = packageName;
mDexFile = dexFile;
mOwnerUserId = ownerUserId;
mLoaderIsa = loaderIsa;
mUsedByOtherApps = isUsedByOtherApps;
mPrimaryOrSplit = primaryOrSplit;
}
}
}