Merge "Create a common lib for app enumeration tests" into rvc-dev
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/biometrics/AbstractBaseTest.java b/apps/CtsVerifier/src/com/android/cts/verifier/biometrics/AbstractBaseTest.java
index 65cf3a2..d1a300e 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/biometrics/AbstractBaseTest.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/biometrics/AbstractBaseTest.java
@@ -45,6 +45,7 @@
     private static final int REQUEST_ENROLL = 1;
 
     abstract protected String getTag();
+    abstract protected boolean isOnPauseAllowed();
 
     protected final Handler mHandler = new Handler(Looper.getMainLooper());
     protected final Executor mExecutor = mHandler::post;
@@ -60,6 +61,21 @@
     }
 
     @Override
+    protected void onPause() {
+        super.onPause();
+
+        // Assume we only enable the pass button when all tests pass. There actually  isn't a way
+        // to easily do something like `this.isTestPassed()`
+        if (!getPassButton().isEnabled() && !isOnPauseAllowed()) {
+            showToastAndLog("This test must be completed without going onPause");
+            // Do not allow the test to continue if it loses foreground. Testers must start over.
+            // 1) This is to avoid any potential change to the current enrollment / biometric state.
+            // 2) The authentication UI must not affect the caller's activity lifecycle.
+            finish();
+        }
+    }
+
+    @Override
     public void onActivityResult(int requestCode, int resultCode, Intent data) {
         if (requestCode == REQUEST_ENROLL) {
             mCurrentlyEnrolling = false;
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/biometrics/BiometricStrongTests.java b/apps/CtsVerifier/src/com/android/cts/verifier/biometrics/BiometricStrongTests.java
index ce71403..367d76d 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/biometrics/BiometricStrongTests.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/biometrics/BiometricStrongTests.java
@@ -63,6 +63,8 @@
     private static final String KEY_NAME_NO_STRONGBOX = "key_without_strongbox";
     private static final byte[] PAYLOAD = new byte[] {1, 2, 3, 4, 5, 6};
 
+    // TODO: Build these lists in a smarter way. For now, when adding a test to this list, please
+    // double check the logic in isOnPauseAllowed()
     private boolean mHasStrongBox;
     private Button mCheckAndEnrollButton;
     private Button mAuthenticateWithoutStrongBoxButton;
@@ -239,6 +241,30 @@
         });
     }
 
+    @Override
+    protected boolean isOnPauseAllowed() {
+        // Test hasn't started yet, user may need to go to Settings to remove enrollments
+        if (mCheckAndEnrollButton.isEnabled()) {
+            return true;
+        }
+
+        // Key invalidation test is currently the last test. Thus, if every other test is currently
+        // completed, let's allow onPause (allow tester to go into settings multiple times if
+        // needed).
+        if (mAuthenticateWithoutStrongBoxPassed && mAuthenticateWithStrongBoxPassed
+                && mAuthenticateUIPassed && mAuthenticateCredential1Passed
+                && mAuthenticateCredential2Passed && mAuthenticateCredential3Passed
+                && mCheckInvalidInputsPassed && mRejectThenAuthenticatePassed) {
+            return true;
+        }
+
+        if (mCurrentlyEnrolling) {
+            return true;
+        }
+
+        return false;
+    }
+
     private boolean isKeyInvalidated(String keyName) {
         try {
             Utils.initCipher(keyName);
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/biometrics/BiometricWeakTests.java b/apps/CtsVerifier/src/com/android/cts/verifier/biometrics/BiometricWeakTests.java
index 56c9224..f6fad5c 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/biometrics/BiometricWeakTests.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/biometrics/BiometricWeakTests.java
@@ -184,14 +184,17 @@
     }
 
     @Override
-    protected void onPause() {
-        super.onPause();
-
-        if (!mCurrentlyEnrolling) {
-            // Do not allow the test to continue if it loses foreground. Testers must start over.
-            // This is to avoid any potential change to the current enrollment / biometric state.
-            finish();
+    protected boolean isOnPauseAllowed() {
+        // Test hasn't started yet, user may need to go to Settings to remove enrollments
+        if (mEnrollButton.isEnabled()) {
+            return true;
         }
+
+        if (mCurrentlyEnrolling) {
+            return true;
+        }
+
+        return false;
     }
 
     private void updatePassButton() {
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/biometrics/CredentialCryptoTests.java b/apps/CtsVerifier/src/com/android/cts/verifier/biometrics/CredentialCryptoTests.java
index 3b3b289..caf9e29 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/biometrics/CredentialCryptoTests.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/biometrics/CredentialCryptoTests.java
@@ -40,7 +40,7 @@
 
 import javax.crypto.Cipher;
 
-public class CredentialCryptoTests extends PassFailButtons.Activity {
+public class CredentialCryptoTests extends AbstractBaseTest {
     private static final String TAG = "CredentialCryptoTests";
 
     private static final String KEY_NAME_STRONGBOX = "credential_timed_key_strongbox";
@@ -57,6 +57,17 @@
     private boolean mCredentialTimedKeyNoStrongBoxPassed;
 
     @Override
+    protected String getTag() {
+        return TAG;
+    }
+
+    @Override
+    protected boolean isOnPauseAllowed() {
+        // Tests haven't started yet
+        return !mCredentialTimedKeyStrongBoxPassed && !mCredentialTimedKeyNoStrongBoxPassed;
+    }
+
+    @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         setContentView(R.layout.biometric_test_credential_crypto_tests);
@@ -163,9 +174,4 @@
             getPassButton().setEnabled(true);
         }
     }
-
-    private void showToastAndLog(String s) {
-        Log.d(TAG, s);
-        Toast.makeText(this, s, Toast.LENGTH_SHORT).show();
-    }
 }
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/biometrics/CredentialEnrolledTests.java b/apps/CtsVerifier/src/com/android/cts/verifier/biometrics/CredentialEnrolledTests.java
index 473579f..1fc950e 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/biometrics/CredentialEnrolledTests.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/biometrics/CredentialEnrolledTests.java
@@ -36,17 +36,22 @@
  * This test checks that when a credential is enrolled, and biometrics are not enrolled,
  * BiometricManager and BiometricPrompt receive the correct results.
  */
-public class CredentialEnrolledTests extends PassFailButtons.Activity {
+public class CredentialEnrolledTests extends AbstractBaseTest {
     private static final String TAG = "CredentialEnrolledTests";
 
-    boolean mBiometricManagerPass;
-    boolean mBiometricPromptSetAllowedAuthenticatorsPass;
-    boolean mBiometricPromptSetDeviceCredentialAllowedPass;
+    private boolean mBiometricManagerPass;
+    private boolean mBiometricPromptSetAllowedAuthenticatorsPass;
+    private boolean mBiometricPromptSetDeviceCredentialAllowedPass;
 
     private final Handler mHandler = new Handler(Looper.getMainLooper());
     private final Executor mExecutor = mHandler::post;
 
     @Override
+    protected String getTag() {
+        return TAG;
+    }
+
+    @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         setContentView(R.layout.biometric_test_credential_enrolled_tests);
@@ -154,9 +159,10 @@
         });
     }
 
-    private void showToastAndLog(String s) {
-        Log.d(TAG, s);
-        Toast.makeText(this, s, Toast.LENGTH_SHORT).show();
+    @Override
+    protected boolean isOnPauseAllowed() {
+        // Allow user to go to Settings, etc to figure out why this test isn't passing.
+        return !mBiometricManagerPass;
     }
 
     private void updatePassButton() {
diff --git a/hostsidetests/appcompat/compatchanges/Android.bp b/hostsidetests/appcompat/compatchanges/Android.bp
index ca0867b..85fb5bb 100644
--- a/hostsidetests/appcompat/compatchanges/Android.bp
+++ b/hostsidetests/appcompat/compatchanges/Android.bp
@@ -28,4 +28,13 @@
         "vts",
         "general-tests",
     ],
+    java_resources: [":cts-global-compat-config"],
 }
+
+global_compat_config {
+    name: "cts-global-compat-config",
+    filename: "cts_all_compat_config.xml",
+}
+
+
+
diff --git a/hostsidetests/appcompat/compatchanges/src/com/android/cts/appcompat/CompatChangesValidConfigTest.java b/hostsidetests/appcompat/compatchanges/src/com/android/cts/appcompat/CompatChangesValidConfigTest.java
new file mode 100644
index 0000000..f9bdc22
--- /dev/null
+++ b/hostsidetests/appcompat/compatchanges/src/com/android/cts/appcompat/CompatChangesValidConfigTest.java
@@ -0,0 +1,197 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.cts.appcompat;
+
+ import static com.google.common.truth.Truth.assertThat;
+
+
+ import java.util.ArrayList;
+ import java.util.Arrays;
+ import java.util.List;
+ import java.util.Objects;
+ import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+ import java.util.stream.Collectors;
+
+ import android.compat.cts.CompatChangeGatingTestCase;
+
+ import org.w3c.dom.Document;
+ import org.w3c.dom.Element;
+ import org.w3c.dom.NamedNodeMap;
+ import org.w3c.dom.Node;
+ import org.w3c.dom.NodeList;
+
+ import javax.xml.parsers.DocumentBuilder;
+ import javax.xml.parsers.DocumentBuilderFactory;
+
+public final class CompatChangesValidConfigTest extends CompatChangeGatingTestCase {
+
+    private static class Change {
+        private static final Pattern CHANGE_REGEX = Pattern.compile("^ChangeId\\((?<changeId>[0-9]+)"
+                                                + "(; name=(?<changeName>[^;]+))?"
+                                                + "(; enableAfterTargetSdk=(?<targetSdk>[0-9]+))?"
+                                                + "(; (?<disabled>disabled))?"
+                                                + "(; packageOverrides=(?<overrides>[^\\)]+))?"
+                                                + "\\)");
+        long changeId;
+        String changeName;
+        int targetSdk;
+        boolean disabled;
+        boolean hasOverrides;
+
+        private Change(long changeId, String changeName, int targetSdk, boolean disabled, boolean hasOverrides) {
+            this.changeId = changeId;
+            this.changeName = changeName;
+            this.targetSdk = targetSdk;
+            this.disabled = disabled;
+            this.hasOverrides = hasOverrides;
+        }
+
+        static Change fromString(String line) {
+            long changeId = 0;
+            String changeName;
+            int targetSdk = 0;
+            boolean disabled = false;
+            boolean hasOverrides = false;
+
+            Matcher matcher = CHANGE_REGEX.matcher(line);
+            if (!matcher.matches()) {
+                throw new RuntimeException("Could not match line " + line);
+            }
+
+            try {
+                changeId = Long.parseLong(matcher.group("changeId"));
+            } catch (NumberFormatException e) {
+                throw new RuntimeException("No or invalid changeId!", e);
+            }
+            changeName = matcher.group("changeName");
+            String targetSdkAsString = matcher.group("targetSdk");
+            if (targetSdkAsString != null) {
+                try {
+                    targetSdk = Integer.parseInt(targetSdkAsString);
+                } catch (NumberFormatException e) {
+                    throw new RuntimeException("Invalid targetSdk for change!", e);
+                }
+            }
+            if (matcher.group("disabled") != null) {
+                disabled = true;
+            }
+            if (matcher.group("overrides") != null) {
+                hasOverrides = true;
+            }
+            return new Change(changeId, changeName, targetSdk, disabled, hasOverrides);
+        }
+
+        static Change fromNode(Node node) {
+            Element element = (Element) node;
+            long changeId = Long.parseLong(element.getAttribute("id"));
+            String changeName = element.getAttribute("name");
+            int targetSdk = 0;
+            if (element.hasAttribute("enableAfterTargetSdk")) {
+                targetSdk = Integer.parseInt(element.getAttribute("enableAfterTargetSdk"));
+            }
+            boolean disabled = false;
+            if (element.hasAttribute("disabled")) {
+                disabled = true;
+            }
+            boolean hasOverrides = false;
+            return new Change(changeId, changeName, targetSdk, disabled, hasOverrides);
+        }
+        @Override
+        public int hashCode() {
+            return Objects.hash(changeId, changeName, targetSdk, disabled, hasOverrides);
+        }
+        @Override
+        public boolean equals(Object other) {
+            if (this == other) {
+                return true;
+            }
+            if (other == null || !(other instanceof Change)) {
+                return false;
+            }
+            Change that = (Change) other;
+            return this.changeId == that.changeId
+                && Objects.equals(this.changeName, that.changeName)
+                && this.targetSdk == that.targetSdk
+                && this.disabled == that.disabled
+                && this.hasOverrides == that.hasOverrides;
+        }
+        @Override
+        public String toString() {
+            final StringBuilder sb = new StringBuilder();
+            sb.append("ChangeId(" + changeId);
+            if (changeName != null && !changeName.isEmpty()) {
+                sb.append("; name=" + changeName);
+            }
+            if (targetSdk != 0) {
+                sb.append("; enableAfterTargetSdk=" + targetSdk);
+            }
+            if (disabled) {
+                sb.append("; disabled");
+            }
+            if (hasOverrides) {
+                sb.append("; packageOverrides={something}");
+            }
+            sb.append(")");
+            return sb.toString();
+        }
+    }
+
+    /**
+     * Get the on device compat config.
+     */
+    private List<Change> getOnDeviceCompatConfig() throws Exception {
+        String config = runCommand("dumpsys platform_compat");
+        return Arrays.stream(config.split("\n"))
+                .map(Change::fromString)
+                .collect(Collectors.toList());
+    }
+
+    /**
+     * Parse the expected (i.e. defined in platform) config xml.
+     */
+    private List<Change> getExpectedCompatConfig() throws Exception {
+        DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
+        DocumentBuilder db = dbf.newDocumentBuilder();
+        Document dom = db.parse(getClass().getResourceAsStream("/cts_all_compat_config.xml"));
+        Element root = dom.getDocumentElement();
+        NodeList changeNodes = root.getElementsByTagName("compat-change");
+        List<Change> changes = new ArrayList<>();
+        for (int nodeIdx = 0; nodeIdx < changeNodes.getLength(); ++nodeIdx) {
+            changes.add(Change.fromNode(changeNodes.item(nodeIdx)));
+        }
+        return changes;
+    }
+
+    /**
+     * Check that there are no overrides.
+     */
+    public void testNoOverrides() throws Exception {
+        for (Change c : getOnDeviceCompatConfig()) {
+            assertThat(c.hasOverrides).isFalse();
+        }
+    }
+
+    /**
+     * Check that the on device config contains all the expected change ids defined in the platform.
+     * The device may contain extra changes, but none may be removed.
+     */
+    public void testDeviceContainsExpectedConfig() throws Exception {
+        assertThat(getOnDeviceCompatConfig()).containsAllIn(getExpectedCompatConfig());
+    }
+
+}
diff --git a/hostsidetests/appsecurity/OWNERS b/hostsidetests/appsecurity/OWNERS
index 81fa0d3..81a053e 100644
--- a/hostsidetests/appsecurity/OWNERS
+++ b/hostsidetests/appsecurity/OWNERS
@@ -5,6 +5,7 @@
 per-file AdoptableHostTest.java = jsharkey@google.com
 per-file ApexSignatureVerificationTest.java = dariofreni@google.com
 per-file ApplicationVisibilityTest.java = toddke@google.com
+per-file AppDataIsolationTests.java = rickywai@google.com
 per-file AppOpsTest.java = moltmann@google.com
 per-file AppSecurityTests.java = cbrubaker@google.com
 per-file AuthBoundKeyTest.java = cbrubaker@google.com
diff --git a/hostsidetests/appsecurity/src/android/appsecurity/cts/AppDataIsolationTests.java b/hostsidetests/appsecurity/src/android/appsecurity/cts/AppDataIsolationTests.java
index 633d1ac..3406bd1 100644
--- a/hostsidetests/appsecurity/src/android/appsecurity/cts/AppDataIsolationTests.java
+++ b/hostsidetests/appsecurity/src/android/appsecurity/cts/AppDataIsolationTests.java
@@ -16,7 +16,10 @@
 
 package android.appsecurity.cts;
 
-import static org.junit.Assert.assertNotNull;
+import static android.appsecurity.cts.Utils.waitForBootCompleted;
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.fail;
 
 import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
 
@@ -32,6 +35,8 @@
 public class AppDataIsolationTests extends BaseAppSecurityTest {
 
     private static final String APPA_APK = "CtsAppDataIsolationAppA.apk";
+    private static final String APP_SHARED_A_APK = "CtsAppDataIsolationAppSharedA.apk";
+    private static final String APP_DIRECT_BOOT_A_APK = "CtsAppDataIsolationAppDirectBootA.apk";
     private static final String APPA_PKG = "com.android.cts.appdataisolation.appa";
     private static final String APPA_CLASS =
             "com.android.cts.appdataisolation.appa.AppATests";
@@ -46,28 +51,46 @@
             "testAppACurProfileDataAccessible";
     private static final String APPA_METHOD_CHECK_REF_PROFILE_NOT_ACCESSIBLE =
             "testAppARefProfileDataNotAccessible";
+    private static final String APPA_METHOD_UNLOCK_DEVICE_AND_VERIFY_CE_DE_EXIST =
+            "testAppAUnlockDeviceAndVerifyCeDeDataExist";
+    private static final String APPA_METHOD_CANNOT_ACCESS_APPB_DIR = "testCannotAccessAppBDataDir";
+
+    private static final String APPA_METHOD_TEST_UNLOCK_DEVICE =
+            "testUnlockDevice";
 
     private static final String APPB_APK = "CtsAppDataIsolationAppB.apk";
+    private static final String APP_SHARED_B_APK = "CtsAppDataIsolationAppSharedB.apk";
     private static final String APPB_PKG = "com.android.cts.appdataisolation.appb";
     private static final String APPB_CLASS =
             "com.android.cts.appdataisolation.appb.AppBTests";
     private static final String APPB_METHOD_CANNOT_ACCESS_APPA_DIR = "testCannotAccessAppADataDir";
+    private static final String APPB_METHOD_CAN_ACCESS_APPA_DIR = "testCanAccessAppADataDir";
+
+    private static final String FBE_MODE_NATIVE = "native";
+    private static final String FBE_MODE_EMULATED = "emulated";
 
     @Before
     public void setUp() throws Exception {
         Utils.prepareSingleUser(getDevice());
         getDevice().uninstallPackage(APPA_PKG);
+        getDevice().uninstallPackage(APPB_PKG);
     }
 
     @After
     public void tearDown() throws Exception {
         getDevice().uninstallPackage(APPA_PKG);
+        getDevice().uninstallPackage(APPB_PKG);
     }
 
     private void forceStopPackage(String packageName) throws Exception {
         getDevice().executeShellCommand("am force-stop " + packageName);
     }
 
+    private void reboot() throws Exception {
+        getDevice().reboot();
+        waitForBootCompleted(getDevice());
+    }
+
     @Test
     public void testAppAbleToAccessItsDataAfterForceStop() throws Exception {
         // Install AppA and verify no data stored
@@ -93,7 +116,123 @@
         runDeviceTests(APPA_PKG, APPA_CLASS, APPA_METHOD_CHECK_DE_DATA_EXISTS);
         runDeviceTests(APPA_PKG, APPA_CLASS, APPA_METHOD_CHECK_CUR_PROFILE_ACCESSIBLE);
         runDeviceTests(APPA_PKG, APPA_CLASS, APPA_METHOD_CHECK_REF_PROFILE_NOT_ACCESSIBLE);
+    }
 
+    @Test
+    public void testAppAbleToAccessItsDataAfterReboot() throws Exception {
+        // Install AppA and verify no data stored
+        new InstallMultiple().addApk(APPA_APK).run();
+        runDeviceTests(APPA_PKG, APPA_CLASS, APPA_METHOD_CHECK_CE_DATA_DOES_NOT_EXIST);
+        runDeviceTests(APPA_PKG, APPA_CLASS, APPA_METHOD_CHECK_DE_DATA_DOES_NOT_EXIST);
+
+        // Create data in CE and DE storage
+        runDeviceTests(APPA_PKG, APPA_CLASS, APPA_METHOD_CREATE_CE_DE_DATA);
+
+        // Verify CE and DE storage contains data, cur profile is accessible and ref profile is
+        // not accessible
+        runDeviceTests(APPA_PKG, APPA_CLASS, APPA_METHOD_CHECK_CE_DATA_EXISTS);
+        runDeviceTests(APPA_PKG, APPA_CLASS, APPA_METHOD_CHECK_DE_DATA_EXISTS);
+        runDeviceTests(APPA_PKG, APPA_CLASS, APPA_METHOD_CHECK_CUR_PROFILE_ACCESSIBLE);
+        runDeviceTests(APPA_PKG, APPA_CLASS, APPA_METHOD_CHECK_REF_PROFILE_NOT_ACCESSIBLE);
+
+        // Reboot and verify CE and DE storage contains data, cur profile is accessible and
+        // ref profile is not accessible
+        reboot();
+        runDeviceTests(APPA_PKG, APPA_CLASS, APPA_METHOD_CHECK_CE_DATA_EXISTS);
+        runDeviceTests(APPA_PKG, APPA_CLASS, APPA_METHOD_CHECK_DE_DATA_EXISTS);
+        runDeviceTests(APPA_PKG, APPA_CLASS, APPA_METHOD_CHECK_CUR_PROFILE_ACCESSIBLE);
+        runDeviceTests(APPA_PKG, APPA_CLASS, APPA_METHOD_CHECK_REF_PROFILE_NOT_ACCESSIBLE);
+    }
+
+    private boolean isFbeModeEmulated() throws Exception {
+        String mode = getDevice().executeShellCommand("sm get-fbe-mode").trim();
+        if (mode.equals(FBE_MODE_EMULATED)) {
+            return true;
+        } else if (mode.equals(FBE_MODE_NATIVE)) {
+            return false;
+        }
+        fail("Unknown FBE mode: " + mode);
+        return false;
+    }
+
+    @Test
+    public void testDirectBootModeWorks() throws Exception {
+        // Install AppA and verify no data stored
+        new InstallMultiple().addApk(APP_DIRECT_BOOT_A_APK).run();
+        new InstallMultiple().addApk(APPB_APK).run();
+        runDeviceTests(APPA_PKG, APPA_CLASS, APPA_METHOD_CHECK_CE_DATA_DOES_NOT_EXIST);
+        runDeviceTests(APPA_PKG, APPA_CLASS, APPA_METHOD_CHECK_DE_DATA_DOES_NOT_EXIST);
+
+        // Create data in CE and DE storage
+        runDeviceTests(APPA_PKG, APPA_CLASS, APPA_METHOD_CREATE_CE_DE_DATA);
+
+        // Verify CE and DE storage contains data, cur profile is accessible and ref profile is
+        // not accessible
+        runDeviceTests(APPA_PKG, APPA_CLASS, APPA_METHOD_CHECK_CE_DATA_EXISTS);
+        runDeviceTests(APPA_PKG, APPA_CLASS, APPA_METHOD_CHECK_DE_DATA_EXISTS);
+        runDeviceTests(APPA_PKG, APPA_CLASS, APPA_METHOD_CHECK_CUR_PROFILE_ACCESSIBLE);
+        runDeviceTests(APPA_PKG, APPA_CLASS, APPA_METHOD_CHECK_REF_PROFILE_NOT_ACCESSIBLE);
+
+        try {
+            // Setup screenlock
+            getDevice().executeShellCommand("settings put global require_password_to_decrypt 0");
+            getDevice().executeShellCommand("locksettings set-disabled false");
+            getDevice().executeShellCommand("locksettings set-pin 12345");
+
+            // Give enough time for vold to update keys
+            Thread.sleep(15000);
+
+            // Follow DirectBootHostTest, reboot system into known state with keys ejected
+            if (isFbeModeEmulated()) {
+                final String res = getDevice().executeShellCommand("sm set-emulate-fbe true");
+                assertThat(res).contains("Emulation not supported");
+                getDevice().waitForDeviceNotAvailable(30000);
+                getDevice().waitForDeviceOnline(120000);
+            } else {
+                getDevice().rebootUntilOnline();
+            }
+            waitForBootCompleted(getDevice());
+
+            // Verify DE data is still readable and writeable, while CE data is not accessible
+            runDeviceTests(APPA_PKG, APPA_CLASS, APPA_METHOD_CHECK_DE_DATA_EXISTS);
+            runDeviceTests(APPA_PKG, APPA_CLASS, APPA_METHOD_CHECK_CE_DATA_DOES_NOT_EXIST);
+            runDeviceTests(APPA_PKG, APPA_CLASS, APPA_METHOD_CHECK_CUR_PROFILE_ACCESSIBLE);
+            runDeviceTests(APPA_PKG, APPA_CLASS, APPA_METHOD_CHECK_REF_PROFILE_NOT_ACCESSIBLE);
+            // Verify cannot access other apps data
+            runDeviceTests(APPA_PKG, APPA_CLASS, APPA_METHOD_CANNOT_ACCESS_APPB_DIR);
+
+            // Unlock device and verify CE DE data still exist, without killing the process, as
+            // test process usually will be killed after the test
+            runDeviceTests(APPA_PKG, APPA_CLASS, APPA_METHOD_UNLOCK_DEVICE_AND_VERIFY_CE_DE_EXIST);
+
+            // Reboot and verify CE and DE storage contains data, cur profile is accessible and
+            // ref profile is not accessible
+            runDeviceTests(APPA_PKG, APPA_CLASS, APPA_METHOD_CHECK_CE_DATA_EXISTS);
+            runDeviceTests(APPA_PKG, APPA_CLASS, APPA_METHOD_CHECK_DE_DATA_EXISTS);
+            runDeviceTests(APPA_PKG, APPA_CLASS, APPA_METHOD_CHECK_CUR_PROFILE_ACCESSIBLE);
+            runDeviceTests(APPA_PKG, APPA_CLASS, APPA_METHOD_CHECK_REF_PROFILE_NOT_ACCESSIBLE);
+        } finally {
+            try {
+                // Always try to unlock first, then clear screenlock setting
+                try {
+                    runDeviceTests(APPA_PKG, APPA_CLASS, APPA_METHOD_TEST_UNLOCK_DEVICE);
+                } catch (Exception e) {}
+                getDevice().executeShellCommand("locksettings clear --old 12345");
+                getDevice().executeShellCommand("locksettings set-disabled true");
+                getDevice().executeShellCommand(
+                        "settings delete global require_password_to_decrypt");
+            } finally {
+                // Get ourselves back into a known-good state
+                if (isFbeModeEmulated()) {
+                    getDevice().executeShellCommand("sm set-emulate-fbe false");
+                    getDevice().waitForDeviceNotAvailable(30000);
+                    getDevice().waitForDeviceOnline();
+                } else {
+                    getDevice().rebootUntilOnline();
+                }
+                getDevice().waitForDeviceAvailable();
+            }
+        }
     }
 
     @Test
@@ -118,4 +257,12 @@
 
         runDeviceTests(APPB_PKG, APPB_CLASS, APPB_METHOD_CANNOT_ACCESS_APPA_DIR);
     }
+
+    @Test
+    public void testSharedAppAbleToAccessOtherAppDataDir() throws Exception {
+        new InstallMultiple().addApk(APP_SHARED_A_APK).run();
+        new InstallMultiple().addApk(APP_SHARED_B_APK).run();
+
+        runDeviceTests(APPB_PKG, APPB_CLASS, APPB_METHOD_CAN_ACCESS_APPA_DIR);
+    }
 }
diff --git a/hostsidetests/appsecurity/src/android/appsecurity/cts/DeviceIdentifierTest.java b/hostsidetests/appsecurity/src/android/appsecurity/cts/DeviceIdentifierTest.java
index 9687e97..1232edd 100644
--- a/hostsidetests/appsecurity/src/android/appsecurity/cts/DeviceIdentifierTest.java
+++ b/hostsidetests/appsecurity/src/android/appsecurity/cts/DeviceIdentifierTest.java
@@ -48,7 +48,7 @@
         assertNotNull(mBuildHelper);
         assertNull(
                 getDevice().installPackage(mBuildHelper.getTestFile(DEVICE_IDENTIFIER_APK), false,
-                        false));
+                        true));
     }
 
     @Override
diff --git a/hostsidetests/appsecurity/test-apps/AppDataIsolationTestApp/Android.bp b/hostsidetests/appsecurity/test-apps/AppDataIsolationTestApp/Android.bp
index 848aeb8..e415ec7 100644
--- a/hostsidetests/appsecurity/test-apps/AppDataIsolationTestApp/Android.bp
+++ b/hostsidetests/appsecurity/test-apps/AppDataIsolationTestApp/Android.bp
@@ -17,7 +17,7 @@
     defaults: ["cts_support_defaults"],
     srcs: ["common/src/**/*.java", "AppA/src/**/*.java"],
     sdk_version: "current",
-    static_libs: ["androidx.test.rules", "truth-prebuilt", "testng"],
+    static_libs: ["androidx.test.rules", "truth-prebuilt", "testng", "ub-uiautomator"],
     libs: ["android.test.base.stubs"],
     // tag this module as a cts test artifact
     test_suites: [
@@ -33,6 +33,46 @@
 }
 
 android_test_helper_app {
+    name: "CtsAppDataIsolationAppSharedA",
+    defaults: ["cts_support_defaults"],
+    srcs: ["common/src/**/*.java", "AppA/src/**/*.java"],
+    sdk_version: "current",
+    static_libs: ["androidx.test.rules", "truth-prebuilt", "testng", "ub-uiautomator"],
+    libs: ["android.test.base.stubs"],
+    // tag this module as a cts test artifact
+    test_suites: [
+        "cts",
+        "vts",
+        "general-tests",
+    ],
+    certificate: ":cts-testkey1",
+    dex_preopt: {
+        enabled: false,
+    },
+    manifest: "AppA/AndroidManifest_shared.xml",
+}
+
+android_test_helper_app {
+    name: "CtsAppDataIsolationAppDirectBootA",
+    defaults: ["cts_support_defaults"],
+    srcs: ["common/src/**/*.java", "AppA/src/**/*.java"],
+    sdk_version: "current",
+    static_libs: ["androidx.test.rules", "truth-prebuilt", "testng", "ub-uiautomator"],
+    libs: ["android.test.base.stubs"],
+    // tag this module as a cts test artifact
+    test_suites: [
+        "cts",
+        "vts",
+        "general-tests",
+    ],
+    certificate: ":cts-testkey1",
+    dex_preopt: {
+        enabled: false,
+    },
+    manifest: "AppA/AndroidManifest_directboot.xml",
+}
+
+android_test_helper_app {
     name: "CtsAppDataIsolationAppB",
     defaults: ["cts_support_defaults"],
     srcs: ["common/src/**/*.java", "AppB/src/**/*.java"],
@@ -51,3 +91,23 @@
     },
     manifest: "AppB/AndroidManifest.xml",
 }
+
+android_test_helper_app {
+    name: "CtsAppDataIsolationAppSharedB",
+    defaults: ["cts_support_defaults"],
+    srcs: ["common/src/**/*.java", "AppB/src/**/*.java"],
+    sdk_version: "current",
+    static_libs: ["androidx.test.rules", "truth-prebuilt", "testng"],
+    libs: ["android.test.base.stubs"],
+    // tag this module as a cts test artifact
+    test_suites: [
+        "cts",
+        "vts",
+        "general-tests",
+    ],
+    certificate: ":cts-testkey1",
+    dex_preopt: {
+        enabled: false,
+    },
+    manifest: "AppB/AndroidManifest_shared.xml",
+}
\ No newline at end of file
diff --git a/hostsidetests/appsecurity/test-apps/AppDataIsolationTestApp/AppA/AndroidManifest_directboot.xml b/hostsidetests/appsecurity/test-apps/AppDataIsolationTestApp/AppA/AndroidManifest_directboot.xml
new file mode 100644
index 0000000..40ba10c
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/AppDataIsolationTestApp/AppA/AndroidManifest_directboot.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2020 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+       package="com.android.cts.appdataisolation.appa">
+
+    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
+
+    <application android:directBootAware="true">
+        <uses-library android:name="android.test.runner" />
+        <receiver android:name=".DummyReceiver"
+                  android:directBootAware="true"
+                  android:exported="true">
+        </receiver>
+    </application>
+
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+                     android:targetPackage="com.android.cts.appdataisolation.appa"
+                     android:label="Test app data isolation."/>
+</manifest>
diff --git a/hostsidetests/appsecurity/test-apps/AppDataIsolationTestApp/AppA/AndroidManifest_shared.xml b/hostsidetests/appsecurity/test-apps/AppDataIsolationTestApp/AppA/AndroidManifest_shared.xml
new file mode 100644
index 0000000..da23a82
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/AppDataIsolationTestApp/AppA/AndroidManifest_shared.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2020 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+       package="com.android.cts.appdataisolation.appa"
+       android:sharedUserId="com.android.cts.appdataisolation.shareduid">
+
+    <application>
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+                     android:targetPackage="com.android.cts.appdataisolation.appa"
+                     android:label="Test app data isolation."/>
+</manifest>
diff --git a/hostsidetests/appsecurity/test-apps/AppDataIsolationTestApp/AppA/src/com/android/cts/appdataisolation/appa/AppATests.java b/hostsidetests/appsecurity/test-apps/AppDataIsolationTestApp/AppA/src/com/android/cts/appdataisolation/appa/AppATests.java
index 5cdc753..493f198 100644
--- a/hostsidetests/appsecurity/test-apps/AppDataIsolationTestApp/AppA/src/com/android/cts/appdataisolation/appa/AppATests.java
+++ b/hostsidetests/appsecurity/test-apps/AppDataIsolationTestApp/AppA/src/com/android/cts/appdataisolation/appa/AppATests.java
@@ -16,32 +16,46 @@
 
 package com.android.cts.appdataisolation.appa;
 
+import static com.android.cts.appdataisolation.common.FileUtils.assertDirDoesNotExist;
+import static com.android.cts.appdataisolation.common.FileUtils.assertDirIsAccessible;
+import static com.android.cts.appdataisolation.common.FileUtils.assertDirIsNotAccessible;
+import static com.android.cts.appdataisolation.common.FileUtils.assertFileDoesNotExist;
+import static com.android.cts.appdataisolation.common.FileUtils.assertFileExists;
+import static com.android.cts.appdataisolation.common.FileUtils.touchFile;
+
+import static org.junit.Assert.assertTrue;
+
+import android.content.BroadcastReceiver;
 import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.ApplicationInfo;
+import android.os.RemoteException;
+import android.support.test.uiautomator.UiDevice;
+import android.view.KeyEvent;
 
 import androidx.test.filters.SmallTest;
 import androidx.test.platform.app.InstrumentationRegistry;
 
-import com.android.cts.appdataisolation.common.FileUtils;
-
 import org.junit.Before;
 import org.junit.Test;
 
-import java.io.IOException;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
 /*
  * This class is a helper class for AppDataIsolationTests to assert and check data stored in app.
  */
 @SmallTest
 public class AppATests {
 
+    private static final String APPB_PKG = "com.android.cts.appdataisolation.appb";
+
     private final static String CE_DATA_FILE_NAME = "ce_data_file";
     private final static String DE_DATA_FILE_NAME = "de_data_file";
 
-    private static final String JAVA_FILE_PERMISSION_DENIED_MSG =
-            "open failed: EACCES (Permission denied)";
-    private static final String JAVA_FILE_NOT_FOUND_MSG =
-            "open failed: ENOENT (No such file or directory)";
-
     private Context mContext;
+    private UiDevice mDevice;
     private String mCePath;
     private String mDePath;
 
@@ -50,51 +64,114 @@
         mContext = InstrumentationRegistry.getInstrumentation().getContext();
         mCePath = mContext.getApplicationInfo().dataDir;
         mDePath = mContext.getApplicationInfo().deviceProtectedDataDir;
+        mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
     }
 
     @Test
-    public void testCreateCeDeAppData() throws IOException {
-        FileUtils.assertFileDoesNotExist(mCePath, CE_DATA_FILE_NAME);
-        FileUtils.assertFileDoesNotExist(mCePath, DE_DATA_FILE_NAME);
-        FileUtils.assertFileDoesNotExist(mDePath, CE_DATA_FILE_NAME);
-        FileUtils.assertFileDoesNotExist(mDePath, DE_DATA_FILE_NAME);
+    public void testCreateCeDeAppData() throws Exception {
+        assertFileDoesNotExist(mCePath, CE_DATA_FILE_NAME);
+        assertFileDoesNotExist(mCePath, DE_DATA_FILE_NAME);
+        assertFileDoesNotExist(mDePath, CE_DATA_FILE_NAME);
+        assertFileDoesNotExist(mDePath, DE_DATA_FILE_NAME);
 
-        FileUtils.touchFile(mCePath, CE_DATA_FILE_NAME);
-        FileUtils.touchFile(mDePath, DE_DATA_FILE_NAME);
+        touchFile(mCePath, CE_DATA_FILE_NAME);
+        touchFile(mDePath, DE_DATA_FILE_NAME);
 
-        FileUtils.assertFileExists(mCePath, CE_DATA_FILE_NAME);
-        FileUtils.assertFileDoesNotExist(mCePath, DE_DATA_FILE_NAME);
-        FileUtils.assertFileDoesNotExist(mDePath, CE_DATA_FILE_NAME);
-        FileUtils.assertFileExists(mDePath, DE_DATA_FILE_NAME);
+        assertFileExists(mCePath, CE_DATA_FILE_NAME);
+        assertFileDoesNotExist(mCePath, DE_DATA_FILE_NAME);
+        assertFileDoesNotExist(mDePath, CE_DATA_FILE_NAME);
+        assertFileExists(mDePath, DE_DATA_FILE_NAME);
     }
 
     @Test
     public void testAppACeDataExists() {
-        FileUtils.assertFileExists(mCePath, CE_DATA_FILE_NAME);
+        assertFileExists(mCePath, CE_DATA_FILE_NAME);
     }
 
     @Test
     public void testAppACeDataDoesNotExist() {
-        FileUtils.assertFileDoesNotExist(mCePath, CE_DATA_FILE_NAME);
+        assertFileDoesNotExist(mCePath, CE_DATA_FILE_NAME);
     }
 
     @Test
     public void testAppADeDataExists() {
-        FileUtils.assertFileExists(mDePath, DE_DATA_FILE_NAME);
+        assertFileExists(mDePath, DE_DATA_FILE_NAME);
     }
 
     @Test
     public void testAppADeDataDoesNotExist() {
-        FileUtils.assertFileDoesNotExist(mDePath, DE_DATA_FILE_NAME);
+        assertFileDoesNotExist(mDePath, DE_DATA_FILE_NAME);
     }
 
     @Test
     public void testAppACurProfileDataAccessible() {
-        FileUtils.assertDirIsAccessible("/data/misc/profiles/cur/0/" + mContext.getPackageName());
+        assertDirIsAccessible("/data/misc/profiles/cur/0/" + mContext.getPackageName());
     }
 
     @Test
     public void testAppARefProfileDataNotAccessible() {
-        FileUtils.assertDirIsNotAccessible("/data/misc/profiles/ref");
+        assertDirIsNotAccessible("/data/misc/profiles/ref");
+    }
+
+    @Test
+    public void testCannotAccessAppBDataDir() throws Exception {
+        ApplicationInfo applicationInfo = mContext.getPackageManager().getApplicationInfo(APPB_PKG,
+                0);
+        assertDirDoesNotExist(applicationInfo.dataDir);
+        assertDirDoesNotExist(applicationInfo.deviceProtectedDataDir);
+        assertDirDoesNotExist("/data/data/" + APPB_PKG);
+        assertDirDoesNotExist("/data/misc/profiles/cur/0/" + APPB_PKG);
+        assertDirIsNotAccessible("/data/misc/profiles/ref");
+    }
+
+    @Test
+    public void testUnlockDevice() throws Exception {
+        mDevice.wakeUp();
+        mDevice.waitForIdle();
+        mDevice.pressMenu();
+        mDevice.waitForIdle();
+        mDevice.pressKeyCode(KeyEvent.KEYCODE_1);
+        mDevice.pressKeyCode(KeyEvent.KEYCODE_2);
+        mDevice.pressKeyCode(KeyEvent.KEYCODE_3);
+        mDevice.pressKeyCode(KeyEvent.KEYCODE_4);
+        mDevice.pressKeyCode(KeyEvent.KEYCODE_5);
+        mDevice.waitForIdle();
+        mDevice.pressEnter();
+        mDevice.waitForIdle();
+        mDevice.pressHome();
+        mDevice.waitForIdle();
+    }
+
+    @Test
+    public void testAppAUnlockDeviceAndVerifyCeDeDataExist() throws Exception {
+
+        final CountDownLatch unlocked = new CountDownLatch(1);
+        final CountDownLatch bootCompleted = new CountDownLatch(1);
+        final BroadcastReceiver receiver = new BroadcastReceiver() {
+            @Override
+            public void onReceive(Context context, Intent intent) {
+                switch(intent.getAction()) {
+                    case Intent.ACTION_USER_UNLOCKED:
+                        unlocked.countDown();
+                        break;
+                    case Intent.ACTION_BOOT_COMPLETED:
+                        bootCompleted.countDown();
+                        break;
+                }
+            }
+        };
+        mContext.registerReceiver(receiver, new IntentFilter(Intent.ACTION_USER_UNLOCKED));
+        mContext.registerReceiver(receiver, new IntentFilter(Intent.ACTION_BOOT_COMPLETED));
+
+        testUnlockDevice();
+
+        assertTrue("User not unlocked", unlocked.await(1, TimeUnit.MINUTES));
+        assertTrue("No locked boot complete", bootCompleted.await(1, TimeUnit.MINUTES));
+
+        // The test app process should be still running, make sure CE DE now is available
+        testAppACeDataExists();
+        testAppADeDataExists();
+        testAppACurProfileDataAccessible();
+        testAppARefProfileDataNotAccessible();
     }
 }
diff --git a/hostsidetests/appsecurity/test-apps/AppDataIsolationTestApp/AppA/src/com/android/cts/appdataisolation/appa/DummyReceiver.java b/hostsidetests/appsecurity/test-apps/AppDataIsolationTestApp/AppA/src/com/android/cts/appdataisolation/appa/DummyReceiver.java
new file mode 100644
index 0000000..cec7317
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/AppDataIsolationTestApp/AppA/src/com/android/cts/appdataisolation/appa/DummyReceiver.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.cts.appdataisolation.appa;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+
+// Dummy receiver to make this app as direct boot aware.
+public class DummyReceiver extends BroadcastReceiver {
+    @Override
+    public void onReceive(Context context, Intent intent) {
+
+    }
+}
diff --git a/hostsidetests/appsecurity/test-apps/AppDataIsolationTestApp/AppB/AndroidManifest_shared.xml b/hostsidetests/appsecurity/test-apps/AppDataIsolationTestApp/AppB/AndroidManifest_shared.xml
new file mode 100644
index 0000000..8e0ecd8
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/AppDataIsolationTestApp/AppB/AndroidManifest_shared.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2020 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+       package="com.android.cts.appdataisolation.appb"
+       android:sharedUserId="com.android.cts.appdataisolation.shareduid">
+
+    <application>
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+                     android:targetPackage="com.android.cts.appdataisolation.appb"
+                     android:label="Test app data isolation."/>
+</manifest>
diff --git a/hostsidetests/appsecurity/test-apps/AppDataIsolationTestApp/AppB/src/com/android/cts/appdataisolation/appb/AppBTests.java b/hostsidetests/appsecurity/test-apps/AppDataIsolationTestApp/AppB/src/com/android/cts/appdataisolation/appb/AppBTests.java
index 84eea95..2c1dab2 100644
--- a/hostsidetests/appsecurity/test-apps/AppDataIsolationTestApp/AppB/src/com/android/cts/appdataisolation/appb/AppBTests.java
+++ b/hostsidetests/appsecurity/test-apps/AppDataIsolationTestApp/AppB/src/com/android/cts/appdataisolation/appb/AppBTests.java
@@ -16,6 +16,11 @@
 
 package com.android.cts.appdataisolation.appb;
 
+import static com.android.cts.appdataisolation.common.FileUtils.assertDirDoesNotExist;
+import static com.android.cts.appdataisolation.common.FileUtils.assertDirIsAccessible;
+import static com.android.cts.appdataisolation.common.FileUtils.assertDirIsNotAccessible;
+import static com.android.cts.appdataisolation.common.FileUtils.assertFileIsAccessible;
+
 import android.content.Context;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager.NameNotFoundException;
@@ -23,21 +28,16 @@
 import androidx.test.filters.SmallTest;
 import androidx.test.platform.app.InstrumentationRegistry;
 
-import com.android.cts.appdataisolation.common.FileUtils;
-
 import org.junit.Before;
 import org.junit.Test;
 
+import java.io.IOException;
+
 @SmallTest
 public class AppBTests {
 
     private static final String APPA_PKG = "com.android.cts.appdataisolation.appa";
 
-    private static final String JAVA_FILE_PERMISSION_DENIED_MSG =
-            "open failed: EACCES (Permission denied)";
-    private static final String JAVA_FILE_NOT_FOUND_MSG =
-            "open failed: ENOENT (No such file or directory)";
-
     private Context mContext;
 
     @Before
@@ -49,10 +49,21 @@
     public void testCannotAccessAppADataDir() throws NameNotFoundException {
         ApplicationInfo applicationInfo = mContext.getPackageManager().getApplicationInfo(APPA_PKG,
                 0);
-        FileUtils.assertDirDoesNotExist(applicationInfo.dataDir);
-        FileUtils.assertDirDoesNotExist(applicationInfo.deviceProtectedDataDir);
-        FileUtils.assertDirDoesNotExist("/data/data/" + APPA_PKG);
-        FileUtils.assertDirDoesNotExist("/data/misc/profiles/cur/0/" + APPA_PKG);
-        FileUtils.assertDirIsNotAccessible("/data/misc/profiles/ref");
+        assertDirDoesNotExist(applicationInfo.dataDir);
+        assertDirDoesNotExist(applicationInfo.deviceProtectedDataDir);
+        assertDirDoesNotExist("/data/data/" + APPA_PKG);
+        assertDirDoesNotExist("/data/misc/profiles/cur/0/" + APPA_PKG);
+        assertDirIsNotAccessible("/data/misc/profiles/ref");
+    }
+
+    @Test
+    public void testCanAccessAppADataDir() throws NameNotFoundException, IOException {
+        ApplicationInfo applicationInfo = mContext.getPackageManager().getApplicationInfo(APPA_PKG,
+                0);
+        assertDirIsAccessible(applicationInfo.dataDir);
+        assertDirIsAccessible(applicationInfo.deviceProtectedDataDir);
+        assertDirIsAccessible("/data/data/" + APPA_PKG);
+        assertFileIsAccessible("/data/misc/profiles/cur/0/" + APPA_PKG + "/primary.prof");
+        assertDirIsNotAccessible("/data/misc/profiles/ref");
     }
 }
diff --git a/hostsidetests/appsecurity/test-apps/AppDataIsolationTestApp/OWNERS b/hostsidetests/appsecurity/test-apps/AppDataIsolationTestApp/OWNERS
new file mode 100644
index 0000000..e39db39
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/AppDataIsolationTestApp/OWNERS
@@ -0,0 +1,5 @@
+# Bug component: 315013
+rickywai@google.com
+alanstokes@google.com
+zezeozue@google.com
+nandana@google.com
diff --git a/hostsidetests/appsecurity/test-apps/AppDataIsolationTestApp/common/src/com/android/cts/appdataisolation/common/FileUtils.java b/hostsidetests/appsecurity/test-apps/AppDataIsolationTestApp/common/src/com/android/cts/appdataisolation/common/FileUtils.java
index 890174c..6772714 100644
--- a/hostsidetests/appsecurity/test-apps/AppDataIsolationTestApp/common/src/com/android/cts/appdataisolation/common/FileUtils.java
+++ b/hostsidetests/appsecurity/test-apps/AppDataIsolationTestApp/common/src/com/android/cts/appdataisolation/common/FileUtils.java
@@ -66,6 +66,12 @@
         assertFileDoesNotExist(path, "FILE_DOES_NOT_EXIST");
     }
 
+    public static void assertFileIsAccessible(String path) throws IOException {
+        try (FileInputStream is = new FileInputStream(path)) {
+            is.read();
+        }
+    }
+
     public static void assertFileDoesNotExist(String path, String name) {
         // Make sure parent dir exists
         File directory = new File(path);
diff --git a/hostsidetests/appsecurity/test-apps/DeviceIdentifiers/AndroidManifest.xml b/hostsidetests/appsecurity/test-apps/DeviceIdentifiers/AndroidManifest.xml
index bbdcd1e..b683ba1 100644
--- a/hostsidetests/appsecurity/test-apps/DeviceIdentifiers/AndroidManifest.xml
+++ b/hostsidetests/appsecurity/test-apps/DeviceIdentifiers/AndroidManifest.xml
@@ -19,6 +19,8 @@
         package="android.appsecurity.cts.deviceids">
     <uses-sdk android:minSdkVersion="28" android:targetSdkVersion="28" />
 
+    <uses-permission android:name="android.permission.READ_PHONE_STATE" />
+
     <application/>
 
     <instrumentation
diff --git a/hostsidetests/appsecurity/test-apps/DeviceIdentifiers/src/android/appsecurity/cts/deviceids/DeviceIdentifierAppOpTest.java b/hostsidetests/appsecurity/test-apps/DeviceIdentifiers/src/android/appsecurity/cts/deviceids/DeviceIdentifierAppOpTest.java
index 71cd7d2..0534895 100644
--- a/hostsidetests/appsecurity/test-apps/DeviceIdentifiers/src/android/appsecurity/cts/deviceids/DeviceIdentifierAppOpTest.java
+++ b/hostsidetests/appsecurity/test-apps/DeviceIdentifiers/src/android/appsecurity/cts/deviceids/DeviceIdentifierAppOpTest.java
@@ -20,7 +20,10 @@
 import static junit.framework.Assert.fail;
 
 import android.content.Context;
+import android.content.pm.PackageManager;
 import android.os.Build;
+import android.telephony.SubscriptionInfo;
+import android.telephony.SubscriptionManager;
 import android.telephony.TelephonyManager;
 import android.test.AndroidTestCase;
 
@@ -43,9 +46,9 @@
 
     @Test
     public void testAccessToDeviceIdentifiersWithAppOp() throws Exception {
+        Context context = InstrumentationRegistry.getContext();
         TelephonyManager telephonyManager =
-                (TelephonyManager) InstrumentationRegistry.getContext().getSystemService(
-                        Context.TELEPHONY_SERVICE);
+                (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
         try {
             assertEquals(String.format(DEVICE_ID_WITH_APPOP_ERROR_MESSAGE, "getDeviceId"),
                     ShellIdentityUtils.invokeMethodWithShellPermissions(telephonyManager,
@@ -70,6 +73,21 @@
             assertEquals(String.format(DEVICE_ID_WITH_APPOP_ERROR_MESSAGE, "Build#getSerial"),
                     ShellIdentityUtils.invokeStaticMethodWithShellPermissions(Build::getSerial),
                     Build.getSerial());
+            if (context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELEPHONY)) {
+                SubscriptionManager subscriptionManager =
+                        (SubscriptionManager) context.getSystemService(
+                                Context.TELEPHONY_SUBSCRIPTION_SERVICE);
+                int subId = subscriptionManager.getDefaultSubscriptionId();
+                if (subId != SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
+                    SubscriptionInfo expectedSubInfo =
+                            ShellIdentityUtils.invokeMethodWithShellPermissions(subscriptionManager,
+                                    (sm) -> sm.getActiveSubscriptionInfo(subId));
+                    SubscriptionInfo actualSubInfo = subscriptionManager.getActiveSubscriptionInfo(
+                            subId);
+                    assertEquals(String.format(DEVICE_ID_WITH_APPOP_ERROR_MESSAGE, "getIccId"),
+                            expectedSubInfo.getIccId(), actualSubInfo.getIccId());
+                }
+            }
         } catch (SecurityException e) {
             fail("An app with the READ_DEVICE_IDENTIFIERS app op set to allowed must be able to "
                     + "access the device identifiers; caught SecurityException instead: "
diff --git a/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/ApplicationHiddenParentTest.java b/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/ApplicationHiddenParentTest.java
index b9837f5..9e5e8d3 100644
--- a/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/ApplicationHiddenParentTest.java
+++ b/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/ApplicationHiddenParentTest.java
@@ -29,7 +29,9 @@
     private PackageManager mPackageManager;
 
     private static final String SYSTEM_PACKAGE_TO_HIDE = "com.android.keychain";
-    private static final String NON_SYSTEM_PACKAGE_TO_HIDE = "com.android.cts.permissionapp";
+    private static final String NON_SYSTEM_NON_INSTALLED_PACKAGE = "com.android.cts.permissionapp";
+    private static final String NON_SYSTEM_INSTALLED_PACKAGE =
+            "com.android.cts.deviceandprofileowner";
 
     @Override
     protected void setUp() throws Exception {
@@ -71,15 +73,51 @@
     public void testSetApplicationHidden_nonSystemPackage() {
         assertThrows(IllegalArgumentException.class, () -> {
             mParentDevicePolicyManager.setApplicationHidden(ADMIN_RECEIVER_COMPONENT,
-                    NON_SYSTEM_PACKAGE_TO_HIDE, true);
+                    NON_SYSTEM_NON_INSTALLED_PACKAGE, true);
             mParentDevicePolicyManager.isApplicationHidden(ADMIN_RECEIVER_COMPONENT,
-                    NON_SYSTEM_PACKAGE_TO_HIDE);
+                    NON_SYSTEM_NON_INSTALLED_PACKAGE);
         });
         assertThrows(IllegalArgumentException.class, () -> {
             mParentDevicePolicyManager.setApplicationHidden(ADMIN_RECEIVER_COMPONENT,
-                    NON_SYSTEM_PACKAGE_TO_HIDE, false);
+                    NON_SYSTEM_NON_INSTALLED_PACKAGE, false);
             mParentDevicePolicyManager.isApplicationHidden(ADMIN_RECEIVER_COMPONENT,
-                    NON_SYSTEM_PACKAGE_TO_HIDE);
+                    NON_SYSTEM_NON_INSTALLED_PACKAGE);
         });
     }
+
+    public void testSetApplicationHidden_nonSystemPackageStackTrace() {
+        StackTraceElement[] stackTrace1 = new StackTraceElement[0];
+        StackTraceElement[] stackTrace2 = new StackTraceElement[0];
+        String message1 = "";
+        String message2 = "";
+
+        // Scenario 1: Non-system non-installed package
+        try {
+            mParentDevicePolicyManager.setApplicationHidden(ADMIN_RECEIVER_COMPONENT,
+                    NON_SYSTEM_NON_INSTALLED_PACKAGE, true);
+        } catch (IllegalArgumentException e) {
+            stackTrace1 = e.getStackTrace();
+            message1 = e.getMessage();
+        }
+
+        // Scenario 2: Non-system installed package
+        try {
+            mParentDevicePolicyManager.setApplicationHidden(ADMIN_RECEIVER_COMPONENT,
+                    NON_SYSTEM_INSTALLED_PACKAGE, true);
+        } catch (IllegalArgumentException e) {
+            stackTrace2 = e.getStackTrace();
+            message2 = e.getMessage();
+        }
+
+        // Ensure the messages and stack traces of both scenarios are equal
+        assertThat(message1).isEqualTo(message2);
+        assertThat(stackTrace1.length).isEqualTo(stackTrace2.length);
+        for (int i = 0; i < stackTrace1.length; i++) {
+            if (stackTrace1[i].getClassName().equals(this.getClass().getName())) {
+                continue;
+            }
+            assertThat(stackTrace1[i].toString()).isEqualTo(stackTrace2[i].toString());
+        }
+    }
+
 }
diff --git a/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/DeviceIdentifiersTest.java b/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/DeviceIdentifiersTest.java
index ebaf592..855958c 100644
--- a/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/DeviceIdentifiersTest.java
+++ b/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/DeviceIdentifiersTest.java
@@ -20,6 +20,8 @@
 import android.content.Context;
 import android.content.pm.PackageManager;
 import android.os.Build;
+import android.telephony.SubscriptionInfo;
+import android.telephony.SubscriptionManager;
 import android.telephony.TelephonyManager;
 
 import com.android.compatibility.common.util.ShellIdentityUtils;
@@ -69,6 +71,19 @@
             assertEquals(String.format(DEVICE_ID_WITH_PERMISSION_ERROR_MESSAGE, "Build#getSerial"),
                     ShellIdentityUtils.invokeStaticMethodWithShellPermissions(Build::getSerial),
                     Build.getSerial());
+            SubscriptionManager subscriptionManager =
+                    (SubscriptionManager) mContext.getSystemService(
+                            Context.TELEPHONY_SUBSCRIPTION_SERVICE);
+            int subId = subscriptionManager.getDefaultSubscriptionId();
+            if (subId != SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
+                SubscriptionInfo expectedSubInfo =
+                        ShellIdentityUtils.invokeMethodWithShellPermissions(subscriptionManager,
+                                (sm) -> sm.getActiveSubscriptionInfo(subId));
+                SubscriptionInfo actualSubInfo = subscriptionManager.getActiveSubscriptionInfo(
+                        subId);
+                assertEquals(String.format(DEVICE_ID_WITH_PERMISSION_ERROR_MESSAGE, "getIccId"),
+                        expectedSubInfo.getIccId(), actualSubInfo.getIccId());
+            }
         } catch (SecurityException e) {
             fail("The profile owner with the READ_PHONE_STATE permission must be able to access "
                     + "the device IDs: " + e);
diff --git a/hostsidetests/devicepolicy/app/DeviceOwner/src/com/android/cts/deviceowner/DeviceIdentifiersTest.java b/hostsidetests/devicepolicy/app/DeviceOwner/src/com/android/cts/deviceowner/DeviceIdentifiersTest.java
index e5ea597..2370cd0 100644
--- a/hostsidetests/devicepolicy/app/DeviceOwner/src/com/android/cts/deviceowner/DeviceIdentifiersTest.java
+++ b/hostsidetests/devicepolicy/app/DeviceOwner/src/com/android/cts/deviceowner/DeviceIdentifiersTest.java
@@ -20,6 +20,8 @@
 import android.content.Context;
 import android.content.pm.PackageManager;
 import android.os.Build;
+import android.telephony.SubscriptionInfo;
+import android.telephony.SubscriptionManager;
 import android.telephony.TelephonyManager;
 
 import com.android.compatibility.common.util.ShellIdentityUtils;
@@ -65,6 +67,19 @@
             assertEquals(String.format(DEVICE_ID_WITH_PERMISSION_ERROR_MESSAGE, "Build#getSerial"),
                     ShellIdentityUtils.invokeStaticMethodWithShellPermissions(Build::getSerial),
                     Build.getSerial());
+            SubscriptionManager subscriptionManager =
+                    (SubscriptionManager) mContext.getSystemService(
+                            Context.TELEPHONY_SUBSCRIPTION_SERVICE);
+            int subId = subscriptionManager.getDefaultSubscriptionId();
+            if (subId != SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
+                SubscriptionInfo expectedSubInfo =
+                        ShellIdentityUtils.invokeMethodWithShellPermissions(subscriptionManager,
+                                (sm) -> sm.getActiveSubscriptionInfo(subId));
+                SubscriptionInfo actualSubInfo = subscriptionManager.getActiveSubscriptionInfo(
+                        subId);
+                assertEquals(String.format(DEVICE_ID_WITH_PERMISSION_ERROR_MESSAGE, "getIccId"),
+                        expectedSubInfo.getIccId(), actualSubInfo.getIccId());
+            }
         } catch (SecurityException e) {
             fail("The device owner with the READ_PHONE_STATE permission must be able to access "
                     + "the device IDs: " + e);
diff --git a/hostsidetests/statsd/src/android/cts/statsd/atom/HostAtomTests.java b/hostsidetests/statsd/src/android/cts/statsd/atom/HostAtomTests.java
index cdaffd1..753ee07 100644
--- a/hostsidetests/statsd/src/android/cts/statsd/atom/HostAtomTests.java
+++ b/hostsidetests/statsd/src/android/cts/statsd/atom/HostAtomTests.java
@@ -541,7 +541,7 @@
         assertThat(getProperty("ro.product.brand")).isEqualTo(atom.getBrand());
         assertThat(getProperty("ro.product.name")).isEqualTo(atom.getProduct());
         assertThat(getProperty("ro.product.device")).isEqualTo(atom.getDevice());
-        assertThat(getProperty("ro.build.version.release")).isEqualTo(atom.getVersionRelease());
+        assertThat(getProperty("ro.build.version.release_or_codename")).isEqualTo(atom.getVersionRelease());
         assertThat(getProperty("ro.build.id")).isEqualTo(atom.getId());
         assertThat(getProperty("ro.build.version.incremental"))
             .isEqualTo(atom.getVersionIncremental());
diff --git a/tests/app/src/android/app/cts/BaseTileServiceTest.java b/tests/app/src/android/app/cts/BaseTileServiceTest.java
index 011f215..dd163ea 100644
--- a/tests/app/src/android/app/cts/BaseTileServiceTest.java
+++ b/tests/app/src/android/app/cts/BaseTileServiceTest.java
@@ -54,8 +54,7 @@
     protected Context mContext;
 
     final static String DUMP_COMMAND =
-            "dumpsys activity service com.android.systemui/.SystemUIService dependency "
-                    + "DumpController qstilehost";
+            "dumpsys activity service com.android.systemui/.SystemUIService QSTileHost";
 
     // Time between checks for state we expect.
     protected static final long CHECK_DELAY = 250;
diff --git a/tests/camera/AndroidTest.xml b/tests/camera/AndroidTest.xml
index 30ee159..62124d1 100644
--- a/tests/camera/AndroidTest.xml
+++ b/tests/camera/AndroidTest.xml
@@ -15,6 +15,10 @@
 -->
 <configuration description="Config for CTS Camera test cases">
     <option name="test-suite-tag" value="cts" />
+    <target_preparer class="com.android.tradefed.targetprep.DeviceSetup">
+      <option name="force-skip-system-props" value="true" /> <!-- avoid restarting device -->
+      <option name="screen-always-on" value="on" />
+    </target_preparer>
     <option name="config-descriptor:metadata" key="component" value="camera" />
     <option name="config-descriptor:metadata" key="parameter" value="instant_app" />
     <option name="config-descriptor:metadata" key="parameter" value="multi_abi" />
diff --git a/tests/camera/api25test/AndroidTest.xml b/tests/camera/api25test/AndroidTest.xml
index b431952..d561f6c 100644
--- a/tests/camera/api25test/AndroidTest.xml
+++ b/tests/camera/api25test/AndroidTest.xml
@@ -15,6 +15,10 @@
 -->
 <configuration description="Config for CTS Camera API 25 test cases">
     <option name="test-suite-tag" value="cts" />
+    <target_preparer class="com.android.tradefed.targetprep.DeviceSetup">
+      <option name="force-skip-system-props" value="true" /> <!-- avoid restarting device -->
+      <option name="screen-always-on" value="on" />
+    </target_preparer>
     <option name="config-descriptor:metadata" key="component" value="camera" />
     <option name="config-descriptor:metadata" key="parameter" value="not_instant_app" />
     <option name="config-descriptor:metadata" key="parameter" value="multi_abi" />
diff --git a/tests/framework/base/windowmanager/app/src/android/server/wm/app/Components.java b/tests/framework/base/windowmanager/app/src/android/server/wm/app/Components.java
index 6751a1a..8a06a46 100644
--- a/tests/framework/base/windowmanager/app/src/android/server/wm/app/Components.java
+++ b/tests/framework/base/windowmanager/app/src/android/server/wm/app/Components.java
@@ -478,6 +478,10 @@
         public static final String KEY_DISPLAY_ID = "display_id";
     }
 
+    public static class LaunchingActivity {
+        public static final String KEY_FINISH_BEFORE_LAUNCH = "finish_before_launch";
+    }
+
     private static ComponentName component(String className) {
         return component(Components.class, className);
     }
diff --git a/tests/framework/base/windowmanager/app_base/src/android/server/wm/app/LaunchingActivity.java b/tests/framework/base/windowmanager/app_base/src/android/server/wm/app/LaunchingActivity.java
index 87924aa..03d2b43 100644
--- a/tests/framework/base/windowmanager/app_base/src/android/server/wm/app/LaunchingActivity.java
+++ b/tests/framework/base/windowmanager/app_base/src/android/server/wm/app/LaunchingActivity.java
@@ -16,6 +16,8 @@
 
 package android.server.wm.app;
 
+import static android.server.wm.app.Components.LaunchingActivity.KEY_FINISH_BEFORE_LAUNCH;
+
 import android.app.Activity;
 import android.content.Intent;
 import android.os.Bundle;
@@ -31,6 +33,10 @@
         super.onCreate(savedInstanceState);
 
         final Intent intent = getIntent();
+        if (intent != null && intent.getExtras() != null
+                && intent.getExtras().getBoolean(KEY_FINISH_BEFORE_LAUNCH)) {
+            finish();
+        }
         if (savedInstanceState == null && intent != null) {
             launchActivityFromExtras(intent.getExtras());
         }
diff --git a/tests/framework/base/windowmanager/src/android/server/wm/StartActivityTests.java b/tests/framework/base/windowmanager/src/android/server/wm/StartActivityTests.java
index 0b711b0..e9c3f63 100644
--- a/tests/framework/base/windowmanager/src/android/server/wm/StartActivityTests.java
+++ b/tests/framework/base/windowmanager/src/android/server/wm/StartActivityTests.java
@@ -23,13 +23,13 @@
 import static android.server.wm.app.Components.TEST_ACTIVITY;
 import static android.server.wm.app.Components.TRANSLUCENT_ACTIVITY;
 import static android.server.wm.app.Components.TestActivity.COMMAND_NAVIGATE_UP_TO;
+import static android.server.wm.app.Components.TestActivity.COMMAND_START_ACTIVITIES;
 import static android.server.wm.app.Components.TestActivity.EXTRA_INTENT;
 import static android.server.wm.app.Components.TestActivity.EXTRA_INTENTS;
-import static android.server.wm.app.Components.TestActivity.COMMAND_START_ACTIVITIES;
 import static android.server.wm.app27.Components.SDK_27_LAUNCHING_ACTIVITY;
 import static android.server.wm.second.Components.SECOND_ACTIVITY;
-
 import static android.view.Display.DEFAULT_DISPLAY;
+
 import static com.google.common.truth.Truth.assertWithMessage;
 
 import static org.junit.Assert.assertEquals;
@@ -41,6 +41,7 @@
 import android.os.Bundle;
 import android.platform.test.annotations.Presubmit;
 import android.server.wm.CommandSession.ActivitySession;
+import android.server.wm.app.Components;
 import android.server.wm.intent.Activities;
 
 import androidx.test.rule.ActivityTestRule;
@@ -130,6 +131,26 @@
                 TEST_ACTIVITY);
     }
 
