Report two-finger touchpad swipes

Bug: 251196347
Test: try out two-finger scroll gestures with an Apple Magic Trackpad 2
Test: atest inputflinger_tests
Change-Id: I9e693350873b90bd50605ab348821224ae1121a8
diff --git a/include/android/input.h b/include/android/input.h
index a0b46de..e1aac65 100644
--- a/include/android/input.h
+++ b/include/android/input.h
@@ -786,6 +786,19 @@
      * The same as {@link AMOTION_EVENT_AXIS_GESTURE_X_OFFSET}, but for the Y axis.
      */
     AMOTION_EVENT_AXIS_GESTURE_Y_OFFSET = 49,
+    /**
+     * Axis constant: X scroll distance axis of a motion event.
+     *
+     * - For a touch pad, reports the distance that should be scrolled in the X axis as a result of
+     *   the user's two-finger scroll gesture, in display pixels.
+     */
+    AMOTION_EVENT_AXIS_GESTURE_SCROLL_X_DISTANCE = 50,
+    /**
+     * Axis constant: Y scroll distance axis of a motion event.
+     *
+     * The same as {@link AMOTION_EVENT_AXIS_GESTURE_SCROLL_X_DISTANCE}, but for the Y axis.
+     */
+    AMOTION_EVENT_AXIS_GESTURE_SCROLL_Y_DISTANCE = 51,
 
     /**
      * Note: This is not an "Axis constant". It does not represent any axis, nor should it be used
@@ -793,7 +806,7 @@
      * to make some computations (like iterating through all possible axes) cleaner.
      * Please update the value accordingly if you add a new axis.
      */
-    AMOTION_EVENT_MAXIMUM_VALID_AXIS_VALUE = AMOTION_EVENT_AXIS_GESTURE_Y_OFFSET,
+    AMOTION_EVENT_MAXIMUM_VALID_AXIS_VALUE = AMOTION_EVENT_AXIS_GESTURE_SCROLL_Y_DISTANCE,
 
     // NOTE: If you add a new axis here you must also add it to several other files.
     //       Refer to frameworks/base/core/java/android/view/MotionEvent.java for the full list.
diff --git a/libs/input/InputEventLabels.cpp b/libs/input/InputEventLabels.cpp
index dd7cbb5..8ffd220 100644
--- a/libs/input/InputEventLabels.cpp
+++ b/libs/input/InputEventLabels.cpp
@@ -394,7 +394,9 @@
     DEFINE_AXIS(GENERIC_15), \
     DEFINE_AXIS(GENERIC_16), \
     DEFINE_AXIS(GESTURE_X_OFFSET), \
-    DEFINE_AXIS(GESTURE_Y_OFFSET)
+    DEFINE_AXIS(GESTURE_Y_OFFSET), \
+    DEFINE_AXIS(GESTURE_SCROLL_X_DISTANCE), \
+    DEFINE_AXIS(GESTURE_SCROLL_Y_DISTANCE)
 
 // NOTE: If you add new LEDs here, you must also add them to Input.h
 #define LEDS_SEQUENCE \
diff --git a/services/inputflinger/InputCommonConverter.cpp b/services/inputflinger/InputCommonConverter.cpp
index 628ce6f..ea0a429 100644
--- a/services/inputflinger/InputCommonConverter.cpp
+++ b/services/inputflinger/InputCommonConverter.cpp
@@ -263,11 +263,11 @@
 static_assert(static_cast<common::Axis>(AMOTION_EVENT_AXIS_GENERIC_14) == common::Axis::GENERIC_14);
 static_assert(static_cast<common::Axis>(AMOTION_EVENT_AXIS_GENERIC_15) == common::Axis::GENERIC_15);
 static_assert(static_cast<common::Axis>(AMOTION_EVENT_AXIS_GENERIC_16) == common::Axis::GENERIC_16);
