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);
+ }
+
}