Dismiss modal with any button press (#32770)

This PR makes ModalBarrier dismiss modal with any button press instead of primary button up, by making it use a private recognizer _AnyTapGestureRecognizer that claims victor and calls onAnyTapDown immediately after it receives any PointerDownEvent.
diff --git a/packages/flutter/lib/src/widgets/modal_barrier.dart b/packages/flutter/lib/src/widgets/modal_barrier.dart
index 567a7f9..e9be288 100644
--- a/packages/flutter/lib/src/widgets/modal_barrier.dart
+++ b/packages/flutter/lib/src/widgets/modal_barrier.dart
@@ -3,6 +3,9 @@
 // found in the LICENSE file.
 
 import 'package:flutter/foundation.dart';
+import 'package:flutter/gestures.dart' show
+  PrimaryPointerGestureRecognizer,
+  GestureDisposition;
 
 import 'basic.dart';
 import 'container.dart';
@@ -81,12 +84,11 @@
         // On Android, the back button is used to dismiss a modal. On iOS, some
         // modal barriers are not dismissible in accessibility mode.
         excluding: !semanticsDismissible || !modalBarrierSemanticsDismissible,
-        child: GestureDetector(
-          onTapDown: (TapDownDetails details) {
+        child: _ModalBarrierGestureDetector(
+          onAnyTapDown: () {
             if (dismissible)
               Navigator.maybePop(context);
           },
-          behavior: HitTestBehavior.opaque,
           child: Semantics(
             label: semanticsDismissible ? semanticsLabel : null,
             textDirection: semanticsDismissible && semanticsLabel != null ? Directionality.of(context) : null,
@@ -175,3 +177,102 @@
     );
   }
 }
+
+// Recognizes tap down by any pointer button unconditionally. When it receives a
+// PointerDownEvent, it immediately claims victor of arena and calls
+// [onAnyTapDown] without any checks.
+//
+// It is used by ModalBarrier to detect any taps on the overlay.
+class _AnyTapGestureRecognizer extends PrimaryPointerGestureRecognizer {
+  _AnyTapGestureRecognizer({
+    Object debugOwner,
+    this.onAnyTapDown,
+  }) : super(debugOwner: debugOwner);
+
+  VoidCallback onAnyTapDown;
+
+  bool _sentTapDown = false;
+
+  @override
+  void addAllowedPointer(PointerDownEvent event) {
+    super.addAllowedPointer(event);
+    resolve(GestureDisposition.accepted);
+  }
+
+  @override
+  void handlePrimaryPointer(PointerEvent event) {
+    if (!_sentTapDown) {
+      if (onAnyTapDown != null)
+        onAnyTapDown();
+      _sentTapDown = true;
+    }
+  }
+
+  @override
+  void didStopTrackingLastPointer(int pointer) {
+    super.didStopTrackingLastPointer(pointer);
+    _sentTapDown = false;
+  }
+
+  @override
+  String get debugDescription => 'any tap';
+}
+
+class _ModalBarrierSemanticsDelegate extends SemanticsGestureDelegate {
+  const _ModalBarrierSemanticsDelegate({this.onAnyTapDown});
+
+  final VoidCallback onAnyTapDown;
+
+  @override
+  void assignSemantics(RenderSemanticsGestureHandler renderObject) {
+    renderObject.onTap = onAnyTapDown;
+  }
+}
+
+class _AnyTapGestureRecognizerFactory extends GestureRecognizerFactory<_AnyTapGestureRecognizer> {
+  const _AnyTapGestureRecognizerFactory({this.onAnyTapDown});
+
+  final VoidCallback onAnyTapDown;
+
+  @override
+  _AnyTapGestureRecognizer constructor() => _AnyTapGestureRecognizer();
+
+  @override
+  void initializer(_AnyTapGestureRecognizer instance) {
+    instance.onAnyTapDown = onAnyTapDown;
+  }
+}
+
+// A GestureDetector used by ModalBarrier. It only has one callback,
+// [onAnyTapDown], which recognizes tap down unconditionally.
+class _ModalBarrierGestureDetector extends StatelessWidget {
+  const _ModalBarrierGestureDetector({
+    Key key,
+    @required this.child,
+    @required this.onAnyTapDown,
+  }) : assert(child != null),
+       assert(onAnyTapDown != null),
+       super(key: key);
+
+  /// The widget below this widget in the tree.
+  /// See [RawGestureDetector.child].
+  final Widget child;
+
+  /// Immediately called when a pointer causes a tap down.
+  /// See [_AnyTapGestureRecognizer.onAnyTapDown].
+  final VoidCallback onAnyTapDown;
+
+  @override
+  Widget build(BuildContext context) {
+    final Map<Type, GestureRecognizerFactory> gestures = <Type, GestureRecognizerFactory>{
+      _AnyTapGestureRecognizer: _AnyTapGestureRecognizerFactory(onAnyTapDown: onAnyTapDown),
+    };
+
+    return RawGestureDetector(
+      gestures: gestures,
+      behavior: HitTestBehavior.opaque,
+      semantics: _ModalBarrierSemanticsDelegate(onAnyTapDown: onAnyTapDown),
+      child: child,
+    );
+  }
+}
diff --git a/packages/flutter/test/widgets/modal_barrier_test.dart b/packages/flutter/test/widgets/modal_barrier_test.dart
index d80ec98..70584a1 100644
--- a/packages/flutter/test/widgets/modal_barrier_test.dart
+++ b/packages/flutter/test/widgets/modal_barrier_test.dart
@@ -7,6 +7,7 @@
 import 'package:flutter/material.dart';
 import 'package:flutter/rendering.dart';
 import 'package:flutter/widgets.dart';
+import 'package:flutter/gestures.dart' show kSecondaryButton;
 
 import 'semantics_tester.dart';
 
@@ -60,7 +61,7 @@
       reason: 'because the tap is not prevented by ModalBarrier');
   });
 
