Add logic for recording dex files use on disk
Add PackageDexUsage to handle the I/O operations of dex usage data. It is responsible to encode, save and load dex Test: runtest -x .../PackageDexUsageTests.java Bug: 32871170 Change-Id: I2acc5430080a7e937c798513d8959ab631decfd9
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user