[Android] Multi-finger gestures throw from MapView.onTouchEvent (invalid pointerIndex / NPE in MoveGestureDetector), surfacing as unhandled PlatformException

Aperta
#1,146 3 commenti 0 reazioni 0 assegnatari Vedi su GitHub

Nessuno ha ancora preso questa issue.

Valutazione

Difficoltà
4/5
Tempo stimato
3-5 giorni
Idoneità per principianti
45/100
Tipo di issue
Bug
Chiarezza
Specificata chiaramente
Stato di attività
Attiva
Stack tecnologico
android, dart, flutter
Ambito
backend, mobile

Direzione di ricerca

Start by tracing Android platform view touch forwarding into FlutterMapView.onTouchEvent in MapboxMapController.kt, then inspect the mapbox-gestures-android pointer tracking paths named in the stack traces (MultiFingerGesture and MoveGestureDetector). Reproduce with the supplied Chaos.java injector on an API 36 emulator and confirm the pointer warnings and exceptions. Done means fast multi-finger sequences no longer throw an unhandled PlatformException; the issue does not name a test file, so identify or add coverage for those sequences.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Descrizione

Environment

  • mapbox_maps_flutter: 2.29.0 (Maps SDK Android 11.29.0) and 2.31.1 (11.31.1), same result on both
  • Flutter 3.44.9
  • Android 16, seen in production on Samsung Galaxy S23 (SM-S911B) and Infinix, reproduced on the API 36 emulator
  • MapWidget with default settings (androidHostingMode: VD, textureView: true); same result with AndroidPlatformViewHostingMode.HC

Problem

Fast multi-finger gestures on the map (fingers going down and up during a pinch or rotate) make mapbox-gestures-android throw from MapView.onTouchEvent. The touch reaches the map through Flutter's platform view touch forwarding (PlatformViewsChannel.touch), so the exception comes back to Dart as a PlatformException from AndroidViewController.sendMotionEvent. Nothing awaits that call, so the error is unhandled and goes to PlatformDispatcher.onError. Crash reporters that use the common recordError(..., fatal: true) setup log it as a crash. The app keeps running and only that touch event is lost.

Right before each exception, logcat shows many W/MultiFingerGesture: Some MotionEvents were not passed to the library or events from different view trees are merged. The gesture detector's pointer tracking gets out of sync with the events it receives, then reads a pointer that is no longer in the event.

We see two variants:

java.lang.IllegalArgumentException: invalid pointerIndex -1 for MotionEvent { action=POINTER_DOWN(1), id[0]=2, x[0]=860.442, y[0]=920.47, id[1]=0, x[1]=461.221, y[1]=804.193, pointerCount=2, ... }
    at android.view.MotionEvent.nativeGetAxisValue(Native Method)
    at android.view.MotionEvent.getX(MotionEvent.java:2811)
    at com.mapbox.android.gestures.MultiFingerGesture.calculateDistances(MultiFingerGesture.java:223)
    at com.mapbox.android.gestures.MultiFingerGesture.analyzeEvent(MultiFingerGesture.java:104)
    at com.mapbox.android.gestures.ProgressiveGesture.analyzeEvent(ProgressiveGesture.java:55)
    at com.mapbox.android.gestures.BaseGesture.analyze(BaseGesture.java:61)
    at com.mapbox.android.gestures.BaseGesture.onTouchEvent(BaseGesture.java:39)
    at com.mapbox.android.gestures.AndroidGesturesManager.onTouchEvent(AndroidGesturesManager.java:196)
    at com.mapbox.maps.plugin.gestures.GesturesPluginImpl.onTouchEvent(GesturesPluginImpl.kt:341)
    at com.mapbox.maps.plugin.MapPluginRegistry.onTouch(MapPluginRegistry.kt:172)
    at com.mapbox.maps.MapController.onTouchEvent(MapController.kt:257)
    at com.mapbox.maps.MapView.onTouchEvent(MapView.kt:508)
    at com.mapbox.maps.mapbox_maps.FlutterMapView.onTouchEvent(MapboxMapController.kt:89)
    at android.view.View.performOnTouchCallback(View.java:16645)
    at android.view.View.dispatchTouchEvent(View.java:16598)
    at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3140)
    at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2814)
    at io.flutter.plugin.platform.PlatformViewsController$1.onTouch(PlatformViewsController.java:436)
    at io.flutter.plugin.platform.PlatformViewsControllerDelegator.onTouch(PlatformViewsControllerDelegator.java:92)
    at io.flutter.embedding.engine.systemchannels.PlatformViewsChannel$1.touch(PlatformViewsChannel.java:221)
    at io.flutter.embedding.engine.systemchannels.PlatformViewsChannel$1.onMethodCall(PlatformViewsChannel.java:67)
java.lang.NullPointerException: Attempt to invoke virtual method 'void com.mapbox.android.gestures.MoveDistancesObject.addNewPosition(float, float)' on a null object reference
    at com.mapbox.android.gestures.MoveGestureDetector.updateMoveDistancesObjects(MoveGestureDetector.java:200)
    at com.mapbox.android.gestures.MoveGestureDetector.analyzeMovement(MoveGestureDetector.java:155)
    at com.mapbox.android.gestures.MultiFingerGesture.analyzeEvent(MultiFingerGesture.java:106)
    at com.mapbox.android.gestures.ProgressiveGesture.analyzeEvent(ProgressiveGesture.java:55)
    at com.mapbox.android.gestures.MoveGestureDetector.analyzeEvent(MoveGestureDetector.java:149)
    at com.mapbox.android.gestures.BaseGesture.analyze(BaseGesture.java:61)
    at com.mapbox.android.gestures.BaseGesture.onTouchEvent(BaseGesture.java:39)
    at com.mapbox.android.gestures.AndroidGesturesManager.onTouchEvent(AndroidGesturesManager.java:196)
    at com.mapbox.maps.plugin.gestures.GesturesPluginImpl.onTouchEvent(GesturesPluginImpl.kt:340)
    at com.mapbox.maps.plugin.MapPluginRegistry.onTouch(MapPluginRegistry.kt:172)
    at com.mapbox.maps.MapController.onTouchEvent(MapController.kt:257)
    at com.mapbox.maps.MapView.onTouchEvent(MapView.kt:508)
    at com.mapbox.maps.mapbox_maps.FlutterMapView.onTouchEvent(MapboxMapController.kt:89)
    ... (same Flutter frames as above)

On the Dart side:

PlatformException(error, invalid pointerIndex -1 for MotionEvent { ... }, null, java.lang.IllegalArgumentException ...)
#0 StandardMethodCodec.decodeEnvelope (message_codecs.dart)
#1 MethodChannel._invokeMethod (platform_channel.dart)
#2 AndroidViewController.sendMotionEvent (platform_views.dart)
#3 AndroidViewController.dispatchPointerEvent (platform_views.dart)

Steps to reproduce

  1. Run any app with a full-screen MapWidget on an Android 16 emulator (API 36), debug build.
  2. Inject multi-finger touches with the small program below. It runs as the shell user and calls InputManagerGlobal.injectInputEvent, the same technique scrcpy uses. It only creates valid sequences: every POINTER_DOWN/POINTER_UP is balanced, and every gesture ends with ACTION_UP.
    javac -source 11 -target 11 -cp $ANDROID_HOME/platforms/android-36/android.jar -d . Chaos.java
    $ANDROID_HOME/build-tools/36.0.0/d8 --output . Chaos*.class
    adb push classes.dex /data/local/tmp/chaos.dex
    # 30 seconds, seed 7, fingers limited to x 40..860, y 260..1500 (free map area, no Flutter widgets on top)
    adb shell CLASSPATH=/data/local/tmp/chaos.dex app_process / Chaos 30 7 40 860 260 1500
    
  3. Watch logcat for MultiFingerGesture warnings and Failed to handle method call on flutter/platform_views.

Result: about 13 to 18 exceptions in 30 seconds, roughly 4,000 to 5,000 injected events, with both hosting modes (VD and HC). It still happens when every finger stays on the map and none lands on overlaid Flutter widgets.

Expected: the gesture detector tolerates the event stream it gets through Flutter's touch forwarding, for example by resetting its state when it sees an unknown or missing pointer, instead of throwing.

Chaos.java (touch injector)
import android.os.SystemClock;
import android.view.InputDevice;
import android.view.MotionEvent;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Random;

// Injects chaotic multi-finger touches as the shell user (same technique as scrcpy).
// Usage: app_process / Chaos <seconds> <seed> [xmin xmax ymin ymax]
public class Chaos {
  static Object im; static Method inject; static float XMIN = 5, XMAX = 1075, YMIN = 600, YMAX = 1800;

  static void init() throws Exception {
    Class<?> c;
    try { c = Class.forName("android.hardware.input.InputManagerGlobal"); }
    catch (ClassNotFoundException e) { c = Class.forName("android.hardware.input.InputManager"); }
    im = c.getMethod("getInstance").invoke(null);
    inject = c.getMethod("injectInputEvent", android.view.InputEvent.class, int.class);
  }

  static class F { int id; float a, r; }

  public static void main(String[] args) throws Exception {
    init(); if (args.length > 5) { XMIN = Float.parseFloat(args[2]); XMAX = Float.parseFloat(args[3]); YMIN = Float.parseFloat(args[4]); YMAX = Float.parseFloat(args[5]); }
    double secs = args.length > 0 ? Double.parseDouble(args[0]) : 20;
    Random rnd = new Random(args.length > 1 ? Long.parseLong(args[1]) : 1);
    long end = SystemClock.uptimeMillis() + (long) (secs * 1000);
    int injected = 0, gestures = 0, nextId = 0;
    while (SystemClock.uptimeMillis() < end) {
      gestures++;
      ArrayList<F> fs = new ArrayList<>();
      float cx = 540, cy = 1100;
      long down = SystemClock.uptimeMillis();
      F f0 = new F(); f0.id = nextId++ % 10; f0.a = rnd.nextFloat() * 6.28f; f0.r = 150; fs.add(f0);
      injected += send(down, MotionEvent.ACTION_DOWN, fs, cx, cy);
      int frames = 20 + rnd.nextInt(80);
      for (int i = 0; i < frames; i++) {
        SystemClock.sleep(8);
        double p = rnd.nextDouble();
        if (fs.size() < 4 && (fs.size() < 2 || p < 0.2)) {
          F f = new F(); f.id = freeId(fs, rnd); f.a = rnd.nextFloat() * 6.28f; f.r = 80 + rnd.nextFloat() * 250;
          int idx = rnd.nextInt(fs.size() + 1); fs.add(idx, f);
          injected += send(down, MotionEvent.ACTION_POINTER_DOWN | (idx << MotionEvent.ACTION_POINTER_INDEX_SHIFT), fs, cx, cy);
        } else if (fs.size() > 1 && p < 0.4) {
          int idx = rnd.nextInt(fs.size());
          injected += send(down, MotionEvent.ACTION_POINTER_UP | (idx << MotionEvent.ACTION_POINTER_INDEX_SHIFT), fs, cx, cy);
          fs.remove(idx);
        }
        cx = Math.max(250, Math.min(830, cx + rnd.nextFloat() * 30 - 15));
        cy = Math.max(800, Math.min(1400, cy + rnd.nextFloat() * 30 - 15));
        for (F f : fs) { f.a += rnd.nextFloat() * 0.5f - 0.2f; f.r = Math.max(40, Math.min(400, f.r + rnd.nextFloat() * 40 - 20)); }
        injected += send(down, MotionEvent.ACTION_MOVE, fs, cx, cy);
      }
      while (fs.size() > 1) {
        int idx = rnd.nextInt(fs.size());
        injected += send(down, MotionEvent.ACTION_POINTER_UP | (idx << MotionEvent.ACTION_POINTER_INDEX_SHIFT), fs, cx, cy);
        fs.remove(idx);
      }
      injected += send(down, MotionEvent.ACTION_UP, fs, cx, cy);
      SystemClock.sleep(rnd.nextInt(40));
    }
    System.out.println("gestos=" + gestures + " eventos=" + injected);
  }

  static int freeId(ArrayList<F> fs, Random rnd) {
    while (true) { int id = rnd.nextInt(10); boolean used = false; for (F f : fs) if (f.id == id) used = true; if (!used) return id; }
  }

  static int send(long down, int action, ArrayList<F> fs, float cx, float cy) throws Exception {
    int n = fs.size();
    MotionEvent.PointerProperties[] pp = new MotionEvent.PointerProperties[n];
    MotionEvent.PointerCoords[] pc = new MotionEvent.PointerCoords[n];
    for (int i = 0; i < n; i++) {
      F f = fs.get(i);
      pp[i] = new MotionEvent.PointerProperties(); pp[i].id = f.id; pp[i].toolType = MotionEvent.TOOL_TYPE_FINGER;
      pc[i] = new MotionEvent.PointerCoords();
      pc[i].x = (float) Math.max(XMIN, Math.min(XMAX, cx + f.r * Math.cos(f.a)));
      pc[i].y = (float) Math.max(YMIN, Math.min(YMAX, cy + f.r * Math.sin(f.a)));
      pc[i].pressure = 1; pc[i].size = 1;
    }
    MotionEvent e = MotionEvent.obtain(down, SystemClock.uptimeMillis(), action, n, pp, pc, 0, 0, 1, 1, 0, 0, InputDevice.SOURCE_TOUCHSCREEN, 0);
    boolean ok = (Boolean) inject.invoke(im, e, 0);
    e.recycle();
    return ok ? 1 : 0;
  }
}

Notes

  • In production this was the top crash in our Crashlytics: 11 events from one user on a Galaxy S23 in a single week, all marked fatal by the standard Flutter Crashlytics setup.
  • Our workaround: in PlatformDispatcher.onError, detect a PlatformException whose Java stack contains android.view.MotionEvent or .onTouchEvent( and record it as non-fatal.
  • A defensive try/catch in FlutterMapView.onTouchEvent, or a fix in MultiFingerGesture/MoveGestureDetector that resets its state instead of throwing, would remove the unhandled PlatformException for every app using the plugin.
Lingua principale
Dart
Stelle
379
Fork
205
Metriche di merge delle PR
Nessuna PR unita negli ultimi 30g

Preparare l'ambiente

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Altre issue di mapbox/mapbox-maps-flutter

Tutte le issue di mapbox/mapbox-maps-flutter

Issue simili

Altre issue su Dart

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.