[flutter] Handle the skip ahead case correctly

Make sure pointer up changes are not lost if something causes
sampling to skip ahead.

Test: fx run-host-tests dart_widget_tests
Change-Id: Ib5e34c2c3a83a3b5259e6828730e083e0f70f50e
Reviewed-on: https://fuchsia-review.googlesource.com/c/experiences/+/400416
Reviewed-by: Sanjay Chouksey <sanjayc@google.com>
Testability-Review: Sanjay Chouksey <sanjayc@google.com>
Commit-Queue: David Reveman <reveman@google.com>
diff --git a/settings/lib/widgets/lib/src/utils/pointer_events_listener.dart b/settings/lib/widgets/lib/src/utils/pointer_events_listener.dart
index 8de20e5..4ea4975 100644
--- a/settings/lib/widgets/lib/src/utils/pointer_events_listener.dart
+++ b/settings/lib/widgets/lib/src/utils/pointer_events_listener.dart
@@ -249,7 +249,15 @@
             break;
           case PointerEventPhase.up:
           case PointerEventPhase.cancel:
-            _downPointers.remove(event.p.pointerId);
+            final p = _downPointers[event.p.pointerId];
+            if (p != null) {
+              // Remove _DownPointer if `up` phase has been handled.
+              if (p.last.p.phase == PointerEventPhase.up) {
+                _downPointers.remove(event.p.pointerId);
+              } else {
+                _downPointers[event.p.pointerId].next = event;
+              }
+            }
             break;
           case PointerEventPhase.add:
           case PointerEventPhase.remove:
diff --git a/settings/lib/widgets/test/pointer_events_listener_test.dart b/settings/lib/widgets/test/pointer_events_listener_test.dart
index 51a322f..f10984b 100644
--- a/settings/lib/widgets/test/pointer_events_listener_test.dart
+++ b/settings/lib/widgets/test/pointer_events_listener_test.dart
@@ -151,7 +151,7 @@
     expect(result[1].change, ui.PointerChange.up);
   });
 
-  test('move without a previous pointer down.', () {
+  test('move without a previous pointer down', () {
     final event0 =
         _createSimulatedPointerEvent(PointerEventPhase.move, 1000, 0.0, 0.0);
     final event1 =
@@ -214,4 +214,68 @@
     expect(result[1].physicalDeltaX, 20.0 * ui.window.devicePixelRatio);
     expect(result[1].physicalDeltaY, 0.0);
   });
+
+  test('skip ahead', () {
+    final event0 =
+        _createSimulatedPointerEvent(PointerEventPhase.down, 1000, 0.0, 0.0);
+    final event1 =
+        _createSimulatedPointerEvent(PointerEventPhase.move, 2000, 10.0, 0.0);
+    final event2 =
+        _createSimulatedPointerEvent(PointerEventPhase.move, 3000, 20.0, 0.0);
+    final event3 =
+        _createSimulatedPointerEvent(PointerEventPhase.up, 4000, 30.0, 0.0);
+
+    var frameTime = Duration(milliseconds: 6);
+    when(scheduler.currentSystemFrameTimeStamp).thenReturn(frameTime);
+
+    pointerEventsListener
+      ..onPointerEvent(event0)
+      ..onPointerEvent(event1)
+      ..onPointerEvent(event2)
+      ..onPointerEvent(event3);
+
+    // No pointer events should have been dispatched yet.
+    expect(result.isEmpty, true);
+
+    // Frame callback should have been requested.
+    FrameCallback callback =
+        verify(scheduler.scheduleFrameCallback(captureThat(isNotNull)))
+            .captured
+            .single;
+    verify(scheduler.scheduleFrame());
+    clearInteractions(scheduler);
+
+    frameTime = Duration(milliseconds: 7);
+    when(scheduler.currentSystemFrameTimeStamp).thenReturn(frameTime);
+    callback(Duration());
+
+    // One pointer event should have been dispatched.
+    expect(result.length, 1);
+    expect(result[0].timeStamp, frameTime + _samplingOffset);
+    expect(result[0].change, ui.PointerChange.down);
+    expect(result[0].physicalX, 5.0 * ui.window.devicePixelRatio);
+    expect(result[0].physicalY, 0.0);
+    expect(result[0].physicalDeltaX, 0.0);
+    expect(result[0].physicalDeltaY, 0.0);
+
+    // Another frame callback should have been requested.
+    callback = verify(scheduler.scheduleFrameCallback(captureThat(isNotNull)))
+        .captured
+        .single;
+    verify(scheduler.scheduleFrame());
+    clearInteractions(scheduler);
+
+    frameTime = Duration(milliseconds: 16);
+    when(scheduler.currentSystemFrameTimeStamp).thenReturn(frameTime);
+    callback(Duration());
+
+    // Last pointer event should have been dispatched.
+    expect(result.length, 2);
+    expect(result[1].timeStamp, frameTime + _samplingOffset);
+    expect(result[1].change, ui.PointerChange.up);
+    expect(result[1].physicalX, 30.0 * ui.window.devicePixelRatio);
+    expect(result[1].physicalY, 0.0);
+    expect(result[1].physicalDeltaX, 25.0 * ui.window.devicePixelRatio);
+    expect(result[1].physicalDeltaY, 0.0);
+  });
 }