Report three- and four-finger swipes

The dispatcher still needs to be modified to only dispatch these to
SysUI windows.

Bug: 251196347
Test: check events received by a custom tester app, and touches shown by
      pointer location overlay
Test: atest inputflinger_tests
Change-Id: I3a7211d4a67e6388231bef158d3748c2e72e128d
diff --git a/include/android/input.h b/include/android/input.h
index 5d19c5c..a0b46de 100644
--- a/include/android/input.h
+++ b/include/android/input.h
@@ -870,6 +870,14 @@
      * The current event stream represents the user swiping with two fingers on a touchpad.
      */
     AMOTION_EVENT_CLASSIFICATION_TWO_FINGER_SWIPE = 3,
+    /**
+     * Classification constant: multi-finger swipe.
+     *
+     * The current event stream represents the user swiping with three or more fingers on a
+     * touchpad. Unlike two-finger swipes, these are only to be handled by the system UI, which is
+     * why they have a separate constant from two-finger swipes.
+     */
+    AMOTION_EVENT_CLASSIFICATION_MULTI_FINGER_SWIPE = 4,
 };
 
 /**
diff --git a/include/input/Input.h b/include/input/Input.h
index 1a35196..7e62ac0 100644
--- a/include/input/Input.h
+++ b/include/input/Input.h
@@ -302,6 +302,12 @@
      * The current gesture represents the user swiping with two fingers on a touchpad.
      */
     TWO_FINGER_SWIPE = AMOTION_EVENT_CLASSIFICATION_TWO_FINGER_SWIPE,
+    /**
+     * The current gesture represents the user swiping with three or more fingers on a touchpad.
+     * Unlike two-finger swipes, these are only to be handled by the system UI, which is why they
+     * have a separate constant from two-finger swipes.
+     */
+    MULTI_FINGER_SWIPE = AMOTION_EVENT_CLASSIFICATION_MULTI_FINGER_SWIPE,
 };
 
 /**
diff --git a/libs/input/Input.cpp b/libs/input/Input.cpp
index d893cb9..000775b 100644
--- a/libs/input/Input.cpp
+++ b/libs/input/Input.cpp
@@ -72,6 +72,8 @@
             return "DEEP_PRESS";
         case MotionClassification::TWO_FINGER_SWIPE:
             return "TWO_FINGER_SWIPE";
+        case MotionClassification::MULTI_FINGER_SWIPE:
+            return "MULTI_FINGER_SWIPE";
     }
 }
 
diff --git a/services/inputflinger/reader/mapper/TouchpadInputMapper.cpp b/services/inputflinger/reader/mapper/TouchpadInputMapper.cpp
index 23d7fdf..3b51be8 100644
--- a/services/inputflinger/reader/mapper/TouchpadInputMapper.cpp
+++ b/services/inputflinger/reader/mapper/TouchpadInputMapper.cpp
@@ -92,7 +92,7 @@
         mGestureInterpreter(NewGestureInterpreter(), DeleteGestureInterpreter),
         mPointerController(getContext()->getPointerController(getDeviceId())),
         mStateConverter(deviceContext),
-        mGestureConverter(*getContext(), getDeviceId()) {
+        mGestureConverter(*getContext(), deviceContext, getDeviceId()) {
     mGestureInterpreter->Initialize(GESTURES_DEVCLASS_TOUCHPAD);
     mGestureInterpreter->SetHardwareProperties(createHardwareProperties(deviceContext));
     // Even though we don't explicitly delete copy/move semantics, it's safe to
diff --git a/services/inputflinger/reader/mapper/gestures/GestureConverter.cpp b/services/inputflinger/reader/mapper/gestures/GestureConverter.cpp
index 8600065..ffc0523 100644
--- a/services/inputflinger/reader/mapper/gestures/GestureConverter.cpp
+++ b/services/inputflinger/reader/mapper/gestures/GestureConverter.cpp
@@ -17,6 +17,7 @@
 #include "gestures/GestureConverter.h"
 
 #include <android/input.h>
+#include <linux/input-event-codes.h>
 
 #include "TouchCursorInputMapperCommon.h"
 #include "input/Input.h"
@@ -44,10 +45,14 @@
 
 } // namespace
 
-GestureConverter::GestureConverter(InputReaderContext& readerContext, int32_t deviceId)
+GestureConverter::GestureConverter(InputReaderContext& readerContext,
+                                   const InputDeviceContext& deviceContext, int32_t deviceId)
       : mDeviceId(deviceId),
         mReaderContext(readerContext),
-        mPointerController(readerContext.getPointerController(deviceId)) {}
+        mPointerController(readerContext.getPointerController(deviceId)) {
+    deviceContext.getAbsoluteAxisInfo(ABS_MT_POSITION_X, &mXAxisInfo);
+    deviceContext.getAbsoluteAxisInfo(ABS_MT_POSITION_Y, &mYAxisInfo);
+}
 
 void GestureConverter::reset() {
     mButtonState = 0;
@@ -60,6 +65,15 @@
             return {handleMove(when, readTime, gesture)};
         case kGestureTypeButtonsChange:
             return handleButtonsChange(when, readTime, gesture);
+        case kGestureTypeSwipe:
+            return handleMultiFingerSwipe(when, readTime, 3, gesture.details.swipe.dx,
+                                          gesture.details.swipe.dy);
+        case kGestureTypeFourFingerSwipe:
+            return handleMultiFingerSwipe(when, readTime, 4, gesture.details.four_finger_swipe.dx,
+                                          gesture.details.four_finger_swipe.dy);
+        case kGestureTypeSwipeLift:
+        case kGestureTypeFourFingerSwipeLift:
+            return handleMultiFingerSwipeLift(when, readTime);
         default:
             // TODO(b/251196347): handle more gesture types.
             return {};
@@ -67,11 +81,6 @@
 }
 
 NotifyArgs GestureConverter::handleMove(nsecs_t when, nsecs_t readTime, const Gesture& gesture) {
-    PointerProperties props;
-    props.clear();
-    props.id = 0;
-    props.toolType = AMOTION_EVENT_TOOL_TYPE_FINGER;
-
     float deltaX = gesture.details.move.dx;
     float deltaY = gesture.details.move.dy;
     rotateDelta(mOrientation, &deltaX, &deltaY);
@@ -93,7 +102,8 @@
 
     const int32_t action = down ? AMOTION_EVENT_ACTION_MOVE : AMOTION_EVENT_ACTION_HOVER_MOVE;
     return makeMotionArgs(when, readTime, action, /* actionButton= */ 0, mButtonState,
