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);