From 017974987f614fa1f8ea46035563e4fc5a84d3de Mon Sep 17 00:00:00 2001 From: DoLT Date: Fri, 5 Dec 2025 16:25:16 +0900 Subject: [PATCH] fix: check both pointer count and action before reusing MotionEvent (#178528) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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! ❌ --- .../platform/PlatformViewsController.java | 13 ++- .../platform/PlatformViewsControllerTest.java | 107 +++++++++++++++++- 2 files changed, 112 insertions(+), 8 deletions(-) diff --git a/engine/src/flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java b/engine/src/flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java index 6c968cafe7e..22bacd050c1 100644 --- a/engine/src/flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java +++ b/engine/src/flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java @@ -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, diff --git a/engine/src/flutter/shell/platform/android/test/io/flutter/plugin/platform/PlatformViewsControllerTest.java b/engine/src/flutter/shell/platform/android/test/io/flutter/plugin/platform/PlatformViewsControllerTest.java index 2a92028136c..528216dea2b 100644 --- a/engine/src/flutter/shell/platform/android/test/io/flutter/plugin/platform/PlatformViewsControllerTest.java +++ b/engine/src/flutter/shell/platform/android/test/io/flutter/plugin/platform/PlatformViewsControllerTest.java @@ -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> frameworkPointerProperties = + Arrays.asList( + Arrays.asList(0, MotionEvent.TOOL_TYPE_FINGER), + Arrays.asList(1, MotionEvent.TOOL_TYPE_FINGER)); + + List> 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() {