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

Commit d6a8e075 authored by Longbo Wei's avatar Longbo Wei
Browse files

a11y: Prevent scroll position update when hovered on panels

Problem: When the cursor hovers over scrollable content, it scrolls as
expected. However, if the cursor moves to the scroll panel, sendClick
updates the scrollPanel position to be (mLastCursorX, mLastCursorY) from
mLastMotion. Ideally, the scroll position should only be updated when
cursor is not on the panel.

Fix: Add mScrollCursorX, mScrollCursorY and perform scroll at
(mScrollCursorX, mScrollCursorY) instead of (mLastCursorX,
mLastCursorY), meanwhile, only update mScrollCursorX, mScrollCursorY
when panels is not hovered.

Note: To detect hovering, built-in function mContentView.isHovered() is
used, and same to this CL - http://ag/31887076

"When the panel is a standard LinearLayout,the View api
setOnHoverListener() does not work for this use case becasuse of the
child button elements. When the button elements become hovered, the
LinearLayout panel considers that as an "ACTION_HOVER_EXIT" even though
the button is actually inside the panel. "

Therefore, to properly use the mContentView.isHovered(), This CL updates
to use com.android.server.accessibility.autoclick.AutoclickLinearLayout.

Video:
 - Before: http://shortn/_yf42nN9HAX
 - After:  http://shortn/_Mb3c7HaY2g

Bug: b/406326893
Test: AutoclickControllerTest
Flag: com.android.server.accessibility.enable_autoclick_indicator
Change-Id: I2d3e5b1b9fff0578019d9d97edc5d57d99a2c1d6
parent 6e602138
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
@@ -42,7 +42,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;
@@ -88,10 +87,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;
@@ -331,11 +331,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) {
@@ -876,37 +876,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;
            }

@@ -956,6 +927,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
@@ -797,8 +797,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(
@@ -822,8 +822,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(
@@ -880,6 +880,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() {