Support iOS devices reporting pressure data of 0 (#28478)

diff --git a/packages/flutter/lib/src/gestures/force_press.dart b/packages/flutter/lib/src/gestures/force_press.dart
index 39b0018..6296a3f 100644
--- a/packages/flutter/lib/src/gestures/force_press.dart
+++ b/packages/flutter/lib/src/gestures/force_press.dart
@@ -206,12 +206,10 @@
 
   @override
   void addPointer(PointerEvent event) {
-    assert(event.pressureMax >= 1.0);
-    // If the device has a maximum pressure of less than or equal to 1,
-    // indicating a faux pressure sensor on this device or a device without a
-    // pressure sensor (ie. on a non iOS device) we want do not want any
-    // callbacks to be called.
-    if (!(event is PointerUpEvent) && event.pressureMax == 1.0) {
+    // If the device has a maximum pressure of less than or equal to 1, it
+    // doesn't have touch pressure sensing capabilities. Do not participate
+    // in the gesture arena.
+    if (!(event is PointerUpEvent) && event.pressureMax <= 1.0) {
       resolve(GestureDisposition.rejected);
     } else {
       startTrackingPointer(event.pointer);
diff --git a/packages/flutter/test/cupertino/text_field_test.dart b/packages/flutter/test/cupertino/text_field_test.dart
index f6b9775..566eb4f 100644
--- a/packages/flutter/test/cupertino/text_field_test.dart
+++ b/packages/flutter/test/cupertino/text_field_test.dart
@@ -1537,6 +1537,7 @@
         controller.selection,
         const TextSelection(baseOffset: 8, extentOffset: 12),
       );
+      // Shows toolbar.
       expect(find.byType(CupertinoButton), findsNWidgets(3));
     },
   );
@@ -1607,44 +1608,85 @@
   );
 
   testWidgets('force press selects word', (WidgetTester tester) async {
-      final TextEditingController controller = TextEditingController(
-        text: 'Atwater Peel Sherbrooke Bonaventure',
-      );
-      await tester.pumpWidget(
-        CupertinoApp(
-          home: Center(
-            child: CupertinoTextField(
-              controller: controller,
-            ),
+    final TextEditingController controller = TextEditingController(
+      text: 'Atwater Peel Sherbrooke Bonaventure',
+    );
+    await tester.pumpWidget(
+      CupertinoApp(
+        home: Center(
+          child: CupertinoTextField(
+            controller: controller,
           ),
         ),
-      );
+      ),
+    );
 
-      final Offset textfieldStart = tester.getTopLeft(find.byType(CupertinoTextField));
+    final Offset textfieldStart = tester.getTopLeft(find.byType(CupertinoTextField));
 
-      const int pointerValue = 1;
-      final TestGesture gesture = await tester.createGesture();
-      await gesture.downWithCustomEvent(
-        textfieldStart + const Offset(150.0, 5.0),
-        PointerDownEvent(
-          pointer: pointerValue,
-          position: textfieldStart + const Offset(150.0, 5.0),
-          pressure: 3.0,
-          pressureMax: 6.0,
-          pressureMin: 0.0
+    const int pointerValue = 1;
+    final TestGesture gesture = await tester.createGesture();
+    await gesture.downWithCustomEvent(
+      textfieldStart + const Offset(150.0, 5.0),
+      PointerDownEvent(
+        pointer: pointerValue,
+        position: textfieldStart + const Offset(150.0, 5.0),
+        pressure: 3.0,
+        pressureMax: 6.0,
+        pressureMin: 0.0
+      ),
+    );
+    // We expect the force press to select a word at the given location.
+    expect(
+      controller.selection,
+      const TextSelection(baseOffset: 8, extentOffset: 12),
+    );
+
+    await gesture.up();
+    await tester.pump();
+    // Shows toolbar.
+    expect(find.byType(CupertinoButton), findsNWidgets(3));
+  });
+
+  testWidgets('force press on unsupported devices falls back to tap', (WidgetTester tester) async {
+    final TextEditingController controller = TextEditingController(
+      text: 'Atwater Peel Sherbrooke Bonaventure',
+    );
+    await tester.pumpWidget(
+      CupertinoApp(
+        home: Center(
+          child: CupertinoTextField(
+            controller: controller,
+          ),
         ),
-      );
-      // We expect the force press to select a word at the given location.
-      expect(
-        controller.selection,
-        const TextSelection(baseOffset: 8, extentOffset: 12),
-      );
+      ),
+    );
 
-      await gesture.up();
-      await tester.pumpAndSettle();
-      expect(find.byType(CupertinoButton), findsNWidgets(3));
-    },
-  );
+    final Offset textfieldStart = tester.getTopLeft(find.byType(CupertinoTextField));
+
+    const int pointerValue = 1;
+    final TestGesture gesture = await tester.createGesture();
+    await gesture.downWithCustomEvent(
+      textfieldStart + const Offset(150.0, 5.0),
+      PointerDownEvent(
+        pointer: pointerValue,
+        position: textfieldStart + const Offset(150.0, 5.0),
+        // iPhone 6 and below report 0 across the board.
+        pressure: 0,
+        pressureMax: 0,
+        pressureMin: 0,
+      ),
+    );
+    await gesture.up();
+    // Fall back to a single tap which selects the edge of the word.
+    expect(
+      controller.selection,
+      const TextSelection.collapsed(offset: 8),
+    );
+
+    await tester.pump();
+    // Falling back to a single tap doesn't trigger a toolbar.
+    expect(find.byType(CupertinoButton), findsNothing);
+  });
 
   testWidgets(
     'text field respects theme',
diff --git a/packages/flutter/test/gestures/force_press_test.dart b/packages/flutter/test/gestures/force_press_test.dart
index 8cfb830..770eccb 100644
--- a/packages/flutter/test/gestures/force_press_test.dart
+++ b/packages/flutter/test/gestures/force_press_test.dart
@@ -109,97 +109,55 @@
     expect(ended, 1);
   });
 
