Commit b41645cc authored by Jason Simmons's avatar Jason Simmons Committed by GitHub

Catch and log any exceptions thrown by an app's gesture recognizer callbacks (#6542)

If a recognizer is interrupted by an exception from a callback, it could be
left in an inconsistent state and be unable to process future events
parent ef386c15
......@@ -182,7 +182,7 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
_initialPosition = event.position;
_pendingDragOffset = Offset.zero;
if (onDown != null)
onDown(new DragDownDetails(globalPosition: _initialPosition));
invokeCallback/*<Null>*/('onDown', () => onDown(new DragDownDetails(globalPosition: _initialPosition)));
}
}
......@@ -196,11 +196,11 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
Offset delta = event.delta;
if (_state == _DragState.accepted) {
if (onUpdate != null) {
onUpdate(new DragUpdateDetails(
invokeCallback/*<Null>*/('onUpdate', () => onUpdate(new DragUpdateDetails(
delta: _getDeltaForDetails(delta),
primaryDelta: _getPrimaryDeltaForDetails(delta),
globalPosition: event.position
));
)));
}
} else {
_pendingDragOffset += delta;
......@@ -218,12 +218,12 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
Offset delta = _pendingDragOffset;
_pendingDragOffset = Offset.zero;
if (onStart != null)
onStart(new DragStartDetails(globalPosition: _initialPosition));
invokeCallback/*<Null>*/('onStart', () => onStart(new DragStartDetails(globalPosition: _initialPosition)));
if (delta != Offset.zero && onUpdate != null) {
onUpdate(new DragUpdateDetails(
invokeCallback/*<Null>*/('onUpdate', () => onUpdate(new DragUpdateDetails(
delta: _getDeltaForDetails(delta),
primaryDelta: _getPrimaryDeltaForDetails(delta)
));
)));
}
}
}
......@@ -239,7 +239,7 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
resolve(GestureDisposition.rejected);
_state = _DragState.ready;
if (onCancel != null)
onCancel();
invokeCallback/*<Null>*/('onCancel', onCancel);
return;
}
bool wasAccepted = (_state == _DragState.accepted);
......@@ -253,9 +253,9 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
final Offset pixelsPerSecond = velocity.pixelsPerSecond;
if (pixelsPerSecond.distanceSquared > kMaxFlingVelocity * kMaxFlingVelocity)
velocity = new Velocity(pixelsPerSecond: (pixelsPerSecond / pixelsPerSecond.distance) * kMaxFlingVelocity);
onEnd(new DragEndDetails(velocity: velocity));
invokeCallback/*<Null>*/('onEnd', () => onEnd(new DragEndDetails(velocity: velocity)));
} else {
onEnd(new DragEndDetails(velocity: Velocity.zero));
invokeCallback/*<Null>*/('onEnd', () => onEnd(new DragEndDetails(velocity: Velocity.zero)));
}
}
_velocityTrackers.clear();
......
......@@ -26,7 +26,7 @@ class LongPressGestureRecognizer extends PrimaryPointerGestureRecognizer {
void didExceedDeadline() {
resolve(GestureDisposition.accepted);
if (onLongPress != null)
onLongPress();
invokeCallback/*<Null>*/('onLongPress', onLongPress);
}
@override
......
......@@ -255,7 +255,7 @@ abstract class MultiDragGestureRecognizer<T extends MultiDragPointerState> exten
assert(state._pendingDelta != null);
Drag drag;
if (onStart != null)
drag = onStart(initialPosition);
drag = invokeCallback/*<Drag>*/('onStart', () => onStart(initialPosition));
if (drag != null) {
state._startDrag(drag);
} else {
......
......@@ -191,7 +191,7 @@ class DoubleTapGestureRecognizer extends GestureRecognizer {
_freezeTracker(tracker);
_trackers.remove(tracker.pointer);
if (onDoubleTap != null)
onDoubleTap();
invokeCallback/*<Null>*/('onDoubleTap', onDoubleTap);
_reset();
}
......@@ -359,7 +359,7 @@ class MultiTapGestureRecognizer extends GestureRecognizer {
longTapDelay: longTapDelay
);
if (onTapDown != null)
onTapDown(event.pointer, new TapDownDetails(globalPosition: event.position));
invokeCallback/*<Null>*/('onTapDown', () => onTapDown(event.pointer, new TapDownDetails(globalPosition: event.position)));
}
@override
......@@ -380,19 +380,19 @@ class MultiTapGestureRecognizer extends GestureRecognizer {
_gestureMap.remove(pointer);
if (resolution == _TapResolution.tap) {
if (onTapUp != null)
onTapUp(pointer, new TapUpDetails(globalPosition: globalPosition));
invokeCallback/*<Null>*/('onTapUp', () => onTapUp(pointer, new TapUpDetails(globalPosition: globalPosition)));
if (onTap != null)
onTap(pointer);
invokeCallback/*<Null>*/('onTap', () => onTap(pointer));
} else {
if (onTapCancel != null)
onTapCancel(pointer);
invokeCallback/*<Null>*/('onTapCancel', () => onTapCancel(pointer));
}
}
void _handleLongTap(int pointer, Point lastPosition) {
assert(_gestureMap.containsKey(pointer));
if (onLongTapDown != null)
onLongTapDown(pointer, new TapDownDetails(globalPosition: lastPosition));
invokeCallback/*<Null>*/('onLongTapDown', () => onLongTapDown(pointer, new TapDownDetails(globalPosition: lastPosition)));
}
@override
......
......@@ -6,6 +6,7 @@ import 'dart:async';
import 'dart:collection';
import 'dart:ui' show Point, Offset;
import 'package:flutter/foundation.dart';
import 'package:meta/meta.dart';
import 'arena.dart';
......@@ -16,6 +17,8 @@ import 'pointer_router.dart';
export 'pointer_router.dart' show PointerRouter;
typedef T RecognizerCallback<T>();
/// The base class that all GestureRecognizers should inherit from.
///
/// Provides a basic API that can be used by classes that work with
......@@ -48,6 +51,28 @@ abstract class GestureRecognizer extends GestureArenaMember {
/// Returns a very short pretty description of the gesture that the
/// recognizer looks for, like 'tap' or 'horizontal drag'.
String toStringShort() => toString();
/// Invoke a callback provided by the application and log any exceptions.
@protected
dynamic/*=T*/ invokeCallback/*<T>*/(String name, RecognizerCallback<dynamic/*=T*/> callback) {
dynamic/*=T*/ result;
try {
result = callback();
} catch (exception, stack) {
FlutterError.reportError(new FlutterErrorDetails(
exception: exception,
stack: stack,
library: 'gesture',
context: 'while handling a gesture',
informationCollector: (StringBuffer information) {
information.writeln('Handler: $name');
information.writeln('Recognizer:');
information.writeln(' $this');
}
));
}
return result;
}
}
/// Base class for gesture recognizers that can only recognize one
......
......@@ -180,9 +180,9 @@ class ScaleGestureRecognizer extends OneSequenceGestureRecognizer {
final Offset pixelsPerSecond = velocity.pixelsPerSecond;
if (pixelsPerSecond.distanceSquared > kMaxFlingVelocity * kMaxFlingVelocity)
velocity = new Velocity(pixelsPerSecond: (pixelsPerSecond / pixelsPerSecond.distance) * kMaxFlingVelocity);
onEnd(new ScaleEndDetails(velocity: velocity));
invokeCallback/*<Null>*/('onEnd', () => onEnd(new ScaleEndDetails(velocity: velocity)));
} else {
onEnd(new ScaleEndDetails(velocity: Velocity.zero));
invokeCallback/*<Null>*/('onEnd', () => onEnd(new ScaleEndDetails(velocity: Velocity.zero)));
}
}
_state = ScaleState.accepted;
......@@ -200,11 +200,11 @@ class ScaleGestureRecognizer extends OneSequenceGestureRecognizer {
if (_state == ScaleState.accepted && !configChanged) {
_state = ScaleState.started;
if (onStart != null)
onStart(new ScaleStartDetails(focalPoint: focalPoint));
invokeCallback/*<Null>*/('onStart', () => onStart(new ScaleStartDetails(focalPoint: focalPoint)));
}
if (_state == ScaleState.started && onUpdate != null)
onUpdate(new ScaleUpdateDetails(scale: _scaleFactor, focalPoint: focalPoint));
invokeCallback/*<Null>*/('onUpdate', () => onUpdate(new ScaleUpdateDetails(scale: _scaleFactor, focalPoint: focalPoint)));
}
@override
......
......@@ -99,7 +99,7 @@ class TapGestureRecognizer extends PrimaryPointerGestureRecognizer {
void resolve(GestureDisposition disposition) {
if (_wonArenaForPrimaryPointer && disposition == GestureDisposition.rejected) {
if (onTapCancel != null)
onTapCancel();
invokeCallback/*<Null>*/('onTapCancel', onTapCancel);
_reset();
}
super.resolve(disposition);
......@@ -126,7 +126,7 @@ class TapGestureRecognizer extends PrimaryPointerGestureRecognizer {
if (pointer == primaryPointer) {
assert(state == GestureRecognizerState.defunct);
if (onTapCancel != null)
onTapCancel();
invokeCallback/*<Null>*/('onTapCancel', onTapCancel);
_reset();
}
}
......@@ -134,7 +134,7 @@ class TapGestureRecognizer extends PrimaryPointerGestureRecognizer {
void _checkDown() {
if (!_sentTapDown) {
if (onTapDown != null)
onTapDown(new TapDownDetails(globalPosition: initialPosition));
invokeCallback/*<Null>*/('onTapDown', () => onTapDown(new TapDownDetails(globalPosition: initialPosition)));
_sentTapDown = true;
}
}
......@@ -143,9 +143,9 @@ class TapGestureRecognizer extends PrimaryPointerGestureRecognizer {
if (_wonArenaForPrimaryPointer && _finalPosition != null) {
resolve(GestureDisposition.accepted);
if (onTapUp != null)
onTapUp(new TapUpDetails(globalPosition: _finalPosition));
invokeCallback/*<Null>*/('onTapUp', () => onTapUp(new TapUpDetails(globalPosition: _finalPosition)));
if (onTap != null)
onTap();
invokeCallback/*<Null>*/('onTap', onTap);
_reset();
}
}
......
......@@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:test/test.dart';
......@@ -259,4 +260,28 @@ void main() {
tap.dispose();
});
testGesture('Should log exceptions from callbacks', (GestureTester tester) {
TapGestureRecognizer tap = new TapGestureRecognizer();
tap.onTap = () {
throw new Exception(test);
};
FlutterExceptionHandler previousErrorHandler = FlutterError.onError;
bool gotError = false;
FlutterError.onError = (FlutterErrorDetails details) {
gotError = true;
};
tap.addPointer(down1);
tester.closeArena(1);
tester.route(down1);
expect(gotError, isFalse);
tester.route(up1);
expect(gotError, isTrue);
FlutterError.onError = previousErrorHandler;
tap.dispose();
});
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment