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