Sends ACTION_CANCEL before the very first touch exploration HOVER event.

This resets the InputDispatcher state in case the user had a pointer
down (and kept it down) before enabling TalkBack or while booting up
the device before TalkBack has started up.

Fix: 364408887
Test: atest TouchExplorerTest
Test: touch and hold on the screen;
      start TalkBack with a shortcut or adb shell;
      lift your finger off the screen;
      touch the screen again, and observe that touch exploration
      functions properly.
Flag: com.android.server.accessibility.reset_input_dispatcher_before_first_touch_exploration
Change-Id: I1c2bdb7b3d32cd124257e60e8c91c073c3040f50
diff --git a/services/accessibility/accessibility.aconfig b/services/accessibility/accessibility.aconfig
index 8e2e0ad..3f604ca 100644
--- a/services/accessibility/accessibility.aconfig
+++ b/services/accessibility/accessibility.aconfig
@@ -209,6 +209,16 @@
 }
 
 flag {
+    name: "reset_input_dispatcher_before_first_touch_exploration"
+    namespace: "accessibility"
+    description: "Resets InputDispatcher state by sending ACTION_CANCEL before the first TouchExploration hover events"
+    bug: "364408887"
+    metadata {
+        purpose: PURPOSE_BUGFIX
+    }
+}
+
+flag {
     name: "scan_packages_without_lock"
     namespace: "accessibility"
     description: "Scans packages for accessibility service/activity info without holding the A11yMS lock"
@@ -221,6 +231,7 @@
     description: "Sends accessibility events in TouchExplorer#onAccessibilityEvent based on internal state to keep it consistent. This reduces test flakiness."
     bug: "295575684"
 }
+
 flag {
     name: "send_hover_events_based_on_event_stream"
     namespace: "accessibility"
diff --git a/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java b/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java
index 04b42e4..0ed239e 100644
--- a/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java
+++ b/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java
@@ -1613,6 +1613,19 @@
                 dispatchGesture(gestureEvent);
             }
             if (!mEvents.isEmpty() && !mRawEvents.isEmpty()) {
+                if (Flags.resetInputDispatcherBeforeFirstTouchExploration()
+                        && !mState.hasResetInputDispatcherState()) {
+                    // Cancel any possible ongoing touch gesture from before touch exploration
+                    // started. This clears out the InputDispatcher event stream state so that it
+                    // is ready to accept new injected HOVER events.
+                    mDispatcher.sendMotionEvent(
+                            mEvents.get(0),
+                            ACTION_CANCEL,
+                            mRawEvents.get(0),
+                            mPointerIdBits,
+                            mPolicyFlags);
+                    setHasResetInputDispatcherState(true);
+                }
                 // Deliver a down event.
                 mDispatcher.sendMotionEvent(
                         mEvents.get(0),
@@ -1773,4 +1786,9 @@
                 + ", mDraggingPointerId: " + mDraggingPointerId
                 + " }";
     }
+
+    @VisibleForTesting
+    void setHasResetInputDispatcherState(boolean value) {
+        mState.setHasResetInputDispatcherState(value);
+    }
 }
diff --git a/services/accessibility/java/com/android/server/accessibility/gestures/TouchState.java b/services/accessibility/java/com/android/server/accessibility/gestures/TouchState.java
index e13994e..f15b8ee 100644
--- a/services/accessibility/java/com/android/server/accessibility/gestures/TouchState.java
+++ b/services/accessibility/java/com/android/server/accessibility/gestures/TouchState.java
@@ -86,6 +86,7 @@
     private MotionEvent mLastInjectedHoverEvent;
     // The last injected hover event used for performing clicks.
     private MotionEvent mLastInjectedHoverEventForClick;
+    private boolean mHasResetInputDispatcherState;
     // The time of the last injected down.
     private long mLastInjectedDownEventTime;
     // Keep track of which pointers sent to the system are down.
@@ -361,6 +362,14 @@
         return mLastInjectedDownEventTime;
     }
 
