diff options
7 files changed, 195 insertions, 1 deletions
diff --git a/core/java/android/hardware/input/KeyGestureEvent.java b/core/java/android/hardware/input/KeyGestureEvent.java index 907f0f2c8fbb..24951c4d516e 100644 --- a/core/java/android/hardware/input/KeyGestureEvent.java +++ b/core/java/android/hardware/input/KeyGestureEvent.java @@ -120,6 +120,7 @@ public final class KeyGestureEvent { public static final int KEY_GESTURE_TYPE_MAGNIFIER_ZOOM_IN = 72; public static final int KEY_GESTURE_TYPE_MAGNIFIER_ZOOM_OUT = 73; public static final int KEY_GESTURE_TYPE_TOGGLE_MAGNIFICATION = 74; + public static final int KEY_GESTURE_TYPE_ACTIVATE_SELECT_TO_SPEAK = 75; public static final int FLAG_CANCELLED = 1; @@ -209,6 +210,7 @@ public final class KeyGestureEvent { KEY_GESTURE_TYPE_MAGNIFIER_ZOOM_IN, KEY_GESTURE_TYPE_MAGNIFIER_ZOOM_OUT, KEY_GESTURE_TYPE_TOGGLE_MAGNIFICATION, + KEY_GESTURE_TYPE_ACTIVATE_SELECT_TO_SPEAK, }) @Retention(RetentionPolicy.SOURCE) public @interface KeyGestureType { @@ -785,6 +787,8 @@ public final class KeyGestureEvent { return "KEY_GESTURE_TYPE_MAGNIFIER_ZOOM_OUT"; case KEY_GESTURE_TYPE_TOGGLE_MAGNIFICATION: return "KEY_GESTURE_TYPE_TOGGLE_MAGNIFICATION"; + case KEY_GESTURE_TYPE_ACTIVATE_SELECT_TO_SPEAK: + return "KEY_GESTURE_TYPE_ACTIVATE_SELECT_TO_SPEAK"; default: return Integer.toHexString(value); } diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index 7402a2f24f97..219cefd273ac 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -4596,6 +4596,11 @@ exists on the device, the accessibility shortcut will be disabled by default. --> <string name="config_defaultAccessibilityService" translatable="false"></string> + <!-- The component name, flattened to a string, for the default select to speak service to be + enabled by the accessibility keyboard shortcut. If the service with the specified component + name is not preinstalled then this shortcut will do nothing. --> + <string name="config_defaultSelectToSpeakService" translatable="false"></string> + <!-- URI for default Accessibility notification sound when to enable accessibility shortcut. --> <string name="config_defaultAccessibilityNotificationSound" translatable="false"></string> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index db81a3be440f..badb98686fb2 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -3718,6 +3718,7 @@ <java-symbol type="string" name="color_correction_feature_name" /> <java-symbol type="string" name="reduce_bright_colors_feature_name" /> <java-symbol type="string" name="config_defaultAccessibilityService" /> + <java-symbol type="string" name="config_defaultSelectToSpeakService" /> <java-symbol type="string" name="config_defaultAccessibilityNotificationSound" /> <java-symbol type="string" name="accessibility_shortcut_spoken_feedback" /> <java-symbol type="array" name="config_trustedAccessibilityServices" /> diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java index 22837acde1c5..d4af7b765254 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java @@ -522,7 +522,8 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub @Override public boolean isKeyGestureSupported(int gestureType) { return switch (gestureType) { - case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_MAGNIFICATION -> true; + case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_MAGNIFICATION, + KeyGestureEvent.KEY_GESTURE_TYPE_ACTIVATE_SELECT_TO_SPEAK -> true; default -> false; }; } @@ -685,6 +686,34 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_MAGNIFICATION: targetName = MAGNIFICATION_CONTROLLER_NAME; break; + case KeyGestureEvent.KEY_GESTURE_TYPE_ACTIVATE_SELECT_TO_SPEAK: + targetName = mContext.getString(R.string.config_defaultSelectToSpeakService); + if (targetName.isEmpty()) { + return false; + } + + final ComponentName targetServiceComponent = TextUtils.isEmpty(targetName) + ? null : ComponentName.unflattenFromString(targetName); + AccessibilityServiceInfo accessibilityServiceInfo; + synchronized (mLock) { + AccessibilityUserState userState = getCurrentUserStateLocked(); + accessibilityServiceInfo = + userState.getInstalledServiceInfoLocked(targetServiceComponent); + } + if (accessibilityServiceInfo == null) { + return false; + } + + // Skip enabling if a warning dialog is required for the feature. + // TODO(b/377752960): Explore better options to instead show the warning dialog + // in this scenario. + if (isAccessibilityServiceWarningRequired(accessibilityServiceInfo)) { + Slog.w(LOG_TAG, + "Accessibility warning is required before this service can be " + + "activated automatically via KEY_GESTURE shortcut."); + return false; + } + break; default: return false; } diff --git a/services/core/java/com/android/server/input/InputGestureManager.java b/services/core/java/com/android/server/input/InputGestureManager.java index bc5c2db3881f..6f3540221b63 100644 --- a/services/core/java/com/android/server/input/InputGestureManager.java +++ b/services/core/java/com/android/server/input/InputGestureManager.java @@ -224,6 +224,9 @@ final class InputGestureManager { systemShortcuts.add(createKeyGesture(KeyEvent.KEYCODE_M, KeyEvent.META_META_ON | KeyEvent.META_ALT_ON, KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_MAGNIFICATION)); + systemShortcuts.add(createKeyGesture(KeyEvent.KEYCODE_S, + KeyEvent.META_META_ON | KeyEvent.META_ALT_ON, + KeyGestureEvent.KEY_GESTURE_TYPE_ACTIVATE_SELECT_TO_SPEAK)); } if (keyboardA11yShortcutControl()) { if (InputSettings.isAccessibilityBounceKeysFeatureEnabled()) { diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java index 945e07913351..d5b930769e43 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java @@ -2208,6 +2208,146 @@ public class AccessibilityManagerServiceTest { verify(mInputFilter).notifyMagnificationShortcutTriggered(anyInt()); } + @Test + @EnableFlags(com.android.hardware.input.Flags.FLAG_ENABLE_TALKBACK_AND_MAGNIFIER_KEY_GESTURES) + public void handleKeyGestureEvent_activateSelectToSpeak_trustedService() { + setupAccessibilityServiceConnection(FLAG_REQUEST_ACCESSIBILITY_BUTTON); + mFakePermissionEnforcer.grant(Manifest.permission.MANAGE_ACCESSIBILITY); + + final AccessibilityServiceInfo trustedService = mockAccessibilityServiceInfo( + new ComponentName("package_a", "class_a"), + /* isSystemApp= */ true, /* isAlwaysOnService= */ true); + AccessibilityUserState userState = mA11yms.getCurrentUserState(); + userState.mInstalledServices.add(trustedService); + mTestableContext.getOrCreateTestableResources().addOverride( + R.string.config_defaultSelectToSpeakService, + trustedService.getComponentName().flattenToString()); + mTestableContext.getOrCreateTestableResources().addOverride( + R.array.config_trustedAccessibilityServices, + new String[]{trustedService.getComponentName().flattenToString()}); + + assertThat(ShortcutUtils.getShortcutTargetsFromSettings(mTestableContext, KEY_GESTURE, + mA11yms.getCurrentUserIdLocked())).isEmpty(); + + mA11yms.handleKeyGestureEvent(new KeyGestureEvent.Builder().setKeyGestureType( + KeyGestureEvent.KEY_GESTURE_TYPE_ACTIVATE_SELECT_TO_SPEAK).setAction( + KeyGestureEvent.ACTION_GESTURE_COMPLETE).build()); + + assertThat(ShortcutUtils.getShortcutTargetsFromSettings(mTestableContext, KEY_GESTURE, + mA11yms.getCurrentUserIdLocked())).containsExactly( + trustedService.getComponentName().flattenToString()); + } + + @Test + @EnableFlags(com.android.hardware.input.Flags.FLAG_ENABLE_TALKBACK_AND_MAGNIFIER_KEY_GESTURES) + public void handleKeyGestureEvent_activateSelectToSpeak_preinstalledService() { + setupAccessibilityServiceConnection(FLAG_REQUEST_ACCESSIBILITY_BUTTON); + mFakePermissionEnforcer.grant(Manifest.permission.MANAGE_ACCESSIBILITY); + + final AccessibilityServiceInfo untrustedService = mockAccessibilityServiceInfo( + new ComponentName("package_a", "class_a"), + /* isSystemApp= */ true, /* isAlwaysOnService= */ true); + AccessibilityUserState userState = mA11yms.getCurrentUserState(); + userState.mInstalledServices.add(untrustedService); + mTestableContext.getOrCreateTestableResources().addOverride( + R.string.config_defaultSelectToSpeakService, + untrustedService.getComponentName().flattenToString()); + + assertThat(ShortcutUtils.getShortcutTargetsFromSettings(mTestableContext, KEY_GESTURE, + mA11yms.getCurrentUserIdLocked())).isEmpty(); + + mA11yms.handleKeyGestureEvent(new KeyGestureEvent.Builder().setKeyGestureType( + KeyGestureEvent.KEY_GESTURE_TYPE_ACTIVATE_SELECT_TO_SPEAK).setAction( + KeyGestureEvent.ACTION_GESTURE_COMPLETE).build()); + + assertThat(ShortcutUtils.getShortcutTargetsFromSettings(mTestableContext, KEY_GESTURE, + mA11yms.getCurrentUserIdLocked())).isEmpty(); + } + + @Test + @EnableFlags(com.android.hardware.input.Flags.FLAG_ENABLE_TALKBACK_AND_MAGNIFIER_KEY_GESTURES) + public void handleKeyGestureEvent_activateSelectToSpeak_downloadedService() { + mFakePermissionEnforcer.grant(Manifest.permission.MANAGE_ACCESSIBILITY); + + final AccessibilityServiceInfo downloadedService = mockAccessibilityServiceInfo( + new ComponentName("package_a", "class_a"), + /* isSystemApp= */ false, /* isAlwaysOnService= */ true); + AccessibilityUserState userState = mA11yms.getCurrentUserState(); + userState.mInstalledServices.add(downloadedService); + mTestableContext.getOrCreateTestableResources().addOverride( + R.string.config_defaultSelectToSpeakService, + downloadedService.getComponentName().flattenToString()); + mTestableContext.getOrCreateTestableResources().addOverride( + R.array.config_trustedAccessibilityServices, + new String[]{downloadedService.getComponentName().flattenToString()}); + + assertThat(ShortcutUtils.getShortcutTargetsFromSettings(mTestableContext, KEY_GESTURE, + mA11yms.getCurrentUserIdLocked())).isEmpty(); + + mA11yms.handleKeyGestureEvent(new KeyGestureEvent.Builder().setKeyGestureType( + KeyGestureEvent.KEY_GESTURE_TYPE_ACTIVATE_SELECT_TO_SPEAK).setAction( + KeyGestureEvent.ACTION_GESTURE_COMPLETE).build()); + + assertThat(ShortcutUtils.getShortcutTargetsFromSettings(mTestableContext, KEY_GESTURE, + mA11yms.getCurrentUserIdLocked())).isEmpty(); + } + + @Test + @EnableFlags(com.android.hardware.input.Flags.FLAG_ENABLE_TALKBACK_AND_MAGNIFIER_KEY_GESTURES) + public void handleKeyGestureEvent_activateSelectToSpeak_defaultNotInstalled() { + mFakePermissionEnforcer.grant(Manifest.permission.MANAGE_ACCESSIBILITY); + + final AccessibilityServiceInfo installedService = mockAccessibilityServiceInfo( + new ComponentName("package_a", "class_a"), + /* isSystemApp= */ true, /* isAlwaysOnService= */ true); + final AccessibilityServiceInfo defaultService = mockAccessibilityServiceInfo( + new ComponentName("package_b", "class_b"), + /* isSystemApp= */ true, /* isAlwaysOnService= */ true); + AccessibilityUserState userState = mA11yms.getCurrentUserState(); + userState.mInstalledServices.add(installedService); + mTestableContext.getOrCreateTestableResources().addOverride( + R.string.config_defaultSelectToSpeakService, + defaultService.getComponentName().flattenToString()); + mTestableContext.getOrCreateTestableResources().addOverride( + R.array.config_trustedAccessibilityServices, + new String[]{defaultService.getComponentName().flattenToString()}); + + assertThat(ShortcutUtils.getShortcutTargetsFromSettings(mTestableContext, KEY_GESTURE, + mA11yms.getCurrentUserIdLocked())).isEmpty(); + + mA11yms.handleKeyGestureEvent(new KeyGestureEvent.Builder().setKeyGestureType( + KeyGestureEvent.KEY_GESTURE_TYPE_ACTIVATE_SELECT_TO_SPEAK).setAction( + KeyGestureEvent.ACTION_GESTURE_COMPLETE).build()); + + assertThat(ShortcutUtils.getShortcutTargetsFromSettings(mTestableContext, KEY_GESTURE, + mA11yms.getCurrentUserIdLocked())).isEmpty(); + } + + @Test + @EnableFlags(com.android.hardware.input.Flags.FLAG_ENABLE_TALKBACK_AND_MAGNIFIER_KEY_GESTURES) + public void handleKeyGestureEvent_activateSelectToSpeak_noDefault() { + mFakePermissionEnforcer.grant(Manifest.permission.MANAGE_ACCESSIBILITY); + + final AccessibilityServiceInfo installedService = mockAccessibilityServiceInfo( + new ComponentName("package_a", "class_a"), + /* isSystemApp= */ true, /* isAlwaysOnService= */ true); + AccessibilityUserState userState = mA11yms.getCurrentUserState(); + userState.mInstalledServices.add(installedService); + mTestableContext.getOrCreateTestableResources().addOverride( + R.array.config_trustedAccessibilityServices, + new String[]{installedService.getComponentName().flattenToString()}); + + assertThat(ShortcutUtils.getShortcutTargetsFromSettings(mTestableContext, KEY_GESTURE, + mA11yms.getCurrentUserIdLocked())).isEmpty(); + + mA11yms.handleKeyGestureEvent(new KeyGestureEvent.Builder().setKeyGestureType( + KeyGestureEvent.KEY_GESTURE_TYPE_ACTIVATE_SELECT_TO_SPEAK).setAction( + KeyGestureEvent.ACTION_GESTURE_COMPLETE).build()); + + assertThat(ShortcutUtils.getShortcutTargetsFromSettings(mTestableContext, KEY_GESTURE, + mA11yms.getCurrentUserIdLocked())).isEmpty(); + } + private Set<String> readStringsFromSetting(String setting) { final Set<String> result = new ArraySet<>(); mA11yms.readColonDelimitedSettingToSet( diff --git a/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt b/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt index 40ea9dc9cd89..09a686ca2c3f 100644 --- a/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt +++ b/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt @@ -794,6 +794,18 @@ class KeyGestureControllerTests { KeyEvent.META_META_ON or KeyEvent.META_ALT_ON, intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) ), + TestData( + "META + ALT + S -> Activate Select to Speak", + intArrayOf( + KeyEvent.KEYCODE_META_LEFT, + KeyEvent.KEYCODE_ALT_LEFT, + KeyEvent.KEYCODE_S + ), + KeyGestureEvent.KEY_GESTURE_TYPE_ACTIVATE_SELECT_TO_SPEAK, + intArrayOf(KeyEvent.KEYCODE_S), + KeyEvent.META_META_ON or KeyEvent.META_ALT_ON, + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + ), ) } |