Skip to content

Android pointer up/down events sent to platform views may include duplicate events or use inconsistent pointer IDs #178189

@jason-simmons

Description

@jason-simmons

While looking at #176574 I noticed that the Android platform view in the app would sometimes receive pointer up/down events that are duplicated or that contain inaccurate pointer IDs.

This can be observed by modifying the NativeView in the app from #176574 to log pointer up/down events:

class MyGestureHandlingView(context: Context) : View(context) {
    private val pointerIds: MutableSet<Int> = mutableSetOf()

    override fun onTouchEvent(event: MotionEvent): Boolean {
        val actionString = MotionEvent.actionToString(event.action)
        val pointerCount = event.pointerCount

        Log.d("NativeView", "NativeView: $actionString, Pointers: $pointerCount, Time: ${event.eventTime}")
        if (event.actionMasked == MotionEvent.ACTION_DOWN || event.actionMasked == MotionEvent.ACTION_POINTER_DOWN) {
            var id: Int;
            if (event.actionMasked == MotionEvent.ACTION_DOWN) {
                id = event.getPointerId(0)
                Log.d("NativeView", "*** DOWN pointerId=$id")
            } else {
                id = event.getPointerId(event.getActionIndex())
                Log.d("NativeView", "*** POINTER_DOWN actionIndex=${event.getActionIndex()} pointerId=$id")
            }
            if (pointerIds.contains(id)) {
                Log.d("NativeView", "*** DOWN MISMATCH id=$id")
            } else {
                pointerIds.add(id)
            }
        } else if (event.actionMasked == MotionEvent.ACTION_UP || event.actionMasked == MotionEvent.ACTION_POINTER_UP) {
            var id: Int;
            if (event.actionMasked == MotionEvent.ACTION_UP) {
                id = event.getPointerId(0)
                Log.d("NativeView", "*** UP pointerId=$id")
            } else {
                id = event.getPointerId(event.getActionIndex())
                Log.d("NativeView", "*** POINTER_UP actionIndex=${event.getActionIndex()} pointerId=$id")
            }
            if (pointerIds.contains(id)) {
                pointerIds.remove(id)
            } else {
                Log.d("NativeView", "*** UP MISMATCH id=$id")
            }
        }

        return super.onTouchEvent(event)
    }
}

Stress testing the app with random multi-pointer gestures will produce issues such as:

  • multiple down events for a pointer ID with no intervening up event
  • multiple up events for a pointer ID with no intervening down event
  • up events for a pointer ID with no previous down event for that ID
  • move events containing pointer IDs where there was no preceding down event

This typically happens because the Android embedder's MotionEventTracker is unable to look up the embedder ID in the pointer event message sent by the framework.

MotionEventTracker records the original Android MotionEvent instances received by the Flutter view. The embedder then sends the data from the events to the framework, which then reconstructs AndroidMotionEvent objects describing events that should be forwarded to the platform view.

The AndroidMotionEvent contains a motionEventId field that was generated when the MotionEventTracker added the original Android event. When the framework sends the AndroidMotionEvent to the embedder, the embedder looks up the motionEventId in the MotionEventTracker. If the tracker finds the original MotionEvent, then the original event is send to the platform view. Otherwise, the embedder sends an event based on the data in the AndroidMotionEvent created by the framework.

Mismatches can happen if one event in an up/down pair was a real Android event retrieved from the MotionEventTracker and the other event was a synthetic event based on the framework's state.

For example, the platform view may receive a synthetic down event containing a pointer ID that was generated by the framework's _AndroidMotionEventConverter. But the up event for the same pointer is the original Android event object stored in the MotionEventTracker and uses a real Android pointer ID with a different value.

The MotionEventTracker will often remove events prematurely because it assumes that all event lookups will happen in the order that the tracker received the events. MotionEventTracker assigns event IDs using an incrementing counter, and MotionEventTracker.pop will remove any events with an ID preceding the event it is trying to match.

But that is not a safe assumption because the framework's _PlatformViewGestureRecognizer may reorder events. The gesture recognizer may hold a pointer down event in its cache until it determines how to represent the gesture. While the down event is deferred, move events with higher event IDs may be sent to the embedder. The MotionEventTracker will delete earlier events including the down event. When the gesture recognizer eventually sends the down event, the MotionEventTracker lookup will fail and the platform view will get the synthetic MotionEvent instead of the original down event.

This also means that the platform view will receive pointer move events containing information about new pointers before the down event for the pointer. During the interval when the _PlatformViewGestureRecognizer is holding the down event, the MotionEventTracker lookups will successfully map the move events to original Android events that contain data for the new pointer. The embedder will send these events unmodified to the platform view even though the embedder has not yet sent that pointer's down event.

Many of these issues do not happen if I patch MotionEventTracker to not delete events based on the event ID. If the MotionEventTracker lookups succeed for every event, then the platform view will see events that look like the original Android event stream. Avoiding the need to generate synthetic events in the framework will reduce the potential for inconsistencies.

But even with that change, there are still other sources of mismatches. In particular, PointerRouter._dispatchEventToRoutes may send the same pointer up event twice to the platform view with the same motionEventId. If the embedder does not filter out the duplicates, then the platform view will see that event multiple times.

@reidbaker @gmackall

Metadata

Metadata

Assignees

Labels

P2Important issues not at the top of the work lista: platform-viewsEmbedding Android/iOS views in Flutter appsplatform-androidAndroid applications specificallyteam-androidOwned by Android platform team

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions