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

Commit cba2e124 authored by Ryan Lin's avatar Ryan Lin Committed by Android (Google) Code Review
Browse files

Merge "Fix incorrect order of Accessibility events"

parents 46f4713e 11aa986b
Loading
Loading
Loading
Loading
+29 −4
Original line number Diff line number Diff line
@@ -37,6 +37,7 @@ import static android.view.accessibility.AccessibilityEvent.TYPE_VIEW_HOVER_EXIT
import static com.android.server.accessibility.gestures.TouchState.ALL_POINTER_ID_BITS;

import android.accessibilityservice.AccessibilityGestureEvent;
import android.annotation.NonNull;
import android.content.Context;
import android.graphics.Point;
import android.graphics.Region;
@@ -48,6 +49,7 @@ import android.view.ViewConfiguration;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityNodeInfo;

import com.android.internal.annotations.VisibleForTesting;
import com.android.server.accessibility.AccessibilityManagerService;
import com.android.server.accessibility.BaseEventStreamTransformation;
import com.android.server.accessibility.EventStreamTransformation;
@@ -162,6 +164,12 @@ public class TouchExplorer extends BaseEventStreamTransformation
     */
    public TouchExplorer(
            Context context, AccessibilityManagerService service, GestureManifold detector) {
        this(context, service, detector, new Handler(context.getMainLooper()));
    }

    @VisibleForTesting
    TouchExplorer(Context context, AccessibilityManagerService service, GestureManifold detector,
            @NonNull Handler mainHandler) {
        mContext = context;
        mAms = service;
        mState = new TouchState();
@@ -169,7 +177,7 @@ public class TouchExplorer extends BaseEventStreamTransformation
        mDispatcher = new EventDispatcher(context, mAms, super.getNext(), mState);
        mDetermineUserIntentTimeout = ViewConfiguration.getDoubleTapTimeout();
        mDoubleTapSlop = ViewConfiguration.get(context).getScaledDoubleTapSlop();
        mHandler = new Handler(context.getMainLooper());
        mHandler = mainHandler;
        mExitGestureDetectionModeDelayed = new ExitGestureDetectionModeDelayed();
        mSendHoverEnterAndMoveDelayed = new SendHoverEnterAndMoveDelayed();
        mSendHoverExitDelayed = new SendHoverExitDelayed();
@@ -290,20 +298,37 @@ public class TouchExplorer extends BaseEventStreamTransformation
    public void onAccessibilityEvent(AccessibilityEvent event) {
        final int eventType = event.getEventType();

        if (eventType == TYPE_VIEW_HOVER_EXIT) {
            sendsPendingA11yEventsIfNeeded();
        }
        super.onAccessibilityEvent(event);
    }

    /*
     * Sends pending {@link AccessibilityEvent#TYPE_TOUCH_EXPLORATION_GESTURE_END} or {@{@link
     * AccessibilityEvent#TYPE_TOUCH_EXPLORATION_GESTURE_END}} after receiving last hover exit
     * event.
     */
    private void sendsPendingA11yEventsIfNeeded() {
        // The last hover exit A11y event should be sent by view after receiving hover exit motion
        // event. In some view hierarchy, the ViewGroup transforms hover move motion event to hover
        // exit motion event and than dispatch to itself. It causes unexpected A11y exit events.
        if (mSendHoverExitDelayed.isPending()) {
            return;
        }
        // The event for gesture end should be strictly after the
        // last hover exit event.
        if (mSendTouchExplorationEndDelayed.isPending() && eventType == TYPE_VIEW_HOVER_EXIT) {
        if (mSendTouchExplorationEndDelayed.isPending()) {
            mSendTouchExplorationEndDelayed.cancel();
            mDispatcher.sendAccessibilityEvent(TYPE_TOUCH_EXPLORATION_GESTURE_END);
        }

        // The event for touch interaction end should be strictly after the
        // last hover exit and the touch exploration gesture end events.
        if (mSendTouchInteractionEndDelayed.isPending() && eventType == TYPE_VIEW_HOVER_EXIT) {
        if (mSendTouchInteractionEndDelayed.isPending()) {
            mSendTouchInteractionEndDelayed.cancel();
            mDispatcher.sendAccessibilityEvent(TYPE_TOUCH_INTERACTION_END);
        }
        super.onAccessibilityEvent(event);
    }

    @Override
+128 −15
Original line number Diff line number Diff line
@@ -24,29 +24,32 @@ import static android.view.MotionEvent.ACTION_MOVE;
import static android.view.MotionEvent.ACTION_POINTER_DOWN;
import static android.view.MotionEvent.ACTION_POINTER_UP;
import static android.view.MotionEvent.ACTION_UP;
import static android.view.ViewConfiguration.getDoubleTapTimeout;

import static com.android.server.accessibility.gestures.TouchState.STATE_CLEAR;
import static com.android.server.accessibility.gestures.TouchState.STATE_DELEGATING;
import static com.android.server.accessibility.gestures.TouchState.STATE_DRAGGING;
import static com.android.server.accessibility.gestures.TouchState.STATE_GESTURE_DETECTING;
import static com.android.server.accessibility.gestures.TouchState.STATE_TOUCH_EXPLORING;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.mock;

import android.content.Context;
import android.graphics.PointF;
import android.os.Looper;
import android.os.SystemClock;
import android.testing.DexmakerShareClassLoaderRule;
import android.view.InputDevice;
import android.view.MotionEvent;
import android.view.ViewConfiguration;
import android.view.accessibility.AccessibilityEvent;

import androidx.test.InstrumentationRegistry;
import androidx.test.runner.AndroidJUnit4;

import com.android.server.accessibility.AccessibilityManagerService;
import com.android.server.accessibility.EventStreamTransformation;
import com.android.server.testutils.OffsettableClock;

import org.junit.Before;
import org.junit.Rule;
@@ -61,6 +64,8 @@ import java.util.List;
public class TouchExplorerTest {

    private static final String LOG_TAG = "TouchExplorerTest";
    // The constant of mDetermineUserIntentTimeout.
    private static final int USER_INTENT_TIMEOUT = getDoubleTapTimeout();
    private static final int FLAG_1FINGER = 0x8000;
    private static final int FLAG_2FINGERS = 0x0100;
    private static final int FLAG_3FINGERS = 0x0200;
@@ -80,6 +85,7 @@ public class TouchExplorerTest {

    private EventStreamTransformation mCaptor;
    private MotionEvent mLastEvent;
    private TestHandler mHandler;
    private TouchExplorer mTouchExplorer;
    private long mLastDownTime = Integer.MIN_VALUE;

@@ -112,26 +118,28 @@ public class TouchExplorerTest {

    @Before
    public void setUp() {
        if (Looper.myLooper() == null) {
            Looper.prepare();
        }
        Context context = InstrumentationRegistry.getContext();
        AccessibilityManagerService ams = new AccessibilityManagerService(context);
        GestureManifold detector = mock(GestureManifold.class);
        mCaptor = new EventCaptor();
        mTouchExplorer = new TouchExplorer(context, ams, detector);
        mHandler = new TestHandler();
        mTouchExplorer = new TouchExplorer(context, ams, detector, mHandler);
        mTouchExplorer.setNext(mCaptor);
    }

    @Test
    public void testOneFingerMove_shouldInjectHoverEvents() {
        goFromStateClearTo(STATE_TOUCH_EXPLORING_1FINGER);
        try {
            Thread.sleep(2 * ViewConfiguration.getDoubleTapTimeout());
        } catch (InterruptedException e) {
            fail("Interrupted while waiting for transition to touch exploring state.");
        }
        // Wait for transiting to touch exploring state.
        mHandler.fastForward(2 * USER_INTENT_TIMEOUT);
        moveEachPointers(mLastEvent, p(10, 10));
        send(mLastEvent);
        goToStateClearFrom(STATE_TOUCH_EXPLORING_1FINGER);
        assertCapturedEvents(ACTION_HOVER_ENTER, ACTION_HOVER_MOVE, ACTION_HOVER_EXIT);
        assertState(STATE_TOUCH_EXPLORING);
    }

    /**
@@ -144,11 +152,8 @@ public class TouchExplorerTest {
        // Inject a set of move events that have the same coordinates as the down event.
        moveEachPointers(mLastEvent, p(0, 0));
        send(mLastEvent);
        try {
            Thread.sleep(2 * ViewConfiguration.getDoubleTapTimeout());
        } catch (InterruptedException e) {
            fail("Interrupted while waiting for transition to touch exploring state.");
        }
        // Wait for transition to touch exploring state.
        mHandler.fastForward(2 * USER_INTENT_TIMEOUT);
        // Now move for real.
        moveEachPointers(mLastEvent, p(10, 10));
        send(mLastEvent);
@@ -181,6 +186,66 @@ public class TouchExplorerTest {
        assertCapturedEvents(ACTION_DOWN, ACTION_MOVE, ACTION_MOVE, ACTION_UP);
    }

    @Test
    public void testUpEvent_OneFingerMove_clearStateAndInjectHoverEvents() {
        goFromStateClearTo(STATE_TOUCH_EXPLORING_1FINGER);
        moveEachPointers(mLastEvent, p(10, 10));
        send(mLastEvent);

        send(upEvent());
        // Wait for sending hover exit event to transit to clear state.
        mHandler.fastForward(USER_INTENT_TIMEOUT);

        assertCapturedEvents(ACTION_HOVER_ENTER, ACTION_HOVER_MOVE, ACTION_HOVER_EXIT);
        assertState(STATE_CLEAR);
    }

    /*
     * The gesture should be completed in USER_INTENT_TIMEOUT duration otherwise the A11y
     * touch-exploration end event runnable will be scheduled after receiving the up event.
     * The distance between start and end point is shorter than the minimum swipe distance.
     * Note that the delayed time  of each runnable is USER_INTENT_TIMEOUT.
     */
    @Test
    public void testFlickCrossViews_clearStateAndExpectedEvents() {
        final int oneThirdUserIntentTimeout = USER_INTENT_TIMEOUT / 3;
        // Touch the first view.
        send(downEvent());

        // Wait for the finger moving to the second view.
        mHandler.fastForward(oneThirdUserIntentTimeout);
        moveEachPointers(mLastEvent, p(10, 10));
        send(mLastEvent);

        // Wait for the finger lifting from the second view.
        mHandler.fastForward(oneThirdUserIntentTimeout);
        // Now there are three delayed Runnables, hover enter/move runnable, hover exit motion event
        // runnable and a11y interaction end event runnable. The last two runnables are scheduled
        // after sending the up event.
        send(upEvent());

        // Wait for running hover enter/move runnable. The runnable is scheduled when sending
        // the down event.
        mHandler.fastForward(oneThirdUserIntentTimeout);
        // Wait for the views responding to hover enter/move events.
        mHandler.fastForward(oneThirdUserIntentTimeout);
        // Simulate receiving the a11y exit event sent by the first view.
        AccessibilityEvent a11yExitEvent = AccessibilityEvent.obtain(
                AccessibilityEvent.TYPE_VIEW_HOVER_EXIT);
        mTouchExplorer.onAccessibilityEvent(a11yExitEvent);

        // Wait for running the hover exit event runnable. After it, touch-exploration end event
        // runnable will be scheduled.
        mHandler.fastForward(oneThirdUserIntentTimeout);
        // Wait for the second views responding to hover exit events.
        mHandler.fastForward(oneThirdUserIntentTimeout);
        // Simulate receiving the a11y exit event sent by the second view.
        mTouchExplorer.onAccessibilityEvent(a11yExitEvent);

        assertCapturedEvents(ACTION_HOVER_ENTER, ACTION_HOVER_MOVE, ACTION_HOVER_EXIT);
        assertState(STATE_CLEAR);
    }

    @Test
    public void testTwoFingersMove_shouldDelegatingAndInjectActionDownPointerDown() {
        goFromStateClearTo(STATE_MOVING_2FINGERS);
@@ -307,7 +372,7 @@ public class TouchExplorerTest {
            }
        } catch (Throwable t) {
            throw new RuntimeException(
                    "Failed to go to state " + TouchState.getStateSymbolicName(state), t);
                    "Failed to go to state " + stateToString(state), t);
        }
    }

@@ -337,7 +402,7 @@ public class TouchExplorerTest {
            }
        } catch (Throwable t) {
            throw new RuntimeException(
                    "Failed to go to state " + TouchState.getStateSymbolicName(state), t);
                    "Failed to return to clear from state " + stateToString(state), t);
        }
    }

@@ -476,4 +541,52 @@ public class TouchExplorerTest {
                /* source */ InputDevice.SOURCE_TOUCHSCREEN,
                /* flags */ 0);
    }

    private static String stateToString(int state) {
        if (state <= STATE_GESTURE_DETECTING /* maximum value of touch state */) {
            return TouchState.getStateSymbolicName(state);
        }
        switch (state) {
            case STATE_TOUCH_EXPLORING_1FINGER:
                return "STATE_TOUCH_EXPLORING_1FINGER";
            case STATE_TOUCH_EXPLORING_2FINGER:
                return "STATE_TOUCH_EXPLORING_2FINGER";
            case STATE_TOUCH_EXPLORING_3FINGER:
                return "STATE_TOUCH_EXPLORING_3FINGER";
            case STATE_MOVING_2FINGERS:
                return "STATE_MOVING_2FINGERS";
            case STATE_MOVING_3FINGERS:
                return "STATE_MOVING_3FINGERS";
            case STATE_DRAGGING_2FINGERS:
                return "STATE_DRAGGING_2FINGERS";
            case STATE_PINCH_2FINGERS:
                return "STATE_PINCH_2FINGERS";
            default:
                return "stateToString -- Unknown state: " + Integer.toHexString(state);
        }
    }

    /**
     * A {@link android.os.Handler} that doesn't process messages until {@link
     * #fastForward(int)} is invoked.
     *
     * @see com.android.server.testutils.TestHandler
     */
    private class TestHandler extends com.android.server.testutils.TestHandler {
        private final OffsettableClock mClock;

        TestHandler() {
            this(null, new OffsettableClock.Stopped());
        }

        TestHandler(Callback callback, OffsettableClock clock) {
            super(Looper.myLooper(), callback, clock);
            mClock = clock;
        }

        void fastForward(int ms) {
            mClock.fastForward(ms);
            timeAdvance();
        }
    }
}
+17 −7
Original line number Diff line number Diff line
@@ -55,18 +55,24 @@ public class TestHandler extends Handler {
    // Boxing is ok here - both msg ids and their pending counts tend to be well below 128
    private final Map<Integer, Integer> mPendingMsgTypeCounts = new ArrayMap<>();
    private final LongSupplier mClock;
    private int  mMessageCount = 0;

    public TestHandler(Callback callback) {
        this(callback, DEFAULT_CLOCK);
    }

    public TestHandler(Callback callback, LongSupplier clock) {
        super(Looper.getMainLooper(), callback);
        this(Looper.getMainLooper(), callback, clock);
    }

    public TestHandler(Looper looper, Callback callback, LongSupplier clock) {
        super(looper, callback);
        mClock = clock;
    }

    @Override
    public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
        ++mMessageCount;
        mPendingMsgTypeCounts.put(msg.what,
                mPendingMsgTypeCounts.getOrDefault(msg.what, 0) + 1);

@@ -78,7 +84,7 @@ public class TestHandler extends Handler {

        // post a dummy queue entry to keep track of message removal
        return super.sendMessageAtTime(msg, Long.MAX_VALUE)
                && mMessages.add(new MsgInfo(Message.obtain(msg), uptimeMillis));
                && mMessages.add(new MsgInfo(Message.obtain(msg), uptimeMillis, mMessageCount));
    }

    /** @see TestHandler */
@@ -142,25 +148,29 @@ public class TestHandler extends Handler {
    public class MsgInfo implements Comparable<MsgInfo> {
        public final Message message;
        public final long sendTime;
        public final int mMessageOrder;
        public final RuntimeException postPoint;

        private MsgInfo(Message message, long sendTime) {
        private MsgInfo(Message message, long sendTime, int messageOrder) {
            this.message = message;
            this.sendTime = sendTime;
            this.postPoint = new RuntimeException("Message originated from here:");
            mMessageOrder = messageOrder;
        }

        @Override
        public int compareTo(MsgInfo o) {
            return Long.compare(sendTime, o.sendTime);
            final int result = Long.compare(sendTime, o.sendTime);
            return result != 0 ? result : Integer.compare(mMessageOrder, o.mMessageOrder);
        }

        @Override
        public String toString() {
            return "MsgInfo{" +
                    "message=" + messageToString(message) +
                    ", sendTime=" + sendTime +
                    '}';
                    "message =" + messageToString(message)
                    + ", sendTime =" + sendTime
                    + ", mMessageOrder =" + mMessageOrder
                    + '}';
        }
    }
}