Accessibility shortcut improvement (5/n)
- Adds support for magnification and multiple shortcut targets assigned
to accessibility shortcut key in AccessibilityManagerService.
- New extra field in ACTION_CHOOSE_ACCESSIBILITY_BUTTON intent to
support accessibility shortcut key.
Bug: 136293963
Test: atest AccessibilityShortcutControllerTest
Test: atest AccessibilityUserStateTest
Test: atest AccessibilityShortcutTest
Change-Id: If0a446dfd269e82ec0d09db92e86f859cdae50d8
diff --git a/api/test-current.txt b/api/test-current.txt
index dacc5d6..ac1c03e 100644
--- a/api/test-current.txt
+++ b/api/test-current.txt
@@ -4594,7 +4594,7 @@
public final class AccessibilityManager {
method public void addAccessibilityServicesStateChangeListener(@NonNull android.view.accessibility.AccessibilityManager.AccessibilityServicesStateChangeListener, @Nullable android.os.Handler);
- method @Nullable @RequiresPermission("android.permission.MANAGE_ACCESSIBILITY") public String getAccessibilityShortcutService();
+ method @NonNull @RequiresPermission("android.permission.MANAGE_ACCESSIBILITY") public java.util.List<java.lang.String> getAccessibilityShortcutTargets(int);
method @RequiresPermission("android.permission.MANAGE_ACCESSIBILITY") public void performAccessibilityShortcut();
method public void removeAccessibilityServicesStateChangeListener(@NonNull android.view.accessibility.AccessibilityManager.AccessibilityServicesStateChangeListener);
}
diff --git a/core/java/android/accessibilityservice/AccessibilityShortcutInfo.java b/core/java/android/accessibilityservice/AccessibilityShortcutInfo.java
index f4cadfd..d79740b 100644
--- a/core/java/android/accessibilityservice/AccessibilityShortcutInfo.java
+++ b/core/java/android/accessibilityservice/AccessibilityShortcutInfo.java
@@ -141,6 +141,16 @@
}
/**
+ * The {@link ComponentName} of the accessibility shortcut target.
+ *
+ * @return The component name
+ */
+ @NonNull
+ public ComponentName getComponentName() {
+ return mComponentName;
+ }
+
+ /**
* The localized summary of the accessibility shortcut target.
*
* @return The localized summary if available, and {@code null} if a summary
diff --git a/core/java/android/view/accessibility/AccessibilityManager.java b/core/java/android/view/accessibility/AccessibilityManager.java
index cc28840..843f8e3 100644
--- a/core/java/android/view/accessibility/AccessibilityManager.java
+++ b/core/java/android/view/accessibility/AccessibilityManager.java
@@ -117,7 +117,7 @@
* Activity action: Launch UI to manage which accessibility service or feature is assigned
* to the navigation bar Accessibility button.
* <p>
- * Input: Nothing.
+ * Input: {@link #EXTRA_SHORTCUT_TYPE} is the shortcut type.
* </p>
* <p>
* Output: Nothing.
@@ -130,6 +130,42 @@
"com.android.internal.intent.action.CHOOSE_ACCESSIBILITY_BUTTON";
/**
+ * Used as an int extra field in {@link #ACTION_CHOOSE_ACCESSIBILITY_BUTTON} intent to specify
+ * the shortcut type.
+ *
+ * @hide
+ */
+ public static final String EXTRA_SHORTCUT_TYPE =
+ "com.android.internal.intent.extra.SHORTCUT_TYPE";
+
+ /**
+ * Used as an int value for {@link #EXTRA_SHORTCUT_TYPE} to represent the accessibility button
+ * shortcut type.
+ *
+ * @hide
+ */
+ public static final int ACCESSIBILITY_BUTTON = 0;
+
+ /**
+ * Used as an int value for {@link #EXTRA_SHORTCUT_TYPE} to represent hardware key shortcut,
+ * such as volume key button.
+ *
+ * @hide
+ */
+ public static final int ACCESSIBILITY_SHORTCUT_KEY = 1;
+
+ /**
+ * Annotations for the shortcut type.
+ * @hide
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(value = {
+ ACCESSIBILITY_BUTTON,
+ ACCESSIBILITY_SHORTCUT_KEY
+ })
+ public @interface ShortcutType {}
+
+ /**
* Annotations for content flag of UI.
* @hide
*/
@@ -1242,27 +1278,28 @@
}
/**
- * Get the component name of the service currently assigned to the accessibility shortcut.
+ * Returns the list of shortcut target names currently assigned to the given shortcut.
*
- * @return The flattened component name
+ * @param shortcutType The shortcut type.
+ * @return The list of shortcut target names.
* @hide
*/
@TestApi
@RequiresPermission(Manifest.permission.MANAGE_ACCESSIBILITY)
- @Nullable
- public String getAccessibilityShortcutService() {
+ @NonNull
+ public List<String> getAccessibilityShortcutTargets(@ShortcutType int shortcutType) {
final IAccessibilityManager service;
synchronized (mLock) {
service = getServiceLocked();
}
if (service != null) {
try {
- return service.getAccessibilityShortcutService();
+ return service.getAccessibilityShortcutTargets(shortcutType);
} catch (RemoteException re) {
re.rethrowFromSystemServer();
}
}
- return null;
+ return Collections.emptyList();
}
/**
diff --git a/core/java/android/view/accessibility/IAccessibilityManager.aidl b/core/java/android/view/accessibility/IAccessibilityManager.aidl
index 023fda5..36515b3 100644
--- a/core/java/android/view/accessibility/IAccessibilityManager.aidl
+++ b/core/java/android/view/accessibility/IAccessibilityManager.aidl
@@ -73,7 +73,7 @@
void performAccessibilityShortcut();
// Requires Manifest.permission.MANAGE_ACCESSIBILITY
- String getAccessibilityShortcutService();
+ List<String> getAccessibilityShortcutTargets(int shortcutType);
// System process only
boolean sendFingerprintGesture(int gestureKeyCode);
diff --git a/core/java/com/android/internal/accessibility/AccessibilityShortcutController.java b/core/java/com/android/internal/accessibility/AccessibilityShortcutController.java
index 0b15cd0..3fdedc8 100644
--- a/core/java/com/android/internal/accessibility/AccessibilityShortcutController.java
+++ b/core/java/com/android/internal/accessibility/AccessibilityShortcutController.java
@@ -17,6 +17,7 @@
package com.android.internal.accessibility;
import static android.view.WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG;
+import static android.view.accessibility.AccessibilityManager.ACCESSIBILITY_SHORTCUT_KEY;
import static com.android.internal.util.ArrayUtils.convertToLongArray;
@@ -34,6 +35,7 @@
import android.media.Ringtone;
import android.media.RingtoneManager;
import android.net.Uri;
+import android.os.Build;
import android.os.Handler;
import android.os.UserHandle;
import android.os.Vibrator;
@@ -52,11 +54,12 @@
import com.android.internal.util.function.pooled.PooledLambda;
import java.util.Collections;
+import java.util.List;
import java.util.Locale;
import java.util.Map;
/**
- * Class to help manage the accessibility shortcut
+ * Class to help manage the accessibility shortcut key
*/
public class AccessibilityShortcutController {
private static final String TAG = "AccessibilityShortcutController";
@@ -66,6 +69,8 @@
new ComponentName("com.android.server.accessibility", "ColorInversion");
public static final ComponentName DALTONIZER_COMPONENT_NAME =
new ComponentName("com.android.server.accessibility", "Daltonizer");
+ public static final String MAGNIFICATION_CONTROLLER_NAME =
+ "com.android.server.accessibility.MagnificationController";
private static final AudioAttributes VIBRATION_ATTRIBUTES = new AudioAttributes.Builder()
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
@@ -84,26 +89,6 @@
public FrameworkObjectProvider mFrameworkObjectProvider = new FrameworkObjectProvider();
/**
- * Get the component name string for the service or feature currently assigned to the
- * accessiblity shortcut
- *
- * @param context A valid context
- * @param userId The user ID of interest
- * @return The flattened component name string of the service selected by the user, or the
- * string for the default service if the user has not made a selection
- */
- public static String getTargetServiceComponentNameString(
- Context context, int userId) {
- final String currentShortcutServiceId = Settings.Secure.getStringForUser(
- context.getContentResolver(), Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE,
- userId);
- if (currentShortcutServiceId != null) {
- return currentShortcutServiceId;
- }
- return context.getString(R.string.config_defaultAccessibilityService);
- }
-
- /**
* @return An immutable map from dummy component names to feature info for toggling a framework
* feature
*/
@@ -163,7 +148,7 @@
/**
* Check if the shortcut is available.
*
- * @param onLockScreen Whether or not the phone is currently locked.
+ * @param phoneLocked Whether or not the phone is currently locked.
*
* @return {@code true} if the shortcut is available
*/
@@ -172,8 +157,7 @@
}
public void onSettingsChanged() {
- final boolean haveValidService =
- !TextUtils.isEmpty(getTargetServiceComponentNameString(mContext, mUserId));
+ final boolean hasShortcutTarget = hasShortcutTarget();
final ContentResolver cr = mContext.getContentResolver();
final boolean enabled = Settings.Secure.getIntForUser(
cr, Settings.Secure.ACCESSIBILITY_SHORTCUT_ENABLED, 1, mUserId) == 1;
@@ -183,7 +167,7 @@
mEnabledOnLockScreen = Settings.Secure.getIntForUser(
cr, Settings.Secure.ACCESSIBILITY_SHORTCUT_ON_LOCK_SCREEN,
dialogAlreadyShown, mUserId) == 1;
- mIsShortcutEnabled = enabled && haveValidService;
+ mIsShortcutEnabled = enabled && hasShortcutTarget;
}
/**
@@ -205,7 +189,6 @@
vibrator.vibrate(vibePattern, -1, VIBRATION_ATTRIBUTES);
}
-
if (dialogAlreadyShown == 0) {
// The first time, we show a warning rather than toggle the service to give the user a
// chance to turn off this feature before stuff gets enabled.
@@ -229,32 +212,44 @@
mAlertDialog.dismiss();
mAlertDialog = null;
}
-
- // Show a toast alerting the user to what's happening
- final String serviceName = getShortcutFeatureDescription(false /* no summary */);
- if (serviceName == null) {
- Slog.e(TAG, "Accessibility shortcut set to invalid service");
- return;
- }
- // For accessibility services, show a toast explaining what we're doing.
- final AccessibilityServiceInfo serviceInfo = getInfoForTargetService();
- if (serviceInfo != null) {
- String toastMessageFormatString = mContext.getString(isServiceEnabled(serviceInfo)
- ? R.string.accessibility_shortcut_disabling_service
- : R.string.accessibility_shortcut_enabling_service);
- String toastMessage = String.format(toastMessageFormatString, serviceName);
- Toast warningToast = mFrameworkObjectProvider.makeToastFromText(
- mContext, toastMessage, Toast.LENGTH_LONG);
- warningToast.getWindowParams().privateFlags |=
- WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS;
- warningToast.show();
- }
-
+ showToast();
mFrameworkObjectProvider.getAccessibilityManagerInstance(mContext)
.performAccessibilityShortcut();
}
}
+ /**
+ * Show toast if current assigned shortcut target is an accessibility service and its target
+ * sdk version is less than or equal to Q, or greater than Q and does not request
+ * accessibility button.
+ */
+ private void showToast() {
+ final AccessibilityServiceInfo serviceInfo = getInfoForTargetService();
+ if (serviceInfo == null) {
+ return;
+ }
+ final String serviceName = getShortcutFeatureDescription(/* no summary */ false);
+ if (serviceName == null) {
+ return;
+ }
+ final boolean requestA11yButton = (serviceInfo.flags
+ & AccessibilityServiceInfo.FLAG_REQUEST_ACCESSIBILITY_BUTTON) != 0;
+ if (serviceInfo.getResolveInfo().serviceInfo.applicationInfo
+ .targetSdkVersion > Build.VERSION_CODES.Q && requestA11yButton) {
+ return;
+ }
+ // For accessibility services, show a toast explaining what we're doing.
+ String toastMessageFormatString = mContext.getString(isServiceEnabled(serviceInfo)
+ ? R.string.accessibility_shortcut_disabling_service
+ : R.string.accessibility_shortcut_enabling_service);
+ String toastMessage = String.format(toastMessageFormatString, serviceName);
+ Toast warningToast = mFrameworkObjectProvider.makeToastFromText(
+ mContext, toastMessage, Toast.LENGTH_LONG);
+ warningToast.getWindowParams().privateFlags |=
+ WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS;
+ warningToast.show();
+ }
+
private AlertDialog createShortcutWarningDialog(int userId) {
final String serviceDescription = getShortcutFeatureDescription(true /* Include summary */);
@@ -288,25 +283,21 @@
}
private AccessibilityServiceInfo getInfoForTargetService() {
- final String currentShortcutServiceString = getTargetServiceComponentNameString(
- mContext, UserHandle.USER_CURRENT);
- if (currentShortcutServiceString == null) {
+ final ComponentName targetComponentName = getShortcutTargetComponentName();
+ if (targetComponentName == null) {
return null;
}
AccessibilityManager accessibilityManager =
mFrameworkObjectProvider.getAccessibilityManagerInstance(mContext);
return accessibilityManager.getInstalledServiceInfoWithComponentName(
- ComponentName.unflattenFromString(currentShortcutServiceString));
+ targetComponentName);
}
private String getShortcutFeatureDescription(boolean includeSummary) {
- final String currentShortcutServiceString = getTargetServiceComponentNameString(
- mContext, UserHandle.USER_CURRENT);
- if (currentShortcutServiceString == null) {
+ final ComponentName targetComponentName = getShortcutTargetComponentName();
+ if (targetComponentName == null) {
return null;
}
- final ComponentName targetComponentName =
- ComponentName.unflattenFromString(currentShortcutServiceString);
final ToggleableFrameworkFeatureInfo frameworkFeatureInfo =
getFrameworkShortcutFeaturesMap().get(targetComponentName);
if (frameworkFeatureInfo != null) {
@@ -372,6 +363,36 @@
}
/**
+ * Returns {@code true} if any shortcut targets were assigned to accessibility shortcut key.
+ */
+ private boolean hasShortcutTarget() {
+ // AccessibilityShortcutController is initialized earlier than AccessibilityManagerService.
+ // AccessibilityManager#getAccessibilityShortcutTargets may not return correct shortcut
+ // targets during boot. Needs to read settings directly here.
+ String shortcutTargets = Settings.Secure.getStringForUser(mContext.getContentResolver(),
+ Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE, mUserId);
+ if (TextUtils.isEmpty(shortcutTargets)) {
+ shortcutTargets = mContext.getString(R.string.config_defaultAccessibilityService);
+ }
+ return !TextUtils.isEmpty(shortcutTargets);
+ }
+
+ /**
+ * Gets the component name of the shortcut target.
+ *
+ * @return The component name, or null if it's assigned by multiple targets.
+ */
+ private ComponentName getShortcutTargetComponentName() {
+ final List<String> shortcutTargets = mFrameworkObjectProvider
+ .getAccessibilityManagerInstance(mContext)
+ .getAccessibilityShortcutTargets(ACCESSIBILITY_SHORTCUT_KEY);
+ if (shortcutTargets.size() != 1) {
+ return null;
+ }
+ return ComponentName.unflattenFromString(shortcutTargets.get(0));
+ }
+
+ /**
* Class to wrap TextToSpeech for shortcut dialog spoken feedback.
*/
private class TtsPrompt implements TextToSpeech.OnInitListener {
diff --git a/core/tests/coretests/src/com/android/internal/accessibility/AccessibilityShortcutControllerTest.java b/core/tests/coretests/src/com/android/internal/accessibility/AccessibilityShortcutControllerTest.java
index 7b405434..82854e5 100644
--- a/core/tests/coretests/src/com/android/internal/accessibility/AccessibilityShortcutControllerTest.java
+++ b/core/tests/coretests/src/com/android/internal/accessibility/AccessibilityShortcutControllerTest.java
@@ -20,6 +20,7 @@
import static android.provider.Settings.Secure.ACCESSIBILITY_SHORTCUT_ENABLED;
import static android.provider.Settings.Secure.ACCESSIBILITY_SHORTCUT_ON_LOCK_SCREEN;
import static android.provider.Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE;
+import static android.view.accessibility.AccessibilityManager.ACCESSIBILITY_SHORTCUT_KEY;
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertFalse;
@@ -49,8 +50,10 @@
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
+import android.content.pm.ServiceInfo;
import android.content.res.Resources;
import android.media.Ringtone;
+import android.os.Build;
import android.os.Handler;
import android.os.Message;
import android.os.Vibrator;
@@ -157,9 +160,12 @@
when(mFrameworkObjectProvider.getRingtone(eq(mContext), any())).thenReturn(mRingtone);
when(mResources.getString(anyInt())).thenReturn("Howdy %s");
+ when(mResources.getString(R.string.config_defaultAccessibilityService)).thenReturn(null);
when(mResources.getIntArray(anyInt())).thenReturn(VIBRATOR_PATTERN_INT);
ResolveInfo resolveInfo = mock(ResolveInfo.class);
+ resolveInfo.serviceInfo = mock(ServiceInfo.class);
+ resolveInfo.serviceInfo.applicationInfo = mApplicationInfo;
when(resolveInfo.loadLabel(anyObject())).thenReturn("Service name");
when(mServiceInfo.getResolveInfo()).thenReturn(resolveInfo);
when(mServiceInfo.getComponentName())
@@ -200,42 +206,47 @@
}
@Test
- public void testShortcutAvailable_enabledButNoServiceWhenCreated_shouldReturnFalse() {
+ public void testShortcutAvailable_enabledButNoServiceWhenCreated_shouldReturnFalse()
+ throws Exception {
configureNoShortcutService();
configureShortcutEnabled(ENABLED_EXCEPT_LOCK_SCREEN);
assertFalse(getController().isAccessibilityShortcutAvailable(false));
}
@Test
- public void testShortcutAvailable_enabledWithValidServiceWhenCreated_shouldReturnTrue() {
+ public void testShortcutAvailable_enabledWithValidServiceWhenCreated_shouldReturnTrue()
+ throws Exception {
configureValidShortcutService();
configureShortcutEnabled(ENABLED_EXCEPT_LOCK_SCREEN);
assertTrue(getController().isAccessibilityShortcutAvailable(false));
}
@Test
- public void testShortcutAvailable_disabledWithValidServiceWhenCreated_shouldReturnFalse() {
+ public void testShortcutAvailable_disabledWithValidServiceWhenCreated_shouldReturnFalse()
+ throws Exception {
configureValidShortcutService();
configureShortcutEnabled(DISABLED_BUT_LOCK_SCREEN_ON);
assertFalse(getController().isAccessibilityShortcutAvailable(false));
}
@Test
- public void testShortcutAvailable_onLockScreenButDisabledThere_shouldReturnFalse() {
+ public void testShortcutAvailable_onLockScreenButDisabledThere_shouldReturnFalse()
+ throws Exception {
configureValidShortcutService();
configureShortcutEnabled(ENABLED_EXCEPT_LOCK_SCREEN);
assertFalse(getController().isAccessibilityShortcutAvailable(true));
}
@Test
- public void testShortcutAvailable_onLockScreenAndEnabledThere_shouldReturnTrue() {
+ public void testShortcutAvailable_onLockScreenAndEnabledThere_shouldReturnTrue()
+ throws Exception {
configureValidShortcutService();
configureShortcutEnabled(ENABLED_INCLUDING_LOCK_SCREEN);
assertTrue(getController().isAccessibilityShortcutAvailable(true));
}
@Test
- public void testShortcutAvailable_onLockScreenAndLockScreenPreferenceUnset() {
+ public void testShortcutAvailable_onLockScreenAndLockScreenPreferenceUnset() throws Exception {
// When the user hasn't specified a lock screen preference, we allow from the lock screen
// as long as the user has agreed to enable the shortcut
configureValidShortcutService();
@@ -249,17 +260,19 @@
}
@Test
- public void testShortcutAvailable_whenServiceIdBecomesNull_shouldReturnFalse() {
+ public void testShortcutAvailable_whenServiceIdBecomesNull_shouldReturnFalse()
+ throws Exception {
configureShortcutEnabled(ENABLED_EXCEPT_LOCK_SCREEN);
configureValidShortcutService();
AccessibilityShortcutController accessibilityShortcutController = getController();
- Settings.Secure.putString(mContentResolver, ACCESSIBILITY_SHORTCUT_TARGET_SERVICE, "");
+ configureNoShortcutService();
accessibilityShortcutController.onSettingsChanged();
assertFalse(accessibilityShortcutController.isAccessibilityShortcutAvailable(false));
}
@Test
- public void testShortcutAvailable_whenServiceIdBecomesNonNull_shouldReturnTrue() {
+ public void testShortcutAvailable_whenServiceIdBecomesNonNull_shouldReturnTrue()
+ throws Exception {
configureShortcutEnabled(ENABLED_EXCEPT_LOCK_SCREEN);
configureNoShortcutService();
AccessibilityShortcutController accessibilityShortcutController = getController();
@@ -269,7 +282,8 @@
}
@Test
- public void testShortcutAvailable_whenShortcutBecomesDisabled_shouldReturnFalse() {
+ public void testShortcutAvailable_whenShortcutBecomesDisabled_shouldReturnFalse()
+ throws Exception {
configureShortcutEnabled(ENABLED_EXCEPT_LOCK_SCREEN);
configureValidShortcutService();
AccessibilityShortcutController accessibilityShortcutController = getController();
@@ -279,7 +293,8 @@
}
@Test
- public void testShortcutAvailable_whenShortcutBecomesEnabled_shouldReturnTrue() {
+ public void testShortcutAvailable_whenShortcutBecomesEnabled_shouldReturnTrue()
+ throws Exception {
configureShortcutEnabled(DISABLED);
configureValidShortcutService();
AccessibilityShortcutController accessibilityShortcutController = getController();
@@ -289,7 +304,8 @@
}
@Test
- public void testShortcutAvailable_whenLockscreenBecomesDisabled_shouldReturnFalse() {
+ public void testShortcutAvailable_whenLockscreenBecomesDisabled_shouldReturnFalse()
+ throws Exception {
configureShortcutEnabled(ENABLED_INCLUDING_LOCK_SCREEN);
configureValidShortcutService();
AccessibilityShortcutController accessibilityShortcutController = getController();
@@ -299,7 +315,8 @@
}
@Test
- public void testShortcutAvailable_whenLockscreenBecomesEnabled_shouldReturnTrue() {
+ public void testShortcutAvailable_whenLockscreenBecomesEnabled_shouldReturnTrue()
+ throws Exception {
configureShortcutEnabled(ENABLED_EXCEPT_LOCK_SCREEN);
configureValidShortcutService();
AccessibilityShortcutController accessibilityShortcutController = getController();
@@ -370,7 +387,7 @@
}
@Test
- public void testClickingDisableButtonInDialog_shouldClearShortcutId() {
+ public void testClickingDisableButtonInDialog_shouldClearShortcutId() throws Exception {
configureShortcutEnabled(ENABLED_EXCEPT_LOCK_SCREEN);
configureValidShortcutService();
Settings.Secure.putInt(mContentResolver, ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN, 0);
@@ -458,7 +475,22 @@
}
@Test
- public void testOnAccessibilityShortcut_showsWarningDialog_shouldTtsSpokenPrompt() {
+ public void testOnAccessibilityShortcut_sdkGreaterThanQ_reqA11yButton_callsServiceWithNoToast()
+ throws Exception {
+ configureShortcutEnabled(ENABLED_EXCEPT_LOCK_SCREEN);
+ configureValidShortcutService();
+ configureApplicationTargetSdkVersion(Build.VERSION_CODES.R);
+ configureRequestAccessibilityButton();
+ Settings.Secure.putInt(mContentResolver, ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN, 1);
+ getController().performAccessibilityShortcut();
+
+ verifyZeroInteractions(mToast);
+ verify(mAccessibilityManagerService).performAccessibilityShortcut();
+ }
+
+ @Test
+ public void testOnAccessibilityShortcut_showsWarningDialog_shouldTtsSpokenPrompt()
+ throws Exception {
configureShortcutEnabled(ENABLED_EXCEPT_LOCK_SCREEN);
configureValidShortcutService();
configureTtsSpokenPromptEnabled();
@@ -482,7 +514,8 @@
}
@Test
- public void testOnAccessibilityShortcut_showsWarningDialog_ttsInitFail_noSpokenPrompt() {
+ public void testOnAccessibilityShortcut_showsWarningDialog_ttsInitFail_noSpokenPrompt()
+ throws Exception {
configureShortcutEnabled(ENABLED_EXCEPT_LOCK_SCREEN);
configureValidShortcutService();
configureTtsSpokenPromptEnabled();
@@ -500,19 +533,28 @@
verify(mRingtone).play();
}
- private void configureNoShortcutService() {
+ private void configureNoShortcutService() throws Exception {
+ when(mAccessibilityManagerService
+ .getAccessibilityShortcutTargets(ACCESSIBILITY_SHORTCUT_KEY))
+ .thenReturn(Collections.emptyList());
Settings.Secure.putString(mContentResolver, ACCESSIBILITY_SHORTCUT_TARGET_SERVICE, "");
}
- private void configureValidShortcutService() {
+ private void configureValidShortcutService() throws Exception {
+ when(mAccessibilityManagerService
+ .getAccessibilityShortcutTargets(ACCESSIBILITY_SHORTCUT_KEY))
+ .thenReturn(Collections.singletonList(SERVICE_NAME_STRING));
Settings.Secure.putString(
mContentResolver, ACCESSIBILITY_SHORTCUT_TARGET_SERVICE, SERVICE_NAME_STRING);
}
- private void configureFirstFrameworkFeature() {
+ private void configureFirstFrameworkFeature() throws Exception {
ComponentName featureComponentName =
(ComponentName) AccessibilityShortcutController.getFrameworkShortcutFeaturesMap()
.keySet().toArray()[0];
+ when(mAccessibilityManagerService
+ .getAccessibilityShortcutTargets(ACCESSIBILITY_SHORTCUT_KEY))
+ .thenReturn(Collections.singletonList(featureComponentName.flattenToString()));
Settings.Secure.putString(mContentResolver, ACCESSIBILITY_SHORTCUT_TARGET_SERVICE,
featureComponentName.flattenToString());
}
@@ -552,6 +594,15 @@
.FLAG_REQUEST_SHORTCUT_WARNING_DIALOG_SPOKEN_FEEDBACK;
}
+ private void configureRequestAccessibilityButton() {
+ mServiceInfo.flags |= AccessibilityServiceInfo
+ .FLAG_REQUEST_ACCESSIBILITY_BUTTON;
+ }
+
+ private void configureApplicationTargetSdkVersion(int versionCode) {
+ mApplicationInfo.targetSdkVersion = versionCode;
+ }
+
private void configureHandlerCallbackInvocation() {
doAnswer((InvocationOnMock invocation) -> {
Message m = (Message) invocation.getArguments()[0];
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
index 7fdd83b..6a6e2b2 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
@@ -16,6 +16,11 @@
package com.android.server.accessibility;
+import static android.view.accessibility.AccessibilityManager.ACCESSIBILITY_BUTTON;
+import static android.view.accessibility.AccessibilityManager.ACCESSIBILITY_SHORTCUT_KEY;
+import static android.view.accessibility.AccessibilityManager.ShortcutType;
+
+import static com.android.internal.accessibility.AccessibilityShortcutController.MAGNIFICATION_CONTROLLER_NAME;
import static com.android.internal.util.FunctionalUtils.ignoreRemoteException;
import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
@@ -31,6 +36,7 @@
import android.app.AlertDialog;
import android.app.PendingIntent;
import android.appwidget.AppWidgetManagerInternal;
+import android.content.ActivityNotFoundException;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.ContentResolver;
@@ -70,6 +76,7 @@
import android.provider.SettingsStringUtil.SettingStringHelper;
import android.text.TextUtils;
import android.text.TextUtils.SimpleStringSplitter;
+import android.util.ArraySet;
import android.util.IntArray;
import android.util.Slog;
import android.util.SparseArray;
@@ -113,9 +120,9 @@
import java.util.Iterator;
import java.util.List;
import java.util.Map;
-import java.util.Objects;
import java.util.Set;
import java.util.function.Consumer;
+import java.util.function.Function;
/**
* This class is instantiated by the system as a system level service and can be
@@ -754,6 +761,8 @@
userState.setTouchExplorationEnabledLocked(touchExplorationEnabled);
userState.setDisplayMagnificationEnabledLocked(false);
userState.setNavBarMagnificationEnabledLocked(false);
+ userState.disableShortcutMagnificationLocked();
+
userState.setAutoclickEnabledLocked(false);
userState.mEnabledServices.clear();
userState.mEnabledServices.add(service);
@@ -1072,6 +1081,8 @@
}
}
+ // TODO(a11y shortcut): Remove this function and Use #performAccessibilityShortcutInternal(
+ // ACCESSIBILITY_BUTTON) instead, after the new Settings shortcut Ui merged.
private void notifyAccessibilityButtonClickedLocked(int displayId) {
final AccessibilityUserState state = getCurrentUserStateLocked();
@@ -1105,8 +1116,8 @@
if (state.getServiceAssignedToAccessibilityButtonLocked() == null
&& !state.isNavBarMagnificationAssignedToAccessibilityButtonLocked()) {
mMainHandler.sendMessage(obtainMessage(
- AccessibilityManagerService::showAccessibilityButtonTargetSelection, this,
- displayId));
+ AccessibilityManagerService::showAccessibilityTargetsSelection, this,
+ displayId, ACCESSIBILITY_BUTTON));
} else if (state.isNavBarMagnificationEnabledLocked()
&& state.isNavBarMagnificationAssignedToAccessibilityButtonLocked()) {
mMainHandler.sendMessage(obtainMessage(
@@ -1125,8 +1136,8 @@
}
// The user may have turned off the assigned service or feature
mMainHandler.sendMessage(obtainMessage(
- AccessibilityManagerService::showAccessibilityButtonTargetSelection, this,
- displayId));
+ AccessibilityManagerService::showAccessibilityTargetsSelection, this,
+ displayId, ACCESSIBILITY_BUTTON));
}
}
@@ -1138,13 +1149,27 @@
}
}
- private void showAccessibilityButtonTargetSelection(int displayId) {
+ private void showAccessibilityTargetsSelection(int displayId,
+ @ShortcutType int shortcutType) {
Intent intent = new Intent(AccessibilityManager.ACTION_CHOOSE_ACCESSIBILITY_BUTTON);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
final Bundle bundle = ActivityOptions.makeBasic().setLaunchDisplayId(displayId).toBundle();
+ bundle.putInt(AccessibilityManager.EXTRA_SHORTCUT_TYPE, shortcutType);
mContext.startActivityAsUser(intent, bundle, UserHandle.of(mCurrentUserId));
}
+ private void launchShortcutTargetActivity(int displayId, ComponentName name) {
+ final Intent intent = new Intent();
+ final Bundle bundle = ActivityOptions.makeBasic().setLaunchDisplayId(displayId).toBundle();
+ intent.setComponent(name);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+ try {
+ mContext.startActivityAsUser(intent, bundle, UserHandle.of(mCurrentUserId));
+ } catch (ActivityNotFoundException ignore) {
+ // ignore the exception
+ }
+ }
+
private void notifyAccessibilityButtonVisibilityChangedLocked(boolean available) {
final AccessibilityUserState state = getCurrentUserStateLocked();
mIsAccessibilityButtonShown = available;
@@ -1353,9 +1378,8 @@
*/
private void readComponentNamesFromSettingLocked(String settingName, int userId,
Set<ComponentName> outComponentNames) {
- String settingValue = Settings.Secure.getStringForUser(mContext.getContentResolver(),
- settingName, userId);
- readComponentNamesFromStringLocked(settingValue, outComponentNames, false);
+ readColonDelimitedSettingToSet(settingName, userId, outComponentNames,
+ str -> ComponentName.unflattenFromString(str));
}
/**
@@ -1370,34 +1394,57 @@
private void readComponentNamesFromStringLocked(String names,
Set<ComponentName> outComponentNames,
boolean doMerge) {
- if (!doMerge) {
- outComponentNames.clear();
- }
- if (names != null) {
- TextUtils.SimpleStringSplitter splitter = mStringColonSplitter;
- splitter.setString(names);
- while (splitter.hasNext()) {
- String str = splitter.next();
- if (str == null || str.length() <= 0) {
- continue;
- }
- ComponentName enabledService = ComponentName.unflattenFromString(str);
- if (enabledService != null) {
- outComponentNames.add(enabledService);
- }
- }
- }
+ readColonDelimitedStringToSet(names, outComponentNames, doMerge,
+ str -> ComponentName.unflattenFromString(str));
}
@Override
public void persistComponentNamesToSettingLocked(String settingName,
Set<ComponentName> componentNames, int userId) {
- StringBuilder builder = new StringBuilder();
- for (ComponentName componentName : componentNames) {
+ persistColonDelimitedSetToSettingLocked(settingName, userId, componentNames,
+ componentName -> componentName.flattenToShortString());
+ }
+
+ private <T> void readColonDelimitedSettingToSet(String settingName, int userId, Set<T> outSet,
+ Function<String, T> toItem) {
+ final String settingValue = Settings.Secure.getStringForUser(mContext.getContentResolver(),
+ settingName, userId);
+ readColonDelimitedStringToSet(settingValue, outSet, false, toItem);
+ }
+
+ private <T> void readColonDelimitedStringToSet(String names, Set<T> outSet, boolean doMerge,
+ Function<String, T> toItem) {
+ if (!doMerge) {
+ outSet.clear();
+ }
+ if (!TextUtils.isEmpty(names)) {
+ final TextUtils.SimpleStringSplitter splitter = mStringColonSplitter;
+ splitter.setString(names);
+ while (splitter.hasNext()) {
+ final String str = splitter.next();
+ if (TextUtils.isEmpty(str)) {
+ continue;
+ }
+ final T item = toItem.apply(str);
+ if (item != null) {
+ outSet.add(item);
+ }
+ }
+ }
+ }
+
+ private <T> void persistColonDelimitedSetToSettingLocked(String settingName, int userId,
+ Set<T> set, Function<T, String> toString) {
+ final StringBuilder builder = new StringBuilder();
+ for (T item : set) {
+ final String str = (item != null ? toString.apply(item) : null);
+ if (TextUtils.isEmpty(str)) {
+ continue;
+ }
if (builder.length() > 0) {
builder.append(COMPONENT_NAME_SEPARATOR);
}
- builder.append(componentName.flattenToShortString());
+ builder.append(str);
}
final long identity = Binder.clearCallingIdentity();
try {
@@ -1537,7 +1584,8 @@
if (userState.isDisplayMagnificationEnabledLocked()) {
flags |= AccessibilityInputFilter.FLAG_FEATURE_SCREEN_MAGNIFIER;
}
- if (userState.isNavBarMagnificationEnabledLocked()) {
+ if (userState.isNavBarMagnificationEnabledLocked()
+ || userState.isShortcutKeyMagnificationEnabledLocked()) {
flags |= AccessibilityInputFilter.FLAG_FEATURE_TRIGGERED_SCREEN_MAGNIFIER;
}
if (userHasMagnificationServicesLocked(userState)) {
@@ -1647,7 +1695,6 @@
mInitialized = true;
updateLegacyCapabilitiesLocked(userState);
updateServicesLocked(userState);
- updateAccessibilityShortcutLocked(userState);
updateWindowsForAccessibilityCallbackLocked(userState);
updateFilterKeyEventsLocked(userState);
updateTouchExplorationLocked(userState);
@@ -1657,6 +1704,7 @@
scheduleUpdateInputFilter(userState);
updateRelevantEventsLocked(userState);
scheduleUpdateClientsIfNeededLocked(userState);
+ updateAccessibilityShortcutKeyTargetsLocked(userState);
updateAccessibilityButtonTargetsLocked(userState);
}
@@ -1751,7 +1799,7 @@
somethingChanged |= readHighTextContrastEnabledSettingLocked(userState);
somethingChanged |= readMagnificationEnabledSettingsLocked(userState);
somethingChanged |= readAutoclickEnabledSettingLocked(userState);
- somethingChanged |= readAccessibilityShortcutSettingLocked(userState);
+ somethingChanged |= readAccessibilityShortcutKeySettingLocked(userState);
somethingChanged |= readAccessibilityButtonSettingsLocked(userState);
somethingChanged |= readUserRecommendedUiTimeoutSettingsLocked(userState);
return somethingChanged;
@@ -1847,57 +1895,43 @@
}
}
- private boolean readAccessibilityShortcutSettingLocked(AccessibilityUserState userState) {
- String componentNameToEnableString = AccessibilityShortcutController
- .getTargetServiceComponentNameString(mContext, userState.mUserId);
- if ((componentNameToEnableString == null) || componentNameToEnableString.isEmpty()) {
- if (userState.getServiceToEnableWithShortcutLocked() == null) {
- return false;
+ private boolean readAccessibilityShortcutKeySettingLocked(AccessibilityUserState userState) {
+ final Set<String> targetsFromSetting = new ArraySet<>();
+ readColonDelimitedSettingToSet(Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE,
+ userState.mUserId, targetsFromSetting, str -> str);
+ if (targetsFromSetting.isEmpty()) {
+ // Fall back to device's default a11y service.
+ final String defaultService = mContext.getString(
+ R.string.config_defaultAccessibilityService);
+ if (!TextUtils.isEmpty(defaultService)) {
+ targetsFromSetting.add(defaultService);
}
- userState.setServiceToEnableWithShortcutLocked(null);
- return true;
- }
- ComponentName componentNameToEnable =
- ComponentName.unflattenFromString(componentNameToEnableString);
- if ((componentNameToEnable != null)
- && componentNameToEnable.equals(userState.getServiceToEnableWithShortcutLocked())) {
- return false;
}
- userState.setServiceToEnableWithShortcutLocked(componentNameToEnable);
+ final Set<String> currentTargets =
+ userState.getShortcutTargetsLocked(ACCESSIBILITY_SHORTCUT_KEY);
+ if (targetsFromSetting.equals(currentTargets)) {
+ return false;
+ }
+ currentTargets.clear();
+ currentTargets.addAll(targetsFromSetting);
scheduleNotifyClientsOfServicesStateChangeLocked(userState);
return true;
}
private boolean readAccessibilityButtonSettingsLocked(AccessibilityUserState userState) {
- String componentId = Settings.Secure.getStringForUser(mContext.getContentResolver(),
- Settings.Secure.ACCESSIBILITY_BUTTON_TARGET_COMPONENT, userState.mUserId);
- if (TextUtils.isEmpty(componentId)) {
- if ((userState.getServiceAssignedToAccessibilityButtonLocked() == null)
- && !userState.isNavBarMagnificationAssignedToAccessibilityButtonLocked()) {
- return false;
- }
- userState.setServiceAssignedToAccessibilityButtonLocked(null);
- userState.setNavBarMagnificationAssignedToAccessibilityButtonLocked(false);
- return true;
- }
+ final Set<String> targetsFromSetting = new ArraySet<>();
+ readColonDelimitedSettingToSet(Settings.Secure.ACCESSIBILITY_BUTTON_TARGET_COMPONENT,
+ userState.mUserId, targetsFromSetting, str -> str);
- if (componentId.equals(MagnificationController.class.getName())) {
- if (userState.isNavBarMagnificationAssignedToAccessibilityButtonLocked()) {
- return false;
- }
- userState.setServiceAssignedToAccessibilityButtonLocked(null);
- userState.setNavBarMagnificationAssignedToAccessibilityButtonLocked(true);
- return true;
- }
-
- ComponentName componentName = ComponentName.unflattenFromString(componentId);
- if (Objects.equals(componentName,
- userState.getServiceAssignedToAccessibilityButtonLocked())) {
+ final Set<String> currentTargets =
+ userState.getShortcutTargetsLocked(ACCESSIBILITY_BUTTON);
+ if (targetsFromSetting.equals(currentTargets)) {
return false;
}
- userState.setServiceAssignedToAccessibilityButtonLocked(componentName);
- userState.setNavBarMagnificationAssignedToAccessibilityButtonLocked(false);
+ currentTargets.clear();
+ currentTargets.addAll(targetsFromSetting);
+ scheduleNotifyClientsOfServicesStateChangeLocked(userState);
return true;
}
@@ -1921,34 +1955,33 @@
}
/**
- * Check if the service that will be enabled by the shortcut is installed. If it isn't,
- * clear the value and the associated setting so a sideloaded service can't spoof the
- * package name of the default service.
- *
- * @param userState
+ * Check if the targets that will be enabled by the accessibility shortcut key is installed.
+ * If it isn't, remove it from the list and associated setting so a side loaded service can't
+ * spoof the package name of the default service.
*/
- private void updateAccessibilityShortcutLocked(AccessibilityUserState userState) {
- if (userState.getServiceToEnableWithShortcutLocked() == null) {
+ private void updateAccessibilityShortcutKeyTargetsLocked(AccessibilityUserState userState) {
+ final Set<String> currentTargets =
+ userState.getShortcutTargetsLocked(ACCESSIBILITY_SHORTCUT_KEY);
+ final int lastSize = currentTargets.size();
+ if (lastSize == 0) {
return;
}
- boolean shortcutServiceIsInstalled =
- AccessibilityShortcutController.getFrameworkShortcutFeaturesMap()
- .containsKey(userState.getServiceToEnableWithShortcutLocked());
- for (int i = 0; !shortcutServiceIsInstalled && (i < userState.mInstalledServices.size());
- i++) {
- if (userState.mInstalledServices.get(i).getComponentName()
- .equals(userState.getServiceToEnableWithShortcutLocked())) {
- shortcutServiceIsInstalled = true;
- }
+ currentTargets.removeIf(
+ name -> !userState.isShortcutTargetInstalledLocked(name));
+ if (lastSize == currentTargets.size()) {
+ return;
}
- if (!shortcutServiceIsInstalled) {
- userState.setServiceToEnableWithShortcutLocked(null);
+
+ // Update setting key with new value.
+ persistColonDelimitedSetToSettingLocked(
+ Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE,
+ userState.mUserId, currentTargets, str -> str);
+ scheduleNotifyClientsOfServicesStateChangeLocked(userState);
+
+ // Disable accessibility shortcut key if there's no shortcut installed.
+ if (currentTargets.isEmpty()) {
final long identity = Binder.clearCallingIdentity();
try {
- Settings.Secure.putStringForUser(mContext.getContentResolver(),
- Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE, null,
- userState.mUserId);
-
Settings.Secure.putIntForUser(mContext.getContentResolver(),
Settings.Secure.ACCESSIBILITY_SHORTCUT_ENABLED, 0, userState.mUserId);
} finally {
@@ -2004,7 +2037,8 @@
// displays in one display. It's not a real display and there's no input events for it.
final ArrayList<Display> displays = getValidDisplayList();
if (userState.isDisplayMagnificationEnabledLocked()
- || userState.isNavBarMagnificationEnabledLocked()) {
+ || userState.isNavBarMagnificationEnabledLocked()
+ || userState.isShortcutKeyMagnificationEnabledLocked()) {
for (int i = 0; i < displays.size(); i++) {
final Display display = displays.get(i);
getMagnificationController().register(display.getDisplayId());
@@ -2088,7 +2122,14 @@
}
}
+ /**
+ * 1) Update accessibility button availability to accessibility services.
+ * 2) Check if the targets that will be enabled by the accessibility button is installed.
+ * If it isn't, remove it from the list and associated setting so a side loaded service can't
+ * spoof the package name of the default service.
+ */
private void updateAccessibilityButtonTargetsLocked(AccessibilityUserState userState) {
+ // Update accessibility button availability.
for (int i = userState.mBoundServices.size() - 1; i >= 0; i--) {
final AccessibilityServiceConnection service = userState.mBoundServices.get(i);
if (service.mRequestAccessibilityButton) {
@@ -2096,6 +2137,24 @@
service.isAccessibilityButtonAvailableLocked(userState));
}
}
+
+ final Set<String> currentTargets =
+ userState.getShortcutTargetsLocked(ACCESSIBILITY_BUTTON);
+ final int lastSize = currentTargets.size();
+ if (lastSize == 0) {
+ return;
+ }
+ currentTargets.removeIf(
+ name -> !userState.isShortcutTargetInstalledLocked(name));
+ if (lastSize == currentTargets.size()) {
+ return;
+ }
+
+ // Update setting key with new value.
+ persistColonDelimitedSetToSettingLocked(
+ Settings.Secure.ACCESSIBILITY_BUTTON_TARGET_COMPONENT,
+ userState.mUserId, currentTargets, str -> str);
+ scheduleNotifyClientsOfServicesStateChangeLocked(userState);
}
private void updateRecommendedUiTimeoutLocked(AccessibilityUserState userState) {
@@ -2156,7 +2215,7 @@
}
/**
- * AIDL-exposed method to be called when the accessibility shortcut is enabled. Requires
+ * AIDL-exposed method to be called when the accessibility shortcut key is enabled. Requires
* permission to write secure settings, since someone with that permission can enable
* accessibility services themselves.
*/
@@ -2168,52 +2227,177 @@
throw new SecurityException(
"performAccessibilityShortcut requires the MANAGE_ACCESSIBILITY permission");
}
+ mMainHandler.sendMessage(obtainMessage(
+ AccessibilityManagerService::performAccessibilityShortcutInternal, this,
+ Display.DEFAULT_DISPLAY, ACCESSIBILITY_SHORTCUT_KEY));
+ }
+
+ /**
+ * Perform the accessibility shortcut action.
+ *
+ * @param shortcutType The shortcut type.
+ * @param displayId The display id of the accessibility button.
+ */
+ private void performAccessibilityShortcutInternal(int displayId,
+ @ShortcutType int shortcutType) {
+ final List<String> shortcutTargets = getAccessibilityShortcutTargetsInternal(shortcutType);
+ if (shortcutTargets.isEmpty()) {
+ Slog.d(LOG_TAG, "No target to perform shortcut, shortcutType=" + shortcutType);
+ return;
+ }
+ // In case there are many targets assigned to the given shortcut.
+ if (shortcutTargets.size() > 1) {
+ showAccessibilityTargetsSelection(displayId, shortcutType);
+ return;
+ }
+ final String targetName = shortcutTargets.get(0);
+ // In case user assigned magnification to the given shortcut.
+ if (targetName.equals(MAGNIFICATION_CONTROLLER_NAME)) {
+ sendAccessibilityButtonToInputFilter(displayId);
+ return;
+ }
+ final ComponentName targetComponentName = ComponentName.unflattenFromString(targetName);
+ if (targetComponentName == null) {
+ Slog.d(LOG_TAG, "Perform shortcut failed, invalid target name:" + targetName);
+ return;
+ }
+ // In case user assigned an accessibility framework feature to the given shortcut.
+ if (performAccessibilityFrameworkFeature(targetComponentName)) {
+ return;
+ }
+ // In case user assigned an accessibility shortcut target to the given shortcut.
+ if (performAccessibilityShortcutTargetActivity(displayId, targetComponentName)) {
+ return;
+ }
+ // in case user assigned an accessibility service to the given shortcut.
+ if (performAccessibilityShortcutTargetService(
+ displayId, shortcutType, targetComponentName)) {
+ return;
+ }
+ }
+
+ private boolean performAccessibilityFrameworkFeature(ComponentName assignedTarget) {
final Map<ComponentName, ToggleableFrameworkFeatureInfo> frameworkFeatureMap =
AccessibilityShortcutController.getFrameworkShortcutFeaturesMap();
- synchronized(mLock) {
- final AccessibilityUserState userState = getUserStateLocked(mCurrentUserId);
- final ComponentName serviceName = userState.getServiceToEnableWithShortcutLocked();
- if (serviceName == null) {
- return;
- }
- if (frameworkFeatureMap.containsKey(serviceName)) {
- // Toggle the requested framework feature
- ToggleableFrameworkFeatureInfo featureInfo = frameworkFeatureMap.get(serviceName);
- SettingStringHelper setting = new SettingStringHelper(mContext.getContentResolver(),
- featureInfo.getSettingKey(), mCurrentUserId);
- // Assuming that the default state will be to have the feature off
- if (!TextUtils.equals(featureInfo.getSettingOnValue(), setting.read())) {
- setting.write(featureInfo.getSettingOnValue());
- } else {
- setting.write(featureInfo.getSettingOffValue());
+ if (!frameworkFeatureMap.containsKey(assignedTarget)) {
+ return false;
+ }
+ // Toggle the requested framework feature
+ final ToggleableFrameworkFeatureInfo featureInfo = frameworkFeatureMap.get(assignedTarget);
+ final SettingStringHelper setting = new SettingStringHelper(mContext.getContentResolver(),
+ featureInfo.getSettingKey(), mCurrentUserId);
+ // Assuming that the default state will be to have the feature off
+ if (!TextUtils.equals(featureInfo.getSettingOnValue(), setting.read())) {
+ setting.write(featureInfo.getSettingOnValue());
+ } else {
+ setting.write(featureInfo.getSettingOffValue());
+ }
+ return true;
+ }
+
+ private boolean performAccessibilityShortcutTargetActivity(int displayId,
+ ComponentName assignedTarget) {
+ synchronized (mLock) {
+ final AccessibilityUserState userState = getCurrentUserStateLocked();
+ for (int i = 0; i < userState.mInstalledShortcuts.size(); i++) {
+ final AccessibilityShortcutInfo shortcutInfo = userState.mInstalledShortcuts.get(i);
+ if (!shortcutInfo.getComponentName().equals(assignedTarget)) {
+ continue;
}
- }
- final long identity = Binder.clearCallingIdentity();
- try {
- if (userState.mComponentNameToServiceMap.get(serviceName) == null) {
- enableAccessibilityServiceLocked(serviceName, mCurrentUserId);
- } else {
- disableAccessibilityServiceLocked(serviceName, mCurrentUserId);
- }
- } finally {
- Binder.restoreCallingIdentity(identity);
+ launchShortcutTargetActivity(displayId, assignedTarget);
+ return true;
}
}
- };
+ return false;
+ }
+
+ /**
+ * Perform accessibility service shortcut action.
+ *
+ * 1) For {@link AccessibilityManager#ACCESSIBILITY_BUTTON} type and services targeting sdk
+ * version <= Q: callbacks to accessibility service if service is bounded and requests
+ * accessibility button.
+ * 2) For {@link AccessibilityManager#ACCESSIBILITY_SHORTCUT_KEY} type and service targeting sdk
+ * version <= Q: turns on / off the accessibility service.
+ * 3) For services targeting sdk version > Q:
+ * a) Turns on / off the accessibility service, if service does not request accessibility
+ * button.
+ * b) Callbacks to accessibility service if service is bounded and requests accessibility
+ * button.
+ */
+ private boolean performAccessibilityShortcutTargetService(int displayId,
+ @ShortcutType int shortcutType, ComponentName assignedTarget) {
+ synchronized (mLock) {
+ final AccessibilityUserState userState = getCurrentUserStateLocked();
+ final AccessibilityServiceInfo installedServiceInfo =
+ userState.getInstalledServiceInfoLocked(assignedTarget);
+ if (installedServiceInfo == null) {
+ Slog.d(LOG_TAG, "Perform shortcut failed, invalid component name:"
+ + assignedTarget);
+ return false;
+ }
+
+ final AccessibilityServiceConnection serviceConnection =
+ userState.getServiceConnectionLocked(assignedTarget);
+ final int targetSdk = installedServiceInfo.getResolveInfo()
+ .serviceInfo.applicationInfo.targetSdkVersion;
+ final boolean requestA11yButton = (installedServiceInfo.flags
+ & AccessibilityServiceInfo.FLAG_REQUEST_ACCESSIBILITY_BUTTON) != 0;
+ // Turns on / off the accessibility service
+ if ((targetSdk <= Build.VERSION_CODES.Q && shortcutType == ACCESSIBILITY_SHORTCUT_KEY)
+ || (targetSdk > Build.VERSION_CODES.Q && !requestA11yButton)) {
+ if (serviceConnection == null) {
+ enableAccessibilityServiceLocked(assignedTarget, mCurrentUserId);
+ } else {
+ disableAccessibilityServiceLocked(assignedTarget, mCurrentUserId);
+ }
+ return true;
+ }
+ // Callbacks to a11y service if it's bounded and requests a11y button.
+ if (serviceConnection == null
+ || !userState.mBoundServices.contains(serviceConnection)
+ || !serviceConnection.mRequestAccessibilityButton) {
+ Slog.d(LOG_TAG, "Perform shortcut failed, service is not ready:"
+ + assignedTarget);
+ return false;
+ }
+ serviceConnection.notifyAccessibilityButtonClickedLocked(displayId);
+ return true;
+ }
+ }
@Override
- public String getAccessibilityShortcutService() {
- if (mContext.checkCallingPermission(Manifest.permission.MANAGE_ACCESSIBILITY)
+ public List<String> getAccessibilityShortcutTargets(@ShortcutType int shortcutType) {
+ if (mContext.checkCallingOrSelfPermission(Manifest.permission.MANAGE_ACCESSIBILITY)
!= PackageManager.PERMISSION_GRANTED) {
throw new SecurityException(
"getAccessibilityShortcutService requires the MANAGE_ACCESSIBILITY permission");
}
- synchronized(mLock) {
- final AccessibilityUserState userState = getUserStateLocked(mCurrentUserId);
- if (userState.getServiceToEnableWithShortcutLocked() == null) {
- return null;
+ return getAccessibilityShortcutTargetsInternal(shortcutType);
+ }
+
+ private List<String> getAccessibilityShortcutTargetsInternal(@ShortcutType int shortcutType) {
+ synchronized (mLock) {
+ final AccessibilityUserState userState = getCurrentUserStateLocked();
+ final ArrayList<String> shortcutTargets = new ArrayList<>(
+ userState.getShortcutTargetsLocked(shortcutType));
+ if (shortcutType != ACCESSIBILITY_BUTTON) {
+ return shortcutTargets;
}
- return userState.getServiceToEnableWithShortcutLocked().flattenToString();
+ // Adds legacy a11y services requesting a11y button into the list.
+ for (int i = userState.mBoundServices.size() - 1; i >= 0; i--) {
+ final AccessibilityServiceConnection service = userState.mBoundServices.get(i);
+ if (!service.mRequestAccessibilityButton
+ || service.getServiceInfo().getResolveInfo().serviceInfo.applicationInfo
+ .targetSdkVersion > Build.VERSION_CODES.Q) {
+ continue;
+ }
+ final String serviceName = service.getComponentName().flattenToString();
+ if (!TextUtils.isEmpty(serviceName)) {
+ shortcutTargets.add(serviceName);
+ }
+ }
+ return shortcutTargets;
}
}
@@ -2609,6 +2793,8 @@
private final Uri mDisplayMagnificationEnabledUri = Settings.Secure.getUriFor(
Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_ENABLED);
+ // TODO(a11y shortcut): Remove this setting key, and have a migrate function in
+ // Setting provider after new shortcut UI merged.
private final Uri mNavBarMagnificationEnabledUri = Settings.Secure.getUriFor(
Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_NAVBAR_ENABLED);
@@ -2713,7 +2899,7 @@
|| mShowImeWithHardKeyboardUri.equals(uri)) {
userState.reconcileSoftKeyboardModeWithSettingsLocked();
} else if (mAccessibilityShortcutServiceIdUri.equals(uri)) {
- if (readAccessibilityShortcutSettingLocked(userState)) {
+ if (readAccessibilityShortcutKeySettingLocked(userState)) {
onUserStateChangedLocked(userState);
}
} else if (mAccessibilityButtonComponentIdUri.equals(uri)) {
@@ -2724,8 +2910,6 @@
|| mUserInteractiveUiTimeoutUri.equals(uri)) {
readUserRecommendedUiTimeoutSettingsLocked(userState);
}
- // TODO(a11y shortcut): Monitor new setting keys, when user adds shortcut, and
- // remove from the list of enabled targets anything that's been uninstalled.
}
}
}
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityServiceConnection.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityServiceConnection.java
index 6cadb6d..cbff6bd 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityServiceConnection.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityServiceConnection.java
@@ -294,6 +294,7 @@
}
}
+ // TODO(a11y shortcut): Refactoring the logic here, after the new Settings shortcut Ui merged.
public boolean isAccessibilityButtonAvailableLocked(AccessibilityUserState userState) {
// If the service does not request the accessibility button, it isn't available
if (!mRequestAccessibilityButton) {
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java
index a0b9866..a163f74 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java
@@ -22,6 +22,11 @@
import static android.accessibilityservice.AccessibilityService.SHOW_MODE_HIDDEN;
import static android.accessibilityservice.AccessibilityService.SHOW_MODE_IGNORE_HARD_KEYBOARD;
import static android.accessibilityservice.AccessibilityService.SHOW_MODE_MASK;
+import static android.view.accessibility.AccessibilityManager.ACCESSIBILITY_BUTTON;
+import static android.view.accessibility.AccessibilityManager.ACCESSIBILITY_SHORTCUT_KEY;
+import static android.view.accessibility.AccessibilityManager.ShortcutType;
+
+import static com.android.internal.accessibility.AccessibilityShortcutController.MAGNIFICATION_CONTROLLER_NAME;
import android.accessibilityservice.AccessibilityService.SoftKeyboardShowMode;
import android.accessibilityservice.AccessibilityServiceInfo;
@@ -33,10 +38,14 @@
import android.os.Binder;
import android.os.RemoteCallbackList;
import android.provider.Settings;
+import android.text.TextUtils;
+import android.util.ArraySet;
import android.util.Slog;
import android.view.accessibility.AccessibilityManager;
import android.view.accessibility.IAccessibilityManagerClient;
+import com.android.internal.accessibility.AccessibilityShortcutController;
+
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.ArrayList;
@@ -79,19 +88,18 @@
final Set<ComponentName> mTouchExplorationGrantedServices = new HashSet<>();
+ final ArraySet<String> mAccessibilityShortcutKeyTargets = new ArraySet<>();
+
+ final ArraySet<String> mAccessibilityButtonTargets = new ArraySet<>();
+
private final ServiceInfoChangeListener mServiceInfoChangeListener;
- private ComponentName mServiceAssignedToAccessibilityButton;
-
private ComponentName mServiceChangingSoftKeyboardMode;
- private ComponentName mServiceToEnableWithShortcut;
-
private boolean mBindInstantServiceAllowed;
private boolean mIsAutoclickEnabled;
private boolean mIsDisplayMagnificationEnabled;
private boolean mIsFilterKeyEventsEnabled;
- private boolean mIsNavBarMagnificationAssignedToAccessibilityButton;
private boolean mIsNavBarMagnificationEnabled;
private boolean mIsPerformGesturesEnabled;
private boolean mIsTextHighContrastEnabled;
@@ -141,11 +149,11 @@
// Clear state persisted in settings.
mEnabledServices.clear();
mTouchExplorationGrantedServices.clear();
+ mAccessibilityShortcutKeyTargets.clear();
+ mAccessibilityButtonTargets.clear();
mIsTouchExplorationEnabled = false;
mIsDisplayMagnificationEnabled = false;
mIsNavBarMagnificationEnabled = false;
- mServiceAssignedToAccessibilityButton = null;
- mIsNavBarMagnificationAssignedToAccessibilityButton = false;
mIsAutoclickEnabled = false;
mUserNonInteractiveUiTimeout = 0;
mUserInteractiveUiTimeout = 0;
@@ -435,6 +443,26 @@
pw.append(", installedServiceCount=").append(String.valueOf(mInstalledServices.size()));
pw.append("}");
pw.println();
+ pw.append(" shortcut key:{");
+ int size = mAccessibilityShortcutKeyTargets.size();
+ for (int i = 0; i < size; i++) {
+ final String componentId = mAccessibilityShortcutKeyTargets.valueAt(i);
+ pw.append(componentId);
+ if (i + 1 < size) {
+ pw.append(", ");
+ }
+ }
+ pw.println("}");
+ pw.append(" button:{");
+ size = mAccessibilityButtonTargets.size();
+ for (int i = 0; i < size; i++) {
+ final String componentId = mAccessibilityButtonTargets.valueAt(i);
+ pw.append(componentId);
+ if (i + 1 < size) {
+ pw.append(", ");
+ }
+ }
+ pw.println("}");
pw.append(" Bound services:{");
final int serviceCount = mBoundServices.size();
for (int j = 0; j < serviceCount; j++) {
@@ -525,20 +553,85 @@
mLastSentClientState = state;
}
- public boolean isNavBarMagnificationAssignedToAccessibilityButtonLocked() {
- return mIsNavBarMagnificationAssignedToAccessibilityButton;
+ public boolean isShortcutKeyMagnificationEnabledLocked() {
+ return mAccessibilityShortcutKeyTargets.contains(MAGNIFICATION_CONTROLLER_NAME);
}
- public void setNavBarMagnificationAssignedToAccessibilityButtonLocked(boolean assigned) {
- mIsNavBarMagnificationAssignedToAccessibilityButton = assigned;
+ /**
+ * Disable both shortcuts' magnification function.
+ */
+ public void disableShortcutMagnificationLocked() {
+ mAccessibilityShortcutKeyTargets.remove(MAGNIFICATION_CONTROLLER_NAME);
+ mAccessibilityButtonTargets.remove(MAGNIFICATION_CONTROLLER_NAME);
}
- public boolean isNavBarMagnificationEnabledLocked() {
- return mIsNavBarMagnificationEnabled;
+ /**
+ * Returns a set which contains the flattened component names and the system class names
+ * assigned to the given shortcut.
+ *
+ * @param shortcutType The shortcut type.
+ * @return The array set of the strings
+ */
+ public ArraySet<String> getShortcutTargetsLocked(@ShortcutType int shortcutType) {
+ if (shortcutType == ACCESSIBILITY_SHORTCUT_KEY) {
+ return mAccessibilityShortcutKeyTargets;
+ } else if (shortcutType == ACCESSIBILITY_BUTTON) {
+ return mAccessibilityButtonTargets;
+ }
+ return null;
}
- public void setNavBarMagnificationEnabledLocked(boolean enabled) {
- mIsNavBarMagnificationEnabled = enabled;
+ /**
+ * Whether or not the given shortcut target is installed in device.
+ *
+ * @param name The shortcut target name
+ * @return true if the shortcut target is installed.
+ */
+ public boolean isShortcutTargetInstalledLocked(String name) {
+ if (TextUtils.isEmpty(name)) {
+ return false;
+ }
+ if (MAGNIFICATION_CONTROLLER_NAME.equals(name)) {
+ return true;
+ }
+
+ final ComponentName componentName = ComponentName.unflattenFromString(name);
+ if (componentName == null) {
+ return false;
+ }
+ if (AccessibilityShortcutController.getFrameworkShortcutFeaturesMap()
+ .containsKey(componentName)) {
+ return true;
+ }
+ if (getInstalledServiceInfoLocked(componentName) != null) {
+ return true;
+ }
+ for (int i = 0; i < mInstalledShortcuts.size(); i++) {
+ if (mInstalledShortcuts.get(i).getComponentName().equals(componentName)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns installed accessibility service info by the given service component name.
+ */
+ public AccessibilityServiceInfo getInstalledServiceInfoLocked(ComponentName componentName) {
+ for (int i = 0; i < mInstalledServices.size(); i++) {
+ final AccessibilityServiceInfo serviceInfo = mInstalledServices.get(i);
+ if (serviceInfo.getComponentName().equals(componentName)) {
+ return serviceInfo;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns accessibility service connection by the given service component name.
+ */
+ public AccessibilityServiceConnection getServiceConnectionLocked(ComponentName componentName) {
+ return mComponentNameToServiceMap.get(componentName);
}
public int getNonInteractiveUiTimeoutLocked() {
@@ -557,14 +650,6 @@
mIsPerformGesturesEnabled = enabled;
}
- public ComponentName getServiceAssignedToAccessibilityButtonLocked() {
- return mServiceAssignedToAccessibilityButton;
- }
-
- public void setServiceAssignedToAccessibilityButtonLocked(ComponentName componentName) {
- mServiceAssignedToAccessibilityButton = componentName;
- }
-
public ComponentName getServiceChangingSoftKeyboardModeLocked() {
return mServiceChangingSoftKeyboardMode;
}
@@ -574,14 +659,6 @@
mServiceChangingSoftKeyboardMode = serviceChangingSoftKeyboardMode;
}
- public ComponentName getServiceToEnableWithShortcutLocked() {
- return mServiceToEnableWithShortcut;
- }
-
- public void setServiceToEnableWithShortcutLocked(ComponentName componentName) {
- mServiceToEnableWithShortcut = componentName;
- }
-
public boolean isTextHighContrastEnabledLocked() {
return mIsTextHighContrastEnabled;
}
@@ -613,4 +690,28 @@
public void setUserNonInteractiveUiTimeoutLocked(int timeout) {
mUserNonInteractiveUiTimeout = timeout;
}
+
+ // TODO(a11y shortcut): These functions aren't necessary, after the new Settings shortcut Ui
+ // is merged.
+ boolean isNavBarMagnificationEnabledLocked() {
+ return mIsNavBarMagnificationEnabled;
+ }
+
+ void setNavBarMagnificationEnabledLocked(boolean enabled) {
+ mIsNavBarMagnificationEnabled = enabled;
+ }
+
+ boolean isNavBarMagnificationAssignedToAccessibilityButtonLocked() {
+ return mAccessibilityButtonTargets.contains(MAGNIFICATION_CONTROLLER_NAME);
+ }
+
+ ComponentName getServiceAssignedToAccessibilityButtonLocked() {
+ final String targetName = mAccessibilityButtonTargets.isEmpty() ? null
+ : mAccessibilityButtonTargets.valueAt(0);
+ if (targetName == null) {
+ return null;
+ }
+ return ComponentName.unflattenFromString(targetName);
+ }
+ // TODO(a11y shortcut): End
}
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityUserStateTest.java b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityUserStateTest.java
index d70e164..96d9c47 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityUserStateTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityUserStateTest.java
@@ -73,7 +73,7 @@
@Mock private AccessibilityUserState.ServiceInfoChangeListener mMockListener;
- @Mock private Context mContext;
+ @Mock private Context mMockContext;
private MockContentResolver mMockResolver;
@@ -85,11 +85,11 @@
FakeSettingsProvider.clearSettingsProvider();
mMockResolver = new MockContentResolver();
mMockResolver.addProvider(Settings.AUTHORITY, new FakeSettingsProvider());
- when(mContext.getContentResolver()).thenReturn(mMockResolver);
+ when(mMockContext.getContentResolver()).thenReturn(mMockResolver);
when(mMockServiceInfo.getComponentName()).thenReturn(COMPONENT_NAME);
when(mMockConnection.getServiceInfo()).thenReturn(mMockServiceInfo);
- mUserState = new AccessibilityUserState(USER_ID, mContext, mMockListener);
+ mUserState = new AccessibilityUserState(USER_ID, mMockContext, mMockListener);
}
@After
@@ -109,11 +109,11 @@
mUserState.setInteractiveUiTimeoutLocked(30);
mUserState.mEnabledServices.add(COMPONENT_NAME);
mUserState.mTouchExplorationGrantedServices.add(COMPONENT_NAME);
+ mUserState.mAccessibilityShortcutKeyTargets.add(COMPONENT_NAME.flattenToString());
+ mUserState.mAccessibilityButtonTargets.add(COMPONENT_NAME.flattenToString());
mUserState.setTouchExplorationEnabledLocked(true);
mUserState.setDisplayMagnificationEnabledLocked(true);
mUserState.setNavBarMagnificationEnabledLocked(true);
- mUserState.setServiceAssignedToAccessibilityButtonLocked(COMPONENT_NAME);
- mUserState.setNavBarMagnificationAssignedToAccessibilityButtonLocked(true);
mUserState.setAutoclickEnabledLocked(true);
mUserState.setUserNonInteractiveUiTimeoutLocked(30);
mUserState.setUserInteractiveUiTimeoutLocked(30);
@@ -128,11 +128,11 @@
assertEquals(0, mUserState.getInteractiveUiTimeoutLocked());
assertTrue(mUserState.mEnabledServices.isEmpty());
assertTrue(mUserState.mTouchExplorationGrantedServices.isEmpty());
+ assertTrue(mUserState.mAccessibilityShortcutKeyTargets.isEmpty());
+ assertTrue(mUserState.mAccessibilityButtonTargets.isEmpty());
assertFalse(mUserState.isTouchExplorationEnabledLocked());
assertFalse(mUserState.isDisplayMagnificationEnabledLocked());
assertFalse(mUserState.isNavBarMagnificationEnabledLocked());
- assertNull(mUserState.getServiceAssignedToAccessibilityButtonLocked());
- assertFalse(mUserState.isNavBarMagnificationAssignedToAccessibilityButtonLocked());
assertFalse(mUserState.isAutoclickEnabledLocked());
assertEquals(0, mUserState.getUserNonInteractiveUiTimeoutLocked());
assertEquals(0, mUserState.getUserInteractiveUiTimeoutLocked());
@@ -287,6 +287,19 @@
verify(mMockConnection).notifySoftKeyboardShowModeChangedLocked(eq(SHOW_MODE_HIDDEN));
}
+ @Test
+ public void isShortcutTargetInstalledLocked_returnTrue() {
+ mUserState.mInstalledServices.add(mMockServiceInfo);
+ assertTrue(mUserState.isShortcutTargetInstalledLocked(COMPONENT_NAME.flattenToString()));
+ }
+
+ @Test
+ public void isShortcutTargetInstalledLocked_invalidTarget_returnFalse() {
+ final ComponentName invalidTarget =
+ new ComponentName("com.android.server.accessibility", "InvalidTarget");
+ assertFalse(mUserState.isShortcutTargetInstalledLocked(invalidTarget.flattenToString()));
+ }
+
private int getSecureIntForUser(String key, int userId) {
return Settings.Secure.getIntForUser(mMockResolver, key, -1, userId);
}