diff options
3 files changed, 187 insertions, 56 deletions
diff --git a/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java index 84158cf911ad..0b9c45de6e40 100644 --- a/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java +++ b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java @@ -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; @@ -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() { @@ -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; + } - @Override - public void exitScrollMode() { - if (mAutoclickScrollPanel != null) { - mAutoclickScrollPanel.hide(); + // For exit button, we only trigger hover state changes, the autoclick system + // will handle the countdown. + if (direction == AutoclickScrollPanel.DIRECTION_EXIT) { + return; + } + // For direction buttons, perform scroll action immediately. + if (hovered && direction != AutoclickScrollPanel.DIRECTION_NONE) { + handleScroll(direction); } } }; @@ -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); @@ -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) { diff --git a/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickScrollPanel.java b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickScrollPanel.java index e79be502a6fc..c71443149687 100644 --- a/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickScrollPanel.java +++ b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickScrollPanel.java @@ -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; @@ -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 {} @@ -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, @@ -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); } /** @@ -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; }); } diff --git a/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickScrollPanelTest.java b/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickScrollPanelTest.java index 02361ff259c2..4c71f7e994b8 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickScrollPanelTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickScrollPanelTest.java @@ -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; @@ -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); |