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

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

Merge "a11y: Prevent scroll position update when hovered on panels" into main

parents df85cadb d6a8e075
Loading
Loading
Loading
Loading
+2 −2
Original line number Diff line number Diff line
@@ -14,7 +14,7 @@
     limitations under the License.
-->

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
<com.android.server.accessibility.autoclick.AutoclickLinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/accessibility_autoclick_scroll_panel"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
@@ -89,4 +89,4 @@
            android:src="@drawable/accessibility_autoclick_scroll_down" />
    </LinearLayout>

</LinearLayout>
</com.android.server.accessibility.autoclick.AutoclickLinearLayout>
+55 −37
Original line number Diff line number Diff line
@@ -44,7 +44,6 @@ import android.net.Uri;
import android.os.Handler;
import android.os.SystemClock;
import android.provider.Settings;
import android.util.Slog;
import android.view.InputDevice;
import android.view.KeyEvent;
import android.view.MotionEvent;
@@ -93,10 +92,11 @@ public class AutoclickController extends BaseEventStreamTransformation {
    private final AccessibilityTraceManager mTrace;
    private final Context mContext;
    private final int mUserId;
    // The position that scroll actual happens.
    @VisibleForTesting
    float mLastCursorX;
    float mScrollCursorX;
    @VisibleForTesting
    float mLastCursorY;
    float mScrollCursorY;

    // Lazily created on the first mouse motion event.
    @VisibleForTesting ClickScheduler mClickScheduler;
@@ -351,11 +351,11 @@ public class AutoclickController extends BaseEventStreamTransformation {
        pointerProps[0].id = 0;
        pointerProps[0].toolType = MotionEvent.TOOL_TYPE_MOUSE;

        // Create pointer coordinates at the last cursor position.
        // Create pointer coordinates at the scroll cursor position.
        PointerCoords[] pointerCoords = new PointerCoords[1];
        pointerCoords[0] = new PointerCoords();
        pointerCoords[0].x = mLastCursorX;
        pointerCoords[0].y = mLastCursorY;
        pointerCoords[0].x = mScrollCursorX;
        pointerCoords[0].y = mScrollCursorY;

        // Set scroll values based on direction.
        switch (direction) {
@@ -928,37 +928,8 @@ 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) {
                    // Save the last cursor position at the moment when sendClick() is called.
                    if (mClickScheduler != null && mClickScheduler.mLastMotionEvent != null) {
                        final int pointerIndex = mClickScheduler.mLastMotionEvent.getActionIndex();
                        mLastCursorX = mClickScheduler.mLastMotionEvent.getX(pointerIndex);
                        mLastCursorY = mClickScheduler.mLastMotionEvent.getY(pointerIndex);

                        // Remove previous scroll panel if exists.
                        if (mAutoclickScrollPanel.isVisible()) {
                            mAutoclickScrollPanel.hide();
                        }
                        // Show scroll panel at the cursor position.
                        mAutoclickScrollPanel.show(mLastCursorX, mLastCursorY);
                    } else {
                        // Fallback: Show scroll panel at its default position (center of screen).
                        Slog.w(LOG_TAG,
                                "Showing scroll panel at default position - no cursor position "
                                        + "available");
                        mAutoclickScrollPanel.show();
                    }
                }
            // Handle scroll-specific click behavior.
            if (handleScrollClick()) {
                return;
            }

@@ -1015,6 +986,53 @@ public class AutoclickController extends BaseEventStreamTransformation {
            sendMotionEvent(actionButton, now);
        }

        /**
         * Handles scroll-specific click behavior when autoclick is triggered.
         *
         * @return true if scroll handling was performed (no further click processing needed),
         * false if regular click processing should continue.
         */
        private boolean handleScrollClick() {
            // Only handle scroll type clicks.
            if (mActiveClickType != AutoclickTypePanel.AUTOCLICK_TYPE_SCROLL
                    || mAutoclickScrollPanel == null) {
                return false;
            }

            // Trigger left click instead of scroll when hovering over type panel.
            if (mAutoclickTypePanel.isHovered()) {
                return false;
            }

            boolean isPanelVisible = mAutoclickScrollPanel.isVisible();
            boolean isPanelHovered = mAutoclickScrollPanel.isHovered();

            // Handle exit button hover case.
            if (isPanelVisible && mHoveredDirection == AutoclickScrollPanel.DIRECTION_EXIT) {
                exitScrollMode();
                return true;
            }

            // Update cursor position when not hovering over panels.
            if (!isPanelHovered) {
                final int pointerIndex = mLastMotionEvent.getActionIndex();
                mScrollCursorX = mLastMotionEvent.getX(pointerIndex);
                mScrollCursorY = mLastMotionEvent.getY(pointerIndex);
            }

            // Show or reposition panel.
            if (isPanelVisible && !isPanelHovered) {
                // Reposition panel when cursor is outside the panel.
                mAutoclickScrollPanel.hide();
                mAutoclickScrollPanel.show(mScrollCursorX, mScrollCursorY);
            } else if (!isPanelVisible) {
                // First time showing the panel.
                mAutoclickScrollPanel.show(mScrollCursorX, mScrollCursorY);
            }

            return true;
        }

        private void sendMotionEvent(int actionButton, long eventTime) {
            MotionEvent downEvent =
                    MotionEvent.obtain(
+8 −3
Original line number Diff line number Diff line
@@ -62,7 +62,7 @@ public class AutoclickScrollPanel {
    public @interface ScrollDirection {}

    private final Context mContext;
    private final View mContentView;
    private final AutoclickLinearLayout mContentView;
    private final WindowManager mWindowManager;
    private final WindowManager.LayoutParams mParams;
    private ScrollPanelControllerInterface mScrollPanelController;
@@ -98,7 +98,7 @@ public class AutoclickScrollPanel {
        mContext = context;
        mWindowManager = windowManager;
        mScrollPanelController = controller;
        mContentView = LayoutInflater.from(context).inflate(
        mContentView = (AutoclickLinearLayout) LayoutInflater.from(context).inflate(
                R.layout.accessibility_autoclick_scroll_panel, null);
        mParams = getDefaultLayoutParams();

@@ -266,7 +266,12 @@ public class AutoclickScrollPanel {
    }

    @VisibleForTesting
    public View getContentViewForTesting() {
    public boolean isHovered() {
        return mContentView.isHovered();
    }

    @VisibleForTesting
    public AutoclickLinearLayout getContentViewForTesting() {
        return mContentView;
    }

+90 −4
Original line number Diff line number Diff line
@@ -820,8 +820,8 @@ public class AutoclickControllerTest {
        // Set cursor position.
        float expectedX = 75f;
        float expectedY = 125f;
        mController.mLastCursorX = expectedX;
        mController.mLastCursorY = expectedY;
        mController.mScrollCursorX = expectedX;
        mController.mScrollCursorY = expectedY;

        // Trigger scroll action in up direction.
        mController.mScrollPanelController.onHoverButtonChange(
@@ -845,8 +845,8 @@ public class AutoclickControllerTest {
        // Set cursor position.
        final float expectedX = 100f;
        final float expectedY = 200f;
        mController.mLastCursorX = expectedX;
        mController.mLastCursorY = expectedY;
        mController.mScrollCursorX = expectedX;
        mController.mScrollCursorY = expectedY;

        // Test UP direction.
        mController.mScrollPanelController.onHoverButtonChange(
@@ -903,6 +903,92 @@ public class AutoclickControllerTest {
        assertThat(scrollCaptor.scrollEvent.getY()).isEqualTo(expectedY);
    }

    @Test
    @EnableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_AUTOCLICK_INDICATOR)
    public void scrollCursor_maintainsScrollPositionWhenPanelHovered() {
        ScrollEventCaptor scrollCaptor = new ScrollEventCaptor();
        mController.setNext(scrollCaptor);

        // Initialize controller.
        injectFakeMouseActionHoverMoveEvent();
        mController.mClickScheduler.updateDelay(0);

        // Set click type to scroll.
        mController.clickPanelController.handleAutoclickTypeChange(
                AutoclickTypePanel.AUTOCLICK_TYPE_SCROLL);

        // Set cursor position.
        float initialX = 100f;
        float initialY = 200f;
        mController.mScrollCursorX = initialX;
        mController.mScrollCursorY = initialY;

        // Create mock panel that is hovered.
        AutoclickScrollPanel mockScrollPanel = mock(AutoclickScrollPanel.class);
        when(mockScrollPanel.isHovered()).thenReturn(true);
        when(mockScrollPanel.isVisible()).thenReturn(true);
        mController.mAutoclickScrollPanel = mockScrollPanel;

        // Move cursor to panel position.
        float newX = 300f;
        float newY = 400f;
        injectFakeMouseMoveEvent(newX, newY, MotionEvent.ACTION_HOVER_MOVE);
        mController.mClickScheduler.updateDelay(0);
        mTestableLooper.processAllMessages();

        // Trigger scroll action in up direction.
        mController.mScrollPanelController.onHoverButtonChange(
                AutoclickScrollPanel.DIRECTION_UP, true);

        // Verify scroll event still happens at the original position instead of new location.
        assertThat(scrollCaptor.scrollEvent).isNotNull();
        assertThat(scrollCaptor.scrollEvent.getX()).isEqualTo(initialX);
        assertThat(scrollCaptor.scrollEvent.getY()).isEqualTo(initialY);
    }

    @Test
    @EnableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_AUTOCLICK_INDICATOR)
    public void scrollCursor_updateScrollPositionWhenPanelNotHovered() {
        ScrollEventCaptor scrollCaptor = new ScrollEventCaptor();
        mController.setNext(scrollCaptor);

        // Initialize controller.
        injectFakeMouseActionHoverMoveEvent();
        mController.mClickScheduler.updateDelay(0);

        // Set click type to scroll.
        mController.clickPanelController.handleAutoclickTypeChange(
                AutoclickTypePanel.AUTOCLICK_TYPE_SCROLL);

        // Set cursor position.
        float initialX = 100f;
        float initialY = 200f;
        mController.mScrollCursorX = initialX;
        mController.mScrollCursorY = initialY;

        // Create mock panel that is not hovered.
        AutoclickScrollPanel mockScrollPanel = mock(AutoclickScrollPanel.class);
        when(mockScrollPanel.isHovered()).thenReturn(false);
        when(mockScrollPanel.isVisible()).thenReturn(true);
        mController.mAutoclickScrollPanel = mockScrollPanel;

        // Move cursor to new position.
        float newX = 300f;
        float newY = 400f;
        injectFakeMouseMoveEvent(newX, newY, MotionEvent.ACTION_HOVER_MOVE);
        mController.mClickScheduler.updateDelay(0);
        mTestableLooper.processAllMessages();

        // Trigger scroll action in up direction.
        mController.mScrollPanelController.onHoverButtonChange(
                AutoclickScrollPanel.DIRECTION_UP, true);

        // Verify scroll event happens at the new position.
        assertThat(scrollCaptor.scrollEvent).isNotNull();
        assertThat(scrollCaptor.scrollEvent.getX()).isEqualTo(newX);
        assertThat(scrollCaptor.scrollEvent.getY()).isEqualTo(newY);
    }

    @Test
    @EnableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_AUTOCLICK_INDICATOR)
    public void sendClick_clickType_doubleclick_triggerClickTwice() {