Fix DelayedTransition async use original MotionEvent obj

Because MotionEvent can reuse, so if we async use MotionEvent, we
should use it's copy, or it will cause some exception randomly.

Bug: 280130713
Test: existing internal+CTS gesture tests
Test: atest TwoFingersDownOrSwipeTest
Change-Id: I5d123ac19e158a490f0f05e3f3112403ddf4e03e
Signed-off-by: Linnan Li <lilinnan@xiaomi.corp-partner.google.com>
diff --git a/core/java/android/accessibilityservice/AccessibilityGestureEvent.java b/core/java/android/accessibilityservice/AccessibilityGestureEvent.java
index 8e01779..15e29c2 100644
--- a/core/java/android/accessibilityservice/AccessibilityGestureEvent.java
+++ b/core/java/android/accessibilityservice/AccessibilityGestureEvent.java
@@ -150,12 +150,13 @@
     private final int mDisplayId;
     private List<MotionEvent> mMotionEvents = new ArrayList<>();
 
-/**
- * Constructs an AccessibilityGestureEvent to be dispatched to an accessibility service.
- * @param gestureId the id number of the gesture.
- * @param displayId the display on which this gesture was performed.
- * @param motionEvents the motion events that lead to this gesture.
- */
+    /**
+     * Constructs an AccessibilityGestureEvent to be dispatched to an accessibility service.
+     *
+     * @param gestureId    the id number of the gesture.
+     * @param displayId    the display on which this gesture was performed.
+     * @param motionEvents the motion events that lead to this gesture.
+     */
     public AccessibilityGestureEvent(
             int gestureId, int displayId, @NonNull List<MotionEvent> motionEvents) {
         mGestureId = gestureId;
@@ -205,6 +206,29 @@
         return mMotionEvents;
     }
 
+    /**
+     * When we asynchronously use {@link AccessibilityGestureEvent}, we should make a copy,
+     * because motionEvent may be recycled before we use async.
+     *
+     * @hide
+     */
+    @NonNull
+    public AccessibilityGestureEvent copyForAsync() {
+        return new AccessibilityGestureEvent(mGestureId, mDisplayId,
+                mMotionEvents.stream().map(MotionEvent::copy).toList());
+    }
+
+    /**
+     * After we use {@link AccessibilityGestureEvent} asynchronously, we should recycle the
+     * MotionEvent, avoid memory leaks.
+     *
+     * @hide
+     */
+    public void recycle() {
+        mMotionEvents.forEach(MotionEvent::recycle);
+        mMotionEvents.clear();
+    }
+
     @NonNull
     @Override
     public String toString() {
diff --git a/core/java/android/view/accessibility/flags/accessibility_flags.aconfig b/core/java/android/view/accessibility/flags/accessibility_flags.aconfig
index 7ab5446..e057660 100644
--- a/core/java/android/view/accessibility/flags/accessibility_flags.aconfig
+++ b/core/java/android/view/accessibility/flags/accessibility_flags.aconfig
@@ -39,6 +39,13 @@
 
 flag {
     namespace: "accessibility"
+    name: "copy_events_for_gesture_detection"
+    description: "Creates copies of MotionEvents and GestureEvents in GestureMatcher"
+    bug: "280130713"
+}
+
+flag {
+    namespace: "accessibility"
     name: "flash_notification_system_api"
     description: "Makes flash notification APIs as system APIs for calling from mainline module"
     bug: "303131332"
@@ -77,4 +84,4 @@
     namespace: "accessibility"
     description: "Feature flag for system pinch zoom gesture detector and related opt-out apis"
     bug: "283323770"
-}
\ No newline at end of file
+}
diff --git a/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java b/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java
index 7187895..0696807 100644
--- a/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java
+++ b/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java
@@ -1848,8 +1848,14 @@
     }
 
     public void notifyGesture(AccessibilityGestureEvent gestureEvent) {
-        mInvocationHandler.obtainMessage(InvocationHandler.MSG_ON_GESTURE,
-                gestureEvent).sendToTarget();
+        if (android.view.accessibility.Flags.copyEventsForGestureDetection()) {
+            // We will use this event async, so copy it because it contains MotionEvents.
+            mInvocationHandler.obtainMessage(InvocationHandler.MSG_ON_GESTURE,
+                    gestureEvent.copyForAsync()).sendToTarget();
+        } else {
+            mInvocationHandler.obtainMessage(InvocationHandler.MSG_ON_GESTURE,
+                    gestureEvent).sendToTarget();
+        }
     }
 
     public void notifySystemActionsChangedLocked() {
@@ -2323,9 +2329,13 @@
             final int type = message.what;
             switch (type) {
                 case MSG_ON_GESTURE: {
-                    notifyGestureInternal((AccessibilityGestureEvent) message.obj);
+                    if (message.obj instanceof AccessibilityGestureEvent gesture) {
+                        notifyGestureInternal(gesture);
+                        if (android.view.accessibility.Flags.copyEventsForGestureDetection()) {
+                            gesture.recycle();
+                        }
+                    }
                 } break;
-
                 case MSG_CLEAR_ACCESSIBILITY_CACHE: {
                     notifyClearAccessibilityCacheInternal();
                 } break;
diff --git a/services/accessibility/java/com/android/server/accessibility/gestures/GestureMatcher.java b/services/accessibility/java/com/android/server/accessibility/gestures/GestureMatcher.java
index 6e2fc69..3668eef 100644
--- a/services/accessibility/java/com/android/server/accessibility/gestures/GestureMatcher.java
+++ b/services/accessibility/java/com/android/server/accessibility/gestures/GestureMatcher.java
@@ -328,13 +328,21 @@
                                 + getStateSymbolicName(mTargetState));
             }
             mHandler.removeCallbacks(this);
