/*
 * Copyright (C) 2016 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.server.pm.dex;

import static com.android.server.pm.dex.PackageDexUsage.DexUseInfo;
import static com.android.server.pm.dex.PackageDexUsage.MAX_SECONDARY_FILES_PER_OWNER;
import static com.android.server.pm.dex.PackageDexUsage.PackageUseInfo;

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 android.os.Build;
import android.platform.test.annotations.Presubmit;

import androidx.test.filters.SmallTest;
import androidx.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.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

@Presubmit
@RunWith(AndroidJUnit4.class)
@SmallTest
public class PackageDexUsageTests {
    private static final String ISA = VMRuntime.getInstructionSet(Build.SUPPORTED_ABIS[0]);

    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/";

        mFooBaseUser0 = new TestData(fooPackageName,
                fooCodeDir + "base.apk", 0, ISA, true, fooPackageName);

        mFooSplit1User0 = new TestData(fooPackageName,
                fooCodeDir + "split-1.apk", 0, ISA, true, fooPackageName);

        mFooSplit2UsedByOtherApps0 = new TestData(fooPackageName,
                fooCodeDir + "split-2.apk", 0, ISA, true, "used.by.other.com");

        mFooSecondary1User0 = new TestData(fooPackageName,
                fooDataDir + "sec-1.dex", 0, ISA, false, fooPackageName);

        mFooSecondary1User1 = new TestData(fooPackageName,
                fooDataDir + "sec-1.dex", 1, ISA, false, fooPackageName);

        mFooSecondary2UsedByOtherApps0 = new TestData(fooPackageName,
                fooDataDir + "sec-2.dex", 0, ISA, false, "used.by.other.com");

        mInvalidIsa = new TestData(fooPackageName,
                fooCodeDir + "base.apk", 0, "INVALID_ISA", true, "INALID_USER");

        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, true, barPackageName);
        mBarSecondary1User0 = new TestData(barPackageName,
                barDataDir + "sec-1.dex", 0, ISA, false, barPackageName);
        mBarSecondary2User1 = new TestData(barPackageName,
                barDataDir1 + "sec-2.dex", 1, ISA, false, barPackageName);
    }

    @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));
        assertTrue(record(mFooSplit1User0));
        // Assert no new information if we add again
        assertFalse(record(mFooBaseUser0));
        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 testRecordTooManySecondaries() {
        int tooManyFiles = MAX_SECONDARY_FILES_PER_OWNER + 1;
        List<TestData> expectedSecondaries = new ArrayList<>();
        for (int i = 1; i <= tooManyFiles; i++) {
            String fooPackageName = "com.google.foo";
            TestData testData = new TestData(fooPackageName,
                    "/data/user/0/" + fooPackageName + "/sec-" + i + "1.dex", 0, ISA, false,
                    fooPackageName);
            if (i < tooManyFiles) {
                assertTrue("Adding " + testData.mDexFile, record(testData));
                expectedSecondaries.add(testData);
            } else {
                assertFalse("Adding " + testData.mDexFile, record(testData));
            }
            assertPackageDexUsage(
                    mPackageDexUsage,
                    /* usdeBy=*/ (Set<String>) null,
                    /* primaryDex= */ null,
                    expectedSecondaries);
        }
    }

    @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)));
        Map<String, Set<String>> packageToCodePaths = new HashMap<>();
        packageToCodePaths.put(mBarBaseUser0.mPackageName,
                new HashSet<>(Arrays.asList(mBarBaseUser0.mDexFile)));
        mPackageDexUsage.syncData(packageToUsersMap, packageToCodePaths, new ArrayList<String>());

        // Assert that only user 1 files are there.
        assertPackageDexUsage(mBarBaseUser0, mBarSecondary2User1);
        assertNull(mPackageDexUsage.getPackageUseInfo(mFooBaseUser0.mPackageName));
    }

    @Test
    public void testSyncDataKeepPackages() {
        PackageDexUsage packageDexUsage = new PackageDexUsage();
        // Write the record we want to keep and which won't be keep by default.
        Set<String> fooUsers = new HashSet<>(Arrays.asList(
                new String[] {mFooBaseUser0.mPackageName}));
        assertTrue(record(packageDexUsage, mFooBaseUser0, fooUsers));
        // Write a record that would be kept by default.
        Set<String> barUsers = new HashSet<>(Arrays.asList(
                new String[] {"another.package", mFooBaseUser0.mPackageName}));
        assertTrue(record(packageDexUsage, mBarBaseUser0, barUsers));

        // Construct the user packages and their code paths (things that will be
        // kept by default during sync).
        Map<String, Set<Integer>> packageToUsersMap = new HashMap<>();
        packageToUsersMap.put(mBarBaseUser0.mPackageName,
                new HashSet<>(Arrays.asList(mBarBaseUser0.mOwnerUserId)));
        Map<String, Set<String>> packageToCodePaths = new HashMap<>();
        packageToCodePaths.put(mBarBaseUser0.mPackageName,
                new HashSet<>(Arrays.asList(mBarBaseUser0.mDexFile)));

        // Sync data.
        List<String> keepData = new ArrayList<String>();
        keepData.add(mFooBaseUser0.mPackageName);
        packageDexUsage.syncData(packageToUsersMap, packageToCodePaths, keepData);

        // Assert that both packages are kept
        assertPackageDexUsage(packageDexUsage, fooUsers, mFooBaseUser0);
        // "another.package" should not be in the loading packages after sync.
        Set<String> expectedBarUsers = new HashSet<>(Arrays.asList(
                new String[] {mFooBaseUser0.mPackageName}));
        assertPackageDexUsage(packageDexUsage, expectedBarUsers,
                mBarBaseUser0.updateUsedBy(mFooBaseUser0.mPackageName));
    }

    @Test
    public void testRemovePackage() {
        // Record Bar secondaries for two different users.
        assertTrue(record(mBarSecondary1User0));
        assertTrue(record(mBarSecondary2User1));

        // Remove the package.
        assertTrue(mPackageDexUsage.removePackage(mBarSecondary1User0.mPackageName));
        // Assert that we can't find the package anymore.
        assertNull(mPackageDexUsage.getPackageUseInfo(mBarSecondary1User0.mPackageName));
    }

    @Test
    public void testRemoveNonexistentPackage() {
        // Record Bar secondaries for two different users.
        assertTrue(record(mBarSecondary1User0));

        // Remove the package.
        assertTrue(mPackageDexUsage.removePackage(mBarSecondary1User0.mPackageName));
        // Remove the package again. It should return false because the package no longer
        // has a record in the use info.
        assertFalse(mPackageDexUsage.removePackage(mBarSecondary1User0.mPackageName));
    }

    @Test
    public void testRemoveUserPackage() {
        // Record Bar secondaries for two different users.
        assertTrue(record(mBarSecondary1User0));
        assertTrue(record(mBarSecondary2User1));

        // Remove user 0 files.
        assertTrue(mPackageDexUsage.removeUserPackage(mBarSecondary1User0.mPackageName,
                mBarSecondary1User0.mOwnerUserId));
        // Assert that only user 1 files are there.
        assertPackageDexUsage(null, mBarSecondary2User1);
    }

    @Test
    public void testRemoveDexFile() {
        // Record Bar secondaries for two different users.
        assertTrue(record(mBarSecondary1User0));
        assertTrue(record(mBarSecondary2User1));

        // Remove mBarSecondary1User0 file.
        assertTrue(mPackageDexUsage.removeDexFile(mBarSecondary1User0.mPackageName,
                mBarSecondary1User0.mDexFile, mBarSecondary1User0.mOwnerUserId));
        // Assert that only user 1 files are there.
        assertPackageDexUsage(null, mBarSecondary2User1);
    }

    @Test
    public void testClearUsedByOtherApps() {
        // Write a package which is used by other apps.
        assertTrue(record(mFooSplit2UsedByOtherApps0));
        assertTrue(mPackageDexUsage.clearUsedByOtherApps(mFooSplit2UsedByOtherApps0.mPackageName));

        // Check that the package is no longer used by other apps.
        TestData noLongerUsedByOtherApps = new TestData(
            mFooSplit2UsedByOtherApps0.mPackageName,
            mFooSplit2UsedByOtherApps0.mDexFile,
            mFooSplit2UsedByOtherApps0.mOwnerUserId,
            mFooSplit2UsedByOtherApps0.mLoaderIsa,
            mFooSplit2UsedByOtherApps0.mPrimaryOrSplit,
            /*usedBy=*/ null);
        assertPackageDexUsage(noLongerUsedByOtherApps);
    }

    @Test
    public void testClearUsedByOtherAppsNonexistent() {
        // Write a package which is used by other apps.
        assertTrue(record(mFooSplit2UsedByOtherApps0));
        assertTrue(mPackageDexUsage.clearUsedByOtherApps(mFooSplit2UsedByOtherApps0.mPackageName));
        // Clearing again should return false as there should be no update on the use info.
        assertFalse(mPackageDexUsage.clearUsedByOtherApps(mFooSplit2UsedByOtherApps0.mPackageName));
    }

    @Test
    public void testRecordDexFileUsers() {
        PackageDexUsage packageDexUsageRecordUsers = new PackageDexUsage();
        Set<String> users = new HashSet<>(Arrays.asList(
                new String[] {"another.package.1"}));
        Set<String> usersExtra = new HashSet<>(Arrays.asList(
                new String[] {"another.package.2", "another.package.3"}));

        assertTrue(record(packageDexUsageRecordUsers, mFooSplit2UsedByOtherApps0, users));
        assertTrue(record(packageDexUsageRecordUsers, mFooSplit2UsedByOtherApps0, usersExtra));

        assertTrue(record(packageDexUsageRecordUsers, mFooSecondary2UsedByOtherApps0, users));
        assertTrue(record(packageDexUsageRecordUsers, mFooSecondary2UsedByOtherApps0, usersExtra));

        packageDexUsageRecordUsers = writeAndReadBack(packageDexUsageRecordUsers);
        // Verify that the users were recorded.
        Set<String> userAll = new HashSet<>(users);
        userAll.addAll(usersExtra);
        assertPackageDexUsage(packageDexUsageRecordUsers, userAll, mFooSplit2UsedByOtherApps0,
                mFooSecondary2UsedByOtherApps0);
    }

    @Test
    public void testRecordDexFileUsersAndTheOwningPackage() {
        PackageDexUsage packageDexUsageRecordUsers = new PackageDexUsage();
        Set<String> users = new HashSet<>(Arrays.asList(
                new String[] {mFooSplit2UsedByOtherApps0.mPackageName}));
        Set<String> usersExtra = new HashSet<>(Arrays.asList(
                new String[] {"another.package.2", "another.package.3"}));

        assertTrue(record(packageDexUsageRecordUsers, mFooSplit2UsedByOtherApps0, users));
        assertTrue(record(packageDexUsageRecordUsers, mFooSplit2UsedByOtherApps0, usersExtra));

        packageDexUsageRecordUsers = writeAndReadBack(packageDexUsageRecordUsers);

        Set<String> expectedUsers = new HashSet<>(users);
        expectedUsers.addAll(usersExtra);
        // Verify that all loading packages were recorded.
        assertPackageDexUsage(
                packageDexUsageRecordUsers, expectedUsers, mFooSplit2UsedByOtherApps0);
    }

    @Test
    public void testRecordClassLoaderContextVariableContext() {
        // Record a secondary dex file.
        assertTrue(record(mFooSecondary1User0));
        // Now update its context.
        TestData fooSecondary1User0NewContext = mFooSecondary1User0.updateClassLoaderContext(
                "PCL[new_context.dex]");
        assertTrue(record(fooSecondary1User0NewContext));

        // Now check that the context was switch to variable.
        TestData expectedContext = mFooSecondary1User0.updateClassLoaderContext(
                PackageDexUsage.VARIABLE_CLASS_LOADER_CONTEXT);

        assertPackageDexUsage(null, expectedContext);
        writeAndReadBack();
        assertPackageDexUsage(null, expectedContext);
    }

    @Test
    public void testRecordClassLoaderContextOverwritten() {
        // Record a secondary dex file.
        assertTrue(record(mFooSecondary1User0));
        // Now update its context.
        TestData fooSecondary1User0NewContext = mFooSecondary1User0.updateClassLoaderContext(
                "PCL[new_context.dex]", true);
        assertTrue(record(fooSecondary1User0NewContext));

        // Now check that the context was overwritten.
        TestData expectedContext = mFooSecondary1User0.updateClassLoaderContext(
                "PCL[new_context.dex]", true);

        assertPackageDexUsage(null, expectedContext);
    }

    @Test
    public void testDexUsageClassLoaderContext() {
        final boolean isUsedByOtherApps = false;
        final int userId = 0;
        PackageDexUsage.DexUseInfo validContext = new DexUseInfo(isUsedByOtherApps, userId,
                "valid_context", "arm");
        assertFalse(validContext.isUnsupportedClassLoaderContext());
        assertFalse(validContext.isVariableClassLoaderContext());

        PackageDexUsage.DexUseInfo variableContext = new DexUseInfo(isUsedByOtherApps, userId,
                PackageDexUsage.VARIABLE_CLASS_LOADER_CONTEXT, "arm");
        assertFalse(variableContext.isUnsupportedClassLoaderContext());
        assertTrue(variableContext.isVariableClassLoaderContext());
    }

    @Test
    public void testRead() {
        String isa = VMRuntime.getInstructionSet(Build.SUPPORTED_ABIS[0]);
        // Equivalent to
        //   record(mFooSplit2UsedByOtherApps0);
        //   record(mFooSecondary1User0);
        //   record(mFooSecondary2UsedByOtherApps0);
        //   record(mBarBaseUser0);
        //   record(mBarSecondary1User0);
        String content = "PACKAGE_MANAGER__PACKAGE_DEX_USAGE__2\n"
                + "com.google.foo\n"
                + "+/data/app/com.google.foo/split-2.apk\n"
                + "@used.by.other.com\n"
                + "#/data/user/0/com.google.foo/sec-2.dex\n"
                + "0,1," + ISA + "\n"
                + "@used.by.other.com\n"
                + "PCL[/data/user/0/com.google.foo/sec-2.dex]\n"
                + "#/data/user/0/com.google.foo/sec-1.dex\n"
                + "0,0," + ISA + "\n"
                + "@\n"
                + "PCL[/data/user/0/com.google.foo/sec-1.dex]\n"
                + "com.google.bar\n"
                + "+/data/app/com.google.bar/base.apk\n"
                + "@com.google.bar\n"
                + "#/data/user/0/com.google.bar/sec-1.dex\n"
                + "0,0," + ISA + "\n"
                + "@\n"
                + "PCL[/data/user/0/com.google.bar/sec-1.dex]";

        PackageDexUsage packageDexUsage = new PackageDexUsage();
        try {
            packageDexUsage.read(new StringReader(content));
        } catch (IOException e) {
            fail();
        }

        // After the read we must sync the data to fill the missing information on the code paths.
        Map<String, Set<Integer>> packageToUsersMap = new HashMap<>();
        Map<String, Set<String>> packageToCodePaths = new HashMap<>();

        // Handle foo package.
        packageToUsersMap.put(
                mFooSplit2UsedByOtherApps0.mPackageName,
                new HashSet<>(Arrays.asList(mFooSplit2UsedByOtherApps0.mOwnerUserId)));
        packageToCodePaths.put(
                mFooSplit2UsedByOtherApps0.mPackageName,
                new HashSet<>(Arrays.asList(mFooSplit2UsedByOtherApps0.mDexFile,
                        mFooSplit1User0.mDexFile, mFooBaseUser0.mDexFile)));
        // Handle bar package.
        packageToUsersMap.put(
                mBarBaseUser0.mPackageName,
                new HashSet<>(Arrays.asList(mBarBaseUser0.mOwnerUserId)));
        packageToCodePaths.put(
                mBarBaseUser0.mPackageName,
                new HashSet<>(Arrays.asList(mBarBaseUser0.mDexFile)));
        // Handle the loading package.
        packageToUsersMap.put(
                mFooSplit2UsedByOtherApps0.mUsedBy,
                new HashSet<>(Arrays.asList(mFooSplit2UsedByOtherApps0.mOwnerUserId)));

        // Sync the data.
        packageDexUsage.syncData(packageToUsersMap, packageToCodePaths, new ArrayList<>());

        // Assert foo code paths.
        assertPackageDexUsage(
                packageDexUsage,
                /*nonDefaultUsers=*/ null,
                mFooSplit2UsedByOtherApps0,
                mFooSecondary2UsedByOtherApps0,
                mFooSecondary1User0);

        // Assert bar code paths.
        assertPackageDexUsage(
                packageDexUsage,
                /*nonDefaultUsers=*/ null,
                mBarBaseUser0,
                mBarSecondary1User0);
    }

    @Test
    public void testUnsupportedClassLoaderDiscardedOnRead() throws Exception {
        String content = "PACKAGE_MANAGER__PACKAGE_DEX_USAGE__2\n"
                + mBarSecondary1User0.mPackageName + "\n"
                + "#" + mBarSecondary1User0.mDexFile + "\n"
                + "0,0," + mBarSecondary1User0.mLoaderIsa + "\n"
                + "@\n"
                + "=UnsupportedClassLoaderContext=\n"

                + mFooSecondary1User0.mPackageName + "\n"
                + "#" + mFooSecondary1User0.mDexFile + "\n"
                + "0,0," + mFooSecondary1User0.mLoaderIsa + "\n"
                + "@\n"
                + mFooSecondary1User0.mClassLoaderContext + "\n";

        mPackageDexUsage.read(new StringReader(content));

        assertPackageDexUsage(mFooBaseUser0, mFooSecondary1User0);
        assertPackageDexUsage(mBarBaseUser0);
    }

    @Test
    public void testEnsureLoadingPackagesCanBeExtended() {
        String isa = VMRuntime.getInstructionSet(Build.SUPPORTED_ABIS[0]);
        String content = "PACKAGE_MANAGER__PACKAGE_DEX_USAGE__2\n"
                + "com.google.foo\n"
                + "+/data/app/com.google.foo/split-2.apk\n"
                + "@\n";
        PackageDexUsage packageDexUsage = new PackageDexUsage();
        try {
            packageDexUsage.read(new StringReader(content));
        } catch (IOException e) {
            fail();
        }
        record(packageDexUsage, mFooSplit2UsedByOtherApps0, mFooSplit2UsedByOtherApps0.getUsedBy());
    }

    private void assertPackageDexUsage(TestData primary, TestData... secondaries) {
        assertPackageDexUsage(mPackageDexUsage, null, primary, secondaries);
    }

    private void assertPackageDexUsage(PackageDexUsage packageDexUsage, Set<String> users,
            TestData primary, TestData... secondaries) {
        assertPackageDexUsage(packageDexUsage, users, primary, Arrays.asList(secondaries));
    }

    private void assertPackageDexUsage(PackageDexUsage packageDexUsage, Set<String> users,
            TestData primary, List<TestData> secondaries) {
        String packageName = primary == null
                ? secondaries.get(0).mPackageName
                : primary.mPackageName;
        boolean primaryUsedByOtherApps = primary != null && primary.isUsedByOtherApps();
        PackageUseInfo pInfo = packageDexUsage.getPackageUseInfo(packageName);

        // Check package use info
        assertNotNull(pInfo);
        if (primary != null) {
            if (users != null) {
                assertEquals(pInfo.getLoadingPackages(primary.mDexFile), users);
            } else if (pInfo.getLoadingPackages(primary.mDexFile) != null) {
                assertEquals(pInfo.getLoadingPackages(primary.mDexFile), primary.getUsedBy());
            }
            assertEquals(primaryUsedByOtherApps, pInfo.isUsedByOtherApps(primary.mDexFile));
        }

        Map<String, DexUseInfo> dexUseInfoMap = pInfo.getDexUseInfoMap();
        assertEquals(secondaries.size(), dexUseInfoMap.size());

        // Check dex use info
        for (TestData testData : secondaries) {
            DexUseInfo dInfo = dexUseInfoMap.get(testData.mDexFile);
            assertNotNull(dInfo);
            if (users != null) {
                assertEquals(testData.mDexFile, dInfo.getLoadingPackages(), users);
            } else {
                assertEquals(testData.mDexFile, dInfo.getLoadingPackages(), testData.getUsedBy());
            }
            assertEquals(testData.isUsedByOtherApps(), dInfo.isUsedByOtherApps());
            assertEquals(testData.mOwnerUserId, dInfo.getOwnerUserId());
            assertEquals(1, dInfo.getLoaderIsas().size());
            assertTrue(dInfo.getLoaderIsas().contains(testData.mLoaderIsa));

            assertEquals(testData.mClassLoaderContext, dInfo.getClassLoaderContext());
        }
    }

    private boolean record(TestData testData) {
        return mPackageDexUsage.record(testData.mPackageName, testData.mDexFile,
                testData.mOwnerUserId, testData.mLoaderIsa,
                testData.mPrimaryOrSplit, testData.mUsedBy, testData.mClassLoaderContext,
                testData.mOverwriteCLC);
    }

    private boolean record(PackageDexUsage packageDexUsage, TestData testData, Set<String> users) {
        boolean result = true;
        for (String user : users) {
            result = result && packageDexUsage.record(testData.mPackageName, testData.mDexFile,
                    testData.mOwnerUserId, testData.mLoaderIsa,
                    testData.mPrimaryOrSplit, user, testData.mClassLoaderContext,
                    testData.mOverwriteCLC);
        }
        return result;
    }

    private void writeAndReadBack() {
        mPackageDexUsage = writeAndReadBack(mPackageDexUsage);
    }

    private PackageDexUsage writeAndReadBack(PackageDexUsage packageDexUsage) {
        try {
            StringWriter writer = new StringWriter();
            packageDexUsage.write(writer);

            PackageDexUsage newPackageDexUsage = new PackageDexUsage();
            newPackageDexUsage.read(new StringReader(writer.toString()));
            return newPackageDexUsage;
        } catch (IOException e) {
            fail("Unexpected IOException: " + e.getMessage());
            return null;
        }
    }

    private static class TestData {
        private final String mPackageName;
        private final String mDexFile;
        private final int mOwnerUserId;
        private final String mLoaderIsa;
        private final boolean mPrimaryOrSplit;
        private final String mUsedBy;
        private final String mClassLoaderContext;
        private final boolean mOverwriteCLC;

        private TestData(String packageName, String dexFile, int ownerUserId,
                String loaderIsa, boolean primaryOrSplit, String usedBy) {
            this(packageName, dexFile, ownerUserId, loaderIsa, primaryOrSplit,
                    usedBy, "PCL[" + dexFile + "]", false);
        }
        private TestData(String packageName, String dexFile, int ownerUserId,
                String loaderIsa, boolean primaryOrSplit, String usedBy,
                String classLoaderContext, boolean overwriteCLC) {
            mPackageName = packageName;
            mDexFile = dexFile;
            mOwnerUserId = ownerUserId;
            mLoaderIsa = loaderIsa;
            mPrimaryOrSplit = primaryOrSplit;
            mUsedBy = usedBy;
            mClassLoaderContext = classLoaderContext;
            mOverwriteCLC = overwriteCLC;
        }

        private TestData updateClassLoaderContext(String newContext) {
            return updateClassLoaderContext(newContext, mOverwriteCLC);
        }

        private TestData updateClassLoaderContext(String newContext, boolean overwriteCLC) {
            return new TestData(mPackageName, mDexFile, mOwnerUserId, mLoaderIsa,
                    mPrimaryOrSplit, mUsedBy, newContext, overwriteCLC);
        }

        private TestData updateUsedBy(String newUsedBy) {
            return new TestData(mPackageName, mDexFile, mOwnerUserId, mLoaderIsa,
                mPrimaryOrSplit, newUsedBy, mClassLoaderContext, mOverwriteCLC);
        }

        private boolean isUsedByOtherApps() {
            return mUsedBy != null && !mPackageName.equals(mUsedBy);
        }

        private Set<String> getUsedBy() {
            Set<String> users = new HashSet<>();
            if ((mUsedBy != null) && (mPrimaryOrSplit || isUsedByOtherApps())) {
                // We do not store the loading package for secondary dex files
                // which are not used by others.
                users.add(mUsedBy);
            }
            return users;
        }
    }
}
