Fix uninstall/disable with multi-user compressed stubs

Updates uninstall logic to ensure that only the user
being acted on is affected.

Previously, uninstalling from any user would disable
the app for user 0. This updates it to only change
the state of the stubbed package for the user that
initiates the uninstall.

This also changes the re-uncompression logic such that
stubs are kept in their system-only state until any
user has marked it installed and enabled.

Also adds tests for all the uninstall scenarios for
a stub system app. Some test cases are migrated from
I6fdd3ed485db0d3f8135266010d6797d9ac37ea0. Only the
corruption test isn't implemented, as this change
doesn't verify the install/scan behavior at boot.

Bug: 159204252

Test: atest com.android.server.pm.test.SystemStubMultiUserDisableUninstallTest
Test: atest PackageManagerServiceHostTests

Change-Id: I4ee2affc058d2bae363c537b3294d5d46a5051a0
This commit is contained in:
Winson
2020-07-23 11:58:05 -07:00
parent 8a6daa6103
commit 328aa43bff
15 changed files with 820 additions and 71 deletions

View File

@@ -18530,7 +18530,6 @@ public class PackageManagerService extends IPackageManager.Stub
// user handle installed state
int[] allUsers;
/** enabled state of the uninstalled application */
final int origEnabledState;
synchronized (mLock) {
uninstalledPs = mSettings.mPackages.get(packageName);
if (uninstalledPs == null) {
@@ -18546,10 +18545,6 @@ public class PackageManagerService extends IPackageManager.Stub
}
disabledSystemPs = mSettings.getDisabledSystemPkgLPr(packageName);
// Save the enabled state before we delete the package. When deleting a stub
// application we always set the enabled state to 'disabled'.
origEnabledState = uninstalledPs == null
? COMPONENT_ENABLED_STATE_DEFAULT : uninstalledPs.getEnabled(userId);
// Static shared libs can be declared by any package, so let us not
// allow removing a package if it provides a lib others depend on.
pkg = mPackages.get(packageName);
@@ -18628,20 +18623,32 @@ public class PackageManagerService extends IPackageManager.Stub
if (stubPkg != null && stubPkg.isStub()) {
final PackageSetting stubPs;
synchronized (mLock) {
// restore the enabled state of the stub; the state is overwritten when
// the stub is uninstalled
stubPs = mSettings.getPackageLPr(stubPkg.getPackageName());
if (stubPs != null) {
stubPs.setEnabled(origEnabledState, userId, "android");
}
}
if (origEnabledState == COMPONENT_ENABLED_STATE_DEFAULT
|| origEnabledState == COMPONENT_ENABLED_STATE_ENABLED) {
if (DEBUG_COMPRESSION) {
Slog.i(TAG, "Enabling system stub after removal; pkg: "
+ stubPkg.getPackageName());
if (stubPs != null) {
boolean enable = false;
for (int aUserId : allUsers) {
if (stubPs.getInstalled(aUserId)) {
int enabled = stubPs.getEnabled(aUserId);
if (enabled == COMPONENT_ENABLED_STATE_DEFAULT
|| enabled == COMPONENT_ENABLED_STATE_ENABLED) {
enable = true;
break;
}
}
}
if (enable) {
if (DEBUG_COMPRESSION) {
Slog.i(TAG, "Enabling system stub after removal; pkg: "
+ stubPkg.getPackageName());
}
enableCompressedPackage(stubPkg, stubPs);
} else if (DEBUG_COMPRESSION) {
Slog.i(TAG, "System stub disabled for all users, leaving uncompressed "
+ "after removal; pkg: " + stubPkg.getPackageName());
}
enableCompressedPackage(stubPkg, stubPs);
}
}
}
@@ -18985,8 +18992,15 @@ public class PackageManagerService extends IPackageManager.Stub
// and re-enable it afterward.
final PackageSetting stubPs = mSettings.mPackages.get(deletedPkg.getPackageName());
if (stubPs != null) {
stubPs.setEnabled(
COMPONENT_ENABLED_STATE_DISABLED, UserHandle.USER_SYSTEM, "android");
int userId = action.user == null
? UserHandle.USER_ALL : action.user.getIdentifier();
if (userId == UserHandle.USER_ALL) {
for (int aUserId : allUserHandles) {
stubPs.setEnabled(COMPONENT_ENABLED_STATE_DISABLED, aUserId, "android");
}
} else if (userId >= UserHandle.USER_SYSTEM) {
stubPs.setEnabled(COMPONENT_ENABLED_STATE_DISABLED, userId, "android");
}
}
}
}

View File