-                          /* pointerCount= */ 1, &props, &coords, xCursorPosition, yCursorPosition);
+                          /* pointerCount= */ 1, mFingerProps.data(), &coords, xCursorPosition,
+                          yCursorPosition);
 }
 
 std::list<NotifyArgs> GestureConverter::handleButtonsChange(nsecs_t when, nsecs_t readTime,
@@ -103,11 +113,6 @@
     mPointerController->setPresentation(PointerControllerInterface::Presentation::POINTER);
     mPointerController->unfade(PointerControllerInterface::Transition::IMMEDIATE);
 
-    PointerProperties props;
-    props.clear();
-    props.id = 0;
-    props.toolType = AMOTION_EVENT_TOOL_TYPE_FINGER;
-
     float xCursorPosition, yCursorPosition;
     mPointerController->getPosition(&xCursorPosition, &yCursorPosition);
 
@@ -131,15 +136,16 @@
             newButtonState |= actionButton;
             pressEvents.push_back(makeMotionArgs(when, readTime, AMOTION_EVENT_ACTION_BUTTON_PRESS,
                                                  actionButton, newButtonState,
-                                                 /* pointerCount= */ 1, &props, &coords,
-                                                 xCursorPosition, yCursorPosition));
+                                                 /* pointerCount= */ 1, mFingerProps.data(),
+                                                 &coords, xCursorPosition, yCursorPosition));
         }
     }
     if (!isPointerDown(mButtonState) && isPointerDown(newButtonState)) {
         mDownTime = when;
         out.push_back(makeMotionArgs(when, readTime, AMOTION_EVENT_ACTION_DOWN,
                                      /* actionButton= */ 0, newButtonState, /* pointerCount= */ 1,
-                                     &props, &coords, xCursorPosition, yCursorPosition));
+                                     mFingerProps.data(), &coords, xCursorPosition,
+                                     yCursorPosition));
     }
     out.splice(out.end(), pressEvents);
 
@@ -155,19 +161,109 @@
             newButtonState &= ~actionButton;
             out.push_back(makeMotionArgs(when, readTime, AMOTION_EVENT_ACTION_BUTTON_RELEASE,
                                          actionButton, newButtonState, /* pointerCount= */ 1,
-                                         &props, &coords, xCursorPosition, yCursorPosition));
+                                         mFingerProps.data(), &coords, xCursorPosition,
+                                         yCursorPosition));
         }
     }
     if (isPointerDown(mButtonState) && !isPointerDown(newButtonState)) {
         coords.setAxisValue(AMOTION_EVENT_AXIS_PRESSURE, 0.0f);
         out.push_back(makeMotionArgs(when, readTime, AMOTION_EVENT_ACTION_UP, /* actionButton= */ 0,
-                                     newButtonState, /* pointerCount= */ 1, &props, &coords,
-                                     xCursorPosition, yCursorPosition));
+                                     newButtonState, /* pointerCount= */ 1, mFingerProps.data(),
+                                     &coords, xCursorPosition, yCursorPosition));
     }
     mButtonState = newButtonState;
     return out;
 }
 
