Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit f39fa244 authored by David Padlipsky's avatar David Padlipsky
Browse files

Implement magnification toggle keyboard shortcut

Adds new "KEY_GESTURE" shortcut type to the AccessibilityManagerService
which tracks Accessibility Services which are able to be activated
directly from a KeyGestureEvent key combo.

Bug: 375277034
Test: atest AccessibilityManagerServiceTest
Test: Manually on device
Flag: com.android.hardware.input.enable_talkback_and_magnifier_key_gestures

Change-Id: I4d65eb7834d1b48628fca46802781054901c0287
parent dad192db
Loading
Loading
Loading
Loading
+4 −0
Original line number Diff line number Diff line
@@ -119,6 +119,7 @@ public final class KeyGestureEvent {
    public static final int KEY_GESTURE_TYPE_RESTORE_FREEFORM_WINDOW_SIZE = 71;
    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 FLAG_CANCELLED = 1;

@@ -207,6 +208,7 @@ public final class KeyGestureEvent {
            KEY_GESTURE_TYPE_RESTORE_FREEFORM_WINDOW_SIZE,
            KEY_GESTURE_TYPE_MAGNIFIER_ZOOM_IN,
            KEY_GESTURE_TYPE_MAGNIFIER_ZOOM_OUT,
            KEY_GESTURE_TYPE_TOGGLE_MAGNIFICATION,
    })
    @Retention(RetentionPolicy.SOURCE)
    public @interface KeyGestureType {
@@ -781,6 +783,8 @@ public final class KeyGestureEvent {
                return "KEY_GESTURE_TYPE_MAGNIFIER_ZOOM_IN";
            case KEY_GESTURE_TYPE_MAGNIFIER_ZOOM_OUT:
                return "KEY_GESTURE_TYPE_MAGNIFIER_ZOOM_OUT";
            case KEY_GESTURE_TYPE_TOGGLE_MAGNIFICATION:
                return "KEY_GESTURE_TYPE_TOGGLE_MAGNIFICATION";
            default:
                return Integer.toHexString(value);
        }
+8 −2
Original line number Diff line number Diff line
@@ -63,6 +63,8 @@ public final class ShortcutConstants {
     * quickly tapping screen 2 times with two fingers as preferred shortcut.
     * {@code QUICK_SETTINGS} for displaying specifying the accessibility services or features which
     * choose Quick Settings as preferred shortcut.
     * {@code KEY_GESTURE} for shortcuts which are directly from key gestures and should be
     * activated always.
     */
    @Retention(RetentionPolicy.SOURCE)
    @IntDef({
@@ -73,6 +75,7 @@ public final class ShortcutConstants {
            UserShortcutType.TWOFINGER_DOUBLETAP,
            UserShortcutType.QUICK_SETTINGS,
            UserShortcutType.GESTURE,
            UserShortcutType.KEY_GESTURE,
            UserShortcutType.ALL
    })
    public @interface UserShortcutType {
@@ -84,8 +87,10 @@ public final class ShortcutConstants {
        int TWOFINGER_DOUBLETAP = 1 << 3;
        int QUICK_SETTINGS = 1 << 4;
        int GESTURE = 1 << 5;
        int KEY_GESTURE = 1 << 6;
        // LINT.ThenChange(:shortcut_type_array)
        int ALL = SOFTWARE | HARDWARE | TRIPLETAP | TWOFINGER_DOUBLETAP | QUICK_SETTINGS | GESTURE;
        int ALL = SOFTWARE | HARDWARE | TRIPLETAP | TWOFINGER_DOUBLETAP | QUICK_SETTINGS | GESTURE
                | KEY_GESTURE;
    }

    /**
@@ -99,7 +104,8 @@ public final class ShortcutConstants {
            UserShortcutType.TRIPLETAP,
            UserShortcutType.TWOFINGER_DOUBLETAP,
            UserShortcutType.QUICK_SETTINGS,
            UserShortcutType.GESTURE
            UserShortcutType.GESTURE,
            UserShortcutType.KEY_GESTURE
            // LINT.ThenChange(:shortcut_type_intdef)
    };

+3 −0
Original line number Diff line number Diff line
@@ -27,6 +27,7 @@ import static com.android.internal.accessibility.common.ShortcutConstants.SERVIC
import static com.android.internal.accessibility.common.ShortcutConstants.USER_SHORTCUT_TYPES;
import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.GESTURE;
import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.HARDWARE;
import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.KEY_GESTURE;
import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.QUICK_SETTINGS;
import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.SOFTWARE;
import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.TRIPLETAP;
@@ -187,6 +188,7 @@ public final class ShortcutUtils {
            case TWOFINGER_DOUBLETAP ->
                    Settings.Secure.ACCESSIBILITY_MAGNIFICATION_TWO_FINGER_TRIPLE_TAP_ENABLED;
            case QUICK_SETTINGS -> Settings.Secure.ACCESSIBILITY_QS_TARGETS;
            case KEY_GESTURE -> Settings.Secure.ACCESSIBILITY_KEY_GESTURE_TARGETS;
            default -> throw new IllegalArgumentException(
                    "Unsupported user shortcut type: " + type);
        };
@@ -209,6 +211,7 @@ public final class ShortcutUtils {
                    TRIPLETAP;
            case Settings.Secure.ACCESSIBILITY_MAGNIFICATION_TWO_FINGER_TRIPLE_TAP_ENABLED ->
                    TWOFINGER_DOUBLETAP;
            case Settings.Secure.ACCESSIBILITY_KEY_GESTURE_TARGETS -> KEY_GESTURE;
            default -> throw new IllegalArgumentException(
                    "Unsupported user shortcut key: " + key);
        };
+9 −0
Original line number Diff line number Diff line
@@ -21,6 +21,7 @@ import static android.provider.Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_MAG

import static com.android.internal.accessibility.common.ShortcutConstants.SERVICES_SEPARATOR;
import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.GESTURE;
import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.KEY_GESTURE;
import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.SOFTWARE;

import static com.google.common.truth.Truth.assertThat;
@@ -122,6 +123,14 @@ public class ShortcutUtilsTest {
        ).isEmpty();
    }

    @Test
    public void getShortcutTargets_keyGestureShortcutNoService_emptyResult() {
        assertThat(
                ShortcutUtils.getShortcutTargetsFromSettings(
                        mContext, KEY_GESTURE, mDefaultUserId)
        ).isEmpty();
    }

    @Test
    public void getShortcutTargets_softwareShortcut1Service_return1Service() {
        setupShortcutTargets(ONE_COMPONENT, Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS);
+100 −4
Original line number Diff line number Diff line
@@ -42,9 +42,11 @@ import static android.provider.Settings.Secure.ACCESSIBILITY_BUTTON_MODE_FLOATIN
import static android.provider.Settings.Secure.ACCESSIBILITY_BUTTON_MODE_GESTURE;
import static android.provider.Settings.Secure.ACCESSIBILITY_BUTTON_MODE_NAVIGATION_BAR;
import static android.provider.Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_NAVBAR_ENABLED;
import static android.view.Display.INVALID_DISPLAY;
import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL;
import static android.view.accessibility.AccessibilityManager.FlashNotificationReason;

import static com.android.hardware.input.Flags.enableTalkbackAndMagnifierKeyGestures;
import static com.android.hardware.input.Flags.keyboardA11yMouseKeys;
import static com.android.internal.accessibility.AccessibilityShortcutController.ACCESSIBILITY_HEARING_AIDS_COMPONENT_NAME;
import static com.android.internal.accessibility.AccessibilityShortcutController.MAGNIFICATION_COMPONENT_NAME;
@@ -55,6 +57,7 @@ import static com.android.internal.accessibility.common.ShortcutConstants.UserSh
import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.ALL;
import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.GESTURE;
import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.HARDWARE;
import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.KEY_GESTURE;
import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.QUICK_SETTINGS;
import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.SOFTWARE;
import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.TRIPLETAP;
@@ -111,6 +114,8 @@ import android.graphics.Rect;
import android.graphics.Region;
import android.hardware.display.DisplayManager;
import android.hardware.fingerprint.IFingerprintService;
import android.hardware.input.InputManager;
import android.hardware.input.KeyGestureEvent;
import android.media.AudioManagerInternal;
import android.net.Uri;
import android.os.Binder;
@@ -338,6 +343,8 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub

    private AlertDialog mEnableTouchExplorationDialog;

    private final InputManager mInputManager;

    private AccessibilityInputFilter mInputFilter;

    private boolean mHasInputFilter;
@@ -503,6 +510,24 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
        }
    }

    private InputManager.KeyGestureEventHandler mKeyGestureEventHandler =
            new InputManager.KeyGestureEventHandler() {
                @Override
                public boolean handleKeyGestureEvent(
                        @NonNull KeyGestureEvent event,
                        @Nullable IBinder focusedToken) {
                    return AccessibilityManagerService.this.handleKeyGestureEvent(event);
                }

                @Override
                public boolean isKeyGestureSupported(int gestureType) {
                    return switch (gestureType) {
                        case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_MAGNIFICATION -> true;
                        default -> false;
                    };
                }
            };

    @VisibleForTesting
    AccessibilityManagerService(
            Context context,
@@ -542,6 +567,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
        mUmi = LocalServices.getService(UserManagerInternal.class);
        // TODO(b/255426725): not used on tests
        mVisibleBgUserIds = null;
        mInputManager = context.getSystemService(InputManager.class);

        init();
    }
@@ -583,6 +609,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
                mUiAutomationManager, this);
        mFlashNotificationsController = new FlashNotificationsController(mContext);
        mUmi = LocalServices.getService(UserManagerInternal.class);
        mInputManager = context.getSystemService(InputManager.class);

        if (UserManager.isVisibleBackgroundUsersEnabled()) {
            mVisibleBgUserIds = new SparseBooleanArray();
@@ -599,6 +626,9 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
        registerBroadcastReceivers();
        new AccessibilityContentObserver(mMainHandler).register(
                mContext.getContentResolver());
        if (enableTalkbackAndMagnifierKeyGestures()) {
            mInputManager.registerKeyGestureEventHandler(mKeyGestureEventHandler);
        }
        disableAccessibilityMenuToMigrateIfNeeded();
    }

@@ -640,6 +670,51 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
        return mIsAccessibilityButtonShown;
    }

    @VisibleForTesting
    boolean handleKeyGestureEvent(KeyGestureEvent event) {
        final boolean complete =
                event.getAction() == KeyGestureEvent.ACTION_GESTURE_COMPLETE
                        && !event.isCancelled();
        final int gestureType = event.getKeyGestureType();
        if (!complete) {
            return false;
        }

        String targetName;
        switch (gestureType) {
            case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_MAGNIFICATION:
                targetName = MAGNIFICATION_CONTROLLER_NAME;
                break;
            default:
                return false;
        }

        List<String> shortcutTargets = getAccessibilityShortcutTargets(
                KEY_GESTURE);
        if (!shortcutTargets.contains(targetName)) {
            int userId;
            synchronized (mLock) {
                userId = mCurrentUserId;
            }
            // TODO(b/377752960): Add dialog to confirm enabling the service and to
            //  activate the first time.
            enableShortcutForTargets(true, UserShortcutType.KEY_GESTURE,
                    List.of(targetName), userId);

            // Do not perform action on first press since it was just registered. Eventually,
            // this will be a separate dialog that appears that requires the user to confirm
            // which will resolve this race condition. For now, just require two presses the
            // first time it is activated.
            return true;
        }

        final int displayId = event.getDisplayId() != INVALID_DISPLAY
                ? event.getDisplayId() : getLastNonProxyTopFocusedDisplayId();
        performAccessibilityShortcutInternal(displayId, KEY_GESTURE, targetName);

        return true;
    }

    @Override
    public Pair<float[], MagnificationSpec> getWindowTransformationMatrixAndMagnificationSpec(
            int windowId) {
@@ -1224,14 +1299,14 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
            int displayId = event.getDisplayId();
            final int windowId = event.getWindowId();
            if (windowId != AccessibilityWindowInfo.UNDEFINED_WINDOW_ID
                    && displayId == Display.INVALID_DISPLAY) {
                    && displayId == INVALID_DISPLAY) {
                displayId = mA11yWindowManager.getDisplayIdByUserIdAndWindowId(
                        resolvedUserId, windowId);
                event.setDisplayId(displayId);
            }
            synchronized (mLock) {
                if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED
                        && displayId != Display.INVALID_DISPLAY
                        && displayId != INVALID_DISPLAY
                        && mA11yWindowManager.isTrackingWindowsLocked(displayId)) {
                    shouldComputeWindows = true;
                }
@@ -3257,6 +3332,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
        updateAccessibilityShortcutTargetsLocked(userState, SOFTWARE);
        updateAccessibilityShortcutTargetsLocked(userState, GESTURE);
        updateAccessibilityShortcutTargetsLocked(userState, QUICK_SETTINGS);
        updateAccessibilityShortcutTargetsLocked(userState, KEY_GESTURE);
        // Update the capabilities before the mode because we will check the current mode is
        // invalid or not..
        updateMagnificationCapabilitiesSettingsChangeLocked(userState);
@@ -3387,6 +3463,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
        somethingChanged |= readAccessibilityShortcutTargetsLocked(userState, QUICK_SETTINGS);
        somethingChanged |= readAccessibilityShortcutTargetsLocked(userState, SOFTWARE);
        somethingChanged |= readAccessibilityShortcutTargetsLocked(userState, GESTURE);
        somethingChanged |= readAccessibilityShortcutTargetsLocked(userState, KEY_GESTURE);
        somethingChanged |= readAccessibilityButtonTargetComponentLocked(userState);
        somethingChanged |= readUserRecommendedUiTimeoutSettingsLocked(userState);
        somethingChanged |= readMagnificationModeForDefaultDisplayLocked(userState);
@@ -3968,6 +4045,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
        if (android.provider.Flags.a11yStandaloneGestureEnabled()) {
            shortcutTypes.add(GESTURE);
        }
        shortcutTypes.add(KEY_GESTURE);

        final ComponentName serviceName = service.getComponentName();
        for (Integer shortcutType: shortcutTypes) {
@@ -4078,13 +4156,15 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
     */
    private void performAccessibilityShortcutInternal(int displayId,
            @UserShortcutType int shortcutType, @Nullable String targetName) {
        final List<String> shortcutTargets = getAccessibilityShortcutTargetsInternal(shortcutType);
        final List<String> shortcutTargets = getAccessibilityShortcutTargetsInternal(
                shortcutType);
        if (shortcutTargets.isEmpty()) {
            Slog.d(LOG_TAG, "No target to perform shortcut, shortcutType=" + shortcutType);
            return;
        }
        // In case the caller specified a target name
        if (targetName != null && !doesShortcutTargetsStringContain(shortcutTargets, targetName)) {
        if (targetName != null && !doesShortcutTargetsStringContain(shortcutTargets,
                targetName)) {
            Slog.v(LOG_TAG, "Perform shortcut failed, invalid target name:" + targetName);
            targetName = null;
        }
@@ -4306,6 +4386,13 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
            return;
        }

        if (shortcutType == UserShortcutType.KEY_GESTURE
                && !enableTalkbackAndMagnifierKeyGestures()) {
            Slog.w(LOG_TAG,
                    "KEY_GESTURE type shortcuts are disabled by feature flag");
            return;
        }

        final String shortcutTypeSettingKey = ShortcutUtils.convertToKey(shortcutType);
        if (shortcutType == UserShortcutType.TRIPLETAP
                || shortcutType == UserShortcutType.TWOFINGER_DOUBLETAP) {
@@ -5683,6 +5770,9 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
        private final Uri mAccessibilityGestureTargetsUri = Settings.Secure.getUriFor(
                Settings.Secure.ACCESSIBILITY_GESTURE_TARGETS);

        private final Uri mAccessibilityKeyGestureTargetsUri = Settings.Secure.getUriFor(
                Settings.Secure.ACCESSIBILITY_KEY_GESTURE_TARGETS);

        private final Uri mUserNonInteractiveUiTimeoutUri = Settings.Secure.getUriFor(
                Settings.Secure.ACCESSIBILITY_NON_INTERACTIVE_UI_TIMEOUT_MS);

@@ -5746,6 +5836,8 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
                    mAccessibilityButtonTargetsUri, false, this, UserHandle.USER_ALL);
            contentResolver.registerContentObserver(
                    mAccessibilityGestureTargetsUri, false, this, UserHandle.USER_ALL);
            contentResolver.registerContentObserver(
                    mAccessibilityKeyGestureTargetsUri, false, this, UserHandle.USER_ALL);
            contentResolver.registerContentObserver(
                    mUserNonInteractiveUiTimeoutUri, false, this, UserHandle.USER_ALL);
            contentResolver.registerContentObserver(
@@ -5828,6 +5920,10 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
                    if (readAccessibilityShortcutTargetsLocked(userState, GESTURE)) {
                        onUserStateChangedLocked(userState);
                    }
                } else if (mAccessibilityKeyGestureTargetsUri.equals(uri)) {
                    if (readAccessibilityShortcutTargetsLocked(userState, KEY_GESTURE)) {
                        onUserStateChangedLocked(userState);
                    }
                } else if (mUserNonInteractiveUiTimeoutUri.equals(uri)
                        || mUserInteractiveUiTimeoutUri.equals(uri)) {
                    readUserRecommendedUiTimeoutSettingsLocked(userState);
Loading