diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java index cdd347beba0a6..9d704e7379789 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 @@ public class PackageManagerService extends IPackageManager.Stub 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 @@ public class PackageManagerService extends IPackageManager.Stub 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 mExpectingBetter = new ArrayMap<>(); + private final ArrayMap>> mExpectingBetter = new ArrayMap<>(); /** * Tracks existing packages prior to receiving an OTA. Keys are package name. @@ -3349,7 +3351,7 @@ public class PackageManagerService extends IPackageManager.Stub + ", 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 @@ public class PackageManagerService extends IPackageManager.Stub // 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 @@ public class PackageManagerService extends IPackageManager.Stub 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> 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()); + } } } } @@ -3900,6 +3913,33 @@ public class PackageManagerService extends IPackageManager.Stub } } + /** + * 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> 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. *

If the compressed file can not be extracted / installed for any reason, the stub @@ -11271,7 +11311,23 @@ public class PackageManagerService extends IPackageManager.Stub 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 @@ public class PackageManagerService extends IPackageManager.Stub * * @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 @@ public class PackageManagerService extends IPackageManager.Stub // 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 @@ public class PackageManagerService extends IPackageManager.Stub 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 @@ public class PackageManagerService extends IPackageManager.Stub } } - // 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 . 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 @@ public class PackageManagerService extends IPackageManager.Stub } } } - 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 @@ public class PackageManagerService extends IPackageManager.Stub // 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 @@ public class PackageManagerService extends IPackageManager.Stub } 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 @@ public class PackageManagerService extends IPackageManager.Stub 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 list"); } assertPackageProcesses(pkg, pkg.getActivities(), procs, "activity"); @@ -12371,7 +12460,7 @@ public class PackageManagerService extends IPackageManager.Stub 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 @@ public class PackageManagerService extends IPackageManager.Stub // 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 @@ public class PackageManagerService extends IPackageManager.Stub 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 @@ public class PackageManagerService extends IPackageManager.Stub // 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" + " "); @@ -12450,7 +12539,7 @@ public class PackageManagerService extends IPackageManager.Stub 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 "); @@ -12470,7 +12559,7 @@ public class PackageManagerService extends IPackageManager.Stub 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 74ec16140c945..3463daf748a69 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 @@ public class PackageCacher { * 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 851ddd1eae48d..46d31d9e907e9 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 @@ public class PackageParser2 implements AutoCloseable { } /** - * 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 e17358d38d8cd..7ae2fe0935de1 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 @@ import org.junit.runner.RunWith 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 @@ class FactoryPackageTest : BaseHostJUnit4Test() { @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 9399030e057c0..f0b60f48e0903 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 org.junit.rules.TemporaryFolder 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 fun retryUntilSuccess(block: () -> Boolean) { 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 @@ internal object HostUtils { } .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 37c999cbee685..85947063dc3a0 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 @@ import org.junit.runner.RunWith 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 @@ class InvalidNewSystemAppTest : BaseHostJUnit4Test() { @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 4becae66633f5..0c5816bd4317a 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 @@ class OriginalPackageMigrationTest : 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 = "PackageManagerTestAppVersion3.apk" - private const val NEW_PKG = "PackageManagerTestAppOriginalOverride.apk" @get:ClassRule val deviceRebootRule = SystemPreparer.TestRuleDelegate(true) @@ -54,9 +50,7 @@ class OriginalPackageMigrationTest : BaseHostJUnit4Test() { @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 @@ class OriginalPackageMigrationTest : BaseHostJUnit4Test() { 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 0000000000000..8d789e03f612e --- /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 46120af065508..99dff086170df 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 @@ import java.util.zip.GZIPOutputStream 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 @@ class SystemStubMultiUserDisableUninstallTest : BaseHostJUnit4Test() { 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())