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

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

Merge "a11y: Handle scrollPanel positioning" into main

parents 043d2389 b4ef31b1
Loading
Loading
Loading
Loading
+14 −1
Original line number Diff line number Diff line
@@ -42,6 +42,7 @@ 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;
@@ -891,9 +892,21 @@ public class AutoclickController extends BaseEventStreamTransformation {
                        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();
                    }
                }
                return;
            }

+71 −4
Original line number Diff line number Diff line
@@ -21,6 +21,7 @@ import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_M
import android.annotation.IntDef;
import android.content.Context;
import android.graphics.PixelFormat;
import android.util.DisplayMetrics;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.MotionEvent;
@@ -45,6 +46,10 @@ public class AutoclickScrollPanel {
    public static final int DIRECTION_EXIT = 4;
    public static final int DIRECTION_NONE = 5;

    // Distance between panel and screen edge.
    // TODO(b/388845721): Finalize edge margin.
    private static final int PANEL_EDGE_MARGIN = 15;

    @IntDef({
            DIRECTION_UP,
            DIRECTION_DOWN,
@@ -59,6 +64,7 @@ public class AutoclickScrollPanel {
    private final Context mContext;
    private final View mContentView;
    private final WindowManager mWindowManager;
    private final WindowManager.LayoutParams mParams;
    private ScrollPanelControllerInterface mScrollPanelController;

    // Scroll panel buttons.
@@ -70,6 +76,10 @@ public class AutoclickScrollPanel {

    private boolean mInScrollMode = false;

    // Panel size determined after measuring.
    private int mPanelWidth;
    private int mPanelHeight;

    /**
     * Interface for handling scroll operations.
     */
@@ -90,6 +100,7 @@ public class AutoclickScrollPanel {
        mScrollPanelController = controller;
        mContentView = LayoutInflater.from(context).inflate(
                R.layout.accessibility_autoclick_scroll_panel, null);
        mParams = getDefaultLayoutParams();

        // Initialize buttons.
        mUpButton = mContentView.findViewById(R.id.scroll_up);
@@ -99,6 +110,13 @@ public class AutoclickScrollPanel {
        mExitButton = mContentView.findViewById(R.id.scroll_exit);

        initializeButtonState();

        // Measure the panel to get its dimensions.
        mContentView.measure(
                View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
                View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
        mPanelWidth = mContentView.getMeasuredWidth();
        mPanelHeight = mContentView.getMeasuredHeight();
    }

    /**
@@ -120,10 +138,60 @@ public class AutoclickScrollPanel {
        if (mInScrollMode) {
            return;
        }
        mWindowManager.addView(mContentView, getLayoutParams());
        mWindowManager.addView(mContentView, mParams);
        mInScrollMode = true;
    }

    /**
     * Shows the autoclick scroll panel positioned at the bottom right of the cursor.
     *
     * @param cursorX The x-coordinate of the cursor.
     * @param cursorY The y-coordinate of the cursor.
     */
    public void show(float cursorX, float cursorY) {
        if (mInScrollMode) {
            return;
        }
        // Position the panel at the cursor location
        positionPanelAtCursor(cursorX, cursorY);
        mWindowManager.addView(mContentView, mParams);
        mInScrollMode = true;
    }

    /**
     * Positions the panel at the bottom right of the cursor coordinates,
     * ensuring it stays within the screen boundaries.
     */
    protected void positionPanelAtCursor(float cursorX, float cursorY) {
        // Set gravity to TOP|LEFT for absolute positioning.
        mParams.gravity = Gravity.LEFT | Gravity.TOP;

        // Get screen dimensions.
        // TODO(b/388845721): Make sure this works on multiple screens.
        DisplayMetrics displayMetrics = mContext.getResources().getDisplayMetrics();
        int screenWidth = displayMetrics.widthPixels;
        int screenHeight = displayMetrics.heightPixels;

        // Calculate initial position.
        int panelX = (int) cursorX;
        int panelY = (int) cursorY;

        // Check if panel would go off right edge of screen.
        if (panelX + mPanelWidth > screenWidth - PANEL_EDGE_MARGIN) {
            // Place to the left of cursor instead if no space left for right edge.
            panelX = (int) cursorX - mPanelWidth;
        }

        // Check if panel would go off bottom edge of screen.
        if (panelY + mPanelHeight > screenHeight - PANEL_EDGE_MARGIN) {
            // Place above cursor instead if no space left for bottom edge.
            panelY = (int) cursorY - mPanelHeight;
        }

        mParams.x = panelX;
        mParams.y = panelY;
    }

    /**
     * Hides the autoclick scroll panel.
     */
@@ -176,7 +244,7 @@ public class AutoclickScrollPanel {
     * Manager.
     */
    @NonNull
    private WindowManager.LayoutParams getLayoutParams() {
    private WindowManager.LayoutParams getDefaultLayoutParams() {
        final WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams();
        layoutParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
        layoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
@@ -189,7 +257,6 @@ public class AutoclickScrollPanel {
                mContext.getString(R.string.accessibility_autoclick_scroll_panel_title);
        layoutParams.width = WindowManager.LayoutParams.WRAP_CONTENT;
        layoutParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
        layoutParams.gravity = Gravity.CENTER;
        return layoutParams;
    }

@@ -205,6 +272,6 @@ public class AutoclickScrollPanel {

    @VisibleForTesting
    public WindowManager.LayoutParams getLayoutParamsForTesting() {
        return getLayoutParams();
        return mParams;
    }
}
+3 −2
Original line number Diff line number Diff line
@@ -24,6 +24,7 @@ import static com.android.server.testutils.MockitoUtilsKt.eq;
import static com.google.common.truth.Truth.assertThat;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyFloat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
@@ -718,7 +719,7 @@ public class AutoclickControllerTest {
        mTestableLooper.processAllMessages();

        // Verify scroll panel is shown once.
        verify(mockScrollPanel, times(1)).show();
        verify(mockScrollPanel, times(1)).show(anyFloat(), anyFloat());
        assertThat(motionEventCaptor.downEvent).isNull();

        // Second significant hover move event to trigger another autoclick.
@@ -726,7 +727,7 @@ public class AutoclickControllerTest {
        mTestableLooper.processAllMessages();

        // Verify scroll panel is still only shown once (not called again).
        verify(mockScrollPanel, times(1)).show();
        verify(mockScrollPanel, times(1)).show(anyFloat(), anyFloat());
        assertThat(motionEventCaptor.downEvent).isNull();
    }

+111 −0
Original line number Diff line number Diff line
@@ -32,6 +32,7 @@ import android.content.Context;
import android.testing.AndroidTestingRunner;
import android.testing.TestableContext;
import android.testing.TestableLooper;
import android.util.DisplayMetrics;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowManager;
@@ -69,6 +70,9 @@ public class AutoclickScrollPanelTest {
    private ImageButton mRightButton;
    private ImageButton mExitButton;

    private int mScreenWidth;
    private int mScreenHeight;

    @Before
    public void setUp() {
        mTestableContext.addMockSystemService(Context.WINDOW_SERVICE, mMockWindowManager);
@@ -83,6 +87,10 @@ public class AutoclickScrollPanelTest {
        mLeftButton = contentView.findViewById(R.id.scroll_left);
        mRightButton = contentView.findViewById(R.id.scroll_right);
        mExitButton = contentView.findViewById(R.id.scroll_exit);

        DisplayMetrics displayMetrics = mTestableContext.getResources().getDisplayMetrics();
        mScreenWidth = displayMetrics.widthPixels;
        mScreenHeight = displayMetrics.heightPixels;
    }

    @Test
@@ -224,6 +232,109 @@ public class AutoclickScrollPanelTest {
                eq(AutoclickScrollPanel.DIRECTION_EXIT), eq(/* hovered= */ true));
    }

    @Test
    public void show_withCursorPosition_addsView() {
        float cursorX = 300;
        float cursorY = 300;

        // Call the new method with cursor coordinates.
        mScrollPanel.show(cursorX, cursorY);

        // Verify view is added to window manager.
        verify(mMockWindowManager).addView(any(), any(WindowManager.LayoutParams.class));

        // Verify panel is visible.
        assertThat(mScrollPanel.isVisible()).isTrue();
    }

    @Test
    public void hideAndReshow_updatesPosition() {
        // First show at one position.
        float firstX = 300;
        float firstY = 300;
        mScrollPanel.show(firstX, firstY);
        assertThat(mScrollPanel.isVisible()).isTrue();

        // Hide panel.
        mScrollPanel.hide();
        assertThat(mScrollPanel.isVisible()).isFalse();

        // Show at different position.
        float secondX = 500;
        float secondY = 500;
        mScrollPanel.show(secondX, secondY);

        // Verify panel is visible.
        assertThat(mScrollPanel.isVisible()).isTrue();

        // Verify view was added twice to window manager.
        verify(mMockWindowManager, times(2)).addView(any(), any(WindowManager.LayoutParams.class));
    }

    @Test
    public void showPanel_normalCase() {
        // Normal case - in the middle of the screen.
        int cursorX = mScreenWidth / 2;
        int cursorY = mScreenHeight / 2;

        // Capture the current layout params before positioning.
        WindowManager.LayoutParams params = mScrollPanel.getLayoutParamsForTesting();
        mScrollPanel.positionPanelAtCursor(cursorX, cursorY);

        // Panel should be at cursor position (gravity is LEFT|TOP).
        assertThat(params.x).isEqualTo(cursorX);
        assertThat(params.y).isEqualTo(cursorY);
    }

    @Test
    public void showPanel_nearRightEdge_positionsLeftOfCursor() {
        // Near right edge case.
        // 100px from right edge.
        int cursorX = mScreenWidth - 10;
        // Center of screen vertically.
        int cursorY = mScreenHeight / 2;

        // Capture the current layout params before positioning.
        WindowManager.LayoutParams params = mScrollPanel.getLayoutParamsForTesting();
        mScrollPanel.positionPanelAtCursor(cursorX, cursorY);

        // Panel should be left of cursor.
        assertThat(params.x).isLessThan(cursorX);
    }

    @Test
    public void showPanel_nearBottomEdge_positionsAboveCursor() {
        // Near bottom edge case.
        // Center of screen horizontally.
        int cursorX = mScreenWidth / 2;
        // 10px from bottom edge.
        int cursorY = mScreenHeight - 10;

        // Capture the current layout params before positioning.
        WindowManager.LayoutParams params = mScrollPanel.getLayoutParamsForTesting();
        mScrollPanel.positionPanelAtCursor(cursorX, cursorY);

        // Panel should be above cursor.
        assertThat(params.y).isLessThan(cursorY);
    }

    @Test
    public void showPanel_nearBottomRightCorner_positionsLeftAndAboveCursor() {
        // Near bottom-right corner case.
        // 10px from right edge.
        int cursorX = mScreenWidth - 10;
        // 10px from bottom edge.
        int cursorY = mScreenHeight - 10;

        // Capture the current layout params before positioning.
        WindowManager.LayoutParams params = mScrollPanel.getLayoutParamsForTesting();
        mScrollPanel.positionPanelAtCursor(cursorX, cursorY);

        // Panel should be left of and above cursor.
        assertThat(params.x).isLessThan(cursorX);
        assertThat(params.y).isLessThan(cursorY);
    }

    // Helper method to simulate a hover event on a view.
    private void triggerHoverEvent(View view, int action) {
        MotionEvent event = MotionEvent.obtain(