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
diff --git a/services/core/java/com/android/server/pm/AbstractStatsBase.java b/services/core/java/com/android/server/pm/AbstractStatsBase.java
index 612c476..0053f58 100644
--- a/services/core/java/com/android/server/pm/AbstractStatsBase.java
+++ b/services/core/java/com/android/server/pm/AbstractStatsBase.java
@@ -60,12 +60,12 @@
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 @@
protected abstract void writeInternal(T data);
- void read(T data) {
+ protected void read(T data) {
if (mLock) {
synchronized (data) {
synchronized (mFileLock) {
diff --git a/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java b/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java
index cfd0af7..45887e1 100644
--- a/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java
+++ b/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java
@@ -24,11 +24,13 @@
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 @@
}
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;
+ }
}
diff --git a/services/core/java/com/android/server/pm/dex/PackageDexUsage.java b/services/core/java/com/android/server/pm/dex/PackageDexUsage.java
new file mode 100644
index 0000000..10384a2
--- /dev/null
+++ b/services/core/java/com/android/server/pm/dex/PackageDexUsage.java
@@ -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;
+ }
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/pm/dex/PackageDexUsageTests.java b/services/tests/servicestests/src/com/android/server/pm/dex/PackageDexUsageTests.java
new file mode 100644
index 0000000..5a42841
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/pm/dex/PackageDexUsageTests.java
@@ -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;
+ }
+
+ }
+}