@@ -423,13 +423,15 @@ class PackageManagerShellCommand extends ShellCommand {
final List<ApplicationInfo> list;
if (packageName == null) {
final ParceledListSlice<ApplicationInfo> packages =
mInterface.getInstalledApplications(
PackageManager.MATCH_SYSTEM_ONLY, UserHandle.USER_SYSTEM);
mInterface.getInstalledApplications(PackageManager.MATCH_SYSTEM_ONLY
| PackageManager.MATCH_UNINSTALLED_PACKAGES,
UserHandle.USER_SYSTEM);
list = packages.getList();
} else {
list = new ArrayList<>(1);
list.add(mInterface.getApplicationInfo(packageName,
PackageManager.MATCH_SYSTEM_ONLY, UserHandle.USER_SYSTEM));
list.add(mInterface.getApplicationInfo(packageName, PackageManager.MATCH_SYSTEM_ONLY
| PackageManager.MATCH_UNINSTALLED_PACKAGES,
UserHandle.USER_SYSTEM));
}
for (ApplicationInfo info : list) {
if (info.isUpdatedSystemApp()) {

View File

@@ -25,17 +25,28 @@ java_test_host {
],
test_suites: ["general-tests"],
java_resources: [
":PackageManagerDummyAppVersion1",
":PackageManagerDummyAppVersion2",
":PackageManagerDummyAppVersion3",
":PackageManagerDummyAppVersion4",
":PackageManagerDummyAppOriginalOverride",
":PackageManagerServiceHostTestsResources",
]
":PackageManagerTestAppStub",
":PackageManagerTestAppVersion1",
":PackageManagerTestAppVersion2",
":PackageManagerTestAppVersion3",
":PackageManagerTestAppVersion3Invalid",
":PackageManagerTestAppVersion4",
":PackageManagerTestAppOriginalOverride",
],
}
filegroup {
name: "PackageManagerServiceHostTestsResources",
srcs: [ "resources/*" ],
path: "resources/"
genrule {
name: "PackageManagerTestAppVersion3Invalid",
tools: [
"soong_zip",
"zipalign",
],
srcs: [
":PackageManagerTestAppVersion3",
],
out: ["PackageManagerTestAppVersion3Invalid.apk"],
cmd: "mkdir -p $(genDir)/apk && unzip $(in) -d $(genDir)/apk" +
" && truncate -s 800 $(genDir)/apk/META-INF/CERT.RSA" +
" && $(location soong_zip) -o $(genDir)/temp.apk -L 0 -C $(genDir)/apk -D $(genDir)/apk" +
" && $(location zipalign) -f 4 $(genDir)/temp.apk $(out)",
}

View File

@@ -21,8 +21,9 @@ import com.android.tradefed.device.ITestDevice
import java.io.File
import java.io.FileOutputStream
internal fun SystemPreparer.pushApk(file: String, partition: Partition) =
pushResourceFile(file, HostUtils.makePathForApk(file, partition).toString())
internal fun SystemPreparer.pushApk(javaResourceName: String, partition: Partition) =
pushResourceFile(javaResourceName, HostUtils.makePathForApk(javaResourceName, partition)
.toString())
internal fun SystemPreparer.deleteApkFolders(
partition: Partition,
@@ -58,4 +59,55 @@ internal object HostUtils {
}
return file
}
/**
* dumpsys package and therefore device.getAppPackageInfo doesn't work immediately after reboot,
* so the following methods parse the package dump directly to see if the path matches.
*/
fun getCodePaths(device: ITestDevice, pkgName: String) =
device.executeShellCommand("pm dump $pkgName")
.lineSequence()
.map(String::trim)
.filter { it.startsWith("codePath=") }
.map { it.removePrefix("codePath=") }
.toList()
private fun userIdLineSequence(device: ITestDevice, pkgName: String) =
device.executeShellCommand("pm dump $pkgName")
.lineSequence()
.dropWhile { !it.startsWith("Packages:") }
.takeWhile {
!it.startsWith("Hidden system packages:") &&
!it.startsWith("Queries:")
}
.map(String::trim)
.filter { it.startsWith("User ") }
fun getUserIdToPkgEnabledState(device: ITestDevice, pkgName: String) =
userIdLineSequence(device, pkgName).associate {
val userId = it.removePrefix("User ")
.takeWhile(Char::isDigit)
.toInt()
val enabled = it.substringAfter("enabled=")
.takeWhile(Char::isDigit)
.toInt()
.let {
when (it) {
0, 1 -> true
else -> false
}
}
userId to enabled
}
fun getUserIdToPkgInstalledState(device: ITestDevice, pkgName: String) =
userIdLineSequence(device, pkgName).associate {
val userId = it.removePrefix("User ")
.takeWhile(Char::isDigit)
.toInt()
val installed = it.substringAfter("installed=")
.takeWhile { !it.isWhitespace() }
.toBoolean()
userId to installed
}
}

View File

@@ -33,11 +33,11 @@ import org.junit.runner.RunWith
class InvalidNewSystemAppTest : BaseHostJUnit4Test() {
companion object {
private const val TEST_PKG_NAME = "com.android.server.pm.test.dummy_app"
private const val VERSION_ONE = "PackageManagerDummyAppVersion1.apk"
private const val VERSION_TWO = "PackageManagerDummyAppVersion2.apk"
private const val VERSION_THREE_INVALID = "PackageManagerDummyAppVersion3Invalid.apk"
private const val VERSION_FOUR = "PackageManagerDummyAppVersion4.apk"
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)
@@ -49,14 +49,14 @@ class InvalidNewSystemAppTest : BaseHostJUnit4Test() {
@get:Rule
val rules = RuleChain.outerRule(tempFolder).around(preparer)!!
private val filePath = HostUtils.makePathForApk("PackageManagerDummyApp.apk", Partition.PRODUCT)
private val filePath = HostUtils.makePathForApk("PackageManagerTestApp.apk", Partition.PRODUCT)
@Before
@After
fun removeApk() {
device.uninstallPackage(TEST_PKG_NAME)
device.deleteFile(filePath.parent.toString())
device.reboot()
preparer.deleteFile(filePath.parent.toString())
.reboot()
}
@Test

View File

@@ -33,11 +33,11 @@ import org.junit.runner.RunWith
class OriginalPackageMigrationTest : BaseHostJUnit4Test() {
companion object {
private const val TEST_PKG_NAME = "com.android.server.pm.test.dummy_app"
private const val VERSION_ONE = "PackageManagerDummyAppVersion1.apk"
private const val VERSION_TWO = "PackageManagerDummyAppVersion2.apk"
private const val VERSION_THREE = "PackageManagerDummyAppVersion3.apk"
private const val NEW_PKG = "PackageManagerDummyAppOriginalOverride.apk"
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)
@@ -55,6 +55,7 @@ class OriginalPackageMigrationTest : BaseHostJUnit4Test() {
fun deleteApkFolders() {
preparer.deleteApkFolders(Partition.SYSTEM, VERSION_ONE, VERSION_TWO, VERSION_THREE,
NEW_PKG)
.reboot()
}
@Test
@@ -99,9 +100,7 @@ class OriginalPackageMigrationTest : BaseHostJUnit4Test() {
}
private fun assertCodePath(apk: String) {
// dumpsys package and therefore device.getAppPackageInfo doesn't work here for some reason,
// so parse the package dump directly to see if the path matches.
assertThat(device.executeShellCommand("pm dump $TEST_PKG_NAME"))
.contains(HostUtils.makePathForApk(apk, Partition.SYSTEM).parent.toString())
assertThat(HostUtils.getCodePaths(device, TEST_PKG_NAME))
.containsExactly(HostUtils.makePathForApk(apk, Partition.SYSTEM).parent.toString())
}
}

View File

@@ -0,0 +1,650 @@
/*
* 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.device.ITestDevice
import com.android.tradefed.device.UserInfo
import com.android.tradefed.testtype.DeviceJUnit4ClassRunner
import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test
import com.google.common.truth.Truth.assertThat
import org.junit.AfterClass
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
import java.util.zip.GZIPOutputStream
@RunWith(DeviceJUnit4ClassRunner::class)
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
* users created specifically for this test.
*/
private const val USER_COUNT = 3
/**
* Whether to manually reset state at each test method without rebooting
* for faster iterative development.
*/
private const val DEBUG_NO_REBOOT = false
@get:ClassRule
val deviceRebootRule = SystemPreparer.TestRuleDelegate(true)
private val parentClassName = SystemStubMultiUserDisableUninstallTest::class.java.simpleName
private val deviceCompressedFile =
HostUtils.makePathForApk("$parentClassName.apk", Partition.PRODUCT).parent
.resolve("$parentClassName.apk.gz")
private val stubFile =
HostUtils.makePathForApk("$parentClassName-Stub.apk", Partition.PRODUCT)
private val secondaryUsers = mutableListOf<Int>()
private val usersToRemove = mutableListOf<Int>()
private var savedDevice: ITestDevice? = null
private var savedPreparer: SystemPreparer? = null
private fun setUpUsers(device: ITestDevice) {
if (this.savedDevice != null) return
this.savedDevice = device
secondaryUsers.clear()
secondaryUsers += device.userInfos.values.map(UserInfo::userId).filterNot { it == 0 }
while (secondaryUsers.size < USER_COUNT) {
secondaryUsers += device.createUser(parentClassName + secondaryUsers.size)
.also { usersToRemove += it }
}
}
@JvmStatic
@AfterClass
fun cleanUp() {
savedDevice ?: return
usersToRemove.forEach {
savedDevice?.removeUser(it)
}
savedDevice?.uninstallPackage(TEST_PKG_NAME)
savedDevice?.deleteFile(stubFile.parent.toString())
savedDevice?.deleteFile(deviceCompressedFile.parent.toString())
savedDevice?.reboot()
savedDevice = null
if (DEBUG_NO_REBOOT) {
savedPreparer?.after()
savedPreparer = null
}
}
}
private val tempFolder = TemporaryFolder()
private val preparer: SystemPreparer = SystemPreparer(tempFolder,
SystemPreparer.RebootStrategy.START_STOP, deviceRebootRule) { this.device }
@get:Rule
val rules = RuleChain.outerRule(tempFolder).let {
if (DEBUG_NO_REBOOT) {
it!!
} else {
it.around(preparer)!!
}
}
private var hostCompressedFile: File? = null
private val previousCodePaths = mutableListOf<String>()
@Before
fun ensureUserAndCompressStubAndInstall() {
setUpUsers(device)
val initialized = hostCompressedFile != null
if (!initialized) {
hostCompressedFile = tempFolder.newFile()
hostCompressedFile!!.outputStream().use {
javaClass.classLoader
.getResource(VERSION_ONE)!!
.openStream()
.use { input ->
GZIPOutputStream(it).use { output ->
input.copyTo(output)
}
}
}
}
device.uninstallPackage(TEST_PKG_NAME)
if (!initialized || !DEBUG_NO_REBOOT) {
savedPreparer = preparer
preparer.pushResourceFile(VERSION_STUB, stubFile.toString())
.pushFile(hostCompressedFile, deviceCompressedFile.toString())
.reboot()
}
// This test forces the state to installed/enabled for all users,
// since it only tests the uninstall/disable side.
installExisting(User.PRIMARY)
installExisting(User.SECONDARY)
ensureEnabled()
// Ensure data app isn't re-installed multiple times by comparing against the original path
val codePath = HostUtils.getCodePaths(device, TEST_PKG_NAME).first()
assertThat(codePath).contains("/data/app")
assertThat(codePath).contains(TEST_PKG_NAME)
previousCodePaths.clear()
previousCodePaths += codePath
assertState(
primaryInstalled = true, primaryEnabled = true,
secondaryInstalled = true, secondaryEnabled = true,
codePaths = listOf(CodePath.SAME, CodePath.SYSTEM)
)
}
@Test
fun disablePrimaryFirstAndUninstall() {
toggleEnabled(false, User.PRIMARY)
assertState(
primaryInstalled = true, primaryEnabled = false,
secondaryInstalled = true, secondaryEnabled = true,
codePaths = listOf(CodePath.SAME, CodePath.SYSTEM)
)
toggleEnabled(false, User.SECONDARY)
assertState(
primaryInstalled = true, primaryEnabled = false,
secondaryInstalled = true, secondaryEnabled = false,
codePaths = listOf(CodePath.SAME, CodePath.SYSTEM)
)
device.uninstallPackage(TEST_PKG_NAME)
assertState(
primaryInstalled = true, primaryEnabled = false,
secondaryInstalled = true, secondaryEnabled = false,
codePaths = listOf(CodePath.SYSTEM)
)
}
@Test
fun disableSecondaryFirstAndUninstall() {
toggleEnabled(false, User.SECONDARY)
assertState(
primaryInstalled = true, primaryEnabled = true,
secondaryInstalled = true, secondaryEnabled = false,
codePaths = listOf(CodePath.SAME, CodePath.SYSTEM)
)
toggleEnabled(false, User.PRIMARY)
assertState(
primaryInstalled = true, primaryEnabled = false,
secondaryInstalled = true, secondaryEnabled = false,
codePaths = listOf(CodePath.SAME, CodePath.SYSTEM)
)
device.uninstallPackage(TEST_PKG_NAME)
assertState(
primaryInstalled = true, primaryEnabled = false,
secondaryInstalled = true, secondaryEnabled = false,
codePaths = listOf(CodePath.SYSTEM)
)
}
@Test
fun disabledUninstalledEnablePrimaryFirst() {
toggleEnabled(false, User.PRIMARY)
toggleEnabled(false, User.SECONDARY)
device.uninstallPackage(TEST_PKG_NAME)
toggleEnabled(true, User.PRIMARY)
assertState(
primaryInstalled = true, primaryEnabled = true,
secondaryInstalled = true, secondaryEnabled = false,
codePaths = listOf(CodePath.DIFFERENT, CodePath.SYSTEM)
)
toggleEnabled(true, User.SECONDARY)
assertState(
primaryInstalled = true, primaryEnabled = true,
secondaryInstalled = true, secondaryEnabled = true,
codePaths = listOf(CodePath.SAME, CodePath.SYSTEM)
)
}
@Test
fun disabledUninstalledEnableSecondaryFirst() {
toggleEnabled(false, User.PRIMARY)
toggleEnabled(false, User.SECONDARY)
device.uninstallPackage(TEST_PKG_NAME)
toggleEnabled(true, User.SECONDARY)
assertState(
primaryInstalled = true, primaryEnabled = false,
secondaryInstalled = true, secondaryEnabled = true,
codePaths = listOf(CodePath.DIFFERENT, CodePath.SYSTEM)
)
toggleEnabled(true, User.PRIMARY)
assertState(
primaryInstalled = true, primaryEnabled = true,
secondaryInstalled = true, secondaryEnabled = true,
codePaths = listOf(CodePath.SAME, CodePath.SYSTEM)
)
}
@Test
fun uninstallPrimaryFirstByUserAndInstallExistingPrimaryFirst() {
uninstall(User.PRIMARY)
assertState(
primaryInstalled = false, primaryEnabled = true,
secondaryInstalled = true, secondaryEnabled = true,
codePaths = listOf(CodePath.DIFFERENT, CodePath.SYSTEM)
)
uninstall(User.SECONDARY)
assertState(
primaryInstalled = false, primaryEnabled = true,
secondaryInstalled = false, secondaryEnabled = true,
codePaths = listOf(CodePath.DIFFERENT, CodePath.SYSTEM)
)
installExisting(User.PRIMARY)
assertState(
primaryInstalled = true, primaryEnabled = true,
secondaryInstalled = false, secondaryEnabled = true,
codePaths = listOf(CodePath.SAME, CodePath.SYSTEM)
)
installExisting(User.SECONDARY)
assertState(
primaryInstalled = true, primaryEnabled = true,
secondaryInstalled = true, secondaryEnabled = true,
codePaths = listOf(CodePath.SAME, CodePath.SYSTEM)
)
}
@Test
fun uninstallSecondaryFirstByUserAndInstallExistingSecondaryFirst() {
uninstall(User.PRIMARY)
assertState(
primaryInstalled = false, primaryEnabled = true,
secondaryInstalled = true, secondaryEnabled = true,
codePaths = listOf(CodePath.DIFFERENT, CodePath.SYSTEM)
)
uninstall(User.SECONDARY)
assertState(
primaryInstalled = false, primaryEnabled = true,
secondaryInstalled = false, secondaryEnabled = true,
codePaths = listOf(CodePath.DIFFERENT, CodePath.SYSTEM)
)
installExisting(User.SECONDARY)
assertState(
primaryInstalled = false, primaryEnabled = true,
secondaryInstalled = true, secondaryEnabled = true,
codePaths = listOf(CodePath.SAME, CodePath.SYSTEM)
)
installExisting(User.PRIMARY)
assertState(
primaryInstalled = true, primaryEnabled = true,
secondaryInstalled = true, secondaryEnabled = true,
codePaths = listOf(CodePath.SAME, CodePath.SYSTEM)
)
}
@Test
fun uninstallUpdatesAndEnablePrimaryFirst() {
device.executeShellCommand("pm uninstall-system-updates $TEST_PKG_NAME")
// Uninstall-system-updates always disables system user 0
// TODO: Is this intentional? There is no user argument for this command.
assertState(
primaryInstalled = true, primaryEnabled = false,
secondaryInstalled = true, secondaryEnabled = true,
// If any user is enabled when uninstalling updates, /data is re-uncompressed
codePaths = listOf(CodePath.DIFFERENT, CodePath.SYSTEM)
)
toggleEnabled(true, User.PRIMARY)
assertState(
primaryInstalled = true, primaryEnabled = true,
secondaryInstalled = true, secondaryEnabled = true,
codePaths = listOf(CodePath.SAME, CodePath.SYSTEM)
)
// Test enabling secondary to ensure path does not change, even though it's already enabled
toggleEnabled(true, User.SECONDARY)
assertState(
primaryInstalled = true, primaryEnabled = true,
secondaryInstalled = true, secondaryEnabled = true,
codePaths = listOf(CodePath.SAME, CodePath.SYSTEM)
)
}
@Test
fun uninstallUpdatesAndEnableSecondaryFirst() {
device.executeShellCommand("pm uninstall-system-updates $TEST_PKG_NAME")
// Uninstall-system-updates always disables system user 0
assertState(
primaryInstalled = true, primaryEnabled = false,
secondaryInstalled = true, secondaryEnabled = true,
// If any user is enabled when uninstalling updates, /data is re-uncompressed
codePaths = listOf(CodePath.DIFFERENT, CodePath.SYSTEM)
)
toggleEnabled(true, User.SECONDARY)
assertState(
primaryInstalled = true, primaryEnabled = false,
secondaryInstalled = true, secondaryEnabled = true,
codePaths = listOf(CodePath.SAME, CodePath.SYSTEM)
)
toggleEnabled(true, User.PRIMARY)
assertState(
primaryInstalled = true, primaryEnabled = true,
secondaryInstalled = true, secondaryEnabled = true,
codePaths = listOf(CodePath.SAME, CodePath.SYSTEM)
)
}
@Test
fun disabledUninstallUpdatesAndEnablePrimaryFirst() {
toggleEnabled(false, User.PRIMARY)
toggleEnabled(false, User.SECONDARY)
device.executeShellCommand("pm uninstall-system-updates $TEST_PKG_NAME")
assertState(
primaryInstalled = true, primaryEnabled = false,
secondaryInstalled = true, secondaryEnabled = false,
codePaths = listOf(CodePath.SYSTEM)
)
toggleEnabled(true, User.PRIMARY)
assertState(
primaryInstalled = true, primaryEnabled = true,
secondaryInstalled = true, secondaryEnabled = false,
codePaths = listOf(CodePath.DIFFERENT, CodePath.SYSTEM)
)
toggleEnabled(true, User.SECONDARY)
assertState(
primaryInstalled = true, primaryEnabled = true,
secondaryInstalled = true, secondaryEnabled = true,
codePaths = listOf(CodePath.SAME, CodePath.SYSTEM)
)
}
@Test
fun disabledUninstallUpdatesAndEnableSecondaryFirst() {
toggleEnabled(false, User.PRIMARY)
toggleEnabled(false, User.SECONDARY)
device.executeShellCommand("pm uninstall-system-updates $TEST_PKG_NAME")
assertState(
primaryInstalled = true, primaryEnabled = false,
secondaryInstalled = true, secondaryEnabled = false,
codePaths = listOf(CodePath.SYSTEM)
)
toggleEnabled(true, User.SECONDARY)
assertState(
primaryInstalled = true, primaryEnabled = false,
secondaryInstalled = true, secondaryEnabled = true,
codePaths = listOf(CodePath.DIFFERENT, CodePath.SYSTEM)
)
toggleEnabled(true, User.PRIMARY)
assertState(
primaryInstalled = true, primaryEnabled = true,
secondaryInstalled = true, secondaryEnabled = true,
codePaths = listOf(CodePath.SAME, CodePath.SYSTEM)
)
}
@Test
fun uninstalledUninstallUpdatesAndEnablePrimaryFirst() {
uninstall(User.PRIMARY)
uninstall(User.SECONDARY)
device.executeShellCommand("pm uninstall-system-updates $TEST_PKG_NAME")
// Uninstall-system-updates always disables system user 0
assertState(
primaryInstalled = false, primaryEnabled = false,
secondaryInstalled = false, secondaryEnabled = true,
codePaths = listOf(CodePath.SYSTEM)
)
toggleEnabled(true, User.PRIMARY)
assertState(
primaryInstalled = false, primaryEnabled = true,
secondaryInstalled = false, secondaryEnabled = true,
codePaths = listOf(CodePath.DIFFERENT, CodePath.SYSTEM)
)
toggleEnabled(true, User.SECONDARY)
assertState(
primaryInstalled = false, primaryEnabled = true,
secondaryInstalled = false, secondaryEnabled = true,
codePaths = listOf(CodePath.SAME, CodePath.SYSTEM)
)
}
@Test
fun uninstalledUninstallUpdatesAndEnableSecondaryFirst() {
uninstall(User.PRIMARY)
uninstall(User.SECONDARY)
device.executeShellCommand("pm uninstall-system-updates $TEST_PKG_NAME")
// Uninstall-system-updates always disables system user 0
assertState(
primaryInstalled = false, primaryEnabled = false,
secondaryInstalled = false, secondaryEnabled = true,
codePaths = listOf(CodePath.SYSTEM)
)
toggleEnabled(true, User.SECONDARY)
assertState(
primaryInstalled = false, primaryEnabled = false,
secondaryInstalled = false, secondaryEnabled = true,
codePaths = listOf(CodePath.DIFFERENT, CodePath.SYSTEM)
)
toggleEnabled(true, User.PRIMARY)
assertState(
primaryInstalled = false, primaryEnabled = true,
secondaryInstalled = false, secondaryEnabled = true,
codePaths = listOf(CodePath.SAME, CodePath.SYSTEM)
)
}
private fun ensureEnabled() {
toggleEnabled(true, User.PRIMARY)
toggleEnabled(true, User.SECONDARY)
assertThat(HostUtils.getUserIdToPkgEnabledState(device, TEST_PKG_NAME).all { it.value })
.isTrue()
}
private fun toggleEnabled(enabled: Boolean, user: User, pkgName: String = TEST_PKG_NAME) {
val command = if (enabled) "enable" else "disable"
@Suppress("UNUSED_VARIABLE") val exhaust: Any = when (user) {
User.PRIMARY -> {
device.executeShellCommand("pm $command --user 0 $pkgName")
}
User.SECONDARY -> {
secondaryUsers.forEach {
device.executeShellCommand("pm $command --user $it $pkgName")
}
}
}
}
private fun uninstall(user: User, pkgName: String = TEST_PKG_NAME) {
@Suppress("UNUSED_VARIABLE") val exhaust: Any = when (user) {
User.PRIMARY -> {
device.executeShellCommand("pm uninstall --user 0 $pkgName")
}
User.SECONDARY -> {
secondaryUsers.forEach {
device.executeShellCommand("pm uninstall --user $it $pkgName")
}
}
}
}
private fun installExisting(user: User, pkgName: String = TEST_PKG_NAME) {
@Suppress("UNUSED_VARIABLE") val exhaust: Any = when (user) {
User.PRIMARY -> {
device.executeShellCommand("pm install-existing --user 0 $pkgName")
}
User.SECONDARY -> {
secondaryUsers.forEach {
device.executeShellCommand("pm install-existing --user $it $pkgName")
}
}
}
}
private fun assertState(
primaryInstalled: Boolean,
primaryEnabled: Boolean,
secondaryInstalled: Boolean,
secondaryEnabled: Boolean,
codePaths: List<CodePath>
) {
HostUtils.getUserIdToPkgInstalledState(device, TEST_PKG_NAME)
.forEach { (userId, installed) ->
if (userId == 0) {
assertThat(installed).isEqualTo(primaryInstalled)
} else {
assertThat(installed).isEqualTo(secondaryInstalled)
}
}
HostUtils.getUserIdToPkgEnabledState(device, TEST_PKG_NAME)
.forEach { (userId, enabled) ->
if (userId == 0) {
assertThat(enabled).isEqualTo(primaryEnabled)
} else {
assertThat(enabled).isEqualTo(secondaryEnabled)
}
}
assertCodePaths(codePaths.first(), codePaths.getOrNull(1))
}
private fun assertCodePaths(firstCodePath: CodePath, secondCodePath: CodePath? = null) {
val codePaths = HostUtils.getCodePaths(device, TEST_PKG_NAME)
assertThat(codePaths).hasSize(listOfNotNull(firstCodePath, secondCodePath).size)
when (firstCodePath) {
CodePath.SAME -> {
assertThat(codePaths[0]).contains("/data/app")
assertThat(codePaths[0]).contains(TEST_PKG_NAME)
assertThat(codePaths[0]).isEqualTo(previousCodePaths.last())
}
CodePath.DIFFERENT -> {
assertThat(codePaths[0]).contains("/data/app")
assertThat(codePaths[0]).contains(TEST_PKG_NAME)
assertThat(previousCodePaths).doesNotContain(codePaths[0])
previousCodePaths.add(codePaths[0])
}
CodePath.SYSTEM -> assertThat(codePaths[0]).isEqualTo(stubFile.parent.toString())
}
when (secondCodePath) {
CodePath.SAME, CodePath.DIFFERENT ->
throw AssertionError("secondDataPath cannot be a data path")
CodePath.SYSTEM -> assertThat(codePaths[1]).isEqualTo(stubFile.parent.toString())
}
}
enum class User {
/** The primary system user 0 */
PRIMARY,
/**
* All other users on the device that are not 0. This is split into an enum so that all
* methods that handle secondary act on all non-system users. Some behaviors only occur
* if a package state is marked for all non-primary users on the device, which can be
* more than just 1.
*/
SECONDARY
}
enum class CodePath {
/** The data code path hasn't changed */
SAME,
/** New data code path */
DIFFERENT,
/** The static system code path */
SYSTEM
}
}

View File

@@ -13,26 +13,32 @@
// limitations under the License.
android_test_helper_app {
name: "PackageManagerDummyAppVersion1",
name: "PackageManagerTestAppStub",
manifest: "AndroidManifestVersion1.xml",
srcs: []
}
android_test_helper_app {
name: "PackageManagerTestAppVersion1",
manifest: "AndroidManifestVersion1.xml"
}
android_test_helper_app {
name: "PackageManagerDummyAppVersion2",
name: "PackageManagerTestAppVersion2",
manifest: "AndroidManifestVersion2.xml"
}
android_test_helper_app {
name: "PackageManagerDummyAppVersion3",
name: "PackageManagerTestAppVersion3",
manifest: "AndroidManifestVersion3.xml"
}
android_test_helper_app {
name: "PackageManagerDummyAppVersion4",
name: "PackageManagerTestAppVersion4",
manifest: "AndroidManifestVersion4.xml"
}
android_test_helper_app {
name: "PackageManagerDummyAppOriginalOverride",
name: "PackageManagerTestAppOriginalOverride",
manifest: "AndroidManifestOriginalOverride.xml"
}

View File

@@ -16,10 +16,10 @@
-->
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.server.pm.test.dummy_app.override"
package="com.android.server.pm.test.test_app.override"
android:versionCode="2"
>
<original-package android:name="com.android.server.pm.test.dummy_app"/>
<original-package android:name="com.android.server.pm.test.test_app"/>
</manifest>

View File

@@ -16,12 +16,12 @@
-->
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.server.pm.test.dummy_app"
package="com.android.server.pm.test.test_app"
android:versionCode="1"
>
<permission
android:name="com.android.server.pm.test.dummy_app.TEST_PERMISSION"
android:name="com.android.server.pm.test.test_app.TEST_PERMISSION"
android:protectionLevel="normal"
/>

View File

@@ -16,12 +16,12 @@
-->
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.server.pm.test.dummy_app"
package="com.android.server.pm.test.test_app"
android:versionCode="2"
>
<permission
android:name="com.android.server.pm.test.dummy_app.TEST_PERMISSION"
android:name="com.android.server.pm.test.test_app.TEST_PERMISSION"
android:protectionLevel="normal"
/>

View File

@@ -16,12 +16,12 @@
-->
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.server.pm.test.dummy_app"
package="com.android.server.pm.test.test_app"
android:versionCode="3"
>
<permission
android:name="com.android.server.pm.test.dummy_app.TEST_PERMISSION"
android:name="com.android.server.pm.test.test_app.TEST_PERMISSION"
android:protectionLevel="normal"
/>

View File

@@ -16,12 +16,12 @@
-->
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.server.pm.test.dummy_app"
package="com.android.server.pm.test.test_app"
android:versionCode="4"
>
<permission
android:name="com.android.server.pm.test.dummy_app.TEST_PERMISSION"
android:name="com.android.server.pm.test.test_app.TEST_PERMISSION"
android:protectionLevel="normal"
/>

View File

@@ -34,6 +34,8 @@ import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import javax.annotation.Nullable;
@@ -49,7 +51,7 @@ import javax.annotation.Nullable;
public class SystemPreparer extends ExternalResource {
private static final long OVERLAY_ENABLE_TIMEOUT_MS = 30000;
// The paths of the files pushed onto the device through this rule.
// The paths of the files pushed onto the device through this rule to be removed after.
private ArrayList<String> mPushedFiles = new ArrayList<>();
// The package names of packages installed through this rule.
@@ -81,7 +83,7 @@ public class SystemPreparer extends ExternalResource {
final ITestDevice device = mDeviceProvider.getDevice();
remount();
assertTrue(device.pushFile(copyResourceToTemp(filePath), outputPath));
mPushedFiles.add(outputPath);
addPushedFile(device, outputPath);
return this;
}
@@ -91,10 +93,23 @@ public class SystemPreparer extends ExternalResource {
final ITestDevice device = mDeviceProvider.getDevice();
remount();
assertTrue(device.pushFile(file, outputPath));
mPushedFiles.add(outputPath);
addPushedFile(device, outputPath);
return this;
}
private void addPushedFile(ITestDevice device, String outputPath)
throws DeviceNotAvailableException {
Path pathCreated = Paths.get(outputPath);
// Find the top most parent that is new to the device
while (pathCreated.getParent() != null
&& !device.doesFileExist(pathCreated.getParent().toString())) {
pathCreated = pathCreated.getParent();
}
mPushedFiles.add(pathCreated.toString());
}
/** Deletes the given path from the device */
public SystemPreparer deleteFile(String file) throws DeviceNotAvailableException {
final ITestDevice device = mDeviceProvider.getDevice();
@@ -203,7 +218,7 @@ public class SystemPreparer extends ExternalResource {
/** Removes installed packages and files that were pushed to the device. */
@Override
protected void after() {
public void after() {
final ITestDevice device = mDeviceProvider.getDevice();
try {
remount();