+    boolean hasResetInputDispatcherState() {
+        return mHasResetInputDispatcherState;
+    }
+
+    void setHasResetInputDispatcherState(boolean value) {
+        mHasResetInputDispatcherState = value;
+    }
+
     public int getLastTouchedWindowId() {
         return mLastTouchedWindowId;
     }
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/gestures/TouchExplorerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/gestures/TouchExplorerTest.java
index 1cd61e9..e5005d1 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/gestures/TouchExplorerTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/gestures/TouchExplorerTest.java
@@ -44,6 +44,8 @@
 import android.graphics.PointF;
 import android.os.Looper;
 import android.os.SystemClock;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.SetFlagsRule;
 import android.testing.DexmakerShareClassLoaderRule;
 import android.view.InputDevice;
 import android.view.MotionEvent;
@@ -56,6 +58,7 @@
 import com.android.server.accessibility.AccessibilityManagerService;
 import com.android.server.accessibility.AccessibilityTraceManager;
 import com.android.server.accessibility.EventStreamTransformation;
+import com.android.server.accessibility.Flags;
 import com.android.server.accessibility.utils.GestureLogParser;
 import com.android.server.testutils.OffsettableClock;
 
@@ -119,6 +122,9 @@
     public final DexmakerShareClassLoaderRule mDexmakerShareClassLoaderRule =
             new DexmakerShareClassLoaderRule();
 
+    @Rule
+    public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
     /**
      * {@link TouchExplorer#sendDownForAllNotInjectedPointers} injecting events with the same object
      * is resulting {@link ArgumentCaptor} to capture events with last state. Before implementation
@@ -154,18 +160,43 @@
         mHandler = new TestHandler();
         mTouchExplorer = new TouchExplorer(mContext, mMockAms, null, mHandler);
         mTouchExplorer.setNext(mCaptor);
+        // Start TouchExplorer in the state where it has already reset InputDispatcher so that
+        // all tests do not start with an irrelevant ACTION_CANCEL.
+        mTouchExplorer.setHasResetInputDispatcherState(true);
     }
 
     @Test
     public void testOneFingerMove_shouldInjectHoverEvents() {
-        goFromStateClearTo(STATE_TOUCH_EXPLORING_1FINGER);
-        // Wait for transiting to touch exploring state.
+        triggerTouchExplorationWithOneFingerDownMoveUp();
+        assertCapturedEvents(ACTION_HOVER_ENTER, ACTION_HOVER_MOVE, ACTION_HOVER_EXIT);
+        assertState(STATE_TOUCH_EXPLORING);
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_RESET_INPUT_DISPATCHER_BEFORE_FIRST_TOUCH_EXPLORATION)
+    public void testStartTouchExploration_shouldResetInputDispatcherStateWithActionCancel() {
+        // Start TouchExplorer in the state where it has *not yet* reset InputDispatcher.
+        mTouchExplorer.setHasResetInputDispatcherState(false);
+        // Trigger touch exploration twice, with a handler fast-forward in between so TouchExplorer
+        // treats these as two separate interactions.
+        triggerTouchExplorationWithOneFingerDownMoveUp();
+        mHandler.fastForward(2 * USER_INTENT_TIMEOUT);
+        triggerTouchExplorationWithOneFingerDownMoveUp();
+
+        assertCapturedEvents(
+                ACTION_CANCEL, // Only one ACTION_CANCEL before the first touch exploration
+                ACTION_HOVER_ENTER, ACTION_HOVER_MOVE, ACTION_HOVER_EXIT,
+                ACTION_HOVER_ENTER, ACTION_HOVER_MOVE, ACTION_HOVER_EXIT);
+        assertState(STATE_TOUCH_EXPLORING);
+    }
+
+    private void triggerTouchExplorationWithOneFingerDownMoveUp() {
+        send(downEvent());
+        // Fast forward so that TouchExplorer's timeouts transition us to the 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);
+        send(upEvent());
     }
 
     /**