Skip to content

fix(android): normalize MotionEvent deviceId so a gesture cannot split across two Unity input devices - #1057

Open
AhmedYAbbas wants to merge 2 commits into
juicycleff:masterfrom
Abdullah-Esnawi:fix/motionevent-deviceid-normalization
Open

fix(android): normalize MotionEvent deviceId so a gesture cannot split across two Unity input devices#1057
AhmedYAbbas wants to merge 2 commits into
juicycleff:masterfrom
Abdullah-Esnawi:fix/motionevent-deviceid-normalization

Conversation

@AhmedYAbbas

Copy link
Copy Markdown

Description

Fixes an Android input freeze where all 3D object interaction inside Unity goes
permanently silent after a drag, while UGUI keeps responding.

Root cause

CustomUnityPlayer.onTouchEvent only rewrote deviceId when it was exactly 0:

if (event.deviceId == 0) {
    val modifiedEvent = event.copy(deviceId = -1)
    ...
}

Android does not guarantee a stable deviceId across a single gesture in this
path. Measured on device, one drag arrived as DOWN(deviceId=5) followed by
UP(deviceId=0) with the same downTime. With the conditional rewrite, the
press went to Unity input device 5 and the release went to device -1, so
device 5's touch was never released. <Touchscreen>/Press latched pressed
forever and every action bound to it stopped firing, while <Pointer>-bound
UGUI was unaffected — which is why the symptom looks like "3D interaction broke
but the UI still works".

Fix

Rewrite deviceId unconditionally to a single NORMALIZED_DEVICE_ID = -1, so a
gesture's DOWN/MOVE/UP always land on one Unity input device. -1 is the
value the original Flutter workaround already used and is known to be picked up
by Unity's Input System.

Also fixed: double-recycle of the incoming event

The old code called event.recycle() on the event handed to onTouchEvent.
View.onTouchEvent does not take ownership — ViewRootImpl still owns that
event and recycles it after dispatch. Recycling here returned it to the pool
twice, so a later MotionEvent.obtain() could hand the same instance to two
owners. That was a real bug, though on-device measurement showed it was not
the cause of this freeze. The recycle() call is removed.

modifiedEvent is intentionally left unrecycled: Unity may retain it past the
call, and recycling it here would reintroduce a genuine use-after-free. An
un-recycled obtain() is only a missed pool return.

Scope

One file, Android only: android/.../CustomUnityPlayer.kt (+35 / -8). No public
API change, no Dart/iOS changes.

Abdullah-Esnawi and others added 2 commits June 22, 2026 16:03
…tor crash

UnityAppController does not implement applicationDidReceiveMemoryWarning:,
so forwarding the iOS memory-warning notification unconditionally throws an
unrecognized-selector NSException and aborts the app the first time a warning
fires while a heavy Unity scene loads. Guard the call with responds(to:).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants