Support multiple app versions across system partitions

The APK with the higher version code will be preferred. This change
skips committing any packages found during scan that have a lower
version code than the package persisted in Settings.

If somehow the newer version cannot be scanned, the older version(s)
will be queued to re-scan, taking the next highest version that does
scan successfully.

As a consequence of this, PackageParser2/PackageCacher was updated
to use the first parent folder of the app being scanned as part of its
cache key. This allows multiple versions to be placed in folders/files
with the same file name, but across partitions. It only takes into
account this first path segment.

Also, a bug was fixed where PackagePartitions wasn't null checking
folders when checking contains.

Bug: 140494776

Test: manual test adding system app to multiple parititions, removing
    either/none/all and rebooting device
Test: atest com.android.server.pm.test.SystemAppScanPriorityTest
Test: atest com.android.overlaytest.remounted

Change-Id: Ie09ccf4b64a0be26d19c9034a68ca4877ca49b81
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index cdd347b..9d704e7 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -545,6 +545,7 @@
     static final int SCAN_AS_SYSTEM_EXT = 1 << 21;
     static final int SCAN_AS_ODM = 1 << 22;
     static final int SCAN_AS_APK_IN_APEX = 1 << 23;
+    static final int SCAN_EXPECTED_BETTER = 1 << 24;
 
     @IntDef(flag = true, prefix = { "SCAN_" }, value = {
             SCAN_NO_DEX,
@@ -804,11 +805,12 @@
     final SparseIntArray mIsolatedOwners = new SparseIntArray();
 
     /**
-     * Tracks new system packages [received in an OTA] that we expect to
-     * find updated user-installed versions. Keys are package name, values
-     * are package location.
+     * Tracks packages that we expect to find updated versions of on disk.
+     * Keys are package name, values are package location and package version code.
+     *
+     * @see #expectBetter(String, File, long)
      */
-    final private ArrayMap<String, File> mExpectingBetter = new ArrayMap<>();
+    private final ArrayMap<String, List<Pair<File, Long>>> mExpectingBetter = new ArrayMap<>();
 
     /**
      * Tracks existing packages prior to receiving an OTA. Keys are package name.
@@ -3349,7 +3351,7 @@
                                     + ", versionCode=" + ps.versionCode
                                     + "; scanned versionCode=" + scannedPkg.getLongVersionCode());
                             removePackageLI(scannedPkg, true);
-                            mExpectingBetter.put(ps.name, ps.getPath());
+                            expectBetter(ps.name, ps.getPath(), ps.versionCode);
                         }
 
                         continue;
@@ -3379,7 +3381,8 @@
                             // We're expecting that the system app should remain disabled, but add
                             // it to expecting better to recover in case the data version cannot
                             // be scanned.
-                            mExpectingBetter.put(disabledPs.name, disabledPs.getPath());
+                            expectBetter(disabledPs.name, disabledPs.getPath(),
+                                    disabledPs.versionCode);
                         }
                     }
                 }
@@ -3480,38 +3483,48 @@
                 for (int i = 0; i < mExpectingBetter.size(); i++) {
                     final String packageName = mExpectingBetter.keyAt(i);
                     if (!mPackages.containsKey(packageName)) {
-                        final File scanFile = mExpectingBetter.valueAt(i);
-
                         logCriticalInfo(Log.WARN, "Expected better " + packageName
                                 + " but never showed up; reverting to system");
 
-                        @ParseFlags int reparseFlags = 0;
-                        @ScanFlags int rescanFlags = 0;
-                        for (int i1 = mDirsToScanAsSystem.size() - 1; i1 >= 0; i1--) {
-                            final ScanPartition partition = mDirsToScanAsSystem.get(i1);
-                            if (partition.containsPrivApp(scanFile)) {
-                                reparseFlags = systemParseFlags;
-                                rescanFlags = systemScanFlags | SCAN_AS_PRIVILEGED
-                                        | partition.scanFlag;
-                                break;
-                            }
-                            if (partition.containsApp(scanFile)) {
-                                reparseFlags = systemParseFlags;
-                                rescanFlags = systemScanFlags | partition.scanFlag;
-                                break;
-                            }
-                        }
-                        if (rescanFlags == 0) {
-                            Slog.e(TAG, "Ignoring unexpected fallback path " + scanFile);
-                            continue;
-                        }
-                        mSettings.enableSystemPackageLPw(packageName);
+                        final List<Pair<File, Long>> scanFiles = mExpectingBetter.valueAt(i);
+                        // Sort ascending and iterate backwards to take highest version code
+                        Collections.sort(scanFiles,
+                                (first, second) -> Long.compare(first.second, second.second));
+                        for (int index = scanFiles.size() - 1; index >= 0; index--) {
+                            File scanFile = scanFiles.get(index).first;
 
-                        try {
-                            scanPackageTracedLI(scanFile, reparseFlags, rescanFlags, 0, null);
-                        } catch (PackageManagerException e) {
-                            Slog.e(TAG, "Failed to parse original system package: "
-                                    + e.getMessage());
+                            @ParseFlags int reparseFlags = 0;
+                            @ScanFlags int rescanFlags = 0;
+                            for (int i1 = mDirsToScanAsSystem.size() - 1; i1 >= 0; i1--) {
+                                final ScanPartition partition = mDirsToScanAsSystem.get(i1);
+                                if (partition.containsPrivApp(scanFile)) {
+                                    reparseFlags = systemParseFlags;
+                                    rescanFlags = systemScanFlags | SCAN_AS_PRIVILEGED
+                                            | partition.scanFlag;
+                                    break;
+                                }
+                                if (partition.containsApp(scanFile)) {
+                                    reparseFlags = systemParseFlags;
+                                    rescanFlags = systemScanFlags | partition.scanFlag;
+                                    break;
+                                }
+                            }
+                            if (rescanFlags == 0) {
+                                Slog.e(TAG, "Ignoring unexpected fallback path " + scanFile);
+                                continue;
+                            }
+                            mSettings.enableSystemPackageLPw(packageName);
+
+                            rescanFlags |= SCAN_EXPECTED_BETTER;
+
+                            try {
+                                scanPackageTracedLI(scanFile, reparseFlags, rescanFlags, 0, null);
+                                // Take first success and break out of for loop
+                                break;
+                            } catch (PackageManagerException e) {
+                                Slog.e(TAG, "Failed to parse original system package: "
+                                        + e.getMessage());
+                            }
                         }
                     }
                 }
@@ -3901,6 +3914,33 @@
     }
 
     /**
+     * Mark a package as skipped during initial scan, expecting a more up to date version to be
+     * available on the scan of a higher priority partition. This can be either a system partition
+     * or the data partition.
+     *
+     * If for some reason that newer version cannot be scanned successfully, the data structure
+     * created here will be used to backtrack in the scanning process to try and take the highest
+     * version code of the package left on disk that scans successfully.
+     *
+     * This can occur if an OTA adds a new system package which the user has already installed an
+     * update on data for. Or if the device image includes multiple versions of the same package,
+     * for cases where the maintainer of a higher priority partition wants to update an app on
+     * a lower priority partition before shipping a device to users.
+     *
+     * @param pkgName the package name identifier to queue under
+     * @param codePath the path to re-scan if needed
+     * @param knownVersionCode the version of the package so that the set of files can be sorted
+     */
+    private void expectBetter(String pkgName, File codePath, long knownVersionCode) {
+        List<Pair<File, Long>> pairs = mExpectingBetter.get(pkgName);
+        if (pairs == null) {
+            pairs = new ArrayList<>(0);
+            mExpectingBetter.put(pkgName, pairs);
+        }
+        pairs.add(Pair.create(codePath, knownVersionCode));
+    }
+
+    /**
      * Extract, install and enable a stub package.
      * <p>If the compressed file can not be extracted / installed for any reason, the stub
      * APK will be installed and the package will be disabled. To recover from this situation,
@@ -11271,7 +11311,23 @@
                 isUpdatedSystemApp = disabledPkgSetting != null;
             }
             applyPolicy(parsedPackage, parseFlags, scanFlags, mPlatformPackage, isUpdatedSystemApp);
-            assertPackageIsValid(parsedPackage, parseFlags, scanFlags);
+            try {
+                assertPackageIsValid(parsedPackage, pkgSetting, parseFlags, scanFlags);
+            } catch (PackageManagerException e) {
+                if (e.error == INSTALL_FAILED_VERSION_DOWNGRADE
+                        && ((parseFlags & PackageParser.PARSE_IS_SYSTEM_DIR) != 0)
+                        && ((scanFlags & SCAN_BOOTING) != 0)) {
+                    if (pkgSetting != null && pkgSetting.getPkg() == null) {
+                        // If a package for the pkgSetting hasn't already been found, this is
+                        // skipping a downgrade on a lower priority partition, and so a later scan
+                        // is expected to fill the package.
+                        expectBetter(pkgSetting.name, new File(parsedPackage.getPath()),
+                                parsedPackage.getLongVersionCode());
+                    }
+                }
+
+                throw e;
+            }
 
             SharedUserSetting sharedUserSetting = null;
             if (parsedPackage.getSharedUserId() != null) {
@@ -12123,9 +12179,9 @@
      *
      * @throws PackageManagerException If the package fails any of the validation checks
      */
-    private void assertPackageIsValid(AndroidPackage pkg, final @ParseFlags int parseFlags,
-            final @ScanFlags int scanFlags)
-                    throws PackageManagerException {
+    private void assertPackageIsValid(AndroidPackage pkg,
+            @Nullable PackageSetting existingPkgSetting, final @ParseFlags int parseFlags,
+            final @ScanFlags int scanFlags) throws PackageManagerException {
         if ((parseFlags & PackageParser.PARSE_ENFORCE_CODE) != 0) {
             assertCodePolicy(pkg);
         }
@@ -12140,11 +12196,11 @@
         // after OTA.
         final boolean isUserInstall = (scanFlags & SCAN_BOOTING) == 0;
         final boolean isFirstBootOrUpgrade = (scanFlags & SCAN_FIRST_BOOT_OR_UPGRADE) != 0;
+        String pkgName = pkg.getPackageName();
         if ((isUserInstall || isFirstBootOrUpgrade)
-                && mApexManager.isApexPackage(pkg.getPackageName())) {
+                && mApexManager.isApexPackage(pkgName)) {
             throw new PackageManagerException(INSTALL_FAILED_DUPLICATE_PACKAGE,
-                    pkg.getPackageName()
-                            + " is an APEX package and can't be installed as an APK.");
+                    pkgName + " is an APEX package and can't be installed as an APK.");
         }
 
         // Make sure we're not adding any bogus keyset info
@@ -12153,7 +12209,7 @@
 
         synchronized (mLock) {
             // The special "android" package can only be defined once
-            if (pkg.getPackageName().equals("android")) {
+            if (pkgName.equals("android")) {
                 if (mAndroidApplication != null) {
                     Slog.w(TAG, "*************************************************");
                     Slog.w(TAG, "Core android package being redefined.  Skipping.");
@@ -12164,12 +12220,46 @@
                 }
             }
 
-            // A package name must be unique; don't allow duplicates
-            if ((scanFlags & SCAN_NEW_INSTALL) == 0
-                    && mPackages.containsKey(pkg.getPackageName())) {
-                throw new PackageManagerException(INSTALL_FAILED_DUPLICATE_PACKAGE,
-                        "Application package " + pkg.getPackageName()
-                        + " already installed.  Skipping duplicate.");
+            final long newLongVersionCode = pkg.getLongVersionCode();
+            if ((scanFlags & SCAN_NEW_INSTALL) == 0) {
+                boolean runDuplicateCheck = false;
+
+                // It's possible to re-scan a package if an updated system app was expected, but
+                // no update on /data could be found. To avoid infinitely looping, a flag is passed
+                // in when re-scanning and this first branch is skipped if the flag is set.
+                if ((scanFlags & SCAN_EXPECTED_BETTER) == 0 && existingPkgSetting != null) {
+                    long existingLongVersionCode = existingPkgSetting.versionCode;
+                    if (newLongVersionCode <= existingLongVersionCode) {
+                        // Must check that real name is equivalent, as it's possible to downgrade
+                        // version code if the package is actually a different package taking over
+                        // a package name through <original-package/>. It is assumed that this
+                        // migration is one time, one way, and that there is no failsafe if this
+                        // doesn't hold true.
+                        if (Objects.equals(existingPkgSetting.realName, pkg.getRealPackage())) {
+                            if (newLongVersionCode != existingLongVersionCode) {
+                                throw new PackageManagerException(
+                                        INSTALL_FAILED_VERSION_DOWNGRADE,
+                                        "Ignoring lower version " + newLongVersionCode
+                                                + " for package " + pkgName
+                                                + " with expected version "
+                                                + existingLongVersionCode);
+                            }
+                        }
+                    } else if ((parseFlags & PackageParser.PARSE_IS_SYSTEM_DIR) != 0
+                            && (scanFlags & SCAN_BOOTING) != 0) {
+                        // During system boot scan, if there's already a package known, but this
+                        // package is higher version, use it instead, ignoring the duplicate check.
+                        // This will store the higher version in the setting object, and the above
+                        // branch/exception will cause future scans to skip the lower versions.
+                        runDuplicateCheck = false;
+                    }
+                }
+
+                if (runDuplicateCheck && mPackages.containsKey(pkgName)) {
+                    throw new PackageManagerException(INSTALL_FAILED_DUPLICATE_PACKAGE,
+                            "Application package " + pkgName
+                                    + " already installed.  Skipping duplicate.");
+                }
             }
 
             if (pkg.isStaticSharedLibrary()) {
@@ -12289,8 +12379,8 @@
                         }
                     }
                 }
-                if (pkg.getLongVersionCode() < minVersionCode
-                        || pkg.getLongVersionCode() > maxVersionCode) {
+                if (newLongVersionCode < minVersionCode
+                        || newLongVersionCode > maxVersionCode) {
                     throw new PackageManagerException("Static shared"
                             + " lib version codes must be ordered as lib versions");
                 }
@@ -12305,11 +12395,10 @@
             // to the user-installed location. If we don't allow this change, any newer,
             // user-installed version of the application will be ignored.
             if ((scanFlags & SCAN_REQUIRE_KNOWN) != 0) {
-                if (mExpectingBetter.containsKey(pkg.getPackageName())) {
-                    Slog.w(TAG, "Relax SCAN_REQUIRE_KNOWN requirement for package "
-                            + pkg.getPackageName());
+                if (mExpectingBetter.containsKey(pkgName)) {
+                    Slog.w(TAG, "Relax SCAN_REQUIRE_KNOWN requirement for package " + pkgName);
                 } else {
-                    PackageSetting known = mSettings.getPackageLPr(pkg.getPackageName());
+                    PackageSetting known = mSettings.getPackageLPr(pkgName);
                     if (known != null) {
                         if (DEBUG_PACKAGE_SCANNING) {
                             Log.d(TAG, "Examining " + pkg.getPath()
@@ -12317,14 +12406,14 @@
                         }
                         if (!pkg.getPath().equals(known.getPathString())) {
                             throw new PackageManagerException(INSTALL_FAILED_PACKAGE_CHANGED,
-                                    "Application package " + pkg.getPackageName()
+                                    "Application package " + pkgName
                                     + " found at " + pkg.getPath()
                                     + " but expected at " + known.getPathString()
                                     + "; ignoring.");
                         }
                     } else {
                         throw new PackageManagerException(INSTALL_FAILED_INVALID_INSTALL_LOCATION,
-                                "Application package " + pkg.getPackageName()
+                                "Application package " + pkgName
                                 + " not found; ignoring.");
                     }
                 }
@@ -12347,7 +12436,7 @@
                             INSTALL_FAILED_PROCESS_NOT_DEFINED,
                             "Can't install because application tag's process attribute "
                                     + pkg.getProcessName()
-                                    + " (in package " + pkg.getPackageName()
+                                    + " (in package " + pkgName
                                     + ") is not included in the <processes> list");
                 }
                 assertPackageProcesses(pkg, pkg.getActivities(), procs, "activity");
@@ -12371,7 +12460,7 @@
                             pkg.getSigningDetails().signatures)) {
                         throw new PackageManagerException("Apps that share a user with a " +
                                 "privileged app must themselves be marked as privileged. " +
-                                pkg.getPackageName() + " shares privileged user " +
+                                pkgName + " shares privileged user " +
                                 pkg.getSharedUserId() + ".");
                     }
                 }
@@ -12388,21 +12477,21 @@
                         // upgraded.
                         Objects.requireNonNull(mOverlayConfig,
                                 "Parsing non-system dir before overlay configs are initialized");
-                        if (!mOverlayConfig.isMutable(pkg.getPackageName())) {
+                        if (!mOverlayConfig.isMutable(pkgName)) {
                             throw new PackageManagerException("Overlay "
-                                    + pkg.getPackageName()
+                                    + pkgName
                                     + " is static and cannot be upgraded.");
                         }
                     } else {
                         if ((scanFlags & SCAN_AS_VENDOR) != 0) {
                             if (pkg.getTargetSdkVersion() < getVendorPartitionVersion()) {
-                                Slog.w(TAG, "System overlay " + pkg.getPackageName()
+                                Slog.w(TAG, "System overlay " + pkgName
                                         + " targets an SDK below the required SDK level of vendor"
                                         + " overlays (" + getVendorPartitionVersion() + ")."
                                         + " This will become an install error in a future release");
                             }
                         } else if (pkg.getTargetSdkVersion() < Build.VERSION.SDK_INT) {
-                            Slog.w(TAG, "System overlay " + pkg.getPackageName()
+                            Slog.w(TAG, "System overlay " + pkgName
                                     + " targets an SDK below the required SDK level of system"
                                     + " overlays (" + Build.VERSION.SDK_INT + ")."
                                     + " This will become an install error in a future release");
@@ -12418,7 +12507,7 @@
                         if (!comparePackageSignatures(platformPkgSetting,
                                 pkg.getSigningDetails().signatures)) {
                             throw new PackageManagerException("Overlay "
-                                    + pkg.getPackageName()
+                                    + pkgName
                                     + " must target Q or later, "
                                     + "or be signed with the platform certificate");
                         }
@@ -12440,7 +12529,7 @@
                                 // check reference signature
                                 if (mOverlayConfigSignaturePackage == null) {
                                     throw new PackageManagerException("Overlay "
-                                            + pkg.getPackageName() + " and target "
+                                            + pkgName + " and target "
                                             + pkg.getOverlayTarget() + " signed with"
                                             + " different certificates, and the overlay lacks"
                                             + " <overlay android:targetName>");
@@ -12450,7 +12539,7 @@
                                 if (!comparePackageSignatures(refPkgSetting,
                                         pkg.getSigningDetails().signatures)) {
                                     throw new PackageManagerException("Overlay "
-                                            + pkg.getPackageName() + " signed with a different "
+                                            + pkgName + " signed with a different "
                                             + "certificate than both the reference package and "
                                             + "target " + pkg.getOverlayTarget() + ", and the "
                                             + "overlay lacks <overlay android:targetName>");
@@ -12470,7 +12559,7 @@
                 if (pkg.getSigningDetails().signatureSchemeVersion < minSignatureSchemeVersion) {
                     throw new PackageManagerException(INSTALL_PARSE_FAILED_NO_CERTIFICATES,
                             "No signature found in package of version " + minSignatureSchemeVersion
-                                    + " or newer for package " + pkg.getPackageName());
+                                    + " or newer for package " + pkgName);
                 }
             }
         }
diff --git a/services/core/java/com/android/server/pm/parsing/PackageCacher.java b/services/core/java/com/android/server/pm/parsing/PackageCacher.java
index 74ec161..3463daf 100644
--- a/services/core/java/com/android/server/pm/parsing/PackageCacher.java
+++ b/services/core/java/com/android/server/pm/parsing/PackageCacher.java
@@ -57,7 +57,16 @@
      * Returns the cache key for a specified {@code packageFile} and {@code flags}.
      */
     private String getCacheKey(File packageFile, int flags) {
-        StringBuilder sb = new StringBuilder(packageFile.getName());
+        StringBuilder sb = new StringBuilder();
+
+        // To support packages with the same file name across partitions, use the partition name
+        // as a prefix. The cache should only be used for cases where the file paths have been
+        // established using the unique partition names, without canonicalization, so any links
+        // which would point to the same partition name should be handled separately.
+        String cachePrefix = packageFile.toPath().getName(0).toString();
+        sb.append(cachePrefix);
+        sb.append('-');
+        sb.append(packageFile.getName());
         sb.append('-');
         sb.append(flags);
 
diff --git a/services/core/java/com/android/server/pm/parsing/PackageParser2.java b/services/core/java/com/android/server/pm/parsing/PackageParser2.java
index 851ddd1..46d31d9 100644
--- a/services/core/java/com/android/server/pm/parsing/PackageParser2.java
+++ b/services/core/java/com/android/server/pm/parsing/PackageParser2.java
@@ -135,7 +135,7 @@
     }
 
     /**
-     * TODO(b/135203078): Document new package parsing
+     * TODO(b/155493909): Document new package parsing
      */
     @AnyThread
     public ParsedPackage parsePackage(File packageFile, int flags, boolean useCaches)
diff --git a/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/FactoryPackageTest.kt b/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/FactoryPackageTest.kt
index e17358d..7ae2fe0 100644
--- a/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/FactoryPackageTest.kt
+++ b/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/FactoryPackageTest.kt
@@ -17,11 +17,8 @@
 class FactoryPackageTest : BaseHostJUnit4Test() {
 
     companion object {
-        private const val TEST_PKG_NAME = "com.android.server.pm.test.test_app"
-
-        private const val VERSION_ONE = "PackageManagerTestAppVersion1.apk"
-        private const val VERSION_TWO = "PackageManagerTestAppVersion2.apk"
         private const val DEVICE_SIDE = "PackageManagerServiceDeviceSideTests.apk"
+        private const val DEVICE_SIDE_PKG_NAME = "com.android.server.pm.test.deviceside"
 
         @get:ClassRule
         val deviceRebootRule = SystemPreparer.TestRuleDelegate(true)
@@ -40,7 +37,8 @@
     @Before
     @After
     fun removeApk() {
-        device.uninstallPackage(TEST_PKG_NAME)
+        HostUtils.deleteAllTestPackages(device, preparer)
+        device.uninstallPackage(DEVICE_SIDE_PKG_NAME)
         device.deleteFile(filePath.parent.toString())
         device.reboot()
     }
diff --git a/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/HostUtils.kt b/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/HostUtils.kt
index 9399030..f0b60f4 100644
--- a/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/HostUtils.kt
+++ b/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/HostUtils.kt
@@ -23,6 +23,15 @@
 import java.io.File
 import java.io.FileOutputStream
 
+internal const val TEST_PKG_NAME = "com.android.server.pm.test.test_app"
+internal const val VERSION_STUB = "PackageManagerTestAppStub.apk"
+internal const val VERSION_ONE = "PackageManagerTestAppVersion1.apk"
+internal const val VERSION_TWO = "PackageManagerTestAppVersion2.apk"
+internal const val VERSION_THREE = "PackageManagerTestAppVersion3.apk"
+internal const val VERSION_THREE_INVALID = "PackageManagerTestAppVersion3Invalid.apk"
+internal const val VERSION_FOUR = "PackageManagerTestAppVersion4.apk"
+internal const val VERSION_OVERRIDE = "PackageManagerTestAppOriginalOverride.apk"
+
 internal fun SystemPreparer.pushApk(javaResourceName: String, partition: Partition) =
         pushResourceFile(javaResourceName, HostUtils.makePathForApk(javaResourceName, partition)
                 .toString())
@@ -89,12 +98,25 @@
 
 internal object HostUtils {
 
-    fun getDataDir(device: ITestDevice, pkgName: String) =
-            device.executeShellCommand("dumpsys package $pkgName")
-                    .lineSequence()
-                    .map(String::trim)
-                    .single { it.startsWith("dataDir=") }
-                    .removePrefix("dataDir=")
+    /**
+     * Since most of the tests use the same test APKs, consolidate the logic for deleting them
+     * before and after a test runs. This also ensures that a failing test doesn't leave an APK on
+     * device that could spill over to another test when developing locally.
+     *
+     * Iterates all partitions since different tests use different partitions.
+     */
+    fun deleteAllTestPackages(device: ITestDevice, preparer: SystemPreparer) {
+        Partition.values().forEach { partition ->
+            device.uninstallPackage(TEST_PKG_NAME)
+            preparer.deleteApkFolders(partition, VERSION_ONE, VERSION_TWO, VERSION_THREE,
+                    VERSION_THREE_INVALID, VERSION_FOUR, VERSION_OVERRIDE)
+        }
+
+        // TODO: There is an optimization that can be made here by hooking into the SystemPreparer's
+        //  reboot rule, avoiding a reboot cycle by doing the delete in line the built in @After
+        //  reboot.
+        preparer.reboot()
+    }
 
     fun makePathForApk(fileName: String, partition: Partition) =
             makePathForApk(File(fileName), partition)
@@ -136,14 +158,35 @@
             }
             .map(String::trim)
 
+    fun getDataDir(device: ITestDevice, pkgName: String) =
+            packageSection(device, pkgName)
+                    .singleOrNull { it.startsWith("dataDir=") }
+                    ?.removePrefix("dataDir=")
+
+    /** Return all code paths for a package. This will include hidden system package code paths. */
     fun getCodePaths(device: ITestDevice, pkgName: String) =
-            device.executeShellCommand("pm dump $pkgName")
-                    .lineSequence()
-                    .map(String::trim)
+            (packageSection(device, pkgName) +
+                    packageSection(device, pkgName, "Hidden system packages"))
                     .filter { it.startsWith("codePath=") }
                     .map { it.removePrefix("codePath=") }
                     .toList()
 
+    fun getVersionCode(device: ITestDevice, pkgName: String) =
+            packageSection(device, pkgName)
+                    .filter { it.startsWith("versionCode=") }
+                    .map { it.removePrefix("versionCode=") }
+                    .map { it.takeWhile { !it.isWhitespace() } }
+                    .map { it.toInt() }
+                    .firstOrNull()
+
+    fun getPrivateFlags(device: ITestDevice, pkgName: String) =
+            packageSection(device, pkgName)
+                    .filter { it.startsWith("privateFlags=") }
+                    .map { it.removePrefix("privateFlags=[ ") }
+                    .map { it.removeSuffix(" ]") }
+                    .map { it.split(" ") }
+                    .firstOrNull()
+
     private fun userIdLineSequence(device: ITestDevice, pkgName: String) =
             packageSection(device, pkgName)
                     .filter { it.startsWith("User ") }
diff --git a/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/InvalidNewSystemAppTest.kt b/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/InvalidNewSystemAppTest.kt
index 37c999c..8594706 100644
--- a/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/InvalidNewSystemAppTest.kt
+++ b/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/InvalidNewSystemAppTest.kt
@@ -33,12 +33,6 @@
 class InvalidNewSystemAppTest : BaseHostJUnit4Test() {
 
     companion object {
-        private const val TEST_PKG_NAME = "com.android.server.pm.test.test_app"
-        private const val VERSION_ONE = "PackageManagerTestAppVersion1.apk"
-        private const val VERSION_TWO = "PackageManagerTestAppVersion2.apk"
-        private const val VERSION_THREE_INVALID = "PackageManagerTestAppVersion3Invalid.apk"
-        private const val VERSION_FOUR = "PackageManagerTestAppVersion4.apk"
-
         @get:ClassRule
         val deviceRebootRule = SystemPreparer.TestRuleDelegate(true)
     }
@@ -55,7 +49,7 @@
     @Before
     @After
     fun removeApk() {
-        device.uninstallPackage(TEST_PKG_NAME)
+        HostUtils.deleteAllTestPackages(device, preparer)
         preparer.deleteFile(filePath.parent.toString())
                 .reboot()
     }
diff --git a/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/OriginalPackageMigrationTest.kt b/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/OriginalPackageMigrationTest.kt
index 4becae6..0c5816b 100644
--- a/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/OriginalPackageMigrationTest.kt
+++ b/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/OriginalPackageMigrationTest.kt
@@ -34,10 +34,6 @@
 
     companion object {
         private const val TEST_PKG_NAME = "com.android.server.pm.test.test_app"
-        private const val VERSION_ONE = "PackageManagerTestAppVersion1.apk"
-        private const val VERSION_TWO = "PackageManagerTestAppVersion2.apk"
-        private const val VERSION_THREE = "PackageManagerTestAppVersion3.apk"
-        private const val NEW_PKG = "PackageManagerTestAppOriginalOverride.apk"
 
         @get:ClassRule
         val deviceRebootRule = SystemPreparer.TestRuleDelegate(true)
@@ -54,9 +50,7 @@
     @Before
     @After
     fun deleteApkFolders() {
-        preparer.deleteApkFolders(Partition.SYSTEM, VERSION_ONE, VERSION_TWO, VERSION_THREE,
-                NEW_PKG)
-                .reboot()
+        HostUtils.deleteAllTestPackages(device, preparer)
     }
 
     @Test
@@ -89,10 +83,10 @@
         device.pushFile(file, "${HostUtils.getDataDir(device, TEST_PKG_NAME)}/files/test.txt")
 
         preparer.deleteApkFolders(Partition.SYSTEM, apk)
-                .pushApk(NEW_PKG, Partition.SYSTEM)
+                .pushApk(VERSION_OVERRIDE, Partition.SYSTEM)
                 .reboot()
 
-        assertCodePath(NEW_PKG)
+        assertCodePath(VERSION_OVERRIDE)
 
         // And then reading the data contents back
         assertThat(device.pullFileContents(
diff --git a/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/SystemAppScanPriorityTest.kt b/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/SystemAppScanPriorityTest.kt
new file mode 100644
index 0000000..8d789e0
--- /dev/null
+++ b/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/SystemAppScanPriorityTest.kt
@@ -0,0 +1,272 @@
+/*
+ * Copyright (C) 2020 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.test
+
+import com.android.internal.util.test.SystemPreparer
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunner
+import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test
+import com.google.common.truth.Truth.assertThat
+import org.junit.After
+import org.junit.Before
+import org.junit.ClassRule
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.RuleChain
+import org.junit.rules.TemporaryFolder
+import org.junit.runner.RunWith
+import java.io.File
+
+/**
+ * Pushes APKs onto various system partitions to verify that multiple versions result in the
+ * highest version being scanned. Also tries to upgrade/replace these APKs which should result
+ * in a version upgrade on reboot.
+ *
+ * This will also verify that APKs under the same folder/file name across different partitions
+ * are parsed as separate entities and don't get combined under the same cache entry.
+ *
+ * Known limitations:
+ * - Does not verify that v1 isn't scanned. It's possible to introduce a bug that upgrades the
+ *     system on every reboot from v1 -> v2, as this isn't easily visible after scan has finished.
+ *     This would also have to successfully preserve the app data though, which seems unlikely.
+ * - This takes a very long time to run. 105 seconds for the first test case, up to 60 seconds for
+ *     each following case. It's theoretically possible to parallelize these tests so that each
+ *     method is run by installing all the apps under different names, requiring only 3 reboots to
+ *     fully verify, rather than 3 * numTestCases.
+ */
+@RunWith(DeviceJUnit4ClassRunner::class)
+class SystemAppScanPriorityTest : BaseHostJUnit4Test() {
+
+    companion object {
+        @get:ClassRule
+        var deviceRebootRule = SystemPreparer.TestRuleDelegate(true)
+    }
+
+    private val tempFolder = TemporaryFolder()
+    private val preparer: SystemPreparer = SystemPreparer(tempFolder,
+            SystemPreparer.RebootStrategy.FULL, deviceRebootRule) { this.device }
+
+    private var firstReboot = true
+
+    @Rule
+    @JvmField
+    val rules = RuleChain.outerRule(tempFolder).around(preparer)!!
+
+    @Before
+    @After
+    fun deleteFiles() {
+        HostUtils.deleteAllTestPackages(device, preparer)
+    }
+
+    @Before
+    fun resetFirstReboot() {
+        firstReboot = true
+    }
+
+    @Test
+    fun takeHigherPriority() {
+        preparer.pushFile(VERSION_ONE, Partition.VENDOR)
+                .pushFile(VERSION_TWO, Partition.PRODUCT)
+                .rebootForTest()
+
+        assertVersionAndPartition(2, Partition.PRODUCT)
+    }
+
+    @Test
+    fun takeLowerPriority() {
+        preparer.pushFile(VERSION_TWO, Partition.VENDOR)
+                .pushFile(VERSION_ONE, Partition.PRODUCT)
+                .rebootForTest()
+
+        assertVersionAndPartition(2, Partition.VENDOR)
+    }
+
+    @Test
+    fun upgradeToHigherOnLowerPriority() {
+        preparer.pushFile(VERSION_ONE, Partition.VENDOR)
+                .pushFile(VERSION_TWO, Partition.PRODUCT)
+                .rebootForTest()
+
+        assertVersionAndPartition(2, Partition.PRODUCT)
+
+        preparer.pushFile(VERSION_THREE, Partition.VENDOR)
+                .rebootForTest()
+
+        assertVersionAndPartition(3, Partition.VENDOR)
+    }
+
+    @Test
+    fun upgradeToNewerOnHigherPriority() {
+        preparer.pushFile(VERSION_ONE, Partition.VENDOR)
+                .pushFile(VERSION_TWO, Partition.PRODUCT)
+                .rebootForTest()
+
+        assertVersionAndPartition(2, Partition.PRODUCT)
+
+        preparer.pushFile(VERSION_THREE, Partition.SYSTEM_EXT)
+                .rebootForTest()
+
+        assertVersionAndPartition(3, Partition.SYSTEM_EXT)
+    }
+
+    @Test
+    fun replaceNewerOnLowerPriority() {
+        preparer.pushFile(VERSION_TWO, Partition.VENDOR)
+                .pushFile(VERSION_ONE, Partition.PRODUCT)
+                .rebootForTest()
+
+        assertVersionAndPartition(2, Partition.VENDOR)
+
+        preparer.pushFile(VERSION_THREE, Partition.VENDOR)
+                .rebootForTest()
+
+        assertVersionAndPartition(3, Partition.VENDOR)
+    }
+
+    @Test
+    fun replaceNewerOnHigherPriority() {
+        preparer.pushFile(VERSION_ONE, Partition.VENDOR)
+                .pushFile(VERSION_TWO, Partition.PRODUCT)
+                .rebootForTest()
+
+        assertVersionAndPartition(2, Partition.PRODUCT)
+
+        preparer.pushFile(VERSION_THREE, Partition.PRODUCT)
+                .rebootForTest()
+
+        assertVersionAndPartition(3, Partition.PRODUCT)
+    }
+
+    @Test
+    fun fallbackToLowerPriority() {
+        preparer.pushFile(VERSION_TWO, Partition.VENDOR)
+                .pushFile(VERSION_ONE, Partition.PRODUCT)
+                .pushFile(VERSION_THREE, Partition.SYSTEM_EXT)
+                .rebootForTest()
+
+        assertVersionAndPartition(3, Partition.SYSTEM_EXT)
+
+        preparer.deleteFile(VERSION_THREE, Partition.SYSTEM_EXT)
+                .rebootForTest()
+
+        assertVersionAndPartition(2, Partition.VENDOR)
+    }
+
+    @Test
+    fun fallbackToHigherPriority() {
+        preparer.pushFile(VERSION_THREE, Partition.VENDOR)
+                .pushFile(VERSION_ONE, Partition.PRODUCT)
+                .pushFile(VERSION_TWO, Partition.SYSTEM_EXT)
+                .rebootForTest()
+
+        assertVersionAndPartition(3, Partition.VENDOR)
+
+        preparer.deleteFile(VERSION_THREE, Partition.VENDOR)
+                .rebootForTest()
+
+        assertVersionAndPartition(2, Partition.SYSTEM_EXT)
+    }
+
+    @Test
+    fun removeBoth() {
+        preparer.pushFile(VERSION_ONE, Partition.VENDOR)
+                .pushFile(VERSION_TWO, Partition.PRODUCT)
+                .rebootForTest()
+
+        assertVersionAndPartition(2, Partition.PRODUCT)
+
+        preparer.deleteFile(VERSION_ONE, Partition.VENDOR)
+                .deleteFile(VERSION_TWO, Partition.PRODUCT)
+                .rebootForTest()
+
+        assertThat(device.getAppPackageInfo(TEST_PKG_NAME)).isNull()
+    }
+
+    private fun assertVersionAndPartition(versionCode: Int, partition: Partition) {
+        assertThat(HostUtils.getVersionCode(device, TEST_PKG_NAME)).isEqualTo(versionCode)
+
+        val privateFlags = HostUtils.getPrivateFlags(device, TEST_PKG_NAME)
+
+        when (partition) {
+            Partition.SYSTEM,
+            Partition.SYSTEM_PRIVILEGED -> {
+                assertThat(privateFlags).doesNotContain(Partition.VENDOR.toString())
+                assertThat(privateFlags).doesNotContain(Partition.PRODUCT.toString())
+                assertThat(privateFlags).doesNotContain(Partition.SYSTEM_EXT.toString())
+            }
+            Partition.VENDOR -> {
+                assertThat(privateFlags).contains(Partition.VENDOR.toString())
+                assertThat(privateFlags).doesNotContain(Partition.PRODUCT.toString())
+                assertThat(privateFlags).doesNotContain(Partition.SYSTEM_EXT.toString())
+            }
+            Partition.PRODUCT -> {
+                assertThat(privateFlags).doesNotContain(Partition.VENDOR.toString())
+                assertThat(privateFlags).contains(Partition.PRODUCT.toString())
+                assertThat(privateFlags).doesNotContain(Partition.SYSTEM_EXT.toString())
+            }
+            Partition.SYSTEM_EXT -> {
+                assertThat(privateFlags).doesNotContain(Partition.VENDOR.toString())
+                assertThat(privateFlags).doesNotContain(Partition.PRODUCT.toString())
+                assertThat(privateFlags).contains(Partition.SYSTEM_EXT.toString())
+            }
+        }.run { /* exhaust */ }
+    }
+
+    // Following methods don't use HostUtils in order to test cache behavior when using the same
+    // name across partitions. Writes all files under the version 1 name.
+    private fun makeDevicePath(partition: Partition) =
+            partition.baseAppFolder
+                    .resolve(File(VERSION_ONE).nameWithoutExtension)
+                    .resolve(VERSION_ONE)
+                    .toString()
+
+    private fun SystemPreparer.pushFile(file: String, partition: Partition) =
+            pushResourceFile(file, makeDevicePath(partition))
+
+    private fun SystemPreparer.deleteFile(file: String, partition: Partition) =
+            deleteFile(makeDevicePath(partition))
+
+    /**
+     * Custom reboot used to write app data after the first reboot. This can then be verified
+     * after each subsequent reboot to ensure no data is lost.
+     */
+    private fun SystemPreparer.rebootForTest() {
+        if (firstReboot) {
+            firstReboot = false
+            preparer.reboot()
+
+            val file = tempFolder.newFile()
+            file.writeText("Test")
+            pushFile(file, "${HostUtils.getDataDir(device, TEST_PKG_NAME)}/files/test.txt")
+        } else {
+            val versionBefore = HostUtils.getVersionCode(device, TEST_PKG_NAME)
+            preparer.reboot()
+            val versionAfter = HostUtils.getVersionCode(device, TEST_PKG_NAME)
+
+            if (versionBefore != null && versionAfter != null) {
+                val fileContents = device.pullFileContents(
+                        "${HostUtils.getDataDir(device, TEST_PKG_NAME)}/files/test.txt")
+                if (versionAfter < versionBefore) {
+                    // A downgrade will wipe app data
+                    assertThat(fileContents).isNull()
+                } else {
+                    // An upgrade or update will preserve app data
+                    assertThat(fileContents).isEqualTo("Test")
+                }
+            }
+        }
+    }
+}
diff --git a/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/SystemStubMultiUserDisableUninstallTest.kt b/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/SystemStubMultiUserDisableUninstallTest.kt
index 46120af..99dff08 100644
--- a/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/SystemStubMultiUserDisableUninstallTest.kt
+++ b/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/SystemStubMultiUserDisableUninstallTest.kt
@@ -37,9 +37,6 @@
 class SystemStubMultiUserDisableUninstallTest : BaseHostJUnit4Test() {
 
     companion object {
-        private const val TEST_PKG_NAME = "com.android.server.pm.test.test_app"
-        private const val VERSION_STUB = "PackageManagerTestAppStub.apk"
-        private const val VERSION_ONE = "PackageManagerTestAppVersion1.apk"
 
         /**
          * How many total users on device to test, including primary. This will clean up any
@@ -90,6 +87,12 @@
                 savedDevice?.removeUser(it)
             }
 
+            savedDevice?.let { device ->
+                savedPreparer?.let { preparer ->
+                    HostUtils.deleteAllTestPackages(device, preparer)
+                }
+            }
+
             savedDevice?.uninstallPackage(TEST_PKG_NAME)
             savedDevice?.deleteFile(stubFile.parent.toString())
             savedDevice?.deleteFile(deviceCompressedFile.parent.toString())