Snap for 8564071 from a35c388d6011ccf146abbfd1535448b364e71853 to mainline-resolv-release
Change-Id: If14c8d16937d6a346539b37646c3ded2ee66f7ad
diff --git a/PREUPLOAD.cfg b/PREUPLOAD.cfg
new file mode 100644
index 0000000..aff86f5
--- /dev/null
+++ b/PREUPLOAD.cfg
@@ -0,0 +1,5 @@
+[Builtin Hooks]
+commit_msg_changeid_field = true
+
+[Hook Scripts]
+checkstyle_hook = ${REPO_ROOT}/prebuilts/checkstyle/checkstyle.py --sha ${PREUPLOAD_COMMIT}
\ No newline at end of file
diff --git a/hostsidetests/packageinstaller/Android.bp b/hostsidetests/packageinstaller/Android.bp
new file mode 100644
index 0000000..b1bc038
--- /dev/null
+++ b/hostsidetests/packageinstaller/Android.bp
@@ -0,0 +1,34 @@
+// Copyright (C) 2021 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 {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_test_host {
+ name: "CtsRootPackageInstallerHostTestCases",
+ defaults: ["cts_defaults"],
+ srcs: ["src/**/*.java"],
+ libs: ["cts-tradefed", "tradefed", "truth-prebuilt"],
+ data: [":CtsRootRollbackManagerHostTestHelperApp"],
+ test_suites: ["cts_root", "general-tests"],
+}
+
+android_test_helper_app {
+ name: "CtsRootPackageInstallerTestCases",
+ srcs: ["app/src/**/*.java"],
+ static_libs: ["androidx.test.rules", "cts-install-lib"],
+ manifest : "app/AndroidManifest.xml",
+ test_suites: ["device-tests"],
+}
diff --git a/hostsidetests/packageinstaller/AndroidTest.xml b/hostsidetests/packageinstaller/AndroidTest.xml
new file mode 100644
index 0000000..10c8d0f
--- /dev/null
+++ b/hostsidetests/packageinstaller/AndroidTest.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 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="Runs PackageInstaller CTS root tests">
+ <option name="test-suite-tag" value="cts_root" />
+ <option name="config-descriptor:metadata" key="component" value="framework" />
+ <option name="config-descriptor:metadata" key="parameter" value="not_instant_app" />
+ <option name="config-descriptor:metadata" key="parameter" value="not_multi_abi" />
+ <option name="config-descriptor:metadata" key="parameter" value="secondary_user" />
+
+ <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+ <option name="cleanup-apks" value="true"/>
+ <option name="test-file-name" value="CtsRootPackageInstallerTestCases.apk"/>
+ </target_preparer>
+ <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
+ <option name="run-command" value="pm uninstall com.android.cts.install.lib.testapp.A" />
+ <option name="run-command" value="pm uninstall com.android.cts.install.lib.testapp.B" />
+ <option name="run-command" value="pm uninstall com.android.cts.install.lib.testapp.C" />
+ <option name="run-command" value="setprop persist.log.tag.PackageInstaller DEBUG" />
+ <option name="teardown-command" value="pm uninstall com.android.cts.install.lib.testapp.A" />
+ <option name="teardown-command" value="pm uninstall com.android.cts.install.lib.testapp.B" />
+ <option name="teardown-command" value="pm uninstall com.android.cts.install.lib.testapp.C" />
+ <option name="run-command" value="svc wifi disable" />
+ <option name="run-command" value="svc data disable" />
+ <option name="teardown-command" value="svc wifi enable" />
+ <option name="teardown-command" value="svc data enable" />
+ </target_preparer>
+ <target_preparer class="com.android.tradefed.targetprep.RebootTargetPreparer" />
+ <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer" />
+ <test class="com.android.tradefed.testtype.HostTest" >
+ <option name="class" value="com.android.cts_root.packageinstaller.host.SessionCleanUpHostTest" />
+ </test>
+</configuration>
diff --git a/hostsidetests/packageinstaller/OWNERS b/hostsidetests/packageinstaller/OWNERS
new file mode 100644
index 0000000..a6203db
--- /dev/null
+++ b/hostsidetests/packageinstaller/OWNERS
@@ -0,0 +1,4 @@
+# Bug component: 36137
+patb@google.com
+schfan@google.com
+wangchun@google.com
diff --git a/hostsidetests/packageinstaller/TEST_MAPPING b/hostsidetests/packageinstaller/TEST_MAPPING
new file mode 100644
index 0000000..0c983a2
--- /dev/null
+++ b/hostsidetests/packageinstaller/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+ "presubmit": [
+ {
+ "name": "CtsRootPackageInstallerHostTestCases"
+ }
+ ]
+}
diff --git a/hostsidetests/packageinstaller/app/AndroidManifest.xml b/hostsidetests/packageinstaller/app/AndroidManifest.xml
new file mode 100644
index 0000000..2f2f4a7
--- /dev/null
+++ b/hostsidetests/packageinstaller/app/AndroidManifest.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 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.cts_root.packageinstaller" >
+
+ <application>
+ <uses-library android:name="android.test.runner" />
+ </application>
+
+ <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="com.android.cts_root.packageinstaller"
+ android:label="PackageInstaller CTS root tests"/>
+
+</manifest>
diff --git a/hostsidetests/packageinstaller/app/src/com/android/cts_root/packageinstaller/SessionCleanUpTest.java b/hostsidetests/packageinstaller/app/src/com/android/cts_root/packageinstaller/SessionCleanUpTest.java
new file mode 100644
index 0000000..3946255
--- /dev/null
+++ b/hostsidetests/packageinstaller/app/src/com/android/cts_root/packageinstaller/SessionCleanUpTest.java
@@ -0,0 +1,297 @@
+/*
+ * Copyright (C) 2021 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.cts_root.packageinstaller;
+
+import static com.android.cts.install.lib.InstallUtils.getInstalledVersion;
+import static com.android.cts.install.lib.InstallUtils.openPackageInstallerSession;
+import static com.android.cts.install.lib.PackageInstallerSessionInfoSubject.assertThat;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.Manifest;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.pm.PackageInstaller;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.cts.install.lib.Install;
+import com.android.cts.install.lib.InstallUtils;
+import com.android.cts.install.lib.LocalIntentSender;
+import com.android.cts.install.lib.TestApp;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.function.Predicate;
+import java.util.function.Supplier;
+
+public class SessionCleanUpTest {
+ private static final int INSTALL_FORCE_PERMISSION_PROMPT = 0x00000400;
+ /**
+ * Time between repeated checks in {@link #retry}.
+ */
+ private static final long RETRY_CHECK_INTERVAL_MILLIS = 500;
+ /**
+ * Maximum number of checks in {@link #retry} before a timeout occurs.
+ */
+ private static final long RETRY_MAX_INTERVALS = 20;
+
+ @Before
+ public void setUp() {
+ InstrumentationRegistry.getInstrumentation().getUiAutomation()
+ .adoptShellPermissionIdentity(
+ Manifest.permission.CLEAR_APP_CACHE,
+ Manifest.permission.INSTALL_PACKAGES,
+ Manifest.permission.DELETE_PACKAGES);
+ }
+
+ @After
+ public void tearDown() {
+ InstrumentationRegistry.getInstrumentation().getUiAutomation()
+ .dropShellPermissionIdentity();
+ }
+
+ private static <T> T retry(Supplier<T> supplier, Predicate<T> predicate, String message)
+ throws InterruptedException {
+ for (int i = 0; i < RETRY_MAX_INTERVALS; i++) {
+ T result = supplier.get();
+ if (predicate.test(result)) {
+ return result;
+ }
+ Thread.sleep(RETRY_CHECK_INTERVAL_MILLIS);
+ }
+ throw new AssertionError(message);
+ }
+
+ private void assertSessionNotExists(int sessionId) throws Exception {
+ // The session is cleaned up asynchronously.
+ // Retry until the session no longer exists.
+ retry(() -> InstallUtils.getPackageInstaller().getSessionInfo(sessionId),
+ info -> info == null,
+ "Session " + sessionId + " not cleaned up");
+ }
+
+ @Test
+ public void testSessionCleanUp_Single_Success() throws Exception {
+ int sessionId = Install.single(TestApp.A1).commit();
+ assertThat(getInstalledVersion(TestApp.A)).isEqualTo(1);
+ assertSessionNotExists(sessionId);
+ }
+
+ @Test
+ public void testSessionCleanUp_Multi_Success() throws Exception {
+ int parentId = Install.multi(TestApp.A1, TestApp.B1).createSession();
+ try (PackageInstaller.Session parent = openPackageInstallerSession(parentId)) {
+ int[] childIds = parent.getChildSessionIds();
+ LocalIntentSender sender = new LocalIntentSender();
+ parent.commit(sender.getIntentSender());
+ InstallUtils.assertStatusSuccess(sender.getResult());
+ assertThat(getInstalledVersion(TestApp.A)).isEqualTo(1);
+ assertThat(getInstalledVersion(TestApp.B)).isEqualTo(1);
+ assertSessionNotExists(parentId);
+ for (int childId : childIds) {
+ assertSessionNotExists(childId);
+ }
+ }
+ }
+
+ @Test
+ public void testSessionCleanUp_Single_VerificationFailed() throws Exception {
+ Install.single(TestApp.A2).commit();
+ int sessionId = Install.single(TestApp.A1).createSession();
+ try (PackageInstaller.Session session = openPackageInstallerSession(sessionId)) {
+ LocalIntentSender sender = new LocalIntentSender();
+ session.commit(sender.getIntentSender());
+ InstallUtils.assertStatusFailure(sender.getResult());
+ assertSessionNotExists(sessionId);
+ }
+ }
+
+ @Test
+ public void testSessionCleanUp_Multi_VerificationFailed() throws Exception {
+ Install.single(TestApp.A2).commit();
+ int parentId = Install.multi(TestApp.A1, TestApp.B1).createSession();
+ try (PackageInstaller.Session parent = openPackageInstallerSession(parentId)) {
+ int[] childIds = parent.getChildSessionIds();
+ LocalIntentSender sender = new LocalIntentSender();
+ parent.commit(sender.getIntentSender());
+ InstallUtils.assertStatusFailure(sender.getResult());
+ assertSessionNotExists(parentId);
+ for (int childId : childIds) {
+ assertSessionNotExists(childId);
+ }
+ }
+ }
+
+ @Test
+ public void testSessionCleanUp_Single_ValidationFailed() throws Exception {
+ int sessionId = Install.single(TestApp.AIncompleteSplit).createSession();
+ try (PackageInstaller.Session session = openPackageInstallerSession(sessionId)) {
+ LocalIntentSender sender = new LocalIntentSender();
+ session.commit(sender.getIntentSender());
+ InstallUtils.assertStatusFailure(sender.getResult());
+ assertSessionNotExists(sessionId);
+ }
+ }
+
+ @Test
+ public void testSessionCleanUp_Multi_ValidationFailed() throws Exception {
+ int parentId = Install.multi(TestApp.AIncompleteSplit, TestApp.B1).createSession();
+ try (PackageInstaller.Session parent = openPackageInstallerSession(parentId)) {
+ int[] childIds = parent.getChildSessionIds();
+ LocalIntentSender sender = new LocalIntentSender();
+ parent.commit(sender.getIntentSender());
+ InstallUtils.assertStatusFailure(sender.getResult());
+ assertSessionNotExists(parentId);
+ for (int childId : childIds) {
+ assertSessionNotExists(childId);
+ }
+ }
+ }
+
+ @Test
+ public void testSessionCleanUp_Single_NoPermission() throws Exception {
+ int sessionId = Install.single(TestApp.A1)
+ .addInstallFlags(INSTALL_FORCE_PERMISSION_PROMPT).createSession();
+ try (PackageInstaller.Session session = openPackageInstallerSession(sessionId)) {
+ LocalIntentSender sender = new LocalIntentSender();
+ session.commit(sender.getIntentSender());
+ Intent intent = sender.getResult();
+ int status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS,
+ PackageInstaller.STATUS_FAILURE);
+ assertThat(status).isEqualTo(PackageInstaller.STATUS_PENDING_USER_ACTION);
+ int idNeedsUserAction = intent.getIntExtra(PackageInstaller.EXTRA_SESSION_ID, -1);
+ InstallUtils.getPackageInstaller().setPermissionsResult(idNeedsUserAction, false);
+ InstallUtils.assertStatusFailure(sender.getResult());
+ assertSessionNotExists(sessionId);
+ }
+ }
+
+ @Test
+ public void testSessionCleanUp_Multi_NoPermission() throws Exception {
+ int parentId = Install.multi(TestApp.A1, TestApp.B1)
+ .addInstallFlags(INSTALL_FORCE_PERMISSION_PROMPT).createSession();
+ try (PackageInstaller.Session parent = openPackageInstallerSession(parentId)) {
+ int[] childIds = parent.getChildSessionIds();
+ LocalIntentSender sender = new LocalIntentSender();
+ parent.commit(sender.getIntentSender());
+ Intent intent = sender.getResult();
+ int status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS,
+ PackageInstaller.STATUS_FAILURE);
+ assertThat(status).isEqualTo(PackageInstaller.STATUS_PENDING_USER_ACTION);
+ int idNeedsUserAction = intent.getIntExtra(PackageInstaller.EXTRA_SESSION_ID, -1);
+ InstallUtils.getPackageInstaller().setPermissionsResult(idNeedsUserAction, false);
+ InstallUtils.assertStatusFailure(sender.getResult());
+ assertSessionNotExists(parentId);
+ for (int childId : childIds) {
+ assertSessionNotExists(childId);
+ }
+ }
+ }
+
+ @Test
+ public void testSessionCleanUp_Single_Expire_Install() throws Exception {
+ int sessionId = Install.single(TestApp.A1).setStaged().commit();
+
+ Context context = InstrumentationRegistry.getInstrumentation().getContext();
+ SharedPreferences prefs = context.getSharedPreferences("test", 0);
+ prefs.edit().putInt("sessionId", sessionId).commit();
+ }
+
+ @Test
+ public void testSessionCleanUp_Single_Expire_VerifyInstall() throws Exception {
+ Context context = InstrumentationRegistry.getInstrumentation().getContext();
+ SharedPreferences prefs = context.getSharedPreferences("test", 0);
+ int sessionId = prefs.getInt("sessionId", -1);
+ assertThat(InstallUtils.getStagedSessionInfo(sessionId)).isStagedSessionApplied();
+ }
+
+ @Test
+ public void testSessionCleanUp_Single_Expire_CleanUp() throws Exception {
+ Context context = InstrumentationRegistry.getInstrumentation().getContext();
+ SharedPreferences prefs = context.getSharedPreferences("test", 0);
+ int sessionId = prefs.getInt("sessionId", -1);
+ assertSessionNotExists(sessionId);
+ }
+
+ @Test
+ public void testSessionCleanUp_Multi_Expire_Install() throws Exception {
+ int parentId = Install.multi(TestApp.A1, TestApp.B1).setStaged().commit();
+ int[] childIds;
+ try (PackageInstaller.Session parent = openPackageInstallerSession(parentId)) {
+ childIds = parent.getChildSessionIds();
+ }
+
+ Context context = InstrumentationRegistry.getInstrumentation().getContext();
+ SharedPreferences prefs = context.getSharedPreferences("test", 0);
+ prefs.edit().putInt("parentId", parentId).commit();
+ prefs.edit().putInt("childId1", childIds[0]).commit();
+ prefs.edit().putInt("childId2", childIds[1]).commit();
+ }
+
+ @Test
+ public void testSessionCleanUp_Multi_Expire_VerifyInstall() throws Exception {
+ Context context = InstrumentationRegistry.getInstrumentation().getContext();
+ SharedPreferences prefs = context.getSharedPreferences("test", 0);
+ int parentId = prefs.getInt("parentId", -1);
+ assertThat(InstallUtils.getStagedSessionInfo(parentId)).isStagedSessionApplied();
+ }
+
+ @Test
+ public void testSessionCleanUp_Multi_Expire_CleanUp() throws Exception {
+ Context context = InstrumentationRegistry.getInstrumentation().getContext();
+ SharedPreferences prefs = context.getSharedPreferences("test", 0);
+ int parentId = prefs.getInt("parentId", -1);
+ int childId1 = prefs.getInt("childId1", -1);
+ int childId2 = prefs.getInt("childId2", -1);
+ assertSessionNotExists(parentId);
+ assertSessionNotExists(childId1);
+ assertSessionNotExists(childId2);
+ }
+
+ @Test
+ public void testSessionCleanUp_LowStorage_Install() throws Exception {
+ int parentId = Install.multi(TestApp.A1, TestApp.B1).createSession();
+ int[] childIds;
+ try (PackageInstaller.Session parent = openPackageInstallerSession(parentId)) {
+ childIds = parent.getChildSessionIds();
+ }
+
+ Context context = InstrumentationRegistry.getInstrumentation().getContext();
+ SharedPreferences prefs = context.getSharedPreferences("test", 0);
+ prefs.edit().putInt("parentId", parentId).commit();
+ prefs.edit().putInt("childId1", childIds[0]).commit();
+ prefs.edit().putInt("childId2", childIds[1]).commit();
+ }
+
+ @Test
+ public void testSessionCleanUp_LowStorage_CleanUp() throws Exception {
+ Context context = InstrumentationRegistry.getInstrumentation().getContext();
+ // Pass Long.MAX_VALUE to ensure old sessions will be abandoned
+ context.getPackageManager().freeStorage(Long.MAX_VALUE, null);
+ SharedPreferences prefs = context.getSharedPreferences("test", 0);
+ int parentId = prefs.getInt("parentId", -1);
+ int childId1 = prefs.getInt("childId1", -1);
+ int childId2 = prefs.getInt("childId2", -1);
+ assertSessionNotExists(parentId);
+ assertSessionNotExists(childId1);
+ assertSessionNotExists(childId2);
+ }
+}
diff --git a/hostsidetests/packageinstaller/src/com/android/cts_root/packageinstaller/host/SessionCleanUpHostTest.java b/hostsidetests/packageinstaller/src/com/android/cts_root/packageinstaller/host/SessionCleanUpHostTest.java
new file mode 100644
index 0000000..5854da4
--- /dev/null
+++ b/hostsidetests/packageinstaller/src/com/android/cts_root/packageinstaller/host/SessionCleanUpHostTest.java
@@ -0,0 +1,235 @@
+/*
+ * Copyright (C) 2021 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.cts_root.packageinstaller.host;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.platform.test.annotations.LargeTest;
+
+import com.android.ddmlib.Log;
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
+import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestRule;
+import org.junit.runner.RunWith;
+import org.junit.runners.model.Statement;
+
+import java.time.Instant;
+import java.util.Date;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+/**
+ * Tests sessions are cleaned up (session id and staging files) when installation fails.
+ */
+@RunWith(DeviceJUnit4ClassRunner.class)
+public class SessionCleanUpHostTest extends BaseHostJUnit4Test {
+ private static final String TAG = "SessionCleanUpHostTest";
+ // Expiry time for staged sessions that have not changed state in this time
+ private static final long MAX_TIME_SINCE_UPDATE_MILLIS = TimeUnit.DAYS.toMillis(21);
+
+ /**
+ * Checks staging directories are deleted when installation fails.
+ */
+ @Rule
+ public TestRule mStagingDirectoryRule = (base, description) -> new Statement() {
+ @Override
+ public void evaluate() throws Throwable {
+ List<String> stagedBefore = getStagingDirectoriesForStagedSessions();
+ List<String> nonStagedBefore = getStagingDirectoriesForNonStagedSessions();
+ Log.d(TAG, "stagedBefore=" + stagedBefore);
+ Log.d(TAG, "nonStagedBefore=" + nonStagedBefore);
+
+ base.evaluate();
+
+ List<String> stagedAfter = getStagingDirectoriesForStagedSessions();
+ List<String> nonStagedAfter = getStagingDirectoriesForNonStagedSessions();
+ Log.d(TAG, "stagedAfter=" + stagedAfter);
+ Log.d(TAG, "nonStagedAfter=" + nonStagedAfter);
+
+ // stagedAfter will be a subset of stagedBefore if all staging directories created
+ // during tests are correctly deleted when installation fails
+ assertThat(stagedBefore).containsAtLeastElementsIn(stagedAfter);
+ assertThat(nonStagedBefore).containsAtLeastElementsIn(nonStagedAfter);
+ }
+ };
+
+ private void run(String method) throws Exception {
+ assertThat(runDeviceTests("com.android.cts_root.packageinstaller",
+ "com.android.cts_root.packageinstaller.SessionCleanUpTest",
+ method)).isTrue();
+ }
+
+ @Before
+ @After
+ public void cleanUp() throws Exception {
+ getDevice().uninstallPackage("com.android.cts.install.lib.testapp.A");
+ getDevice().uninstallPackage("com.android.cts.install.lib.testapp.B");
+ getDevice().uninstallPackage("com.android.cts.install.lib.testapp.C");
+ }
+
+ /**
+ * Tests a successful single-package session is cleaned up.
+ */
+ @Test
+ public void testSessionCleanUp_Single_Success() throws Exception {
+ run("testSessionCleanUp_Single_Success");
+ }
+
+ /**
+ * Tests a successful multi-package session is cleaned up.
+ */
+ @Test
+ public void testSessionCleanUp_Multi_Success() throws Exception {
+ run("testSessionCleanUp_Multi_Success");
+ }
+
+ /**
+ * Tests a single-package session is cleaned up when verification failed.
+ */
+ @Test
+ public void testSessionCleanUp_Single_VerificationFailed() throws Exception {
+ run("testSessionCleanUp_Single_VerificationFailed");
+ }
+
+ /**
+ * Tests a multi-package session is cleaned up when verification failed.
+ */
+ @Test
+ public void testSessionCleanUp_Multi_VerificationFailed() throws Exception {
+ run("testSessionCleanUp_Multi_VerificationFailed");
+ }
+
+ /**
+ * Tests a single-package session is cleanup up when validation failed.
+ */
+ @Test
+ public void testSessionCleanUp_Single_ValidationFailed() throws Exception {
+ run("testSessionCleanUp_Single_ValidationFailed");
+ }
+
+ /**
+ * Tests a multi-package session is cleaned up when validation failed.
+ */
+ @Test
+ public void testSessionCleanUp_Multi_ValidationFailed() throws Exception {
+ run("testSessionCleanUp_Multi_ValidationFailed");
+ }
+
+ /**
+ * Tests a single-package session is cleaned up when user rejected the permission.
+ */
+ @Test
+ public void testSessionCleanUp_Single_NoPermission() throws Exception {
+ run("testSessionCleanUp_Single_NoPermission");
+ }
+
+ /**
+ * Tests a multi-package session is cleaned up when user rejected the permission.
+ */
+ @Test
+ public void testSessionCleanUp_Multi_NoPermission() throws Exception {
+ run("testSessionCleanUp_Multi_NoPermission");
+ }
+
+ /**
+ * Tests a single-package session is cleaned up when it expired.
+ */
+ @LargeTest
+ @Ignore("b/217132609")
+ @Test
+ public void testSessionCleanUp_Single_Expire() throws Exception {
+ run("testSessionCleanUp_Single_Expire_Install");
+ getDevice().reboot();
+ run("testSessionCleanUp_Single_Expire_VerifyInstall");
+ expireSessions();
+ run("testSessionCleanUp_Single_Expire_CleanUp");
+ }
+
+ /**
+ * Tests a multi-package session is cleaned up when it expired.
+ */
+ @Ignore("b/217132609")
+ @Test
+ public void testSessionCleanUp_Multi_Expire() throws Exception {
+ run("testSessionCleanUp_Multi_Expire_Install");
+ getDevice().reboot();
+ run("testSessionCleanUp_Multi_Expire_VerifyInstall");
+ expireSessions();
+ run("testSessionCleanUp_Multi_Expire_CleanUp");
+ }
+
+ /**
+ * Tests sessions are cleaned up on low storage.
+ */
+ @Test
+ public void testSessionCleanUp_LowStorage() throws Exception {
+ Instant t1 = Instant.ofEpochMilli(getDevice().getDeviceDate());
+ Instant t2 = t1.plusMillis(TimeUnit.DAYS.toMillis(1));
+ try {
+ run("testSessionCleanUp_LowStorage_Install");
+ // Advance system clock to have old sessions
+ getDevice().setDate(Date.from(t2));
+ getDevice().executeShellCommand("am broadcast -a android.intent.action.TIME_SET");
+ // Run PackageManager#freeStorage to abandon old sessions
+ run("testSessionCleanUp_LowStorage_CleanUp");
+ } finally {
+ // Restore system clock
+ getDevice().setDate(Date.from(t1));
+ getDevice().executeShellCommand("am broadcast -a android.intent.action.TIME_SET");
+ }
+ }
+
+ private List<String> getStagingDirectoriesForNonStagedSessions() throws Exception {
+ return getStagingDirectories("/data/app", "vmdl\\d+.tmp");
+ }
+
+ private List<String> getStagingDirectoriesForStagedSessions() throws Exception {
+ return getStagingDirectories("/data/app-staging", "session_\\d+");
+ }
+
+ private List<String> getStagingDirectories(String baseDir, String pattern) throws Exception {
+ return getDevice().getFileEntry(baseDir).getChildren(false)
+ .stream().filter(entry -> entry.getName().matches(pattern))
+ .map(entry -> entry.getName())
+ .collect(Collectors.toList());
+ }
+
+ private void expireSessions() throws Exception {
+ Instant t1 = Instant.ofEpochMilli(getDevice().getDeviceDate());
+ Instant t2 = t1.plusMillis(MAX_TIME_SINCE_UPDATE_MILLIS);
+ try {
+ // Advance system clock by MAX_TIME_SINCE_UPDATE_MILLIS to expire the staged session
+ getDevice().setDate(Date.from(t2));
+ getDevice().executeShellCommand("am broadcast -a android.intent.action.TIME_SET");
+ // Restart system server to run expiration
+ getDevice().executeShellCommand("stop");
+ getDevice().executeShellCommand("start");
+ getDevice().waitForDeviceAvailable();
+ } finally {
+ // Restore system clock
+ getDevice().setDate(Date.from(t1));
+ getDevice().executeShellCommand("am broadcast -a android.intent.action.TIME_SET");
+ }
+ }
+}
diff --git a/hostsidetests/rollback/Android.bp b/hostsidetests/rollback/Android.bp
new file mode 100644
index 0000000..85ac53f
--- /dev/null
+++ b/hostsidetests/rollback/Android.bp
@@ -0,0 +1,36 @@
+// Copyright (C) 2021 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 {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_test_host {
+ name: "CtsRootRollbackManagerHostTestCases",
+ defaults: ["cts_defaults"],
+ srcs: ["src/**/*.java"],
+ libs: ["cts-tradefed", "cts-shim-host-lib", "tradefed", "truth-prebuilt"],
+ static_libs: ["cts-install-lib-host"],
+ data: [":CtsRootRollbackManagerHostTestHelperApp"],
+ test_suites: ["cts_root", "general-tests"],
+}
+
+android_test_helper_app {
+ name: "CtsRootRollbackManagerHostTestHelperApp",
+ srcs: ["app/src/**/*.java"],
+ static_libs: ["androidx.test.rules", "cts-rollback-lib", "cts-install-lib"],
+ manifest : "app/AndroidManifest.xml",
+ sdk_version: "test_current",
+ test_suites: ["device-tests"],
+}
diff --git a/hostsidetests/rollback/AndroidTest.xml b/hostsidetests/rollback/AndroidTest.xml
new file mode 100644
index 0000000..35e7cda
--- /dev/null
+++ b/hostsidetests/rollback/AndroidTest.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 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="Runs the RollbackManager host tests">
+ <option name="test-suite-tag" value="cts_root" />
+ <option name="config-descriptor:metadata" key="component" value="framework" />
+ <option name="config-descriptor:metadata" key="parameter" value="not_instant_app" />
+ <option name="config-descriptor:metadata" key="parameter" value="not_multi_abi" />
+ <option name="config-descriptor:metadata" key="parameter" value="not_secondary_user" />
+ <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+ <option name="cleanup-apks" value="true" />
+ <option name="test-file-name" value="CtsRootRollbackManagerHostTestHelperApp.apk" />
+ </target_preparer>
+ <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer" />
+ <test class="com.android.tradefed.testtype.HostTest" >
+ <option name="class" value="com.android.cts_root.rollback.host.RollbackManagerHostTest" />
+ </test>
+ <!-- Controller that will skip the module if a native bridge situation is detected -->
+ <!-- For example: module wants to run arm and device is x86 -->
+ <object type="module_controller" class="com.android.tradefed.testtype.suite.module.NativeBridgeModuleController" />
+</configuration>
diff --git a/hostsidetests/rollback/OWNERS b/hostsidetests/rollback/OWNERS
new file mode 100644
index 0000000..b8578eb
--- /dev/null
+++ b/hostsidetests/rollback/OWNERS
@@ -0,0 +1,5 @@
+# Bug component: 557916
+olilan@google.com
+wangchun@google.com
+gavincorkery@google.com
+*
diff --git a/hostsidetests/rollback/TEST_MAPPING b/hostsidetests/rollback/TEST_MAPPING
new file mode 100644
index 0000000..ac1e898
--- /dev/null
+++ b/hostsidetests/rollback/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+ "presubmit-large": [
+ {
+ "name": "CtsRootRollbackManagerHostTestCases"
+ }
+ ]
+}
diff --git a/hostsidetests/rollback/app/AndroidManifest.xml b/hostsidetests/rollback/app/AndroidManifest.xml
new file mode 100644
index 0000000..37b3939
--- /dev/null
+++ b/hostsidetests/rollback/app/AndroidManifest.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 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.cts_root.rollback.host.app" >
+
+ <queries>
+ <package android:name="com.android.cts.ctsshim" />
+ <package android:name="com.android.cts.priv.ctsshim" />
+ </queries>
+
+ <application>
+ <uses-library android:name="android.test.runner" />
+ </application>
+
+ <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="com.android.cts_root.rollback.host.app"
+ android:label="Helper for CTS-root host tests of RollbackManager"/>
+
+</manifest>
diff --git a/hostsidetests/rollback/app/src/com/android/cts_root/rollback/host/app/HostTestHelper.java b/hostsidetests/rollback/app/src/com/android/cts_root/rollback/host/app/HostTestHelper.java
new file mode 100644
index 0000000..7b5aa79
--- /dev/null
+++ b/hostsidetests/rollback/app/src/com/android/cts_root/rollback/host/app/HostTestHelper.java
@@ -0,0 +1,266 @@
+/*
+ * Copyright (C) 2021 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.cts_root.rollback.host.app;
+
+import static com.android.cts.rollback.lib.RollbackInfoSubject.assertThat;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.Manifest;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.pm.PackageInstaller;
+import android.content.pm.PackageManager;
+import android.content.rollback.RollbackInfo;
+import android.content.rollback.RollbackManager;
+import android.os.storage.StorageManager;
+import android.provider.DeviceConfig;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.cts.install.lib.Install;
+import com.android.cts.install.lib.InstallUtils;
+import com.android.cts.install.lib.TestApp;
+import com.android.cts.rollback.lib.Rollback;
+import com.android.cts.rollback.lib.RollbackUtils;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * On-device helper test methods used for host-driven rollback tests.
+ */
+@RunWith(JUnit4.class)
+public class HostTestHelper {
+ private static final String PROPERTY_WATCHDOG_TRIGGER_FAILURE_COUNT =
+ "watchdog_trigger_failure_count";
+
+ @Before
+ public void setup() {
+ InstallUtils.adoptShellPermissionIdentity(
+ Manifest.permission.INSTALL_PACKAGES,
+ Manifest.permission.DELETE_PACKAGES,
+ Manifest.permission.TEST_MANAGE_ROLLBACKS,
+ Manifest.permission.FORCE_STOP_PACKAGES,
+ Manifest.permission.WRITE_DEVICE_CONFIG);
+ }
+
+ @After
+ public void teardown() {
+ InstallUtils.dropShellPermissionIdentity();
+ }
+
+ @Test
+ public void cleanUp() {
+ // Remove all pending rollbacks
+ RollbackManager rm = RollbackUtils.getRollbackManager();
+ rm.getAvailableRollbacks().stream().flatMap(info -> info.getPackages().stream())
+ .map(info -> info.getPackageName()).forEach(rm::expireRollbackForPackage);
+ }
+
+ @Test
+ public void testRollbackDataPolicy_Phase1_Install() throws Exception {
+ Install.multi(TestApp.A1, TestApp.B1, TestApp.C1).commit();
+ // Write user data version = 1
+ InstallUtils.processUserData(TestApp.A);
+ InstallUtils.processUserData(TestApp.B);
+ InstallUtils.processUserData(TestApp.C);
+
+ Install a2 = Install.single(TestApp.A2).setStaged()
+ .setEnableRollback(PackageManager.ROLLBACK_DATA_POLICY_WIPE);
+ Install b2 = Install.single(TestApp.B2).setStaged()
+ .setEnableRollback(PackageManager.ROLLBACK_DATA_POLICY_RESTORE);
+ Install c2 = Install.single(TestApp.C2).setStaged()
+ .setEnableRollback(PackageManager.ROLLBACK_DATA_POLICY_RETAIN);
+ Install.multi(a2, b2, c2).setEnableRollback().setStaged().commit();
+ }
+
+ @Test
+ public void testRollbackDataPolicy_Phase2_Rollback() throws Exception {
+ assertThat(InstallUtils.getInstalledVersion(TestApp.A)).isEqualTo(2);
+ assertThat(InstallUtils.getInstalledVersion(TestApp.B)).isEqualTo(2);
+ // Write user data version = 2
+ InstallUtils.processUserData(TestApp.A);
+ InstallUtils.processUserData(TestApp.B);
+ InstallUtils.processUserData(TestApp.C);
+
+ RollbackInfo info = RollbackUtils.getAvailableRollback(TestApp.A);
+ RollbackUtils.rollback(info.getRollbackId());
+ }
+
+ @Test
+ public void testRollbackDataPolicy_Phase3_VerifyRollback() throws Exception {
+ assertThat(InstallUtils.getInstalledVersion(TestApp.A)).isEqualTo(1);
+ assertThat(InstallUtils.getInstalledVersion(TestApp.B)).isEqualTo(1);
+ assertThat(InstallUtils.getInstalledVersion(TestApp.C)).isEqualTo(1);
+ // Read user data version from userdata.txt
+ // A's user data version is -1 for user data is wiped.
+ // B's user data version is 1 for user data is restored.
+ // C's user data version is 2 for user data is retained.
+ assertThat(InstallUtils.getUserDataVersion(TestApp.A)).isEqualTo(-1);
+ assertThat(InstallUtils.getUserDataVersion(TestApp.B)).isEqualTo(1);
+ assertThat(InstallUtils.getUserDataVersion(TestApp.C)).isEqualTo(2);
+ }
+
+ @Test
+ public void testRollbackApkDataDirectories_Phase1_InstallV1() throws Exception {
+ Install.single(TestApp.A1).commit();
+ }
+
+ @Test
+ public void testRollbackApkDataDirectories_Phase2_InstallV2() throws Exception {
+ Install.single(TestApp.A2).setStaged().setEnableRollback().commit();
+ }
+
+ @Test
+ public void testRollbackApkDataDirectories_Phase3_Rollback() throws Exception {
+ RollbackInfo available = RollbackUtils.getAvailableRollback(TestApp.A);
+ RollbackUtils.rollback(available.getRollbackId(), TestApp.A2);
+ }
+
+ @Test
+ public void testExpireSession_Phase1_Install() throws Exception {
+ Install.single(TestApp.A1).commit();
+ int sessionId = Install.single(TestApp.A2).setEnableRollback().setStaged().commit();
+
+ Context context = InstrumentationRegistry.getInstrumentation().getContext();
+ SharedPreferences prefs = context.getSharedPreferences("test", 0);
+ prefs.edit().putInt("sessionId", sessionId).commit();
+ }
+
+ @Test
+ public void testExpireSession_Phase2_VerifyInstall() throws Exception {
+ assertThat(InstallUtils.getInstalledVersion(TestApp.A)).isEqualTo(2);
+ RollbackInfo rollback = RollbackUtils.getAvailableRollback(TestApp.A);
+ assertThat(rollback).isNotNull();
+ }
+
+ @Test
+ public void testExpireSession_Phase3_VerifyRollback() throws Exception {
+ RollbackInfo rollback = RollbackUtils.getAvailableRollback(TestApp.A);
+ assertThat(rollback).isNotNull();
+
+ // Check the session is expired
+ Context context = InstrumentationRegistry.getInstrumentation().getContext();
+ SharedPreferences prefs = context.getSharedPreferences("test", 0);
+ int sessionId = prefs.getInt("sessionId", -1);
+ PackageInstaller.SessionInfo info = InstallUtils.getStagedSessionInfo(sessionId);
+ assertThat(info).isNull();
+ }
+
+ @Test
+ public void testRollbackApexDataDirectories_Phase1_Install() throws Exception {
+ Install.single(TestApp.Apex2).setStaged().setEnableRollback().commit();
+ }
+
+ @Test
+ public void testBadApkOnly_Phase1_Install() throws Exception {
+ Install.single(TestApp.A1).commit();
+ Install.single(TestApp.ACrashing2).setEnableRollback().setStaged().commit();
+ }
+
+ @Test
+ public void testBadApkOnly_Phase2_VerifyInstall() throws Exception {
+ assertThat(InstallUtils.getInstalledVersion(TestApp.A)).isEqualTo(2);
+
+ RollbackInfo rollback = RollbackUtils.getAvailableRollback(TestApp.A);
+ assertThat(rollback).isNotNull();
+ assertThat(rollback).packagesContainsExactly(Rollback.from(TestApp.A2).to(TestApp.A1));
+ assertThat(rollback.isStaged()).isTrue();
+
+ DeviceConfig.setProperty(DeviceConfig.NAMESPACE_ROLLBACK,
+ PROPERTY_WATCHDOG_TRIGGER_FAILURE_COUNT,
+ Integer.toString(5), false);
+ RollbackUtils.sendCrashBroadcast(TestApp.A, 4);
+ // Sleep for a while to make sure we don't trigger rollback
+ Thread.sleep(TimeUnit.SECONDS.toMillis(30));
+ }
+
+ @Test
+ public void testBadApkOnly_Phase3_VerifyRollback() throws Exception {
+ assertThat(InstallUtils.getInstalledVersion(TestApp.A)).isEqualTo(1);
+
+ RollbackInfo rollback = RollbackUtils.getCommittedRollback(TestApp.A);
+ assertThat(rollback).isNotNull();
+ assertThat(rollback).packagesContainsExactly(Rollback.from(TestApp.A2).to(TestApp.A1));
+ assertThat(rollback).causePackagesContainsExactly(TestApp.ACrashing2);
+ assertThat(rollback).isStaged();
+ assertThat(rollback.getCommittedSessionId()).isNotEqualTo(-1);
+ }
+
+ @Test
+ public void testNativeWatchdogTriggersRollback_Phase1_Install() throws Exception {
+ Install.single(TestApp.A1).commit();
+ Install.single(TestApp.A2).setEnableRollback().setStaged().commit();
+ }
+
+ @Test
+ public void testNativeWatchdogTriggersRollback_Phase2_VerifyInstall() throws Exception {
+ assertThat(InstallUtils.getInstalledVersion(TestApp.A)).isEqualTo(2);
+ RollbackInfo rollback = RollbackUtils.getAvailableRollback(TestApp.A);
+ assertThat(rollback).isNotNull();
+ }
+
+ @Test
+ public void testNativeWatchdogTriggersRollback_Phase3_VerifyRollback() throws Exception {
+ assertThat(InstallUtils.getInstalledVersion(TestApp.A)).isEqualTo(1);
+ RollbackInfo rollback = RollbackUtils.getCommittedRollback(TestApp.A);
+ assertThat(rollback).isNotNull();
+ }
+
+ @Test
+ public void testNativeWatchdogTriggersRollbackForAll_Phase1_Install() throws Exception {
+ Install.single(TestApp.A1).commit();
+ Install.single(TestApp.B1).commit();
+ Install.single(TestApp.A2).setEnableRollback().setStaged().commit();
+ Install.single(TestApp.B2).setEnableRollback().setStaged().commit();
+ }
+
+ @Test
+ public void testNativeWatchdogTriggersRollbackForAll_Phase2_VerifyInstall() throws Exception {
+ assertThat(InstallUtils.getInstalledVersion(TestApp.A)).isEqualTo(2);
+ assertThat(InstallUtils.getInstalledVersion(TestApp.B)).isEqualTo(2);
+ RollbackInfo rollbackA = RollbackUtils.getAvailableRollback(TestApp.A);
+ RollbackInfo rollbackB = RollbackUtils.getAvailableRollback(TestApp.B);
+ assertThat(rollbackA).isNotNull();
+ assertThat(rollbackB).isNotNull();
+ assertThat(rollbackA.getRollbackId()).isNotEqualTo(rollbackB.getRollbackId());
+ }
+
+ @Test
+ public void testNativeWatchdogTriggersRollbackForAll_Phase3_VerifyRollback() throws Exception {
+ assertThat(InstallUtils.getInstalledVersion(TestApp.A)).isEqualTo(1);
+ assertThat(InstallUtils.getInstalledVersion(TestApp.B)).isEqualTo(1);
+ RollbackInfo rollbackA = RollbackUtils.getCommittedRollback(TestApp.A);
+ RollbackInfo rollbackB = RollbackUtils.getCommittedRollback(TestApp.B);
+ assertThat(rollbackA).isNotNull();
+ assertThat(rollbackB).isNotNull();
+ assertThat(rollbackA.getRollbackId()).isNotEqualTo(rollbackB.getRollbackId());
+ }
+
+ @Test
+ public void isCheckpointSupported() {
+ Context context = InstrumentationRegistry.getInstrumentation().getContext();
+ StorageManager sm = (StorageManager) context.getSystemService(Context.STORAGE_SERVICE);
+ assertThat(sm.isCheckpointSupported()).isTrue();
+ }
+}
diff --git a/hostsidetests/rollback/src/com/android/cts_root/rollback/host/RollbackManagerHostTest.java b/hostsidetests/rollback/src/com/android/cts_root/rollback/host/RollbackManagerHostTest.java
new file mode 100644
index 0000000..a23adf5
--- /dev/null
+++ b/hostsidetests/rollback/src/com/android/cts_root/rollback/host/RollbackManagerHostTest.java
@@ -0,0 +1,599 @@
+/*
+ * Copyright (C) 2021 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.cts_root.rollback.host;
+
+import static com.android.cts.shim.lib.ShimPackage.SHIM_APEX_PACKAGE_NAME;
+import static com.android.cts.shim.lib.ShimPackage.SHIM_PACKAGE_NAME;
+import static com.android.cts_root.rollback.host.WatchdogEventLogger.Subject.assertThat;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeTrue;
+
+import android.cts.install.lib.host.InstallUtilsHost;
+
+import com.android.ddmlib.Log;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.IFileEntry;
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
+import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.time.Instant;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+/**
+ * CTS-root host tests for RollbackManager APIs.
+ */
+@RunWith(DeviceJUnit4ClassRunner.class)
+public class RollbackManagerHostTest extends BaseHostJUnit4Test {
+ private static final String TAG = "RollbackManagerHostTest";
+
+ private static final int NATIVE_CRASHES_THRESHOLD = 5;
+
+ private static final String REASON_APP_CRASH = "REASON_APP_CRASH";
+ private static final String REASON_NATIVE_CRASH = "REASON_NATIVE_CRASH";
+ private static final String ROLLBACK_INITIATE = "ROLLBACK_INITIATE";
+ private static final String ROLLBACK_BOOT_TRIGGERED = "ROLLBACK_BOOT_TRIGGERED";
+ private static final String ROLLBACK_SUCCESS = "ROLLBACK_SUCCESS";
+
+ private static final String TESTAPP_A = "com.android.cts.install.lib.testapp.A";
+ private static final String TEST_SUBDIR = "/subdir/";
+ private static final String TEST_FILENAME_1 = "test_file.txt";
+ private static final String TEST_STRING_1 = "hello this is a test";
+ private static final String TEST_FILENAME_2 = "another_file.txt";
+ private static final String TEST_STRING_2 = "this is a different file";
+ private static final String TEST_FILENAME_3 = "also.xyz";
+ private static final String TEST_STRING_3 = "also\n a\n test\n string";
+ private static final String TEST_FILENAME_4 = "one_more.test";
+ private static final String TEST_STRING_4 = "once more unto the test";
+
+ // Expiry time for staged sessions that have not changed state in this time
+ private static final long MAX_TIME_SINCE_UPDATE_MILLIS = TimeUnit.DAYS.toMillis(21);
+
+ private final InstallUtilsHost mHostUtils = new InstallUtilsHost(this);
+ private WatchdogEventLogger mLogger = new WatchdogEventLogger();
+
+ private void run(String method) throws Exception {
+ assertThat(runDeviceTests("com.android.cts_root.rollback.host.app",
+ "com.android.cts_root.rollback.host.app.HostTestHelper",
+ method)).isTrue();
+ }
+
+ @Before
+ @After
+ public void cleanUp() throws Exception {
+ getDevice().enableAdbRoot();
+ getDevice().executeShellCommand("for i in $(pm list staged-sessions --only-sessionid "
+ + "--only-parent); do pm install-abandon $i; done");
+ getDevice().uninstallPackage("com.android.cts.install.lib.testapp.A");
+ getDevice().uninstallPackage("com.android.cts.install.lib.testapp.B");
+ getDevice().uninstallPackage("com.android.cts.install.lib.testapp.C");
+ run("cleanUp");
+ mHostUtils.uninstallShimApexIfNecessary();
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ getDevice().enableAdbRoot();
+ mLogger.start(getDevice());
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ getDevice().enableAdbRoot();
+ mLogger.stop();
+ }
+
+ /**
+ * Tests user data is restored according to the preset rollback data policy.
+ */
+ @Test
+ public void testRollbackDataPolicy() throws Exception {
+ List<String> before = getSnapshotDirectories("/data/misc_ce/0/rollback");
+
+ run("testRollbackDataPolicy_Phase1_Install");
+ getDevice().reboot();
+ run("testRollbackDataPolicy_Phase2_Rollback");
+ getDevice().reboot();
+ run("testRollbackDataPolicy_Phase3_VerifyRollback");
+
+ // Verify snapshots are deleted after restoration
+ List<String> after = getSnapshotDirectories("/data/misc_ce/0/rollback");
+ // Only check directories newly created during the test
+ after.removeAll(before);
+ // There should be only one /data/misc_ce/0/rollback/<rollbackId> created during test
+ assertThat(after).hasSize(1);
+ assertDirectoryIsEmpty(after.get(0));
+ }
+
+ /**
+ * Tests that userdata of apk-in-apex is restored when apex is rolled back.
+ */
+ @Test
+ public void testRollbackApkInApexDataDirectories_Ce() throws Exception {
+ // Push files to apk data directory
+ String oldFilePath1 = apkDataDirCe(
+ SHIM_PACKAGE_NAME, 0) + "/" + TEST_FILENAME_1;
+ String oldFilePath2 = apkDataDirCe(
+ SHIM_PACKAGE_NAME, 0) + TEST_SUBDIR + TEST_FILENAME_2;
+ pushString(TEST_STRING_1, oldFilePath1);
+ pushString(TEST_STRING_2, oldFilePath2);
+
+ // Install new version of the APEX with rollback enabled
+ run("testRollbackApexDataDirectories_Phase1_Install");
+ getDevice().reboot();
+
+ // Replace files in data directory
+ String newFilePath3 = apkDataDirCe(
+ SHIM_PACKAGE_NAME, 0) + "/" + TEST_FILENAME_3;
+ String newFilePath4 = apkDataDirCe(
+ SHIM_PACKAGE_NAME, 0) + TEST_SUBDIR + TEST_FILENAME_4;
+ getDevice().deleteFile(oldFilePath1);
+ getDevice().deleteFile(oldFilePath2);
+ pushString(TEST_STRING_3, newFilePath3);
+ pushString(TEST_STRING_4, newFilePath4);
+
+ // Roll back the APEX
+ getDevice().executeShellCommand("pm rollback-app " + SHIM_APEX_PACKAGE_NAME);
+ getDevice().reboot();
+
+ // Verify that old files have been restored and new files are gone
+ assertFileContents(TEST_STRING_1, oldFilePath1);
+ assertFileContents(TEST_STRING_2, oldFilePath2);
+ assertFileNotExists(newFilePath3);
+ assertFileNotExists(newFilePath4);
+ }
+
+ /**
+ * Tests that data in DE apk data directory is restored when apk is rolled back.
+ */
+ @Test
+ public void testRollbackApkDataDirectories_De() throws Exception {
+ // Install version 1 of TESTAPP_A
+ run("testRollbackApkDataDirectories_Phase1_InstallV1");
+
+ // Push files to apk data directory
+ String oldFilePath1 = apkDataDirDe(TESTAPP_A, 0) + "/" + TEST_FILENAME_1;
+ String oldFilePath2 = apkDataDirDe(TESTAPP_A, 0) + TEST_SUBDIR + TEST_FILENAME_2;
+ pushString(TEST_STRING_1, oldFilePath1);
+ pushString(TEST_STRING_2, oldFilePath2);
+
+ // Install version 2 of TESTAPP_A with rollback enabled
+ run("testRollbackApkDataDirectories_Phase2_InstallV2");
+ getDevice().reboot();
+
+ // Replace files in data directory
+ String newFilePath3 = apkDataDirDe(TESTAPP_A, 0) + "/" + TEST_FILENAME_3;
+ String newFilePath4 = apkDataDirDe(TESTAPP_A, 0) + TEST_SUBDIR + TEST_FILENAME_4;
+ getDevice().deleteFile(oldFilePath1);
+ getDevice().deleteFile(oldFilePath2);
+ pushString(TEST_STRING_3, newFilePath3);
+ pushString(TEST_STRING_4, newFilePath4);
+
+ // Roll back the APK
+ run("testRollbackApkDataDirectories_Phase3_Rollback");
+ getDevice().reboot();
+
+ // Verify that old files have been restored and new files are gone
+ assertFileContents(TEST_STRING_1, oldFilePath1);
+ assertFileContents(TEST_STRING_2, oldFilePath2);
+ assertFileNotExists(newFilePath3);
+ assertFileNotExists(newFilePath4);
+ }
+
+ /**
+ * Tests that data in CE apex data directory is restored when apex is rolled back.
+ */
+ @Test
+ public void testRollbackApexDataDirectories_Ce() throws Exception {
+ List<String> before = getSnapshotDirectories("/data/misc_ce/0/apexrollback");
+
+ // Push files to apex data directory
+ String oldFilePath1 = apexDataDirCe(
+ SHIM_APEX_PACKAGE_NAME, 0) + "/" + TEST_FILENAME_1;
+ String oldFilePath2 = apexDataDirCe(
+ SHIM_APEX_PACKAGE_NAME, 0) + TEST_SUBDIR + TEST_FILENAME_2;
+ pushString(TEST_STRING_1, oldFilePath1);
+ pushString(TEST_STRING_2, oldFilePath2);
+
+ // Install new version of the APEX with rollback enabled
+ run("testRollbackApexDataDirectories_Phase1_Install");
+ getDevice().reboot();
+
+ // Replace files in data directory
+ String newFilePath3 = apexDataDirCe(
+ SHIM_APEX_PACKAGE_NAME, 0) + "/" + TEST_FILENAME_3;
+ String newFilePath4 = apexDataDirCe(
+ SHIM_APEX_PACKAGE_NAME, 0) + TEST_SUBDIR + TEST_FILENAME_4;
+ getDevice().deleteFile(oldFilePath1);
+ getDevice().deleteFile(oldFilePath2);
+ pushString(TEST_STRING_3, newFilePath3);
+ pushString(TEST_STRING_4, newFilePath4);
+
+ // Roll back the APEX
+ getDevice().executeShellCommand("pm rollback-app " + SHIM_APEX_PACKAGE_NAME);
+ getDevice().reboot();
+
+ // Verify that old files have been restored and new files are gone
+ assertFileContents(TEST_STRING_1, oldFilePath1);
+ assertFileContents(TEST_STRING_2, oldFilePath2);
+ assertFileNotExists(newFilePath3);
+ assertFileNotExists(newFilePath4);
+
+ // Verify snapshots are deleted after restoration
+ List<String> after = getSnapshotDirectories("/data/misc_ce/0/apexrollback");
+ // Only check directories newly created during the test
+ after.removeAll(before);
+ // There should be only one /data/misc_ce/0/apexrollback/<rollbackId> created during test
+ assertThat(after).hasSize(1);
+ assertDirectoryIsEmpty(after.get(0));
+ }
+
+ /**
+ * Tests that data in DE (user) apex data directory is restored when apex is rolled back.
+ */
+ @Test
+ public void testRollbackApexDataDirectories_DeUser() throws Exception {
+ List<String> before = getSnapshotDirectories("/data/misc_de/0/apexrollback");
+
+ // Push files to apex data directory
+ String oldFilePath1 = apexDataDirDeUser(
+ SHIM_APEX_PACKAGE_NAME, 0) + "/" + TEST_FILENAME_1;
+ String oldFilePath2 = apexDataDirDeUser(
+ SHIM_APEX_PACKAGE_NAME, 0) + TEST_SUBDIR + TEST_FILENAME_2;
+ pushString(TEST_STRING_1, oldFilePath1);
+ pushString(TEST_STRING_2, oldFilePath2);
+
+ // Install new version of the APEX with rollback enabled
+ run("testRollbackApexDataDirectories_Phase1_Install");
+ getDevice().reboot();
+
+ // Replace files in data directory
+ String newFilePath3 = apexDataDirDeUser(
+ SHIM_APEX_PACKAGE_NAME, 0) + "/" + TEST_FILENAME_3;
+ String newFilePath4 = apexDataDirDeUser(
+ SHIM_APEX_PACKAGE_NAME, 0) + TEST_SUBDIR + TEST_FILENAME_4;
+ getDevice().deleteFile(oldFilePath1);
+ getDevice().deleteFile(oldFilePath2);
+ pushString(TEST_STRING_3, newFilePath3);
+ pushString(TEST_STRING_4, newFilePath4);
+
+ // Roll back the APEX
+ getDevice().executeShellCommand("pm rollback-app " + SHIM_APEX_PACKAGE_NAME);
+ getDevice().reboot();
+
+ // Verify that old files have been restored and new files are gone
+ assertFileContents(TEST_STRING_1, oldFilePath1);
+ assertFileContents(TEST_STRING_2, oldFilePath2);
+ assertFileNotExists(newFilePath3);
+ assertFileNotExists(newFilePath4);
+
+ // Verify snapshots are deleted after restoration
+ List<String> after = getSnapshotDirectories("/data/misc_de/0/apexrollback");
+ // Only check directories newly created during the test
+ after.removeAll(before);
+ // There should be only one /data/misc_de/0/apexrollback/<rollbackId> created during test
+ assertThat(after).hasSize(1);
+ assertDirectoryIsEmpty(after.get(0));
+ }
+
+ /**
+ * Tests that data in DE_sys apex data directory is restored when apex is rolled back.
+ */
+ @Test
+ public void testRollbackApexDataDirectories_DeSys() throws Exception {
+ List<String> before = getSnapshotDirectories("/data/misc/apexrollback");
+
+ // Push files to apex data directory
+ String oldFilePath1 = apexDataDirDeSys(
+ SHIM_APEX_PACKAGE_NAME) + "/" + TEST_FILENAME_1;
+ String oldFilePath2 = apexDataDirDeSys(
+ SHIM_APEX_PACKAGE_NAME) + TEST_SUBDIR + TEST_FILENAME_2;
+ pushString(TEST_STRING_1, oldFilePath1);
+ pushString(TEST_STRING_2, oldFilePath2);
+
+ // Install new version of the APEX with rollback enabled
+ run("testRollbackApexDataDirectories_Phase1_Install");
+ getDevice().reboot();
+
+ // Replace files in data directory
+ String newFilePath3 = apexDataDirDeSys(
+ SHIM_APEX_PACKAGE_NAME) + "/" + TEST_FILENAME_3;
+ String newFilePath4 = apexDataDirDeSys(
+ SHIM_APEX_PACKAGE_NAME) + TEST_SUBDIR + TEST_FILENAME_4;
+ getDevice().deleteFile(oldFilePath1);
+ getDevice().deleteFile(oldFilePath2);
+ pushString(TEST_STRING_3, newFilePath3);
+ pushString(TEST_STRING_4, newFilePath4);
+
+ // Roll back the APEX
+ getDevice().executeShellCommand("pm rollback-app " + SHIM_APEX_PACKAGE_NAME);
+ getDevice().reboot();
+
+ // Verify that old files have been restored and new files are gone
+ assertFileContents(TEST_STRING_1, oldFilePath1);
+ assertFileContents(TEST_STRING_2, oldFilePath2);
+ assertFileNotExists(newFilePath3);
+ assertFileNotExists(newFilePath4);
+
+ // Verify snapshots are deleted after restoration
+ List<String> after = getSnapshotDirectories("/data/misc/apexrollback");
+ // Only check directories newly created during the test
+ after.removeAll(before);
+ // There should be only one /data/misc/apexrollback/<rollbackId> created during test
+ assertThat(after).hasSize(1);
+ assertDirectoryIsEmpty(after.get(0));
+ }
+
+ /**
+ * Tests that apex CE snapshots are deleted when its rollback is deleted.
+ */
+ @Test
+ public void testExpireApexRollback() throws Exception {
+ List<String> before = getSnapshotDirectories("/data/misc_ce/0/apexrollback");
+
+ // Push files to apex data directory
+ String oldFilePath1 = apexDataDirCe(
+ SHIM_APEX_PACKAGE_NAME, 0) + "/" + TEST_FILENAME_1;
+ String oldFilePath2 = apexDataDirCe(
+ SHIM_APEX_PACKAGE_NAME, 0) + TEST_SUBDIR + TEST_FILENAME_2;
+ pushString(TEST_STRING_1, oldFilePath1);
+ pushString(TEST_STRING_2, oldFilePath2);
+
+ // Install new version of the APEX with rollback enabled
+ run("testRollbackApexDataDirectories_Phase1_Install");
+ getDevice().reboot();
+
+ List<String> after = getSnapshotDirectories("/data/misc_ce/0/apexrollback");
+ // Only check directories newly created during the test
+ after.removeAll(before);
+ // There should be only one /data/misc_ce/0/apexrollback/<rollbackId> created during test
+ assertThat(after).hasSize(1);
+ // Expire all rollbacks and check CE snapshot directories are deleted
+ run("cleanUp");
+ assertFileNotExists(after.get(0));
+ }
+
+ /**
+ * Tests an available rollback shouldn't be deleted when its session expires.
+ */
+ @Test
+ public void testExpireSession() throws Exception {
+ run("testExpireSession_Phase1_Install");
+ getDevice().reboot();
+ run("testExpireSession_Phase2_VerifyInstall");
+
+ // Advance system clock by MAX_TIME_SINCE_UPDATE_MILLIS to expire the staged session
+ Instant t1 = Instant.ofEpochMilli(getDevice().getDeviceDate());
+ Instant t2 = t1.plusMillis(MAX_TIME_SINCE_UPDATE_MILLIS);
+
+ try {
+ getDevice().setDate(Date.from(t2));
+ // Send the broadcast to ensure the time change is properly propagated
+ getDevice().executeShellCommand("am broadcast -a android.intent.action.TIME_SET");
+ // TODO(b/197298469): Time change will be lost after reboot and sessions won't be
+ // expired correctly. We restart system server so sessions will be reloaded and expired
+ // as a workaround.
+ getDevice().executeShellCommand("stop");
+ getDevice().executeShellCommand("start");
+ getDevice().waitForDeviceAvailable();
+ run("testExpireSession_Phase3_VerifyRollback");
+ } finally {
+ // Restore system clock
+ getDevice().setDate(Date.from(t1));
+ getDevice().executeShellCommand("am broadcast -a android.intent.action.TIME_SET");
+ }
+ }
+
+ /**
+ * Tests watchdog triggered staged rollbacks involving only apks.
+ */
+ @Test
+ public void testBadApkOnly() throws Exception {
+ run("testBadApkOnly_Phase1_Install");
+ getDevice().reboot();
+ run("testBadApkOnly_Phase2_VerifyInstall");
+
+ // Launch the app to crash to trigger rollback
+ startActivity(TESTAPP_A);
+ // Wait for reboot to happen
+ waitForDeviceNotAvailable(2, TimeUnit.MINUTES);
+
+ getDevice().waitForDeviceAvailable();
+
+ run("testBadApkOnly_Phase3_VerifyRollback");
+
+ assertThat(mLogger).eventOccurred(ROLLBACK_INITIATE, null, REASON_APP_CRASH, TESTAPP_A);
+ assertThat(mLogger).eventOccurred(ROLLBACK_BOOT_TRIGGERED, null, null, null);
+ assertThat(mLogger).eventOccurred(ROLLBACK_SUCCESS, null, null, null);
+ }
+
+ /**
+ * Tests rollbacks triggered by the native watchdog.
+ */
+ @Test
+ public void testNativeWatchdogTriggersRollback() throws Exception {
+ run("testNativeWatchdogTriggersRollback_Phase1_Install");
+ getDevice().reboot();
+ run("testNativeWatchdogTriggersRollback_Phase2_VerifyInstall");
+
+ // crash system_server enough times to trigger a rollback
+ crashProcess("system_server", NATIVE_CRASHES_THRESHOLD);
+
+ // Rollback should be committed automatically now.
+ // Give time for rollback to be committed. This could take a while,
+ // because we need all of the following to happen:
+ // 1. system_server comes back up and boot completes.
+ // 2. Rollback health observer detects updatable crashing signal.
+ // 3. Staged rollback session becomes ready.
+ // 4. Device actually reboots.
+ // So we give a generous timeout here.
+ waitForDeviceNotAvailable(5, TimeUnit.MINUTES);
+ getDevice().waitForDeviceAvailable();
+
+ // verify rollback committed
+ run("testNativeWatchdogTriggersRollback_Phase3_VerifyRollback");
+
+ assertThat(mLogger).eventOccurred(ROLLBACK_INITIATE, null, REASON_NATIVE_CRASH, null);
+ assertThat(mLogger).eventOccurred(ROLLBACK_BOOT_TRIGGERED, null, null, null);
+ assertThat(mLogger).eventOccurred(ROLLBACK_SUCCESS, null, null, null);
+ }
+
+ /**
+ * Tests rollbacks triggered by the native watchdog.
+ */
+ @Test
+ public void testNativeWatchdogTriggersRollbackForAll() throws Exception {
+ // This test requires committing multiple staged rollbacks
+ assumeTrue(isCheckpointSupported());
+
+ // Install 2 packages with rollback enabled.
+ run("testNativeWatchdogTriggersRollbackForAll_Phase1_Install");
+ getDevice().reboot();
+
+ // Verify that we have 2 rollbacks available
+ run("testNativeWatchdogTriggersRollbackForAll_Phase2_VerifyInstall");
+
+ // crash system_server enough times to trigger rollbacks
+ crashProcess("system_server", NATIVE_CRASHES_THRESHOLD);
+
+ // Rollback should be committed automatically now.
+ // Give time for rollback to be committed. This could take a while,
+ // because we need all of the following to happen:
+ // 1. system_server comes back up and boot completes.
+ // 2. Rollback health observer detects updatable crashing signal.
+ // 3. Staged rollback session becomes ready.
+ // 4. Device actually reboots.
+ // So we give a generous timeout here.
+ waitForDeviceNotAvailable(5, TimeUnit.MINUTES);
+ getDevice().waitForDeviceAvailable();
+
+ // verify all available rollbacks have been committed
+ run("testNativeWatchdogTriggersRollbackForAll_Phase3_VerifyRollback");
+
+ assertThat(mLogger).eventOccurred(ROLLBACK_INITIATE, null, REASON_NATIVE_CRASH, null);
+ assertThat(mLogger).eventOccurred(ROLLBACK_BOOT_TRIGGERED, null, null, null);
+ assertThat(mLogger).eventOccurred(ROLLBACK_SUCCESS, null, null, null);
+ }
+
+ private List<String> getSnapshotDirectories(String baseDir) throws Exception {
+ IFileEntry f = getDevice().getFileEntry(baseDir);
+ if (f == null) {
+ Log.d(TAG, "baseDir doesn't exist: " + baseDir);
+ return Collections.EMPTY_LIST;
+ }
+ List<String> list = f.getChildren(false)
+ .stream().filter(entry -> entry.getName().matches("\\d+(-prerestore)?"))
+ .map(entry -> entry.getFullPath())
+ .collect(Collectors.toList());
+ Log.d(TAG, "getSnapshotDirectories=" + list);
+ return list;
+ }
+
+ private void assertDirectoryIsEmpty(String path) {
+ try {
+ IFileEntry file = getDevice().getFileEntry(path);
+ assertWithMessage("Not a directory: " + path).that(file.isDirectory()).isTrue();
+ assertWithMessage("Directory not empty: " + path)
+ .that(file.getChildren(false)).isEmpty();
+ } catch (DeviceNotAvailableException e) {
+ fail("Can't access directory: " + path);
+ }
+ }
+
+ private void assertFileContents(String expectedContents, String path) throws Exception {
+ String actualContents = getDevice().pullFileContents(path);
+ assertWithMessage("Failed to retrieve file=%s", path).that(actualContents).isNotNull();
+ assertWithMessage("Mismatched file contents, path=%s", path)
+ .that(actualContents).isEqualTo(expectedContents);
+ }
+
+ private void assertFileNotExists(String path) throws Exception {
+ assertWithMessage("File shouldn't exist, path=%s", path)
+ .that(getDevice().getFileEntry(path)).isNull();
+ }
+
+ private static String apkDataDirCe(String apkName, int userId) {
+ return String.format("/data/user/%d/%s", userId, apkName);
+ }
+
+ private static String apkDataDirDe(String apkName, int userId) {
+ return String.format("/data/user_de/%d/%s", userId, apkName);
+ }
+
+ private static String apexDataDirCe(String apexName, int userId) {
+ return String.format("/data/misc_ce/%d/apexdata/%s", userId, apexName);
+ }
+
+ private static String apexDataDirDeUser(String apexName, int userId) {
+ return String.format("/data/misc_de/%d/apexdata/%s", userId, apexName);
+ }
+
+ private static String apexDataDirDeSys(String apexName) {
+ return String.format("/data/misc/apexdata/%s", apexName);
+ }
+
+ private void pushString(String contents, String path) throws Exception {
+ assertWithMessage("Failed to push file to device, content=%s path=%s", contents, path)
+ .that(getDevice().pushString(contents, path)).isTrue();
+ }
+
+ private void waitForDeviceNotAvailable(long timeout, TimeUnit unit) {
+ assertWithMessage("waitForDeviceNotAvailable() timed out in %s %s", timeout, unit)
+ .that(getDevice().waitForDeviceNotAvailable(unit.toMillis(timeout))).isTrue();
+ }
+
+ private void startActivity(String packageName) throws Exception {
+ String cmd = "am start -S -a android.intent.action.MAIN "
+ + "-c android.intent.category.LAUNCHER " + packageName;
+ getDevice().executeShellCommand(cmd);
+ }
+
+ private void crashProcess(String processName, int numberOfCrashes) throws Exception {
+ String pid = "";
+ String lastPid = "invalid";
+ for (int i = 0; i < numberOfCrashes; ++i) {
+ // This condition makes sure before we kill the process, the process is running AND
+ // the last crash was finished.
+ while ("".equals(pid) || lastPid.equals(pid)) {
+ pid = getDevice().executeShellCommand("pidof " + processName);
+ }
+ getDevice().executeShellCommand("kill " + pid);
+ lastPid = pid;
+ }
+ }
+
+ private boolean isCheckpointSupported() throws Exception {
+ try {
+ run("isCheckpointSupported");
+ return true;
+ } catch (AssertionError ignore) {
+ return false;
+ }
+ }
+}
diff --git a/hostsidetests/rollback/src/com/android/cts_root/rollback/host/WatchdogEventLogger.java b/hostsidetests/rollback/src/com/android/cts_root/rollback/host/WatchdogEventLogger.java
new file mode 100644
index 0000000..59eb027
--- /dev/null
+++ b/hostsidetests/rollback/src/com/android/cts_root/rollback/host/WatchdogEventLogger.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2021 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.cts_root.rollback.host;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.android.tradefed.device.ITestDevice;
+
+import com.google.common.truth.FailureMetadata;
+import com.google.common.truth.Truth;
+
+public class WatchdogEventLogger {
+ private static final String[] ROLLBACK_EVENT_TYPES = {
+ "ROLLBACK_INITIATE", "ROLLBACK_BOOT_TRIGGERED", "ROLLBACK_SUCCESS"};
+ private static final String[] ROLLBACK_EVENT_ATTRS = {
+ "logPackage", "rollbackReason", "failedPackageName"};
+ private static final String PROP_PREFIX = "persist.sys.rollbacktest.";
+
+ private ITestDevice mDevice;
+
+ private void resetProperties(boolean enabled) throws Exception {
+ assertThat(mDevice.setProperty(
+ PROP_PREFIX + "enabled", String.valueOf(enabled))).isTrue();
+ for (String type : ROLLBACK_EVENT_TYPES) {
+ String key = PROP_PREFIX + type;
+ assertThat(mDevice.setProperty(key, "")).isTrue();
+ for (String attr : ROLLBACK_EVENT_ATTRS) {
+ assertThat(mDevice.setProperty(key + "." + attr, "")).isTrue();
+ }
+ }
+ }
+
+ public void start(ITestDevice device) throws Exception {
+ mDevice = device;
+ resetProperties(true);
+ }
+
+ public void stop() throws Exception {
+ if (mDevice != null) {
+ resetProperties(false);
+ }
+ }
+
+ private boolean matchProperty(String type, String attr, String expectedVal) throws Exception {
+ String key = PROP_PREFIX + type + "." + attr;
+ String val = mDevice.getProperty(key);
+ return expectedVal == null || expectedVal.equals(val);
+ }
+
+ /**
+ * Returns whether a Watchdog event has occurred that matches the given criteria.
+ *
+ * Check the value of all non-null parameters against the list of Watchdog events that have
+ * occurred, and return {@code true} if an event exists which matches all criteria.
+ */
+ public boolean watchdogEventOccurred(String type, String logPackage,
+ String rollbackReason, String failedPackageName) throws Exception {
+ return mDevice.getBooleanProperty(PROP_PREFIX + type, false)
+ && matchProperty(type, "logPackage", logPackage)
+ && matchProperty(type, "rollbackReason", rollbackReason)
+ && matchProperty(type, "failedPackageName", failedPackageName);
+ }
+
+ static class Subject extends com.google.common.truth.Subject {
+ private final WatchdogEventLogger mActual;
+
+ private Subject(FailureMetadata failureMetadata, WatchdogEventLogger subject) {
+ super(failureMetadata, subject);
+ mActual = subject;
+ }
+
+ private static Factory<Subject,
+ WatchdogEventLogger> loggers() {
+ return Subject::new;
+ }
+
+ static Subject assertThat(WatchdogEventLogger actual) {
+ return Truth.assertAbout(loggers()).that(actual);
+ }
+
+ void eventOccurred(String type, String logPackage, String rollbackReason,
+ String failedPackageName) throws Exception {
+ check("watchdogEventOccurred(type=%s, logPackage=%s, rollbackReason=%s, "
+ + "failedPackageName=%s)", type, logPackage, rollbackReason, failedPackageName)
+ .that(mActual.watchdogEventOccurred(type, logPackage, rollbackReason,
+ failedPackageName)).isTrue();
+ }
+ }
+}
diff --git a/tests/packagemanagerlocal/Android.bp b/tests/packagemanagerlocal/Android.bp
new file mode 100644
index 0000000..c1f3115
--- /dev/null
+++ b/tests/packagemanagerlocal/Android.bp
@@ -0,0 +1,39 @@
+// Copyright (C) 2022 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 {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test {
+ name: "CtsRootPackageManagerLocalTests",
+ defaults: ["cts_defaults"],
+ static_libs: [
+ "androidx.test.core",
+ "androidx.test.rules",
+ "platform-test-annotations",
+ "services.core",
+ "truth-prebuilt",
+ ],
+ libs: [
+ "android.test.runner",
+ "android.test.base",
+ ],
+ srcs: ["src/**/*.java"],
+ test_suites: [
+ "cts_root",
+ "general-tests",
+ ],
+ sdk_version: "test_current",
+}
diff --git a/tests/packagemanagerlocal/AndroidManifest.xml b/tests/packagemanagerlocal/AndroidManifest.xml
new file mode 100644
index 0000000..9f8c2f7
--- /dev/null
+++ b/tests/packagemanagerlocal/AndroidManifest.xml
@@ -0,0 +1,28 @@
+<!--
+ ~ Copyright (C) 2022 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="android.packagemanagerlocal.cts_root" >
+
+ <application>
+ <uses-library android:name="android.test.runner" />
+ </application>
+
+ <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="android"
+ android:label="PackageManagerLocal CTS root tests"/>
+
+</manifest>
diff --git a/tests/packagemanagerlocal/AndroidTest.xml b/tests/packagemanagerlocal/AndroidTest.xml
new file mode 100644
index 0000000..0095b11
--- /dev/null
+++ b/tests/packagemanagerlocal/AndroidTest.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2022 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="Runs PackageManagerLocal CTS root tests">
+ <option name="test-suite-tag" value="apct" />
+ <option name="test-suite-tag" value="apct-instrumentation" />
+
+ <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer"/>
+
+ <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+ <option name="cleanup-apks" value="true"/>
+ <option name="test-file-name" value="CtsRootPackageManagerLocalTests.apk"/>
+ </target_preparer>
+
+ <!-- Restart to clear test code from system server -->
+ <target_preparer class="com.android.tradefed.targetprep.DeviceCleaner">
+ <option name="cleanup-action" value="REBOOT" />
+ </target_preparer>
+
+ <test class="com.android.tradefed.testtype.AndroidJUnitTest">
+ <option name="package" value="android.packagemanagerlocal.cts_root"/>
+ <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
+ <option name="restart" value="false" />
+ </test>
+</configuration>
diff --git a/tests/packagemanagerlocal/OWNERS b/tests/packagemanagerlocal/OWNERS
new file mode 100644
index 0000000..4e5fe55
--- /dev/null
+++ b/tests/packagemanagerlocal/OWNERS
@@ -0,0 +1,3 @@
+# Bug component: 36137
+alexbuy@google.com
+patb@google.com
diff --git a/tests/packagemanagerlocal/TEST_MAPPING b/tests/packagemanagerlocal/TEST_MAPPING
new file mode 100644
index 0000000..54b32af
--- /dev/null
+++ b/tests/packagemanagerlocal/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+ "presubmit": [
+ {
+ "name": "CtsRootPackageManagerLocalTests"
+ }
+ ]
+}
diff --git a/tests/packagemanagerlocal/src/android/packagemanagerlocal/cts_root/PackageManagerLocalTest.java b/tests/packagemanagerlocal/src/android/packagemanagerlocal/cts_root/PackageManagerLocalTest.java
new file mode 100644
index 0000000..9ba383b
--- /dev/null
+++ b/tests/packagemanagerlocal/src/android/packagemanagerlocal/cts_root/PackageManagerLocalTest.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2022 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 android.packagemanagerlocal.cts_root;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.server.LocalManagerRegistry;
+import com.android.server.pm.PackageManagerLocal;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+import java.nio.file.Files;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+@RunWith(AndroidJUnit4.class)
+public final class PackageManagerLocalTest {
+ private static final String TAG = "PackageManagerLocalTest";
+
+ private PackageManagerLocal mPackageManagerLocal;
+
+ @Before
+ public void setup() {
+ mPackageManagerLocal = LocalManagerRegistry.getManager(PackageManagerLocal.class);
+ }
+
+ @Test
+ public void testPackageManagerLocal_ReconcileSdkData_DifferentStorageFlags() throws Exception {
+ final String volumeUuid = null;
+ final String packageName = "android.packagemanagerlocal.test";
+ final List<String> subDirNames = Arrays.asList("one", "two@random");
+ final int userId = 0;
+ final int appId = 10000;
+ final int previousAppId = -1;
+ final String seInfo = "default";
+
+ // There are two flags: FLAG_STORAGE_CE and FLAG_STORAGE_DE. So total of 4 combination
+ // to test.
+ for (int currentFlag = 0; currentFlag < 4; currentFlag++) {
+ final String errorMsg = "Failed for flag: " + currentFlag;
+
+ File cePackageDirFile = new File("/data/misc_ce/0/sdksandbox/" + packageName);
+ File dePackageDirFile = new File("/data/misc_de/0/sdksandbox/" + packageName);
+
+ try {
+ mPackageManagerLocal.reconcileSdkData(volumeUuid, packageName, subDirNames, userId,
+ appId, previousAppId, seInfo, currentFlag);
+
+ // Verify that sdk data directories have been created in the desired location
+ boolean hasCeFlag = (currentFlag & PackageManagerLocal.FLAG_STORAGE_CE) != 0;
+ if (hasCeFlag) {
+ assertWithMessage(errorMsg).that(cePackageDirFile.isDirectory()).isTrue();
+ assertWithMessage(errorMsg).that(
+ cePackageDirFile.list()).asList().containsExactly("one", "two@random");
+ } else {
+ assertWithMessage(errorMsg).that(cePackageDirFile.exists()).isFalse();
+ }
+
+ boolean hasDeFlag = (currentFlag & PackageManagerLocal.FLAG_STORAGE_DE) != 0;
+ if (hasDeFlag) {
+ assertWithMessage(errorMsg).that(dePackageDirFile.isDirectory()).isTrue();
+ assertWithMessage(errorMsg).that(
+ dePackageDirFile.list()).asList().containsExactly("one", "two@random");
+ } else {
+ assertWithMessage(errorMsg).that(dePackageDirFile.exists()).isFalse();
+ }
+ } finally {
+ // Clean up the created directories
+ final List<String> emptyDir = new ArrayList<String>();
+ mPackageManagerLocal.reconcileSdkData(volumeUuid, packageName, emptyDir, userId,
+ appId, previousAppId, seInfo, currentFlag);
+ Files.deleteIfExists(cePackageDirFile.toPath());
+ Files.deleteIfExists(dePackageDirFile.toPath());
+ }
+ }
+ }
+
+ @Test
+ public void testPackageManagerLocal_ReconcileSdkData_Reconcile() throws Exception {
+ final String volumeUuid = null;
+ final String packageName = "android.packagemanagerlocal.test";
+ final List<String> subDirNames = Arrays.asList("one", "two@random");
+ final int userId = 0;
+ final int appId = 10000;
+ final int previousAppId = -1;
+ final String seInfo = "default";
+ final int flag = PackageManagerLocal.FLAG_STORAGE_CE;
+
+ File cePackageDirFile = new File("/data/misc_ce/0/sdksandbox/" + packageName);
+
+ try {
+ mPackageManagerLocal.reconcileSdkData(volumeUuid, packageName, subDirNames, userId,
+ appId, previousAppId, seInfo, flag);
+
+ // Call reconcileSdkData again, with different subDirNames
+ final List<String> differentSubDirNames = Arrays.asList("three");
+ mPackageManagerLocal.reconcileSdkData(volumeUuid, packageName, differentSubDirNames,
+ userId, appId, previousAppId, seInfo, flag);
+
+ // Verify that sdk data directories have been created in the desired location
+ assertThat(cePackageDirFile.isDirectory()).isTrue();
+ assertThat(cePackageDirFile.list()).asList().containsExactly("three");
+ } finally {
+ // Clean up the created directories
+ final List<String> emptyDir = new ArrayList<String>();
+ mPackageManagerLocal.reconcileSdkData(volumeUuid, packageName, emptyDir, userId, appId,
+ previousAppId, seInfo, flag);
+ Files.deleteIfExists(cePackageDirFile.toPath());
+ }
+ }
+}
diff --git a/tests/packagewatchdog/Android.bp b/tests/packagewatchdog/Android.bp
new file mode 100644
index 0000000..54d4c25
--- /dev/null
+++ b/tests/packagewatchdog/Android.bp
@@ -0,0 +1,39 @@
+// Copyright (C) 2021 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 {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test {
+ name: "CtsRootPackageWatchdogTestCases",
+ defaults: ["cts_defaults"],
+ static_libs: [
+ "androidx.test.core",
+ "androidx.test.rules",
+ "services.core",
+ "truth-prebuilt",
+ "platform-test-annotations",
+ ],
+ libs: [
+ "android.test.runner",
+ "android.test.base",
+ ],
+ srcs: ["src/**/*.java"],
+ test_suites: [
+ "cts_root",
+ "general-tests",
+ ],
+ sdk_version: "test_current",
+}
diff --git a/tests/packagewatchdog/AndroidManifest.xml b/tests/packagewatchdog/AndroidManifest.xml
new file mode 100644
index 0000000..e700b15
--- /dev/null
+++ b/tests/packagewatchdog/AndroidManifest.xml
@@ -0,0 +1,28 @@
+<!--
+ ~ Copyright (C) 2021 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="android.packagewatchdog.cts_root" >
+
+ <application>
+ <uses-library android:name="android.test.runner" />
+ </application>
+
+ <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="android"
+ android:label="PackageWatchdog CTS root tests"/>
+
+</manifest>
diff --git a/tests/packagewatchdog/AndroidTest.xml b/tests/packagewatchdog/AndroidTest.xml
new file mode 100644
index 0000000..6620098
--- /dev/null
+++ b/tests/packagewatchdog/AndroidTest.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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="Runs PackageWatchdog CTS root tests">
+ <option name="test-suite-tag" value="apct" />
+ <option name="test-suite-tag" value="apct-instrumentation" />
+
+ <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer"/>
+
+ <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+ <option name="cleanup-apks" value="true"/>
+ <option name="test-file-name" value="CtsRootPackageWatchdogTestCases.apk"/>
+ </target_preparer>
+
+ <!-- Restart to clear test code from system server -->
+ <target_preparer class="com.android.tradefed.targetprep.DeviceCleaner">
+ <option name="cleanup-action" value="REBOOT" />
+ </target_preparer>
+
+ <test class="com.android.tradefed.testtype.AndroidJUnitTest">
+ <option name="package" value="android.packagewatchdog.cts_root"/>
+ <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
+ <option name="restart" value="false" />
+ </test>
+</configuration>
diff --git a/tests/packagewatchdog/OWNERS b/tests/packagewatchdog/OWNERS
new file mode 100644
index 0000000..0342627
--- /dev/null
+++ b/tests/packagewatchdog/OWNERS
@@ -0,0 +1,4 @@
+# Bug component: 557916
+gavincorkery@google.com
+wangchun@google.com
+olilan@google.com
\ No newline at end of file
diff --git a/tests/packagewatchdog/src/android/packagewatchdog/cts_root/PackageWatchdogTest.java b/tests/packagewatchdog/src/android/packagewatchdog/cts_root/PackageWatchdogTest.java
new file mode 100644
index 0000000..8f425bc
--- /dev/null
+++ b/tests/packagewatchdog/src/android/packagewatchdog/cts_root/PackageWatchdogTest.java
@@ -0,0 +1,304 @@
+/*
+ * Copyright (C) 2021 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 android.packagewatchdog.cts_root;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.Context;
+import android.content.pm.VersionedPackage;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.server.PackageWatchdog;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+public class PackageWatchdogTest {
+
+ private PackageWatchdog mPackageWatchdog;
+
+
+ private static final String APP_A = "com.app.a";
+ private static final String APP_B = "com.app.b";
+ private static final String OBSERVER_NAME_1 = "observer-1";
+ private static final String OBSERVER_NAME_2 = "observer-2";
+ private static final int VERSION_CODE = 1;
+ private static final long SHORT_DURATION = TimeUnit.MINUTES.toMillis(5);
+ private static final int FAILURE_COUNT_THRESHOLD = 5;
+
+ private CountDownLatch mLatch1, mLatch2;
+ private TestObserver mTestObserver1, mTestObserver2;
+
+ @Before
+ public void setUp() {
+ Context mContext = InstrumentationRegistry.getInstrumentation().getContext();
+ mPackageWatchdog = PackageWatchdog.getInstance(mContext);
+ mLatch1 = new CountDownLatch(1);
+ mLatch2 = new CountDownLatch(1);
+ }
+
+ @After
+ public void tearDown() {
+ if (mTestObserver1 != null) {
+ mPackageWatchdog.unregisterHealthObserver(mTestObserver1);
+ }
+ if (mTestObserver2 != null) {
+ mPackageWatchdog.unregisterHealthObserver(mTestObserver2);
+ }
+ }
+
+ @Test
+ public void testAppCrashIsMitigated() throws Exception {
+ CountDownLatch latch = new CountDownLatch(1);
+ mTestObserver1 = new TestObserver(OBSERVER_NAME_1, latch);
+ mPackageWatchdog.registerHealthObserver(mTestObserver1);
+ mPackageWatchdog.startObservingHealth(
+ mTestObserver1, List.of(APP_A), SHORT_DURATION);
+ raiseFatalFailure(Arrays.asList(new VersionedPackage(APP_A,
+ VERSION_CODE)), PackageWatchdog.FAILURE_REASON_APP_CRASH);
+ assertThat(latch.await(5, TimeUnit.SECONDS)).isTrue();
+ assertThat(mTestObserver1.mMitigatedPackages).isEqualTo(List.of(APP_A));
+ }
+
+ /** Test that nothing happens if an app crashes that is not watched by any observer.*/
+ @Test
+ public void testAppCrashWithoutObserver() throws Exception {
+ mTestObserver1 = new TestObserver(OBSERVER_NAME_1, mLatch1);
+
+ mPackageWatchdog.startObservingHealth(mTestObserver1, Arrays.asList(APP_A), SHORT_DURATION);
+ raiseFatalFailure(Arrays.asList(new VersionedPackage(APP_B,
+ VERSION_CODE)), PackageWatchdog.FAILURE_REASON_APP_CRASH);
+
+ // Small break to allow failure to be noted.
+ Thread.sleep(1000);
+ assertThat(mTestObserver1.mMitigatedPackages).isEmpty();
+ }
+
+ /**
+ * Test that multiple observers may register to watch certain packages and that they receive
+ * the correct callbacks.
+ */
+ @Test
+ public void testRegisteringMultipleObservers() throws Exception {
+ mTestObserver1 = new TestObserver(OBSERVER_NAME_1, mLatch1);
+ mTestObserver2 = new TestObserver(OBSERVER_NAME_2, mLatch2);
+
+ mPackageWatchdog.startObservingHealth(mTestObserver1, Arrays.asList(APP_A), SHORT_DURATION);
+ mPackageWatchdog.startObservingHealth(
+ mTestObserver2, Arrays.asList(APP_A, APP_B), SHORT_DURATION);
+ raiseFatalFailure(Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE),
+ new VersionedPackage(APP_B, VERSION_CODE)),
+ PackageWatchdog.FAILURE_REASON_APP_CRASH);
+ assertThat(mLatch1.await(5, TimeUnit.SECONDS)).isTrue();
+ assertThat(mLatch2.await(5, TimeUnit.SECONDS)).isTrue();
+
+ // The failed packages should be the same as the registered ones to ensure registration is
+ // done successfully
+ assertThat(mTestObserver1.mHealthCheckFailedPackages).containsExactly(APP_A);
+ assertThat(mTestObserver2.mHealthCheckFailedPackages).containsExactly(APP_A, APP_B);
+ }
+
+
+ /**
+ * Test that an unregistered observer is not notified for a failing package it previous
+ * observed.
+ */
+ @Test
+ public void testUnregistration() throws Exception {
+ mTestObserver1 = new TestObserver(OBSERVER_NAME_1);
+ mTestObserver2 = new TestObserver(OBSERVER_NAME_2, mLatch2);
+ mPackageWatchdog.startObservingHealth(mTestObserver2, Arrays.asList(APP_A), SHORT_DURATION);
+ mPackageWatchdog.startObservingHealth(mTestObserver1, Arrays.asList(APP_A), SHORT_DURATION);
+
+ mPackageWatchdog.unregisterHealthObserver(mTestObserver1);
+
+ raiseFatalFailure(Arrays.asList(new VersionedPackage(APP_A,
+ VERSION_CODE)), PackageWatchdog.FAILURE_REASON_APP_CRASH);
+
+ assertThat(mLatch2.await(1, TimeUnit.MINUTES)).isTrue();
+
+
+ assertThat(mTestObserver1.mHealthCheckFailedPackages).isEmpty();
+ assertThat(mTestObserver2.mHealthCheckFailedPackages).containsExactly(APP_A);
+
+ }
+
+ /**
+ * Test package failure under threshold does not notify observers
+ */
+ @Test
+ public void testNoPackageFailureBeforeThreshold() throws Exception {
+ mTestObserver1 = new TestObserver(OBSERVER_NAME_1);
+ mTestObserver2 = new TestObserver(OBSERVER_NAME_2);
+
+ mPackageWatchdog.startObservingHealth(mTestObserver2, Arrays.asList(APP_A), SHORT_DURATION);
+ mPackageWatchdog.startObservingHealth(mTestObserver1, Arrays.asList(APP_A), SHORT_DURATION);
+
+ for (int i = 0; i < FAILURE_COUNT_THRESHOLD - 1; i++) {
+ mPackageWatchdog.onPackageFailure(Arrays.asList(
+ new VersionedPackage(APP_A, VERSION_CODE)),
+ PackageWatchdog.FAILURE_REASON_UNKNOWN);
+ }
+
+ // Small break to allow failure to be noted.
+ Thread.sleep(1000);
+
+ // Verify that observers are not notified
+ assertThat(mTestObserver1.mHealthCheckFailedPackages).isEmpty();
+ assertThat(mTestObserver2.mHealthCheckFailedPackages).isEmpty();
+ }
+
+ /** Test that observers execute correctly for failures reasons that skip thresholding. */
+ @Test
+ public void testImmediateFailures() throws Exception {
+ mLatch1 = new CountDownLatch(2);
+ mTestObserver1 = new TestObserver(OBSERVER_NAME_1, mLatch1);
+
+ mPackageWatchdog.startObservingHealth(mTestObserver1, Arrays.asList(APP_A), SHORT_DURATION);
+
+ mPackageWatchdog.onPackageFailure(Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE)),
+ PackageWatchdog.FAILURE_REASON_EXPLICIT_HEALTH_CHECK);
+ mPackageWatchdog.onPackageFailure(Arrays.asList(new VersionedPackage(APP_B, VERSION_CODE)),
+ PackageWatchdog.FAILURE_REASON_NATIVE_CRASH);
+
+ assertThat(mLatch1.await(5, TimeUnit.SECONDS)).isTrue();
+ assertThat(mTestObserver1.mMitigatedPackages).containsExactly(APP_A, APP_B);
+ }
+
+ /**
+ * Test that a persistent observer will mitigate failures if it wishes to observe a package.
+ */
+ @Test
+ public void testPersistentObserverWatchesPackage() throws Exception {
+ mTestObserver1 = new TestObserver(OBSERVER_NAME_1, mLatch1);
+ mTestObserver1.setPersistent(true);
+ mTestObserver1.setMayObservePackages(true);
+
+ mTestObserver2 = new TestObserver(OBSERVER_NAME_2, mLatch2);
+
+ mPackageWatchdog.startObservingHealth(mTestObserver1, Arrays.asList(APP_B), SHORT_DURATION);
+
+ raiseFatalFailure(Arrays.asList(new VersionedPackage(APP_A,
+ VERSION_CODE)), PackageWatchdog.FAILURE_REASON_APP_CRASH);
+ assertThat(mLatch1.await(5, TimeUnit.SECONDS)).isTrue();
+
+ // Persistent observer will observe the failing package.
+ assertThat(mTestObserver1.mHealthCheckFailedPackages).containsExactly(APP_A);
+
+ // A non-persistent observer will not observe the failing package.
+ assertThat(mTestObserver2.mHealthCheckFailedPackages).isEmpty();
+ }
+
+ /**
+ * Test that a persistent observer will not mitigate failures if it does not wish to observe
+ * a given package.
+ */
+ @Test
+ public void testPersistentObserverDoesNotWatchPackage() {
+ mTestObserver1 = new TestObserver(OBSERVER_NAME_1);
+ mTestObserver1.setPersistent(true);
+ mTestObserver1.setMayObservePackages(false);
+
+ mPackageWatchdog.startObservingHealth(mTestObserver1, Arrays.asList(APP_B), SHORT_DURATION);
+
+ raiseFatalFailure(Arrays.asList(new VersionedPackage(APP_A,
+ VERSION_CODE)), PackageWatchdog.FAILURE_REASON_UNKNOWN);
+ assertThat(mTestObserver1.mHealthCheckFailedPackages).isEmpty();
+ }
+
+ private void raiseFatalFailure(List<VersionedPackage> failingPackages, int failureReason) {
+ int failureCount = FAILURE_COUNT_THRESHOLD;
+ if (failureReason == PackageWatchdog.FAILURE_REASON_NATIVE_CRASH
+ || failureReason == PackageWatchdog.FAILURE_REASON_EXPLICIT_HEALTH_CHECK) {
+ failureCount = 1;
+ }
+ for (int i = 0; i < failureCount; i++) {
+ mPackageWatchdog.onPackageFailure(failingPackages, failureReason);
+ }
+ }
+
+ private static class TestObserver implements PackageWatchdog.PackageHealthObserver {
+ private final String mName;
+ private final int mImpact;
+ private boolean mIsPersistent = false;
+ private boolean mMayObservePackages = false;
+ final List<String> mMitigatedPackages = new ArrayList<>();
+ final List<String> mHealthCheckFailedPackages = new ArrayList<>();
+ private final CountDownLatch mLatch;
+
+ TestObserver(String name, CountDownLatch latch) {
+ mName = name;
+ mLatch = latch;
+ mImpact = PackageWatchdog.PackageHealthObserverImpact.USER_IMPACT_MEDIUM;
+ }
+
+ TestObserver(String name) {
+ mName = name;
+ mLatch = new CountDownLatch(1);
+ mImpact = PackageWatchdog.PackageHealthObserverImpact.USER_IMPACT_MEDIUM;
+ }
+
+ public int onHealthCheckFailed(VersionedPackage versionedPackage, int failureReason,
+ int mitigationCount) {
+ mHealthCheckFailedPackages.add(versionedPackage.getPackageName());
+ return mImpact;
+ }
+
+ public boolean execute(VersionedPackage versionedPackage, int failureReason,
+ int mitigationCount) {
+ mMitigatedPackages.add(versionedPackage.getPackageName());
+ mLatch.countDown();
+ return true;
+ }
+
+ public String getName() {
+ return mName;
+ }
+
+ public int onBootLoop(int level) {
+ return mImpact;
+ }
+
+ public boolean executeBootLoopMitigation(int level) {
+ return true;
+ }
+
+ public boolean isPersistent() {
+ return mIsPersistent;
+ }
+
+ public boolean mayObservePackage(String packageName) {
+ return mMayObservePackages;
+ }
+
+ private void setPersistent(boolean isPersistent) {
+ mIsPersistent = isPersistent;
+ }
+
+ private void setMayObservePackages(boolean mayObservePackages) {
+ mMayObservePackages = mayObservePackages;
+ }
+ }
+}
diff --git a/tests/stats/AndroidTest.xml b/tests/stats/AndroidTest.xml
index 55ab762..9eab530 100644
--- a/tests/stats/AndroidTest.xml
+++ b/tests/stats/AndroidTest.xml
@@ -18,6 +18,8 @@
<option name="test-suite-tag" value="apct" />
<option name="test-suite-tag" value="apct-instrumentation" />
+ <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer"/>
+
<target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
<option name="cleanup-apks" value="true"/>
<option name="test-file-name" value="CtsRootStatsDeviceTestCases.apk"/>
diff --git a/tests/usage/AndroidTest.xml b/tests/usage/AndroidTest.xml
index ef4c8ad..6ea4a99 100644
--- a/tests/usage/AndroidTest.xml
+++ b/tests/usage/AndroidTest.xml
@@ -18,6 +18,8 @@
<option name="test-suite-tag" value="apct" />
<option name="test-suite-tag" value="apct-instrumentation" />
+ <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer"/>
+
<target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
<option name="cleanup-apks" value="true"/>
<option name="test-file-name" value="CtsRootUsageDeviceTestCases.apk"/>