Loading services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java +58 −24 Original line number Diff line number Diff line Loading @@ -92,8 +92,10 @@ public class AutoclickController extends BaseEventStreamTransformation { (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; private static final float SCROLL_AMOUNT = 0.5f; protected static final long CONTINUOUS_SCROLL_INTERVAL = 30; private Handler mContinuousScrollHandler; private Runnable mContinuousScrollRunnable; private final AccessibilityTraceManager mTrace; private final Context mContext; Loading Loading @@ -123,7 +125,8 @@ public class AutoclickController extends BaseEventStreamTransformation { private @AutoclickType int mActiveClickType = AUTOCLICK_TYPE_LEFT_CLICK; // Default scroll direction is DIRECTION_NONE. private @AutoclickScrollPanel.ScrollDirection int mHoveredDirection = DIRECTION_NONE; @VisibleForTesting protected @AutoclickScrollPanel.ScrollDirection int mHoveredDirection = DIRECTION_NONE; // True during the duration of a dragging event. private boolean mDragModeIsDragging = false; Loading Loading @@ -181,31 +184,22 @@ public class AutoclickController extends BaseEventStreamTransformation { // Update the hover direction. if (hovered) { mHoveredDirection = direction; } else if (mHoveredDirection == direction) { // Safety check: Only clear hover tracking if this is the same button // we're currently tracking. mHoveredDirection = AutoclickScrollPanel.DIRECTION_NONE; } // For exit button, we only trigger hover state changes, the autoclick system // will handle the countdown. // For exit button, return early and the autoclick system will handle the // countdown then exit scroll mode. if (direction == AutoclickScrollPanel.DIRECTION_EXIT) { return; } // Handle all non-exit buttons when hovered. if (hovered) { // Clear the indicator. if (mAutoclickIndicatorScheduler != null) { mAutoclickIndicatorScheduler.cancel(); if (mAutoclickIndicatorView != null) { mAutoclickIndicatorView.clearIndicator(); } } // Perform scroll action. // For scroll directions, start continuous scrolling. if (direction != DIRECTION_NONE) { handleScroll(direction); startContinuousScroll(direction); } } else if (mHoveredDirection == direction) { // If not hovered, stop scrolling — but only if the mouse leaves the same // button that started it. This avoids stopping the scroll when the mouse // briefly moves over other buttons. stopContinuousScroll(); } } }; Loading Loading @@ -267,6 +261,16 @@ public class AutoclickController extends BaseEventStreamTransformation { mAutoclickScrollPanel = new AutoclickScrollPanel(mContext, mWindowManager, mScrollPanelController); // Initialize continuous scroll handler and runnable. mContinuousScrollHandler = new Handler(handler.getLooper()); mContinuousScrollRunnable = new Runnable() { @Override public void run() { handleScroll(mHoveredDirection); mContinuousScrollHandler.postDelayed(this, CONTINUOUS_SCROLL_INTERVAL); } }; mAutoclickTypePanel.show(); mWindowManager.addView(mAutoclickIndicatorView, mAutoclickIndicatorView.getLayoutParams()); } Loading Loading @@ -323,6 +327,11 @@ public class AutoclickController extends BaseEventStreamTransformation { mAutoclickScrollPanel.hide(); mAutoclickScrollPanel = null; } if (mContinuousScrollHandler != null) { mContinuousScrollHandler.removeCallbacks(mContinuousScrollRunnable); mContinuousScrollHandler = null; } } private void scheduleClick(MotionEvent event, int policyFlags) { Loading Loading @@ -366,6 +375,14 @@ public class AutoclickController extends BaseEventStreamTransformation { * Handles scroll operations in the specified direction. */ private void handleScroll(@AutoclickScrollPanel.ScrollDirection int direction) { // Remove the autoclick indicator view when hovering on directional buttons. if (mAutoclickIndicatorScheduler != null) { mAutoclickIndicatorScheduler.cancel(); if (mAutoclickIndicatorView != null) { mAutoclickIndicatorView.clearIndicator(); } } final long now = SystemClock.uptimeMillis(); // Create pointer properties. Loading Loading @@ -426,8 +443,25 @@ public class AutoclickController extends BaseEventStreamTransformation { if (mAutoclickScrollPanel != null) { mAutoclickScrollPanel.hide(); } stopContinuousScroll(); } private void startContinuousScroll(@AutoclickScrollPanel.ScrollDirection int direction) { if (mContinuousScrollHandler != null) { handleScroll(direction); mContinuousScrollHandler.postDelayed(mContinuousScrollRunnable, CONTINUOUS_SCROLL_INTERVAL); } } private void stopContinuousScroll() { if (mContinuousScrollHandler != null) { mContinuousScrollHandler.removeCallbacks(mContinuousScrollRunnable); } mHoveredDirection = DIRECTION_NONE; } @VisibleForTesting void onChangeForTesting(boolean selfChange, Uri uri) { mAutoclickSettingsObserver.onChange(selfChange, uri); Loading services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickScrollPanel.java +0 −9 Original line number Diff line number Diff line Loading @@ -227,15 +227,6 @@ public class AutoclickScrollPanel { case MotionEvent.ACTION_HOVER_ENTER: hovered = true; break; case MotionEvent.ACTION_HOVER_MOVE: // For direction buttons, continuously trigger scroll on hover move. if (direction != DIRECTION_EXIT) { hovered = true; } else { // Ignore hover move events for exit button. return true; } break; case MotionEvent.ACTION_HOVER_EXIT: hovered = false; break; Loading services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickControllerTest.java +65 −4 Original line number Diff line number Diff line Loading @@ -18,6 +18,7 @@ package com.android.server.accessibility.autoclick; import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; import static com.android.server.accessibility.autoclick.AutoclickController.CONTINUOUS_SCROLL_INTERVAL; import static com.android.server.accessibility.autoclick.AutoclickTypePanel.AUTOCLICK_TYPE_RIGHT_CLICK; import static com.android.server.testutils.MockitoUtilsKt.eq; Loading Loading @@ -67,16 +68,20 @@ import java.util.List; @TestableLooper.RunWithLooper(setAsMainLooper = true) public class AutoclickControllerTest { @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule(); @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule(); @Rule public TestableContext mTestableContext = new TestableContext(getInstrumentation().getContext()); private TestableLooper mTestableLooper; @Mock private AccessibilityTraceManager mMockTrace; @Mock private WindowManager mMockWindowManager; @Mock private AccessibilityTraceManager mMockTrace; @Mock private WindowManager mMockWindowManager; private AutoclickController mController; private MotionEventCaptor mMotionEventCaptor; Loading Loading @@ -1271,6 +1276,62 @@ public class AutoclickControllerTest { assertThat(mController.hasOngoingLongPressForTesting()).isFalse(); } @Test @EnableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_AUTOCLICK_INDICATOR) public void continuousScroll_completeLifecycle() { // Set up event capturer to track scroll events. ScrollEventCaptor scrollCaptor = new ScrollEventCaptor(); mController.setNext(scrollCaptor); // Initialize controller. injectFakeMouseActionHoverMoveEvent(); // Set cursor position. float expectedX = 100f; float expectedY = 200f; mController.mScrollCursorX = expectedX; mController.mScrollCursorY = expectedY; // Start scrolling by hovering UP button. mController.mScrollPanelController.onHoverButtonChange( AutoclickScrollPanel.DIRECTION_UP, true); // Verify initial hover state and event. assertThat(mController.mHoveredDirection).isEqualTo(AutoclickScrollPanel.DIRECTION_UP); assertThat(scrollCaptor.eventCount).isEqualTo(1); assertThat(scrollCaptor.scrollEvent.getAxisValue(MotionEvent.AXIS_VSCROLL)).isGreaterThan( 0); // Simulate continuous scrolling by triggering runnable. scrollCaptor.eventCount = 0; // Advance time by CONTINUOUS_SCROLL_INTERVAL (30ms) and process messages. mTestableLooper.moveTimeForward(CONTINUOUS_SCROLL_INTERVAL); mTestableLooper.processAllMessages(); // Advance time again to trigger second scroll event. mTestableLooper.moveTimeForward(CONTINUOUS_SCROLL_INTERVAL); mTestableLooper.processAllMessages(); // Verify multiple scroll events were generated. assertThat(scrollCaptor.eventCount).isEqualTo(2); assertThat(scrollCaptor.scrollEvent.getX()).isEqualTo(expectedX); assertThat(scrollCaptor.scrollEvent.getY()).isEqualTo(expectedY); // Stop scrolling by un-hovering the button. mController.mScrollPanelController.onHoverButtonChange( AutoclickScrollPanel.DIRECTION_UP, false); // Verify direction is reset. assertThat(mController.mHoveredDirection).isEqualTo(AutoclickScrollPanel.DIRECTION_NONE); // Verify no more scroll events are generated after stopping. int countBeforeRunnable = scrollCaptor.eventCount; mTestableLooper.moveTimeForward(CONTINUOUS_SCROLL_INTERVAL); mTestableLooper.processAllMessages(); assertThat(scrollCaptor.eventCount).isEqualTo(countBeforeRunnable); } /** * ========================================================================= * Helper Functions Loading services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickScrollPanelTest.java +7 −7 Original line number Diff line number Diff line Loading @@ -146,7 +146,7 @@ public class AutoclickScrollPanelTest { // Test hover move. reset(mMockScrollPanelController); triggerHoverEvent(mUpButton, MotionEvent.ACTION_HOVER_MOVE); verify(mMockScrollPanelController).onHoverButtonChange( verify(mMockScrollPanelController, never()).onHoverButtonChange( eq(AutoclickScrollPanel.DIRECTION_UP), eq(/* hovered= */ true)); // Test hover exit. Loading Loading @@ -184,7 +184,7 @@ public class AutoclickScrollPanelTest { triggerHoverEvent(mUpButton, MotionEvent.ACTION_HOVER_ENTER); triggerHoverEvent(mUpButton, MotionEvent.ACTION_HOVER_MOVE); triggerHoverEvent(mUpButton, MotionEvent.ACTION_HOVER_MOVE); verify(mMockScrollPanelController, times(3)).onHoverButtonChange( verify(mMockScrollPanelController, times(1)).onHoverButtonChange( eq(AutoclickScrollPanel.DIRECTION_UP), eq(true)); // Case 2. Move from left button to exit button. Loading @@ -192,17 +192,17 @@ public class AutoclickScrollPanelTest { triggerHoverEvent(mLeftButton, MotionEvent.ACTION_HOVER_ENTER); triggerHoverEvent(mLeftButton, MotionEvent.ACTION_HOVER_MOVE); triggerHoverEvent(mLeftButton, MotionEvent.ACTION_HOVER_EXIT); triggerHoverEvent(mExitButton, MotionEvent.ACTION_HOVER_MOVE); triggerHoverEvent(mExitButton, MotionEvent.ACTION_HOVER_ENTER); triggerHoverEvent(mExitButton, MotionEvent.ACTION_HOVER_MOVE); triggerHoverEvent(mExitButton, MotionEvent.ACTION_HOVER_EXIT); // Verify left button events - 2 'true' calls (enter+move) and 1 'false' call (exit). verify(mMockScrollPanelController, times(2)).onHoverButtonChange( // Verify left button events - 1 'true' call (enter) and 1 'false' call (exit). verify(mMockScrollPanelController, times(1)).onHoverButtonChange( eq(AutoclickScrollPanel.DIRECTION_LEFT), eq(/* hovered= */ true)); verify(mMockScrollPanelController).onHoverButtonChange( eq(AutoclickScrollPanel.DIRECTION_LEFT), eq(/* hovered= */ false)); // Verify exit button events - hover_move is ignored so 1 'true' call (enter) and 1 // 'false' call (exit). // Verify exit button events - 1 'true' call (enter) and 1 'false' call (exit). verify(mMockScrollPanelController).onHoverButtonChange( eq(AutoclickScrollPanel.DIRECTION_EXIT), eq(/* hovered= */ true)); verify(mMockScrollPanelController).onHoverButtonChange( Loading Loading
services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java +58 −24 Original line number Diff line number Diff line Loading @@ -92,8 +92,10 @@ public class AutoclickController extends BaseEventStreamTransformation { (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; private static final float SCROLL_AMOUNT = 0.5f; protected static final long CONTINUOUS_SCROLL_INTERVAL = 30; private Handler mContinuousScrollHandler; private Runnable mContinuousScrollRunnable; private final AccessibilityTraceManager mTrace; private final Context mContext; Loading Loading @@ -123,7 +125,8 @@ public class AutoclickController extends BaseEventStreamTransformation { private @AutoclickType int mActiveClickType = AUTOCLICK_TYPE_LEFT_CLICK; // Default scroll direction is DIRECTION_NONE. private @AutoclickScrollPanel.ScrollDirection int mHoveredDirection = DIRECTION_NONE; @VisibleForTesting protected @AutoclickScrollPanel.ScrollDirection int mHoveredDirection = DIRECTION_NONE; // True during the duration of a dragging event. private boolean mDragModeIsDragging = false; Loading Loading @@ -181,31 +184,22 @@ public class AutoclickController extends BaseEventStreamTransformation { // Update the hover direction. if (hovered) { mHoveredDirection = direction; } else if (mHoveredDirection == direction) { // Safety check: Only clear hover tracking if this is the same button // we're currently tracking. mHoveredDirection = AutoclickScrollPanel.DIRECTION_NONE; } // For exit button, we only trigger hover state changes, the autoclick system // will handle the countdown. // For exit button, return early and the autoclick system will handle the // countdown then exit scroll mode. if (direction == AutoclickScrollPanel.DIRECTION_EXIT) { return; } // Handle all non-exit buttons when hovered. if (hovered) { // Clear the indicator. if (mAutoclickIndicatorScheduler != null) { mAutoclickIndicatorScheduler.cancel(); if (mAutoclickIndicatorView != null) { mAutoclickIndicatorView.clearIndicator(); } } // Perform scroll action. // For scroll directions, start continuous scrolling. if (direction != DIRECTION_NONE) { handleScroll(direction); startContinuousScroll(direction); } } else if (mHoveredDirection == direction) { // If not hovered, stop scrolling — but only if the mouse leaves the same // button that started it. This avoids stopping the scroll when the mouse // briefly moves over other buttons. stopContinuousScroll(); } } }; Loading Loading @@ -267,6 +261,16 @@ public class AutoclickController extends BaseEventStreamTransformation { mAutoclickScrollPanel = new AutoclickScrollPanel(mContext, mWindowManager, mScrollPanelController); // Initialize continuous scroll handler and runnable. mContinuousScrollHandler = new Handler(handler.getLooper()); mContinuousScrollRunnable = new Runnable() { @Override public void run() { handleScroll(mHoveredDirection); mContinuousScrollHandler.postDelayed(this, CONTINUOUS_SCROLL_INTERVAL); } }; mAutoclickTypePanel.show(); mWindowManager.addView(mAutoclickIndicatorView, mAutoclickIndicatorView.getLayoutParams()); } Loading Loading @@ -323,6 +327,11 @@ public class AutoclickController extends BaseEventStreamTransformation { mAutoclickScrollPanel.hide(); mAutoclickScrollPanel = null; } if (mContinuousScrollHandler != null) { mContinuousScrollHandler.removeCallbacks(mContinuousScrollRunnable); mContinuousScrollHandler = null; } } private void scheduleClick(MotionEvent event, int policyFlags) { Loading Loading @@ -366,6 +375,14 @@ public class AutoclickController extends BaseEventStreamTransformation { * Handles scroll operations in the specified direction. */ private void handleScroll(@AutoclickScrollPanel.ScrollDirection int direction) { // Remove the autoclick indicator view when hovering on directional buttons. if (mAutoclickIndicatorScheduler != null) { mAutoclickIndicatorScheduler.cancel(); if (mAutoclickIndicatorView != null) { mAutoclickIndicatorView.clearIndicator(); } } final long now = SystemClock.uptimeMillis(); // Create pointer properties. Loading Loading @@ -426,8 +443,25 @@ public class AutoclickController extends BaseEventStreamTransformation { if (mAutoclickScrollPanel != null) { mAutoclickScrollPanel.hide(); } stopContinuousScroll(); } private void startContinuousScroll(@AutoclickScrollPanel.ScrollDirection int direction) { if (mContinuousScrollHandler != null) { handleScroll(direction); mContinuousScrollHandler.postDelayed(mContinuousScrollRunnable, CONTINUOUS_SCROLL_INTERVAL); } } private void stopContinuousScroll() { if (mContinuousScrollHandler != null) { mContinuousScrollHandler.removeCallbacks(mContinuousScrollRunnable); } mHoveredDirection = DIRECTION_NONE; } @VisibleForTesting void onChangeForTesting(boolean selfChange, Uri uri) { mAutoclickSettingsObserver.onChange(selfChange, uri); Loading
services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickScrollPanel.java +0 −9 Original line number Diff line number Diff line Loading @@ -227,15 +227,6 @@ public class AutoclickScrollPanel { case MotionEvent.ACTION_HOVER_ENTER: hovered = true; break; case MotionEvent.ACTION_HOVER_MOVE: // For direction buttons, continuously trigger scroll on hover move. if (direction != DIRECTION_EXIT) { hovered = true; } else { // Ignore hover move events for exit button. return true; } break; case MotionEvent.ACTION_HOVER_EXIT: hovered = false; break; Loading
services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickControllerTest.java +65 −4 Original line number Diff line number Diff line Loading @@ -18,6 +18,7 @@ package com.android.server.accessibility.autoclick; import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; import static com.android.server.accessibility.autoclick.AutoclickController.CONTINUOUS_SCROLL_INTERVAL; import static com.android.server.accessibility.autoclick.AutoclickTypePanel.AUTOCLICK_TYPE_RIGHT_CLICK; import static com.android.server.testutils.MockitoUtilsKt.eq; Loading Loading @@ -67,16 +68,20 @@ import java.util.List; @TestableLooper.RunWithLooper(setAsMainLooper = true) public class AutoclickControllerTest { @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule(); @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule(); @Rule public TestableContext mTestableContext = new TestableContext(getInstrumentation().getContext()); private TestableLooper mTestableLooper; @Mock private AccessibilityTraceManager mMockTrace; @Mock private WindowManager mMockWindowManager; @Mock private AccessibilityTraceManager mMockTrace; @Mock private WindowManager mMockWindowManager; private AutoclickController mController; private MotionEventCaptor mMotionEventCaptor; Loading Loading @@ -1271,6 +1276,62 @@ public class AutoclickControllerTest { assertThat(mController.hasOngoingLongPressForTesting()).isFalse(); } @Test @EnableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_AUTOCLICK_INDICATOR) public void continuousScroll_completeLifecycle() { // Set up event capturer to track scroll events. ScrollEventCaptor scrollCaptor = new ScrollEventCaptor(); mController.setNext(scrollCaptor); // Initialize controller. injectFakeMouseActionHoverMoveEvent(); // Set cursor position. float expectedX = 100f; float expectedY = 200f; mController.mScrollCursorX = expectedX; mController.mScrollCursorY = expectedY; // Start scrolling by hovering UP button. mController.mScrollPanelController.onHoverButtonChange( AutoclickScrollPanel.DIRECTION_UP, true); // Verify initial hover state and event. assertThat(mController.mHoveredDirection).isEqualTo(AutoclickScrollPanel.DIRECTION_UP); assertThat(scrollCaptor.eventCount).isEqualTo(1); assertThat(scrollCaptor.scrollEvent.getAxisValue(MotionEvent.AXIS_VSCROLL)).isGreaterThan( 0); // Simulate continuous scrolling by triggering runnable. scrollCaptor.eventCount = 0; // Advance time by CONTINUOUS_SCROLL_INTERVAL (30ms) and process messages. mTestableLooper.moveTimeForward(CONTINUOUS_SCROLL_INTERVAL); mTestableLooper.processAllMessages(); // Advance time again to trigger second scroll event. mTestableLooper.moveTimeForward(CONTINUOUS_SCROLL_INTERVAL); mTestableLooper.processAllMessages(); // Verify multiple scroll events were generated. assertThat(scrollCaptor.eventCount).isEqualTo(2); assertThat(scrollCaptor.scrollEvent.getX()).isEqualTo(expectedX); assertThat(scrollCaptor.scrollEvent.getY()).isEqualTo(expectedY); // Stop scrolling by un-hovering the button. mController.mScrollPanelController.onHoverButtonChange( AutoclickScrollPanel.DIRECTION_UP, false); // Verify direction is reset. assertThat(mController.mHoveredDirection).isEqualTo(AutoclickScrollPanel.DIRECTION_NONE); // Verify no more scroll events are generated after stopping. int countBeforeRunnable = scrollCaptor.eventCount; mTestableLooper.moveTimeForward(CONTINUOUS_SCROLL_INTERVAL); mTestableLooper.processAllMessages(); assertThat(scrollCaptor.eventCount).isEqualTo(countBeforeRunnable); } /** * ========================================================================= * Helper Functions Loading
services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickScrollPanelTest.java +7 −7 Original line number Diff line number Diff line Loading @@ -146,7 +146,7 @@ public class AutoclickScrollPanelTest { // Test hover move. reset(mMockScrollPanelController); triggerHoverEvent(mUpButton, MotionEvent.ACTION_HOVER_MOVE); verify(mMockScrollPanelController).onHoverButtonChange( verify(mMockScrollPanelController, never()).onHoverButtonChange( eq(AutoclickScrollPanel.DIRECTION_UP), eq(/* hovered= */ true)); // Test hover exit. Loading Loading @@ -184,7 +184,7 @@ public class AutoclickScrollPanelTest { triggerHoverEvent(mUpButton, MotionEvent.ACTION_HOVER_ENTER); triggerHoverEvent(mUpButton, MotionEvent.ACTION_HOVER_MOVE); triggerHoverEvent(mUpButton, MotionEvent.ACTION_HOVER_MOVE); verify(mMockScrollPanelController, times(3)).onHoverButtonChange( verify(mMockScrollPanelController, times(1)).onHoverButtonChange( eq(AutoclickScrollPanel.DIRECTION_UP), eq(true)); // Case 2. Move from left button to exit button. Loading @@ -192,17 +192,17 @@ public class AutoclickScrollPanelTest { triggerHoverEvent(mLeftButton, MotionEvent.ACTION_HOVER_ENTER); triggerHoverEvent(mLeftButton, MotionEvent.ACTION_HOVER_MOVE); triggerHoverEvent(mLeftButton, MotionEvent.ACTION_HOVER_EXIT); triggerHoverEvent(mExitButton, MotionEvent.ACTION_HOVER_MOVE); triggerHoverEvent(mExitButton, MotionEvent.ACTION_HOVER_ENTER); triggerHoverEvent(mExitButton, MotionEvent.ACTION_HOVER_MOVE); triggerHoverEvent(mExitButton, MotionEvent.ACTION_HOVER_EXIT); // Verify left button events - 2 'true' calls (enter+move) and 1 'false' call (exit). verify(mMockScrollPanelController, times(2)).onHoverButtonChange( // Verify left button events - 1 'true' call (enter) and 1 'false' call (exit). verify(mMockScrollPanelController, times(1)).onHoverButtonChange( eq(AutoclickScrollPanel.DIRECTION_LEFT), eq(/* hovered= */ true)); verify(mMockScrollPanelController).onHoverButtonChange( eq(AutoclickScrollPanel.DIRECTION_LEFT), eq(/* hovered= */ false)); // Verify exit button events - hover_move is ignored so 1 'true' call (enter) and 1 // 'false' call (exit). // Verify exit button events - 1 'true' call (enter) and 1 'false' call (exit). verify(mMockScrollPanelController).onHoverButtonChange( eq(AutoclickScrollPanel.DIRECTION_EXIT), eq(/* hovered= */ true)); verify(mMockScrollPanelController).onHoverButtonChange( Loading