-// TODO(hcutts): add GESTURE_X_OFFSET and GESTURE_Y_OFFSET.
+// TODO(b/251196347): add GESTURE_{X,Y}_OFFSET and GESTURE_SCROLL_{X,Y}_DISTANCE.
 // If you added a new axis, consider whether this should also be exposed as a HAL axis. Update the
 // static_assert below and add the new axis here, or leave a comment summarizing your decision.
 static_assert(static_cast<common::Axis>(AMOTION_EVENT_MAXIMUM_VALID_AXIS_VALUE) ==
-              static_cast<common::Axis>(AMOTION_EVENT_AXIS_GESTURE_Y_OFFSET));
+              static_cast<common::Axis>(AMOTION_EVENT_AXIS_GESTURE_SCROLL_Y_DISTANCE));
 
 static common::VideoFrame getHalVideoFrame(const TouchVideoFrame& frame) {
     common::VideoFrame out;
diff --git a/services/inputflinger/reader/mapper/gestures/GestureConverter.cpp b/services/inputflinger/reader/mapper/gestures/GestureConverter.cpp
index ffc0523..575acb0 100644
--- a/services/inputflinger/reader/mapper/gestures/GestureConverter.cpp
+++ b/services/inputflinger/reader/mapper/gestures/GestureConverter.cpp
@@ -65,6 +65,10 @@
             return {handleMove(when, readTime, gesture)};
         case kGestureTypeButtonsChange:
             return handleButtonsChange(when, readTime, gesture);
+        case kGestureTypeScroll:
+            return handleScroll(when, readTime, gesture);
+        case kGestureTypeFling:
+            return {handleFling(when, readTime, gesture)};
         case kGestureTypeSwipe:
             return handleMultiFingerSwipe(when, readTime, 3, gesture.details.swipe.dx,
                                           gesture.details.swipe.dy);
@@ -175,6 +179,59 @@
     return out;
 }
 
+std::list<NotifyArgs> GestureConverter::handleScroll(nsecs_t when, nsecs_t readTime,
+                                                     const Gesture& gesture) {
+    std::list<NotifyArgs> out;
+    PointerCoords& coords = mFakeFingerCoords[0];
+    float xCursorPosition, yCursorPosition;
+    mPointerController->getPosition(&xCursorPosition, &yCursorPosition);
+    if (mCurrentClassification != MotionClassification::TWO_FINGER_SWIPE) {
+        mCurrentClassification = MotionClassification::TWO_FINGER_SWIPE;
+        coords.setAxisValue(AMOTION_EVENT_AXIS_X, xCursorPosition);
+        coords.setAxisValue(AMOTION_EVENT_AXIS_Y, yCursorPosition);
+        coords.setAxisValue(AMOTION_EVENT_AXIS_PRESSURE, 1.0f);
+        mDownTime = when;
+        out.push_back(makeMotionArgs(when, readTime, AMOTION_EVENT_ACTION_DOWN,
+                                     /* actionButton= */ 0, mButtonState, /* pointerCount= */ 1,
+                                     mFingerProps.data(), mFakeFingerCoords.data(), xCursorPosition,
+                                     yCursorPosition));
+    }
+    float deltaX = gesture.details.scroll.dx;
+    float deltaY = gesture.details.scroll.dy;
+    rotateDelta(mOrientation, &deltaX, &deltaY);
+
+    coords.setAxisValue(AMOTION_EVENT_AXIS_X, coords.getAxisValue(AMOTION_EVENT_AXIS_X) - deltaX);
+    coords.setAxisValue(AMOTION_EVENT_AXIS_Y, coords.getAxisValue(AMOTION_EVENT_AXIS_Y) - deltaY);
+    // TODO(b/262876643): set AXIS_GESTURE_{X,Y}_OFFSET.
+    coords.setAxisValue(AMOTION_EVENT_AXIS_GESTURE_SCROLL_X_DISTANCE, gesture.details.scroll.dx);
+    coords.setAxisValue(AMOTION_EVENT_AXIS_GESTURE_SCROLL_Y_DISTANCE, gesture.details.scroll.dy);
+    out.push_back(makeMotionArgs(when, readTime, AMOTION_EVENT_ACTION_MOVE, /* actionButton= */ 0,
+                                 mButtonState, /* pointerCount= */ 1, mFingerProps.data(),
+                                 mFakeFingerCoords.data(), xCursorPosition, yCursorPosition));
+    return out;
+}
+
+NotifyArgs GestureConverter::handleFling(nsecs_t when, nsecs_t readTime, const Gesture& gesture) {
+    // We don't actually want to use the gestures library's fling velocity values (to ensure
+    // consistency between touchscreen and touchpad flings), so we're just using the "start fling"
+    // gestures as a marker for the end of a two-finger scroll gesture.
+    if (gesture.details.fling.fling_state != GESTURES_FLING_START ||
+        mCurrentClassification != MotionClassification::TWO_FINGER_SWIPE) {
+        return {};
+    }
+
+    float xCursorPosition, yCursorPosition;
+    mPointerController->getPosition(&xCursorPosition, &yCursorPosition);
+    mFakeFingerCoords[0].setAxisValue(AMOTION_EVENT_AXIS_GESTURE_SCROLL_X_DISTANCE, 0);
+    mFakeFingerCoords[0].setAxisValue(AMOTION_EVENT_AXIS_GESTURE_SCROLL_Y_DISTANCE, 0);
+    NotifyArgs args = makeMotionArgs(when, readTime, AMOTION_EVENT_ACTION_UP,
+                                     /* actionButton= */ 0, mButtonState, /* pointerCount= */ 1,
+                                     mFingerProps.data(), mFakeFingerCoords.data(), xCursorPosition,
+                                     yCursorPosition);
+    mCurrentClassification = MotionClassification::NONE;
+    return args;
+}
+
 [[nodiscard]] std::list<NotifyArgs> GestureConverter::handleMultiFingerSwipe(nsecs_t when,
                                                                              nsecs_t readTime,
                                                                              uint32_t fingerCount,
diff --git a/services/inputflinger/reader/mapper/gestures/GestureConverter.h b/services/inputflinger/reader/mapper/gestures/GestureConverter.h
index ae5581d..6bea2d9 100644
--- a/services/inputflinger/reader/mapper/gestures/GestureConverter.h
+++ b/services/inputflinger/reader/mapper/gestures/GestureConverter.h
@@ -47,9 +47,12 @@
                                                       const Gesture& gesture);
 
 private:
-    NotifyArgs handleMove(nsecs_t when, nsecs_t readTime, const Gesture& gesture);
+    [[nodiscard]] NotifyArgs handleMove(nsecs_t when, nsecs_t readTime, const Gesture& gesture);
     [[nodiscard]] std::list<NotifyArgs> handleButtonsChange(nsecs_t when, nsecs_t readTime,
                                                             const Gesture& gesture);
+    [[nodiscard]] std::list<NotifyArgs> handleScroll(nsecs_t when, nsecs_t readTime,
+                                                     const Gesture& gesture);
+    [[nodiscard]] NotifyArgs handleFling(nsecs_t when, nsecs_t readTime, const Gesture& gesture);
     [[nodiscard]] std::list<NotifyArgs> handleMultiFingerSwipe(nsecs_t when, nsecs_t readTime,
                                                                uint32_t fingerCount, float dx,
                                                                float dy);
diff --git a/services/inputflinger/tests/GestureConverter_test.cpp b/services/inputflinger/tests/GestureConverter_test.cpp
index 1c7ec76..683e78e 100644
--- a/services/inputflinger/tests/GestureConverter_test.cpp
+++ b/services/inputflinger/tests/GestureConverter_test.cpp
@@ -239,6 +239,117 @@
                       WithToolType(AMOTION_EVENT_TOOL_TYPE_FINGER)));
 }
 
+TEST_F(GestureConverterTest, Scroll) {
+    const nsecs_t downTime = 12345;
+    InputDeviceContext deviceContext(*mDevice, EVENTHUB_ID);
+    GestureConverter converter(*mReader->getContext(), deviceContext, DEVICE_ID);
+
+    Gesture startGesture(kGestureScroll, ARBITRARY_GESTURE_TIME, ARBITRARY_GESTURE_TIME, 0, 10);
+    std::list<NotifyArgs> args = converter.handleGesture(downTime, READ_TIME, startGesture);
+    ASSERT_EQ(2u, args.size());
+
+    ASSERT_THAT(std::get<NotifyMotionArgs>(args.front()),
+                AllOf(WithMotionAction(AMOTION_EVENT_ACTION_DOWN), WithCoords(POINTER_X, POINTER_Y),
+                      WithGestureScrollDistance(0, 0, EPSILON),
+                      WithMotionClassification(MotionClassification::TWO_FINGER_SWIPE),
+                      WithToolType(AMOTION_EVENT_TOOL_TYPE_FINGER), WithDownTime(downTime)));
+    args.pop_front();
+    ASSERT_THAT(std::get<NotifyMotionArgs>(args.front()),
+                AllOf(WithMotionAction(AMOTION_EVENT_ACTION_MOVE),
+                      WithCoords(POINTER_X, POINTER_Y - 10),
+                      WithGestureScrollDistance(0, 10, EPSILON),
+                      WithMotionClassification(MotionClassification::TWO_FINGER_SWIPE),
+                      WithToolType(AMOTION_EVENT_TOOL_TYPE_FINGER)));
+
+    Gesture continueGesture(kGestureScroll, ARBITRARY_GESTURE_TIME, ARBITRARY_GESTURE_TIME, 0, 5);
+    args = converter.handleGesture(ARBITRARY_TIME, READ_TIME, continueGesture);
+    ASSERT_EQ(1u, args.size());
+    ASSERT_THAT(std::get<NotifyMotionArgs>(args.front()),
+                AllOf(WithMotionAction(AMOTION_EVENT_ACTION_MOVE),
+                      WithCoords(POINTER_X, POINTER_Y - 15),
+                      WithGestureScrollDistance(0, 5, EPSILON),
+                      WithMotionClassification(MotionClassification::TWO_FINGER_SWIPE),
+                      WithToolType(AMOTION_EVENT_TOOL_TYPE_FINGER)));
+
+    Gesture flingGesture(kGestureFling, ARBITRARY_GESTURE_TIME, ARBITRARY_GESTURE_TIME, 1, 1,
+                         GESTURES_FLING_START);
+    args = converter.handleGesture(ARBITRARY_TIME, READ_TIME, flingGesture);
+    ASSERT_EQ(1u, args.size());
+    ASSERT_THAT(std::get<NotifyMotionArgs>(args.front()),
+                AllOf(WithMotionAction(AMOTION_EVENT_ACTION_UP),
+                      WithCoords(POINTER_X, POINTER_Y - 15),
+                      WithGestureScrollDistance(0, 0, EPSILON),
+                      WithMotionClassification(MotionClassification::TWO_FINGER_SWIPE),
+                      WithToolType(AMOTION_EVENT_TOOL_TYPE_FINGER)));
+}
+
+TEST_F(GestureConverterTest, Scroll_Rotated) {
+    const nsecs_t downTime = 12345;
+    InputDeviceContext deviceContext(*mDevice, EVENTHUB_ID);
+    GestureConverter converter(*mReader->getContext(), deviceContext, DEVICE_ID);
+    converter.setOrientation(ui::ROTATION_90);
+
+    Gesture startGesture(kGestureScroll, ARBITRARY_GESTURE_TIME, ARBITRARY_GESTURE_TIME, 0, 10);
+    std::list<NotifyArgs> args = converter.handleGesture(downTime, READ_TIME, startGesture);
+    ASSERT_EQ(2u, args.size());
+
+    ASSERT_THAT(std::get<NotifyMotionArgs>(args.front()),
+                AllOf(WithMotionAction(AMOTION_EVENT_ACTION_DOWN), WithCoords(POINTER_X, POINTER_Y),
+                      WithGestureScrollDistance(0, 0, EPSILON),
+                      WithMotionClassification(MotionClassification::TWO_FINGER_SWIPE),
+                      WithToolType(AMOTION_EVENT_TOOL_TYPE_FINGER), WithDownTime(downTime)));
+    args.pop_front();
+    ASSERT_THAT(std::get<NotifyMotionArgs>(args.front()),
+                AllOf(WithMotionAction(AMOTION_EVENT_ACTION_MOVE),
+                      WithCoords(POINTER_X - 10, POINTER_Y),
+                      WithGestureScrollDistance(0, 10, EPSILON),
+                      WithMotionClassification(MotionClassification::TWO_FINGER_SWIPE),
+                      WithToolType(AMOTION_EVENT_TOOL_TYPE_FINGER)));
+
+    Gesture continueGesture(kGestureScroll, ARBITRARY_GESTURE_TIME, ARBITRARY_GESTURE_TIME, 0, 5);
+    args = converter.handleGesture(ARBITRARY_TIME, READ_TIME, continueGesture);
+    ASSERT_EQ(1u, args.size());
+    ASSERT_THAT(std::get<NotifyMotionArgs>(args.front()),
+                AllOf(WithMotionAction(AMOTION_EVENT_ACTION_MOVE),
+                      WithCoords(POINTER_X - 15, POINTER_Y),
+                      WithGestureScrollDistance(0, 5, EPSILON),
+                      WithMotionClassification(MotionClassification::TWO_FINGER_SWIPE),
+                      WithToolType(AMOTION_EVENT_TOOL_TYPE_FINGER)));
+
+    Gesture flingGesture(kGestureFling, ARBITRARY_GESTURE_TIME, ARBITRARY_GESTURE_TIME, 1, 1,
+                         GESTURES_FLING_START);
+    args = converter.handleGesture(ARBITRARY_TIME, READ_TIME, flingGesture);
+    ASSERT_EQ(1u, args.size());
+    ASSERT_THAT(std::get<NotifyMotionArgs>(args.front()),
+                AllOf(WithMotionAction(AMOTION_EVENT_ACTION_UP),
+                      WithCoords(POINTER_X - 15, POINTER_Y),
+                      WithGestureScrollDistance(0, 0, EPSILON),
+                      WithMotionClassification(MotionClassification::TWO_FINGER_SWIPE),
+                      WithToolType(AMOTION_EVENT_TOOL_TYPE_FINGER)));
+}
+
+TEST_F(GestureConverterTest, Scroll_ClearsClassificationAndOffsetsAfterGesture) {
+    InputDeviceContext deviceContext(*mDevice, EVENTHUB_ID);
+    GestureConverter converter(*mReader->getContext(), deviceContext, DEVICE_ID);
+
+    Gesture startGesture(kGestureScroll, ARBITRARY_GESTURE_TIME, ARBITRARY_GESTURE_TIME, 0, 10);
+    std::list<NotifyArgs> args = converter.handleGesture(ARBITRARY_TIME, READ_TIME, startGesture);
+
+    Gesture continueGesture(kGestureScroll, ARBITRARY_GESTURE_TIME, ARBITRARY_GESTURE_TIME, 0, 5);
+    args = converter.handleGesture(ARBITRARY_TIME, READ_TIME, continueGesture);
+
+    Gesture flingGesture(kGestureFling, ARBITRARY_GESTURE_TIME, ARBITRARY_GESTURE_TIME, 1, 1,
+                         GESTURES_FLING_START);
+    args = converter.handleGesture(ARBITRARY_TIME, READ_TIME, flingGesture);
+
+    Gesture moveGesture(kGestureMove, ARBITRARY_GESTURE_TIME, ARBITRARY_GESTURE_TIME, -5, 10);
+    args = converter.handleGesture(ARBITRARY_TIME, READ_TIME, moveGesture);
+    ASSERT_EQ(1u, args.size());
+    ASSERT_THAT(std::get<NotifyMotionArgs>(args.front()),
+                AllOf(WithMotionClassification(MotionClassification::NONE),
+                      WithGestureScrollDistance(0, 0, EPSILON)));
+}
+
 TEST_F(GestureConverterTest, ThreeFingerSwipe_ClearsClassificationAndOffsetsAfterGesture) {
     InputDeviceContext deviceContext(*mDevice, EVENTHUB_ID);
     GestureConverter converter(*mReader->getContext(), deviceContext, DEVICE_ID);
diff --git a/services/inputflinger/tests/TestInputListenerMatchers.h b/services/inputflinger/tests/TestInputListenerMatchers.h
index 64c2c75..53e4066 100644
--- a/services/inputflinger/tests/TestInputListenerMatchers.h
+++ b/services/inputflinger/tests/TestInputListenerMatchers.h
@@ -100,6 +100,19 @@
     return xDiff <= epsilon && yDiff <= epsilon;
 }
 
+MATCHER_P3(WithGestureScrollDistance, x, y, epsilon,
+           "InputEvent with specified touchpad gesture scroll distance") {
+    const auto argXDistance =
+            arg.pointerCoords[0].getAxisValue(AMOTION_EVENT_AXIS_GESTURE_SCROLL_X_DISTANCE);
+    const auto argYDistance =
+            arg.pointerCoords[0].getAxisValue(AMOTION_EVENT_AXIS_GESTURE_SCROLL_Y_DISTANCE);
+    const double xDiff = fabs(argXDistance - x);
+    const double yDiff = fabs(argYDistance - y);
+    *result_listener << "expected gesture offset (" << x << ", " << y << ") within " << epsilon
+                     << ", but got (" << argXDistance << ", " << argYDistance << ")";
+    return xDiff <= epsilon && yDiff <= epsilon;
+}
+
 MATCHER_P(WithPressure, pressure, "InputEvent with specified pressure") {
     const auto argPressure = arg.pointerCoords[0].getAxisValue(AMOTION_EVENT_AXIS_PRESSURE);
     *result_listener << "expected pressure " << pressure << ", but got " << argPressure;
@@ -141,4 +154,9 @@
     return arg.eventTime == eventTime;
 }
 
+MATCHER_P(WithDownTime, downTime, "InputEvent with specified downTime") {
+    *result_listener << "expected down time " << downTime << ", but got " << arg.downTime;
+    return arg.downTime == downTime;
+}
+
 } // namespace android