Merge "Support multiple app versions across system partitions"

This commit is contained in:
TreeHugger Robot
2020-11-03 01:49:08 +00:00
committed by Android (Google) Code Review
9 changed files with 503 additions and 101 deletions

View File

@@ -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<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 @@ 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<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());
}
}
}
}
@@ -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<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
@@ -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 <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 @@ 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 <processes> 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"
+ " <overlay android:targetName>");
@@ -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 <overlay android:targetName>");
@@ -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);
}
}
}

View File

@@ -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);

View File

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

View File

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

View File

@@ -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 ") }

View File

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

View File

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

View File

@@ -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")
}
}
}
}
}

View File

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