Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit ee08f6fa authored by Gavin Williams's avatar Gavin Williams Committed by Android (Google) Code Review
Browse files

Merge "a11y: Resume autoclick when hovering panel" into main

parents 5db8667f e0bd3981
Loading
Loading
Loading
Loading
+2 −2
Original line number Diff line number Diff line
@@ -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>
+21 −8
Original line number Diff line number Diff line
@@ -103,12 +103,16 @@ public class AutoclickController extends BaseEventStreamTransformation {
                @Override
                public void toggleAutoclickPause(boolean paused) {
                    if (paused) {
                        if (mClickScheduler != null) {
                            mClickScheduler.cancel();
                        cancelPendingClick();
                    }
                        if (mAutoclickIndicatorScheduler != null) {
                            mAutoclickIndicatorScheduler.cancel();
                }

                @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
+80 −0
Original line number Diff line number Diff line
/*
 * 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);
        }
    }
}
+15 −3
Original line number Diff line number Diff line
@@ -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;
    }

+89 −0
Original line number Diff line number Diff line
@@ -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);
Loading