Merge "Support multiple app versions across system partitions"
This commit is contained in:
committed by
Android (Google) Code Review
commit
f6bd60e262
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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 ") }
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
|
||||
Reference in New Issue
Block a user