+[[nodiscard]] std::list<NotifyArgs> GestureConverter::handleMultiFingerSwipe(nsecs_t when,
+                                                                             nsecs_t readTime,
+                                                                             uint32_t fingerCount,
+                                                                             float dx, float dy) {
+    std::list<NotifyArgs> out = {};
+    float xCursorPosition, yCursorPosition;
+    mPointerController->getPosition(&xCursorPosition, &yCursorPosition);
+    if (mCurrentClassification != MotionClassification::MULTI_FINGER_SWIPE) {
+        // If the user changes the number of fingers mid-way through a swipe (e.g. they start with
+        // three and then put a fourth finger down), the gesture library will treat it as two
+        // separate swipes with an appropriate lift event between them, so we don't have to worry
+        // about the finger count changing mid-swipe.
+        mCurrentClassification = MotionClassification::MULTI_FINGER_SWIPE;
+        mSwipeFingerCount = fingerCount;
+
+        constexpr float FAKE_FINGER_SPACING = 100;
+        float xCoord = xCursorPosition - FAKE_FINGER_SPACING * (mSwipeFingerCount - 1) / 2;
+        for (size_t i = 0; i < mSwipeFingerCount; i++) {
+            PointerCoords& coords = mFakeFingerCoords[i];
+            coords.clear();
+            coords.setAxisValue(AMOTION_EVENT_AXIS_X, xCoord);
+            coords.setAxisValue(AMOTION_EVENT_AXIS_Y, yCursorPosition);
+            coords.setAxisValue(AMOTION_EVENT_AXIS_PRESSURE, 1.0f);
+            xCoord += FAKE_FINGER_SPACING;
+        }
+
+        mDownTime = when;
+        out.push_back(makeMotionArgs(when, readTime, AMOTION_EVENT_ACTION_DOWN,
+                                     /* actionButton= */ 0, mButtonState, /* pointerCount= */ 1,
+                                     mFingerProps.data(), mFakeFingerCoords.data(), xCursorPosition,
+                                     yCursorPosition));
+        for (size_t i = 1; i < mSwipeFingerCount; i++) {
+            out.push_back(makeMotionArgs(when, readTime,
+                                         AMOTION_EVENT_ACTION_POINTER_DOWN |
+                                                 (i << AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT),
+                                         /* actionButton= */ 0, mButtonState,
+                                         /* pointerCount= */ i + 1, mFingerProps.data(),
+                                         mFakeFingerCoords.data(), xCursorPosition,
+                                         yCursorPosition));
+        }
+    }
+    for (size_t i = 0; i < mSwipeFingerCount; i++) {
+        PointerCoords& coords = mFakeFingerCoords[i];
+        coords.setAxisValue(AMOTION_EVENT_AXIS_X, coords.getAxisValue(AMOTION_EVENT_AXIS_X) + dx);
+        // TODO(b/251196347): Set the gesture properties appropriately to avoid needing to negate
+        // the Y values.
+        coords.setAxisValue(AMOTION_EVENT_AXIS_Y, coords.getAxisValue(AMOTION_EVENT_AXIS_Y) - dy);
+    }
+    float xOffset = dx / (mXAxisInfo.maxValue - mXAxisInfo.minValue);
+    // TODO(b/251196347): Set the gesture properties appropriately to avoid needing to negate the Y
+    // values.
+    float yOffset = -dy / (mYAxisInfo.maxValue - mYAxisInfo.minValue);
+    mFakeFingerCoords[0].setAxisValue(AMOTION_EVENT_AXIS_GESTURE_X_OFFSET, xOffset);
+    mFakeFingerCoords[0].setAxisValue(AMOTION_EVENT_AXIS_GESTURE_Y_OFFSET, yOffset);
+    out.push_back(makeMotionArgs(when, readTime, AMOTION_EVENT_ACTION_MOVE, /* actionButton= */ 0,
+                                 mButtonState, /* pointerCount= */ mSwipeFingerCount,
+                                 mFingerProps.data(), mFakeFingerCoords.data(), xCursorPosition,
+                                 yCursorPosition));
+    return out;
+}
+
+[[nodiscard]] std::list<NotifyArgs> GestureConverter::handleMultiFingerSwipeLift(nsecs_t when,
+                                                                                 nsecs_t readTime) {
+    std::list<NotifyArgs> out = {};
+    if (mCurrentClassification != MotionClassification::MULTI_FINGER_SWIPE) {
+        return out;
+    }
+    float xCursorPosition, yCursorPosition;
+    mPointerController->getPosition(&xCursorPosition, &yCursorPosition);
+    mFakeFingerCoords[0].setAxisValue(AMOTION_EVENT_AXIS_GESTURE_X_OFFSET, 0);
+    mFakeFingerCoords[0].setAxisValue(AMOTION_EVENT_AXIS_GESTURE_Y_OFFSET, 0);
+
+    for (size_t i = mSwipeFingerCount; i > 1; i--) {
+        out.push_back(makeMotionArgs(when, readTime,
+                                     AMOTION_EVENT_ACTION_POINTER_UP |
+                                             ((i - 1) << AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT),
+                                     /* actionButton= */ 0, mButtonState, /* pointerCount= */ i,
+                                     mFingerProps.data(), mFakeFingerCoords.data(), xCursorPosition,
+                                     yCursorPosition));
+    }
+    out.push_back(makeMotionArgs(when, readTime, AMOTION_EVENT_ACTION_UP,
+                                 /* actionButton= */ 0, mButtonState, /* pointerCount= */ 1,
+                                 mFingerProps.data(), mFakeFingerCoords.data(), xCursorPosition,
+                                 yCursorPosition));
+    mCurrentClassification = MotionClassification::NONE;
+    mSwipeFingerCount = 0;
+    return out;
+}
+
 NotifyMotionArgs GestureConverter::makeMotionArgs(nsecs_t when, nsecs_t readTime, int32_t action,
                                                   int32_t actionButton, int32_t buttonState,
                                                   uint32_t pointerCount,
@@ -181,10 +277,10 @@
                             mPointerController->getDisplayId(), /* policyFlags= */ 0, action,
                             /* actionButton= */ actionButton, /* flags= */ 0,
                             mReaderContext.getGlobalMetaState(), buttonState,
-                            MotionClassification::NONE, AMOTION_EVENT_EDGE_FLAG_NONE, pointerCount,
-                            pointerProperties, pointerCoords,
-                            /* xPrecision= */ 1.0f, /* yPrecision= */ 1.0f, xCursorPosition,
-                            yCursorPosition, /* downTime= */ mDownTime, /* videoFrames= */ {});
+                            mCurrentClassification, AMOTION_EVENT_EDGE_FLAG_NONE, pointerCount,
+                            pointerProperties, pointerCoords, /* xPrecision= */ 1.0f,
+                            /* yPrecision= */ 1.0f, xCursorPosition, yCursorPosition,
+                            /* downTime= */ mDownTime, /* videoFrames= */ {});
 }
 
 } // namespace android
