summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Longbo Wei <longbowei@google.com> 2025-03-06 21:26:26 +0000
committer Longbo Wei <longbowei@google.com> 2025-03-12 15:50:13 +0000
commitaf17ef561655e2a48477cf65b859fe08b2108f2a (patch)
tree5f4bbf1d61c0aa2f15c2b5737803eb944f4e7335
parentecfab51084945ac0975fc6a85a85fcd432f88427 (diff)
a11y: Add countdown when hovering on exit
Right now, the scroll panel hides as soon as the user hovers over the exit button, which is not ideal because it can close by accident. This CL fixes it by adding a delay before hiding the panel when the exit button is hovered. The delay time is the same as the set countdown. Video: http://shortn/_JxFXNsIXKe Bug: b/401509893 Test: AutoclickScrollPaneTest Flag: com.android.server.accessibility.enable_autoclick_indicator Change-Id: I537131d57e97ef83305fdb492b93423dd36596ce
-rw-r--r--services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java54
-rw-r--r--services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickScrollPanel.java71
-rw-r--r--services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickScrollPanelTest.java118
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);