Fix original-package support

This broke in P. Basic support was broken by a simple negation
issue with the logic that checks for the original package.

That, along with the suggestion in the bug to fix the renamed
package association means this should now work as expected,
carrying data over from a previous installed, differently named
package.

Bug: 131355130
Bug: 132749720
Bug: 111967720

Test: atest PackageManagerServiceHostTests

Change-Id: Ifc4c7af47c4b633cd27ba4a40b6baa0e27960d71
This commit is contained in:
Winson
2020-06-10 10:34:43 -07:00
parent a621dad0b6
commit af8399a682
17 changed files with 754 additions and 153 deletions

View File

@@ -19,6 +19,9 @@ java_test_host {
"tradefed",
"junit",
],
static_libs: [
"frameworks-base-hostutils",
],
test_suites: ["general-tests"],
java_resources: [
":com.android.overlaytest.overlaid",

View File

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

View File

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

View File

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

View File

@@ -69,6 +69,9 @@
"exclude-annotation": "androidx.test.filters.Suppress"
}
]
},
{
"name": "PackageManagerServiceHostTests"
}
],
"postsubmit": [

View File

@@ -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",
]
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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"
/>

View File

@@ -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"
/>

View File

@@ -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"
/>

View File

@@ -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",
],
}

View File

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