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

Commit 6c963286 authored by Yuhan Yang's avatar Yuhan Yang
Browse files

Implement autoclick long pressing

When autoclick type is long press, hold action down until
long press is triggered.

Screencast:
  go/screencast-njy3njm5ndczmjk0ntqwohwxzta3ywy1zi03ma

Test: atest AutoclickControllerTest
Bug: 400744833
Flag: com.android.server.accessibility.enable_autoclick_indicator
Change-Id: I48c26b7d86472d0d3b57ffd474a80eafb8209c0b
parent d6941c1e
Loading
Loading
Loading
Loading
+117 −22
Original line number Diff line number Diff line
@@ -29,6 +29,7 @@ import static com.android.server.accessibility.autoclick.AutoclickScrollPanel.DI
import static com.android.server.accessibility.autoclick.AutoclickTypePanel.AUTOCLICK_TYPE_DOUBLE_CLICK;
import static com.android.server.accessibility.autoclick.AutoclickTypePanel.AUTOCLICK_TYPE_DRAG;
import static com.android.server.accessibility.autoclick.AutoclickTypePanel.AUTOCLICK_TYPE_LEFT_CLICK;
import static com.android.server.accessibility.autoclick.AutoclickTypePanel.AUTOCLICK_TYPE_LONG_PRESS;
import static com.android.server.accessibility.autoclick.AutoclickTypePanel.AUTOCLICK_TYPE_RIGHT_CLICK;
import static com.android.server.accessibility.autoclick.AutoclickTypePanel.AUTOCLICK_TYPE_SCROLL;
import static com.android.server.accessibility.autoclick.AutoclickTypePanel.AutoclickType;
@@ -85,6 +86,11 @@ public class AutoclickController extends BaseEventStreamTransformation {
    public static final int DEFAULT_AUTOCLICK_DELAY_TIME = Flags.enableAutoclickIndicator()
            ? AUTOCLICK_DELAY_WITH_INDICATOR_DEFAULT : AUTOCLICK_DELAY_DEFAULT;

    // Duration before a press turns into a long press.
    // Factor 1.5 is needed, otherwise a long press is not safely detected.
    public static final long LONG_PRESS_TIMEOUT =
            (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;
@@ -125,6 +131,12 @@ public class AutoclickController extends BaseEventStreamTransformation {
    // move.
    private long mDragModeClickDownTime;

    // True during the duration of a long press event.
    private boolean mHasOngoingLongPress = false;
    // The MotionEvent downTime attribute associated with the originating click for a long press
    // move.
    private long mLongPressDownTime;

    @VisibleForTesting
    final ClickPanelControllerInterface clickPanelController =
            new ClickPanelControllerInterface() {
@@ -426,6 +438,11 @@ public class AutoclickController extends BaseEventStreamTransformation {
        return mDragModeIsDragging;
    }

    @VisibleForTesting
    boolean hasOngoingLongPressForTesting() {
        return mHasOngoingLongPress;
    }

    /**
     * Observes autoclick setting values, and updates ClickScheduler delay and indicator size
     * whenever the setting value changes.
@@ -768,6 +785,11 @@ public class AutoclickController extends BaseEventStreamTransformation {
            if (mDragModeIsDragging) {
                clearDraggingState();
            }

            if (mHasOngoingLongPress) {
                clearLongPressState();
            }

            resetInternalState();
            mHandler.removeCallbacks(this);
        }
@@ -789,6 +811,23 @@ public class AutoclickController extends BaseEventStreamTransformation {
            mDragModeIsDragging = false;
        }

        // Cancel the pending long press to avoid potential side effects from
        // leaving it in an inconsistent state.
        private void clearLongPressState() {
            if (mLastMotionEvent != null) {
                // A final ACTION_CANCEL event needs to be sent to alert the system that long press
                // has ended.
                MotionEvent cancelEvent = MotionEvent.obtain(mLastMotionEvent);
                cancelEvent.setAction(MotionEvent.ACTION_CANCEL);
                cancelEvent.setDownTime(mLongPressDownTime);
                AutoclickController.super.onMotionEvent(cancelEvent, cancelEvent,
                        mEventPolicyFlags);
            }

            resetSelectedClickTypeIfNecessary();
            mHasOngoingLongPress = false;
        }

        /**
         * Updates the meta state that should be used for click sequence.
         */
@@ -943,6 +982,12 @@ public class AutoclickController extends BaseEventStreamTransformation {
                return;
            }

            // Clear pending long press in case another click action jumps between long pressing
            // down and up events.
            if (mHasOngoingLongPress) {
                clearLongPressState();
            }

            // Always triggers left-click when the cursor hovers over the autoclick type panel, to
            // always allow users to change a different click type. Otherwise, if one chooses the
            // right-click, this user won't be able to rely on autoclick to select other click
@@ -989,6 +1034,10 @@ public class AutoclickController extends BaseEventStreamTransformation {
                        startDragEvent();
                    }
                    return;
                case AUTOCLICK_TYPE_LONG_PRESS:
                    actionButton = BUTTON_PRIMARY;
                    sendLongPress();
                    return;
                default:
                    break;
            }
@@ -1043,22 +1092,8 @@ public class AutoclickController extends BaseEventStreamTransformation {
        }

        private void sendMotionEvent(int actionButton, long eventTime) {
            MotionEvent downEvent =
                    MotionEvent.obtain(
                            /* downTime= */ eventTime,
                            /* eventTime= */ eventTime,
                            MotionEvent.ACTION_DOWN,
                            /* pointerCount= */ 1,
                            mTempPointerProperties,
                            mTempPointerCoords,
                            mMetaState,
                            actionButton,
                            /* xPrecision= */ 1.0f,
                            /* yPrecision= */ 1.0f,
                            mLastMotionEvent.getDeviceId(),
                            /* edgeFlags= */ 0,
                            mLastMotionEvent.getSource(),
                            mLastMotionEvent.getFlags());
            MotionEvent downEvent = buildMotionEvent(
                    eventTime, eventTime, actionButton, mLastMotionEvent);

            MotionEvent pressEvent = MotionEvent.obtain(downEvent);
            pressEvent.setAction(MotionEvent.ACTION_BUTTON_PRESS);
@@ -1086,6 +1121,66 @@ public class AutoclickController extends BaseEventStreamTransformation {
            upEvent.recycle();
        }

        // TODO(b/400744833): Reset Autoclick type to left click whenever a long press happens.
        private void sendLongPress() {
            mHasOngoingLongPress = true;
            mLongPressDownTime = SystemClock.uptimeMillis();
            MotionEvent downEvent = buildMotionEvent(
                    mLongPressDownTime, mLongPressDownTime, BUTTON_PRIMARY, mLastMotionEvent);

            MotionEvent pressEvent = MotionEvent.obtain(downEvent);
            pressEvent.setAction(MotionEvent.ACTION_BUTTON_PRESS);

            AutoclickController.super.onMotionEvent(downEvent, downEvent, mEventPolicyFlags);
            AutoclickController.super.onMotionEvent(pressEvent, pressEvent, mEventPolicyFlags);

            mHandler.postDelayed(new Runnable() {
                @Override
                public void run() {
                    long upTime = SystemClock.uptimeMillis();
                    MotionEvent releaseEvent = buildMotionEvent(
                            mLongPressDownTime, upTime, BUTTON_PRIMARY, downEvent);
                    releaseEvent.setAction(MotionEvent.ACTION_BUTTON_RELEASE);
                    releaseEvent.setButtonState(0);

                    MotionEvent upEvent = MotionEvent.obtain(releaseEvent);
                    upEvent.setAction(MotionEvent.ACTION_UP);
                    upEvent.setButtonState(0);

                    AutoclickController.super.onMotionEvent(
                            releaseEvent, releaseEvent, mEventPolicyFlags);
                    AutoclickController.super.onMotionEvent(
                            upEvent, upEvent, mEventPolicyFlags);

                    downEvent.recycle();
                    pressEvent.recycle();
                    releaseEvent.recycle();
                    upEvent.recycle();
                    mHasOngoingLongPress = false;
                }
            }, LONG_PRESS_TIMEOUT);
        }

        private @NonNull MotionEvent buildMotionEvent(
                long downTime, long eventTime, int actionButton,
                @NonNull MotionEvent lastMotionEvent) {
            return MotionEvent.obtain(
                            /* downTime= */ downTime,
                            /* eventTime= */ eventTime,
                            MotionEvent.ACTION_DOWN,
                            /* pointerCount= */ 1,
                            mTempPointerProperties,
                            mTempPointerCoords,
                            mMetaState,
                            actionButton,
                            /* xPrecision= */ 1.0f,
                            /* yPrecision= */ 1.0f,
                            lastMotionEvent.getDeviceId(),
                            /* edgeFlags= */ 0,
                            lastMotionEvent.getSource(),
                            lastMotionEvent.getFlags());
        }

        // To start a drag event, only send the DOWN and BUTTON_PRESS events.
        private void startDragEvent() {
            mDragModeClickDownTime = SystemClock.uptimeMillis();
+89 −5
Original line number Diff line number Diff line
@@ -59,6 +59,9 @@ import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;

import java.util.ArrayList;
import java.util.List;

/** Test cases for {@link AutoclickController}. */
@RunWith(AndroidTestingRunner.class)
@TestableLooper.RunWithLooper(setAsMainLooper = true)
@@ -83,31 +86,44 @@ public class AutoclickControllerTest {
        public MotionEvent buttonReleaseEvent;
        public MotionEvent upEvent;
        public MotionEvent moveEvent;
        public MotionEvent cancelEvent;
        public int eventCount = 0;
        private List<MotionEvent> mEventList = new ArrayList<>();

        @Override
        public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
            MotionEvent eventCopy = MotionEvent.obtain(event);
            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    downEvent = eventCopy;
                    eventCount++;
                    break;
                case MotionEvent.ACTION_BUTTON_PRESS:
                    buttonPressEvent = eventCopy;
                    eventCount++;
                    break;
                case MotionEvent.ACTION_BUTTON_RELEASE:
                    buttonReleaseEvent = eventCopy;
                    eventCount++;
                    break;
                case MotionEvent.ACTION_UP:
                    upEvent = eventCopy;
                    eventCount++;
                    break;
                case MotionEvent.ACTION_MOVE:
                    moveEvent = eventCopy;
                    eventCount++;
                    break;
                case MotionEvent.ACTION_CANCEL:
                    cancelEvent = eventCopy;
                    break;
                default:
                    return;
            }
            mEventList.add(eventCopy);
            eventCount++;
        }

        public void assertCapturedEvents(int... actionsInOrder) {
            assertThat(eventCount).isEqualTo(mEventList.size());
            for (int i = 0; i < eventCount; i++) {
                assertThat(actionsInOrder[i])
                        .isEqualTo(mEventList.get(i).getAction());
            }
        }
    }
@@ -1187,6 +1203,74 @@ public class AutoclickControllerTest {
        verify(mockAutoclickTypePanel, Mockito.never()).resetSelectedClickType();
    }

    @Test
    @EnableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_AUTOCLICK_INDICATOR)
    public void sendClick_clickType_longPress_triggerPressAndHold() {
        MotionEventCaptor motionEventCaptor = new MotionEventCaptor();
        mController.setNext(motionEventCaptor);

        injectFakeMouseActionHoverMoveEvent();
        // Set delay to zero so click is scheduled to run immediately.
        mController.mClickScheduler.updateDelay(0);
        // Set click type to long press.
        mController.clickPanelController.handleAutoclickTypeChange(
                AutoclickTypePanel.AUTOCLICK_TYPE_LONG_PRESS);
        AutoclickTypePanel mockAutoclickTypePanel = mock(AutoclickTypePanel.class);
        mController.mAutoclickTypePanel = mockAutoclickTypePanel;

        // Send hover move event.
        injectFakeMouseMoveEvent(/* x= */ 30f, /* y= */ 0, MotionEvent.ACTION_HOVER_MOVE);
        mTestableLooper.processAllMessages();
        assertThat(motionEventCaptor.downEvent).isNotNull();
        assertThat(motionEventCaptor.downEvent.getButtonState()).isEqualTo(
                MotionEvent.BUTTON_PRIMARY);
        assertThat(motionEventCaptor.upEvent).isNull();

        // When all messages (with delays) are processed.
        mTestableLooper.moveTimeForward(mController.LONG_PRESS_TIMEOUT);
        mTestableLooper.processAllMessages();
        assertThat(motionEventCaptor.upEvent).isNotNull();
        motionEventCaptor.assertCapturedEvents(
                MotionEvent.ACTION_DOWN, MotionEvent.ACTION_BUTTON_PRESS,
                MotionEvent.ACTION_BUTTON_RELEASE, MotionEvent.ACTION_UP);
    }

    @Test
    @EnableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_AUTOCLICK_INDICATOR)
    public void sendClick_clickType_longPress_interruptCancelsLongPress() {
        MotionEventCaptor motionEventCaptor = new MotionEventCaptor();
        mController.setNext(motionEventCaptor);

        injectFakeMouseActionHoverMoveEvent();
        // Set delay to zero so click is scheduled to run immediately.
        mController.mClickScheduler.updateDelay(0);
        // Set click type to long press.
        mController.clickPanelController.handleAutoclickTypeChange(
                AutoclickTypePanel.AUTOCLICK_TYPE_LONG_PRESS);
        AutoclickTypePanel mockAutoclickTypePanel = mock(AutoclickTypePanel.class);
        mController.mAutoclickTypePanel = mockAutoclickTypePanel;

        // Send hover move event.
        injectFakeMouseMoveEvent(/* x= */ 100f, /* y= */ 0, MotionEvent.ACTION_HOVER_MOVE);
        mTestableLooper.processAllMessages();
        assertThat(motionEventCaptor.downEvent).isNotNull();
        assertThat(motionEventCaptor.downEvent.getButtonState()).isEqualTo(
                MotionEvent.BUTTON_PRIMARY);
        assertThat(motionEventCaptor.upEvent).isNull();
        assertThat(mController.hasOngoingLongPressForTesting()).isTrue();
        assertThat(motionEventCaptor.cancelEvent).isNull();

        // Send another hover move event to interrupt the long press.
        mTestableLooper.moveTimeForward(mController.LONG_PRESS_TIMEOUT / 2);
        mController.clickPanelController.handleAutoclickTypeChange(
                AutoclickTypePanel.AUTOCLICK_TYPE_LEFT_CLICK);
        injectFakeMouseMoveEvent(/* x= */ 0, /* y= */ 30f, MotionEvent.ACTION_HOVER_MOVE);
        mController.mClickScheduler.run();
        mTestableLooper.processAllMessages();
        assertThat(motionEventCaptor.cancelEvent).isNotNull();
        assertThat(mController.hasOngoingLongPressForTesting()).isFalse();
    }

    /**
     * =========================================================================
     * Helper Functions