diff options
| author | 2025-02-27 10:35:24 -0800 | |
|---|---|---|
| committer | 2025-02-27 10:35:24 -0800 | |
| commit | ee08f6fa711d8f74eb80d1ef729f887e3fd82cf9 (patch) | |
| tree | 16e48f60c75466e829f287fb7cea5146a327b715 | |
| parent | 5db8667fbee77f37fb17de84f0d014c618ee90b1 (diff) | |
| parent | e0bd39818ed644e25170b84091490db4417836eb (diff) | |
Merge "a11y: Resume autoclick when hovering panel" into main
7 files changed, 352 insertions, 13 deletions
diff --git a/core/res/res/layout/accessibility_autoclick_type_panel.xml b/core/res/res/layout/accessibility_autoclick_type_panel.xml index cedbdc175488..902ef7fc38e8 100644 --- a/core/res/res/layout/accessibility_autoclick_type_panel.xml +++ b/core/res/res/layout/accessibility_autoclick_type_panel.xml @@ -17,7 +17,7 @@ */ --> -<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" +<com.android.server.accessibility.autoclick.AutoclickLinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/accessibility_autoclick_type_panel" android:layout_width="wrap_content" android:layout_height="wrap_content" @@ -130,4 +130,4 @@ </LinearLayout> -</LinearLayout> +</com.android.server.accessibility.autoclick.AutoclickLinearLayout> 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 bb3c710b0c23..0f6f86b39458 100644 --- a/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java +++ b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java @@ -103,12 +103,16 @@ public class AutoclickController extends BaseEventStreamTransformation { @Override public void toggleAutoclickPause(boolean paused) { if (paused) { - if (mClickScheduler != null) { - mClickScheduler.cancel(); - } - if (mAutoclickIndicatorScheduler != null) { - mAutoclickIndicatorScheduler.cancel(); - } + cancelPendingClick(); + } + } + + @Override + public void onHoverChange(boolean hovered) { + // Cancel all pending clicks when the mouse moves outside the panel while + // autoclick is still paused. + if (!hovered && isPaused()) { + cancelPendingClick(); } } }; @@ -226,8 +230,17 @@ public class AutoclickController extends BaseEventStreamTransformation { } private boolean isPaused() { - // TODO (b/397460424): Unpause when hovering over panel. - return Flags.enableAutoclickIndicator() && mAutoclickTypePanel.isPaused(); + return Flags.enableAutoclickIndicator() && mAutoclickTypePanel.isPaused() + && !mAutoclickTypePanel.isHovered(); + } + + private void cancelPendingClick() { + if (mClickScheduler != null) { + mClickScheduler.cancel(); + } + if (mAutoclickIndicatorScheduler != null) { + mAutoclickIndicatorScheduler.cancel(); + } } @VisibleForTesting diff --git a/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickLinearLayout.java b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickLinearLayout.java new file mode 100644 index 000000000000..fe8adf75704d --- /dev/null +++ b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickLinearLayout.java @@ -0,0 +1,80 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.accessibility.autoclick; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.widget.LinearLayout; + +/** + * A custom LinearLayout that provides enhanced hover event handling. + * This class overrides hover methods to track hover events for the entire panel ViewGroup, + * including the descendant buttons. This allows for consistent hover behavior and feedback + * across the entire layout. + */ +public class AutoclickLinearLayout extends LinearLayout { + public interface OnHoverChangedListener { + /** + * Called when the hover state of the AutoclickLinearLayout changes. + * + * @param hovered {@code true} if the view is now hovered, {@code false} otherwise. + */ + void onHoverChanged(boolean hovered); + } + + private OnHoverChangedListener mListener; + + public AutoclickLinearLayout(Context context) { + super(context); + } + + public AutoclickLinearLayout(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public AutoclickLinearLayout(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public AutoclickLinearLayout(Context context, AttributeSet attrs, int defStyleAttr, + int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + public void setOnHoverChangedListener(OnHoverChangedListener listener) { + mListener = listener; + } + + @Override + public boolean onInterceptHoverEvent(MotionEvent event) { + int action = event.getActionMasked(); + setHovered(action == MotionEvent.ACTION_HOVER_ENTER + || action == MotionEvent.ACTION_HOVER_MOVE); + + return false; + } + + @Override + public void onHoverChanged(boolean hovered) { + super.onHoverChanged(hovered); + + if (mListener != null) { + mListener.onHoverChanged(hovered); + } + } +} diff --git a/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickTypePanel.java b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickTypePanel.java index ab4b3b13eece..57bbb4a7a0a7 100644 --- a/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickTypePanel.java +++ b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickTypePanel.java @@ -110,11 +110,18 @@ public class AutoclickTypePanel { * @param paused {@code true} to pause autoclick, {@code false} to resume. */ void toggleAutoclickPause(boolean paused); + + /** + * Called when the hovered state of the panel changes. + * + * @param hovered {@code true} if the panel is now hovered, {@code false} otherwise. + */ + void onHoverChange(boolean hovered); } private final Context mContext; - private final View mContentView; + private final AutoclickLinearLayout mContentView; private final WindowManager mWindowManager; @@ -164,8 +171,9 @@ public class AutoclickTypePanel { R.drawable.accessibility_autoclick_resume); mContentView = - LayoutInflater.from(context) + (AutoclickLinearLayout) LayoutInflater.from(context) .inflate(R.layout.accessibility_autoclick_type_panel, null); + mContentView.setOnHoverChangedListener(mClickPanelController::onHoverChange); mLeftClickButton = mContentView.findViewById(R.id.accessibility_autoclick_left_click_layout); mRightClickButton = @@ -339,6 +347,10 @@ public class AutoclickTypePanel { return mPaused; } + public boolean isHovered() { + return mContentView.isHovered(); + } + /** Toggles the panel expanded or collapsed state. */ private void togglePanelExpansion(@AutoclickType int clickType) { final LinearLayout button = getButtonFromClickType(clickType); @@ -520,7 +532,7 @@ public class AutoclickTypePanel { @VisibleForTesting @NonNull - View getContentViewForTesting() { + AutoclickLinearLayout getContentViewForTesting() { return mContentView; } 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 17d8882b487c..ea83825cd810 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 @@ -571,6 +571,95 @@ public class AutoclickControllerTest { assertThat(mController.mClickScheduler.getScheduledClickTimeForTesting()).isNotEqualTo(-1); } + @Test + @EnableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_AUTOCLICK_INDICATOR) + public void pauseButton_panelNotHovered_clickNotTriggeredWhenPaused() { + injectFakeMouseActionHoverMoveEvent(); + + // Pause autoclick and ensure the panel is not hovered. + AutoclickTypePanel mockAutoclickTypePanel = mock(AutoclickTypePanel.class); + when(mockAutoclickTypePanel.isPaused()).thenReturn(true); + when(mockAutoclickTypePanel.isHovered()).thenReturn(false); + mController.mAutoclickTypePanel = mockAutoclickTypePanel; + + // Send hover move event. + MotionEvent hoverMove = MotionEvent.obtain( + /* downTime= */ 0, + /* eventTime= */ 100, + /* action= */ MotionEvent.ACTION_HOVER_MOVE, + /* x= */ 30f, + /* y= */ 0f, + /* metaState= */ 0); + hoverMove.setSource(InputDevice.SOURCE_MOUSE); + mController.onMotionEvent(hoverMove, hoverMove, /* policyFlags= */ 0); + + // Verify click is not triggered. + assertThat(mController.mClickScheduler.getIsActiveForTesting()).isFalse(); + assertThat(mController.mClickScheduler.getScheduledClickTimeForTesting()).isEqualTo(-1); + } + + @Test + @EnableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_AUTOCLICK_INDICATOR) + public void pauseButton_panelHovered_clickTriggeredWhenPaused() { + injectFakeMouseActionHoverMoveEvent(); + + // Pause autoclick and hover the panel. + AutoclickTypePanel mockAutoclickTypePanel = mock(AutoclickTypePanel.class); + when(mockAutoclickTypePanel.isPaused()).thenReturn(true); + when(mockAutoclickTypePanel.isHovered()).thenReturn(true); + mController.mAutoclickTypePanel = mockAutoclickTypePanel; + + // Send hover move event. + MotionEvent hoverMove = MotionEvent.obtain( + /* downTime= */ 0, + /* eventTime= */ 100, + /* action= */ MotionEvent.ACTION_HOVER_MOVE, + /* x= */ 30f, + /* y= */ 0f, + /* metaState= */ 0); + hoverMove.setSource(InputDevice.SOURCE_MOUSE); + mController.onMotionEvent(hoverMove, hoverMove, /* policyFlags= */ 0); + + // Verify click is triggered. + assertThat(mController.mClickScheduler.getIsActiveForTesting()).isTrue(); + assertThat(mController.mClickScheduler.getScheduledClickTimeForTesting()).isNotEqualTo(-1); + } + + @Test + @EnableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_AUTOCLICK_INDICATOR) + public void pauseButton_unhoveringCancelsClickWhenPaused() { + injectFakeMouseActionHoverMoveEvent(); + + // Pause autoclick and hover the panel. + AutoclickTypePanel mockAutoclickTypePanel = mock(AutoclickTypePanel.class); + when(mockAutoclickTypePanel.isPaused()).thenReturn(true); + when(mockAutoclickTypePanel.isHovered()).thenReturn(true); + mController.mAutoclickTypePanel = mockAutoclickTypePanel; + + // Send hover move event. + MotionEvent hoverMove = MotionEvent.obtain( + /* downTime= */ 0, + /* eventTime= */ 100, + /* action= */ MotionEvent.ACTION_HOVER_MOVE, + /* x= */ 30f, + /* y= */ 0f, + /* metaState= */ 0); + hoverMove.setSource(InputDevice.SOURCE_MOUSE); + mController.onMotionEvent(hoverMove, hoverMove, /* policyFlags= */ 0); + + // Verify click is triggered. + assertThat(mController.mClickScheduler.getIsActiveForTesting()).isTrue(); + assertThat(mController.mClickScheduler.getScheduledClickTimeForTesting()).isNotEqualTo(-1); + + // Now simulate the pointer being moved outside the panel. + when(mockAutoclickTypePanel.isHovered()).thenReturn(false); + mController.clickPanelController.onHoverChange(/* hovered= */ false); + + // Verify pending click is canceled. + assertThat(mController.mClickScheduler.getIsActiveForTesting()).isFalse(); + assertThat(mController.mClickScheduler.getScheduledClickTimeForTesting()).isEqualTo(-1); + } + private void injectFakeMouseActionHoverMoveEvent() { MotionEvent event = getFakeMotionHoverMoveEvent(); event.setSource(InputDevice.SOURCE_MOUSE); diff --git a/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickLinearLayoutTest.java b/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickLinearLayoutTest.java new file mode 100644 index 000000000000..9e629f7c87a2 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickLinearLayoutTest.java @@ -0,0 +1,102 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.accessibility.autoclick; + +import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; + +import static com.google.common.truth.Truth.assertThat; + +import android.testing.AndroidTestingRunner; +import android.testing.TestableContext; +import android.view.MotionEvent; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Test cases for {@link AutoclickLinearLayout}. */ +@RunWith(AndroidTestingRunner.class) +public class AutoclickLinearLayoutTest { + private boolean mHovered; + + private final AutoclickLinearLayout.OnHoverChangedListener mListener = + new AutoclickLinearLayout.OnHoverChangedListener() { + @Override + public void onHoverChanged(boolean hovered) { + mHovered = hovered; + } + }; + + @Rule + public TestableContext mTestableContext = + new TestableContext(getInstrumentation().getContext()); + private AutoclickLinearLayout mAutoclickLinearLayout; + + @Before + public void setUp() { + mAutoclickLinearLayout = new AutoclickLinearLayout(mTestableContext); + } + + @Test + public void autoclickLinearLayout_hoverChangedListener_setHovered() { + mHovered = false; + mAutoclickLinearLayout.setOnHoverChangedListener(mListener); + mAutoclickLinearLayout.onHoverChanged(/* hovered= */ true); + assertThat(mHovered).isTrue(); + } + + @Test + public void autoclickLinearLayout_hoverChangedListener_setNotHovered() { + mHovered = true; + + mAutoclickLinearLayout.setOnHoverChangedListener(mListener); + mAutoclickLinearLayout.onHoverChanged(/* hovered= */ false); + assertThat(mHovered).isFalse(); + } + + @Test + public void autoclickLinearLayout_onInterceptHoverEvent_hovered() { + mAutoclickLinearLayout.setHovered(false); + mAutoclickLinearLayout.onInterceptHoverEvent( + getFakeMotionEvent(MotionEvent.ACTION_HOVER_ENTER)); + assertThat(mAutoclickLinearLayout.isHovered()).isTrue(); + + mAutoclickLinearLayout.setHovered(false); + mAutoclickLinearLayout.onInterceptHoverEvent( + getFakeMotionEvent(MotionEvent.ACTION_HOVER_MOVE)); + assertThat(mAutoclickLinearLayout.isHovered()).isTrue(); + } + + @Test + public void autoclickLinearLayout_onInterceptHoverEvent_hoveredExit() { + mAutoclickLinearLayout.setHovered(true); + mAutoclickLinearLayout.onInterceptHoverEvent( + getFakeMotionEvent(MotionEvent.ACTION_HOVER_EXIT)); + assertThat(mAutoclickLinearLayout.isHovered()).isFalse(); + } + + private MotionEvent getFakeMotionEvent(int motionEventAction) { + return MotionEvent.obtain( + /* downTime= */ 0, + /* eventTime= */ 0, + /* action= */ motionEventAction, + /* x= */ 0, + /* y= */ 0, + /* metaState= */ 0); + } +} diff --git a/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickTypePanelTest.java b/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickTypePanelTest.java index 9e123406dff5..f7b16c808c50 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickTypePanelTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickTypePanelTest.java @@ -78,6 +78,7 @@ public class AutoclickTypePanelTest { private @AutoclickType int mActiveClickType = AUTOCLICK_TYPE_LEFT_CLICK; private boolean mPaused; + private boolean mHovered; private final ClickPanelControllerInterface clickPanelController = new ClickPanelControllerInterface() { @@ -90,6 +91,11 @@ public class AutoclickTypePanelTest { public void toggleAutoclickPause(boolean paused) { mPaused = paused; } + + @Override + public void onHoverChange(boolean hovered) { + mHovered = hovered; + } }; @Before @@ -412,6 +418,33 @@ public class AutoclickTypePanelTest { upEvent.recycle(); } + @Test + public void hovered_IsHovered() { + AutoclickLinearLayout mContext = mAutoclickTypePanel.getContentViewForTesting(); + + assertThat(mAutoclickTypePanel.isHovered()).isFalse(); + mContext.onInterceptHoverEvent(getFakeMotionHoverMoveEvent()); + assertThat(mAutoclickTypePanel.isHovered()).isTrue(); + } + + @Test + public void hovered_OnHoverChange_isHovered() { + AutoclickLinearLayout mContext = mAutoclickTypePanel.getContentViewForTesting(); + + mHovered = false; + mContext.onHoverChanged(true); + assertThat(mHovered).isTrue(); + } + + @Test + public void hovered_OnHoverChange_isNotHovered() { + AutoclickLinearLayout mContext = mAutoclickTypePanel.getContentViewForTesting(); + + mHovered = true; + mContext.onHoverChanged(false); + assertThat(mHovered).isFalse(); + } + private void verifyButtonHasSelectedStyle(@NonNull LinearLayout button) { GradientDrawable gradientDrawable = (GradientDrawable) button.getBackground(); assertThat(gradientDrawable.getColor().getDefaultColor()) @@ -426,4 +459,14 @@ public class AutoclickTypePanelTest { assertThat(params.x).isEqualTo(expectedPosition[2]); assertThat(params.y).isEqualTo(expectedPosition[3]); } + + private MotionEvent getFakeMotionHoverMoveEvent() { + return MotionEvent.obtain( + /* downTime= */ 0, + /* eventTime= */ 0, + /* action= */ MotionEvent.ACTION_HOVER_MOVE, + /* x= */ 0, + /* y= */ 0, + /* metaState= */ 0); + } } |