diff --git a/services/inputflinger/reader/mapper/gestures/GestureConverter.h b/services/inputflinger/reader/mapper/gestures/GestureConverter.h
index d6a51d2..ae5581d 100644
--- a/services/inputflinger/reader/mapper/gestures/GestureConverter.h
+++ b/services/inputflinger/reader/mapper/gestures/GestureConverter.h
@@ -16,12 +16,15 @@
 
 #pragma once
 
+#include <array>
 #include <list>
 #include <memory>
 
 #include <PointerControllerInterface.h>
 #include <utils/Timers.h>
 
+#include "EventHub.h"
+#include "InputDevice.h"
 #include "InputReaderContext.h"
 #include "NotifyArgs.h"
 #include "ui/Rotation.h"
@@ -34,7 +37,8 @@
 // PointerController calls.
 class GestureConverter {
 public:
-    GestureConverter(InputReaderContext& readerContext, int32_t deviceId);
+    GestureConverter(InputReaderContext& readerContext, const InputDeviceContext& deviceContext,
+                     int32_t deviceId);
 
     void setOrientation(ui::Rotation orientation) { mOrientation = orientation; }
     void reset();
@@ -46,6 +50,10 @@
     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> handleMultiFingerSwipe(nsecs_t when, nsecs_t readTime,
+                                                               uint32_t fingerCount, float dx,
+                                                               float dy);
+    [[nodiscard]] std::list<NotifyArgs> handleMultiFingerSwipeLift(nsecs_t when, nsecs_t readTime);
 
     NotifyMotionArgs makeMotionArgs(nsecs_t when, nsecs_t readTime, int32_t action,
                                     int32_t actionButton, int32_t buttonState,
@@ -59,10 +67,26 @@
     std::shared_ptr<PointerControllerInterface> mPointerController;
 
     ui::Rotation mOrientation = ui::ROTATION_0;
+    RawAbsoluteAxisInfo mXAxisInfo;
+    RawAbsoluteAxisInfo mYAxisInfo;
+
     // The current button state according to the gestures library, but converted into MotionEvent
     // button values (AMOTION_EVENT_BUTTON_...).
     uint32_t mButtonState = 0;
     nsecs_t mDownTime = 0;
+
+    MotionClassification mCurrentClassification = MotionClassification::NONE;
+    uint32_t mSwipeFingerCount = 0;
+    static constexpr size_t MAX_FAKE_FINGERS = 4;
+    // We never need any PointerProperties other than the finger tool type, so we can just keep a
+    // const array of them.
+    const std::array<PointerProperties, MAX_FAKE_FINGERS> mFingerProps = {{
+            {.id = 0, .toolType = AMOTION_EVENT_TOOL_TYPE_FINGER},
+            {.id = 1, .toolType = AMOTION_EVENT_TOOL_TYPE_FINGER},
+            {.id = 2, .toolType = AMOTION_EVENT_TOOL_TYPE_FINGER},
+            {.id = 3, .toolType = AMOTION_EVENT_TOOL_TYPE_FINGER},
+    }};
+    std::array<PointerCoords, MAX_FAKE_FINGERS> mFakeFingerCoords = {};
 };
 
 } // namespace android
diff --git a/services/inputflinger/tests/GestureConverter_test.cpp b/services/inputflinger/tests/GestureConverter_test.cpp
index 74c2028..1c7ec76 100644
--- a/services/inputflinger/tests/GestureConverter_test.cpp
+++ b/services/inputflinger/tests/GestureConverter_test.cpp
@@ -38,6 +38,7 @@
 class GestureConverterTest : public testing::Test {
 protected:
     static constexpr int32_t DEVICE_ID = END_RESERVED_ID + 1000;
+    static constexpr int32_t EVENTHUB_ID = 1;
     static constexpr stime_t ARBITRARY_GESTURE_TIME = 1.2;
     static constexpr float POINTER_X = 100;
     static constexpr float POINTER_Y = 200;
@@ -48,6 +49,9 @@
         mFakeListener = std::make_unique<TestInputListener>();
         mReader = std::make_unique<InstrumentedInputReader>(mFakeEventHub, mFakePolicy,
                                                             *mFakeListener);
+        mDevice = newDevice();
+        mFakeEventHub->addAbsoluteAxis(EVENTHUB_ID, ABS_MT_POSITION_X, -500, 500, 0, 0, 20);
+        mFakeEventHub->addAbsoluteAxis(EVENTHUB_ID, ABS_MT_POSITION_Y, -500, 500, 0, 0, 20);
 
         mFakePointerController = std::make_shared<FakePointerController>();
         mFakePointerController->setBounds(0, 0, 800 - 1, 480 - 1);
@@ -55,15 +59,32 @@
         mFakePolicy->setPointerController(mFakePointerController);
     }
 
+    std::shared_ptr<InputDevice> newDevice() {
+        InputDeviceIdentifier identifier;
+        identifier.name = "device";
+        identifier.location = "USB1";
+        identifier.bus = 0;
+        std::shared_ptr<InputDevice> device =
+                std::make_shared<InputDevice>(mReader->getContext(), DEVICE_ID, /* generation= */ 2,
+                                              identifier);
+        mReader->pushNextDevice(device);
+        mFakeEventHub->addDevice(EVENTHUB_ID, identifier.name, InputDeviceClass::TOUCHPAD,
+                                 identifier.bus);
+        mReader->loopOnce();
+        return device;
+    }
+
     std::shared_ptr<FakeEventHub> mFakeEventHub;
     sp<FakeInputReaderPolicy> mFakePolicy;
     std::unique_ptr<TestInputListener> mFakeListener;
     std::unique_ptr<InstrumentedInputReader> mReader;
