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

Commit 2047bdf1 authored by Longbo Wei's avatar Longbo Wei Committed by Android (Google) Code Review
Browse files

Merge "a11y: Add countdown when hovering on exit" into main

parents 5e848712 af17ef56
Loading
Loading
Loading
Loading
+47 −7
Original line number Diff line number Diff line
@@ -25,6 +25,7 @@ import static android.view.accessibility.AccessibilityManager.AUTOCLICK_REVERT_T

import static com.android.server.accessibility.autoclick.AutoclickIndicatorView.SHOW_INDICATOR_DELAY_TIME;
import static com.android.server.accessibility.autoclick.AutoclickTypePanel.AUTOCLICK_TYPE_DOUBLE_CLICK;
import static com.android.server.accessibility.autoclick.AutoclickScrollPanel.DIRECTION_NONE;
import static com.android.server.accessibility.autoclick.AutoclickTypePanel.AUTOCLICK_TYPE_LEFT_CLICK;
import static com.android.server.accessibility.autoclick.AutoclickTypePanel.AUTOCLICK_TYPE_RIGHT_CLICK;
import static com.android.server.accessibility.autoclick.AutoclickTypePanel.AUTOCLICK_TYPE_SCROLL;
@@ -97,6 +98,9 @@ public class AutoclickController extends BaseEventStreamTransformation {
    // Default click type is left-click.
    private @AutoclickType int mActiveClickType = AUTOCLICK_TYPE_LEFT_CLICK;

    // Default scroll direction is DIRECTION_NONE.
    private @AutoclickScrollPanel.ScrollDirection int mHoveredDirection = DIRECTION_NONE;

    @VisibleForTesting
    final ClickPanelControllerInterface clickPanelController =
            new ClickPanelControllerInterface() {
@@ -131,14 +135,26 @@ public class AutoclickController extends BaseEventStreamTransformation {
    final AutoclickScrollPanel.ScrollPanelControllerInterface mScrollPanelController =
            new AutoclickScrollPanel.ScrollPanelControllerInterface() {
                @Override
                public void handleScroll(@AutoclickScrollPanel.ScrollDirection int direction) {
                    // TODO(b/388845721): Perform actual scroll.
                public void onHoverButtonChange(
                        @AutoclickScrollPanel.ScrollDirection int direction,
                        boolean hovered) {
                    // Update the hover direction.
                    if (hovered) {
                        mHoveredDirection = direction;
                    } else if (mHoveredDirection == direction) {
                        // Safety check: Only clear hover tracking if this is the same button
                        // we're currently tracking.
                        mHoveredDirection = AutoclickScrollPanel.DIRECTION_NONE;
                    }

                    // For exit button, we only trigger hover state changes, the autoclick system
                    // will handle the countdown.
                    if (direction == AutoclickScrollPanel.DIRECTION_EXIT) {
                        return;
                    }

                @Override
                public void exitScrollMode() {
                    if (mAutoclickScrollPanel != null) {
                        mAutoclickScrollPanel.hide();
                    // For direction buttons, perform scroll action immediately.
                    if (hovered && direction != AutoclickScrollPanel.DIRECTION_NONE) {
                        handleScroll(direction);
                    }
                }
            };
@@ -285,6 +301,22 @@ public class AutoclickController extends BaseEventStreamTransformation {
        }
    }

    /**
     * Handles scroll operations in the specified direction.
     */
    public void handleScroll(@AutoclickScrollPanel.ScrollDirection int direction) {
        // TODO(b/388845721): Perform actual scroll.
    }

    /**
     * Exits scroll mode and hides the scroll panel UI.
     */
    public void exitScrollMode() {
        if (mAutoclickScrollPanel != null) {
            mAutoclickScrollPanel.hide();
        }
    }

    @VisibleForTesting
    void onChangeForTesting(boolean selfChange, Uri uri) {
        mAutoclickSettingsObserver.onChange(selfChange, uri);
@@ -776,6 +808,14 @@ public class AutoclickController extends BaseEventStreamTransformation {
                return;
            }

            if (mAutoclickScrollPanel != null && mAutoclickScrollPanel.isVisible()) {
                // If exit button is hovered, exit scroll mode after countdown and return early.
                if (mHoveredDirection == AutoclickScrollPanel.DIRECTION_EXIT) {
                    exitScrollMode();
                }
                return;
            }

            // Handle scroll type specially, show scroll panel instead of sending click events.
            if (mActiveClickType == AutoclickTypePanel.AUTOCLICK_TYPE_SCROLL) {
                if (mAutoclickScrollPanel != null) {
+44 −27
Original line number Diff line number Diff line
@@ -23,6 +23,7 @@ import android.content.Context;
import android.graphics.PixelFormat;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowInsets;
import android.view.WindowManager;
@@ -41,12 +42,16 @@ public class AutoclickScrollPanel {
    public static final int DIRECTION_DOWN = 1;
    public static final int DIRECTION_LEFT = 2;
    public static final int DIRECTION_RIGHT = 3;
    public static final int DIRECTION_EXIT = 4;
    public static final int DIRECTION_NONE = 5;

    @IntDef({
            DIRECTION_UP,
            DIRECTION_DOWN,
            DIRECTION_LEFT,
            DIRECTION_RIGHT
            DIRECTION_RIGHT,
            DIRECTION_EXIT,
            DIRECTION_NONE,
    })
    @Retention(RetentionPolicy.SOURCE)
    public @interface ScrollDirection {}
@@ -70,16 +75,12 @@ public class AutoclickScrollPanel {
     */
    public interface ScrollPanelControllerInterface {
        /**
         * Called when a scroll direction is hovered.
         * Called when a button hover state changes.
         *
         * @param direction The direction to scroll: one of {@link ScrollDirection} values.
         * @param direction The direction associated with the button.
         * @param hovered Whether the button is being hovered or not.
         */
        void handleScroll(@ScrollDirection int direction);

        /**
         * Called when the exit button is hovered.
         */
        void exitScrollMode();
        void onHoverButtonChange(@ScrollDirection int direction, boolean hovered);
    }

    public AutoclickScrollPanel(Context context, WindowManager windowManager,
@@ -104,19 +105,12 @@ public class AutoclickScrollPanel {
     * Sets up hover listeners for scroll panel buttons.
     */
    private void initializeButtonState() {
        // Set up hover listeners for direction buttons.
        setupHoverListenerForDirectionButton(mUpButton, DIRECTION_UP);
        setupHoverListenerForDirectionButton(mLeftButton, DIRECTION_LEFT);
        setupHoverListenerForDirectionButton(mRightButton, DIRECTION_RIGHT);
        setupHoverListenerForDirectionButton(mDownButton, DIRECTION_DOWN);

        // Set up hover listener for exit button.
        mExitButton.setOnHoverListener((v, event) -> {
            if (mScrollPanelController != null) {
                mScrollPanelController.exitScrollMode();
            }
            return true;
        });
        // Set up hover listeners for all buttons.
        setupHoverListenerForButton(mUpButton, DIRECTION_UP);
        setupHoverListenerForButton(mLeftButton, DIRECTION_LEFT);
        setupHoverListenerForButton(mRightButton, DIRECTION_RIGHT);
        setupHoverListenerForButton(mDownButton, DIRECTION_DOWN);
        setupHoverListenerForButton(mExitButton, DIRECTION_EXIT);
    }

    /**
@@ -142,14 +136,37 @@ public class AutoclickScrollPanel {
    }

    /**
     * Sets up a hover listener for a direction button.
     * Sets up a hover listener for a button.
     */
    private void setupHoverListenerForDirectionButton(ImageButton button,
            @ScrollDirection int direction) {
    private void setupHoverListenerForButton(ImageButton button, @ScrollDirection int direction) {
        button.setOnHoverListener((v, event) -> {
            if (mScrollPanelController != null) {
                mScrollPanelController.handleScroll(direction);
            if (mScrollPanelController == null) {
                return true;
            }

            boolean hovered;
            switch (event.getAction()) {
                case MotionEvent.ACTION_HOVER_ENTER:
                    hovered = true;
                    break;
                case MotionEvent.ACTION_HOVER_MOVE:
                    // For direction buttons, continuously trigger scroll on hover move.
                    if (direction != DIRECTION_EXIT) {
                        hovered = true;
                    } else {
                        // Ignore hover move events for exit button.
                        return true;
                    }
                    break;
                case MotionEvent.ACTION_HOVER_EXIT:
                    hovered = false;
                    break;
                default:
                    return true;
            }

            // Notify the controller about the hover change.
            mScrollPanelController.onHoverButtonChange(direction, hovered);
            return true;
        });
    }
+96 −22
Original line number Diff line number Diff line
@@ -21,8 +21,12 @@ import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentat
import static com.google.common.truth.Truth.assertThat;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.never;

import android.content.Context;
import android.testing.AndroidTestingRunner;
@@ -125,37 +129,107 @@ public class AutoclickScrollPanelTest {
    }

    @Test
    public void directionButtons_onHover_callsHandleScroll() {
        // Test up button.
        triggerHoverEvent(mUpButton);
        verify(mMockScrollPanelController).handleScroll(AutoclickScrollPanel.DIRECTION_UP);

        // Test down button.
        triggerHoverEvent(mDownButton);
        verify(mMockScrollPanelController).handleScroll(AutoclickScrollPanel.DIRECTION_DOWN);

        // Test left button.
        triggerHoverEvent(mLeftButton);
        verify(mMockScrollPanelController).handleScroll(AutoclickScrollPanel.DIRECTION_LEFT);

        // Test right button.
        triggerHoverEvent(mRightButton);
        verify(mMockScrollPanelController).handleScroll(AutoclickScrollPanel.DIRECTION_RIGHT);
    public void directionButtons_hoverEvents_callsHoverButtonChange() {
        // Test hover enter on direction button.
        triggerHoverEvent(mUpButton, MotionEvent.ACTION_HOVER_ENTER);
        verify(mMockScrollPanelController).onHoverButtonChange(
                eq(AutoclickScrollPanel.DIRECTION_UP), eq(/* hovered= */ true));

        // Test hover move.
        reset(mMockScrollPanelController);
        triggerHoverEvent(mUpButton, MotionEvent.ACTION_HOVER_MOVE);
        verify(mMockScrollPanelController).onHoverButtonChange(
                eq(AutoclickScrollPanel.DIRECTION_UP), eq(/* hovered= */ true));

        // Test hover exit.
        reset(mMockScrollPanelController);
        triggerHoverEvent(mUpButton, MotionEvent.ACTION_HOVER_EXIT);
        verify(mMockScrollPanelController).onHoverButtonChange(
                eq(AutoclickScrollPanel.DIRECTION_UP), eq(/* hovered= */ false));
    }

    @Test
    public void exitButton_onHover_callsExitScrollMode() {
        // Test exit button.
        triggerHoverEvent(mExitButton);
        verify(mMockScrollPanelController).exitScrollMode();
    public void exitButton_hoverEvents_callsHoverButtonChange() {
        // Test hover enter on exit button.
        triggerHoverEvent(mExitButton, MotionEvent.ACTION_HOVER_ENTER);
        verify(mMockScrollPanelController).onHoverButtonChange(
                eq(AutoclickScrollPanel.DIRECTION_EXIT), eq(/* hovered= */ true));

        // Test hover exit - should call the hover change method with false.
        reset(mMockScrollPanelController);
        triggerHoverEvent(mExitButton, MotionEvent.ACTION_HOVER_EXIT);
        verify(mMockScrollPanelController).onHoverButtonChange(
                eq(AutoclickScrollPanel.DIRECTION_EXIT), eq(/* hovered= */ false));

        // Test exit button hover move - should be ignored.
        reset(mMockScrollPanelController);
        triggerHoverEvent(mExitButton, MotionEvent.ACTION_HOVER_MOVE);
        verify(mMockScrollPanelController, never()).onHoverButtonChange(
                eq(AutoclickScrollPanel.DIRECTION_EXIT), anyBoolean());
    }

    @Test
    public void hoverOnButtonSequence_handledCorrectly() {
        // Test a realistic sequence of events.
        // Case 1. Hover enter on up button, then hover move with in up button twice.
        reset(mMockScrollPanelController);
        triggerHoverEvent(mUpButton, MotionEvent.ACTION_HOVER_ENTER);
        triggerHoverEvent(mUpButton, MotionEvent.ACTION_HOVER_MOVE);
        triggerHoverEvent(mUpButton, MotionEvent.ACTION_HOVER_MOVE);
        verify(mMockScrollPanelController, times(3)).onHoverButtonChange(
                eq(AutoclickScrollPanel.DIRECTION_UP), eq(true));

        // Case 2. Move from left button to exit button.
        reset(mMockScrollPanelController);
        triggerHoverEvent(mLeftButton, MotionEvent.ACTION_HOVER_ENTER);
        triggerHoverEvent(mLeftButton, MotionEvent.ACTION_HOVER_MOVE);
        triggerHoverEvent(mLeftButton, MotionEvent.ACTION_HOVER_EXIT);
        triggerHoverEvent(mExitButton, MotionEvent.ACTION_HOVER_MOVE);
        triggerHoverEvent(mExitButton, MotionEvent.ACTION_HOVER_ENTER);
        triggerHoverEvent(mExitButton, MotionEvent.ACTION_HOVER_EXIT);

        // Verify left button events - 2 'true' calls (enter+move) and 1 'false' call (exit).
        verify(mMockScrollPanelController, times(2)).onHoverButtonChange(
                eq(AutoclickScrollPanel.DIRECTION_LEFT), eq(/* hovered= */ true));
        verify(mMockScrollPanelController).onHoverButtonChange(
                eq(AutoclickScrollPanel.DIRECTION_LEFT), eq(/* hovered= */ false));
        // Verify exit button events - hover_move is ignored so 1 'true' call (enter) and 1
        // 'false' call (exit).
        verify(mMockScrollPanelController).onHoverButtonChange(
                eq(AutoclickScrollPanel.DIRECTION_EXIT), eq(/* hovered= */ true));
        verify(mMockScrollPanelController).onHoverButtonChange(
                eq(AutoclickScrollPanel.DIRECTION_EXIT), eq(/* hovered= */ false));

        // Case 3. Quick transitions between buttons: left → right → down → exit
        reset(mMockScrollPanelController);
        triggerHoverEvent(mLeftButton, MotionEvent.ACTION_HOVER_EXIT);
        triggerHoverEvent(mRightButton, MotionEvent.ACTION_HOVER_ENTER);
        triggerHoverEvent(mRightButton, MotionEvent.ACTION_HOVER_EXIT);
        triggerHoverEvent(mDownButton, MotionEvent.ACTION_HOVER_ENTER);
        triggerHoverEvent(mDownButton, MotionEvent.ACTION_HOVER_EXIT);
        triggerHoverEvent(mExitButton, MotionEvent.ACTION_HOVER_ENTER);

        // Verify all hover enter/exit events were properly handled
        verify(mMockScrollPanelController).onHoverButtonChange(
                eq(AutoclickScrollPanel.DIRECTION_LEFT), eq(/* hovered= */ false));
        verify(mMockScrollPanelController).onHoverButtonChange(
                eq(AutoclickScrollPanel.DIRECTION_RIGHT), eq(/* hovered= */ true));
        verify(mMockScrollPanelController).onHoverButtonChange(
                eq(AutoclickScrollPanel.DIRECTION_RIGHT), eq(/* hovered= */ false));
        verify(mMockScrollPanelController).onHoverButtonChange(
                eq(AutoclickScrollPanel.DIRECTION_DOWN), eq(/* hovered= */ true));
        verify(mMockScrollPanelController).onHoverButtonChange(
                eq(AutoclickScrollPanel.DIRECTION_DOWN), eq(/* hovered= */ false));
        verify(mMockScrollPanelController).onHoverButtonChange(
                eq(AutoclickScrollPanel.DIRECTION_EXIT), eq(/* hovered= */ true));
    }

    // Helper method to simulate a hover event on a view.
    private void triggerHoverEvent(View view) {
    private void triggerHoverEvent(View view, int action) {
        MotionEvent event = MotionEvent.obtain(
                /* downTime= */ 0,
                /* eventTime= */ 0,
                /* action= */ MotionEvent.ACTION_HOVER_ENTER,
                /* action= */ action,
                /* x= */ 0,
                /* y= */ 0,
                /* metaState= */ 0);