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())