+    @Test
+    public void testStartActivityFromFinishingActivity() {
+        // launch TEST_ACTIVITY from LAUNCHING_ACTIVITY
+        getLaunchActivityBuilder()
+                .setTargetActivity(TEST_ACTIVITY)
+                .setFinishBeforeLaunch(true)
+                .execute();
+
+        // launch LAUNCHING_ACTIVITY again
+        getLaunchActivityBuilder()
+                .setTargetActivity(LAUNCHING_ACTIVITY)
+                .setUseInstrumentation()
+                .execute();
+
+        // make sure TEST_ACTIVITY is still on top and resumed
+        mWmState.computeState(TEST_ACTIVITY);
+        mWmState.assertResumedActivity("Test Activity should be remained on top and resumed",
+                TEST_ACTIVITY);
+    }
+
     /**
      * Ensures you can start an {@link Activity} from a non {@link Activity}
      * {@link android.content.Context} when the target sdk is between N and O Mr1.
diff --git a/tests/framework/base/windowmanager/util/src/android/server/wm/ActivityManagerTestBase.java b/tests/framework/base/windowmanager/util/src/android/server/wm/ActivityManagerTestBase.java
index d49f11f..24033a2 100644
--- a/tests/framework/base/windowmanager/util/src/android/server/wm/ActivityManagerTestBase.java
+++ b/tests/framework/base/windowmanager/util/src/android/server/wm/ActivityManagerTestBase.java
@@ -83,6 +83,7 @@
 import static android.server.wm.app.Components.BroadcastReceiverActivity.EXTRA_FINISH_BROADCAST;
 import static android.server.wm.app.Components.BroadcastReceiverActivity.EXTRA_MOVE_BROADCAST_TO_BACK;
 import static android.server.wm.app.Components.LAUNCHING_ACTIVITY;
+import static android.server.wm.app.Components.LaunchingActivity.KEY_FINISH_BEFORE_LAUNCH;
 import static android.server.wm.app.Components.PipActivity.ACTION_EXPAND_PIP;
 import static android.server.wm.app.Components.PipActivity.ACTION_SET_REQUESTED_ORIENTATION;
 import static android.server.wm.app.Components.PipActivity.EXTRA_PIP_ORIENTATION;
@@ -1919,6 +1920,7 @@
         private boolean mMultipleTask;
         private boolean mAllowMultipleInstances = true;
         private boolean mLaunchTaskBehind;
+        private boolean mFinishBeforeLaunch;
         private int mDisplayId = INVALID_DISPLAY;
         private int mWindowingMode = -1;
         private int mActivityType = ACTIVITY_TYPE_UNDEFINED;
@@ -1988,6 +1990,11 @@
             return this;
         }
 
+        public LaunchActivityBuilder setFinishBeforeLaunch(boolean finishBeforeLaunch) {
+            mFinishBeforeLaunch = finishBeforeLaunch;
+            return this;
+        }
+
         public ComponentName getTargetActivity() {
             return mTargetActivity;
         }
@@ -2162,6 +2169,9 @@
             if (mReorderToFront) {
                 commandBuilder.append(" --ez " + KEY_REORDER_TO_FRONT + " true");
             }
+            if (mFinishBeforeLaunch) {
+                commandBuilder.append(" --ez " + KEY_FINISH_BEFORE_LAUNCH + " true");
+            }
             if (mDisplayId != INVALID_DISPLAY) {
                 commandBuilder.append(" --ei " + KEY_DISPLAY_ID + " ").append(mDisplayId);
             }
diff --git a/tests/suspendapps/Android.bp b/tests/suspendapps/Android.bp
new file mode 100644
index 0000000..7cdcad2
--- /dev/null
+++ b/tests/suspendapps/Android.bp
@@ -0,0 +1,20 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+filegroup {
+    name: "CtsSuspendHelpersConstants",
+    srcs: [
+        "test-apps/**/*.java",
+    ],
+}
diff --git a/tests/suspendapps/OWNERS b/tests/suspendapps/OWNERS
new file mode 100644
index 0000000..7455b17
--- /dev/null
+++ b/tests/suspendapps/OWNERS
@@ -0,0 +1,3 @@
+# Bug component: 36137
+suprabh@google.com
+varunshah@google.com
diff --git a/tests/suspendapps/permission/Android.bp b/tests/suspendapps/permission/Android.bp
new file mode 100644
index 0000000..b7ce222
--- /dev/null
+++ b/tests/suspendapps/permission/Android.bp
@@ -0,0 +1,35 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+android_test {
+    name: "CtsSuspendAppsPermissionTestCases",
+    defaults: ["cts_defaults"],
+
+    srcs: [
+        "src/**/*.java",
+        ":CtsSuspendHelpersConstants",
+    ],
+
+    static_libs: [
+        "androidx.test.rules",
+    ],
+
+    sdk_version: "system_current",
+
+    test_suites: [
+        "cts",
+        "vts",
+        "general-tests",
+    ],
+}
diff --git a/tests/suspendapps/permission/AndroidManifest.xml b/tests/suspendapps/permission/AndroidManifest.xml
new file mode 100755
index 0000000..01b334a
--- /dev/null
+++ b/tests/suspendapps/permission/AndroidManifest.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="android.suspendapps.permission.cts">
+
+    <!-- This test checks that PM.setPackagesSuspended cannot be called without having
+    android.permission.SUSPEND_APPS or being an admin -->
+
+    <application android:label="Suspend Apps Permission CTS">
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+                     android:functionalTest="true"
+                     android:targetPackage="android.suspendapps.permission.cts"
+                     android:label="Suspend Apps Permission CTS"/>
+</manifest>
diff --git a/tests/suspendapps/permission/AndroidTest.xml b/tests/suspendapps/permission/AndroidTest.xml
new file mode 100644
index 0000000..fb593aa
--- /dev/null
+++ b/tests/suspendapps/permission/AndroidTest.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<configuration description="Config for CTS Suspend Apps test cases">
+    <option name="test-suite-tag" value="cts" />
+    <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="CtsSuspendAppsPermissionTestCases.apk" />
+        <option name="test-file-name" value="CtsSuspendTestApp.apk" />
+    </target_preparer>
+
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+        <option name="package" value="android.suspendapps.permission.cts" />
+        <option name="runtime-hint" value="10s" />
+    </test>
+
+</configuration>
diff --git a/tests/suspendapps/permission/src/android/suspendapps/permission/cts/NegativePermissionsTest.java b/tests/suspendapps/permission/src/android/suspendapps/permission/cts/NegativePermissionsTest.java
new file mode 100644
index 0000000..0b8b9e5
--- /dev/null
+++ b/tests/suspendapps/permission/src/android/suspendapps/permission/cts/NegativePermissionsTest.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.suspendapps.permission.cts;
+
+import static android.content.pm.PackageManager.RESTRICTION_HIDE_FROM_SUGGESTIONS;
+import static android.content.pm.PackageManager.RESTRICTION_HIDE_NOTIFICATIONS;
+import static android.content.pm.PackageManager.RESTRICTION_NONE;
+
+import static org.junit.Assert.fail;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.content.pm.SuspendDialogInfo;
+import android.util.Log;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.suspendapps.suspendtestapp.Constants;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.concurrent.Callable;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class NegativePermissionsTest {
+    private static final String TAG = NegativePermissionsTest.class.getSimpleName();
+    private static final String[] PACKAGES_TO_SUSPEND = new String[] {
+            Constants.PACKAGE_NAME,
+            Constants.ANDROID_PACKAGE_NAME_2,
+    };
+
+    private PackageManager mPackageManager;
+
+    @Before
+    public void setUp() {
+        final Context context = InstrumentationRegistry.getTargetContext();
+        mPackageManager = context.getPackageManager();
+    }
+
+    private void assertSecurityException(Callable callable, String tag) {
+        try {
+            callable.call();
+        } catch (SecurityException e) {
+            // Passed.
+            return;
+        } catch (Exception e) {
+            Log.e(TAG, "Unexpected exception while calling [" + tag + "]", e);
+            fail("Unexpected exception while calling [" + tag + "]: " + e.getMessage());
+        }
+        fail("Call [" + tag + "] succeeded without permissions");
+    }
+
+    @Test
+    public void setPackagesSuspended() {
+        assertSecurityException(
+                () -> mPackageManager.setPackagesSuspended(PACKAGES_TO_SUSPEND, true, null, null,
+                        (SuspendDialogInfo) null), "setPackagesSuspended:true");
+        assertSecurityException(
+                () -> mPackageManager.setPackagesSuspended(PACKAGES_TO_SUSPEND, false, null, null,
+                        (SuspendDialogInfo) null), "setPackagesSuspended:false");
+    }
+
+    @Test
+    public void setPackagesSuspended_deprecated() {
+        assertSecurityException(
+                () -> mPackageManager.setPackagesSuspended(PACKAGES_TO_SUSPEND, true, null, null,
+                        (String) null), "setPackagesSuspended:true");
+        assertSecurityException(
+                () -> mPackageManager.setPackagesSuspended(PACKAGES_TO_SUSPEND, false, null, null,
+                        (String) null), "setPackagesSuspended:false");
+    }
+
+    @Test
+    public void setDistractingPackageRestrictions() {
+        assertSecurityException(
+                () -> mPackageManager.setDistractingPackageRestrictions(PACKAGES_TO_SUSPEND,
+                        RESTRICTION_HIDE_FROM_SUGGESTIONS),
+                "setDistractingPackageRestrictions:HIDE_FROM_SUGGESTIONS");
+        assertSecurityException(
+                () -> mPackageManager.setDistractingPackageRestrictions(PACKAGES_TO_SUSPEND,
+                        RESTRICTION_HIDE_FROM_SUGGESTIONS | RESTRICTION_HIDE_NOTIFICATIONS),
+                "setDistractingPackageRestrictions:HIDE_FROM_SUGGESTIONS|HIDE_NOTIFICATIONS");
+        assertSecurityException(
+                () -> mPackageManager.setDistractingPackageRestrictions(PACKAGES_TO_SUSPEND,
+                        RESTRICTION_HIDE_NOTIFICATIONS),
+                "setDistractingPackageRestrictions:HIDE_NOTIFICATIONS");
+        assertSecurityException(
+                () -> mPackageManager.setDistractingPackageRestrictions(PACKAGES_TO_SUSPEND,
+                        RESTRICTION_NONE), "setDistractingPackageRestrictions:NONE");
+    }
+}
diff --git a/tests/suspendapps/test-apps/SuspendTestApp/Android.bp b/tests/suspendapps/test-apps/SuspendTestApp/Android.bp
new file mode 100644
index 0000000..c6b3016
--- /dev/null
+++ b/tests/suspendapps/test-apps/SuspendTestApp/Android.bp
@@ -0,0 +1,53 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+android_test_helper_app {
+    name: "CtsSuspendTestApp",
+    defaults: ["cts_support_defaults"],
+
+    srcs: [
+        "src/**/*.java",
+    ],
+
+    sdk_version: "current",
+
+    test_suites: [
+        "cts",
+        "vts",
+        "general-tests",
+    ],
+}
+
+// -----------------------------------------------
+
+android_test_helper_app {
+    name: "CtsSuspendTestApp2",
+    defaults: ["cts_support_defaults"],
+
+    srcs: [
+        "src/**/*.java",
+    ],
+
+    sdk_version: "current",
+
+    test_suites: [
+        "cts",
+        "vts",
+        "general-tests",
+    ],
+
+    aaptflags: [
+        "--rename-manifest-package com.android.suspendapps.suspendtestapp2",
+    ],
+}
diff --git a/tests/suspendapps/test-apps/SuspendTestApp/AndroidManifest.xml b/tests/suspendapps/test-apps/SuspendTestApp/AndroidManifest.xml
new file mode 100644
index 0000000..d8ab015
--- /dev/null
+++ b/tests/suspendapps/test-apps/SuspendTestApp/AndroidManifest.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+        package="com.android.suspendapps.suspendtestapp">
+
+    <application android:label="Suspend Test App">
+        <activity android:name=".SuspendTestActivity"
+                  android:exported="true" />
+        <receiver android:name=".SuspendTestReceiver"
+                  android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.MY_PACKAGE_SUSPENDED" />
+                <action android:name="android.intent.action.MY_PACKAGE_UNSUSPENDED" />
+            </intent-filter>
+        </receiver>
+    </application>
+
+</manifest>
\ No newline at end of file
diff --git a/tests/suspendapps/test-apps/SuspendTestApp/src/com/android/suspendapps/suspendtestapp/Constants.java b/tests/suspendapps/test-apps/SuspendTestApp/src/com/android/suspendapps/suspendtestapp/Constants.java
new file mode 100644
index 0000000..a4dbd26
--- /dev/null
+++ b/tests/suspendapps/test-apps/SuspendTestApp/src/com/android/suspendapps/suspendtestapp/Constants.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.suspendapps.suspendtestapp;
+
+public interface Constants {
+    String PACKAGE_NAME = "com.android.suspendapps.suspendtestapp";
+    String ANDROID_PACKAGE_NAME_2 = "com.android.suspendapps.suspendtestapp2";
+    String INSTRUMENTATION_PACKAGE = "android.suspendapps.cts";
+}
diff --git a/tests/suspendapps/test-apps/SuspendTestApp/src/com/android/suspendapps/suspendtestapp/SuspendTestActivity.java b/tests/suspendapps/test-apps/SuspendTestApp/src/com/android/suspendapps/suspendtestapp/SuspendTestActivity.java
new file mode 100644
index 0000000..a3a6879
--- /dev/null
+++ b/tests/suspendapps/test-apps/SuspendTestApp/src/com/android/suspendapps/suspendtestapp/SuspendTestActivity.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.suspendapps.suspendtestapp;
+
+import static com.android.suspendapps.suspendtestapp.Constants.INSTRUMENTATION_PACKAGE;
+import static com.android.suspendapps.suspendtestapp.Constants.PACKAGE_NAME;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.util.Log;
+
+public class SuspendTestActivity extends Activity {
+    private static final String TAG = SuspendTestActivity.class.getSimpleName();
+    private static final String ACTION_FINISH_TEST_ACTIVITY =
+            PACKAGE_NAME + ".action.FINISH_TEST_ACTIVITY";
+
+    public static final String ACTION_REPORT_TEST_ACTIVITY_STOPPED =
+            PACKAGE_NAME + ".action.REPORT_TEST_ACTIVITY_STOPPED";
+    public static final String ACTION_REPORT_TEST_ACTIVITY_STARTED =
+            PACKAGE_NAME + ".action.REPORT_TEST_ACTIVITY_STARTED";
+
+    private boolean mReportStartStop;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        Log.d(TAG, "onCreate");
+        super.onCreate(savedInstanceState);
+        if (ACTION_FINISH_TEST_ACTIVITY.equals(getIntent().getAction())) {
+            finish();
+        } else {
+            mReportStartStop = true;
+        }
+    }
+
+    @Override
+    protected void onStart() {
+        Log.d(TAG, "onStart");
+        super.onStart();
+        if (mReportStartStop) {
+            final Intent reportStart = new Intent(ACTION_REPORT_TEST_ACTIVITY_STARTED)
+                    .putExtras(getIntent())
+                    .setPackage(INSTRUMENTATION_PACKAGE);
+            sendBroadcast(reportStart);
+        }
+    }
+
+    @Override
+    public void onStop() {
+        Log.d(TAG, "onStop");
+        super.onStop();
+        if (mReportStartStop) {
+            final Intent reportStop = new Intent(ACTION_REPORT_TEST_ACTIVITY_STOPPED)
+                    .setPackage(INSTRUMENTATION_PACKAGE);
+            sendBroadcast(reportStop);
+        }
+    }
+}
diff --git a/tests/suspendapps/test-apps/SuspendTestApp/src/com/android/suspendapps/suspendtestapp/SuspendTestReceiver.java b/tests/suspendapps/test-apps/SuspendTestApp/src/com/android/suspendapps/suspendtestapp/SuspendTestReceiver.java
new file mode 100644
index 0000000..db89271
--- /dev/null
+++ b/tests/suspendapps/test-apps/SuspendTestApp/src/com/android/suspendapps/suspendtestapp/SuspendTestReceiver.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.suspendapps.suspendtestapp;
+
+import static com.android.suspendapps.suspendtestapp.Constants.INSTRUMENTATION_PACKAGE;
+import static com.android.suspendapps.suspendtestapp.Constants.PACKAGE_NAME;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.os.Bundle;
+import android.util.Log;
+
+public class SuspendTestReceiver extends BroadcastReceiver {
+    private static final String TAG = SuspendTestReceiver.class.getSimpleName();
+
+    public static final String EXTRA_SUSPENDED = PACKAGE_NAME + ".extra.SUSPENDED";
+    public static final String ACTION_GET_SUSPENDED_STATE =
+            PACKAGE_NAME + ".action.GET_SUSPENDED_STATE";
+    public static final String EXTRA_SUSPENDED_APP_EXTRAS =
+            PACKAGE_NAME + ".extra.SUSPENDED_APP_EXTRAS";
+    public static final String ACTION_REPORT_MY_PACKAGE_SUSPENDED =
+            PACKAGE_NAME + ".action.REPORT_MY_PACKAGE_SUSPENDED";
+    public static final String ACTION_REPORT_MY_PACKAGE_UNSUSPENDED =
+            PACKAGE_NAME + ".action.REPORT_MY_PACKAGE_UNSUSPENDED";
+
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        final PackageManager packageManager = context.getPackageManager();
+        Log.d(TAG, "Received action " + intent.getAction());
+        final Bundle appExtras;
+        switch (intent.getAction()) {
+            case ACTION_GET_SUSPENDED_STATE:
+                final Bundle result = new Bundle();
+                final boolean suspended = packageManager.isPackageSuspended();
+                appExtras = packageManager.getSuspendedPackageAppExtras();
+                result.putBoolean(EXTRA_SUSPENDED, suspended);
+                result.putBundle(EXTRA_SUSPENDED_APP_EXTRAS, appExtras);
+                setResult(0, null, result);
+                break;
+            case Intent.ACTION_MY_PACKAGE_SUSPENDED:
+                appExtras = intent.getBundleExtra(Intent.EXTRA_SUSPENDED_PACKAGE_EXTRAS);
+                final Intent reportSuspendIntent = new Intent(ACTION_REPORT_MY_PACKAGE_SUSPENDED)
+                        .putExtra(EXTRA_SUSPENDED_APP_EXTRAS, appExtras)
+                        .setPackage(INSTRUMENTATION_PACKAGE);
+                context.sendBroadcast(reportSuspendIntent);
+                break;
+            case Intent.ACTION_MY_PACKAGE_UNSUSPENDED:
+                final Intent reportUnsuspendIntent =
+                        new Intent(ACTION_REPORT_MY_PACKAGE_UNSUSPENDED)
+                        .setPackage(INSTRUMENTATION_PACKAGE)
+                        .putExtra(Intent.EXTRA_PACKAGE_NAME, context.getPackageName());
+                context.sendBroadcast(reportUnsuspendIntent);
+                break;
+            default:
+                Log.e(TAG, "Unknown action: " + intent.getAction());
+        }
+    }
+}
diff --git a/tests/suspendapps/test-apps/TestDeviceAdmin/Android.bp b/tests/suspendapps/test-apps/TestDeviceAdmin/Android.bp
new file mode 100644
index 0000000..36fcc32
--- /dev/null
+++ b/tests/suspendapps/test-apps/TestDeviceAdmin/Android.bp
@@ -0,0 +1,28 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+android_test_helper_app {
+    name: "CtsSuspendTestDeviceAdmin",
+    defaults: ["cts_support_defaults"],
+
+    srcs: ["src/**/*.java"],
+
+    sdk_version: "current",
+
+    test_suites: [
+        "cts",
+        "vts",
+        "general-tests",
+    ],
+}
diff --git a/tests/suspendapps/test-apps/TestDeviceAdmin/AndroidManifest.xml b/tests/suspendapps/test-apps/TestDeviceAdmin/AndroidManifest.xml
new file mode 100644
index 0000000..3368398
--- /dev/null
+++ b/tests/suspendapps/test-apps/TestDeviceAdmin/AndroidManifest.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+        package="com.android.suspendapps.testdeviceadmin" >
+
+    <application android:label="CTS Device Admin" android:testOnly="true">
+        <receiver
+            android:name=".TestDeviceAdmin"
+            android:permission="android.permission.BIND_DEVICE_ADMIN">
+            <meta-data
+                android:name="android.app.device_admin"
+                android:resource="@xml/device_admin"/>
+            <intent-filter>
+                <action android:name="android.app.action.DEVICE_ADMIN_ENABLED" />
+            </intent-filter>
+        </receiver>
+        <receiver
+            android:name=".TestCommsReceiver"
+            android:exported="true" />
+    </application>
+</manifest>
diff --git a/tests/suspendapps/test-apps/TestDeviceAdmin/res/xml/device_admin.xml b/tests/suspendapps/test-apps/TestDeviceAdmin/res/xml/device_admin.xml
new file mode 100644
index 0000000..bde48c4
--- /dev/null
+++ b/tests/suspendapps/test-apps/TestDeviceAdmin/res/xml/device_admin.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<device-admin xmlns:android="http://schemas.android.com/apk/res/android">
+    <uses-policies>
+        <limit-password />
+        <watch-login />
+        <reset-password />
+        <force-lock />
+        <wipe-data />
+        <expire-password />
+        <encrypted-storage />
+        <disable-camera />
+        <disable-keyguard-features />
+    </uses-policies>
+</device-admin>
diff --git a/tests/suspendapps/test-apps/TestDeviceAdmin/src/com/android/suspendapps/testdeviceadmin/TestCommsReceiver.java b/tests/suspendapps/test-apps/TestDeviceAdmin/src/com/android/suspendapps/testdeviceadmin/TestCommsReceiver.java
new file mode 100644
index 0000000..d864775
--- /dev/null
+++ b/tests/suspendapps/test-apps/TestDeviceAdmin/src/com/android/suspendapps/testdeviceadmin/TestCommsReceiver.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.suspendapps.testdeviceadmin;
+
+import static android.app.Activity.RESULT_OK;
+
+import android.app.admin.DevicePolicyManager;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.util.Log;
+
+import java.util.Arrays;
+
+public class TestCommsReceiver extends BroadcastReceiver {
+    private static final String TAG = TestCommsReceiver.class.getSimpleName();
+
+    public static final String PACKAGE_NAME = "com.android.suspendapps.testdeviceadmin";
+    public static final String ACTION_UNSUSPEND = PACKAGE_NAME + ".action.UNSUSPEND";
+    public static final String ACTION_SUSPEND = PACKAGE_NAME + ".action.SUSPEND";
+    public static final String ACTION_BLOCK_UNINSTALL = PACKAGE_NAME + ".action.BLOCK_UNINSTALL";
+    public static final String ACTION_UNBLOCK_UNINSTALL =
+            PACKAGE_NAME + ".action.UNBLOCK_UNINSTALL";
+    public static final String ACTION_ADD_USER_RESTRICTION =
+            PACKAGE_NAME + ".action.ADD_USER_RESTRICTION";
+    public static final String ACTION_CLEAR_USER_RESTRICTION =
+            PACKAGE_NAME + ".action.CLEAR_USER_RESTRICTION";
+    public static final String EXTRA_USER_RESTRICTION = PACKAGE_NAME + ".extra.USER_RESTRICTION";
+
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        Log.d(TAG, "Received request " + intent.getAction());
+
+        final DevicePolicyManager dpm = context.getSystemService(DevicePolicyManager.class);
+        final ComponentName deviceAdmin = new ComponentName(PACKAGE_NAME,
+                TestDeviceAdmin.class.getName());
+        final String packageName = intent.getStringExtra(Intent.EXTRA_PACKAGE_NAME);
+        final String userRestriction = intent.getStringExtra(EXTRA_USER_RESTRICTION);
+        boolean suspend = false;
+        boolean block = false;
+        switch (intent.getAction()) {
+            case ACTION_SUSPEND:
+                suspend = true;
+                // Intentional fall-through
+            case ACTION_UNSUSPEND:
+                if (packageName == null) {
+                    Log.e(TAG, "Need package to complete suspend/unsuspend request");
+                    break;
+                }
+                final String[] errored = dpm.setPackagesSuspended(deviceAdmin,
+                        new String[]{packageName}, suspend);
+                if (errored.length > 0) {
+                    Log.e(TAG, "Could not update packages: " + Arrays.toString(errored)
+                            + " for request: " + intent.getAction());
+                    break;
+                }
+                setResultCode(RESULT_OK);
+                break;
+            case ACTION_BLOCK_UNINSTALL:
+                block = true;
+                // Intentional fall-through
+            case ACTION_UNBLOCK_UNINSTALL:
+                if (packageName == null) {
+                    Log.e(TAG, "Need package to complete block/unblock uninstall request");
+                    break;
+                }
+                dpm.setUninstallBlocked(deviceAdmin, packageName, block);
+                setResultCode(RESULT_OK);
+                break;
+            case ACTION_ADD_USER_RESTRICTION:
+                if (userRestriction == null) {
+                    Log.e(TAG, "No user restriction provided to set");
+                    break;
+                }
+                dpm.addUserRestriction(deviceAdmin, userRestriction);
+                setResultCode(RESULT_OK);
+                break;
+            case ACTION_CLEAR_USER_RESTRICTION:
+                if (userRestriction == null) {
+                    Log.e(TAG, "No user restriction provided to clear");
+                    break;
+                }
+                dpm.clearUserRestriction(deviceAdmin, userRestriction);
+                setResultCode(RESULT_OK);
+                break;
+        }
+    }
+}
diff --git a/tests/suspendapps/test-apps/TestDeviceAdmin/src/com/android/suspendapps/testdeviceadmin/TestDeviceAdmin.java b/tests/suspendapps/test-apps/TestDeviceAdmin/src/com/android/suspendapps/testdeviceadmin/TestDeviceAdmin.java
new file mode 100644
index 0000000..400eef6
--- /dev/null
+++ b/tests/suspendapps/test-apps/TestDeviceAdmin/src/com/android/suspendapps/testdeviceadmin/TestDeviceAdmin.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.suspendapps.testdeviceadmin;
+
+import android.app.admin.DeviceAdminReceiver;
+
+public class TestDeviceAdmin extends DeviceAdminReceiver {
+}
diff --git a/tests/suspendapps/tests/Android.bp b/tests/suspendapps/tests/Android.bp
new file mode 100644
index 0000000..15a239e
--- /dev/null
+++ b/tests/suspendapps/tests/Android.bp
@@ -0,0 +1,37 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+android_test {
+    name: "CtsSuspendAppsTestCases",
+    defaults: ["cts_defaults"],
+
+    srcs: [
+        "src/**/*.java",
+        ":CtsSuspendHelpersConstants",
+    ],
+
+    static_libs: [
+        "ub-uiautomator",
+        "androidx.test.rules",
+        "compatibility-device-util-axt",
+    ],
+
+    platform_apis: true,
+
+    test_suites: [
+        "cts",
+        "vts",
+        "general-tests",
+    ],
+}
diff --git a/tests/suspendapps/tests/AndroidManifest.xml b/tests/suspendapps/tests/AndroidManifest.xml
new file mode 100755
index 0000000..61dd2f2
--- /dev/null
+++ b/tests/suspendapps/tests/AndroidManifest.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="android.suspendapps.cts">
+
+    <application android:label="CTS Suspend Apps Test">
+        <activity android:name=".SuspendedDetailsActivity"
+                  android:permission="android.permission.SEND_SHOW_SUSPENDED_APP_DETAILS">
+            <intent-filter>
+                <action android:name="android.intent.action.SHOW_SUSPENDED_APP_DETAILS" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+        </activity>
+
+        <receiver android:name=".UnsuspendReceiver">
+            <intent-filter>
+                <action android:name="android.intent.action.PACKAGE_UNSUSPENDED_MANUALLY" />
+            </intent-filter>
+        </receiver>
+
+    </application>
+
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+                     android:functionalTest="true"
+                     android:targetPackage="android.suspendapps.cts"
+                     android:label="CTS Suspend Apps Test"/>
+</manifest>
diff --git a/tests/suspendapps/tests/AndroidTest.xml b/tests/suspendapps/tests/AndroidTest.xml
new file mode 100644
index 0000000..70d2aaf
--- /dev/null
+++ b/tests/suspendapps/tests/AndroidTest.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<configuration description="Config for CTS Suspend Apps test cases">
+    <option name="test-suite-tag" value="cts" />
+    <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" />
+    <!-- TODO (b/150741315): Change when the module is ready for secondary user -->
+    <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="install-arg" value="-t" />
+        <option name="test-file-name" value="CtsSuspendAppsTestCases.apk" />
+        <option name="test-file-name" value="CtsSuspendTestApp.apk" />
+        <option name="test-file-name" value="CtsSuspendTestApp2.apk" />
+        <option name="test-file-name" value="CtsSuspendTestDeviceAdmin.apk" />
+    </target_preparer>
+
+    <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
+        <!-- This is done to bring the app out of the stopped state -->
+        <option name="run-command" value="am start -a com.android.suspendapps.suspendtestapp.action.FINISH_TEST_ACTIVITY com.android.suspendapps.suspendtestapp/.SuspendTestActivity" />
+    </target_preparer>
+
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+        <option name="package" value="android.suspendapps.cts" />
+        <option name="runtime-hint" value="30s" />
+    </test>
+
+</configuration>
diff --git a/tests/suspendapps/tests/res/drawable/ic_settings.xml b/tests/suspendapps/tests/res/drawable/ic_settings.xml
new file mode 100644
index 0000000..138a680
--- /dev/null
+++ b/tests/suspendapps/tests/res/drawable/ic_settings.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24.0"
+    android:viewportHeight="24.0"
+    android:tint="?android:attr/colorAccent">
+    <path
+        android:fillColor="#FF000000"
+        android:pathData="M21.4 14.2l-1.94-1.45c.03-.25 .04 -.5 .04 -.76s-.01-.51-.04-.76L21.4 9.8c.42-.31
+.52 -.94 .24 -1.41l-1.6-2.76c-.28-.48-.88-.7-1.36-.5l-2.14 .91
+c-.48-.37-1.01-.68-1.57-.92l-.27-2.2c-.06-.52-.56-.92-1.11-.92h-3.18c-.55 0-1.05
+.4 -1.11 .92 l-.26 2.19c-.57 .24 -1.1 .55 -1.58 .92 l-2.14-.91c-.48-.2-1.08 .02
+-1.36 .5 l-1.6 2.76c-.28 .48 -.18 1.1 .24 1.42l1.94 1.45c-.03 .24 -.04 .49 -.04
+.75 s.01 .51 .04 .76 L2.6 14.2c-.42 .31 -.52 .94 -.24 1.41l1.6 2.76c.28 .48 .88
+.7 1.36 .5 l2.14-.91c.48 .37 1.01 .68 1.57 .92 l.27 2.19c.06 .53 .56 .93 1.11
+.93 h3.18c.55 0 1.04-.4 1.11-.92l.27-2.19c.56-.24 1.09-.55 1.57-.92l2.14 .91
+c.48 .2 1.08-.02 1.36-.5l1.6-2.76c.28-.48 .18 -1.1-.24-1.42zM12 15.5c-1.93
+0-3.5-1.57-3.5-3.5s1.57-3.5 3.5-3.5 3.5 1.57 3.5 3.5-1.57 3.5-3.5 3.5z"/>
+</vector>
diff --git a/tests/suspendapps/tests/res/values/strings.xml b/tests/suspendapps/tests/res/values/strings.xml
new file mode 100755
index 0000000..a19edbc
--- /dev/null
+++ b/tests/suspendapps/tests/res/values/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="dialog_title">App is paused</string>
+    <string name="dialog_message">You cannot use %1$s anymore today</string>
+    <string name="more_details_button_text">Obscure text</string>
+    <string name="unsuspend_button_text">Escape hatch</string>
+</resources>
diff --git a/tests/suspendapps/tests/src/android/suspendapps/cts/AppCommunicationReceiver.java b/tests/suspendapps/tests/src/android/suspendapps/cts/AppCommunicationReceiver.java
new file mode 100644
index 0000000..ec07d3c
--- /dev/null
+++ b/tests/suspendapps/tests/src/android/suspendapps/cts/AppCommunicationReceiver.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.suspendapps.cts;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Handler;
+import android.util.Log;
+
+import java.util.concurrent.SynchronousQueue;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Wrapper around {@link BroadcastReceiver} to listen for communication from the test app.
+ */
+public class AppCommunicationReceiver extends BroadcastReceiver {
+    private static final String TAG = AppCommunicationReceiver.class.getSimpleName();
+    private Context mContext;
+    private boolean mRegistered;
+    private SynchronousQueue<Intent> mIntentQueue = new SynchronousQueue<>();
+
+    AppCommunicationReceiver(Context context) {
+        this.mContext = context;
+    }
+
+    void register(Handler handler, String... actions) {
+        mRegistered = true;
+        final IntentFilter intentFilter = new IntentFilter();
+        for (String action : actions) {
+            intentFilter.addAction(action);
+        }
+        mContext.registerReceiver(this, intentFilter, null, handler);
+    }
+
+    void unregister() {
+        if (mRegistered) {
+            mRegistered = false;
+            mContext.unregisterReceiver(this);
+        }
+    }
+
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        Log.d(TAG, "AppCommunicationReceiver#onReceive: " + intent.getAction());
+        try {
+            mIntentQueue.offer(intent, 5, TimeUnit.SECONDS);
+        } catch (InterruptedException ie) {
+            throw new RuntimeException("Receiver thread interrupted", ie);
+        }
+    }
+
+    Intent pollForIntent(long secondsToWait) {
+        if (!mRegistered) {
+            throw new IllegalStateException("Receiver not registered");
+        }
+        final Intent intent;
+        try {
+            intent = mIntentQueue.poll(secondsToWait, TimeUnit.SECONDS);
+        } catch (InterruptedException ie) {
+            throw new RuntimeException("Interrupted while waiting for app broadcast", ie);
+        }
+        return intent;
+    }
+
+    void drainPendingBroadcasts() {
+        while (pollForIntent(5) != null) {
+            // Repeat until no incoming intent.
+        }
+    }
+}
diff --git a/tests/suspendapps/tests/src/android/suspendapps/cts/Constants.java b/tests/suspendapps/tests/src/android/suspendapps/cts/Constants.java
new file mode 100644
index 0000000..6604df4
--- /dev/null
+++ b/tests/suspendapps/tests/src/android/suspendapps/cts/Constants.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.suspendapps.cts;
+
+import com.android.suspendapps.testdeviceadmin.TestCommsReceiver;
+import com.android.suspendapps.testdeviceadmin.TestDeviceAdmin;
+
+public interface Constants {
+    String PACKAGE_NAME = "android.suspendapps.cts";
+    String ACTION_REPORT_MORE_DETAILS_ACTIVITY_STARTED =
+            PACKAGE_NAME + ".action.REPORT_MORE_DETAILS_ACTIVITY_STARTED";
+    String ACTION_REPORT_PACKAGE_UNSUSPENDED_MANUALLY =
+            PACKAGE_NAME + ".action.REPORT_PACKAGE_UNSUSPENDED_MANUALLY";
+    String EXTRA_RECEIVED_PACKAGE_NAME = PACKAGE_NAME + ".extra.RECEIVED_PACKAGE_NAME";
+    String TEST_APP_PACKAGE_NAME = com.android.suspendapps.suspendtestapp.Constants.PACKAGE_NAME;
+    String TEST_APP_2_PACKAGE_NAME =
+            com.android.suspendapps.suspendtestapp.Constants.ANDROID_PACKAGE_NAME_2;
+    String[] TEST_PACKAGE_ARRAY = new String[] {TEST_APP_PACKAGE_NAME};
+    String[] ALL_TEST_PACKAGES = new String[] {TEST_APP_PACKAGE_NAME, TEST_APP_2_PACKAGE_NAME};
+    String DEVICE_ADMIN_PACKAGE = TestCommsReceiver.PACKAGE_NAME;
+    String DEVICE_ADMIN_COMPONENT =
+            DEVICE_ADMIN_PACKAGE + "/" + TestDeviceAdmin.class.getName();
+}
diff --git a/tests/suspendapps/tests/src/android/suspendapps/cts/DistractingPackageTest.java b/tests/suspendapps/tests/src/android/suspendapps/cts/DistractingPackageTest.java
new file mode 100644
index 0000000..01b7f5a
--- /dev/null
+++ b/tests/suspendapps/tests/src/android/suspendapps/cts/DistractingPackageTest.java
@@ -0,0 +1,156 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.suspendapps.cts;
+
+import static android.content.Intent.EXTRA_PACKAGE_NAME;
+import static android.content.pm.PackageManager.RESTRICTION_HIDE_FROM_SUGGESTIONS;
+import static android.content.pm.PackageManager.RESTRICTION_HIDE_NOTIFICATIONS;
+import static android.suspendapps.cts.Constants.ALL_TEST_PACKAGES;
+import static android.suspendapps.cts.Constants.DEVICE_ADMIN_COMPONENT;
+import static android.suspendapps.cts.Constants.TEST_APP_PACKAGE_NAME;
+import static android.suspendapps.cts.Constants.TEST_PACKAGE_ARRAY;
+import static android.suspendapps.cts.SuspendTestUtils.addAndAssertProfileOwner;
+import static android.suspendapps.cts.SuspendTestUtils.createSingleKeyBundle;
+import static android.suspendapps.cts.SuspendTestUtils.removeDeviceAdmin;
+import static android.suspendapps.cts.SuspendTestUtils.requestDpmAction;
+
+import static com.android.suspendapps.testdeviceadmin.TestCommsReceiver.ACTION_ADD_USER_RESTRICTION;
+import static com.android.suspendapps.testdeviceadmin.TestCommsReceiver.ACTION_BLOCK_UNINSTALL;
+import static com.android.suspendapps.testdeviceadmin.TestCommsReceiver.ACTION_UNBLOCK_UNINSTALL;
+import static com.android.suspendapps.testdeviceadmin.TestCommsReceiver.EXTRA_USER_RESTRICTION;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeTrue;
+
+import android.annotation.NonNull;
+import android.app.admin.DevicePolicyManager;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.pm.LauncherApps;
+import android.content.pm.PackageManager;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.UserHandle;
+import android.os.UserManager;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.compatibility.common.util.FeatureUtil;
+import com.android.compatibility.common.util.SystemUtil;
+import com.android.internal.util.ArrayUtils;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Arrays;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class DistractingPackageTest {
+    private Context mContext;
+    private PackageManager mPackageManager;
+    private Handler mHandler;
+
+    @Before
+    public void setUp() {
+        mContext = InstrumentationRegistry.getTargetContext();
+        mPackageManager = mContext.getPackageManager();
+        mHandler = new Handler(Looper.getMainLooper());
+    }
+
+    private int getCurrentUserId() {
+        final String result = SystemUtil.runShellCommand("am get-current-user",
+                output -> !output.isEmpty());
+        return Integer.parseInt(result.trim());
+    }
+
+    private void setDistractionFlagsAndAssertResult(String[] packagesToRestrict,
+            int distractionFlags, @NonNull String[] expectedToFail) throws Exception {
+        final String[] failed = SystemUtil.callWithShellPermissionIdentity(
+                () -> mPackageManager.setDistractingPackageRestrictions(packagesToRestrict,
+                        distractionFlags));
+        if (failed == null || failed.length != expectedToFail.length
+                || !ArrayUtils.containsAll(failed, expectedToFail)) {
+            fail("setDistractingPackageRestrictions failure: failed packages: " + Arrays.toString(
+                    failed) + "; expected to fail: " + Arrays.toString(expectedToFail));
+        }
+    }
+
+    @Test
+    public void testShouldHideFromSuggestions() throws Exception {
+        final int currentUserId = getCurrentUserId();
+        final LauncherApps launcherApps = mContext.getSystemService(LauncherApps.class);
+        assertFalse("shouldHideFromSuggestions true before setting the flag",
+                launcherApps.shouldHideFromSuggestions(TEST_APP_PACKAGE_NAME,
+                        UserHandle.of(currentUserId)));
+        setDistractionFlagsAndAssertResult(TEST_PACKAGE_ARRAY, RESTRICTION_HIDE_FROM_SUGGESTIONS,
+                ArrayUtils.emptyArray(String.class));
+        assertTrue("shouldHideFromSuggestions false after setting the flag",
+                launcherApps.shouldHideFromSuggestions(TEST_APP_PACKAGE_NAME,
+                        UserHandle.of(currentUserId)));
+    }
+
+    @Test
+    public void testCannotRestrictWhenUninstallBlocked() throws Exception {
+        assumeTrue(FeatureUtil.hasSystemFeature(PackageManager.FEATURE_DEVICE_ADMIN));
+        addAndAssertProfileOwner();
+        assertTrue("Block uninstall request failed", requestDpmAction(ACTION_BLOCK_UNINSTALL,
+                createSingleKeyBundle(EXTRA_PACKAGE_NAME, TEST_APP_PACKAGE_NAME), mHandler));
+        setDistractionFlagsAndAssertResult(ALL_TEST_PACKAGES, RESTRICTION_HIDE_NOTIFICATIONS,
+                TEST_PACKAGE_ARRAY);
+    }
+
+    private void assertCannotRestrictUnderUserRestriction(String userRestriction) throws Exception {
+        addAndAssertProfileOwner();
+        final Bundle extras = createSingleKeyBundle(EXTRA_USER_RESTRICTION, userRestriction);
+        assertTrue("Request to add restriction" + userRestriction + " failed",
+                requestDpmAction(ACTION_ADD_USER_RESTRICTION, extras, mHandler));
+        setDistractionFlagsAndAssertResult(ALL_TEST_PACKAGES, RESTRICTION_HIDE_FROM_SUGGESTIONS,
+                ALL_TEST_PACKAGES);
+    }
+
+    @Test
+    public void testCannotRestrictUnderDisallowAppsControl() throws Exception {
+        assumeTrue(FeatureUtil.hasSystemFeature(PackageManager.FEATURE_DEVICE_ADMIN));
+        assertCannotRestrictUnderUserRestriction(UserManager.DISALLOW_APPS_CONTROL);
+    }
+
+    @Test
+    public void testCannotRestrictUnderDisallowUninstallApps() throws Exception {
+        assumeTrue(FeatureUtil.hasSystemFeature(PackageManager.FEATURE_DEVICE_ADMIN));
+        assertCannotRestrictUnderUserRestriction(UserManager.DISALLOW_UNINSTALL_APPS);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        final DevicePolicyManager dpm = mContext.getSystemService(DevicePolicyManager.class);
+        if (dpm.isAdminActive(ComponentName.unflattenFromString(DEVICE_ADMIN_COMPONENT))) {
+            final Bundle extras = createSingleKeyBundle(EXTRA_PACKAGE_NAME, TEST_APP_PACKAGE_NAME);
+            requestDpmAction(ACTION_UNBLOCK_UNINSTALL, extras, mHandler);
+            removeDeviceAdmin();
+        }
+        setDistractionFlagsAndAssertResult(ALL_TEST_PACKAGES, PackageManager.RESTRICTION_NONE,
+                ArrayUtils.emptyArray(String.class));
+    }
+}
diff --git a/tests/suspendapps/tests/src/android/suspendapps/cts/DualSuspendTests.java b/tests/suspendapps/tests/src/android/suspendapps/cts/DualSuspendTests.java
new file mode 100644
index 0000000..92f1927
--- /dev/null
+++ b/tests/suspendapps/tests/src/android/suspendapps/cts/DualSuspendTests.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.suspendapps.cts;
+
+import static android.suspendapps.cts.Constants.TEST_APP_PACKAGE_NAME;
+import static android.suspendapps.cts.SuspendTestUtils.addAndAssertProfileOwner;
+import static android.suspendapps.cts.SuspendTestUtils.createSingleKeyBundle;
+import static android.suspendapps.cts.SuspendTestUtils.removeDeviceAdmin;
+import static android.suspendapps.cts.SuspendTestUtils.requestDpmAction;
+import static com.android.suspendapps.suspendtestapp.SuspendTestReceiver.ACTION_REPORT_MY_PACKAGE_SUSPENDED;
+import static com.android.suspendapps.suspendtestapp.SuspendTestReceiver.ACTION_REPORT_MY_PACKAGE_UNSUSPENDED;
+import static com.android.suspendapps.testdeviceadmin.TestCommsReceiver.ACTION_SUSPEND;
+import static com.android.suspendapps.testdeviceadmin.TestCommsReceiver.ACTION_UNSUSPEND;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeTrue;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.os.Handler;
+import android.os.Looper;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.LargeTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.compatibility.common.util.FeatureUtil;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@LargeTest
+public class DualSuspendTests {
+    private Context mContext;
+    private Handler mReceiverHandler;
+    private AppCommunicationReceiver mAppCommsReceiver;
+
+    @Before
+    public void setUp() {
+        mContext = InstrumentationRegistry.getTargetContext();
+        mReceiverHandler = new Handler(Looper.getMainLooper());
+        mAppCommsReceiver = new AppCommunicationReceiver(mContext);
+        assumeTrue("Skipping test that requires device admin",
+                FeatureUtil.hasSystemFeature(PackageManager.FEATURE_DEVICE_ADMIN));
+        addAndAssertProfileOwner();
+    }
+
+    private boolean setSuspendViaDPM(boolean suspend) throws Exception {
+        return requestDpmAction(suspend ? ACTION_SUSPEND : ACTION_UNSUSPEND,
+                createSingleKeyBundle(Intent.EXTRA_PACKAGE_NAME, TEST_APP_PACKAGE_NAME),
+                mReceiverHandler);
+    }
+
+    @Test
+    public void testMyPackageSuspended() throws Exception {
+        mAppCommsReceiver.register(mReceiverHandler, ACTION_REPORT_MY_PACKAGE_SUSPENDED);
+        SuspendTestUtils.suspend(null, null, null);
+        Intent receivedIntent = mAppCommsReceiver.pollForIntent(5);
+        assertNotNull("Did not receive intent from app for first suspend", receivedIntent);
+        assertEquals(ACTION_REPORT_MY_PACKAGE_SUSPENDED, receivedIntent.getAction());
+        assertTrue("Suspend via dpm failed", setSuspendViaDPM(true));
+        receivedIntent = mAppCommsReceiver.pollForIntent(5);
+        assertNotNull("Did not receive intent from app for second suspend", receivedIntent);
+        assertEquals(ACTION_REPORT_MY_PACKAGE_SUSPENDED, receivedIntent.getAction());
+    }
+
+    @Test
+    public void testMyPackageUnsuspended() throws Exception {
+        mAppCommsReceiver.register(mReceiverHandler, ACTION_REPORT_MY_PACKAGE_UNSUSPENDED);
+        SuspendTestUtils.suspend(null, null, null);
+        assertTrue("Suspend via dpm failed", setSuspendViaDPM(true));
+        mAppCommsReceiver.drainPendingBroadcasts();
+        SuspendTestUtils.unsuspendAll();
+        Intent receivedIntent = mAppCommsReceiver.pollForIntent(5);
+        if (receivedIntent != null) {
+            fail("Unexpected intent " + receivedIntent.getAction() + " received");
+        }
+        assertTrue("Unsuspend via dpm failed", setSuspendViaDPM(false));
+        receivedIntent = mAppCommsReceiver.pollForIntent(5);
+        assertNotNull("Did not receive intent after second unsuspend", receivedIntent);
+        assertEquals(ACTION_REPORT_MY_PACKAGE_UNSUSPENDED, receivedIntent.getAction());
+    }
+
+    @Test
+    public void testIsPackageSuspended() throws Exception {
+        final PackageManager pm = mContext.getPackageManager();
+        assertFalse(pm.isPackageSuspended(TEST_APP_PACKAGE_NAME));
+        SuspendTestUtils.suspend(null, null, null);
+        assertTrue("Suspend via dpm failed", setSuspendViaDPM(true));
+        assertTrue("Package should be suspended by both",
+                pm.isPackageSuspended(TEST_APP_PACKAGE_NAME));
+        SuspendTestUtils.unsuspendAll();
+        assertTrue("Package should be suspended by dpm",
+                pm.isPackageSuspended(TEST_APP_PACKAGE_NAME));
+        SuspendTestUtils.suspend(null, null, null);
+        assertTrue("Unsuspend via dpm failed", setSuspendViaDPM(false));
+        assertTrue("Package should be suspended by shell",
+                pm.isPackageSuspended(TEST_APP_PACKAGE_NAME));
+        SuspendTestUtils.unsuspendAll();
+        assertFalse("Package should be suspended by neither",
+                pm.isPackageSuspended(TEST_APP_PACKAGE_NAME));
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        mAppCommsReceiver.unregister();
+        SuspendTestUtils.unsuspendAll();
+        setSuspendViaDPM(false);
+        removeDeviceAdmin();
+    }
+}
diff --git a/tests/suspendapps/tests/src/android/suspendapps/cts/SuspendPackagesTest.java b/tests/suspendapps/tests/src/android/suspendapps/cts/SuspendPackagesTest.java
new file mode 100644
index 0000000..21024f9
--- /dev/null
+++ b/tests/suspendapps/tests/src/android/suspendapps/cts/SuspendPackagesTest.java
@@ -0,0 +1,652 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.suspendapps.cts;
+
+import static android.app.AppOpsManager.MODE_ALLOWED;
+import static android.content.Intent.EXTRA_PACKAGE_NAME;
+import static android.content.pm.SuspendDialogInfo.BUTTON_ACTION_UNSUSPEND;
+import static android.os.UserManager.DISALLOW_APPS_CONTROL;
+import static android.os.UserManager.DISALLOW_UNINSTALL_APPS;
+import static android.suspendapps.cts.Constants.ACTION_REPORT_MORE_DETAILS_ACTIVITY_STARTED;
+import static android.suspendapps.cts.Constants.ACTION_REPORT_PACKAGE_UNSUSPENDED_MANUALLY;
+import static android.suspendapps.cts.Constants.ALL_TEST_PACKAGES;
+import static android.suspendapps.cts.Constants.DEVICE_ADMIN_COMPONENT;
+import static android.suspendapps.cts.Constants.DEVICE_ADMIN_PACKAGE;
+import static android.suspendapps.cts.Constants.EXTRA_RECEIVED_PACKAGE_NAME;
+import static android.suspendapps.cts.Constants.TEST_APP_PACKAGE_NAME;
+import static android.suspendapps.cts.Constants.TEST_PACKAGE_ARRAY;
+import static android.suspendapps.cts.SuspendTestUtils.addAndAssertProfileOwner;
+import static android.suspendapps.cts.SuspendTestUtils.createSingleKeyBundle;
+import static android.suspendapps.cts.SuspendTestUtils.removeDeviceAdmin;
+import static android.suspendapps.cts.SuspendTestUtils.requestDpmAction;
+
+import static com.android.suspendapps.suspendtestapp.SuspendTestActivity.ACTION_REPORT_TEST_ACTIVITY_STARTED;
+import static com.android.suspendapps.suspendtestapp.SuspendTestActivity.ACTION_REPORT_TEST_ACTIVITY_STOPPED;
+import static com.android.suspendapps.suspendtestapp.SuspendTestReceiver.ACTION_REPORT_MY_PACKAGE_SUSPENDED;
+import static com.android.suspendapps.suspendtestapp.SuspendTestReceiver.ACTION_REPORT_MY_PACKAGE_UNSUSPENDED;
+import static com.android.suspendapps.testdeviceadmin.TestCommsReceiver.ACTION_ADD_USER_RESTRICTION;
+import static com.android.suspendapps.testdeviceadmin.TestCommsReceiver.ACTION_BLOCK_UNINSTALL;
+import static com.android.suspendapps.testdeviceadmin.TestCommsReceiver.ACTION_SUSPEND;
+import static com.android.suspendapps.testdeviceadmin.TestCommsReceiver.ACTION_UNBLOCK_UNINSTALL;
+import static com.android.suspendapps.testdeviceadmin.TestCommsReceiver.ACTION_UNSUSPEND;
+import static com.android.suspendapps.testdeviceadmin.TestCommsReceiver.EXTRA_USER_RESTRICTION;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeTrue;
+
+import android.Manifest;
+import android.app.AppOpsManager;
+import android.app.admin.DevicePolicyManager;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.LauncherApps;
+import android.content.pm.PackageManager;
+import android.content.pm.SuspendDialogInfo;
+import android.os.BaseBundle;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.PersistableBundle;
+import android.os.UserHandle;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject2;
+import android.support.test.uiautomator.Until;
+import android.suspendapps.cts.R;
+import android.util.ArrayMap;
+
+import androidx.annotation.Nullable;
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.LargeTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.compatibility.common.util.FeatureUtil;
+import com.android.compatibility.common.util.SystemUtil;
+import com.android.suspendapps.suspendtestapp.SuspendTestActivity;
+import com.android.suspendapps.suspendtestapp.SuspendTestReceiver;
+
+import libcore.util.EmptyArray;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Arrays;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.regex.Pattern;
+
+@RunWith(AndroidJUnit4.class)
+@LargeTest
+public class SuspendPackagesTest {
+    private static final String TEST_APP_LABEL = "Suspend Test App";
+
+    private Context mContext;
+    private PackageManager mPackageManager;
+    private AppOpsManager mAppOpsManager;
+    private Handler mReceiverHandler;
+    private AppCommunicationReceiver mAppCommsReceiver;
+    private UiDevice mUiDevice;
+
+    /** Do not use with {@link #mAppCommsReceiver} in the same test as both use the same handler. */
+    private Bundle requestAppAction(String action) throws InterruptedException {
+        final AtomicReference<Bundle> result = new AtomicReference<>();
+        final CountDownLatch receiverLatch = new CountDownLatch(1);
+        final ComponentName testReceiverComponent = new ComponentName(TEST_APP_PACKAGE_NAME,
+                SuspendTestReceiver.class.getCanonicalName());
+        final Intent broadcastIntent = new Intent(action)
+                .setComponent(testReceiverComponent)
+                .setFlags(Intent.FLAG_RECEIVER_FOREGROUND);
+        mContext.sendOrderedBroadcast(broadcastIntent, null, new BroadcastReceiver() {
+            @Override
+            public void onReceive(Context context, Intent intent) {
+                result.set(getResultExtras(true));
+                receiverLatch.countDown();
+            }
+        }, mReceiverHandler, 0, null, null);
+
+        assertTrue("Test receiver timed out ", receiverLatch.await(5, TimeUnit.SECONDS));
+        return result.get();
+    }
+
+    private PersistableBundle getExtras(String keyPrefix, long lval, String sval, double dval) {
+        final PersistableBundle extras = new PersistableBundle(3);
+        extras.putLong(keyPrefix + ".LONG_VALUE", lval);
+        extras.putDouble(keyPrefix + ".DOUBLE_VALUE", dval);
+        extras.putString(keyPrefix + ".STRING_VALUE", sval);
+        return extras;
+    }
+
+    private void startTestAppActivity(@Nullable Bundle extras) {
+        final Intent testActivity = new Intent()
+                .setComponent(new ComponentName(TEST_APP_PACKAGE_NAME,
+                        SuspendTestActivity.class.getCanonicalName()))
+                .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        if (extras != null) {
+            testActivity.putExtras(extras);
+        }
+        mContext.startActivity(testActivity);
+    }
+
+    private static boolean areSameExtras(BaseBundle expected, BaseBundle received) {
+        if (expected == null || received == null) {
+            return expected == received;
+        }
+        final Set<String> keys = expected.keySet();
+        if (keys.size() != received.keySet().size()) {
+            return false;
+        }
+        for (String key : keys) {
+            if (!Objects.equals(expected.get(key), received.get(key))) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    private static void assertSameExtras(String message, BaseBundle expected, BaseBundle received) {
+        if (!areSameExtras(expected, received)) {
+            fail(message + ": [expected: " + expected + "; received: " + received + "]");
+        }
+    }
+
+    private void addAndAssertDeviceOwner() {
+        SystemUtil.runShellCommand("dpm set-device-owner --user cur " + DEVICE_ADMIN_COMPONENT,
+                output -> output.startsWith("Success"));
+    }
+
+    private void addAndAssertDeviceAdmin() {
+        SystemUtil.runShellCommand("dpm set-active-admin --user cur " + DEVICE_ADMIN_COMPONENT,
+                output -> output.startsWith("Success"));
+    }
+
+    @Before
+    public void setUp() {
+        mContext = InstrumentationRegistry.getTargetContext();
+        mPackageManager = mContext.getPackageManager();
+        mAppOpsManager = mContext.getSystemService(AppOpsManager.class);
+        mReceiverHandler = new Handler(Looper.getMainLooper());
+        mUiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
+        mAppCommsReceiver = new AppCommunicationReceiver(mContext);
+    }
+
+    @Test
+    public void testIsPackageSuspended() throws Exception {
+        SuspendTestUtils.suspend(null, null, null);
+        assertTrue("isPackageSuspended is false",
+                mPackageManager.isPackageSuspended(TEST_APP_PACKAGE_NAME));
+    }
+
+    @Test
+    public void testSuspendedStateFromApp() throws Exception {
+        Bundle resultFromApp = requestAppAction(SuspendTestReceiver.ACTION_GET_SUSPENDED_STATE);
+        assertFalse(resultFromApp.getBoolean(SuspendTestReceiver.EXTRA_SUSPENDED, true));
+        assertNull(resultFromApp.getBundle(SuspendTestReceiver.EXTRA_SUSPENDED_APP_EXTRAS));
+
+        final PersistableBundle appExtras = getExtras("testSuspendedStateFromApp", 20, "20", 0.2);
+        SuspendTestUtils.suspend(appExtras, null, null);
+
+        resultFromApp = requestAppAction(SuspendTestReceiver.ACTION_GET_SUSPENDED_STATE);
+        assertTrue("resultFromApp:suspended is false",
+                resultFromApp.getBoolean(SuspendTestReceiver.EXTRA_SUSPENDED));
+        final Bundle receivedAppExtras =
+                resultFromApp.getBundle(SuspendTestReceiver.EXTRA_SUSPENDED_APP_EXTRAS);
+        assertSameExtras("Received app extras different to the ones supplied",
+                appExtras, receivedAppExtras);
+    }
+
+    @Test
+    public void testMyPackageSuspendedUnsuspended() throws Exception {
+        mAppCommsReceiver.register(mReceiverHandler, ACTION_REPORT_MY_PACKAGE_SUSPENDED,
+                ACTION_REPORT_MY_PACKAGE_UNSUSPENDED);
+        mAppCommsReceiver.drainPendingBroadcasts();
+        final PersistableBundle appExtras = getExtras("testMyPackageSuspendBroadcasts", 1, "1", .1);
+        SuspendTestUtils.suspend(appExtras, null, null);
+        Intent intentFromApp = mAppCommsReceiver.pollForIntent(5);
+        assertNotNull("Did not receive any broadcast from test app", intentFromApp);
+        assertEquals("MY_PACKAGE_SUSPENDED delivery not reported",
+                ACTION_REPORT_MY_PACKAGE_SUSPENDED, intentFromApp.getAction());
+        assertSameExtras("Received app extras different to the ones supplied", appExtras,
+                intentFromApp.getBundleExtra(SuspendTestReceiver.EXTRA_SUSPENDED_APP_EXTRAS));
+        SuspendTestUtils.unsuspendAll();
+        intentFromApp = mAppCommsReceiver.pollForIntent(5);
+        assertNotNull("Did not receive any broadcast from test app", intentFromApp);
+        assertEquals("MY_PACKAGE_UNSUSPENDED delivery not reported",
+                ACTION_REPORT_MY_PACKAGE_UNSUSPENDED, intentFromApp.getAction());
+    }
+
+    @Test
+    public void testUpdatingAppExtras() throws Exception {
+        mAppCommsReceiver.register(mReceiverHandler, ACTION_REPORT_MY_PACKAGE_SUSPENDED);
+        final PersistableBundle extras1 = getExtras("testMyPackageSuspendedOnChangingExtras", 1,
+                "1", 0.1);
+        SuspendTestUtils.suspend(extras1, null, null);
+        Intent intentFromApp = mAppCommsReceiver.pollForIntent(5);
+        assertNotNull("Did not receive any broadcast from test app", intentFromApp);
+        assertEquals("MY_PACKAGE_SUSPENDED delivery not reported",
+                ACTION_REPORT_MY_PACKAGE_SUSPENDED, intentFromApp.getAction());
+        assertSameExtras("Received app extras different to the ones supplied", extras1,
+                intentFromApp.getBundleExtra(SuspendTestReceiver.EXTRA_SUSPENDED_APP_EXTRAS));
+        final PersistableBundle extras2 = getExtras("testMyPackageSuspendedOnChangingExtras", 2,
+                "2", 0.2);
+        SuspendTestUtils.suspend(extras2, null, null);
+        intentFromApp = mAppCommsReceiver.pollForIntent(5);
+        assertNotNull("Did not receive any broadcast from test app", intentFromApp);
+        assertEquals("MY_PACKAGE_SUSPENDED delivery not reported",
+                ACTION_REPORT_MY_PACKAGE_SUSPENDED, intentFromApp.getAction());
+        assertSameExtras("Received app extras different to the updated extras", extras2,
+                intentFromApp.getBundleExtra(SuspendTestReceiver.EXTRA_SUSPENDED_APP_EXTRAS));
+    }
+
+    @Test
+    public void testCannotSuspendSelf() throws Exception {
+        final String[] self = new String[]{mContext.getPackageName()};
+        SuspendTestUtils.suspendAndAssertResult(self, null, null, null, self);
+    }
+
+    @Test
+    public void testActivityStoppedOnSuspend() throws Exception {
+        mAppCommsReceiver.register(mReceiverHandler, ACTION_REPORT_TEST_ACTIVITY_STARTED,
+                ACTION_REPORT_TEST_ACTIVITY_STOPPED);
+        startTestAppActivity(null);
+        Intent intentFromApp = mAppCommsReceiver.pollForIntent(5);
+        assertNotNull("Did not receive any broadcast from test app", intentFromApp);
+        assertEquals("Test activity start not reported",
+                ACTION_REPORT_TEST_ACTIVITY_STARTED, intentFromApp.getAction());
+        SuspendTestUtils.suspend(null, null, null);
+        intentFromApp = mAppCommsReceiver.pollForIntent(5);
+        assertNotNull("Did not receive any broadcast from test app", intentFromApp);
+        assertEquals("Test activity stop not reported on suspending the test app",
+                ACTION_REPORT_TEST_ACTIVITY_STOPPED, intentFromApp.getAction());
+    }
+
+    @Test
+    public void testCanSuspendWhenProfileOwner() throws Exception {
+        assumeTrue(FeatureUtil.hasSystemFeature(PackageManager.FEATURE_DEVICE_ADMIN));
+        addAndAssertProfileOwner();
+        SuspendTestUtils.suspend(null, null, null);
+    }
+
+    @Test
+    public void testCanSuspendWhenDeviceOwner() throws Exception {
+        assumeTrue(FeatureUtil.hasSystemFeature(PackageManager.FEATURE_DEVICE_ADMIN));
+        addAndAssertDeviceOwner();
+        SuspendTestUtils.suspend(null, null, null);
+    }
+
+    private int getCurrentUserId() {
+        final String result = SystemUtil.runShellCommand("am get-current-user");
+        return Integer.parseInt(result.trim());
+    }
+
+    @Test
+    public void testLauncherCallback() throws Exception {
+        final PersistableBundle suppliedExtras = getExtras("testLauncherCallback", 2, "2", 0.2);
+        final int currentUserId = getCurrentUserId();
+        final AtomicReference<String> callBackErrors = new AtomicReference<>("");
+        final CountDownLatch oldCallbackLatch = new CountDownLatch(1);
+        final CountDownLatch newCallbackLatch = new CountDownLatch(1);
+        LauncherApps.Callback testCallback = new StubbedCallback() {
+            @Override
+            public void onPackagesSuspended(String[] packageNames, UserHandle user) {
+                oldCallbackLatch.countDown();
+            }
+
+            @Override
+            public void onPackagesSuspended(String[] packageNames, UserHandle user,
+                    Bundle launcherExtras) {
+                final StringBuilder errorString = new StringBuilder();
+                if (!Arrays.equals(packageNames, TEST_PACKAGE_ARRAY)) {
+                    errorString.append("Received unexpected packageNames in onPackagesSuspended: ");
+                    errorString.append(Arrays.toString(packageNames));
+                    errorString.append(". ");
+                }
+                if (user.getIdentifier() != currentUserId) {
+                    errorString.append("Received wrong user " + user.getIdentifier()
+                            + ", current user: " + currentUserId + ". ");
+                }
+                if (!areSameExtras(launcherExtras, suppliedExtras)) {
+                    errorString.append("Unexpected launcherExtras, supplied: " + suppliedExtras
+                            + ", received: " + launcherExtras + ". ");
+                }
+                callBackErrors.set(errorString.toString());
+                newCallbackLatch.countDown();
+            }
+        };
+        final LauncherApps launcherApps = mContext.getSystemService(LauncherApps.class);
+        launcherApps.registerCallback(testCallback, mReceiverHandler);
+        SuspendTestUtils.suspend(null, suppliedExtras, null);
+        assertFalse("Old callback invoked", oldCallbackLatch.await(2, TimeUnit.SECONDS));
+        assertTrue("New callback not invoked", newCallbackLatch.await(2, TimeUnit.SECONDS));
+        final String errors = callBackErrors.get();
+        assertTrue("Callbacks did not complete as expected: " + errors, errors.isEmpty());
+        launcherApps.unregisterCallback(testCallback);
+    }
+
+    private void turnScreenOn() throws Exception {
+        if (!mUiDevice.isScreenOn()) {
+            mUiDevice.wakeUp();
+        }
+        SystemUtil.runShellCommandForNoOutput("wm dismiss-keyguard");
+    }
+
+    @Test
+    public void testInterceptorActivity_moreDetails() throws Exception {
+        turnScreenOn();
+        mAppCommsReceiver.register(mReceiverHandler, ACTION_REPORT_MORE_DETAILS_ACTIVITY_STARTED,
+                ACTION_REPORT_TEST_ACTIVITY_STARTED);
+        final SuspendDialogInfo dialogInfo = new SuspendDialogInfo.Builder()
+                .setIcon(R.drawable.ic_settings)
+                .setTitle(R.string.dialog_title)
+                .setMessage(R.string.dialog_message)
+                .setNeutralButtonText(R.string.more_details_button_text)
+                .build();
+        SuspendTestUtils.suspend(null, null, dialogInfo);
+        startTestAppActivity(null);
+        // Test activity should not start
+        assertNull("No broadcast was expected from app", mAppCommsReceiver.pollForIntent(2));
+
+        // The dialog should have correct specifications
+        final String expectedTitle = mContext.getResources().getString(R.string.dialog_title);
+        final String expectedMessage = mContext.getResources().getString(R.string.dialog_message,
+                TEST_APP_LABEL);
+        final String expectedButtonText = mContext.getResources().getString(
+                R.string.more_details_button_text);
+
+        assertNotNull("Given dialog title: " + expectedTitle + " not shown",
+                mUiDevice.wait(Until.findObject(By.text(expectedTitle)), 5000));
+        assertNotNull("Given dialog message: " + expectedMessage + " not shown",
+                mUiDevice.findObject(By.text(expectedMessage)));
+        // Sometimes, button texts can have styles that override case (e.g. b/134033532)
+        final Pattern buttonTextIgnoreCase = Pattern.compile(Pattern.quote(expectedButtonText),
+                Pattern.CASE_INSENSITIVE);
+        final UiObject2 moreDetailsButton = mUiDevice.findObject(
+                By.clickable(true).text(buttonTextIgnoreCase));
+        assertNotNull(expectedButtonText + " button not shown", moreDetailsButton);
+
+        // Tapping on the neutral button should start the correct intent.
+        moreDetailsButton.click();
+        final Intent intentFromApp = mAppCommsReceiver.pollForIntent(5);
+        assertNotNull("Did not receive any broadcast from test app", intentFromApp);
+        assertEquals("More details activity start not reported",
+                ACTION_REPORT_MORE_DETAILS_ACTIVITY_STARTED, intentFromApp.getAction());
+        final String receivedPackageName = intentFromApp.getStringExtra(
+                EXTRA_RECEIVED_PACKAGE_NAME);
+        assertEquals("Wrong package name received by \'More Details\' activity",
+                TEST_APP_PACKAGE_NAME, receivedPackageName);
+    }
+
+    @Test
+    public void testInterceptorActivity_unsuspend() throws Exception {
+        turnScreenOn();
+        mAppCommsReceiver.register(mReceiverHandler, ACTION_REPORT_PACKAGE_UNSUSPENDED_MANUALLY,
+                ACTION_REPORT_TEST_ACTIVITY_STARTED);
+        final SuspendDialogInfo dialogInfo = new SuspendDialogInfo.Builder()
+                .setIcon(R.drawable.ic_settings)
+                .setTitle(R.string.dialog_title)
+                .setMessage(R.string.dialog_message)
+                .setNeutralButtonText(R.string.unsuspend_button_text)
+                .setNeutralButtonAction(BUTTON_ACTION_UNSUSPEND)
+                .build();
+        SuspendTestUtils.suspend(null, null, dialogInfo);
+        final Bundle extrasForStart = new Bundle(getExtras("unsuspend", 90, "sval", 0.9));
+        startTestAppActivity(extrasForStart);
+        // Test activity should not start. Not yet.
+        assertNull("No broadcast was expected from app", mAppCommsReceiver.pollForIntent(2));
+
+        final String expectedTitle = mContext.getResources().getString(R.string.dialog_title);
+        final String expectedButtonText = mContext.getResources().getString(
+                R.string.unsuspend_button_text);
+
+        assertNotNull("Given dialog title: " + expectedTitle + " not shown",
+                mUiDevice.wait(Until.findObject(By.text(expectedTitle)), 5000));
+
+        // Sometimes, button texts can have styles that override case (e.g. b/134033532)
+        final Pattern buttonTextIgnoreCase = Pattern.compile(Pattern.quote(expectedButtonText),
+                Pattern.CASE_INSENSITIVE);
+        final UiObject2 unsuspendButton = mUiDevice.findObject(
+                By.clickable(true).text(buttonTextIgnoreCase));
+        assertNotNull(expectedButtonText + " button not shown", unsuspendButton);
+
+        // Tapping on the neutral button should:
+        // 1. Unsuspend the test app
+        // 2. Launch the previously intercepted intent
+        // 3. Tell the suspending package that the test app was unsuspended
+        unsuspendButton.click();
+
+        final ArrayMap<String, Bundle> receivedActionExtras = new ArrayMap<>(2);
+        Intent intentFromApp = mAppCommsReceiver.pollForIntent(5);
+        assertNotNull("Did not receive any broadcast from test app", intentFromApp);
+        receivedActionExtras.put(intentFromApp.getAction(), intentFromApp.getExtras());
+
+        intentFromApp = mAppCommsReceiver.pollForIntent(5);
+        assertNotNull("Expecting 2 broadcasts, received only 1", intentFromApp);
+        receivedActionExtras.put(intentFromApp.getAction(), intentFromApp.getExtras());
+
+        assertTrue("Test activity start not reported",
+                receivedActionExtras.containsKey(ACTION_REPORT_TEST_ACTIVITY_STARTED));
+        assertSameExtras("Did not receive extras supplied to startActivity on unsuspend",
+                extrasForStart, receivedActionExtras.get(ACTION_REPORT_TEST_ACTIVITY_STARTED));
+
+        assertTrue("PACKAGE_UNSUSPENDED_MANUALLY not reported",
+                receivedActionExtras.containsKey(ACTION_REPORT_PACKAGE_UNSUSPENDED_MANUALLY));
+        final Bundle extras = receivedActionExtras.get(ACTION_REPORT_PACKAGE_UNSUSPENDED_MANUALLY);
+        assertNotNull("No extras with ACTION_REPORT_PACKAGE_UNSUSPENDED_MANUALLY", extras);
+        assertEquals("Did not receive unsuspended package name", TEST_APP_PACKAGE_NAME,
+                extras.getString(EXTRA_RECEIVED_PACKAGE_NAME));
+
+        assertFalse("Test package still suspended",
+                mPackageManager.isPackageSuspended(TEST_APP_PACKAGE_NAME));
+
+        intentFromApp = mAppCommsReceiver.pollForIntent(5);
+        assertNull("Expecting only 2 broadcasts, received one more ", intentFromApp);
+    }
+
+    @Test
+    public void testTestAppsSuspendable() throws Exception {
+        final String[] unsuspendablePkgs = SystemUtil.callWithShellPermissionIdentity(() ->
+                mPackageManager.getUnsuspendablePackages(ALL_TEST_PACKAGES));
+        assertTrue("Normal test apps unsuspendable: " + Arrays.toString(unsuspendablePkgs),
+                unsuspendablePkgs.length == 0);
+    }
+
+    @Test
+    public void testDeviceAdminUnsuspendable() throws Exception {
+        assumeTrue(FeatureUtil.hasSystemFeature(PackageManager.FEATURE_DEVICE_ADMIN));
+        addAndAssertDeviceAdmin();
+        final String[] unsuspendablePkgs = SystemUtil.callWithShellPermissionIdentity(() ->
+                mPackageManager.getUnsuspendablePackages(new String[]{DEVICE_ADMIN_PACKAGE}));
+        assertTrue("Device admin suspendable", (unsuspendablePkgs.length == 1)
+                && DEVICE_ADMIN_PACKAGE.equals(unsuspendablePkgs[0]));
+    }
+
+    private void assertOpDisallowed(int op) throws Exception {
+        final int testUid = mPackageManager.getPackageUid(TEST_APP_PACKAGE_NAME, 0);
+        final int mode = SystemUtil.callWithShellPermissionIdentity(
+                () -> mAppOpsManager.checkOpNoThrow(op, testUid, TEST_APP_PACKAGE_NAME));
+        assertNotEquals("Op " + AppOpsManager.opToName(op) + " allowed while package is suspended",
+                MODE_ALLOWED, mode);
+    }
+
+    @Test
+    public void testOpRecordAudioOnSuspend() throws Exception {
+        final int recordAudioOp = AppOpsManager.permissionToOpCode(
+                Manifest.permission.RECORD_AUDIO);
+        SuspendTestUtils.suspend(null, null, null);
+        assertOpDisallowed(recordAudioOp);
+    }
+
+    @Test
+    public void testOpCameraOnSuspend() throws Exception {
+        final int cameraOp = AppOpsManager.permissionToOpCode(Manifest.permission.CAMERA);
+        SuspendTestUtils.suspend(null, null, null);
+        assertOpDisallowed(cameraOp);
+    }
+
+    @Test
+    public void testCannotSuspendWhenUninstallBlocked() throws Exception {
+        assumeTrue(FeatureUtil.hasSystemFeature(PackageManager.FEATURE_DEVICE_ADMIN));
+        addAndAssertProfileOwner();
+        final Bundle extras = createSingleKeyBundle(EXTRA_PACKAGE_NAME, TEST_APP_PACKAGE_NAME);
+        assertTrue("Block uninstall request failed", requestDpmAction(
+                ACTION_BLOCK_UNINSTALL, extras, mReceiverHandler));
+        SuspendTestUtils.suspendAndAssertResult(ALL_TEST_PACKAGES, null, null, null,
+                TEST_PACKAGE_ARRAY);
+    }
+
+    private void assertCannotSuspendUnderUserRestriction(String userRestriction) throws Exception {
+        addAndAssertProfileOwner();
+        final Bundle extras = createSingleKeyBundle(EXTRA_USER_RESTRICTION, userRestriction);
+        assertTrue("Request to add restriction" + userRestriction + " failed",
+                requestDpmAction(ACTION_ADD_USER_RESTRICTION, extras, mReceiverHandler));
+        SuspendTestUtils.suspendAndAssertResult(ALL_TEST_PACKAGES, null, null, null,
+                ALL_TEST_PACKAGES);
+    }
+
+    @Test
+    public void testCannotSuspendUnderDisallowAppsControl() throws Exception {
+        assumeTrue(FeatureUtil.hasSystemFeature(PackageManager.FEATURE_DEVICE_ADMIN));
+        assertCannotSuspendUnderUserRestriction(DISALLOW_APPS_CONTROL);
+    }
+
+    @Test
+    public void testCannotSuspendUnderDisallowUninstallApps() throws Exception {
+        assumeTrue(FeatureUtil.hasSystemFeature(PackageManager.FEATURE_DEVICE_ADMIN));
+        assertCannotSuspendUnderUserRestriction(DISALLOW_UNINSTALL_APPS);
+    }
+
+    private void assertDpmCanSuspendUnderUserRestriction(String userRestriction) throws Exception {
+        addAndAssertProfileOwner();
+        mAppCommsReceiver.register(mReceiverHandler, ACTION_REPORT_MY_PACKAGE_SUSPENDED);
+        mAppCommsReceiver.drainPendingBroadcasts();
+        Bundle extra = createSingleKeyBundle(EXTRA_USER_RESTRICTION, userRestriction);
+        assertTrue("Request to add restriction" + userRestriction + " failed",
+                requestDpmAction(ACTION_ADD_USER_RESTRICTION, extra, mReceiverHandler));
+        extra = createSingleKeyBundle(Intent.EXTRA_PACKAGE_NAME, TEST_APP_PACKAGE_NAME);
+        assertTrue("Request to suspend via dpm failed",
+                requestDpmAction(ACTION_SUSPEND, extra, mReceiverHandler));
+        final Intent intentFromApp = mAppCommsReceiver.pollForIntent(5);
+        assertNotNull("No intent received from app", intentFromApp);
+        assertEquals(ACTION_REPORT_MY_PACKAGE_SUSPENDED, intentFromApp.getAction());
+    }
+
+    @Test
+    public void testDpmCanSuspendUnderDisallowAppsControl() throws Exception {
+        assumeTrue(FeatureUtil.hasSystemFeature(PackageManager.FEATURE_DEVICE_ADMIN));
+        assertDpmCanSuspendUnderUserRestriction(DISALLOW_APPS_CONTROL);
+    }
+
+    @Test
+    public void testDpmCanSuspendUnderDisallowUninstallApps() throws Exception {
+        assumeTrue(FeatureUtil.hasSystemFeature(PackageManager.FEATURE_DEVICE_ADMIN));
+        assertDpmCanSuspendUnderUserRestriction(DISALLOW_UNINSTALL_APPS);
+    }
+
+    private void assertUnsuspendedOnUserRestriction(String userRestriction) throws Exception {
+        assertTrue("Test app not suspended before setting user restriction",
+                mPackageManager.isPackageSuspended(TEST_APP_PACKAGE_NAME));
+        mAppCommsReceiver.register(mReceiverHandler, ACTION_REPORT_MY_PACKAGE_UNSUSPENDED);
+        mAppCommsReceiver.drainPendingBroadcasts();
+        addAndAssertProfileOwner();
+        final Bundle extras = createSingleKeyBundle(EXTRA_USER_RESTRICTION, userRestriction);
+        assertTrue("Request to add restriction " + userRestriction + " failed",
+                requestDpmAction(ACTION_ADD_USER_RESTRICTION, extras, mReceiverHandler));
+        final Intent intentFromApp = mAppCommsReceiver.pollForIntent(5);
+        assertNotNull("No intent received from app", intentFromApp);
+        assertEquals(ACTION_REPORT_MY_PACKAGE_UNSUSPENDED, intentFromApp.getAction());
+    }
+
+    @Test
+    public void testUnsuspendedOnDisallowUninstallApps() throws Exception {
+        assumeTrue(FeatureUtil.hasSystemFeature(PackageManager.FEATURE_DEVICE_ADMIN));
+        SuspendTestUtils.suspend(null, null, null);
+        assertUnsuspendedOnUserRestriction(DISALLOW_UNINSTALL_APPS);
+    }
+
+    @Test
+    public void testUnsuspendedOnDisallowAppsControl() throws Exception {
+        assumeTrue(FeatureUtil.hasSystemFeature(PackageManager.FEATURE_DEVICE_ADMIN));
+        SuspendTestUtils.suspend(null, null, null);
+        assertUnsuspendedOnUserRestriction(DISALLOW_APPS_CONTROL);
+    }
+
+    @Test
+    public void testUnsuspendedOnUninstallBlocked() throws Exception {
+        assumeTrue(FeatureUtil.hasSystemFeature(PackageManager.FEATURE_DEVICE_ADMIN));
+        SuspendTestUtils.suspendAndAssertResult(ALL_TEST_PACKAGES, null, null, null,
+                EmptyArray.STRING);
+        mAppCommsReceiver.register(mReceiverHandler, ACTION_REPORT_MY_PACKAGE_UNSUSPENDED);
+        mAppCommsReceiver.drainPendingBroadcasts();
+        addAndAssertProfileOwner();
+        final Bundle extras = createSingleKeyBundle(EXTRA_PACKAGE_NAME, TEST_APP_PACKAGE_NAME);
+        assertTrue("Block uninstall request failed", requestDpmAction(
+                ACTION_BLOCK_UNINSTALL, extras, mReceiverHandler));
+        final Intent intentFromApp = mAppCommsReceiver.pollForIntent(5);
+        assertNotNull("No intent received from app", intentFromApp);
+        assertEquals(ACTION_REPORT_MY_PACKAGE_UNSUSPENDED, intentFromApp.getAction());
+        assertEquals(TEST_APP_PACKAGE_NAME, intentFromApp.getStringExtra(EXTRA_PACKAGE_NAME));
+        assertNull("Unexpected 2nd broadcast", mAppCommsReceiver.pollForIntent(5));
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        final DevicePolicyManager dpm = mContext.getSystemService(DevicePolicyManager.class);
+        if (dpm.isAdminActive(ComponentName.unflattenFromString(DEVICE_ADMIN_COMPONENT))) {
+            final Bundle extras = createSingleKeyBundle(EXTRA_PACKAGE_NAME, TEST_APP_PACKAGE_NAME);
+            requestDpmAction(ACTION_UNBLOCK_UNINSTALL, extras, mReceiverHandler);
+            requestDpmAction(ACTION_UNSUSPEND, extras, mReceiverHandler);
+            removeDeviceAdmin();
+        }
+        SuspendTestUtils.unsuspendAll();
+        mAppCommsReceiver.unregister();
+    }
+
+    private abstract static class StubbedCallback extends LauncherApps.Callback {
+        @Override
+        public void onPackageRemoved(String packageName, UserHandle user) {
+        }
+
+        @Override
+        public void onPackageAdded(String packageName, UserHandle user) {
+        }
+
+        @Override
+        public void onPackageChanged(String packageName, UserHandle user) {
+        }
+
+        @Override
+        public void onPackagesAvailable(String[] packageNames, UserHandle user, boolean replacing) {
+        }
+
+        @Override
+        public void onPackagesUnavailable(String[] packageNames, UserHandle user,
+                boolean replacing) {
+        }
+    }
+}
diff --git a/tests/suspendapps/tests/src/android/suspendapps/cts/SuspendTestUtils.java b/tests/suspendapps/tests/src/android/suspendapps/cts/SuspendTestUtils.java
new file mode 100644
index 0000000..f2d8572
--- /dev/null
+++ b/tests/suspendapps/tests/src/android/suspendapps/cts/SuspendTestUtils.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.suspendapps.cts;
+
+import static android.app.Activity.RESULT_CANCELED;
+import static android.app.Activity.RESULT_OK;
+
+import static android.suspendapps.cts.Constants.ALL_TEST_PACKAGES;
+import static android.suspendapps.cts.Constants.DEVICE_ADMIN_COMPONENT;
+import static android.suspendapps.cts.Constants.DEVICE_ADMIN_PACKAGE;
+import static android.suspendapps.cts.Constants.TEST_PACKAGE_ARRAY;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.annotation.Nullable;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.SuspendDialogInfo;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.PersistableBundle;
+
+import androidx.annotation.NonNull;
+import androidx.test.InstrumentationRegistry;
+
+import com.android.compatibility.common.util.SystemUtil;
+import com.android.internal.util.ArrayUtils;
+import com.android.suspendapps.testdeviceadmin.TestCommsReceiver;
+
+import libcore.util.EmptyArray;
+
+import java.util.Arrays;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+public class SuspendTestUtils {
+
+    private SuspendTestUtils() {
+    }
+
+    private static Context getContext() {
+        return InstrumentationRegistry.getTargetContext();
+    }
+
+    // Wrapping the suspend/unsuspend methods in another class is needed to avoid error in pre-Q
+    // devices. Direct methods of the test class are enumerated and visited by JUnit, and the
+    // Q+ class SuspendDialogInfo being a parameter type triggers an exception. See b/129913414.
+    static void suspend(PersistableBundle appExtras,
+            PersistableBundle launcherExtras, SuspendDialogInfo dialogInfo) throws Exception {
+        suspendAndAssertResult(TEST_PACKAGE_ARRAY, appExtras, launcherExtras, dialogInfo,
+                EmptyArray.STRING);
+    }
+
+    static void suspendAndAssertResult(String[] packagesToSuspend, PersistableBundle appExtras,
+            PersistableBundle launcherExtras, SuspendDialogInfo dialogInfo,
+            @NonNull String[] expectedToFail) throws Exception {
+        final PackageManager packageManager = getContext().getPackageManager();
+        final String[] failed = SystemUtil.callWithShellPermissionIdentity(
+                () -> packageManager.setPackagesSuspended(packagesToSuspend, true, appExtras,
+                        launcherExtras, dialogInfo));
+        if (failed == null || failed.length != expectedToFail.length
+                || !ArrayUtils.containsAll(failed, expectedToFail)) {
+            fail("setPackagesSuspended failure: failed packages: " + Arrays.toString(failed)
+                    + "; Expected to fail: " + Arrays.toString(expectedToFail));
+        }
+    }
+
+    static void unsuspendAll() throws Exception {
+        final PackageManager packageManager = getContext().getPackageManager();
+        final String[] unchangedPackages = SystemUtil.callWithShellPermissionIdentity(() ->
+                packageManager.setPackagesSuspended(ALL_TEST_PACKAGES, false, null, null,
+                        (SuspendDialogInfo) null));
+        assertTrue("setPackagesSuspended returned non-empty list", unchangedPackages.length == 0);
+    }
+
+    static Bundle createSingleKeyBundle(String key, String value) {
+        final Bundle extras = new Bundle(1);
+        extras.putString(key, value);
+        return extras;
+    }
+
+    static void addAndAssertProfileOwner() {
+        SystemUtil.runShellCommand("dpm set-profile-owner --user cur " + DEVICE_ADMIN_COMPONENT,
+                output -> output.startsWith("Success"));
+    }
+
+    static void removeDeviceAdmin() {
+        SystemUtil.runShellCommand("dpm remove-active-admin --user cur " + DEVICE_ADMIN_COMPONENT);
+    }
+
+    /**
+     * Uses broadcasts to request a specific action from device admin via {@link TestCommsReceiver}
+     *
+     * @return true if the request succeeded
+     */
+    static boolean requestDpmAction(String action, @Nullable Bundle extras, Handler resultHandler)
+            throws InterruptedException {
+        final Intent requestIntent = new Intent(action)
+                .setClassName(DEVICE_ADMIN_PACKAGE, TestCommsReceiver.class.getName())
+                .addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
+        if (extras != null) {
+            requestIntent.putExtras(extras);
+        }
+        final CountDownLatch resultLatch = new CountDownLatch(1);
+        final AtomicInteger result = new AtomicInteger(RESULT_CANCELED);
+        getContext().sendOrderedBroadcast(requestIntent, null,
+                new BroadcastReceiver() {
+                    @Override
+                    public void onReceive(Context context, Intent intent) {
+                        result.set(getResultCode());
+                        resultLatch.countDown();
+                    }
+                }, resultHandler, RESULT_CANCELED, null, null);
+        assertTrue("Broadcast " + requestIntent.getAction() + " timed out",
+                resultLatch.await(10, TimeUnit.SECONDS));
+        return result.get() == RESULT_OK;
+    }
+}
diff --git a/tests/suspendapps/tests/src/android/suspendapps/cts/SuspendedDetailsActivity.java b/tests/suspendapps/tests/src/android/suspendapps/cts/SuspendedDetailsActivity.java
new file mode 100644
index 0000000..932a51b
--- /dev/null
+++ b/tests/suspendapps/tests/src/android/suspendapps/cts/SuspendedDetailsActivity.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.suspendapps.cts;
+
+import static android.suspendapps.cts.Constants.ACTION_REPORT_MORE_DETAILS_ACTIVITY_STARTED;
+import static android.suspendapps.cts.Constants.EXTRA_RECEIVED_PACKAGE_NAME;
+import static android.suspendapps.cts.Constants.PACKAGE_NAME;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.util.Log;
+
+public class SuspendedDetailsActivity extends Activity {
+    private static final String TAG = SuspendedDetailsActivity.class.getSimpleName();
+
+    @Override
+    protected void onStart() {
+        Log.d(TAG, "onStart");
+        final String suspendedPackage =  getIntent().getStringExtra(Intent.EXTRA_PACKAGE_NAME);
+        super.onStart();
+        final Intent reportStart = new Intent(ACTION_REPORT_MORE_DETAILS_ACTIVITY_STARTED)
+                .putExtra(EXTRA_RECEIVED_PACKAGE_NAME, suspendedPackage)
+                .setPackage(PACKAGE_NAME);
+        sendBroadcast(reportStart);
+        finish();
+    }
+}
diff --git a/tests/suspendapps/tests/src/android/suspendapps/cts/UnsuspendReceiver.java b/tests/suspendapps/tests/src/android/suspendapps/cts/UnsuspendReceiver.java
new file mode 100644
index 0000000..8304ccc
--- /dev/null
+++ b/tests/suspendapps/tests/src/android/suspendapps/cts/UnsuspendReceiver.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.suspendapps.cts;
+
+import static android.suspendapps.cts.Constants.ACTION_REPORT_PACKAGE_UNSUSPENDED_MANUALLY;
+import static android.suspendapps.cts.Constants.EXTRA_RECEIVED_PACKAGE_NAME;
+import static android.suspendapps.cts.Constants.PACKAGE_NAME;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.util.Log;
+
+public class UnsuspendReceiver extends BroadcastReceiver {
+    private static final String TAG = UnsuspendReceiver.class.getSimpleName();
+
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        switch (intent.getAction()) {
+            case Intent.ACTION_PACKAGE_UNSUSPENDED_MANUALLY:
+                final String suspendedPackage = intent.getStringExtra(Intent.EXTRA_PACKAGE_NAME);
+                final Intent reportReceipt = new Intent(ACTION_REPORT_PACKAGE_UNSUSPENDED_MANUALLY)
+                        .setPackage(PACKAGE_NAME)
+                        .putExtra(EXTRA_RECEIVED_PACKAGE_NAME, suspendedPackage);
+                context.sendBroadcast(reportReceipt);
+                break;
+            default:
+                Log.w(TAG, "Unknown action " + intent.getAction());
+                break;
+        }
+    }
+}
diff --git a/tests/tests/hardware/res/raw/nintendo_switchpro_keyeventtests.json b/tests/tests/hardware/res/raw/nintendo_switchpro_keyeventtests.json
new file mode 100644
index 0000000..ea91a64
--- /dev/null
+++ b/tests/tests/hardware/res/raw/nintendo_switchpro_keyeventtests.json
@@ -0,0 +1,38 @@
+[
+  {
+    "name": "Press BUTTON_A",
+    "reports": [
+      [0x30, 0xe2, 0x40, 0x04, 0x00, 0x00, 0xcf, 0xf7, 0x7c, 0xcf, 0xf7, 0x7c, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00],
+      [0x30, 0xe2, 0x40, 0x00, 0x00, 0x00, 0xcf, 0xf7, 0x7c, 0xcf, 0xf7, 0x7c, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "KEYBOARD | GAMEPAD",
+    "events": [
+      {"action": "DOWN", "keycode": "BUTTON_A"},
+      {"action": "UP", "keycode": "BUTTON_A"}
+    ]
+  },
+  {
+    "name": "Press BUTTON_B",
+    "reports": [
+      [0x30, 0xe2, 0x40, 0x08, 0x00, 0x00, 0xcf, 0xf7, 0x7c, 0xcf, 0xf7, 0x7c, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00],
+      [0x30, 0xe2, 0x40, 0x00, 0x00, 0x00, 0xcf, 0xf7, 0x7c, 0xcf, 0xf7, 0x7c, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "KEYBOARD | GAMEPAD",
+    "events": [
+      {"action": "DOWN", "keycode": "BUTTON_B"},
+      {"action": "UP", "keycode": "BUTTON_B"}
+    ]
+  }
+]
diff --git a/tests/tests/hardware/res/raw/nintendo_switchpro_motioneventtests.json b/tests/tests/hardware/res/raw/nintendo_switchpro_motioneventtests.json
new file mode 100644
index 0000000..3789332
--- /dev/null
+++ b/tests/tests/hardware/res/raw/nintendo_switchpro_motioneventtests.json
@@ -0,0 +1,14 @@
+[
+  {
+    "name": "Sanity check - should not produce any events",
+    "reports": [
+      [0x30, 0xe2, 0x40, 0x00, 0x00, 0x00, 0xcf, 0xf7, 0x7c, 0xcf, 0xf7, 0x7c, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "JOYSTICK",
+    "events": [
+    ]
+  }
+]
diff --git a/tests/tests/hardware/res/raw/nintendo_switchpro_register.json b/tests/tests/hardware/res/raw/nintendo_switchpro_register.json
new file mode 100644
index 0000000..d1b1939
--- /dev/null
+++ b/tests/tests/hardware/res/raw/nintendo_switchpro_register.json
@@ -0,0 +1,65 @@
+{
+  "id": 1,
+  "command": "register",
+  "name": "Nintendo Switch Pro Controller (Test)",
+  "vid": 0x057e,
+  "pid": 0x2009,
+  "bus": "bluetooth",
+  "descriptor": [0x05, 0x01, 0x09, 0x05, 0xa1, 0x01, 0x06, 0x01, 0xff, 0x85, 0x21, 0x09, 0x21,
+    0x75, 0x08, 0x95, 0x30, 0x81, 0x02, 0x85, 0x30, 0x09, 0x30, 0x75, 0x08, 0x95, 0x30, 0x81,
+    0x02, 0x85, 0x31, 0x09, 0x31, 0x75, 0x08, 0x96, 0x69, 0x01, 0x81, 0x02, 0x85, 0x32, 0x09,
+    0x32, 0x75, 0x08, 0x96, 0x69, 0x01, 0x81, 0x02, 0x85, 0x33, 0x09, 0x33, 0x75, 0x08, 0x96,
+    0x69, 0x01, 0x81, 0x02, 0x85, 0x3f, 0x05, 0x09, 0x19, 0x01, 0x29, 0x10, 0x15, 0x00, 0x25,
+    0x01, 0x75, 0x01, 0x95, 0x10, 0x81, 0x02, 0x05, 0x01, 0x09, 0x39, 0x15, 0x00, 0x25, 0x07,
+    0x75, 0x04, 0x95, 0x01, 0x81, 0x42, 0x05, 0x09, 0x75, 0x04, 0x95, 0x01, 0x81, 0x01, 0x05,
+    0x01, 0x09, 0x30, 0x09, 0x31, 0x09, 0x33, 0x09, 0x34, 0x16, 0x00, 0x00, 0x27, 0xff, 0xff,
+    0x00, 0x00, 0x75, 0x10, 0x95, 0x04, 0x81, 0x02, 0x06, 0x01, 0xff, 0x85, 0x01, 0x09, 0x01,
+    0x75, 0x08, 0x95, 0x30, 0x91, 0x02, 0x85, 0x10, 0x09, 0x10, 0x75, 0x08, 0x95, 0x30, 0x91,
+    0x02, 0x85, 0x11, 0x09, 0x11, 0x75, 0x08, 0x95, 0x30, 0x91, 0x02, 0x85, 0x12, 0x09, 0x12,
+    0x75, 0x08, 0x95, 0x30, 0x91, 0x02, 0xc0],
+  "outputs": [
+    {
+      "description": "Ack for 'set report mode' (0x03)",
+      "output": [0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x30],
+      "response": [0x21, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0xa, 0xb, 0x1,
+        0x03]
+    },
+    {
+      "description": "Ack for 'enable rumble' (0x48)",
+      "output": [0x1, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x48, 0x1],
+      "response": [0x21, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0xa, 0xb, 0x1,
+        0x48]
+    },
+    {
+      "description": "Some other rumble command?",
+      "output": [0x1, 0x4, 0x0, 0x1, 0x64, 0x64, 0x0, 0x1, 0x64, 0x64, 0x48, 0x3],
+      "response": [0x21, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0xa, 0xb, 0x1,
+        0x48]
+    },
+    {
+      "description": "Some other rumble command? -- 2",
+      "output": [0x1, 0x4, 0x0, 0x1, 0x64, 0x64, 0x0, 0x1, 0x64, 0x64, 0x48, 0x7],
+      "response": [0x21, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0xa, 0xb, 0x1,
+        0x48]
+    },
+    {
+      "description": "Info about MAC address (0x2)",
+      "output": [0x1, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2],
+      "response": [0x21, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0xa, 0xb, 0x1,
+        0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0]
+    },
+    {
+      "description": "Ack for 'set player led' (0x30)",
+      "output": [0x1, 0x4, 0x0, 0x1, 0x40, 0x40, 0x0, 0x1, 0x40, 0x40, 0x30, 0xf],
+      "response": [0x21, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0xa, 0xb, 0x1,
+        0x30]
+    },
+    {
+      "description": "Ack for 'set home led' (0x38)",
+      "output": [0x1, 0x5, 0x0, 0x1, 0x40, 0x40, 0x0, 0x1, 0x40, 0x40, 0x38, 0x1, 0x0, 0x0, 0x11,
+        0x11],
+      "response": [0x21, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0xa, 0xb,
+        0x1, 0x38]
+    }
+  ]
+}
diff --git a/tests/tests/hardware/src/android/hardware/input/cts/tests/NintendoSwitchProTest.java b/tests/tests/hardware/src/android/hardware/input/cts/tests/NintendoSwitchProTest.java
new file mode 100644
index 0000000..c826b59
--- /dev/null
+++ b/tests/tests/hardware/src/android/hardware/input/cts/tests/NintendoSwitchProTest.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.hardware.input.cts.tests;
+
+import android.hardware.cts.R;
+import android.os.SystemClock;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.MediumTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class NintendoSwitchProTest extends InputTestCase {
+    public NintendoSwitchProTest() {
+        super(R.raw.nintendo_switchpro_register);
+    }
+
+    @Before
+    public void setUp() {
+        super.setUp();
+        /**
+         * During probe, hid-nintendo sends commands to the joystick and waits for some of those
+         * commands to execute. Somewhere in the middle of the commands, the driver will register
+         * an input device, which is the notification received by InputTestCase.
+         * If a command is still being waited on while we start writing
+         * events to uhid, all incoming events are dropped, because probe() still hasn't finished.
+         * To ensure that hid-nintendo probe is done, add a delay here.
+         */
+        SystemClock.sleep(1000);
+    }
+
+    @Test
+    public void testAllKeys() {
+        testInputEvents(R.raw.nintendo_switchpro_keyeventtests);
+    }
+
+    @Test
+    public void testAllMotions() {
+        testInputEvents(R.raw.nintendo_switchpro_motioneventtests);
+    }
+}
diff --git a/tests/tests/mediaparser/src/android/media/mediaparser/cts/MockMediaParserInputReader.java b/tests/tests/mediaparser/src/android/media/mediaparser/cts/MockMediaParserInputReader.java
index d7fe90d..c906020 100644
--- a/tests/tests/mediaparser/src/android/media/mediaparser/cts/MockMediaParserInputReader.java
+++ b/tests/tests/mediaparser/src/android/media/mediaparser/cts/MockMediaParserInputReader.java
@@ -31,8 +31,7 @@
     }
 
     @Override
-    public int read(byte[] buffer, int offset, int readLength)
-            throws IOException, InterruptedException {
+    public int read(byte[] buffer, int offset, int readLength) throws IOException {
         return mFakeExtractorInput.read(buffer, offset, readLength);
     }
 
diff --git a/tests/tests/mediaparser/src/android/media/mediaparser/cts/MockMediaParserOutputConsumer.java b/tests/tests/mediaparser/src/android/media/mediaparser/cts/MockMediaParserOutputConsumer.java
index c3eafe7..6728c78 100644
--- a/tests/tests/mediaparser/src/android/media/mediaparser/cts/MockMediaParserOutputConsumer.java
+++ b/tests/tests/mediaparser/src/android/media/mediaparser/cts/MockMediaParserOutputConsumer.java
@@ -33,6 +33,7 @@
 import com.google.android.exoplayer2.video.ColorInfo;
 
 import java.io.IOException;
+import java.io.InterruptedIOException;
 import java.util.ArrayList;
 
 public class MockMediaParserOutputConsumer implements MediaParser.OutputConsumer {
@@ -81,13 +82,19 @@
 
     @Override
     public void onSampleData(int trackIndex, MediaParser.InputReader inputReader)
-            throws IOException, InterruptedException {
-        mFakeExtractorOutput
-                .track(trackIndex, C.TRACK_TYPE_UNKNOWN)
-                .sampleData(
-                        new ExtractorInputAdapter(inputReader),
-                        (int) inputReader.getLength(),
-                        false);
+            throws IOException {
+        try {
+            mFakeExtractorOutput
+                    .track(trackIndex, C.TRACK_TYPE_UNKNOWN)
+                    .sampleData(
+                            new ExtractorInputAdapter(inputReader),
+                            (int) inputReader.getLength(),
+                            false);
+        } catch (InterruptedException e) {
+            // TODO: Remove this exception replacement once we update the ExoPlayer
+            // version.
+            throw new InterruptedIOException();
+        }
     }
 
     @Override
diff --git a/tests/tests/net/src/android/net/wifi/cts/WifiNetworkSuggestionTest.java b/tests/tests/net/src/android/net/wifi/cts/WifiNetworkSuggestionTest.java
new file mode 100644
index 0000000..994b6c9
--- /dev/null
+++ b/tests/tests/net/src/android/net/wifi/cts/WifiNetworkSuggestionTest.java
@@ -0,0 +1,245 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.wifi.cts;
+
+import static android.net.wifi.WifiEnterpriseConfig.Eap.AKA;
+
+import android.net.MacAddress;
+import android.net.wifi.WifiEnterpriseConfig;
+import android.net.wifi.WifiNetworkSuggestion;
+import android.net.wifi.hotspot2.PasspointConfiguration;
+import android.net.wifi.hotspot2.pps.Credential;
+import android.net.wifi.hotspot2.pps.HomeSp;
+import android.telephony.TelephonyManager;
+import android.test.AndroidTestCase;
+
+public class WifiNetworkSuggestionTest extends AndroidTestCase {
+    private static final String TEST_SSID = "testSsid";
+    private static final String TEST_BSSID = "00:df:aa:bc:12:23";
+    private static final String TEST_PASSPHRASE = "testPassword";
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        if (!WifiFeature.isWifiSupported(getContext())) {
+            // skip the test if WiFi is not supported
+            return;
+        }
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        if (!WifiFeature.isWifiSupported(getContext())) {
+            // skip the test if WiFi is not supported
+            super.tearDown();
+            return;
+        }
+        super.tearDown();
+    }
+
+    private WifiNetworkSuggestion.Builder createBuilderWithCommonParams() {
+        return createBuilderWithCommonParams(false);
+    }
+
+    private WifiNetworkSuggestion.Builder createBuilderWithCommonParams(boolean isPasspoint) {
+        WifiNetworkSuggestion.Builder builder = new WifiNetworkSuggestion.Builder();
+        if (!isPasspoint) {
+            builder.setSsid(TEST_SSID);
+            builder.setBssid(MacAddress.fromString(TEST_BSSID));
+            builder.setIsEnhancedOpen(false);
+            builder.setIsHiddenSsid(true);
+        }
+        builder.setPriority(0);
+        builder.setIsAppInteractionRequired(true);
+        builder.setIsUserInteractionRequired(true);
+        builder.setIsMetered(true);
+        builder.setCarrierId(TelephonyManager.UNKNOWN_CARRIER_ID);
+        builder.setCredentialSharedWithUser(true);
+        builder.setIsInitialAutojoinEnabled(true);
+        builder.setUntrusted(false);
+        return builder;
+    }
+
+    private void validateCommonParams(WifiNetworkSuggestion suggestion) {
+        validateCommonParams(suggestion, false);
+    }
+
+    private void validateCommonParams(WifiNetworkSuggestion suggestion, boolean isPasspoint) {
+        assertNotNull(suggestion);
+        assertNotNull(suggestion.getWifiConfiguration());
+        if (!isPasspoint) {
+            assertEquals(TEST_SSID, suggestion.getSsid());
+            assertEquals(TEST_BSSID, suggestion.getBssid().toString());
+            assertFalse(suggestion.isEnhancedOpen());
+            assertTrue(suggestion.isHiddenSsid());
+        }
+        assertEquals(0, suggestion.getPriority());
+        assertTrue(suggestion.isAppInteractionRequired());
+        assertTrue(suggestion.isUserInteractionRequired());
+        assertTrue(suggestion.isMetered());
+        assertTrue(suggestion.isCredentialSharedWithUser());
+        assertTrue(suggestion.isInitialAutojoinEnabled());
+        assertFalse(suggestion.isUntrusted());
+    }
+
+    /**
+     * Tests {@link android.net.wifi.WifiNetworkSuggestion.Builder} class.
+     */
+    public void testBuilderWithWpa2Passphrase() throws Exception {
+        if (!WifiFeature.isWifiSupported(getContext())) {
+            // skip the test if WiFi is not supported
+            return;
+        }
+        WifiNetworkSuggestion suggestion =
+                createBuilderWithCommonParams()
+                .setWpa2Passphrase(TEST_PASSPHRASE)
+                .build();
+        validateCommonParams(suggestion);
+        assertEquals(TEST_PASSPHRASE, suggestion.getPassphrase());
+        assertNotNull(suggestion.getEnterpriseConfig());
+        assertNull(suggestion.getPasspointConfig());
+    }
+
+    /**
+     * Tests {@link android.net.wifi.WifiNetworkSuggestion.Builder} class.
+     */
+    public void testBuilderWithWpa3Passphrase() throws Exception {
+        if (!WifiFeature.isWifiSupported(getContext())) {
+            // skip the test if WiFi is not supported
+            return;
+        }
+        WifiNetworkSuggestion suggestion =
+                createBuilderWithCommonParams()
+                        .setWpa3Passphrase(TEST_PASSPHRASE)
+                        .build();
+        validateCommonParams(suggestion);
+        assertEquals(TEST_PASSPHRASE, suggestion.getPassphrase());
+        assertNotNull(suggestion.getEnterpriseConfig());
+        assertNull(suggestion.getPasspointConfig());
+    }
+
+    /**
+     * Tests {@link android.net.wifi.WifiNetworkSuggestion.Builder} class.
+     */
+    public void testBuilderWithWapiPassphrase() throws Exception {
+        if (!WifiFeature.isWifiSupported(getContext())) {
+            // skip the test if WiFi is not supported
+            return;
+        }
+        WifiNetworkSuggestion suggestion =
+                createBuilderWithCommonParams()
+                        .setWapiPassphrase(TEST_PASSPHRASE)
+                        .build();
+        validateCommonParams(suggestion);
+        assertEquals(TEST_PASSPHRASE, suggestion.getPassphrase());
+        assertNotNull(suggestion.getEnterpriseConfig());
+        assertNull(suggestion.getPasspointConfig());
+    }
+
+    private static WifiEnterpriseConfig createEnterpriseConfig() {
+        WifiEnterpriseConfig config = new WifiEnterpriseConfig();
+        config.setEapMethod(AKA);
+        return config;
+    }
+
+    /**
+     * Tests {@link android.net.wifi.WifiNetworkSuggestion.Builder} class.
+     */
+    public void testBuilderWithWpa2Enterprise() throws Exception {
+        if (!WifiFeature.isWifiSupported(getContext())) {
+            // skip the test if WiFi is not supported
+            return;
+        }
+        WifiEnterpriseConfig enterpriseConfig = createEnterpriseConfig();
+        WifiNetworkSuggestion suggestion =
+                createBuilderWithCommonParams()
+                        .setWpa2EnterpriseConfig(enterpriseConfig)
+                        .build();
+        validateCommonParams(suggestion);
+        assertNull(suggestion.getPassphrase());
+        assertNotNull(suggestion.getEnterpriseConfig());
+        assertEquals(enterpriseConfig.getEapMethod(),
+                suggestion.getEnterpriseConfig().getEapMethod());
+        assertNull(suggestion.getPasspointConfig());
+    }
+
+    /**
+     * Tests {@link android.net.wifi.WifiNetworkSuggestion.Builder} class.
+     */
+    public void testBuilderWithWpa3Enterprise() throws Exception {
+        if (!WifiFeature.isWifiSupported(getContext())) {
+            // skip the test if WiFi is not supported
+            return;
+        }
+        WifiEnterpriseConfig enterpriseConfig = createEnterpriseConfig();
+        WifiNetworkSuggestion suggestion =
+                createBuilderWithCommonParams()
+                        .setWpa3EnterpriseConfig(enterpriseConfig)
+                        .build();
+        validateCommonParams(suggestion);
+        assertNull(suggestion.getPassphrase());
+        assertNotNull(suggestion.getEnterpriseConfig());
+        assertEquals(enterpriseConfig.getEapMethod(),
+                suggestion.getEnterpriseConfig().getEapMethod());
+        assertNull(suggestion.getPasspointConfig());
+    }
+
+    /**
+     * Helper function for creating a {@link PasspointConfiguration} for testing.
+     *
+     * @return {@link PasspointConfiguration}
+     */
+    private static PasspointConfiguration createPasspointConfig() {
+        HomeSp homeSp = new HomeSp();
+        homeSp.setFqdn("fqdn");
+        homeSp.setFriendlyName("friendly name");
+        homeSp.setRoamingConsortiumOis(new long[] {0x55, 0x66});
+        Credential cred = new Credential();
+        cred.setRealm("realm");
+        cred.setUserCredential(null);
+        cred.setCertCredential(null);
+        cred.setSimCredential(new Credential.SimCredential());
+        cred.getSimCredential().setImsi("1234*");
+        cred.getSimCredential().setEapType(23); // EAP-AKA
+        cred.setCaCertificate(null);
+        cred.setClientCertificateChain(null);
+        cred.setClientPrivateKey(null);
+        PasspointConfiguration config = new PasspointConfiguration();
+        config.setHomeSp(homeSp);
+        config.setCredential(cred);
+        return config;
+    }
+
+    /**
+     * Tests {@link android.net.wifi.WifiNetworkSuggestion.Builder} class.
+     */
+    public void testBuilderWithPasspointConfig() throws Exception {
+        if (!WifiFeature.isWifiSupported(getContext())) {
+            // skip the test if WiFi is not supported
+            return;
+        }
+        PasspointConfiguration passpointConfig = createPasspointConfig();
+        WifiNetworkSuggestion suggestion =
+                createBuilderWithCommonParams(true)
+                        .setPasspointConfig(passpointConfig)
+                        .build();
+        validateCommonParams(suggestion, true);
+        assertNull(suggestion.getPassphrase());
+        assertNotNull(suggestion.getEnterpriseConfig());
+        assertEquals(passpointConfig, suggestion.getPasspointConfig());
+    }
+}
diff --git a/tests/tests/telephony3/src/android/telephony3/cts/TelephonyManagerTest.java b/tests/tests/telephony3/src/android/telephony3/cts/TelephonyManagerTest.java
index 57f2e9b..eaf161e 100644
--- a/tests/tests/telephony3/src/android/telephony3/cts/TelephonyManagerTest.java
+++ b/tests/tests/telephony3/src/android/telephony3/cts/TelephonyManagerTest.java
@@ -21,7 +21,10 @@
 import static org.junit.Assert.fail;
 
 import android.content.Context;
+import android.content.pm.PackageManager;
 import android.os.Build;
+import android.telephony.SubscriptionInfo;
+import android.telephony.SubscriptionManager;
 import android.telephony.TelephonyManager;
 
 import androidx.test.InstrumentationRegistry;
@@ -39,13 +42,13 @@
  */
 @RunWith(AndroidJUnit4.class)
 public class TelephonyManagerTest {
+    private Context mContext;
     private TelephonyManager mTelephonyManager;
 
     @Before
     public void setUp() throws Exception {
-        mTelephonyManager =
-                (TelephonyManager) InstrumentationRegistry.getContext().getSystemService(
-                        Context.TELEPHONY_SERVICE);
+        mContext = InstrumentationRegistry.getContext();
+        mTelephonyManager = (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE);
     }
 
     @Test
@@ -86,6 +89,23 @@
                     "An app targeting pre-Q with the READ_PHONE_STATE permission granted must "
                             + "receive " + Build.UNKNOWN + " when invoking Build.getSerial",
                     Build.getSerial(), Build.UNKNOWN);
+            // Previous getIccId documentation does not indicate the value returned if the ICC ID is
+            // not available, so to prevent NPEs SubscriptionInfo#getIccId will return an empty
+            // string if the caller does not have permission to access this identifier.
+            if (mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELEPHONY)) {
+                SubscriptionManager subscriptionManager =
+                        (SubscriptionManager) mContext.getSystemService(
+                                Context.TELEPHONY_SUBSCRIPTION_SERVICE);
+                int subId = subscriptionManager.getDefaultSubscriptionId();
+                if (subId != SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
+                    SubscriptionInfo subInfo = subscriptionManager.getActiveSubscriptionInfo(
+                            subId);
+                    assertEquals(
+                            "An app targeting pre-Q with the READ_PHONE_STATE permission granted "
+                                    + "must receive an empty string when invoking getIccId",
+                            "", subInfo.getIccId());
+                }
+            }
         } catch (SecurityException e) {
             fail("An app targeting pre-Q with the READ_PHONE_STATE permission granted must "
                     + "receive null (or "
diff --git a/tests/tests/view/jni/android_view_cts_ChoreographerNativeTest.cpp b/tests/tests/view/jni/android_view_cts_ChoreographerNativeTest.cpp
index 0877e1b..ab4ce58 100644
--- a/tests/tests/view/jni/android_view_cts_ChoreographerNativeTest.cpp
+++ b/tests/tests/view/jni/android_view_cts_ChoreographerNativeTest.cpp
@@ -319,10 +319,8 @@
 
     AChoreographer_registerRefreshRateCallback(choreographer, refreshRateCallback, cb);
 
-    ALooper* looper = ALooper_forThread();
     // Give the display system time to push an initial callback.
     std::this_thread::sleep_for(NOMINAL_VSYNC_PERIOD * 10);
-    ALooper_pollAll(16, nullptr, nullptr, nullptr);
     verifyRefreshRateCallback(env, cb, 1);
     AChoreographer_unregisterRefreshRateCallback(choreographer, refreshRateCallback, cb);
 }
@@ -335,23 +333,18 @@
 
     AChoreographer_registerRefreshRateCallback(choreographer, refreshRateCallback, cb1);
 
-    ALooper* looper = ALooper_forThread();
-
     // Give the display system time to push an initial callback.
     std::this_thread::sleep_for(NOMINAL_VSYNC_PERIOD * 10);
-    ALooper_pollAll(16, nullptr, nullptr, nullptr);
     verifyRefreshRateCallback(env, cb1, 1);
 
     AChoreographer_unregisterRefreshRateCallback(choreographer, refreshRateCallback, cb1);
     // Flush out pending callback events for the callback
     std::this_thread::sleep_for(NOMINAL_VSYNC_PERIOD * 10);
-    ALooper_pollAll(16, nullptr, nullptr, nullptr);
     resetRefreshRateCallback(cb1);
 
     AChoreographer_registerRefreshRateCallback(choreographer, refreshRateCallback, cb2);
     // Verify that cb2 is called on registration, but not cb1.
     std::this_thread::sleep_for(NOMINAL_VSYNC_PERIOD * 10);
-    ALooper_pollAll(16, nullptr, nullptr, nullptr);
     verifyRefreshRateCallback(env, cb1, 0);
     verifyRefreshRateCallback(env, cb2, 1);
     AChoreographer_unregisterRefreshRateCallback(choreographer, refreshRateCallback, cb2);
@@ -366,12 +359,9 @@
     AChoreographer_registerRefreshRateCallback(choreographer, refreshRateCallback, cb1);
     AChoreographer_registerRefreshRateCallback(choreographer, refreshRateCallback, cb2);
 
-    ALooper* looper = ALooper_forThread();
-
     // Give the display system time to push an initial refresh rate change.
     // Polling the event will allow both callbacks to be triggered.
     std::this_thread::sleep_for(NOMINAL_VSYNC_PERIOD * 10);
-    ALooper_pollAll(16, nullptr, nullptr, nullptr);
     verifyRefreshRateCallback(env, cb1, 1);
     verifyRefreshRateCallback(env, cb2, 1);
 
@@ -388,28 +378,50 @@
     AChoreographer_registerRefreshRateCallback(choreographer, refreshRateCallback, cb1);
     AChoreographer_registerRefreshRateCallback(choreographer, refreshRateCallback, cb1);
 
-    ALooper* looper = ALooper_forThread();
-
     // Give the display system time to push an initial callback.
     std::this_thread::sleep_for(NOMINAL_VSYNC_PERIOD * 10);
-    ALooper_pollAll(16, nullptr, nullptr, nullptr);
     verifyRefreshRateCallback(env, cb1, 1);
 
     AChoreographer_unregisterRefreshRateCallback(choreographer, refreshRateCallback, cb1);
     // Flush out pending callback events for the callback
     std::this_thread::sleep_for(NOMINAL_VSYNC_PERIOD * 10);
-    ALooper_pollAll(16, nullptr, nullptr, nullptr);
     resetRefreshRateCallback(cb1);
 
     AChoreographer_registerRefreshRateCallback(choreographer, refreshRateCallback, cb2);
     // Verify that cb1 is not called again, even thiough it was registered once
     // and unregistered again
     std::this_thread::sleep_for(NOMINAL_VSYNC_PERIOD * 10);
-    ALooper_pollAll(16, nullptr, nullptr, nullptr);
     verifyRefreshRateCallback(env, cb1, 0);
     AChoreographer_unregisterRefreshRateCallback(choreographer, refreshRateCallback, cb2);
 }
 
+// This test must be run on the UI thread for fine-grained control of looper
+// scheduling.
+static void android_view_cts_ChoreographerNativeTest_testRefreshRateCallbackMixedWithFrameCallbacks(
+        JNIEnv* env, jclass, jlong choreographerPtr) {
+    AChoreographer* choreographer = reinterpret_cast<AChoreographer*>(choreographerPtr);
+    RefreshRateCallback* cb = new RefreshRateCallback("cb");
+
+    AChoreographer_registerRefreshRateCallback(choreographer, refreshRateCallback, cb);
+
+    Callback* cb1 = new Callback("cb1");
+    Callback* cb64 = new Callback("cb64");
+    auto start = now();
+
+    auto delay = std::chrono::duration_cast<std::chrono::milliseconds>(DELAY_PERIOD).count();
+    AChoreographer_postFrameCallbackDelayed(choreographer, frameCallback, cb1, delay);
+    AChoreographer_postFrameCallbackDelayed64(choreographer, frameCallback64, cb64, delay);
+
+    std::this_thread::sleep_for(DELAY_PERIOD + NOMINAL_VSYNC_PERIOD * 10);
+    ALooper_pollAll(16, nullptr, nullptr, nullptr);
+    verifyRefreshRateCallback(env, cb, 1);
+    verifyCallback(env, cb64, 1, start, DELAY_PERIOD + NOMINAL_VSYNC_PERIOD * 11);
+    const auto delayToTestFor32Bit =
+            sizeof(long) == sizeof(int64_t) ? DELAY_PERIOD + NOMINAL_VSYNC_PERIOD * 11 : ZERO;
+    verifyCallback(env, cb1, 1, start, delayToTestFor32Bit);
+    AChoreographer_unregisterRefreshRateCallback(choreographer, refreshRateCallback, cb);
+}
+
 static JNINativeMethod gMethods[] = {
     {  "nativeGetChoreographer", "()J",
             (void *) android_view_cts_ChoreographerNativeTest_getChoreographer},
@@ -435,6 +447,8 @@
             (void *) android_view_cts_ChoreographerNativeTest_testMultipleRefreshRateCallbacks},
     {  "nativeTestAttemptToAddRefreshRateCallbackTwiceDoesNotAddTwice", "(J)V",
             (void *) android_view_cts_ChoreographerNativeTest_testAttemptToAddRefreshRateCallbackTwiceDoesNotAddTwice},
+    {  "nativeTestRefreshRateCallbackMixedWithFrameCallbacks", "(J)V",
+            (void *) android_view_cts_ChoreographerNativeTest_testRefreshRateCallbackMixedWithFrameCallbacks},
 };
 
 int register_android_view_cts_ChoreographerNativeTest(JNIEnv* env)
diff --git a/tests/tests/view/src/android/view/cts/ChoreographerNativeTest.java b/tests/tests/view/src/android/view/cts/ChoreographerNativeTest.java
index 20a9bda..a8e0134 100644
--- a/tests/tests/view/src/android/view/cts/ChoreographerNativeTest.java
+++ b/tests/tests/view/src/android/view/cts/ChoreographerNativeTest.java
@@ -60,6 +60,7 @@
     private static native void nativeTestMultipleRefreshRateCallbacks(long ptr);
     private static native void nativeTestAttemptToAddRefreshRateCallbackTwiceDoesNotAddTwice(
             long ptr);
+    private static native void nativeTestRefreshRateCallbackMixedWithFrameCallbacks(long ptr);
 
     private Context mContext;
     private DisplayManager mDisplayManager;
@@ -151,4 +152,11 @@
         nativeTestAttemptToAddRefreshRateCallbackTwiceDoesNotAddTwice(mChoreographerPtr);
     }
 
+    @UiThreadTest
+    @SmallTest
+    @Test
+    public void testRefreshRateCallbackMixedWithFrameCallbacks() {
+        nativeTestRefreshRateCallbackMixedWithFrameCallbacks(mChoreographerPtr);
+    }
+
 }