Loading services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java +47 −7 Original line number Diff line number Diff line Loading @@ -25,6 +25,7 @@ import static android.view.accessibility.AccessibilityManager.AUTOCLICK_REVERT_T import static com.android.server.accessibility.autoclick.AutoclickIndicatorView.SHOW_INDICATOR_DELAY_TIME; import static com.android.server.accessibility.autoclick.AutoclickTypePanel.AUTOCLICK_TYPE_DOUBLE_CLICK; import static com.android.server.accessibility.autoclick.AutoclickScrollPanel.DIRECTION_NONE; import static com.android.server.accessibility.autoclick.AutoclickTypePanel.AUTOCLICK_TYPE_LEFT_CLICK; import static com.android.server.accessibility.autoclick.AutoclickTypePanel.AUTOCLICK_TYPE_RIGHT_CLICK; import static com.android.server.accessibility.autoclick.AutoclickTypePanel.AUTOCLICK_TYPE_SCROLL; Loading Loading @@ -97,6 +98,9 @@ public class AutoclickController extends BaseEventStreamTransformation { // Default click type is left-click. private @AutoclickType int mActiveClickType = AUTOCLICK_TYPE_LEFT_CLICK; // Default scroll direction is DIRECTION_NONE. private @AutoclickScrollPanel.ScrollDirection int mHoveredDirection = DIRECTION_NONE; @VisibleForTesting final ClickPanelControllerInterface clickPanelController = new ClickPanelControllerInterface() { Loading Loading @@ -131,14 +135,26 @@ public class AutoclickController extends BaseEventStreamTransformation { final AutoclickScrollPanel.ScrollPanelControllerInterface mScrollPanelController = new AutoclickScrollPanel.ScrollPanelControllerInterface() { @Override public void handleScroll(@AutoclickScrollPanel.ScrollDirection int direction) { // TODO(b/388845721): Perform actual scroll. public void onHoverButtonChange( @AutoclickScrollPanel.ScrollDirection int direction, boolean hovered) { // 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. if (direction == AutoclickScrollPanel.DIRECTION_EXIT) { return; } @Override public void exitScrollMode() { if (mAutoclickScrollPanel != null) { mAutoclickScrollPanel.hide(); // For direction buttons, perform scroll action immediately. if (hovered && direction != AutoclickScrollPanel.DIRECTION_NONE) { handleScroll(direction); } } }; Loading Loading @@ -285,6 +301,22 @@ public class AutoclickController extends BaseEventStreamTransformation { } } /** * Handles scroll operations in the specified direction. */ public void handleScroll(@AutoclickScrollPanel.ScrollDirection int direction) { // TODO(b/388845721): Perform actual scroll. } /** * Exits scroll mode and hides the scroll panel UI. */ public void exitScrollMode() { if (mAutoclickScrollPanel != null) { mAutoclickScrollPanel.hide(); } } @VisibleForTesting void onChangeForTesting(boolean selfChange, Uri uri) { mAutoclickSettingsObserver.onChange(selfChange, uri); Loading Loading @@ -776,6 +808,14 @@ 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) { Loading services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickScrollPanel.java +44 −27 Original line number Diff line number Diff line Loading @@ -23,6 +23,7 @@ import android.content.Context; import android.graphics.PixelFormat; import android.view.Gravity; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.WindowInsets; import android.view.WindowManager; Loading @@ -41,12 +42,16 @@ public class AutoclickScrollPanel { public static final int DIRECTION_DOWN = 1; public static final int DIRECTION_LEFT = 2; public static final int DIRECTION_RIGHT = 3; public static final int DIRECTION_EXIT = 4; public static final int DIRECTION_NONE = 5; @IntDef({ DIRECTION_UP, DIRECTION_DOWN, DIRECTION_LEFT, DIRECTION_RIGHT DIRECTION_RIGHT, DIRECTION_EXIT, DIRECTION_NONE, }) @Retention(RetentionPolicy.SOURCE) public @interface ScrollDirection {} Loading @@ -70,16 +75,12 @@ public class AutoclickScrollPanel { */ public interface ScrollPanelControllerInterface { /** * Called when a scroll direction is hovered. * Called when a button hover state changes. * * @param direction The direction to scroll: one of {@link ScrollDirection} values. * @param direction The direction associated with the button. * @param hovered Whether the button is being hovered or not. */ void handleScroll(@ScrollDirection int direction); /** * Called when the exit button is hovered. */ void exitScrollMode(); void onHoverButtonChange(@ScrollDirection int direction, boolean hovered); } public AutoclickScrollPanel(Context context, WindowManager windowManager, Loading @@ -104,19 +105,12 @@ public class AutoclickScrollPanel { * Sets up hover listeners for scroll panel buttons. */ private void initializeButtonState() { // Set up hover listeners for direction buttons. setupHoverListenerForDirectionButton(mUpButton, DIRECTION_UP); setupHoverListenerForDirectionButton(mLeftButton, DIRECTION_LEFT); setupHoverListenerForDirectionButton(mRightButton, DIRECTION_RIGHT); setupHoverListenerForDirectionButton(mDownButton, DIRECTION_DOWN); // Set up hover listener for exit button. mExitButton.setOnHoverListener((v, event) -> { if (mScrollPanelController != null) { mScrollPanelController.exitScrollMode(); } return true; }); // Set up hover listeners for all buttons. setupHoverListenerForButton(mUpButton, DIRECTION_UP); setupHoverListenerForButton(mLeftButton, DIRECTION_LEFT); setupHoverListenerForButton(mRightButton, DIRECTION_RIGHT); setupHoverListenerForButton(mDownButton, DIRECTION_DOWN); setupHoverListenerForButton(mExitButton, DIRECTION_EXIT); } /** Loading @@ -142,14 +136,37 @@ public class AutoclickScrollPanel { } /** * Sets up a hover listener for a direction button. * Sets up a hover listener for a button. */ private void setupHoverListenerForDirectionButton(ImageButton button, @ScrollDirection int direction) { private void setupHoverListenerForButton(ImageButton button, @ScrollDirection int direction) { button.setOnHoverListener((v, event) -> { if (mScrollPanelController != null) { mScrollPanelController.handleScroll(direction); if (mScrollPanelController == null) { return true; } boolean hovered; switch (event.getAction()) { 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; default: return true; } // Notify the controller about the hover change. mScrollPanelController.onHoverButtonChange(direction, hovered); return true; }); } Loading services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickScrollPanelTest.java +96 −22 Original line number Diff line number Diff line Loading @@ -21,8 +21,12 @@ import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentat import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.times; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.never; import android.content.Context; import android.testing.AndroidTestingRunner; Loading Loading @@ -125,37 +129,107 @@ public class AutoclickScrollPanelTest { } @Test public void directionButtons_onHover_callsHandleScroll() { // Test up button. triggerHoverEvent(mUpButton); verify(mMockScrollPanelController).handleScroll(AutoclickScrollPanel.DIRECTION_UP); // Test down button. triggerHoverEvent(mDownButton); verify(mMockScrollPanelController).handleScroll(AutoclickScrollPanel.DIRECTION_DOWN); // Test left button. triggerHoverEvent(mLeftButton); verify(mMockScrollPanelController).handleScroll(AutoclickScrollPanel.DIRECTION_LEFT); // Test right button. triggerHoverEvent(mRightButton); verify(mMockScrollPanelController).handleScroll(AutoclickScrollPanel.DIRECTION_RIGHT); public void directionButtons_hoverEvents_callsHoverButtonChange() { // Test hover enter on direction button. triggerHoverEvent(mUpButton, MotionEvent.ACTION_HOVER_ENTER); verify(mMockScrollPanelController).onHoverButtonChange( eq(AutoclickScrollPanel.DIRECTION_UP), eq(/* hovered= */ true)); // Test hover move. reset(mMockScrollPanelController); triggerHoverEvent(mUpButton, MotionEvent.ACTION_HOVER_MOVE); verify(mMockScrollPanelController).onHoverButtonChange( eq(AutoclickScrollPanel.DIRECTION_UP), eq(/* hovered= */ true)); // Test hover exit. reset(mMockScrollPanelController); triggerHoverEvent(mUpButton, MotionEvent.ACTION_HOVER_EXIT); verify(mMockScrollPanelController).onHoverButtonChange( eq(AutoclickScrollPanel.DIRECTION_UP), eq(/* hovered= */ false)); } @Test public void exitButton_onHover_callsExitScrollMode() { // Test exit button. triggerHoverEvent(mExitButton); verify(mMockScrollPanelController).exitScrollMode(); public void exitButton_hoverEvents_callsHoverButtonChange() { // Test hover enter on exit button. triggerHoverEvent(mExitButton, MotionEvent.ACTION_HOVER_ENTER); verify(mMockScrollPanelController).onHoverButtonChange( eq(AutoclickScrollPanel.DIRECTION_EXIT), eq(/* hovered= */ true)); // Test hover exit - should call the hover change method with false. reset(mMockScrollPanelController); triggerHoverEvent(mExitButton, MotionEvent.ACTION_HOVER_EXIT); verify(mMockScrollPanelController).onHoverButtonChange( eq(AutoclickScrollPanel.DIRECTION_EXIT), eq(/* hovered= */ false)); // Test exit button hover move - should be ignored. reset(mMockScrollPanelController); triggerHoverEvent(mExitButton, MotionEvent.ACTION_HOVER_MOVE); verify(mMockScrollPanelController, never()).onHoverButtonChange( eq(AutoclickScrollPanel.DIRECTION_EXIT), anyBoolean()); } @Test public void hoverOnButtonSequence_handledCorrectly() { // Test a realistic sequence of events. // Case 1. Hover enter on up button, then hover move with in up button twice. reset(mMockScrollPanelController); triggerHoverEvent(mUpButton, MotionEvent.ACTION_HOVER_ENTER); triggerHoverEvent(mUpButton, MotionEvent.ACTION_HOVER_MOVE); triggerHoverEvent(mUpButton, MotionEvent.ACTION_HOVER_MOVE); verify(mMockScrollPanelController, times(3)).onHoverButtonChange( eq(AutoclickScrollPanel.DIRECTION_UP), eq(true)); // Case 2. Move from left button to exit button. reset(mMockScrollPanelController); 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_EXIT); // Verify left button events - 2 'true' calls (enter+move) and 1 'false' call (exit). verify(mMockScrollPanelController, times(2)).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(mMockScrollPanelController).onHoverButtonChange( eq(AutoclickScrollPanel.DIRECTION_EXIT), eq(/* hovered= */ true)); verify(mMockScrollPanelController).onHoverButtonChange( eq(AutoclickScrollPanel.DIRECTION_EXIT), eq(/* hovered= */ false)); // Case 3. Quick transitions between buttons: left → right → down → exit reset(mMockScrollPanelController); triggerHoverEvent(mLeftButton, MotionEvent.ACTION_HOVER_EXIT); triggerHoverEvent(mRightButton, MotionEvent.ACTION_HOVER_ENTER); triggerHoverEvent(mRightButton, MotionEvent.ACTION_HOVER_EXIT); triggerHoverEvent(mDownButton, MotionEvent.ACTION_HOVER_ENTER); triggerHoverEvent(mDownButton, MotionEvent.ACTION_HOVER_EXIT); triggerHoverEvent(mExitButton, MotionEvent.ACTION_HOVER_ENTER); // Verify all hover enter/exit events were properly handled verify(mMockScrollPanelController).onHoverButtonChange( eq(AutoclickScrollPanel.DIRECTION_LEFT), eq(/* hovered= */ false)); verify(mMockScrollPanelController).onHoverButtonChange( eq(AutoclickScrollPanel.DIRECTION_RIGHT), eq(/* hovered= */ true)); verify(mMockScrollPanelController).onHoverButtonChange( eq(AutoclickScrollPanel.DIRECTION_RIGHT), eq(/* hovered= */ false)); verify(mMockScrollPanelController).onHoverButtonChange( eq(AutoclickScrollPanel.DIRECTION_DOWN), eq(/* hovered= */ true)); verify(mMockScrollPanelController).onHoverButtonChange( eq(AutoclickScrollPanel.DIRECTION_DOWN), eq(/* hovered= */ false)); verify(mMockScrollPanelController).onHoverButtonChange( eq(AutoclickScrollPanel.DIRECTION_EXIT), eq(/* hovered= */ true)); } // Helper method to simulate a hover event on a view. private void triggerHoverEvent(View view) { private void triggerHoverEvent(View view, int action) { MotionEvent event = MotionEvent.obtain( /* downTime= */ 0, /* eventTime= */ 0, /* action= */ MotionEvent.ACTION_HOVER_ENTER, /* action= */ action, /* x= */ 0, /* y= */ 0, /* metaState= */ 0); Loading Loading
services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java +47 −7 Original line number Diff line number Diff line Loading @@ -25,6 +25,7 @@ import static android.view.accessibility.AccessibilityManager.AUTOCLICK_REVERT_T import static com.android.server.accessibility.autoclick.AutoclickIndicatorView.SHOW_INDICATOR_DELAY_TIME; import static com.android.server.accessibility.autoclick.AutoclickTypePanel.AUTOCLICK_TYPE_DOUBLE_CLICK; import static com.android.server.accessibility.autoclick.AutoclickScrollPanel.DIRECTION_NONE; import static com.android.server.accessibility.autoclick.AutoclickTypePanel.AUTOCLICK_TYPE_LEFT_CLICK; import static com.android.server.accessibility.autoclick.AutoclickTypePanel.AUTOCLICK_TYPE_RIGHT_CLICK; import static com.android.server.accessibility.autoclick.AutoclickTypePanel.AUTOCLICK_TYPE_SCROLL; Loading Loading @@ -97,6 +98,9 @@ public class AutoclickController extends BaseEventStreamTransformation { // Default click type is left-click. private @AutoclickType int mActiveClickType = AUTOCLICK_TYPE_LEFT_CLICK; // Default scroll direction is DIRECTION_NONE. private @AutoclickScrollPanel.ScrollDirection int mHoveredDirection = DIRECTION_NONE; @VisibleForTesting final ClickPanelControllerInterface clickPanelController = new ClickPanelControllerInterface() { Loading Loading @@ -131,14 +135,26 @@ public class AutoclickController extends BaseEventStreamTransformation { final AutoclickScrollPanel.ScrollPanelControllerInterface mScrollPanelController = new AutoclickScrollPanel.ScrollPanelControllerInterface() { @Override public void handleScroll(@AutoclickScrollPanel.ScrollDirection int direction) { // TODO(b/388845721): Perform actual scroll. public void onHoverButtonChange( @AutoclickScrollPanel.ScrollDirection int direction, boolean hovered) { // 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. if (direction == AutoclickScrollPanel.DIRECTION_EXIT) { return; } @Override public void exitScrollMode() { if (mAutoclickScrollPanel != null) { mAutoclickScrollPanel.hide(); // For direction buttons, perform scroll action immediately. if (hovered && direction != AutoclickScrollPanel.DIRECTION_NONE) { handleScroll(direction); } } }; Loading Loading @@ -285,6 +301,22 @@ public class AutoclickController extends BaseEventStreamTransformation { } } /** * Handles scroll operations in the specified direction. */ public void handleScroll(@AutoclickScrollPanel.ScrollDirection int direction) { // TODO(b/388845721): Perform actual scroll. } /** * Exits scroll mode and hides the scroll panel UI. */ public void exitScrollMode() { if (mAutoclickScrollPanel != null) { mAutoclickScrollPanel.hide(); } } @VisibleForTesting void onChangeForTesting(boolean selfChange, Uri uri) { mAutoclickSettingsObserver.onChange(selfChange, uri); Loading Loading @@ -776,6 +808,14 @@ 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) { Loading
services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickScrollPanel.java +44 −27 Original line number Diff line number Diff line Loading @@ -23,6 +23,7 @@ import android.content.Context; import android.graphics.PixelFormat; import android.view.Gravity; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.WindowInsets; import android.view.WindowManager; Loading @@ -41,12 +42,16 @@ public class AutoclickScrollPanel { public static final int DIRECTION_DOWN = 1; public static final int DIRECTION_LEFT = 2; public static final int DIRECTION_RIGHT = 3; public static final int DIRECTION_EXIT = 4; public static final int DIRECTION_NONE = 5; @IntDef({ DIRECTION_UP, DIRECTION_DOWN, DIRECTION_LEFT, DIRECTION_RIGHT DIRECTION_RIGHT, DIRECTION_EXIT, DIRECTION_NONE, }) @Retention(RetentionPolicy.SOURCE) public @interface ScrollDirection {} Loading @@ -70,16 +75,12 @@ public class AutoclickScrollPanel { */ public interface ScrollPanelControllerInterface { /** * Called when a scroll direction is hovered. * Called when a button hover state changes. * * @param direction The direction to scroll: one of {@link ScrollDirection} values. * @param direction The direction associated with the button. * @param hovered Whether the button is being hovered or not. */ void handleScroll(@ScrollDirection int direction); /** * Called when the exit button is hovered. */ void exitScrollMode(); void onHoverButtonChange(@ScrollDirection int direction, boolean hovered); } public AutoclickScrollPanel(Context context, WindowManager windowManager, Loading @@ -104,19 +105,12 @@ public class AutoclickScrollPanel { * Sets up hover listeners for scroll panel buttons. */ private void initializeButtonState() { // Set up hover listeners for direction buttons. setupHoverListenerForDirectionButton(mUpButton, DIRECTION_UP); setupHoverListenerForDirectionButton(mLeftButton, DIRECTION_LEFT); setupHoverListenerForDirectionButton(mRightButton, DIRECTION_RIGHT); setupHoverListenerForDirectionButton(mDownButton, DIRECTION_DOWN); // Set up hover listener for exit button. mExitButton.setOnHoverListener((v, event) -> { if (mScrollPanelController != null) { mScrollPanelController.exitScrollMode(); } return true; }); // Set up hover listeners for all buttons. setupHoverListenerForButton(mUpButton, DIRECTION_UP); setupHoverListenerForButton(mLeftButton, DIRECTION_LEFT); setupHoverListenerForButton(mRightButton, DIRECTION_RIGHT); setupHoverListenerForButton(mDownButton, DIRECTION_DOWN); setupHoverListenerForButton(mExitButton, DIRECTION_EXIT); } /** Loading @@ -142,14 +136,37 @@ public class AutoclickScrollPanel { } /** * Sets up a hover listener for a direction button. * Sets up a hover listener for a button. */ private void setupHoverListenerForDirectionButton(ImageButton button, @ScrollDirection int direction) { private void setupHoverListenerForButton(ImageButton button, @ScrollDirection int direction) { button.setOnHoverListener((v, event) -> { if (mScrollPanelController != null) { mScrollPanelController.handleScroll(direction); if (mScrollPanelController == null) { return true; } boolean hovered; switch (event.getAction()) { 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; default: return true; } // Notify the controller about the hover change. mScrollPanelController.onHoverButtonChange(direction, hovered); return true; }); } Loading
services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickScrollPanelTest.java +96 −22 Original line number Diff line number Diff line Loading @@ -21,8 +21,12 @@ import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentat import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.times; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.never; import android.content.Context; import android.testing.AndroidTestingRunner; Loading Loading @@ -125,37 +129,107 @@ public class AutoclickScrollPanelTest { } @Test public void directionButtons_onHover_callsHandleScroll() { // Test up button. triggerHoverEvent(mUpButton); verify(mMockScrollPanelController).handleScroll(AutoclickScrollPanel.DIRECTION_UP); // Test down button. triggerHoverEvent(mDownButton); verify(mMockScrollPanelController).handleScroll(AutoclickScrollPanel.DIRECTION_DOWN); // Test left button. triggerHoverEvent(mLeftButton); verify(mMockScrollPanelController).handleScroll(AutoclickScrollPanel.DIRECTION_LEFT); // Test right button. triggerHoverEvent(mRightButton); verify(mMockScrollPanelController).handleScroll(AutoclickScrollPanel.DIRECTION_RIGHT); public void directionButtons_hoverEvents_callsHoverButtonChange() { // Test hover enter on direction button. triggerHoverEvent(mUpButton, MotionEvent.ACTION_HOVER_ENTER); verify(mMockScrollPanelController).onHoverButtonChange( eq(AutoclickScrollPanel.DIRECTION_UP), eq(/* hovered= */ true)); // Test hover move. reset(mMockScrollPanelController); triggerHoverEvent(mUpButton, MotionEvent.ACTION_HOVER_MOVE); verify(mMockScrollPanelController).onHoverButtonChange( eq(AutoclickScrollPanel.DIRECTION_UP), eq(/* hovered= */ true)); // Test hover exit. reset(mMockScrollPanelController); triggerHoverEvent(mUpButton, MotionEvent.ACTION_HOVER_EXIT); verify(mMockScrollPanelController).onHoverButtonChange( eq(AutoclickScrollPanel.DIRECTION_UP), eq(/* hovered= */ false)); } @Test public void exitButton_onHover_callsExitScrollMode() { // Test exit button. triggerHoverEvent(mExitButton); verify(mMockScrollPanelController).exitScrollMode(); public void exitButton_hoverEvents_callsHoverButtonChange() { // Test hover enter on exit button. triggerHoverEvent(mExitButton, MotionEvent.ACTION_HOVER_ENTER); verify(mMockScrollPanelController).onHoverButtonChange( eq(AutoclickScrollPanel.DIRECTION_EXIT), eq(/* hovered= */ true)); // Test hover exit - should call the hover change method with false. reset(mMockScrollPanelController); triggerHoverEvent(mExitButton, MotionEvent.ACTION_HOVER_EXIT); verify(mMockScrollPanelController).onHoverButtonChange( eq(AutoclickScrollPanel.DIRECTION_EXIT), eq(/* hovered= */ false)); // Test exit button hover move - should be ignored. reset(mMockScrollPanelController); triggerHoverEvent(mExitButton, MotionEvent.ACTION_HOVER_MOVE); verify(mMockScrollPanelController, never()).onHoverButtonChange( eq(AutoclickScrollPanel.DIRECTION_EXIT), anyBoolean()); } @Test public void hoverOnButtonSequence_handledCorrectly() { // Test a realistic sequence of events. // Case 1. Hover enter on up button, then hover move with in up button twice. reset(mMockScrollPanelController); triggerHoverEvent(mUpButton, MotionEvent.ACTION_HOVER_ENTER); triggerHoverEvent(mUpButton, MotionEvent.ACTION_HOVER_MOVE); triggerHoverEvent(mUpButton, MotionEvent.ACTION_HOVER_MOVE); verify(mMockScrollPanelController, times(3)).onHoverButtonChange( eq(AutoclickScrollPanel.DIRECTION_UP), eq(true)); // Case 2. Move from left button to exit button. reset(mMockScrollPanelController); 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_EXIT); // Verify left button events - 2 'true' calls (enter+move) and 1 'false' call (exit). verify(mMockScrollPanelController, times(2)).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(mMockScrollPanelController).onHoverButtonChange( eq(AutoclickScrollPanel.DIRECTION_EXIT), eq(/* hovered= */ true)); verify(mMockScrollPanelController).onHoverButtonChange( eq(AutoclickScrollPanel.DIRECTION_EXIT), eq(/* hovered= */ false)); // Case 3. Quick transitions between buttons: left → right → down → exit reset(mMockScrollPanelController); triggerHoverEvent(mLeftButton, MotionEvent.ACTION_HOVER_EXIT); triggerHoverEvent(mRightButton, MotionEvent.ACTION_HOVER_ENTER); triggerHoverEvent(mRightButton, MotionEvent.ACTION_HOVER_EXIT); triggerHoverEvent(mDownButton, MotionEvent.ACTION_HOVER_ENTER); triggerHoverEvent(mDownButton, MotionEvent.ACTION_HOVER_EXIT); triggerHoverEvent(mExitButton, MotionEvent.ACTION_HOVER_ENTER); // Verify all hover enter/exit events were properly handled verify(mMockScrollPanelController).onHoverButtonChange( eq(AutoclickScrollPanel.DIRECTION_LEFT), eq(/* hovered= */ false)); verify(mMockScrollPanelController).onHoverButtonChange( eq(AutoclickScrollPanel.DIRECTION_RIGHT), eq(/* hovered= */ true)); verify(mMockScrollPanelController).onHoverButtonChange( eq(AutoclickScrollPanel.DIRECTION_RIGHT), eq(/* hovered= */ false)); verify(mMockScrollPanelController).onHoverButtonChange( eq(AutoclickScrollPanel.DIRECTION_DOWN), eq(/* hovered= */ true)); verify(mMockScrollPanelController).onHoverButtonChange( eq(AutoclickScrollPanel.DIRECTION_DOWN), eq(/* hovered= */ false)); verify(mMockScrollPanelController).onHoverButtonChange( eq(AutoclickScrollPanel.DIRECTION_EXIT), eq(/* hovered= */ true)); } // Helper method to simulate a hover event on a view. private void triggerHoverEvent(View view) { private void triggerHoverEvent(View view, int action) { MotionEvent event = MotionEvent.obtain( /* downTime= */ 0, /* eventTime= */ 0, /* action= */ MotionEvent.ACTION_HOVER_ENTER, /* action= */ action, /* x= */ 0, /* y= */ 0, /* metaState= */ 0); Loading