+            recycleEvent();
         }
 
         public void post(
                 int state, long delay, MotionEvent event, MotionEvent rawEvent, int policyFlags) {
+            // Recycle the old event first if necessary, to handle duplicate calls to post.
+            recycleEvent();
             mTargetState = state;
-            mEvent = event;
-            mRawEvent = rawEvent;
+            if (android.view.accessibility.Flags.copyEventsForGestureDetection()) {
+                mEvent = event.copy();
+                mRawEvent = rawEvent.copy();
+            } else {
+                mEvent = event;
+                mRawEvent = rawEvent;
+            }
             mPolicyFlags = policyFlags;
             mHandler.postDelayed(this, delay);
             if (DEBUG) {
@@ -367,6 +375,19 @@
                                 + getStateSymbolicName(mTargetState));
             }
             setState(mTargetState, mEvent, mRawEvent, mPolicyFlags);
+            recycleEvent();
+        }
+
+        private void recycleEvent() {
+            if (android.view.accessibility.Flags.copyEventsForGestureDetection()) {
+                if (mEvent == null || mRawEvent == null) {
+                    return;
+                }
+                mEvent.recycle();
+                mRawEvent.recycle();
+                mEvent = null;
+                mRawEvent = null;
+            }
         }
     }
 
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/magnification/TwoFingersDownOrSwipeTest.java b/services/tests/servicestests/src/com/android/server/accessibility/magnification/TwoFingersDownOrSwipeTest.java
index 162d2a9..d94faec 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/magnification/TwoFingersDownOrSwipeTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/magnification/TwoFingersDownOrSwipeTest.java
@@ -20,6 +20,7 @@
 import static com.android.server.accessibility.utils.TouchEventGenerator.twoPointersDownEvents;
 
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.argThat;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.after;
 import static org.mockito.Mockito.timeout;
@@ -27,6 +28,10 @@
 
 import android.content.Context;
 import android.graphics.PointF;
+import android.platform.test.annotations.RequiresFlagsDisabled;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
 import android.view.Display;
 import android.view.MotionEvent;
 import android.view.ViewConfiguration;
@@ -37,6 +42,7 @@
 
 import org.junit.Before;
 import org.junit.BeforeClass;
+import org.junit.Rule;
 import org.junit.Test;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
@@ -48,6 +54,9 @@
  */
 public class TwoFingersDownOrSwipeTest {
 
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
     private static final float DEFAULT_X = 100f;
     private static final float DEFAULT_Y = 100f;
 
@@ -85,7 +94,8 @@
     }
 
     @Test
-    public void sendTwoFingerDownEvent_onGestureCompleted() {
+    @RequiresFlagsDisabled(android.view.accessibility.Flags.FLAG_COPY_EVENTS_FOR_GESTURE_DETECTION)
+    public void sendTwoFingerDownEvent_onGestureCompleted_withoutCopiedEvents() {
         final List<MotionEvent> downEvents = twoPointersDownEvents(Display.DEFAULT_DISPLAY,
                 new PointF(DEFAULT_X, DEFAULT_Y), new PointF(DEFAULT_X + 10, DEFAULT_Y + 10));
 
@@ -99,6 +109,23 @@
     }
 
     @Test
+    @RequiresFlagsEnabled(android.view.accessibility.Flags.FLAG_COPY_EVENTS_FOR_GESTURE_DETECTION)
+    public void sendTwoFingerDownEvent_onGestureCompleted() {
+        final List<MotionEvent> downEvents = twoPointersDownEvents(Display.DEFAULT_DISPLAY,
+                new PointF(DEFAULT_X, DEFAULT_Y), new PointF(DEFAULT_X + 10, DEFAULT_Y + 10));
+
+        for (MotionEvent event : downEvents) {
+            mGesturesObserver.onMotionEvent(event, event, 0);
+        }
+
+        verify(mListener, timeout(sTimeoutMillis)).onGestureCompleted(
+                eq(MagnificationGestureMatcher.GESTURE_TWO_FINGERS_DOWN_OR_SWIPE),
+                argThat(argument -> downEvents.get(1).getId() == argument.getId()),
+                argThat(argument -> downEvents.get(1).getId() == argument.getId()),
+                eq(0));
+    }
+
+    @Test
     public void sendSingleTapEvent_onGestureCancelled() {
         final MotionEvent downEvent = TouchEventGenerator.downEvent(Display.DEFAULT_DISPLAY,
                 DEFAULT_X, DEFAULT_Y);