diff options
9 files changed, 167 insertions, 29 deletions
diff --git a/core/java/android/hardware/input/KeyGestureEvent.java b/core/java/android/hardware/input/KeyGestureEvent.java index 66d073fa791e..cb1e0161441f 100644 --- a/core/java/android/hardware/input/KeyGestureEvent.java +++ b/core/java/android/hardware/input/KeyGestureEvent.java @@ -129,6 +129,7 @@ public final class KeyGestureEvent { public static final int KEY_GESTURE_TYPE_MAGNIFICATION_PAN_RIGHT = 79; public static final int KEY_GESTURE_TYPE_MAGNIFICATION_PAN_UP = 80; public static final int KEY_GESTURE_TYPE_MAGNIFICATION_PAN_DOWN = 81; + public static final int KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS = 82; public static final int FLAG_CANCELLED = 1; @@ -225,6 +226,7 @@ public final class KeyGestureEvent { KEY_GESTURE_TYPE_MAGNIFICATION_PAN_RIGHT, KEY_GESTURE_TYPE_MAGNIFICATION_PAN_UP, KEY_GESTURE_TYPE_MAGNIFICATION_PAN_DOWN, + KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS, }) @Retention(RetentionPolicy.SOURCE) public @interface KeyGestureType { @@ -807,6 +809,8 @@ public final class KeyGestureEvent { return "KEY_GESTURE_TYPE_MAGNIFICATION_PAN_UP"; case KEY_GESTURE_TYPE_MAGNIFICATION_PAN_DOWN: return "KEY_GESTURE_TYPE_MAGNIFICATION_PAN_DOWN"; + case KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS: + return "KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS"; default: return Integer.toHexString(value); } diff --git a/core/java/com/android/internal/accessibility/util/AccessibilityUtils.java b/core/java/com/android/internal/accessibility/util/AccessibilityUtils.java index 0b1ecf78d28c..d03bb5c3cb17 100644 --- a/core/java/com/android/internal/accessibility/util/AccessibilityUtils.java +++ b/core/java/com/android/internal/accessibility/util/AccessibilityUtils.java @@ -29,6 +29,7 @@ import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; +import android.content.pm.ServiceInfo; import android.os.Build; import android.os.UserHandle; import android.provider.Settings; @@ -351,4 +352,24 @@ public final class AccessibilityUtils { } return result; } + + /** Returns the {@link ComponentName} of an installed accessibility service by label. */ + @Nullable + public static ComponentName getInstalledAccessibilityServiceComponentNameByLabel( + Context context, String label) { + AccessibilityManager accessibilityManager = + context.getSystemService(AccessibilityManager.class); + List<AccessibilityServiceInfo> serviceInfos = + accessibilityManager.getInstalledAccessibilityServiceList(); + + for (AccessibilityServiceInfo service : serviceInfos) { + final ServiceInfo serviceInfo = service.getResolveInfo().serviceInfo; + if (label.equals(serviceInfo.loadLabel(context.getPackageManager()).toString()) + && (serviceInfo.applicationInfo.isSystemApp() + || serviceInfo.applicationInfo.isUpdatedSystemApp())) { + return new ComponentName(serviceInfo.packageName, serviceInfo.name); + } + } + return null; + } } diff --git a/services/core/java/com/android/server/input/InputGestureManager.java b/services/core/java/com/android/server/input/InputGestureManager.java index 9f785ac81398..24296406da00 100644 --- a/services/core/java/com/android/server/input/InputGestureManager.java +++ b/services/core/java/com/android/server/input/InputGestureManager.java @@ -19,6 +19,7 @@ package com.android.server.input; import static android.hardware.input.InputGestureData.createKeyTrigger; import static com.android.hardware.input.Flags.enableTalkbackAndMagnifierKeyGestures; +import static com.android.hardware.input.Flags.enableVoiceAccessKeyGestures; import static com.android.hardware.input.Flags.keyboardA11yShortcutControl; import static com.android.server.flags.Flags.newBugreportKeyboardShortcut; import static com.android.window.flags.Flags.enableMoveToNextDisplayShortcut; @@ -240,6 +241,13 @@ final class InputGestureManager { KeyEvent.META_META_ON | KeyEvent.META_ALT_ON, KeyGestureEvent.KEY_GESTURE_TYPE_ACTIVATE_SELECT_TO_SPEAK)); } + if (enableVoiceAccessKeyGestures()) { + systemShortcuts.add( + createKeyGesture( + KeyEvent.KEYCODE_V, + KeyEvent.META_META_ON | KeyEvent.META_ALT_ON, + KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS)); + } if (enableTaskResizingKeyboardShortcuts()) { systemShortcuts.add(createKeyGesture( KeyEvent.KEYCODE_LEFT_BRACKET, diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java index 516213b32354..7f511e1e2aa1 100644 --- a/services/core/java/com/android/server/policy/PhoneWindowManager.java +++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java @@ -85,15 +85,16 @@ import static android.view.contentprotection.flags.Flags.createAccessibilityOver import static com.android.hardware.input.Flags.enableNew25q2Keycodes; import static com.android.hardware.input.Flags.enableTalkbackAndMagnifierKeyGestures; +import static com.android.hardware.input.Flags.enableVoiceAccessKeyGestures; import static com.android.hardware.input.Flags.inputManagerLifecycleSupport; import static com.android.hardware.input.Flags.keyboardA11yShortcutControl; import static com.android.hardware.input.Flags.modifierShortcutDump; import static com.android.hardware.input.Flags.overridePowerKeyBehaviorInFocusedWindow; import static com.android.hardware.input.Flags.useKeyGestureEventHandler; +import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.SCREENSHOT_KEYCHORD_DELAY; import static com.android.server.GestureLauncherService.DOUBLE_POWER_TAP_COUNT_THRESHOLD; import static com.android.server.flags.Flags.modifierShortcutManagerMultiuser; import static com.android.server.flags.Flags.newBugreportKeyboardShortcut; -import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.SCREENSHOT_KEYCHORD_DELAY; import static com.android.server.policy.WindowManagerPolicy.WindowManagerFuncs.CAMERA_LENS_COVERED; import static com.android.server.policy.WindowManagerPolicy.WindowManagerFuncs.CAMERA_LENS_COVER_ABSENT; import static com.android.server.policy.WindowManagerPolicy.WindowManagerFuncs.CAMERA_LENS_UNCOVERED; @@ -502,6 +503,8 @@ public class PhoneWindowManager implements WindowManagerPolicy { private TalkbackShortcutController mTalkbackShortcutController; + private VoiceAccessShortcutController mVoiceAccessShortcutController; + private WindowWakeUpPolicy mWindowWakeUpPolicy; /** @@ -2265,6 +2268,10 @@ public class PhoneWindowManager implements WindowManagerPolicy { return new TalkbackShortcutController(mContext); } + VoiceAccessShortcutController getVoiceAccessShortcutController() { + return new VoiceAccessShortcutController(mContext); + } + WindowWakeUpPolicy getWindowWakeUpPolicy() { return new WindowWakeUpPolicy(mContext); } @@ -2512,6 +2519,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { com.android.internal.R.integer.config_keyguardDrawnTimeout); mKeyguardDelegate = injector.getKeyguardServiceDelegate(); mTalkbackShortcutController = injector.getTalkbackShortcutController(); + mVoiceAccessShortcutController = injector.getVoiceAccessShortcutController(); mWindowWakeUpPolicy = injector.getWindowWakeUpPolicy(); initKeyCombinationRules(); initSingleKeyGestureRules(injector.getLooper()); @@ -4262,6 +4270,8 @@ public class PhoneWindowManager implements WindowManagerPolicy { .isAccessibilityShortcutAvailable(false); case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_TALKBACK: return enableTalkbackAndMagnifierKeyGestures(); + case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS: + return enableVoiceAccessKeyGestures(); default: return false; } @@ -4492,6 +4502,14 @@ public class PhoneWindowManager implements WindowManagerPolicy { return true; } break; + case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS: + if (enableVoiceAccessKeyGestures()) { + if (complete) { + mVoiceAccessShortcutController.toggleVoiceAccess(mCurrentUserId); + } + return true; + } + break; case KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_APPLICATION: AppLaunchData data = event.getAppLaunchData(); if (complete && canLaunchApp && data != null diff --git a/services/core/java/com/android/server/policy/TalkbackShortcutController.java b/services/core/java/com/android/server/policy/TalkbackShortcutController.java index 9e16a7d5e83a..efda337527d4 100644 --- a/services/core/java/com/android/server/policy/TalkbackShortcutController.java +++ b/services/core/java/com/android/server/policy/TalkbackShortcutController.java @@ -18,20 +18,15 @@ package com.android.server.policy; import static com.android.internal.util.FrameworkStatsLog.ACCESSIBILITY_SHORTCUT_REPORTED__SHORTCUT_TYPE__A11Y_WEAR_TRIPLE_PRESS_GESTURE; -import android.accessibilityservice.AccessibilityServiceInfo; import android.content.ComponentName; import android.content.Context; -import android.content.pm.PackageManager; -import android.content.pm.ServiceInfo; import android.os.UserHandle; import android.provider.Settings; -import android.view.accessibility.AccessibilityManager; import com.android.internal.accessibility.util.AccessibilityStatsLogUtils; import com.android.internal.accessibility.util.AccessibilityUtils; import com.android.internal.annotations.VisibleForTesting; -import java.util.List; import java.util.Set; /** @@ -42,7 +37,6 @@ import java.util.Set; class TalkbackShortcutController { private static final String TALKBACK_LABEL = "TalkBack"; private final Context mContext; - private final PackageManager mPackageManager; public enum ShortcutSource { GESTURE, @@ -51,7 +45,6 @@ class TalkbackShortcutController { TalkbackShortcutController(Context context) { mContext = context; - mPackageManager = mContext.getPackageManager(); } /** @@ -63,7 +56,10 @@ class TalkbackShortcutController { boolean toggleTalkback(int userId, ShortcutSource source) { final Set<ComponentName> enabledServices = AccessibilityUtils.getEnabledServicesFromSettings(mContext, userId); - ComponentName componentName = getTalkbackComponent(); + ComponentName componentName = + AccessibilityUtils.getInstalledAccessibilityServiceComponentNameByLabel( + mContext, TALKBACK_LABEL); + ; if (componentName == null) { return false; } @@ -83,21 +79,6 @@ class TalkbackShortcutController { return isTalkbackAlreadyEnabled; } - private ComponentName getTalkbackComponent() { - AccessibilityManager accessibilityManager = mContext.getSystemService( - AccessibilityManager.class); - List<AccessibilityServiceInfo> serviceInfos = - accessibilityManager.getInstalledAccessibilityServiceList(); - - for (AccessibilityServiceInfo service : serviceInfos) { - final ServiceInfo serviceInfo = service.getResolveInfo().serviceInfo; - if (isTalkback(serviceInfo)) { - return new ComponentName(serviceInfo.packageName, serviceInfo.name); - } - } - return null; - } - boolean isTalkBackShortcutGestureEnabled() { return Settings.System.getIntForUser(mContext.getContentResolver(), Settings.System.WEAR_ACCESSIBILITY_GESTURE_ENABLED, @@ -120,9 +101,4 @@ class TalkbackShortcutController { ACCESSIBILITY_SHORTCUT_REPORTED__SHORTCUT_TYPE__A11Y_WEAR_TRIPLE_PRESS_GESTURE, /* serviceEnabled= */ true); } - - private boolean isTalkback(ServiceInfo info) { - return TALKBACK_LABEL.equals(info.loadLabel(mPackageManager).toString()) - && (info.applicationInfo.isSystemApp() || info.applicationInfo.isUpdatedSystemApp()); - } } diff --git a/services/core/java/com/android/server/policy/VoiceAccessShortcutController.java b/services/core/java/com/android/server/policy/VoiceAccessShortcutController.java new file mode 100644 index 000000000000..a37fb1140e06 --- /dev/null +++ b/services/core/java/com/android/server/policy/VoiceAccessShortcutController.java @@ -0,0 +1,62 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.policy; + +import android.content.ComponentName; +import android.content.Context; +import android.util.Slog; + +import com.android.internal.accessibility.util.AccessibilityUtils; + +import androidx.annotation.VisibleForTesting; + +import java.util.Set; + +/** This class controls voice access shortcut related operations such as toggling, querying. */ +class VoiceAccessShortcutController { + private static final String TAG = VoiceAccessShortcutController.class.getSimpleName(); + private static final String VOICE_ACCESS_LABEL = "Voice Access"; + + private final Context mContext; + + VoiceAccessShortcutController(Context context) { + mContext = context; + } + + /** + * A function that toggles voice access service. + * + * @return whether voice access is enabled after being toggled. + */ + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + boolean toggleVoiceAccess(int userId) { + final Set<ComponentName> enabledServices = + AccessibilityUtils.getEnabledServicesFromSettings(mContext, userId); + ComponentName componentName = + AccessibilityUtils.getInstalledAccessibilityServiceComponentNameByLabel( + mContext, VOICE_ACCESS_LABEL); + if (componentName == null) { + Slog.e(TAG, "Toggle Voice Access failed due to componentName being null"); + return false; + } + + boolean newState = !enabledServices.contains(componentName); + AccessibilityUtils.setAccessibilityServiceState(mContext, componentName, newState, userId); + + return newState; + } +} diff --git a/services/tests/wmtests/src/com/android/server/policy/KeyGestureEventTests.java b/services/tests/wmtests/src/com/android/server/policy/KeyGestureEventTests.java index 9d4d94bebfd9..85ef466b2477 100644 --- a/services/tests/wmtests/src/com/android/server/policy/KeyGestureEventTests.java +++ b/services/tests/wmtests/src/com/android/server/policy/KeyGestureEventTests.java @@ -758,6 +758,18 @@ public class KeyGestureEventTests extends ShortcutKeyTestBase { } @Test + @EnableFlags(com.android.hardware.input.Flags.FLAG_ENABLE_VOICE_ACCESS_KEY_GESTURES) + public void testKeyGestureToggleVoiceAccess() { + Assert.assertTrue( + sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS)); + mPhoneWindowManager.assertVoiceAccess(true); + + Assert.assertTrue( + sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS)); + mPhoneWindowManager.assertVoiceAccess(false); + } + + @Test public void testKeyGestureToggleDoNotDisturb() { mPhoneWindowManager.overrideZenMode(Settings.Global.ZEN_MODE_OFF); Assert.assertTrue( diff --git a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java index 6c48ba26a475..4ff3d433632a 100644 --- a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java +++ b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java @@ -201,6 +201,8 @@ class TestPhoneWindowManager { private boolean mIsTalkBackEnabled; private boolean mIsTalkBackShortcutGestureEnabled; + private boolean mIsVoiceAccessEnabled; + private Intent mBrowserIntent; private Intent mSmsIntent; @@ -225,6 +227,18 @@ class TestPhoneWindowManager { } } + private class TestVoiceAccessShortcutController extends VoiceAccessShortcutController { + TestVoiceAccessShortcutController(Context context) { + super(context); + } + + @Override + boolean toggleVoiceAccess(int currentUserId) { + mIsVoiceAccessEnabled = !mIsVoiceAccessEnabled; + return mIsVoiceAccessEnabled; + } + } + private class TestInjector extends PhoneWindowManager.Injector { TestInjector(Context context, WindowManagerPolicy.WindowManagerFuncs funcs) { super(context, funcs); @@ -260,6 +274,10 @@ class TestPhoneWindowManager { return new TestTalkbackShortcutController(mContext); } + VoiceAccessShortcutController getVoiceAccessShortcutController() { + return new TestVoiceAccessShortcutController(mContext); + } + WindowWakeUpPolicy getWindowWakeUpPolicy() { return mWindowWakeUpPolicy; } @@ -1024,6 +1042,11 @@ class TestPhoneWindowManager { Assert.assertEquals(expectEnabled, mIsTalkBackEnabled); } + void assertVoiceAccess(boolean expectEnabled) { + mTestLooper.dispatchAll(); + Assert.assertEquals(expectEnabled, mIsVoiceAccessEnabled); + } + void assertKeyGestureEventSentToKeyGestureController(int gestureType) { verify(mInputManagerInternal) .handleKeyGestureInKeyGestureController(anyInt(), any(), anyInt(), eq(gestureType)); diff --git a/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt b/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt index 4d7085feb98f..d35c9008e8cb 100644 --- a/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt +++ b/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt @@ -830,6 +830,18 @@ class KeyGestureControllerTests { KeyEvent.META_META_ON or KeyEvent.META_ALT_ON, intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) ), + TestData( + "META + ALT + 'V' -> Toggle Voice Access", + intArrayOf( + KeyEvent.KEYCODE_META_LEFT, + KeyEvent.KEYCODE_ALT_LEFT, + KeyEvent.KEYCODE_V + ), + KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS, + intArrayOf(KeyEvent.KEYCODE_V), + KeyEvent.META_META_ON or KeyEvent.META_ALT_ON, + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + ), ) } @@ -843,6 +855,7 @@ class KeyGestureControllerTests { com.android.hardware.input.Flags.FLAG_KEYBOARD_A11Y_STICKY_KEYS_FLAG, com.android.hardware.input.Flags.FLAG_KEYBOARD_A11Y_MOUSE_KEYS, com.android.hardware.input.Flags.FLAG_ENABLE_TALKBACK_AND_MAGNIFIER_KEY_GESTURES, + com.android.hardware.input.Flags.FLAG_ENABLE_VOICE_ACCESS_KEY_GESTURES, com.android.window.flags.Flags.FLAG_ENABLE_MOVE_TO_NEXT_DISPLAY_SHORTCUT, com.android.window.flags.Flags.FLAG_ENABLE_TASK_RESIZING_KEYBOARD_SHORTCUTS ) @@ -861,6 +874,7 @@ class KeyGestureControllerTests { com.android.hardware.input.Flags.FLAG_KEYBOARD_A11Y_STICKY_KEYS_FLAG, com.android.hardware.input.Flags.FLAG_KEYBOARD_A11Y_MOUSE_KEYS, com.android.hardware.input.Flags.FLAG_ENABLE_TALKBACK_AND_MAGNIFIER_KEY_GESTURES, + com.android.hardware.input.Flags.FLAG_ENABLE_VOICE_ACCESS_KEY_GESTURES, com.android.window.flags.Flags.FLAG_ENABLE_MOVE_TO_NEXT_DISPLAY_SHORTCUT, com.android.window.flags.Flags.FLAG_ENABLE_TASK_RESIZING_KEYBOARD_SHORTCUTS ) |