+    std::shared_ptr<InputDevice> mDevice;
     std::shared_ptr<FakePointerController> mFakePointerController;
 };
 
 TEST_F(GestureConverterTest, Move) {
-    GestureConverter converter(*mReader->getContext(), DEVICE_ID);
+    InputDeviceContext deviceContext(*mDevice, EVENTHUB_ID);
+    GestureConverter converter(*mReader->getContext(), deviceContext, DEVICE_ID);
 
     Gesture moveGesture(kGestureMove, ARBITRARY_GESTURE_TIME, ARBITRARY_GESTURE_TIME, -5, 10);
     std::list<NotifyArgs> args = converter.handleGesture(ARBITRARY_TIME, READ_TIME, moveGesture);
@@ -79,7 +100,8 @@
 }
 
 TEST_F(GestureConverterTest, Move_Rotated) {
-    GestureConverter converter(*mReader->getContext(), DEVICE_ID);
+    InputDeviceContext deviceContext(*mDevice, EVENTHUB_ID);
+    GestureConverter converter(*mReader->getContext(), deviceContext, DEVICE_ID);
     converter.setOrientation(ui::ROTATION_90);
 
     Gesture moveGesture(kGestureMove, ARBITRARY_GESTURE_TIME, ARBITRARY_GESTURE_TIME, -5, 10);
@@ -96,7 +118,8 @@
 }
 
 TEST_F(GestureConverterTest, ButtonsChange) {
-    GestureConverter converter(*mReader->getContext(), DEVICE_ID);
+    InputDeviceContext deviceContext(*mDevice, EVENTHUB_ID);
+    GestureConverter converter(*mReader->getContext(), deviceContext, DEVICE_ID);
 
     // Press left and right buttons at once
     Gesture downGesture(kGestureButtonsChange, ARBITRARY_GESTURE_TIME, ARBITRARY_GESTURE_TIME,
@@ -161,7 +184,8 @@
 }
 
 TEST_F(GestureConverterTest, DragWithButton) {
-    GestureConverter converter(*mReader->getContext(), DEVICE_ID);
+    InputDeviceContext deviceContext(*mDevice, EVENTHUB_ID);
+    GestureConverter converter(*mReader->getContext(), deviceContext, DEVICE_ID);
 
     // Press the button
     Gesture downGesture(kGestureButtonsChange, ARBITRARY_GESTURE_TIME, ARBITRARY_GESTURE_TIME,
@@ -215,4 +239,228 @@
                       WithToolType(AMOTION_EVENT_TOOL_TYPE_FINGER)));
 }
 
