diff options
17 files changed, 754 insertions, 153 deletions
diff --git a/core/tests/overlaytests/remount/Android.bp b/core/tests/overlaytests/remount/Android.bp index 4e79a4574af4..939334c94312 100644 --- a/core/tests/overlaytests/remount/Android.bp +++ b/core/tests/overlaytests/remount/Android.bp @@ -19,6 +19,9 @@ java_test_host { "tradefed", "junit", ], + static_libs: [ + "frameworks-base-hostutils", + ], test_suites: ["general-tests"], java_resources: [ ":com.android.overlaytest.overlaid", diff --git a/core/tests/overlaytests/remount/src/com/android/overlaytest/remounted/OverlayRemountedTestBase.java b/core/tests/overlaytests/remount/src/com/android/overlaytest/remounted/OverlayRemountedTestBase.java index 14b5bf6eacba..1a39e20f9a2b 100644 --- a/core/tests/overlaytests/remount/src/com/android/overlaytest/remounted/OverlayRemountedTestBase.java +++ b/core/tests/overlaytests/remount/src/com/android/overlaytest/remounted/OverlayRemountedTestBase.java @@ -18,6 +18,7 @@ package com.android.overlaytest.remounted; import static org.junit.Assert.fail; +import com.android.internal.util.test.SystemPreparer; import com.android.tradefed.device.DeviceNotAvailableException; import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test; diff --git a/core/tests/overlaytests/remount/src/com/android/overlaytest/remounted/SystemPreparer.java b/core/tests/overlaytests/remount/src/com/android/overlaytest/remounted/SystemPreparer.java deleted file mode 100644 index bb72d0ee1d03..000000000000 --- a/core/tests/overlaytests/remount/src/com/android/overlaytest/remounted/SystemPreparer.java +++ /dev/null @@ -1,151 +0,0 @@ -/* - * Copyright (C) 2019 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.overlaytest.remounted; - -import static org.junit.Assert.assertTrue; - -import com.android.tradefed.device.DeviceNotAvailableException; -import com.android.tradefed.device.ITestDevice; - -import org.junit.Assert; -import org.junit.rules.ExternalResource; -import org.junit.rules.TemporaryFolder; - -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; - -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. - private ArrayList<String> mPushedFiles = new ArrayList<>(); - - // The package names of packages installed through this rule. - private ArrayList<String> mInstalledPackages = new ArrayList<>(); - - private final TemporaryFolder mHostTempFolder; - private final DeviceProvider mDeviceProvider; - - SystemPreparer(TemporaryFolder hostTempFolder, DeviceProvider deviceProvider) { - mHostTempFolder = hostTempFolder; - mDeviceProvider = deviceProvider; - } - - /** Copies a file within the host test jar to a path on device. */ - SystemPreparer pushResourceFile(String resourcePath, - String outputPath) throws DeviceNotAvailableException, IOException { - final ITestDevice device = mDeviceProvider.getDevice(); - remount(); - assertTrue(device.pushFile(copyResourceToTemp(resourcePath), outputPath)); - mPushedFiles.add(outputPath); - return this; - } - - /** Installs an APK within the host test jar onto the device. */ - SystemPreparer installResourceApk(String resourcePath, String packageName) - throws DeviceNotAvailableException, IOException { - final ITestDevice device = mDeviceProvider.getDevice(); - final File tmpFile = copyResourceToTemp(resourcePath); - final String result = device.installPackage(tmpFile, true /* reinstall */); - Assert.assertNull(result); - mInstalledPackages.add(packageName); - return this; - } - - /** Sets the enable state of an overlay package. */ - SystemPreparer setOverlayEnabled(String packageName, boolean enabled) - throws DeviceNotAvailableException { - final ITestDevice device = mDeviceProvider.getDevice(); - final String enable = enabled ? "enable" : "disable"; - - // Wait for the overlay to change its enabled state. - final long endMillis = System.currentTimeMillis() + OVERLAY_ENABLE_TIMEOUT_MS; - String result; - while (System.currentTimeMillis() <= endMillis) { - device.executeShellCommand(String.format("cmd overlay %s %s", enable, packageName)); - result = device.executeShellCommand("cmd overlay dump isenabled " - + packageName); - if (((enabled) ? "true\n" : "false\n").equals(result)) { - return this; - } - - try { - Thread.sleep(200); - } catch (InterruptedException ignore) { - } - } - - throw new IllegalStateException(String.format("Failed to %s overlay %s:\n%s", enable, - packageName, device.executeShellCommand("cmd overlay list"))); - } - - /** Restarts the device and waits until after boot is completed. */ - SystemPreparer reboot() throws DeviceNotAvailableException { - final ITestDevice device = mDeviceProvider.getDevice(); - device.reboot(); - return this; - } - - SystemPreparer remount() throws DeviceNotAvailableException { - mDeviceProvider.getDevice().executeAdbCommand("remount"); - return this; - } - - /** Copies a file within the host test jar to a temporary file on the host machine. */ - private File copyResourceToTemp(String resourcePath) throws IOException { - final File tempFile = mHostTempFolder.newFile(resourcePath); - final ClassLoader classLoader = getClass().getClassLoader(); - try (InputStream assetIs = classLoader.getResource(resourcePath).openStream(); - FileOutputStream assetOs = new FileOutputStream(tempFile)) { - if (assetIs == null) { - throw new IllegalStateException("Failed to find resource " + resourcePath); - } - - int b; - while ((b = assetIs.read()) >= 0) { - assetOs.write(b); - } - } - - return tempFile; - } - - /** Removes installed packages and files that were pushed to the device. */ - @Override - protected void after() { - final ITestDevice device = mDeviceProvider.getDevice(); - try { - remount(); - for (final String file : mPushedFiles) { - device.deleteFile(file); - } - for (final String packageName : mInstalledPackages) { - device.uninstallPackage(packageName); - } - device.reboot(); - } catch (DeviceNotAvailableException e) { - Assert.fail(e.toString()); - } - } - - interface DeviceProvider { - ITestDevice getDevice(); - } -} diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java index 766fae64f647..f67ac442858a 100644 --- a/services/core/java/com/android/server/pm/PackageManagerService.java +++ b/services/core/java/com/android/server/pm/PackageManagerService.java @@ -11053,7 +11053,7 @@ public class PackageManagerService extends IPackageManager.Stub } else { pkgSetting = result.pkgSetting; if (originalPkgSetting != null) { - mSettings.addRenamedPackageLPw(parsedPackage.getPackageName(), + mSettings.addRenamedPackageLPw(parsedPackage.getRealPackage(), originalPkgSetting.name); mTransferredPackages.add(originalPkgSetting.name); } @@ -11162,7 +11162,7 @@ public class PackageManagerService extends IPackageManager.Stub @GuardedBy("mLock") private @Nullable PackageSetting getOriginalPackageLocked(@NonNull AndroidPackage pkg, @Nullable String renamedPkgName) { - if (!isPackageRenamed(pkg, renamedPkgName)) { + if (isPackageRenamed(pkg, renamedPkgName)) { return null; } for (int i = ArrayUtils.size(pkg.getOriginalPackages()) - 1; i >= 0; --i) { diff --git a/services/core/java/com/android/server/pm/TEST_MAPPING b/services/core/java/com/android/server/pm/TEST_MAPPING index 2614076e9b6c..eb51cc3cd25c 100644 --- a/services/core/java/com/android/server/pm/TEST_MAPPING +++ b/services/core/java/com/android/server/pm/TEST_MAPPING @@ -69,6 +69,9 @@ "exclude-annotation": "androidx.test.filters.Suppress" } ] + }, + { + "name": "PackageManagerServiceHostTests" } ], "postsubmit": [ diff --git a/services/tests/PackageManagerServiceTests/host/Android.bp b/services/tests/PackageManagerServiceTests/host/Android.bp new file mode 100644 index 000000000000..dad001b52b15 --- /dev/null +++ b/services/tests/PackageManagerServiceTests/host/Android.bp @@ -0,0 +1,33 @@ +// 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. + +java_test_host { + name: "PackageManagerServiceHostTests", + srcs: ["src/**/*.kt"], + libs: [ + "tradefed", + "junit", + "truth-prebuilt", + ], + static_libs: [ + "frameworks-base-hostutils", + ], + test_suites: ["general-tests"], + java_resources: [ + ":PackageManagerDummyAppVersion1", + ":PackageManagerDummyAppVersion2", + ":PackageManagerDummyAppVersion3", + ":PackageManagerDummyAppOriginalOverride", + ] +} diff --git a/services/tests/PackageManagerServiceTests/host/AndroidTest.xml b/services/tests/PackageManagerServiceTests/host/AndroidTest.xml new file mode 100644 index 000000000000..dc8c8113fac2 --- /dev/null +++ b/services/tests/PackageManagerServiceTests/host/AndroidTest.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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. + --> + +<configuration description="Test module config for PackageManagerServiceHostTests"> + <option name="test-tag" value="PackageManagerServiceHostTests" /> + + <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer"/> + + <test class="com.android.tradefed.testtype.HostTest"> + <option name="jar" value="PackageManagerServiceHostTests.jar" /> + </test> +</configuration> diff --git a/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/HostUtils.kt b/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/HostUtils.kt new file mode 100644 index 000000000000..4927c45550b5 --- /dev/null +++ b/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/HostUtils.kt @@ -0,0 +1,46 @@ +/* + * 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 java.io.File + +internal fun SystemPreparer.pushApk(file: String, partition: Partition) = + pushResourceFile(file, HostUtils.makePathForApk(file, partition)) + +internal fun SystemPreparer.deleteApk(file: String, partition: Partition) = + deleteFile(partition.baseFolder.resolve(file.removeSuffix(".apk")).toString()) + +internal object HostUtils { + + fun getDataDir(device: ITestDevice, pkgName: String) = + device.executeShellCommand("dumpsys package $pkgName") + .lineSequence() + .map(String::trim) + .single { it.startsWith("dataDir=") } + .removePrefix("dataDir=") + + fun makePathForApk(fileName: String, partition: Partition) = + makePathForApk(File(fileName), partition) + + fun makePathForApk(file: File, partition: Partition) = + partition.baseFolder + .resolve(file.nameWithoutExtension) + .resolve(file.name) + .toString() +} diff --git a/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/OriginalPackageMigrationTest.kt b/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/OriginalPackageMigrationTest.kt new file mode 100644 index 000000000000..90494c591363 --- /dev/null +++ b/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/OriginalPackageMigrationTest.kt @@ -0,0 +1,98 @@ +/* + * 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.ClassRule +import org.junit.Rule +import org.junit.Test +import org.junit.rules.RuleChain +import org.junit.rules.TemporaryFolder +import org.junit.runner.RunWith + +@RunWith(DeviceJUnit4ClassRunner::class) +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" + + @get:ClassRule + val deviceRebootRule = SystemPreparer.TestRuleDelegate(true) + } + + private val tempFolder = TemporaryFolder() + private val preparer: SystemPreparer = SystemPreparer(tempFolder, + SystemPreparer.RebootStrategy.START_STOP, deviceRebootRule) { this.device } + + @get:Rule + val rules = RuleChain.outerRule(tempFolder).around(preparer)!! + + @Test + fun lowerVersion() { + runForApk(VERSION_ONE) + } + + @Test + fun sameVersion() { + runForApk(VERSION_TWO) + } + + @Test + fun higherVersion() { + runForApk(VERSION_THREE) + } + + // A bug was found where renamed the package during parsing was leading to an invalid version + // code check at scan time. A lower version package was being dropped after reboot. To test + // this, the override APK is defined as versionCode 2 and the original package is given + // versionCode 1, 2, and 3 from the other methods. + private fun runForApk(apk: String) { + preparer.pushApk(apk, Partition.SYSTEM) + .reboot() + + device.getAppPackageInfo(TEST_PKG_NAME).run { + assertThat(codePath).contains(apk.removeSuffix(".apk")) + } + + // Ensure data is preserved by writing to the original dataDir + val file = tempFolder.newFile().apply { writeText("Test") } + device.pushFile(file, "${HostUtils.getDataDir(device, TEST_PKG_NAME)}/files/test.txt") + + preparer.deleteApk(apk, Partition.SYSTEM) + .pushApk(NEW_PKG, Partition.SYSTEM) + .reboot() + + device.getAppPackageInfo(TEST_PKG_NAME) + .run { + assertThat(this.toString()).isNotEmpty() + assertThat(codePath) + .contains(NEW_PKG.removeSuffix(".apk")) + } + + // And then reading the data contents back + assertThat(device.pullFileContents( + "${HostUtils.getDataDir(device, TEST_PKG_NAME)}/files/test.txt")) + .isEqualTo("Test") + } +} diff --git a/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/Partition.kt b/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/Partition.kt new file mode 100644 index 000000000000..35192a73ceda --- /dev/null +++ b/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/Partition.kt @@ -0,0 +1,31 @@ +/* + * 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 java.nio.file.Path +import java.nio.file.Paths + +// Unfortunately no easy way to access PMS SystemPartitions, so mock them here +internal enum class Partition(val baseFolder: Path) { + SYSTEM("/system/app"), + VENDOR("/vendor/app"), + PRODUCT("/product/app"), + SYSTEM_EXT("/system_ext/app") + ; + + constructor(baseFolder: String) : this(Paths.get(baseFolder)) +} diff --git a/services/tests/PackageManagerServiceTests/host/test-apps/Android.bp b/services/tests/PackageManagerServiceTests/host/test-apps/Android.bp new file mode 100644 index 000000000000..9568faa7dfd0 --- /dev/null +++ b/services/tests/PackageManagerServiceTests/host/test-apps/Android.bp @@ -0,0 +1,33 @@ +// 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. + +android_test_helper_app { + name: "PackageManagerDummyAppVersion1", + manifest: "AndroidManifestVersion1.xml" +} + +android_test_helper_app { + name: "PackageManagerDummyAppVersion2", + manifest: "AndroidManifestVersion2.xml" +} + +android_test_helper_app { + name: "PackageManagerDummyAppVersion3", + manifest: "AndroidManifestVersion3.xml" +} + +android_test_helper_app { + name: "PackageManagerDummyAppOriginalOverride", + manifest: "AndroidManifestOriginalOverride.xml" +} diff --git a/services/tests/PackageManagerServiceTests/host/test-apps/AndroidManifestOriginalOverride.xml b/services/tests/PackageManagerServiceTests/host/test-apps/AndroidManifestOriginalOverride.xml new file mode 100644 index 000000000000..f16e1bc8a927 --- /dev/null +++ b/services/tests/PackageManagerServiceTests/host/test-apps/AndroidManifestOriginalOverride.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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. + --> +<manifest + xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.server.pm.test.dummy_app.override" + android:versionCode="2" + > + + <original-package android:name="com.android.server.pm.test.dummy_app"/> + +</manifest> diff --git a/services/tests/PackageManagerServiceTests/host/test-apps/AndroidManifestVersion1.xml b/services/tests/PackageManagerServiceTests/host/test-apps/AndroidManifestVersion1.xml new file mode 100644 index 000000000000..d772050d7fd0 --- /dev/null +++ b/services/tests/PackageManagerServiceTests/host/test-apps/AndroidManifestVersion1.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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. + --> +<manifest + xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.server.pm.test.dummy_app" + android:versionCode="1" + /> diff --git a/services/tests/PackageManagerServiceTests/host/test-apps/AndroidManifestVersion2.xml b/services/tests/PackageManagerServiceTests/host/test-apps/AndroidManifestVersion2.xml new file mode 100644 index 000000000000..53f836b222e6 --- /dev/null +++ b/services/tests/PackageManagerServiceTests/host/test-apps/AndroidManifestVersion2.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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. + --> +<manifest + xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.server.pm.test.dummy_app" + android:versionCode="2" + /> diff --git a/services/tests/PackageManagerServiceTests/host/test-apps/AndroidManifestVersion3.xml b/services/tests/PackageManagerServiceTests/host/test-apps/AndroidManifestVersion3.xml new file mode 100644 index 000000000000..90ca9d0ac02c --- /dev/null +++ b/services/tests/PackageManagerServiceTests/host/test-apps/AndroidManifestVersion3.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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. + --> +<manifest + xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.server.pm.test.dummy_app" + android:versionCode="3" + /> diff --git a/tests/utils/hostutils/Android.bp b/tests/utils/hostutils/Android.bp new file mode 100644 index 000000000000..c9ad70280aa6 --- /dev/null +++ b/tests/utils/hostutils/Android.bp @@ -0,0 +1,28 @@ +// 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. +// + +java_library_host { + name: "frameworks-base-hostutils", + + srcs: [ + "src/**/*.java", + "src/**/*.kt", + ], + + libs: [ + "tradefed", + "junit", + ], +} diff --git a/tests/utils/hostutils/src/com/android/internal/util/test/SystemPreparer.java b/tests/utils/hostutils/src/com/android/internal/util/test/SystemPreparer.java new file mode 100644 index 000000000000..6bd6985f9675 --- /dev/null +++ b/tests/utils/hostutils/src/com/android/internal/util/test/SystemPreparer.java @@ -0,0 +1,362 @@ +/* + * 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.internal.util.test; + +import static org.junit.Assert.assertTrue; + +import com.android.tradefed.device.DeviceNotAvailableException; +import com.android.tradefed.device.ITestDevice; +import com.android.tradefed.log.LogUtil; + +import org.junit.Assert; +import org.junit.ClassRule; +import org.junit.rules.ExternalResource; +import org.junit.rules.TemporaryFolder; +import org.junit.rules.TestRule; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; + +import javax.annotation.Nullable; + +/** + * Allows pushing files onto the device and various options for rebooting. Useful for installing + * APKs/files to system partitions which otherwise wouldn't be easily changed. + * + * It's strongly recommended to pass in a {@link ClassRule} annotated {@link TestRuleDelegate} to + * do a full reboot at the end of a test to ensure the device is in a valid state, assuming the + * default {@link RebootStrategy#FULL} isn't used. + */ +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. + private ArrayList<String> mPushedFiles = new ArrayList<>(); + + // The package names of packages installed through this rule. + private ArrayList<String> mInstalledPackages = new ArrayList<>(); + + private final TemporaryFolder mHostTempFolder; + private final DeviceProvider mDeviceProvider; + private final RebootStrategy mRebootStrategy; + private final TearDownRule mTearDownRule; + + public SystemPreparer(TemporaryFolder hostTempFolder, DeviceProvider deviceProvider) { + this(hostTempFolder, RebootStrategy.FULL, null, deviceProvider); + } + + public SystemPreparer(TemporaryFolder hostTempFolder, RebootStrategy rebootStrategy, + @Nullable TestRuleDelegate testRuleDelegate, DeviceProvider deviceProvider) { + mHostTempFolder = hostTempFolder; + mDeviceProvider = deviceProvider; + mRebootStrategy = rebootStrategy; + mTearDownRule = new TearDownRule(mDeviceProvider); + if (testRuleDelegate != null) { + testRuleDelegate.setDelegate(mTearDownRule); + } + } + + /** Copies a file within the host test jar to a path on device. */ + public SystemPreparer pushResourceFile(String filePath, String outputPath) + throws DeviceNotAvailableException, IOException { + final ITestDevice device = mDeviceProvider.getDevice(); + remount(); + assertTrue(device.pushFile(copyResourceToTemp(filePath), outputPath)); + mPushedFiles.add(outputPath); + return this; + } + + /** Copies a file directly from the host file system to a path on device. */ + public SystemPreparer pushFile(File file, String outputPath) + throws DeviceNotAvailableException { + final ITestDevice device = mDeviceProvider.getDevice(); + remount(); + assertTrue(device.pushFile(file, outputPath)); + mPushedFiles.add(outputPath); + return this; + } + + /** Deletes the given path from the device */ + public SystemPreparer deleteFile(String file) throws DeviceNotAvailableException { + final ITestDevice device = mDeviceProvider.getDevice(); + remount(); + device.deleteFile(file); + return this; + } + + /** Installs an APK within the host test jar onto the device. */ + public SystemPreparer installResourceApk(String resourcePath, String packageName) + throws DeviceNotAvailableException, IOException { + final ITestDevice device = mDeviceProvider.getDevice(); + final File tmpFile = copyResourceToTemp(resourcePath); + final String result = device.installPackage(tmpFile, true /* reinstall */); + Assert.assertNull(result); + mInstalledPackages.add(packageName); + return this; + } + + /** Sets the enable state of an overlay package. */ + public SystemPreparer setOverlayEnabled(String packageName, boolean enabled) + throws DeviceNotAvailableException { + final ITestDevice device = mDeviceProvider.getDevice(); + final String enable = enabled ? "enable" : "disable"; + + // Wait for the overlay to change its enabled state. + final long endMillis = System.currentTimeMillis() + OVERLAY_ENABLE_TIMEOUT_MS; + String result; + while (System.currentTimeMillis() <= endMillis) { + device.executeShellCommand(String.format("cmd overlay %s %s", enable, packageName)); + result = device.executeShellCommand("cmd overlay dump isenabled " + + packageName); + if (((enabled) ? "true\n" : "false\n").equals(result)) { + return this; + } + + try { + Thread.sleep(200); + } catch (InterruptedException ignore) { + } + } + + throw new IllegalStateException(String.format("Failed to %s overlay %s:\n%s", enable, + packageName, device.executeShellCommand("cmd overlay list"))); + } + + /** Restarts the device and waits until after boot is completed. */ + public SystemPreparer reboot() throws DeviceNotAvailableException { + ITestDevice device = mDeviceProvider.getDevice(); + switch (mRebootStrategy) { + case FULL: + device.reboot(); + break; + case UNTIL_ONLINE: + device.rebootUntilOnline(); + break; + case USERSPACE: + device.rebootUserspace(); + break; + case USERSPACE_UNTIL_ONLINE: + device.rebootUserspaceUntilOnline(); + break; + case START_STOP: + device.executeShellCommand("stop"); + device.executeShellCommand("start"); + ITestDevice.RecoveryMode cachedRecoveryMode = device.getRecoveryMode(); + device.setRecoveryMode(ITestDevice.RecoveryMode.ONLINE); + + if (device.isEncryptionSupported()) { + if (device.isDeviceEncrypted()) { + LogUtil.CLog.e("Device is encrypted after userspace reboot!"); + device.unlockDevice(); + } + } + + device.setRecoveryMode(cachedRecoveryMode); + device.waitForDeviceAvailable(); + break; + } + return this; + } + + public SystemPreparer remount() throws DeviceNotAvailableException { + mTearDownRule.remount(); + return this; + } + + /** Copies a file within the host test jar to a temporary file on the host machine. */ + private File copyResourceToTemp(String resourcePath) throws IOException { + final File tempFile = mHostTempFolder.newFile(); + final ClassLoader classLoader = getClass().getClassLoader(); + try (InputStream assetIs = classLoader.getResource(resourcePath).openStream(); + FileOutputStream assetOs = new FileOutputStream(tempFile)) { + if (assetIs == null) { + throw new IllegalStateException("Failed to find resource " + resourcePath); + } + + int b; + while ((b = assetIs.read()) >= 0) { + assetOs.write(b); + } + } + + return tempFile; + } + + /** Removes installed packages and files that were pushed to the device. */ + @Override + protected void after() { + final ITestDevice device = mDeviceProvider.getDevice(); + try { + remount(); + for (final String file : mPushedFiles) { + device.deleteFile(file); + } + for (final String packageName : mInstalledPackages) { + device.uninstallPackage(packageName); + } + reboot(); + } catch (DeviceNotAvailableException e) { + Assert.fail(e.toString()); + } + } + + /** + * A hacky workaround since {@link org.junit.AfterClass} and {@link ClassRule} require static + * members. Will defer assignment of the actual {@link TestRule} to execute until after any + * test case has been run. + * + * In effect, this makes the {@link ITestDevice} to be accessible after all test cases have + * been executed, allowing {@link ITestDevice#reboot()} to be used to fully restore the device. + */ + public static class TestRuleDelegate implements TestRule { + + private boolean mThrowOnNull; + + @Nullable + private TestRule mTestRule; + + public TestRuleDelegate(boolean throwOnNull) { + mThrowOnNull = throwOnNull; + } + + public void setDelegate(TestRule testRule) { + mTestRule = testRule; + } + + @Override + public Statement apply(Statement base, Description description) { + if (mTestRule == null) { + if (mThrowOnNull) { + throw new IllegalStateException("TestRule delegate was not set"); + } else { + return new Statement() { + @Override + public void evaluate() throws Throwable { + base.evaluate(); + } + }; + } + } + + Statement statement = mTestRule.apply(base, description); + mTestRule = null; + return statement; + } + } + + /** + * Forces a full reboot at the end of the test class to restore any device state. + */ + private static class TearDownRule extends ExternalResource { + + private DeviceProvider mDeviceProvider; + private boolean mInitialized; + private boolean mWasVerityEnabled; + private boolean mWasAdbRoot; + private boolean mIsVerityEnabled; + + TearDownRule(DeviceProvider deviceProvider) { + mDeviceProvider = deviceProvider; + } + + @Override + protected void before() { + // This method will never be run + } + + @Override + protected void after() { + try { + initialize(); + ITestDevice device = mDeviceProvider.getDevice(); + if (mWasVerityEnabled != mIsVerityEnabled) { + device.executeShellCommand( + mWasVerityEnabled ? "enable-verity" : "disable-verity"); + } + device.reboot(); + if (!mWasAdbRoot) { + device.disableAdbRoot(); + } + } catch (DeviceNotAvailableException e) { + Assert.fail(e.toString()); + } + } + + /** + * Remount is done inside this class so that the verity state can be tracked. + */ + public void remount() throws DeviceNotAvailableException { + initialize(); + ITestDevice device = mDeviceProvider.getDevice(); + device.enableAdbRoot(); + if (mIsVerityEnabled) { + mIsVerityEnabled = false; + device.executeShellCommand("disable-verity"); + device.reboot(); + } + device.executeShellCommand("remount"); + device.waitForDeviceAvailable(); + } + + private void initialize() throws DeviceNotAvailableException { + if (mInitialized) { + return; + } + mInitialized = true; + ITestDevice device = mDeviceProvider.getDevice(); + mWasAdbRoot = device.isAdbRoot(); + device.enableAdbRoot(); + String veritySystem = device.getProperty("partition.system.verified"); + String verityVendor = device.getProperty("partition.vendor.verified"); + mWasVerityEnabled = (veritySystem != null && !veritySystem.isEmpty()) + || (verityVendor != null && !verityVendor.isEmpty()); + mIsVerityEnabled = mWasVerityEnabled; + } + } + + public interface DeviceProvider { + ITestDevice getDevice(); + } + + /** + * How to reboot the device. Ordered from slowest to fastest. + */ + public enum RebootStrategy { + /** @see ITestDevice#reboot() */ + FULL, + + /** @see ITestDevice#rebootUntilOnline() () */ + UNTIL_ONLINE, + + /** @see ITestDevice#rebootUserspace() */ + USERSPACE, + + /** @see ITestDevice#rebootUserspaceUntilOnline() () */ + USERSPACE_UNTIL_ONLINE, + + /** + * Uses shell stop && start to "reboot" the device. May leave invalid state after each test. + * Whether this matters or not depends on what's being tested. + */ + START_STOP + } +} |