Handle Cupertino back gesture interrupted by Navigator push (#28756)

diff --git a/packages/flutter/lib/src/cupertino/route.dart b/packages/flutter/lib/src/cupertino/route.dart
index f7f6c3c..e577a0a 100644
--- a/packages/flutter/lib/src/cupertino/route.dart
+++ b/packages/flutter/lib/src/cupertino/route.dart
@@ -272,8 +272,8 @@
 
     _CupertinoBackGestureController<T> backController;
     backController = _CupertinoBackGestureController<T>(
-      navigator: route.navigator,
-      controller: route.controller,
+      route: route,
+      controller: route.controller, // protected access
       onEnded: () {
         backController?.dispose();
         backController = null;
@@ -576,22 +576,15 @@
   ///
   /// The [navigator] and [controller] arguments must not be null.
   _CupertinoBackGestureController({
-    @required this.navigator,
+    @required this.route,
     @required this.controller,
     @required this.onEnded,
-  }) : assert(navigator != null),
-       assert(controller != null),
-       assert(onEnded != null) {
-    navigator.didStartUserGesture();
+  }) : assert(route != null), assert(controller != null), assert(onEnded != null) {
+    route.navigator.didStartUserGesture();
   }
 
-  /// The navigator that this object is controlling.
-  final NavigatorState navigator;
-
-  /// The animation controller that the route uses to drive its transition
-  /// animation.
+  final PageRoute<T> route;
   final AnimationController controller;
-
   final VoidCallback onEnded;
 
   bool _animating = false;
@@ -626,8 +619,10 @@
       // The closer the panel is to dismissing, the shorter the animation is.
       // We want to cap the animation time, but we want to use a linear curve
       // to determine it.
-      final int droppedPageForwardAnimationTime = min(lerpDouble(_kMaxDroppedSwipePageForwardAnimationTime, 0, controller.value).floor(),
-                                   _kMaxPageBackAnimationTime);
+      final int droppedPageForwardAnimationTime = min(
+        lerpDouble(_kMaxDroppedSwipePageForwardAnimationTime, 0, controller.value).floor(),
+        _kMaxPageBackAnimationTime,
+      );
       controller.animateTo(1.0, duration: Duration(milliseconds: droppedPageForwardAnimationTime), curve: animationCurve);
     } else {
       final int droppedPageBackAnimationTime = lerpDouble(0, _kMaxDroppedSwipePageForwardAnimationTime, controller.value).floor();
@@ -652,14 +647,14 @@
     }
     _animating = false;
     if (status == AnimationStatus.dismissed)
-      navigator.pop<T>(); // this will cause the route to get disposed, which will dispose us
+      route.navigator.removeRoute(route); // this will cause the route to get disposed, which will dispose us
     onEnded(); // this will call dispose if popping the route failed to do so
   }
 
   void dispose() {
     if (_animating)
       controller.removeStatusListener(_handleStatusChanged);
-    navigator.didStopUserGesture();
+    route.navigator?.didStopUserGesture();
   }
 }
 
diff --git a/packages/flutter/lib/src/widgets/routes.dart b/packages/flutter/lib/src/widgets/routes.dart
index b571702..84ab501 100644
--- a/packages/flutter/lib/src/widgets/routes.dart
+++ b/packages/flutter/lib/src/widgets/routes.dart
@@ -151,11 +151,11 @@
           overlayEntries.first.opaque = false;
         break;
       case AnimationStatus.dismissed:
-        // We might still be the current route if a subclass is controlling the
+        // We might still be an active route if a subclass is controlling the
         // the transition and hits the dismissed status. For example, the iOS
         // back gesture drives this animation to the dismissed status before
-        // popping the navigator.
-        if (!isCurrent) {
+        // removing the route and disposing it.
+        if (!isActive) {
           navigator.finalizeRoute(this);
           assert(overlayEntries.isEmpty);
         }
diff --git a/packages/flutter/test/cupertino/route_test.dart b/packages/flutter/test/cupertino/route_test.dart
index 1c2dd12..16396e2 100644
--- a/packages/flutter/test/cupertino/route_test.dart
+++ b/packages/flutter/test/cupertino/route_test.dart
@@ -3,6 +3,7 @@
 // found in the LICENSE file.
 
 import 'package:flutter/cupertino.dart';
+import 'package:flutter/material.dart';
 import 'package:flutter/rendering.dart';
 import 'package:flutter_test/flutter_test.dart';
 
@@ -253,4 +254,79 @@
     expect(find.widgetWithText(CupertinoButton, 'Back'), findsOneWidget);
     expect(tester.getTopLeft(find.text('Back')).dx, 8.0 + 34.0 + 6.0);
   });
+
+  testWidgets('Back swipe dismiss interrupted by route push', (WidgetTester tester) async {
+    final GlobalKey scaffoldKey = GlobalKey();
+
+    await tester.pumpWidget(
+      MaterialApp(
+        theme: ThemeData(platform: TargetPlatform.iOS),
+        home: Scaffold(
+          key: scaffoldKey,
+          body: Center(
+            child: RaisedButton(
+              onPressed: () {
+                Navigator.push<void>(scaffoldKey.currentContext, MaterialPageRoute<void>(
+                  builder: (BuildContext context) {
+                    return const Scaffold(
+                      body: Center(child: Text('route')),
+                    );
+                  },
+                ));
+              },
+              child: const Text('push'),
+            ),
+          ),
+        ),
+      ),
+    );
+
+    // Check the basic iOS back-swipe dismiss transition. Dragging the pushed
+    // route halfway across the screen will trigger the iOS dismiss animation
+
+    await tester.tap(find.text('push'));
+    await tester.pumpAndSettle();
+    expect(find.text('route'), findsOneWidget);
+    expect(find.text('push'), findsNothing);
+
+    TestGesture gesture = await tester.startGesture(const Offset(5, 300));
+    await gesture.moveBy(const Offset(400, 0));
+    await gesture.up();
+    await tester.pump();
+    expect( // The 'route' route has been dragged to the right, halfway across the screen
+      tester.getTopLeft(find.ancestor(of: find.text('route'), matching: find.byType(Scaffold))),
+      const Offset(400, 0),
+    );
+    expect( // The 'push' route is sliding in from the left.
+      tester.getTopLeft(find.ancestor(of: find.text('push'), matching: find.byType(Scaffold))).dx,
+      lessThan(0),
+    );
+    await tester.pumpAndSettle();
+    expect(find.text('push'), findsOneWidget);
+    expect(
+      tester.getTopLeft(find.ancestor(of: find.text('push'), matching: find.byType(Scaffold))),
+      Offset.zero,
+    );
+    expect(find.text('route'), findsNothing);
+
+
+    // Run the dismiss animation 75%, which exposes the route "push" button,
+    // and then press the button. MaterialPageTransition duration is 300ms,
+    // 275 = 300 * 0.75.
+
+    await tester.tap(find.text('push'));
+    await tester.pumpAndSettle();
+    expect(find.text('route'), findsOneWidget);
+    expect(find.text('push'), findsNothing);
+
+    gesture = await tester.startGesture(const Offset(5, 300));
+    await gesture.moveBy(const Offset(400, 0)); // drag halfway
+    await gesture.up();
+    await tester.pump(const Duration(milliseconds: 275)); // partially dismiss "route"
+    expect(find.text('route'), findsOneWidget);
+    await tester.tap(find.text('push'));
+    await tester.pumpAndSettle();
+    expect(find.text('route'), findsOneWidget);
+    expect(find.text('push'), findsNothing);
+  });
 }