+TEST_F(GestureConverterTest, ThreeFingerSwipe_ClearsClassificationAndOffsetsAfterGesture) {
+    InputDeviceContext deviceContext(*mDevice, EVENTHUB_ID);
+    GestureConverter converter(*mReader->getContext(), deviceContext, DEVICE_ID);
+
+    Gesture startGesture(kGestureSwipe, ARBITRARY_GESTURE_TIME, ARBITRARY_GESTURE_TIME, /* dx= */ 0,
+                         /* dy= */ 0);
+    std::list<NotifyArgs> args = converter.handleGesture(ARBITRARY_TIME, READ_TIME, startGesture);
+
+    Gesture liftGesture(kGestureSwipeLift, ARBITRARY_GESTURE_TIME, ARBITRARY_GESTURE_TIME);
+    args = converter.handleGesture(ARBITRARY_TIME, READ_TIME, liftGesture);
+
+    Gesture moveGesture(kGestureMove, ARBITRARY_GESTURE_TIME, ARBITRARY_GESTURE_TIME, /* dx= */ -5,
+                        /* dy= */ 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),
+                      WithGestureOffset(0, 0, EPSILON)));
+}
+
+TEST_F(GestureConverterTest, ThreeFingerSwipe_Vertical) {
+    // The gestures library will "lock" a swipe into the dimension it starts in. For example, if you
+    // start swiping up and then start moving left or right, it'll return gesture events with only Y
+    // deltas until you lift your fingers and start swiping again. That's why each of these tests
+    // only checks movement in one dimension.
+    InputDeviceContext deviceContext(*mDevice, EVENTHUB_ID);
+    GestureConverter converter(*mReader->getContext(), deviceContext, DEVICE_ID);
+
+    Gesture startGesture(kGestureSwipe, ARBITRARY_GESTURE_TIME, ARBITRARY_GESTURE_TIME, /* dx= */ 0,
+                         /* dy= */ 10);
+    std::list<NotifyArgs> args = converter.handleGesture(ARBITRARY_TIME, READ_TIME, startGesture);
+    ASSERT_EQ(4u, args.size());
+
+    // Three fake fingers should be created. We don't actually care where they are, so long as they
+    // move appropriately.
+    NotifyMotionArgs arg = std::get<NotifyMotionArgs>(args.front());
+    ASSERT_THAT(arg,
+                AllOf(WithMotionAction(AMOTION_EVENT_ACTION_DOWN), WithGestureOffset(0, 0, EPSILON),
+                      WithMotionClassification(MotionClassification::MULTI_FINGER_SWIPE),
+                      WithPointerCount(1u), WithToolType(AMOTION_EVENT_TOOL_TYPE_FINGER)));
+    PointerCoords finger0Start = arg.pointerCoords[0];
+    args.pop_front();
+    arg = std::get<NotifyMotionArgs>(args.front());
+    ASSERT_THAT(arg,
+                AllOf(WithMotionAction(AMOTION_EVENT_ACTION_POINTER_DOWN |
+                                       1 << AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT),
+                      WithGestureOffset(0, 0, EPSILON),
+                      WithMotionClassification(MotionClassification::MULTI_FINGER_SWIPE),
+                      WithPointerCount(2u), WithToolType(AMOTION_EVENT_TOOL_TYPE_FINGER)));
+    PointerCoords finger1Start = arg.pointerCoords[1];
+    args.pop_front();
+    arg = std::get<NotifyMotionArgs>(args.front());
+    ASSERT_THAT(arg,
+                AllOf(WithMotionAction(AMOTION_EVENT_ACTION_POINTER_DOWN |
+                                       2 << AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT),
+                      WithGestureOffset(0, 0, EPSILON),
+                      WithMotionClassification(MotionClassification::MULTI_FINGER_SWIPE),
+                      WithPointerCount(3u), WithToolType(AMOTION_EVENT_TOOL_TYPE_FINGER)));
+    PointerCoords finger2Start = arg.pointerCoords[2];
+    args.pop_front();
+
+    arg = std::get<NotifyMotionArgs>(args.front());
+    ASSERT_THAT(arg,
+                AllOf(WithMotionAction(AMOTION_EVENT_ACTION_MOVE),
+                      WithGestureOffset(0, -0.01, EPSILON),
+                      WithMotionClassification(MotionClassification::MULTI_FINGER_SWIPE),
+                      WithPointerCount(3u), WithToolType(AMOTION_EVENT_TOOL_TYPE_FINGER)));
+    EXPECT_EQ(arg.pointerCoords[0].getX(), finger0Start.getX());
+    EXPECT_EQ(arg.pointerCoords[1].getX(), finger1Start.getX());
+    EXPECT_EQ(arg.pointerCoords[2].getX(), finger2Start.getX());
+    EXPECT_EQ(arg.pointerCoords[0].getY(), finger0Start.getY() - 10);
+    EXPECT_EQ(arg.pointerCoords[1].getY(), finger1Start.getY() - 10);
+    EXPECT_EQ(arg.pointerCoords[2].getY(), finger2Start.getY() - 10);
+
+    Gesture continueGesture(kGestureSwipe, ARBITRARY_GESTURE_TIME, ARBITRARY_GESTURE_TIME,
+                            /* dx= */ 0, /* dy= */ 5);
+    args = converter.handleGesture(ARBITRARY_TIME, READ_TIME, continueGesture);
+    ASSERT_EQ(1u, args.size());
+    arg = std::get<NotifyMotionArgs>(args.front());
+    ASSERT_THAT(arg,
+                AllOf(WithMotionAction(AMOTION_EVENT_ACTION_MOVE),
+                      WithGestureOffset(0, -0.005, EPSILON),
+                      WithMotionClassification(MotionClassification::MULTI_FINGER_SWIPE),
+                      WithPointerCount(3u), WithToolType(AMOTION_EVENT_TOOL_TYPE_FINGER)));
+    EXPECT_EQ(arg.pointerCoords[0].getX(), finger0Start.getX());
+    EXPECT_EQ(arg.pointerCoords[1].getX(), finger1Start.getX());
+    EXPECT_EQ(arg.pointerCoords[2].getX(), finger2Start.getX());
+    EXPECT_EQ(arg.pointerCoords[0].getY(), finger0Start.getY() - 15);
+    EXPECT_EQ(arg.pointerCoords[1].getY(), finger1Start.getY() - 15);
+    EXPECT_EQ(arg.pointerCoords[2].getY(), finger2Start.getY() - 15);
+
+    Gesture liftGesture(kGestureSwipeLift, ARBITRARY_GESTURE_TIME, ARBITRARY_GESTURE_TIME);
+    args = converter.handleGesture(ARBITRARY_TIME, READ_TIME, liftGesture);
+    ASSERT_EQ(3u, args.size());
+    ASSERT_THAT(std::get<NotifyMotionArgs>(args.front()),
+                AllOf(WithMotionAction(AMOTION_EVENT_ACTION_POINTER_UP |
+                                       2 << AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT),
+                      WithGestureOffset(0, 0, EPSILON),
+                      WithMotionClassification(MotionClassification::MULTI_FINGER_SWIPE),
+                      WithPointerCount(3u), WithToolType(AMOTION_EVENT_TOOL_TYPE_FINGER)));
+    args.pop_front();
+    ASSERT_THAT(std::get<NotifyMotionArgs>(args.front()),
+                AllOf(WithMotionAction(AMOTION_EVENT_ACTION_POINTER_UP |
+                                       1 << AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT),
+                      WithGestureOffset(0, 0, EPSILON),
+                      WithMotionClassification(MotionClassification::MULTI_FINGER_SWIPE),
+                      WithPointerCount(2u), WithToolType(AMOTION_EVENT_TOOL_TYPE_FINGER)));
+    args.pop_front();
+    ASSERT_THAT(std::get<NotifyMotionArgs>(args.front()),
+                AllOf(WithMotionAction(AMOTION_EVENT_ACTION_UP), WithGestureOffset(0, 0, EPSILON),
+                      WithMotionClassification(MotionClassification::MULTI_FINGER_SWIPE),
+                      WithPointerCount(1u), WithToolType(AMOTION_EVENT_TOOL_TYPE_FINGER)));
+}
+
+TEST_F(GestureConverterTest, FourFingerSwipe_Horizontal) {
+    InputDeviceContext deviceContext(*mDevice, EVENTHUB_ID);
+    GestureConverter converter(*mReader->getContext(), deviceContext, DEVICE_ID);
+
+    Gesture startGesture(kGestureFourFingerSwipe, ARBITRARY_GESTURE_TIME, ARBITRARY_GESTURE_TIME,
+                         /* dx= */ 10, /* dy= */ 0);
+    std::list<NotifyArgs> args = converter.handleGesture(ARBITRARY_TIME, READ_TIME, startGesture);
+    ASSERT_EQ(5u, args.size());
+
+    // Four fake fingers should be created. We don't actually care where they are, so long as they
+    // move appropriately.
+    NotifyMotionArgs arg = std::get<NotifyMotionArgs>(args.front());
+    ASSERT_THAT(arg,
+                AllOf(WithMotionAction(AMOTION_EVENT_ACTION_DOWN), WithGestureOffset(0, 0, EPSILON),
+                      WithMotionClassification(MotionClassification::MULTI_FINGER_SWIPE),
+                      WithPointerCount(1u), WithToolType(AMOTION_EVENT_TOOL_TYPE_FINGER)));
+    PointerCoords finger0Start = arg.pointerCoords[0];
+    args.pop_front();
+    arg = std::get<NotifyMotionArgs>(args.front());
+    ASSERT_THAT(arg,
+                AllOf(WithMotionAction(AMOTION_EVENT_ACTION_POINTER_DOWN |
+                                       1 << AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT),
+                      WithGestureOffset(0, 0, EPSILON),
+                      WithMotionClassification(MotionClassification::MULTI_FINGER_SWIPE),
+                      WithPointerCount(2u), WithToolType(AMOTION_EVENT_TOOL_TYPE_FINGER)));
+    PointerCoords finger1Start = arg.pointerCoords[1];
+    args.pop_front();
+    arg = std::get<NotifyMotionArgs>(args.front());
+    ASSERT_THAT(arg,
+                AllOf(WithMotionAction(AMOTION_EVENT_ACTION_POINTER_DOWN |
+                                       2 << AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT),
+                      WithGestureOffset(0, 0, EPSILON),
+                      WithMotionClassification(MotionClassification::MULTI_FINGER_SWIPE),
+                      WithPointerCount(3u), WithToolType(AMOTION_EVENT_TOOL_TYPE_FINGER)));
+    PointerCoords finger2Start = arg.pointerCoords[2];
+    args.pop_front();
+    arg = std::get<NotifyMotionArgs>(args.front());
+    ASSERT_THAT(arg,
+                AllOf(WithMotionAction(AMOTION_EVENT_ACTION_POINTER_DOWN |
+                                       3 << AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT),
+                      WithGestureOffset(0, 0, EPSILON),
+                      WithMotionClassification(MotionClassification::MULTI_FINGER_SWIPE),
+                      WithPointerCount(4u), WithToolType(AMOTION_EVENT_TOOL_TYPE_FINGER)));
+    PointerCoords finger3Start = arg.pointerCoords[3];
+    args.pop_front();
+
+    arg = std::get<NotifyMotionArgs>(args.front());
+    ASSERT_THAT(arg,
+                AllOf(WithMotionAction(AMOTION_EVENT_ACTION_MOVE),
+                      WithGestureOffset(0.01, 0, EPSILON),
+                      WithMotionClassification(MotionClassification::MULTI_FINGER_SWIPE),
+                      WithPointerCount(4u), WithToolType(AMOTION_EVENT_TOOL_TYPE_FINGER)));
+    EXPECT_EQ(arg.pointerCoords[0].getX(), finger0Start.getX() + 10);
+    EXPECT_EQ(arg.pointerCoords[1].getX(), finger1Start.getX() + 10);
+    EXPECT_EQ(arg.pointerCoords[2].getX(), finger2Start.getX() + 10);
+    EXPECT_EQ(arg.pointerCoords[3].getX(), finger3Start.getX() + 10);
+    EXPECT_EQ(arg.pointerCoords[0].getY(), finger0Start.getY());
+    EXPECT_EQ(arg.pointerCoords[1].getY(), finger1Start.getY());
+    EXPECT_EQ(arg.pointerCoords[2].getY(), finger2Start.getY());
+    EXPECT_EQ(arg.pointerCoords[3].getY(), finger3Start.getY());
+
+    Gesture continueGesture(kGestureFourFingerSwipe, ARBITRARY_GESTURE_TIME, ARBITRARY_GESTURE_TIME,
+                            /* dx= */ 5, /* dy= */ 0);
+    args = converter.handleGesture(ARBITRARY_TIME, READ_TIME, continueGesture);
+    ASSERT_EQ(1u, args.size());
+    arg = std::get<NotifyMotionArgs>(args.front());
+    ASSERT_THAT(arg,
+                AllOf(WithMotionAction(AMOTION_EVENT_ACTION_MOVE),
+                      WithGestureOffset(0.005, 0, EPSILON),
+                      WithMotionClassification(MotionClassification::MULTI_FINGER_SWIPE),
+                      WithPointerCount(4u), WithToolType(AMOTION_EVENT_TOOL_TYPE_FINGER)));
+    EXPECT_EQ(arg.pointerCoords[0].getX(), finger0Start.getX() + 15);
+    EXPECT_EQ(arg.pointerCoords[1].getX(), finger1Start.getX() + 15);
+    EXPECT_EQ(arg.pointerCoords[2].getX(), finger2Start.getX() + 15);
+    EXPECT_EQ(arg.pointerCoords[3].getX(), finger3Start.getX() + 15);
+    EXPECT_EQ(arg.pointerCoords[0].getY(), finger0Start.getY());
+    EXPECT_EQ(arg.pointerCoords[1].getY(), finger1Start.getY());
+    EXPECT_EQ(arg.pointerCoords[2].getY(), finger2Start.getY());
+    EXPECT_EQ(arg.pointerCoords[3].getY(), finger3Start.getY());
+
+    Gesture liftGesture(kGestureSwipeLift, ARBITRARY_GESTURE_TIME, ARBITRARY_GESTURE_TIME);
+    args = converter.handleGesture(ARBITRARY_TIME, READ_TIME, liftGesture);
+    ASSERT_EQ(4u, args.size());
+    ASSERT_THAT(std::get<NotifyMotionArgs>(args.front()),
+                AllOf(WithMotionAction(AMOTION_EVENT_ACTION_POINTER_UP |
+                                       3 << AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT),
+                      WithGestureOffset(0, 0, EPSILON),
+                      WithMotionClassification(MotionClassification::MULTI_FINGER_SWIPE),
+                      WithPointerCount(4u), WithToolType(AMOTION_EVENT_TOOL_TYPE_FINGER)));
+    args.pop_front();
+    ASSERT_THAT(std::get<NotifyMotionArgs>(args.front()),
+                AllOf(WithMotionAction(AMOTION_EVENT_ACTION_POINTER_UP |
+                                       2 << AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT),
+                      WithGestureOffset(0, 0, EPSILON),
+                      WithMotionClassification(MotionClassification::MULTI_FINGER_SWIPE),
+                      WithPointerCount(3u), WithToolType(AMOTION_EVENT_TOOL_TYPE_FINGER)));
+    args.pop_front();
+    ASSERT_THAT(std::get<NotifyMotionArgs>(args.front()),
+                AllOf(WithMotionAction(AMOTION_EVENT_ACTION_POINTER_UP |
+                                       1 << AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT),
+                      WithGestureOffset(0, 0, EPSILON),
+                      WithMotionClassification(MotionClassification::MULTI_FINGER_SWIPE),
+                      WithPointerCount(2u), WithToolType(AMOTION_EVENT_TOOL_TYPE_FINGER)));
+    args.pop_front();
+    ASSERT_THAT(std::get<NotifyMotionArgs>(args.front()),
+                AllOf(WithMotionAction(AMOTION_EVENT_ACTION_UP), WithGestureOffset(0, 0, EPSILON),
+                      WithMotionClassification(MotionClassification::MULTI_FINGER_SWIPE),
+                      WithPointerCount(1u), WithToolType(AMOTION_EVENT_TOOL_TYPE_FINGER)));
+}
+
 } // namespace android
