a11y: Allow continuously scroll
Video: http://shortn/_mELXo72oHV
Bug: b/409650027
Test: AutoclickScrollPointIndicatorTest
Flag: com.android.server.accessibility.enable_autoclick_indicator
Change-Id: I162f0fd0f8f818e1fddfa787d3e81ef4969678f9
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 d7bc5df..13b2030 100644
--- a/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java
+++ b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java
@@ -92,8 +92,10 @@
(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;
@@ -123,7 +125,8 @@
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;
@@ -181,31 +184,22 @@
// 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;
- }
-
- // Handle all non-exit buttons when hovered.
- if (hovered) {
- // Clear the indicator.
- if (mAutoclickIndicatorScheduler != null) {
- mAutoclickIndicatorScheduler.cancel();
- if (mAutoclickIndicatorView != null) {
- mAutoclickIndicatorView.clearIndicator();
- }
+ // For exit button, return early and the autoclick system will handle the
+ // countdown then exit scroll mode.
+ if (direction == AutoclickScrollPanel.DIRECTION_EXIT) {
+ return;
}
- // 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();
}
}
};
@@ -267,6 +261,16 @@
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());
}
@@ -323,6 +327,11 @@
mAutoclickScrollPanel.hide();
mAutoclickScrollPanel = null;
}
+
+ if (mContinuousScrollHandler != null) {
+ mContinuousScrollHandler.removeCallbacks(mContinuousScrollRunnable);
+ mContinuousScrollHandler = null;
+ }
}
private void scheduleClick(MotionEvent event, int policyFlags) {
@@ -366,6 +375,14 @@
* 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.
@@ -426,8 +443,25 @@
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);
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 f0d0c45..fe111c3 100644
--- a/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickScrollPanel.java
+++ b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickScrollPanel.java
@@ -227,15 +227,6 @@
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;
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickControllerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickControllerTest.java
index 84320356..f838e9b 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickControllerTest.java
@@ -18,6 +18,7 @@
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;
@@ -67,16 +68,20 @@
@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;
@@ -1271,6 +1276,62 @@
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
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 f460c8c..94ae105 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
@@ -146,7 +146,7 @@
// 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.
@@ -184,7 +184,7 @@
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.
@@ -192,17 +192,17 @@
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(