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

Commit 7339b85f authored by “Longbo's avatar “Longbo
Browse files

a11y: Allow continuously scroll

Video: http://shortn/_mELXo72oHV

Bug: b/409650027
Test: AutoclickScrollPointIndicatorTest
Flag: com.android.server.accessibility.enable_autoclick_indicator
Change-Id: I162f0fd0f8f818e1fddfa787d3e81ef4969678f9
parent 5f7fe743
Loading
Loading
Loading
Loading
+58 −24
Original line number Diff line number Diff line
@@ -92,8 +92,10 @@ public class AutoclickController extends BaseEventStreamTransformation {
            (long) (ViewConfiguration.getLongPressTimeout() * 1.5f);

    private static final String LOG_TAG = AutoclickController.class.getSimpleName();
    // TODO(b/393559560): Finalize scroll amount.
    private static final float SCROLL_AMOUNT = 1.0f;
    private static final float SCROLL_AMOUNT = 0.5f;
    protected static final long CONTINUOUS_SCROLL_INTERVAL = 30;
    private Handler mContinuousScrollHandler;
    private Runnable mContinuousScrollRunnable;

    private final AccessibilityTraceManager mTrace;
    private final Context mContext;
@@ -123,7 +125,8 @@ public class AutoclickController extends BaseEventStreamTransformation {
    private @AutoclickType int mActiveClickType = AUTOCLICK_TYPE_LEFT_CLICK;

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

    // True during the duration of a dragging event.
    private boolean mDragModeIsDragging = false;
@@ -181,31 +184,22 @@ public class AutoclickController extends BaseEventStreamTransformation {
                    // 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.
                        // For exit button, return early and the autoclick system will handle the
                        // countdown then exit scroll mode.
                        if (direction == AutoclickScrollPanel.DIRECTION_EXIT) {
                            return;
                        }

                    // Handle all non-exit buttons when hovered.
                    if (hovered) {
                        // Clear the indicator.
                        if (mAutoclickIndicatorScheduler != null) {
                            mAutoclickIndicatorScheduler.cancel();
                            if (mAutoclickIndicatorView != null) {
                                mAutoclickIndicatorView.clearIndicator();
                            }
                        }
                        // Perform scroll action.
                        // For scroll directions, start continuous scrolling.
                        if (direction != DIRECTION_NONE) {
                            handleScroll(direction);
                            startContinuousScroll(direction);
                        }
                    } else if (mHoveredDirection == direction) {
                        // If not hovered, stop scrolling — but only if the mouse leaves the same
                        // button that started it. This avoids stopping the scroll when the mouse
                        // briefly moves over other buttons.
                        stopContinuousScroll();
                    }
                }
            };
@@ -267,6 +261,16 @@ public class AutoclickController extends BaseEventStreamTransformation {
        mAutoclickScrollPanel = new AutoclickScrollPanel(mContext, mWindowManager,
                mScrollPanelController);

        // Initialize continuous scroll handler and runnable.
        mContinuousScrollHandler = new Handler(handler.getLooper());
        mContinuousScrollRunnable = new Runnable() {
            @Override
            public void run() {
                handleScroll(mHoveredDirection);
                mContinuousScrollHandler.postDelayed(this, CONTINUOUS_SCROLL_INTERVAL);
            }
        };

        mAutoclickTypePanel.show();
        mWindowManager.addView(mAutoclickIndicatorView, mAutoclickIndicatorView.getLayoutParams());
    }
@@ -323,6 +327,11 @@ public class AutoclickController extends BaseEventStreamTransformation {
            mAutoclickScrollPanel.hide();
            mAutoclickScrollPanel = null;
        }

        if (mContinuousScrollHandler != null) {
            mContinuousScrollHandler.removeCallbacks(mContinuousScrollRunnable);
            mContinuousScrollHandler = null;
        }
    }

    private void scheduleClick(MotionEvent event, int policyFlags) {
@@ -366,6 +375,14 @@ public class AutoclickController extends BaseEventStreamTransformation {
     * Handles scroll operations in the specified direction.
     */
    private void handleScroll(@AutoclickScrollPanel.ScrollDirection int direction) {
        // Remove the autoclick indicator view when hovering on directional buttons.
        if (mAutoclickIndicatorScheduler != null) {
            mAutoclickIndicatorScheduler.cancel();
            if (mAutoclickIndicatorView != null) {
                mAutoclickIndicatorView.clearIndicator();
            }
        }

        final long now = SystemClock.uptimeMillis();

        // Create pointer properties.
@@ -426,8 +443,25 @@ public class AutoclickController extends BaseEventStreamTransformation {
        if (mAutoclickScrollPanel != null) {
            mAutoclickScrollPanel.hide();
        }
        stopContinuousScroll();
    }

    private void startContinuousScroll(@AutoclickScrollPanel.ScrollDirection int direction) {
        if (mContinuousScrollHandler != null) {
            handleScroll(direction);
            mContinuousScrollHandler.postDelayed(mContinuousScrollRunnable,
                    CONTINUOUS_SCROLL_INTERVAL);
        }
    }

    private void stopContinuousScroll() {
        if (mContinuousScrollHandler != null) {
            mContinuousScrollHandler.removeCallbacks(mContinuousScrollRunnable);
        }
        mHoveredDirection = DIRECTION_NONE;
    }


    @VisibleForTesting
    void onChangeForTesting(boolean selfChange, Uri uri) {
        mAutoclickSettingsObserver.onChange(selfChange, uri);
+0 −9
Original line number Diff line number Diff line
@@ -227,15 +227,6 @@ public class AutoclickScrollPanel {
                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;
+65 −4
Original line number Diff line number Diff line
@@ -18,6 +18,7 @@ package com.android.server.accessibility.autoclick;

import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;

import static com.android.server.accessibility.autoclick.AutoclickController.CONTINUOUS_SCROLL_INTERVAL;
import static com.android.server.accessibility.autoclick.AutoclickTypePanel.AUTOCLICK_TYPE_RIGHT_CLICK;
import static com.android.server.testutils.MockitoUtilsKt.eq;

@@ -67,16 +68,20 @@ import java.util.List;
@TestableLooper.RunWithLooper(setAsMainLooper = true)
public class AutoclickControllerTest {

    @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
    @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule();
    @Rule
    public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
    @Rule
    public final MockitoRule mMockitoRule = MockitoJUnit.rule();

    @Rule
    public TestableContext mTestableContext =
            new TestableContext(getInstrumentation().getContext());

    private TestableLooper mTestableLooper;
    @Mock private AccessibilityTraceManager mMockTrace;
    @Mock private WindowManager mMockWindowManager;
    @Mock
    private AccessibilityTraceManager mMockTrace;
    @Mock
    private WindowManager mMockWindowManager;
    private AutoclickController mController;
    private MotionEventCaptor mMotionEventCaptor;

@@ -1271,6 +1276,62 @@ public class AutoclickControllerTest {
        assertThat(mController.hasOngoingLongPressForTesting()).isFalse();
    }

    @Test
    @EnableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_AUTOCLICK_INDICATOR)
    public void continuousScroll_completeLifecycle() {
        // Set up event capturer to track scroll events.
        ScrollEventCaptor scrollCaptor = new ScrollEventCaptor();
        mController.setNext(scrollCaptor);

        // Initialize controller.
        injectFakeMouseActionHoverMoveEvent();

        // Set cursor position.
        float expectedX = 100f;
        float expectedY = 200f;
        mController.mScrollCursorX = expectedX;
        mController.mScrollCursorY = expectedY;

        // Start scrolling by hovering UP button.
        mController.mScrollPanelController.onHoverButtonChange(
                AutoclickScrollPanel.DIRECTION_UP, true);

        // Verify initial hover state and event.
        assertThat(mController.mHoveredDirection).isEqualTo(AutoclickScrollPanel.DIRECTION_UP);
        assertThat(scrollCaptor.eventCount).isEqualTo(1);
        assertThat(scrollCaptor.scrollEvent.getAxisValue(MotionEvent.AXIS_VSCROLL)).isGreaterThan(
                0);

        // Simulate continuous scrolling by triggering runnable.
        scrollCaptor.eventCount = 0;

        // Advance time by CONTINUOUS_SCROLL_INTERVAL (30ms) and process messages.
        mTestableLooper.moveTimeForward(CONTINUOUS_SCROLL_INTERVAL);
        mTestableLooper.processAllMessages();

        // Advance time again to trigger second scroll event.
        mTestableLooper.moveTimeForward(CONTINUOUS_SCROLL_INTERVAL);
        mTestableLooper.processAllMessages();

        // Verify multiple scroll events were generated.
        assertThat(scrollCaptor.eventCount).isEqualTo(2);
        assertThat(scrollCaptor.scrollEvent.getX()).isEqualTo(expectedX);
        assertThat(scrollCaptor.scrollEvent.getY()).isEqualTo(expectedY);

        // Stop scrolling by un-hovering the button.
        mController.mScrollPanelController.onHoverButtonChange(
                AutoclickScrollPanel.DIRECTION_UP, false);

        // Verify direction is reset.
        assertThat(mController.mHoveredDirection).isEqualTo(AutoclickScrollPanel.DIRECTION_NONE);

        // Verify no more scroll events are generated after stopping.
        int countBeforeRunnable = scrollCaptor.eventCount;
        mTestableLooper.moveTimeForward(CONTINUOUS_SCROLL_INTERVAL);
        mTestableLooper.processAllMessages();
        assertThat(scrollCaptor.eventCount).isEqualTo(countBeforeRunnable);
    }

    /**
     * =========================================================================
     * Helper Functions
+7 −7
Original line number Diff line number Diff line
@@ -146,7 +146,7 @@ public class AutoclickScrollPanelTest {
        // Test hover move.
        reset(mMockScrollPanelController);
        triggerHoverEvent(mUpButton, MotionEvent.ACTION_HOVER_MOVE);
        verify(mMockScrollPanelController).onHoverButtonChange(
        verify(mMockScrollPanelController, never()).onHoverButtonChange(
                eq(AutoclickScrollPanel.DIRECTION_UP), eq(/* hovered= */ true));

        // Test hover exit.
@@ -184,7 +184,7 @@ public class AutoclickScrollPanelTest {
        triggerHoverEvent(mUpButton, MotionEvent.ACTION_HOVER_ENTER);
        triggerHoverEvent(mUpButton, MotionEvent.ACTION_HOVER_MOVE);
        triggerHoverEvent(mUpButton, MotionEvent.ACTION_HOVER_MOVE);
        verify(mMockScrollPanelController, times(3)).onHoverButtonChange(
        verify(mMockScrollPanelController, times(1)).onHoverButtonChange(
                eq(AutoclickScrollPanel.DIRECTION_UP), eq(true));

        // Case 2. Move from left button to exit button.
@@ -192,17 +192,17 @@ public class AutoclickScrollPanelTest {
        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_MOVE);
        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(
        // Verify left button events - 1 'true' call (enter) and 1 'false' call (exit).
        verify(mMockScrollPanelController, times(1)).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 exit button events - 1 'true' call (enter) and 1 'false' call (exit).
        verify(mMockScrollPanelController).onHoverButtonChange(
                eq(AutoclickScrollPanel.DIRECTION_EXIT), eq(/* hovered= */ true));
        verify(mMockScrollPanelController).onHoverButtonChange(