-  testGesture('Force presses are not recognized on devices with low maxmium pressure', (GestureTester tester) {
-    // Device specific constants that represent those from the iPhone X
-    const double pressureMin = 0;
-    const double pressureMax = 1.0;
+  testGesture('Invalid pressure ranges capabilities are not recognized', (GestureTester tester) {
+    void testGestureWithMaxPressure(double pressureMax) {
+      int started = 0;
+      int peaked = 0;
+      int updated = 0;
+      int ended = 0;
 
-    // Interpolated Flutter pressure values.
-    const double startPressure = 0.4; // = Device pressure of 2.66.
-    const double peakPressure = 0.85; // = Device pressure of 5.66.
+      final ForcePressGestureRecognizer force = ForcePressGestureRecognizer();
 
-    int started = 0;
-    int peaked = 0;
-    int updated = 0;
-    int ended = 0;
+      force.onStart = (ForcePressDetails details) => started += 1;
+      force.onPeak = (ForcePressDetails details) => peaked += 1;
+      force.onUpdate = (ForcePressDetails details) => updated += 1;
+      force.onEnd = (ForcePressDetails details) => ended += 1;
 
-    void onStart(ForcePressDetails details) {
-      started += 1;
+      const int pointerValue = 1;
+      final TestPointer pointer = TestPointer(pointerValue);
+      final PointerDownEvent down = PointerDownEvent(pointer: pointerValue, position: const Offset(10.0, 10.0), pressure: 0, pressureMin: 0, pressureMax: pressureMax);
+      pointer.setDownInfo(down, const Offset(10.0, 10.0));
+      force.addPointer(down);
+      tester.closeArena(pointerValue);
+
+      expect(started, 0);
+      expect(peaked, 0);
+      expect(updated, 0);
+      expect(ended, 0);
+
+      // Pressure fed into the test environment simulates the values received directly from the device.
+      tester.route(PointerMoveEvent(pointer: pointerValue, position: const Offset(10.0, 10.0), pressure: 10, pressureMin: 0, pressureMax: pressureMax));
+
+      // Regardless of current pressure, this recognizer shouldn't participate or
+      // trigger any callbacks.
+      expect(started, 0);
+      expect(peaked, 0);
+      expect(updated, 0);
+      expect(ended, 0);
+
+      tester.route(pointer.up());
+
+      // There should still be no callbacks.
+      expect(started, 0);
+      expect(updated, 0);
+      expect(peaked, 0);
+      expect(ended, 0);
     }
 
-    final ForcePressGestureRecognizer force = ForcePressGestureRecognizer(startPressure: startPressure, peakPressure: peakPressure);
-
-    force.onStart = onStart;
-    force.onPeak = (ForcePressDetails details) => peaked += 1;
-    force.onUpdate = (ForcePressDetails details) => updated += 1;
-    force.onEnd = (ForcePressDetails details) => ended += 1;
-
-    const int pointerValue = 1;
-    final TestPointer pointer = TestPointer(pointerValue);
-    const PointerDownEvent down = PointerDownEvent(pointer: pointerValue, position: Offset(10.0, 10.0), pressure: 0, pressureMin: pressureMin, pressureMax: pressureMax);
-    pointer.setDownInfo(down, const Offset(10.0, 10.0));
-    force.addPointer(down);
-    tester.closeArena(pointerValue);
-
-    expect(started, 0);
-    expect(peaked, 0);
-    expect(updated, 0);
-    expect(ended, 0);
-
-    // Pressure fed into the test environment simulates the values received directly from the device.
-    tester.route(const PointerMoveEvent(pointer: pointerValue, position: Offset(10.0, 10.0), pressure: 2.5, pressureMin: pressureMin, pressureMax: pressureMax));
-
-    // We have not hit the start pressure, so no events should be true.
-    expect(started, 0);
-    expect(peaked, 0);
-    expect(updated, 0);
-    expect(ended, 0);
-
-    tester.route(const PointerMoveEvent(pointer: pointerValue, position: Offset(10.0, 10.0), pressure: 2.8, pressureMin: pressureMin, pressureMax: pressureMax));
-
-    // We have just hit the start pressure so just the start event should be triggered and one update call should have occurred.
-    expect(started, 0);
-    expect(peaked, 0);
-    expect(updated, 0);
-    expect(ended, 0);
-
-    tester.route(const PointerMoveEvent(pointer: pointerValue, position: Offset(10.0, 10.0), pressure: 3.3, pressureMin: pressureMin, pressureMax: pressureMax));
-    tester.route(const PointerMoveEvent(pointer: pointerValue, position: Offset(10.0, 10.0), pressure: 4.0, pressureMin: pressureMin, pressureMax: pressureMax));
-    tester.route(const PointerMoveEvent(pointer: pointerValue, position: Offset(10.0, 10.0), pressure: 5.0, pressureMin: pressureMin, pressureMax: pressureMax));
-    tester.route(const PointerMoveEvent(pointer: pointerValue, position: Offset(10.0, 10.0), pressure: 1.0, pressureMin: pressureMin, pressureMax: pressureMax));
-
-    // We have exceeded the start pressure so update should be greater than 0.
-    expect(started, 0);
-    expect(updated, 0);
-    expect(peaked, 0);
-    expect(ended, 0);
-
-    tester.route(const PointerMoveEvent(pointer: pointerValue, position: Offset(10.0, 10.0), pressure: 6.0, pressureMin: pressureMin, pressureMax: pressureMax));
-
-    // We have exceeded the peak pressure so peak pressure should be true.
-    expect(started, 0);
-    expect(updated, 0);
-    expect(peaked, 0);
-    expect(ended, 0);
-
-    tester.route(const PointerMoveEvent(pointer: pointerValue, position: Offset(10.0, 10.0), pressure: 3.3, pressureMin: pressureMin, pressureMax: pressureMax));
-    tester.route(const PointerMoveEvent(pointer: pointerValue, position: Offset(10.0, 10.0), pressure: 4.0, pressureMin: pressureMin, pressureMax: pressureMax));
-    tester.route(const PointerMoveEvent(pointer: pointerValue, position: Offset(10.0, 10.0), pressure: 5.0, pressureMin: pressureMin, pressureMax: pressureMax));
-    tester.route(const PointerMoveEvent(pointer: pointerValue, position: Offset(10.0, 10.0), pressure: 1.0, pressureMin: pressureMin, pressureMax: pressureMax));
-
-    // Update is still called.
-    expect(started, 0);
-    expect(updated, 0);
-    expect(peaked, 0);
-    expect(ended, 0);
-
-    tester.route(pointer.up());
-
-    // We have ended the gesture so ended should be true.
-    expect(started, 0);
-    expect(updated, 0);
-    expect(peaked, 0);
-    expect(ended, 0);
+    testGestureWithMaxPressure(0);
+    testGestureWithMaxPressure(1);
+    testGestureWithMaxPressure(-1);
+    testGestureWithMaxPressure(0.5);
   });
 
   testGesture('If minimum pressure is not reached, start and end callbacks are not called', (GestureTester tester) {
diff --git a/packages/flutter/test/material/text_field_test.dart b/packages/flutter/test/material/text_field_test.dart
index 7642cfb..8477b3d 100644
--- a/packages/flutter/test/material/text_field_test.dart
+++ b/packages/flutter/test/material/text_field_test.dart
@@ -4083,6 +4083,7 @@
         controller.selection,
         const TextSelection(baseOffset: 8, extentOffset: 12),
       );
+      // The toolbar is still showing.
       expect(find.byType(CupertinoButton), findsNWidgets(3));
     },
   );
@@ -4272,6 +4273,7 @@
         controller.selection,
         const TextSelection.collapsed(offset: 3, affinity: TextAffinity.downstream),
       );
+      // Cursor move doesn't trigger a toolbar initially.
       expect(find.byType(CupertinoButton), findsNothing);
 
       await gesture.moveBy(const Offset(50, 0));
@@ -4282,6 +4284,7 @@
         controller.selection,
         const TextSelection.collapsed(offset: 6, affinity: TextAffinity.downstream),
       );