diff --git a/services/inputflinger/tests/TestInputListenerMatchers.h b/services/inputflinger/tests/TestInputListenerMatchers.h
index e5a4b14..64c2c75 100644
--- a/services/inputflinger/tests/TestInputListenerMatchers.h
+++ b/services/inputflinger/tests/TestInputListenerMatchers.h
@@ -16,6 +16,8 @@
 
 #pragma once
 
+#include <cmath>
+
 #include <android/input.h>
 #include <gmock/gmock.h>
 #include <gtest/gtest.h>
@@ -66,6 +68,11 @@
     return arg.keyCode == keyCode;
 }
 
+MATCHER_P(WithPointerCount, count, "MotionEvent with specified number of pointers") {
+    *result_listener << "expected " << count << " pointer(s), but got " << arg.pointerCount;
+    return arg.pointerCount == count;
+}
+
 MATCHER_P2(WithCoords, x, y, "InputEvent with specified coords") {
     const auto argX = arg.pointerCoords[0].getX();
     const auto argY = arg.pointerCoords[0].getY();
@@ -82,6 +89,17 @@
     return argX == x && argY == y;
 }
 
+MATCHER_P3(WithGestureOffset, dx, dy, epsilon,
+           "InputEvent with specified touchpad gesture offset") {
+    const auto argGestureX = arg.pointerCoords[0].getAxisValue(AMOTION_EVENT_AXIS_GESTURE_X_OFFSET);
+    const auto argGestureY = arg.pointerCoords[0].getAxisValue(AMOTION_EVENT_AXIS_GESTURE_Y_OFFSET);
+    const double xDiff = fabs(argGestureX - dx);
+    const double yDiff = fabs(argGestureY - dy);
+    *result_listener << "expected gesture offset (" << dx << ", " << dy << ") within " << epsilon
+                     << ", but got (" << argGestureX << ", " << argGestureY << ")";
+    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;
@@ -100,6 +118,13 @@
     return arg.flags == flags;
 }
 
+MATCHER_P(WithMotionClassification, classification,
+          "InputEvent with specified MotionClassification") {
+    *result_listener << "expected classification " << motionClassificationToString(classification)
+                     << ", but got " << motionClassificationToString(arg.classification);
+    return arg.classification == classification;
+}
+
 MATCHER_P(WithButtonState, buttons, "InputEvent with specified button state") {
     *result_listener << "expected button state " << buttons << ", but got " << arg.buttonState;
     return arg.buttonState == buttons;