fix: check both pointer count and action before reusing MotionEvent (#178528)

Fix issue https://github.com/flutter/flutter/issues/169486

## Problem

After [PR #178015](https://github.com/flutter/flutter/pull/178015) was
merged, Android platform views experience gesture blocking after
multi-touch operations (e.g., pinch-to-zoom). The issue occurs because:

1. PR #178015 changed framework behavior to encode original pointer
count in events
2. Framework now sends events with **different actions** than the
original MotionEvent
- Example: Framework sends `ACTION_MOVE (2)` while original event is
`ACTION_POINTER_UP (6)`
3. The existing engine code only checked **pointer count**, not action
4. This caused the engine to use original event with wrong action →
gesture blocked

### Logs Showing the Issue

trackedEvent.pointerCount=2, touch.pointerCount=2, trackedEvent.action=6
(ACTION_POINTER_UP), touch.action=2 (ACTION_MOVE) → Pointer counts
MATCH, using original event → WebView receives ACTION_POINTER_UP but
expects ACTION_MOVE → Gesture blocked! 
This commit is contained in:
DoLT 2025-12-05 16:25:16 +09:00 committed by GitHub
parent d50dfb9bf7
commit 017974987f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 112 additions and 8 deletions

View File

@ -739,25 +739,26 @@ public class PlatformViewsController implements PlatformViewsAccessibilityDelega
.toArray(new PointerProperties[touch.pointerCount]);
if (!usingVirtualDiplay && trackedEvent != null) {
// We have the original event. Check if pointer counts match.
if (trackedEvent.getPointerCount() == touch.pointerCount) {
// Pointer counts match - we can safely use the original event with offset.
// We have the original event. Check if pointer counts and actions match.
if (trackedEvent.getPointerCount() == touch.pointerCount
&& trackedEvent.getAction() == touch.action) {
// This preserves the verifiable input flag.
translateMotionEvent(trackedEvent, pointerCoords);
return trackedEvent;
}
// Pointer count mismatch detected (e.g., gesture recognizer filtered some pointers).
// Pointer count or action mismatch detected
// (e.g., gesture recognizer filtered some pointers).
// This commonly occurs when:
// - Multi-touch gestures (zoom/pinch) are filtered by gesture recognizers
//
// We must reconstruct the event with the correct pointer count from Flutter.
// We must reconstruct the event with the correct pointer count and action from Flutter.
// Unfortunately, this loses Android's verifiable input flag because there is no
// public API to modify pointer count while preserving verifiability.
return MotionEvent.obtain(
trackedEvent.getDownTime(),
trackedEvent.getEventTime(),
trackedEvent.getAction(),
touch.action, // Use framework's action
touch.pointerCount, // Use framework's pointer count
pointerProperties,
pointerCoords,

View File

@ -407,8 +407,8 @@ public class PlatformViewsControllerTest {
frameWorkTouch,
false // usingVirtualDisplays
);
assertEquals(resolvedEvent.getAction(), original.getAction());
assertNotEquals(resolvedEvent.getAction(), frameWorkTouch.action);
assertEquals(frameWorkTouch.action, resolvedEvent.getAction());
assertNotEquals(original.getAction(), resolvedEvent.getAction());
}
private MotionEvent makePlatformViewTouchAndInvokeToMotionEvent(
@ -752,6 +752,109 @@ public class PlatformViewsControllerTest {
assertEquals(100.0f, resolvedEvent.getY(1), 0.001f);
}
@Test
public void toMotionEvent_handlesActionMismatch() {
// This test verifies the fix for action mismatch after PR #178015.
// When framework sends different action than original (e.g., ACTION_MOVE instead of
// ACTION_POINTER_UP during multi-touch), we must reconstruct the event.
MotionEventTracker motionEventTracker = MotionEventTracker.getInstance();
PlatformViewsController platformViewsController = new PlatformViewsController();
// Original multi-touch event with ACTION_POINTER_UP (action code 6)
// This happens when second finger lifts during zoom gesture
MotionEvent.PointerProperties[] properties = new MotionEvent.PointerProperties[2];
properties[0] = new MotionEvent.PointerProperties();
properties[0].id = 0;
properties[0].toolType = MotionEvent.TOOL_TYPE_FINGER;
properties[1] = new MotionEvent.PointerProperties();
properties[1].id = 1;
properties[1].toolType = MotionEvent.TOOL_TYPE_FINGER;
MotionEvent.PointerCoords[] coords = new MotionEvent.PointerCoords[2];
coords[0] = new MotionEvent.PointerCoords();
coords[0].x = 100;
coords[0].y = 100;
coords[1] = new MotionEvent.PointerCoords();
coords[1].x = 200;
coords[1].y = 200;
MotionEvent original =
MotionEvent.obtain(
10, // downTime
10, // eventTime
MotionEvent.ACTION_POINTER_UP, // action = 6
2, // pointerCount
properties,
coords,
0, // metaState
0, // buttonState
1.0f, // xPrecision
1.0f, // yPrecision
0, // deviceId
0, // edgeFlags
0, // source
0 // flags
);
MotionEventTracker.MotionEventId motionEventId = motionEventTracker.track(original);
// After PR #178015, framework sends ACTION_MOVE (2) instead of ACTION_POINTER_UP (6)
// Pointer count matches (2), but action is different
List<List<Integer>> frameworkPointerProperties =
Arrays.asList(
Arrays.asList(0, MotionEvent.TOOL_TYPE_FINGER),
Arrays.asList(1, MotionEvent.TOOL_TYPE_FINGER));
List<List<Double>> frameworkPointerCoords =
Arrays.asList(
Arrays.asList(0.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 100.0, 100.0),
Arrays.asList(0.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 200.0, 200.0));
PlatformViewTouch touch =
new PlatformViewTouch(
0, // viewId
original.getDownTime(),
original.getEventTime(),
MotionEvent.ACTION_MOVE, // Framework sends ACTION_MOVE (2)
2, // pointerCount - matches original!
frameworkPointerProperties,
frameworkPointerCoords,
original.getMetaState(),
original.getButtonState(),
original.getXPrecision(),
original.getYPrecision(),
original.getDeviceId(),
original.getEdgeFlags(),
original.getSource(),
original.getFlags(),
motionEventId.getId());
MotionEvent resolvedEvent =
platformViewsController.toMotionEvent(
1, // density
touch,
false // usingVirtualDisplays
);
// Verify that resolved event uses framework's action (ACTION_MOVE)
// not original (ACTION_POINTER_UP)
assertEquals(MotionEvent.ACTION_MOVE, resolvedEvent.getAction());
assertNotEquals(original.getAction(), resolvedEvent.getAction());
// Verify pointer count matches
assertEquals(2, resolvedEvent.getPointerCount());
// Verify coordinates are correct
assertEquals(100.0f, resolvedEvent.getX(0), 0.001f);
assertEquals(100.0f, resolvedEvent.getY(0), 0.001f);
assertEquals(200.0f, resolvedEvent.getX(1), 0.001f);
assertEquals(200.0f, resolvedEvent.getY(1), 0.001f);
// Verify other properties preserved
assertEquals(original.getDownTime(), resolvedEvent.getDownTime());
assertEquals(original.getEventTime(), resolvedEvent.getEventTime());
}
@Test
@Config(shadows = {ShadowFlutterJNI.class, ShadowPlatformTaskQueue.class})
public void getPlatformViewById_hybridComposition() {