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

Commit c86d5512 authored by Wenyu Zhang's avatar Wenyu Zhang Committed by Android (Google) Code Review
Browse files

Merge changes from topic "ignore-cursor-movement" into main

* changes:
  [3/3] autoclick: Implement ignore minor cursor movement setting
  [2/3] autoclick: Draw click dot indicator
  [1/3] autoclick: Do not reset timer when cursor moves within indicator
parents 0ab4b41c adb12131
Loading
Loading
Loading
Loading
+10 −3
Original line number Diff line number Diff line
@@ -1298,8 +1298,7 @@ public class AutoclickController extends BaseEventStreamTransformation implement
            // If the panel is hovered, always use the default slop so it's easier to click the
            // closely spaced buttons.
            double slop =
                    ((Flags.enableAutoclickIndicator() && mIgnoreMinorCursorMovement
                            && !isPanelHovered())
                    ((Flags.enableAutoclickIndicator() && !isPanelHovered())
                            ? mMovementSlop
                            : DEFAULT_MOVEMENT_SLOP);
            return delta > slop;
@@ -1307,6 +1306,9 @@ public class AutoclickController extends BaseEventStreamTransformation implement

        public void setIgnoreMinorCursorMovement(boolean ignoreMinorCursorMovement) {
            mIgnoreMinorCursorMovement = ignoreMinorCursorMovement;
            if (mAutoclickIndicatorView != null) {
                mAutoclickIndicatorView.setIgnoreMinorCursorMovement(ignoreMinorCursorMovement);
            }
        }

        public void setRevertToLeftClick(boolean revertToLeftClick) {
@@ -1363,7 +1365,12 @@ public class AutoclickController extends BaseEventStreamTransformation implement
                mTempPointerCoords = new PointerCoords[1];
                mTempPointerCoords[0] = new PointerCoords();
            }
            if (mIgnoreMinorCursorMovement) {
                mTempPointerCoords[0].x = mAnchorCoords.x;
                mTempPointerCoords[0].y = mAnchorCoords.y;
            } else {
                mLastMotionEvent.getPointerCoords(pointerIndex, mTempPointerCoords[0]);
            }

            int actionButton = BUTTON_PRIMARY;
            switch (selectedClickType) {
+54 −0
Original line number Diff line number Diff line
@@ -23,6 +23,7 @@ import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.Configuration;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PixelFormat;
import android.graphics.RectF;
@@ -48,6 +49,11 @@ public class AutoclickIndicatorView extends View {

    static final int MINIMAL_ANIMATION_DURATION = 50;

    // The radius of the click point indicator.
    private static final float POINT_RADIUS_DP = 4f;

    private static final float POINT_STROKE_WIDTH_DP = 1f;

    private final int mColor = R.color.materialColorPrimary;

    // Radius of the indicator circle.
@@ -55,11 +61,15 @@ public class AutoclickIndicatorView extends View {

    // Paint object used to draw the indicator.
    private final Paint mPaint;
    private final Paint mPointPaint;

    private final ValueAnimator mAnimator;

    private final RectF mRingRect;

    private final float mPointSizePx;
    private final float mPointStrokeWidthPx;

    // x and y coordinates of the mouse.
    private float mMouseX;
    private float mMouseY;
@@ -76,6 +86,8 @@ public class AutoclickIndicatorView extends View {
    // Status of whether the visual indicator should display or not.
    private boolean showIndicator = false;

    private boolean mIgnoreMinorCursorMovement = false;

    public AutoclickIndicatorView(Context context) {
        super(context);

@@ -84,6 +96,15 @@ public class AutoclickIndicatorView extends View {
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeWidth(10);

        // Convert dp to pixels based on screen density for the point indicator.
        float density = getResources().getDisplayMetrics().density;
        mPointSizePx = POINT_RADIUS_DP * density;
        mPointStrokeWidthPx = POINT_STROKE_WIDTH_DP * density;

        // Setup paint for drawing the point indicator.
        mPointPaint = new Paint();
        mPointPaint.setAntiAlias(true);

        mAnimator = ValueAnimator.ofFloat(0, 360);
        mAnimator.setDuration(mAnimationDuration);
        mAnimator.setInterpolator(new LinearInterpolator());
@@ -124,12 +145,33 @@ public class AutoclickIndicatorView extends View {
        super.onDraw(canvas);

        if (showIndicator) {
            // Draw the ring indicator.
            mRingRect.set(
                    /* left= */ mSnapshotX - mRadius,
                    /* top= */ mSnapshotY - mRadius,
                    /* right= */ mSnapshotX + mRadius,
                    /* bottom= */ mSnapshotY + mRadius);
            canvas.drawArc(mRingRect, /* startAngle= */ -90, mSweepAngle, false, mPaint);

            // Draw a point indicator. When mIgnoreMinorCursorMovement is true, the point stays at
            // the center of the ring. Otherwise, it follows the mouse movement.
            final float pointX;
            final float pointY;
            if (mIgnoreMinorCursorMovement) {
                pointX = mSnapshotX;
                pointY = mSnapshotY;
            } else {
                pointX = mMouseX;
                pointY = mMouseY;
            }
            mPointPaint.setStyle(Paint.Style.FILL);
            mPointPaint.setColor(Color.BLACK);
            canvas.drawCircle(pointX, pointY, mPointSizePx, mPointPaint);

            mPointPaint.setStyle(Paint.Style.STROKE);
            mPointPaint.setStrokeWidth(mPointStrokeWidthPx);
            mPointPaint.setColor(Color.WHITE);
            canvas.drawCircle(pointX, pointY, mPointSizePx, mPointPaint);
        }
    }

@@ -156,8 +198,16 @@ public class AutoclickIndicatorView extends View {
    }

    public void setCoordination(float x, float y) {
        if (mMouseX == x && mMouseY == y) {
            return;
        }
        mMouseX = x;
        mMouseY = y;

        // Redraw the click point indicator with the updated coordinates.
        if (showIndicator) {
            invalidate();
        }
    }

    public void setRadius(int radius) {
@@ -192,4 +242,8 @@ public class AutoclickIndicatorView extends View {
        mAnimationDuration = Math.max(duration, MINIMAL_ANIMATION_DURATION);
        mAnimator.setDuration(mAnimationDuration);
    }

    public void setIgnoreMinorCursorMovement(boolean ignoreMinorCursorMovement) {
        mIgnoreMinorCursorMovement = ignoreMinorCursorMovement;
    }
}
+70 −14
Original line number Diff line number Diff line
@@ -429,7 +429,7 @@ public class AutoclickControllerTest {
        long initialScheduledTime = mController.mClickScheduler.getScheduledClickTimeForTesting();

        // Significant change in x (30f difference) and y (30f difference)
        injectFakeMouseMoveEvent(/* x= */ 60f, /* y= */ 70f, MotionEvent.ACTION_HOVER_MOVE);
        injectFakeMouseMoveEvent(/* x= */ 100f, /* y= */ 100f, MotionEvent.ACTION_HOVER_MOVE);

        // Verify that the scheduled click time has changed (click was rescheduled).
        assertThat(mController.mClickScheduler.getScheduledClickTimeForTesting())
@@ -486,7 +486,7 @@ public class AutoclickControllerTest {

    @Test
    @EnableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_AUTOCLICK_INDICATOR)
    public void onIgnoreCursorMovementFromSettingsChange_clickTriggered() {
    public void onNotIgnoreCursorMovement_clickNotTriggered_whenMoveIsWithinSlop() {
        // Send initial mouse movement.
        injectFakeMouseActionHoverMoveEvent();

@@ -500,16 +500,72 @@ public class AutoclickControllerTest {
                Settings.Secure.getUriFor(
                        Settings.Secure.ACCESSIBILITY_AUTOCLICK_CURSOR_AREA_SIZE));

        // Move the mouse down less than customSize radius but ignore custom movement is not enabled
        // so a click is triggered.
        // Move the mouse down less than customSize radius. Even if ignore custom movement is not
        // enabled, a click is not triggered as long as the move is within the slop.
        float moveDownY = customSize - 100;
        injectFakeMouseMoveEvent(/* x= */ 0, /* y= */ moveDownY, MotionEvent.ACTION_HOVER_MOVE);
        assertThat(mController.mClickScheduler.getIsActiveForTesting()).isTrue();
        assertThat(mController.mClickScheduler.getIsActiveForTesting()).isFalse();
    }

    @Test
    @EnableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_AUTOCLICK_INDICATOR)
    public void sendClick_ignoreMinorMovementTrue_clicksAtAnchorPosition() {
        initializeAutoclick();
        enableIgnoreMinorCursorMovement();

        // First move event to set the anchor.
        float anchorX = 50f;
        float anchorY = 60f;
        injectFakeMouseMoveEvent(anchorX, anchorY, MotionEvent.ACTION_HOVER_MOVE);

        // Second move event to trigger the click.
        float lastX = 80f;
        float lastY = 80f;
        injectFakeMouseMoveEvent(lastX, lastY, MotionEvent.ACTION_HOVER_MOVE);
        mController.mClickScheduler.run();

        // Verify click happened at anchor position, not the last position.
        assertThat(mMotionEventCaptor.downEvent).isNotNull();
        assertThat(mMotionEventCaptor.downEvent.getX()).isEqualTo(anchorX);
        assertThat(mMotionEventCaptor.downEvent.getY()).isEqualTo(anchorY);
    }

    @Test
    @EnableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_AUTOCLICK_INDICATOR)
    public void sendClick_ignoreMinorMovementFalse_clicksAtLastPosition() {
        initializeAutoclick();

        // Ensure setting is off.
        Settings.Secure.putIntForUser(
                mTestableContext.getContentResolver(),
                Settings.Secure.ACCESSIBILITY_AUTOCLICK_IGNORE_MINOR_CURSOR_MOVEMENT,
                AccessibilityUtils.State.OFF,
                mTestableContext.getUserId());
        mController.onChangeForTesting(
                /* selfChange= */ true,
                Settings.Secure.getUriFor(
                        Settings.Secure.ACCESSIBILITY_AUTOCLICK_IGNORE_MINOR_CURSOR_MOVEMENT));

        // First move event to set the anchor.
        float anchorX = 50f;
        float anchorY = 60f;
        injectFakeMouseMoveEvent(anchorX, anchorY, MotionEvent.ACTION_HOVER_MOVE);

        // Second move event to trigger the click.
        float lastX = 80f;
        float lastY = 80f;
        injectFakeMouseMoveEvent(lastX, lastY, MotionEvent.ACTION_HOVER_MOVE);
        mController.mClickScheduler.run();

        // Verify click happened at the last position.
        assertThat(mMotionEventCaptor.downEvent).isNotNull();
        assertThat(mMotionEventCaptor.downEvent.getX()).isEqualTo(lastX);
        assertThat(mMotionEventCaptor.downEvent.getY()).isEqualTo(lastY);
    }

    @Test
    @EnableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_AUTOCLICK_INDICATOR)
    public void onIgnoreCursorMovementFromSettingsChange_clickNotTriggered() {
    public void onIgnoreCursorMovement_clickNotTriggered_whenMoveIsWithinSlop() {
        // Move mouse to initialize autoclick panel before enabling ignore minor cursor movement.
        injectFakeMouseActionHoverMoveEvent();
        enableIgnoreMinorCursorMovement();
@@ -524,8 +580,8 @@ public class AutoclickControllerTest {
                Settings.Secure.getUriFor(
                        Settings.Secure.ACCESSIBILITY_AUTOCLICK_CURSOR_AREA_SIZE));

        // After enabling ignore custom movement, move the mouse right, less than customSize radius
        // so a click won't be triggered.
        // No matter if ignore custom movement is enabled or not, a click won't be triggered as long
        // as the move is inside the slop.
        float moveRightX = customSize - 100;
        injectFakeMouseMoveEvent(/* x= */ moveRightX, /* y= */ 0, MotionEvent.ACTION_HOVER_MOVE);
        assertThat(mController.mClickScheduler.getIsActiveForTesting()).isFalse();
@@ -581,7 +637,7 @@ public class AutoclickControllerTest {
        assertThat(mController.mClickScheduler.getScheduledClickTimeForTesting()).isEqualTo(-1);

        // Send move again to trigger click and verify there is now a pending click.
        injectFakeMouseMoveEvent(/* x= */ 30f, /* y= */ 0, MotionEvent.ACTION_HOVER_MOVE);
        injectFakeMouseMoveEvent(/* x= */ 100f, /* y= */ 0, MotionEvent.ACTION_HOVER_MOVE);
        assertThat(mController.mClickScheduler.getIsActiveForTesting()).isTrue();
        assertThat(mController.mClickScheduler.getScheduledClickTimeForTesting()).isNotEqualTo(-1);
    }
@@ -730,7 +786,7 @@ public class AutoclickControllerTest {
        initializeAutoclick();

        // Send hover move event.
        injectFakeMouseMoveEvent(/* x= */ 30f, /* y= */ 0, MotionEvent.ACTION_HOVER_MOVE);
        injectFakeMouseMoveEvent(/* x= */ 100f, /* y= */ 0, MotionEvent.ACTION_HOVER_MOVE);
        mTestableLooper.processAllMessages();

        // Verify left click sent.
@@ -750,7 +806,7 @@ public class AutoclickControllerTest {
        mController.mAutoclickTypePanel = mockAutoclickTypePanel;

        // Send hover move event.
        injectFakeMouseMoveEvent(/* x= */ 30f, /* y= */ 0, MotionEvent.ACTION_HOVER_MOVE);
        injectFakeMouseMoveEvent(/* x= */ 100f, /* y= */ 0, MotionEvent.ACTION_HOVER_MOVE);
        mTestableLooper.processAllMessages();

        // Verify right click sent.
@@ -773,7 +829,7 @@ public class AutoclickControllerTest {
        mController.mAutoclickScrollPanel = mockScrollPanel;

        // First hover move event.
        injectFakeMouseMoveEvent(/* x= */ 30f, /* y= */ 0, MotionEvent.ACTION_HOVER_MOVE);
        injectFakeMouseMoveEvent(/* x= */ 100f, /* y= */ 0, MotionEvent.ACTION_HOVER_MOVE);
        mTestableLooper.processAllMessages();

        // Verify scroll panel is shown once.
@@ -1138,7 +1194,7 @@ public class AutoclickControllerTest {
        mController.clickPanelController.handleAutoclickTypeChange(
                AutoclickTypePanel.AUTOCLICK_TYPE_DRAG);

        injectFakeMouseMoveEvent(/* x= */ 30, /* y= */ 0, MotionEvent.ACTION_HOVER_MOVE);
        injectFakeMouseMoveEvent(/* x= */ 100, /* y= */ 0, MotionEvent.ACTION_HOVER_MOVE);
        mTestableLooper.processAllMessages();

        // Verify only two motion events were sent.
@@ -1291,7 +1347,7 @@ public class AutoclickControllerTest {
        mController.mAutoclickTypePanel = mockAutoclickTypePanel;

        // Send hover move event.
        injectFakeMouseMoveEvent(/* x= */ 30f, /* y= */ 0, MotionEvent.ACTION_HOVER_MOVE);
        injectFakeMouseMoveEvent(/* x= */ 100f, /* y= */ 0, MotionEvent.ACTION_HOVER_MOVE);
        mTestableLooper.processAllMessages();
        assertThat(motionEventCaptor.downEvent).isNotNull();
        assertThat(motionEventCaptor.downEvent.getButtonState()).isEqualTo(
+77 −1
Original line number Diff line number Diff line
@@ -37,12 +37,12 @@ import android.graphics.RectF;
import android.testing.AndroidTestingRunner;
import android.testing.TestableContext;
import android.testing.TestableLooper;
import android.view.accessibility.AccessibilityManager;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
@@ -76,6 +76,52 @@ public class AutoclickIndicatorViewTest {
        // Verify ring is drawn.
        verify(mMockCanvas).drawArc(
                any(RectF.class), eq(-90f), anyFloat(), eq(false), any(Paint.class));
        // Verify point is drawn (fill and stroke).
        verify(mMockCanvas, times(2)).drawCircle(eq(100f), eq(200f), anyFloat(), any(Paint.class));
    }

    @Test
    public void onDraw_mouseMovesAfterIndicatorShown_pointMovesWithMouse() {
        mAutoclickIndicatorView.setCoordination(100f, 200f);
        mAutoclickIndicatorView.redrawIndicator();
        // After indicator is shown, mouse moves to a new position.
        mAutoclickIndicatorView.setCoordination(300f, 400f);

        mAutoclickIndicatorView.onDraw(mMockCanvas);

        // Verify ring is drawn at the original snapshot position.
        ArgumentCaptor<RectF> rectCaptor = ArgumentCaptor.forClass(RectF.class);
        verify(mMockCanvas)
                .drawArc(rectCaptor.capture(), eq(-90f), anyFloat(), eq(false), any(Paint.class));
        RectF ringRect = rectCaptor.getValue();
        assertThat(ringRect.centerX()).isEqualTo(100f);
        assertThat(ringRect.centerY()).isEqualTo(200f);

        // Verify point is drawn at the new mouse position (fill and stroke).
        verify(mMockCanvas, times(2)).drawCircle(eq(300f), eq(400f), anyFloat(), any(Paint.class));
    }

    @Test
    public void onDraw_ignoreMinorMovementTrue_pointDoesNotMoveWithMouse() {
        mAutoclickIndicatorView.setIgnoreMinorCursorMovement(true);
        mAutoclickIndicatorView.setCoordination(100f, 200f);
        mAutoclickIndicatorView.redrawIndicator();
        // After indicator is shown, mouse moves to a new position.
        mAutoclickIndicatorView.setCoordination(300f, 400f);

        mAutoclickIndicatorView.onDraw(mMockCanvas);

        // Verify ring is drawn at the original snapshot position.
        ArgumentCaptor<RectF> rectCaptor = ArgumentCaptor.forClass(RectF.class);
        verify(mMockCanvas)
                .drawArc(rectCaptor.capture(), eq(-90f), anyFloat(), eq(false), any(Paint.class));
        RectF ringRect = rectCaptor.getValue();
        assertThat(ringRect.centerX()).isEqualTo(100f);
        assertThat(ringRect.centerY()).isEqualTo(200f);

        // Verify point is drawn at the original snapshot position, not the new mouse position.
        verify(mMockCanvas, times(2)).drawCircle(eq(100f), eq(200f), anyFloat(), any(Paint.class));
        verify(mMockCanvas, never()).drawCircle(eq(300f), eq(400f), anyFloat(), any(Paint.class));
    }

    @Test
@@ -86,12 +132,15 @@ public class AutoclickIndicatorViewTest {

        verify(mMockCanvas, never()).drawArc(
                any(RectF.class), anyFloat(), anyFloat(), anyBoolean(), any(Paint.class));
        verify(mMockCanvas, never())
                .drawCircle(anyFloat(), anyFloat(), anyFloat(), any(Paint.class));
    }

    @Test
    public void setCoordination_showIndicatorTrue_invalidatesView() {
        mSpyAutoclickIndicatorView.setCoordination(100f, 200f);
        mSpyAutoclickIndicatorView.redrawIndicator();
        clearInvocations(mSpyAutoclickIndicatorView);

        mSpyAutoclickIndicatorView.setCoordination(300f, 400f);

@@ -109,6 +158,17 @@ public class AutoclickIndicatorViewTest {
        verify(mSpyAutoclickIndicatorView, never()).invalidate();
    }

    @Test
    public void setCoordination_sameCoordinates_doesNotInvalidateView() {
        mSpyAutoclickIndicatorView.setCoordination(100f, 200f);
        mSpyAutoclickIndicatorView.redrawIndicator();
        clearInvocations(mSpyAutoclickIndicatorView);

        mSpyAutoclickIndicatorView.setCoordination(100f, 200f);

        verify(mSpyAutoclickIndicatorView, never()).invalidate();
    }

    @Test
    public void redrawIndicator_startsAnimation() {
        mAutoclickIndicatorView.redrawIndicator();
@@ -117,6 +177,12 @@ public class AutoclickIndicatorViewTest {
        assertThat(animator.isStarted()).isTrue();
    }

    @Test
    public void redrawIndicator_invalidatesView() {
        mSpyAutoclickIndicatorView.redrawIndicator();
        verify(mSpyAutoclickIndicatorView).invalidate();
    }

    @Test
    public void clearIndicator_cancelsAnimation() {
        mAutoclickIndicatorView.redrawIndicator();
@@ -126,6 +192,16 @@ public class AutoclickIndicatorViewTest {
        assertThat(animator.isStarted()).isFalse();
    }

    @Test
    public void clearIndicator_invalidatesView() {
        mSpyAutoclickIndicatorView.redrawIndicator();
        clearInvocations(mSpyAutoclickIndicatorView);

        mSpyAutoclickIndicatorView.clearIndicator();

        verify(mSpyAutoclickIndicatorView).invalidate();
    }

    @Test
    public void setAnimationDuration_updatesAnimatorDuration() {
        int testDuration = 1000;