-  testWidgets('ModalBarrier pops the Navigator when dismissed', (WidgetTester tester) async {
+  testWidgets('ModalBarrier pops the Navigator when dismissed by primay tap', (WidgetTester tester) async {
     final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{
       '/': (BuildContext context) => FirstWidget(),
       '/modal': (BuildContext context) => SecondWidget(),
@@ -85,6 +86,56 @@
       reason: 'The route should have been dismissed by tapping the barrier.');
   });
 
+  testWidgets('ModalBarrier pops the Navigator when dismissed by primary tap down', (WidgetTester tester) async {
+    final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{
+      '/': (BuildContext context) => FirstWidget(),
+      '/modal': (BuildContext context) => SecondWidget(),
+    };
+
+    await tester.pumpWidget(MaterialApp(routes: routes));
+
+    // Initially the barrier is not visible
+    expect(find.byKey(const ValueKey<String>('barrier')), findsNothing);
+
+    // Tapping on X routes to the barrier
+    await tester.tap(find.text('X'));
+    await tester.pump(); // begin transition
+    await tester.pump(const Duration(seconds: 1)); // end transition
+
+    // Tap on the barrier to dismiss it
+    await tester.press(find.byKey(const ValueKey<String>('barrier')));
+    await tester.pump(); // begin transition
+    await tester.pump(const Duration(seconds: 1)); // end transition
+
+    expect(find.byKey(const ValueKey<String>('barrier')), findsNothing,
+      reason: 'The route should have been dismissed by tapping the barrier.');
+  });
+
+  testWidgets('ModalBarrier pops the Navigator when dismissed by non-primary tap down', (WidgetTester tester) async {
+    final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{
+      '/': (BuildContext context) => FirstWidget(),
+      '/modal': (BuildContext context) => SecondWidget(),
+    };
+
+    await tester.pumpWidget(MaterialApp(routes: routes));
+
+    // Initially the barrier is not visible
+    expect(find.byKey(const ValueKey<String>('barrier')), findsNothing);
+
+    // Tapping on X routes to the barrier
+    await tester.tap(find.text('X'));
+    await tester.pump(); // begin transition
+    await tester.pump(const Duration(seconds: 1)); // end transition
+
+    // Tap on the barrier to dismiss it
+    await tester.press(find.byKey(const ValueKey<String>('barrier')), buttons: kSecondaryButton);
+    await tester.pump(); // begin transition
+    await tester.pump(const Duration(seconds: 1)); // end transition
+
+    expect(find.byKey(const ValueKey<String>('barrier')), findsNothing,
+      reason: 'The route should have been dismissed by tapping the barrier.');
+  });
+
   testWidgets('ModalBarrier does not pop the Navigator with a WillPopScope that returns false', (WidgetTester tester) async {
     bool willPopCalled = false;
     final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{