+      // Still no toolbar.
       expect(find.byType(CupertinoButton), findsNothing);
 
       await gesture.moveBy(const Offset(50, 0));
@@ -4292,6 +4295,7 @@
         controller.selection,
         const TextSelection.collapsed(offset: 9, affinity: TextAffinity.downstream),
       );
+      // Still no toolbar.
       expect(find.byType(CupertinoButton), findsNothing);
 
       await gesture.up();
@@ -4554,7 +4558,6 @@
   );
 
   testWidgets('force press does not select a word on (android)', (WidgetTester tester) async {
-    debugDefaultTargetPlatformOverride = TargetPlatform.android;
     final TextEditingController controller = TextEditingController(
       text: 'Atwater Peel Sherbrooke Bonaventure',
     );
@@ -4588,27 +4591,26 @@
     expect(controller.selection, const TextSelection.collapsed(offset: -1));
 
     await gesture.up();
-    await tester.pumpAndSettle();
+    await tester.pump();
     expect(find.byType(FlatButton), findsNothing);
-    debugDefaultTargetPlatformOverride = null;
   });
 
   testWidgets('force press selects word (iOS)', (WidgetTester tester) async {
-    debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
     final TextEditingController controller = TextEditingController(
       text: 'Atwater Peel Sherbrooke Bonaventure',
     );
     await tester.pumpWidget(
-      CupertinoApp(
-        home: Center(
-          child: CupertinoTextField(
+      MaterialApp(
+        theme: ThemeData(platform: TargetPlatform.iOS),
+        home: Material(
+          child: TextField(
             controller: controller,
           ),
         ),
       ),
     );
 
-    final Offset textfieldStart = tester.getTopLeft(find.byType(CupertinoTextField));
+    final Offset textfieldStart = tester.getTopLeft(find.byType(TextField));
 
     const int pointerValue = 1;
     final Offset offset = textfieldStart + const Offset(150.0, 5.0);
@@ -4632,9 +4634,54 @@
     );
 
     await gesture.up();
-    await tester.pumpAndSettle();
+    await tester.pump();
     expect(find.byType(CupertinoButton), findsNWidgets(3));
-    debugDefaultTargetPlatformOverride = null;
+  });
+
+  testWidgets('tap on non-force-press-supported devices work (iOS)', (WidgetTester tester) async {
+    final TextEditingController controller = TextEditingController(
+      text: 'Atwater Peel Sherbrooke Bonaventure',
+    );
+    await tester.pumpWidget(
+      MaterialApp(
+        theme: ThemeData(platform: TargetPlatform.iOS),
+        home: Material(
+          child: TextField(
+            controller: controller,
+          ),
+        ),
+      ),
+    );
+
+    final Offset textfieldStart = tester.getTopLeft(find.byType(TextField));
+
+    const int pointerValue = 1;
+    final Offset offset = textfieldStart + const Offset(150.0, 5.0);
+    final TestGesture gesture = await tester.createGesture();
+    await gesture.downWithCustomEvent(
+      offset,
+      PointerDownEvent(
+        pointer: pointerValue,
+        position: offset,
+        // iPhone 6 and below report 0 across the board.
+        pressure: 0,
+        pressureMax: 0,
+        pressureMin: 0,
+      ),
+    );
+
+    await gesture.updateWithCustomEvent(PointerMoveEvent(pointer: pointerValue, position: textfieldStart + const Offset(150.0, 5.0), pressure: 0.5, pressureMin: 0, pressureMax: 1));
+    await gesture.up();
+    // The event should fallback to a normal tap and move the cursor.
+    // Single taps selects the edge of the word.
+    expect(
+      controller.selection,
+      const TextSelection.collapsed(offset: 8),
+    );
+
+    await tester.pump();
+    // Single taps shouldn't trigger the toolbar.
+    expect(find.byType(CupertinoButton), findsNothing);
   });
 
   testWidgets('default TextField debugFillProperties', (WidgetTester tester) async {