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;
+        }
+
+    }
+}