Update accessibility shortcut CTS tests
Bug: 136293963
Test: atest AccessibilityShortcutTest
Change-Id: I9c08e55c27e24a7060abaae1064d96720c696579
diff --git a/tests/accessibility/AndroidManifest.xml b/tests/accessibility/AndroidManifest.xml
index d4cea0f..c9e0c71 100644
--- a/tests/accessibility/AndroidManifest.xml
+++ b/tests/accessibility/AndroidManifest.xml
@@ -57,11 +57,31 @@
android:resource="@xml/speaking_and_vibrating_accessibilityservice" />
</service>
+ <service android:name=".AccessibilityButtonService"
+ android:label="@string/title_accessibility_button_service"
+ android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
+ <intent-filter>
+ <action android:name="android.accessibilityservice.AccessibilityService"/>
+ </intent-filter>
+ <meta-data android:name="android.accessibilityservice"
+ android:resource="@xml/accessibility_button_service" />
+ </service>
+
<activity
android:label="@string/some_description"
android:name=".DummyActivity"
android:screenOrientation="locked"/>
+ <activity android:name=".AccessibilityShortcutTargetActivity"
+ android:label="@string/shortcut_target_title">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN"/>
+ <category android:name="android.intent.category.ACCESSIBILITY_SHORTCUT_TARGET" />
+ </intent-filter>
+ <meta-data android:name="android.accessibilityshortcut.target"
+ android:resource="@xml/shortcut_target_activity"/>
+ </activity>
+
</application>
<instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
diff --git a/tests/accessibility/res/layout/shortcut_target.xml b/tests/accessibility/res/layout/shortcut_target.xml
new file mode 100644
index 0000000..42d10a0
--- /dev/null
+++ b/tests/accessibility/res/layout/shortcut_target.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2019 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:gravity="center"
+ android:orientation="vertical">
+ <Button
+ android:id="@+id/targetActionBtn"
+ android:text="@string/shortcut_button_title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"/>
+</LinearLayout>
diff --git a/tests/accessibility/res/values/strings.xml b/tests/accessibility/res/values/strings.xml
index 293d5b0..df5c9f2 100644
--- a/tests/accessibility/res/values/strings.xml
+++ b/tests/accessibility/res/values/strings.xml
@@ -26,10 +26,19 @@
<!-- String title for the vibrating accessibility service -->
<string name="title_speaking_and_vibrating_accessibility_service">Speaking and Vibrating Accessibility Service</string>
+ <!-- String title for the accessibility button service -->
+ <string name="title_accessibility_button_service">Accessibility Button Service</string>
+
<!-- Description of the speaking accessibility service -->
<string name="some_description">Some description</string>
<!-- Summary of the speaking accessibility service -->
<string name="some_summary">Some summary</string>
+ <!-- String title for the button of shortcut target activity -->
+ <string name="shortcut_button_title">Action</string>
+
+ <!-- String title for the shortcut target activity -->
+ <string name="shortcut_target_title">Shortcut Target</string>
+
</resources>
diff --git a/tests/accessibility/res/xml/accessibility_button_service.xml b/tests/accessibility/res/xml/accessibility_button_service.xml
new file mode 100644
index 0000000..d475266
--- /dev/null
+++ b/tests/accessibility/res/xml/accessibility_button_service.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
+ android:description="@string/some_description"
+ android:accessibilityEventTypes="typeAllMask"
+ android:accessibilityFeedbackType="feedbackGeneric"
+ android:accessibilityFlags="flagRequestAccessibilityButton"
+ android:notificationTimeout="0" />
\ No newline at end of file
diff --git a/tests/accessibility/res/xml/shortcut_target_activity.xml b/tests/accessibility/res/xml/shortcut_target_activity.xml
new file mode 100644
index 0000000..b258d3f
--- /dev/null
+++ b/tests/accessibility/res/xml/shortcut_target_activity.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+ ~ Copyright (C) 2019 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<accessibility-shortcut-target xmlns:android="http://schemas.android.com/apk/res/android"
+ android:description="@string/some_description"
+ android:summary="@string/some_summary"
+/>
\ No newline at end of file
diff --git a/tests/accessibility/src/android/view/accessibility/cts/AccessibilityButtonService.java b/tests/accessibility/src/android/view/accessibility/cts/AccessibilityButtonService.java
new file mode 100644
index 0000000..3b4b8fd
--- /dev/null
+++ b/tests/accessibility/src/android/view/accessibility/cts/AccessibilityButtonService.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.accessibility.cts;
+
+import android.accessibility.cts.common.InstrumentedAccessibilityService;
+
+/**
+ * An accessibility service that requests accessibility button.
+ */
+public class AccessibilityButtonService extends InstrumentedAccessibilityService {
+}
diff --git a/tests/accessibility/src/android/view/accessibility/cts/AccessibilityManagerTest.java b/tests/accessibility/src/android/view/accessibility/cts/AccessibilityManagerTest.java
index 274d13c..f1eab6c 100644
--- a/tests/accessibility/src/android/view/accessibility/cts/AccessibilityManagerTest.java
+++ b/tests/accessibility/src/android/view/accessibility/cts/AccessibilityManagerTest.java
@@ -17,10 +17,6 @@
package android.view.accessibility.cts;
import static android.accessibility.cts.common.InstrumentedAccessibilityService.TIMEOUT_SERVICE_ENABLE;
-import static android.accessibility.cts.common.ServiceControlUtils.getEnabledServices;
-import static android.accessibility.cts.common.ServiceControlUtils.waitForConditionWithServiceStateChange;
-
-import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@@ -38,9 +34,6 @@
import android.content.Context;
import android.content.pm.ServiceInfo;
import android.os.Handler;
-import android.platform.test.annotations.AppModeFull;
-import android.provider.Settings;
-import android.text.TextUtils;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityManager;
import android.view.accessibility.AccessibilityManager.AccessibilityStateChangeListener;
@@ -444,64 +437,6 @@
}
}
- @AppModeFull
- @Test
- public void performShortcut_withoutPermission_fails() {
- UiAutomation uiAutomation = sInstrumentation.getUiAutomation(
- UiAutomation.FLAG_DONT_SUPPRESS_ACCESSIBILITY_SERVICES);
-
- String originalShortcut = configureShortcut(
- uiAutomation, SpeakingAccessibilityService.COMPONENT_NAME.flattenToString());
- try {
- mAccessibilityManager.performAccessibilityShortcut();
- fail("No security exception thrown when performing shortcut without permission");
- } catch (SecurityException e) {
- // Expected
- } finally {
- configureShortcut(uiAutomation, originalShortcut);
- uiAutomation.destroy();
- }
- assertTrue(TextUtils.isEmpty(getEnabledServices(mTargetContext.getContentResolver())));
- }
-
- @AppModeFull
- @Test
- public void performShortcut_withPermission_succeeds() {
- UiAutomation uiAutomation = sInstrumentation.getUiAutomation(
- UiAutomation.FLAG_DONT_SUPPRESS_ACCESSIBILITY_SERVICES);
-
- String originalShortcut = configureShortcut(
- uiAutomation, SpeakingAccessibilityService.COMPONENT_NAME.flattenToString());
- try {
- runWithShellPermissionIdentity(uiAutomation,
- () -> mAccessibilityManager.performAccessibilityShortcut());
- // Make sure the service starts up
- final SpeakingAccessibilityService service =
- SpeakingAccessibilityService.getInstanceForClass(
- SpeakingAccessibilityService.class, TIMEOUT_SERVICE_ENABLE);
- assertTrue("Speaking accessibility service starts up", service != null);
- } finally {
- configureShortcut(uiAutomation, originalShortcut);
- uiAutomation.destroy();
- }
- }
-
- private String configureShortcut(UiAutomation uiAutomation, String shortcutService) {
- String currentService = Settings.Secure.getString(mTargetContext.getContentResolver(),
- Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE);
- putSecureSetting(uiAutomation, Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE,
- shortcutService);
- if (shortcutService != null) {
- runWithShellPermissionIdentity(uiAutomation, () ->
- waitForConditionWithServiceStateChange(mTargetContext, () -> TextUtils.equals(
- mAccessibilityManager.getAccessibilityShortcutService(),
- shortcutService),
- TIMEOUT_SERVICE_ENABLE,
- "accessibility shortcut set to test service"));
- }
- return currentService;
- }
-
private void assertAtomicBooleanBecomes(AtomicBoolean atomicBoolean,
boolean expectedValue, Object waitObject, String message)
throws Exception {
diff --git a/tests/accessibility/src/android/view/accessibility/cts/AccessibilityShortcutTargetActivity.java b/tests/accessibility/src/android/view/accessibility/cts/AccessibilityShortcutTargetActivity.java
new file mode 100644
index 0000000..921a769
--- /dev/null
+++ b/tests/accessibility/src/android/view/accessibility/cts/AccessibilityShortcutTargetActivity.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.accessibility.cts;
+
+import android.app.Activity;
+import android.app.KeyguardManager;
+import android.content.Context;
+import android.os.Bundle;
+import android.view.accessibility.cts.R;
+
+/**
+ * The accessibility shortcut target activity.
+ */
+public class AccessibilityShortcutTargetActivity extends Activity {
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.shortcut_target);
+ }
+}
diff --git a/tests/accessibility/src/android/view/accessibility/cts/AccessibilityShortcutTest.java b/tests/accessibility/src/android/view/accessibility/cts/AccessibilityShortcutTest.java
new file mode 100644
index 0000000..5f61cd2
--- /dev/null
+++ b/tests/accessibility/src/android/view/accessibility/cts/AccessibilityShortcutTest.java
@@ -0,0 +1,458 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.accessibility.cts;
+
+import static android.accessibility.cts.common.InstrumentedAccessibilityService.TIMEOUT_SERVICE_ENABLE;
+import static android.accessibility.cts.common.ServiceControlUtils.waitForConditionWithServiceStateChange;
+import static android.app.UiAutomation.FLAG_DONT_SUPPRESS_ACCESSIBILITY_SERVICES;
+import static android.provider.Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE;
+
+import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.accessibility.cts.common.AccessibilityDumpOnFailureRule;
+import android.accessibility.cts.common.InstrumentedAccessibilityServiceTestRule;
+import android.accessibility.cts.common.ShellCommandBuilder;
+import android.accessibilityservice.AccessibilityButtonController;
+import android.accessibilityservice.AccessibilityButtonController.AccessibilityButtonCallback;
+import android.accessibilityservice.AccessibilityService;
+import android.app.Activity;
+import android.app.Instrumentation;
+import android.app.Instrumentation.ActivityMonitor;
+import android.app.Service;
+import android.app.UiAutomation;
+import android.content.ComponentName;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.platform.test.annotations.AppModeFull;
+import android.provider.Settings;
+import android.text.TextUtils;
+import android.view.accessibility.AccessibilityManager;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.compatibility.common.util.TestUtils;
+
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.RuleChain;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * Tests accessibility shortcut related functionality
+ */
+@AppModeFull
+@RunWith(AndroidJUnit4.class)
+public class AccessibilityShortcutTest {
+ private static final int ACCESSIBILITY_BUTTON = 0;
+ private static final int ACCESSIBILITY_SHORTCUT_KEY = 1;
+
+ private static final String ACCESSIBILITY_BUTTON_TARGET_COMPONENT =
+ "accessibility_button_target_component";
+
+ private static final char DELIMITER = ':';
+ private static final String[] EMPTY_STRING_ARRAY = new String[0];
+
+ private static Instrumentation sInstrumentation;
+ private static UiAutomation sUiAutomation;
+
+ private final InstrumentedAccessibilityServiceTestRule<SpeakingAccessibilityService>
+ mServiceRule = new InstrumentedAccessibilityServiceTestRule<>(
+ SpeakingAccessibilityService.class, false);
+
+ private final InstrumentedAccessibilityServiceTestRule<AccessibilityButtonService>
+ mA11yButtonServiceRule = new InstrumentedAccessibilityServiceTestRule<>(
+ AccessibilityButtonService.class, false);
+
+ private final AccessibilityDumpOnFailureRule mDumpOnFailureRule =
+ new AccessibilityDumpOnFailureRule();
+
+ @Rule
+ public final RuleChain mRuleChain = RuleChain
+ .outerRule(mServiceRule)
+ .around(mA11yButtonServiceRule)
+ .around(mDumpOnFailureRule);
+
+ private Context mTargetContext;
+ private ContentResolver mContentResolver;
+ private AccessibilityManager mAccessibilityManager;
+
+ private ActivityMonitor mActivityMonitor;
+ private Activity mShortcutTargetActivity;
+
+ private String mSpeakingA11yServiceName;
+ private String mShortcutTargetActivityName;
+ private String mA11yButtonServiceName;
+
+ // These are the current shortcut states before doing the tests. Roll back them after the tests.
+ private String[] mA11yShortcutTargets;
+ private String[] mA11yButtonTargets;
+ private List<String> mA11yShortcutTargetList;
+ private List<String> mA11yButtonTargetList;
+
+ @BeforeClass
+ public static void oneTimeSetup() {
+ sInstrumentation = InstrumentationRegistry.getInstrumentation();
+ sUiAutomation = sInstrumentation.getUiAutomation(FLAG_DONT_SUPPRESS_ACCESSIBILITY_SERVICES);
+ }
+
+ @AfterClass
+ public static void postTestTearDown() {
+ sUiAutomation.destroy();
+ }
+
+ @Before
+ public void setUp() {
+ mTargetContext = sInstrumentation.getTargetContext();
+ mContentResolver = mTargetContext.getContentResolver();
+ mAccessibilityManager = (AccessibilityManager) mTargetContext.getSystemService(
+ Service.ACCESSIBILITY_SERVICE);
+ mSpeakingA11yServiceName = new ComponentName(mTargetContext,
+ SpeakingAccessibilityService.class).flattenToString();
+ mShortcutTargetActivityName = new ComponentName(mTargetContext,
+ AccessibilityShortcutTargetActivity.class).flattenToString();
+ mA11yButtonServiceName = new ComponentName(mTargetContext,
+ AccessibilityButtonService.class).flattenToString();
+ mActivityMonitor = new ActivityMonitor(
+ AccessibilityShortcutTargetActivity.class.getName(), null, false);
+ sInstrumentation.addMonitor(mActivityMonitor);
+
+ // Reads current shortcut states.
+ readShortcutStates();
+ }
+
+ @After
+ public void tearDown() {
+ if (mActivityMonitor != null) {
+ sInstrumentation.removeMonitor(mActivityMonitor);
+ }
+ if (mShortcutTargetActivity != null) {
+ sInstrumentation.runOnMainSync(() -> mShortcutTargetActivity.finish());
+ }
+
+ // Rollback default shortcut states.
+ if (configureShortcut(ACCESSIBILITY_SHORTCUT_KEY, mA11yShortcutTargets)) {
+ waitForShortcutStateChange(ACCESSIBILITY_SHORTCUT_KEY, mA11yShortcutTargetList);
+ }
+ if (configureShortcut(ACCESSIBILITY_BUTTON, mA11yButtonTargets)) {
+ waitForShortcutStateChange(ACCESSIBILITY_BUTTON, mA11yButtonTargetList);
+ }
+ }
+
+ @Test
+ public void performAccessibilityShortcut_withoutPermission_throwsSecurityException() {
+ try {
+ mAccessibilityManager.performAccessibilityShortcut();
+ fail("No security exception thrown when performing shortcut without permission");
+ } catch (SecurityException e) {
+ // Expected
+ }
+ }
+
+ @Test
+ public void performAccessibilityShortcut_launchAccessibilityService() {
+ configureShortcut(ACCESSIBILITY_SHORTCUT_KEY, mSpeakingA11yServiceName);
+ waitForShortcutStateChange(ACCESSIBILITY_SHORTCUT_KEY,
+ Arrays.asList(mSpeakingA11yServiceName));
+
+ runWithShellPermissionIdentity(sUiAutomation,
+ () -> mAccessibilityManager.performAccessibilityShortcut());
+
+ // Make sure the service starts up
+ final SpeakingAccessibilityService service = mServiceRule.getService();
+ assertTrue("Speaking accessibility service starts up", service != null);
+ }
+
+ @Test
+ public void performAccessibilityShortcut_launchShortcutTargetActivity() {
+ configureShortcut(ACCESSIBILITY_SHORTCUT_KEY, mShortcutTargetActivityName);
+ waitForShortcutStateChange(ACCESSIBILITY_SHORTCUT_KEY,
+ Arrays.asList(mShortcutTargetActivityName));
+
+ runWithShellPermissionIdentity(sUiAutomation,
+ () -> mAccessibilityManager.performAccessibilityShortcut());
+
+ // Make sure the activity starts up
+ mShortcutTargetActivity = mActivityMonitor.waitForActivityWithTimeout(
+ TIMEOUT_SERVICE_ENABLE);
+ assertTrue("Accessibility shortcut target starts up",
+ mShortcutTargetActivity != null);
+ }
+
+ @Test
+ public void performAccessibilityShortcut_withReqA11yButtonService_a11yButtonCallback() {
+ mA11yButtonServiceRule.enableService();
+ configureShortcut(ACCESSIBILITY_SHORTCUT_KEY, mA11yButtonServiceName);
+ waitForShortcutStateChange(ACCESSIBILITY_SHORTCUT_KEY,
+ Arrays.asList(mA11yButtonServiceName));
+
+ performShortcutAndWaitForA11yButtonClicked(mA11yButtonServiceRule.getService());
+ }
+
+ @Test
+ public void getAccessibilityShortcut_withoutPermission_throwsSecurityException() {
+ try {
+ mAccessibilityManager.getAccessibilityShortcutTargets(ACCESSIBILITY_BUTTON);
+ fail("No security exception thrown when get shortcut without permission");
+ } catch (SecurityException e) {
+ // Expected
+ }
+ }
+
+ @Test
+ public void getAccessibilityShortcut_assignedShortcutTarget_returnAssignedTarget() {
+ configureShortcut(ACCESSIBILITY_BUTTON, mSpeakingA11yServiceName);
+ waitForShortcutStateChange(ACCESSIBILITY_BUTTON, Arrays.asList(mSpeakingA11yServiceName));
+ }
+
+ @Test
+ public void getAccessibilityShortcut_multipleTargets_returnMultipleTargets() {
+ configureShortcut(ACCESSIBILITY_BUTTON,
+ mSpeakingA11yServiceName, mShortcutTargetActivityName);
+ waitForShortcutStateChange(ACCESSIBILITY_BUTTON,
+ Arrays.asList(mSpeakingA11yServiceName, mShortcutTargetActivityName));
+ }
+
+ /**
+ * Reads current shortcut states.
+ */
+ private void readShortcutStates() {
+ mA11yShortcutTargets = getComponentIdArray(Settings.Secure.getString(mContentResolver,
+ ACCESSIBILITY_SHORTCUT_TARGET_SERVICE));
+ mA11yButtonTargets = getComponentIdArray(Settings.Secure.getString(mContentResolver,
+ ACCESSIBILITY_BUTTON_TARGET_COMPONENT));
+ runWithShellPermissionIdentity(sUiAutomation, () -> {
+ mA11yShortcutTargetList = mAccessibilityManager
+ .getAccessibilityShortcutTargets(ACCESSIBILITY_SHORTCUT_KEY);
+ mA11yButtonTargetList = mAccessibilityManager
+ .getAccessibilityShortcutTargets(ACCESSIBILITY_BUTTON);
+ });
+ }
+
+ /**
+ * Returns an array of component names by given colon-separated component name string.
+ *
+ * @param componentIds The colon-separated component name string.
+ * @return The array of component names.
+ */
+ @NonNull
+ private String[] getComponentIdArray(String componentIds) {
+ final List<String> nameList = getComponentIdList(componentIds);
+ if (nameList.isEmpty()) {
+ return EMPTY_STRING_ARRAY;
+ }
+ final String[] result = new String[nameList.size()];
+ return nameList.toArray(result);
+ }
+
+ /**
+ * Return a list of component names by given colon-separated component name string.
+ *
+ * @param componentIds The colon-separated component name string.
+ * @return The list.
+ */
+ @NonNull
+ private List<String> getComponentIdList(String componentIds) {
+ final ArrayList<String> componentIdList = new ArrayList<>();
+ if (TextUtils.isEmpty(componentIds)) {
+ return componentIdList;
+ }
+
+ final TextUtils.SimpleStringSplitter splitter =
+ new TextUtils.SimpleStringSplitter(DELIMITER);
+ splitter.setString(componentIds);
+ for (String name : splitter) {
+ if (TextUtils.isEmpty(name)) {
+ continue;
+ }
+ componentIdList.add(name);
+ }
+ return componentIdList;
+ }
+
+ /**
+ * Return a colon-separated component name string by given string array.
+ *
+ * @param componentIds The array of component names.
+ * @return A colon-separated component name string.
+ */
+ @Nullable
+ private String getComponentIdString(String ... componentIds) {
+ final StringBuilder stringBuilder = new StringBuilder();
+ for (String componentId : componentIds) {
+ if (TextUtils.isEmpty(componentId)) {
+ continue;
+ }
+ if (stringBuilder.length() != 0) {
+ stringBuilder.append(DELIMITER);
+ }
+ stringBuilder.append(componentId);
+ }
+
+ if (stringBuilder.length() == 0) {
+ return null;
+ }
+ return stringBuilder.toString();
+ }
+
+ /**
+ * Update the shortcut settings.
+ *
+ * @param shortcutType The shortcut type.
+ * @param newUseShortcutList The component names which use the shortcut.
+ * @return true if the new states updated.
+ */
+ private boolean configureShortcut(int shortcutType, String ... newUseShortcutList) {
+ final String useShortcutList = getComponentIdString(newUseShortcutList);
+ if (shortcutType == ACCESSIBILITY_SHORTCUT_KEY) {
+ return updateAccessibilityShortcut(useShortcutList);
+ } else {
+ return updateAccessibilityButton(useShortcutList);
+ }
+ }
+
+ /**
+ * Update the setting keys of the accessibility shortcut.
+ *
+ * @param newUseShortcutList The value of ACCESSIBILITY_SHORTCUT_TARGET_SERVICE
+ * @return true if the new states updated.
+ */
+ private boolean updateAccessibilityShortcut(String newUseShortcutList) {
+ final ShellCommandBuilder command = ShellCommandBuilder.create(sUiAutomation);
+ final String useShortcutList = Settings.Secure.getString(mContentResolver,
+ ACCESSIBILITY_SHORTCUT_TARGET_SERVICE);
+ boolean changes = false;
+ if (!TextUtils.equals(useShortcutList, newUseShortcutList)) {
+ if (TextUtils.isEmpty(newUseShortcutList)) {
+ command.deleteSecureSetting(ACCESSIBILITY_SHORTCUT_TARGET_SERVICE);
+ } else {
+ command.putSecureSetting(ACCESSIBILITY_SHORTCUT_TARGET_SERVICE, newUseShortcutList);
+ }
+ changes = true;
+ }
+ if (changes) {
+ command.run();
+ }
+ return changes;
+ }
+
+ /**
+ * Update the setting keys of the accessibility button.
+ *
+ * @param newUseShortcutList The value of ACCESSIBILITY_BUTTON_TARGET_COMPONENT
+ * @return true if the new states updated.
+ */
+ private boolean updateAccessibilityButton(String newUseShortcutList) {
+ final ShellCommandBuilder command = ShellCommandBuilder.create(sUiAutomation);
+ final String useShortcutList = Settings.Secure.getString(mContentResolver,
+ ACCESSIBILITY_BUTTON_TARGET_COMPONENT);
+ boolean changes = false;
+ if (!TextUtils.equals(useShortcutList, newUseShortcutList)) {
+ if (TextUtils.isEmpty(newUseShortcutList)) {
+ command.deleteSecureSetting(ACCESSIBILITY_BUTTON_TARGET_COMPONENT);
+ } else {
+ command.putSecureSetting(ACCESSIBILITY_BUTTON_TARGET_COMPONENT, newUseShortcutList);
+ }
+ changes = true;
+ }
+ if (changes) {
+ command.run();
+ }
+ return changes;
+ }
+
+ /**
+ * Waits for the shortcut state changed, and gets current shortcut list is the same with
+ * expected one.
+ *
+ * @param shortcutType The shortcut type.
+ * @param expectedList The expected shortcut targets returned from
+ * {@link AccessibilityManager#getAccessibilityShortcutTargets(int)}.
+ */
+ private void waitForShortcutStateChange(int shortcutType, List<String> expectedList) {
+ final StringBuilder message = new StringBuilder();
+ if (shortcutType == ACCESSIBILITY_SHORTCUT_KEY) {
+ message.append("Accessibility Shortcut, ");
+ } else {
+ message.append("Accessibility Button, ");
+ }
+ message.append("expect:").append(expectedList);
+ runWithShellPermissionIdentity(sUiAutomation, () ->
+ waitForConditionWithServiceStateChange(mTargetContext, () -> {
+ final List<String> currentShortcuts =
+ mAccessibilityManager.getAccessibilityShortcutTargets(shortcutType);
+ if (currentShortcuts.size() != expectedList.size()) {
+ return false;
+ }
+ for (String expect : expectedList) {
+ if (!currentShortcuts.contains(expect)) {
+ return false;
+ }
+ }
+ return true;
+ }, TIMEOUT_SERVICE_ENABLE, message.toString()));
+ }
+
+ /**
+ * Perform shortcut and wait for accessibility button clicked call back.
+ *
+ * @param service The accessibility service
+ */
+ private void performShortcutAndWaitForA11yButtonClicked(AccessibilityService service) {
+ final AtomicBoolean clicked = new AtomicBoolean();
+ final AccessibilityButtonCallback callback = new AccessibilityButtonCallback() {
+ @Override
+ public void onClicked(AccessibilityButtonController controller) {
+ synchronized (clicked) {
+ clicked.set(true);
+ clicked.notifyAll();
+ }
+ }
+
+ @Override
+ public void onAvailabilityChanged(AccessibilityButtonController controller,
+ boolean available) {
+ /* do nothing */
+ }
+ };
+ try {
+ service.getAccessibilityButtonController()
+ .registerAccessibilityButtonCallback(callback);
+ runWithShellPermissionIdentity(sUiAutomation,
+ () -> mAccessibilityManager.performAccessibilityShortcut());
+ TestUtils.waitOn(clicked, () -> clicked.get(), TIMEOUT_SERVICE_ENABLE,
+ "Wait for a11y button clicked");
+ } finally {
+ service.getAccessibilityButtonController()
+ .unregisterAccessibilityButtonCallback(callback);
+ }
+ }
+}