Native support for rotary encoder high-res scroll

Test: atest RotaryEncoderInputMapperTest
Test: atest VirtualRotaryEncoderTest
Flag: android.companion.virtualdevice.flags.high_resolution_scroll
Bug: 320328752
Change-Id: Iac9092597010582bd3f55e51ee63e9eb9c8d9433
diff --git a/include/input/Input.h b/include/input/Input.h
index 17672d1..77d7448 100644
--- a/include/input/Input.h
+++ b/include/input/Input.h
@@ -196,11 +196,11 @@
 #define MAX_POINTER_ID 31
 
 /*
- * Number of high resolution mouse scroll units for one detent (mouse wheel click), as defined in
+ * Number of high resolution scroll units for one detent (scroll wheel click), as defined in
  * evdev. This is relevant when an input device is emitting REL_WHEEL_HI_RES or REL_HWHEEL_HI_RES
  * events.
  */
-constexpr int32_t kEvdevMouseHighResScrollUnitsPerDetent = 120;
+constexpr int32_t kEvdevHighResScrollUnitsPerDetent = 120;
 
 /*
  * Declare a concrete type for the NDK's input event forward declaration.
diff --git a/include/input/VirtualInputDevice.h b/include/input/VirtualInputDevice.h
index 9fbae73..dabe45c 100644
--- a/include/input/VirtualInputDevice.h
+++ b/include/input/VirtualInputDevice.h
@@ -129,6 +129,9 @@
     VirtualRotaryEncoder(android::base::unique_fd fd);
     virtual ~VirtualRotaryEncoder() override;
     bool writeScrollEvent(float scrollAmount, std::chrono::nanoseconds eventTime);
+
+private:
+    int32_t mAccumulatedHighResScrollAmount;
 };
 
 } // namespace android
diff --git a/libs/input/VirtualInputDevice.cpp b/libs/input/VirtualInputDevice.cpp
index 2e3e1a0..0579967 100644
--- a/libs/input/VirtualInputDevice.cpp
+++ b/libs/input/VirtualInputDevice.cpp
@@ -279,13 +279,17 @@
 bool VirtualMouse::writeScrollEvent(float xAxisMovement, float yAxisMovement,
                                     std::chrono::nanoseconds eventTime) {
     if (!vd_flags::high_resolution_scroll()) {
-        return writeInputEvent(EV_REL, REL_HWHEEL, xAxisMovement, eventTime) &&
-                writeInputEvent(EV_REL, REL_WHEEL, yAxisMovement, eventTime) &&
+        return writeInputEvent(EV_REL, REL_HWHEEL, static_cast<int32_t>(xAxisMovement),
+                               eventTime) &&
+                writeInputEvent(EV_REL, REL_WHEEL, static_cast<int32_t>(yAxisMovement),
+                                eventTime) &&
                 writeInputEvent(EV_SYN, SYN_REPORT, 0, eventTime);
     }
 
-    const int32_t highResScrollX = xAxisMovement * kEvdevMouseHighResScrollUnitsPerDetent;
-    const int32_t highResScrollY = yAxisMovement * kEvdevMouseHighResScrollUnitsPerDetent;
+    const auto highResScrollX =
+            static_cast<int32_t>(xAxisMovement * kEvdevHighResScrollUnitsPerDetent);
+    const auto highResScrollY =
+            static_cast<int32_t>(yAxisMovement * kEvdevHighResScrollUnitsPerDetent);
     bool highResScrollResult =
             writeInputEvent(EV_REL, REL_HWHEEL_HI_RES, highResScrollX, eventTime) &&
             writeInputEvent(EV_REL, REL_WHEEL_HI_RES, highResScrollY, eventTime);
@@ -299,19 +303,19 @@
     // (single mouse wheel click).
     mAccumulatedHighResScrollX += highResScrollX;
     mAccumulatedHighResScrollY += highResScrollY;
-    const int32_t scrollX = mAccumulatedHighResScrollX / kEvdevMouseHighResScrollUnitsPerDetent;
-    const int32_t scrollY = mAccumulatedHighResScrollY / kEvdevMouseHighResScrollUnitsPerDetent;
+    const int32_t scrollX = mAccumulatedHighResScrollX / kEvdevHighResScrollUnitsPerDetent;
+    const int32_t scrollY = mAccumulatedHighResScrollY / kEvdevHighResScrollUnitsPerDetent;
     if (scrollX != 0) {
         if (!writeInputEvent(EV_REL, REL_HWHEEL, scrollX, eventTime)) {
             return false;
         }
-        mAccumulatedHighResScrollX %= kEvdevMouseHighResScrollUnitsPerDetent;
+        mAccumulatedHighResScrollX %= kEvdevHighResScrollUnitsPerDetent;
     }
     if (scrollY != 0) {
         if (!writeInputEvent(EV_REL, REL_WHEEL, scrollY, eventTime)) {
             return false;
         }
-        mAccumulatedHighResScrollY %= kEvdevMouseHighResScrollUnitsPerDetent;
+        mAccumulatedHighResScrollY %= kEvdevHighResScrollUnitsPerDetent;
     }
 
     return writeInputEvent(EV_SYN, SYN_REPORT, 0, eventTime);
@@ -550,14 +554,38 @@
 }
 
 // --- VirtualRotaryEncoder ---
-VirtualRotaryEncoder::VirtualRotaryEncoder(unique_fd fd) : VirtualInputDevice(std::move(fd)) {}
+VirtualRotaryEncoder::VirtualRotaryEncoder(unique_fd fd)
+      : VirtualInputDevice(std::move(fd)), mAccumulatedHighResScrollAmount(0) {}
 
 VirtualRotaryEncoder::~VirtualRotaryEncoder() {}
 
 bool VirtualRotaryEncoder::writeScrollEvent(float scrollAmount,
                                             std::chrono::nanoseconds eventTime) {
-    return writeInputEvent(EV_REL, REL_WHEEL, static_cast<int32_t>(scrollAmount), eventTime) &&
-            writeInputEvent(EV_SYN, SYN_REPORT, 0, eventTime);
+    if (!vd_flags::high_resolution_scroll()) {
+        return writeInputEvent(EV_REL, REL_WHEEL, static_cast<int32_t>(scrollAmount), eventTime) &&
+                writeInputEvent(EV_SYN, SYN_REPORT, 0, eventTime);
+    }
+
+    const auto highResScrollAmount =
+            static_cast<int32_t>(scrollAmount * kEvdevHighResScrollUnitsPerDetent);
+    if (!writeInputEvent(EV_REL, REL_WHEEL_HI_RES, highResScrollAmount, eventTime)) {
+        return false;
+    }
+
+    // According to evdev spec, a high-resolution scroll device needs to emit REL_WHEEL / REL_HWHEEL
+    // events in addition to high-res scroll events. Regular scroll events can approximate high-res
+    // scroll events, so we send a regular scroll event when the accumulated scroll motion reaches a
+    // detent (single wheel click).
+    mAccumulatedHighResScrollAmount += highResScrollAmount;
+    const int32_t scroll = mAccumulatedHighResScrollAmount / kEvdevHighResScrollUnitsPerDetent;
+    if (scroll != 0) {
+        if (!writeInputEvent(EV_REL, REL_WHEEL, scroll, eventTime)) {
+            return false;
+        }
+        mAccumulatedHighResScrollAmount %= kEvdevHighResScrollUnitsPerDetent;
+    }
+
+    return writeInputEvent(EV_SYN, SYN_REPORT, 0, eventTime);
 }
 
 } // namespace android
diff --git a/services/inputflinger/reader/mapper/RotaryEncoderInputMapper.cpp b/services/inputflinger/reader/mapper/RotaryEncoderInputMapper.cpp
index 20fd359..b72cc6e 100644
--- a/services/inputflinger/reader/mapper/RotaryEncoderInputMapper.cpp
+++ b/services/inputflinger/reader/mapper/RotaryEncoderInputMapper.cpp
@@ -27,11 +27,14 @@
 
 namespace android {
 
+constexpr float kDefaultScaleFactor = 1.0f;
+
 RotaryEncoderInputMapper::RotaryEncoderInputMapper(InputDeviceContext& deviceContext,
                                                    const InputReaderConfiguration& readerConfig)
-      : InputMapper(deviceContext, readerConfig), mOrientation(ui::ROTATION_0) {
-    mSource = AINPUT_SOURCE_ROTARY_ENCODER;
-}
+      : InputMapper(deviceContext, readerConfig),
+        mSource(AINPUT_SOURCE_ROTARY_ENCODER),
+        mScalingFactor(kDefaultScaleFactor),
+        mOrientation(ui::ROTATION_0) {}
 
 RotaryEncoderInputMapper::~RotaryEncoderInputMapper() {}
 
@@ -51,9 +54,10 @@
         std::optional<float> scalingFactor = config.getFloat("device.scalingFactor");
         if (!scalingFactor.has_value()) {
             ALOGW("Rotary Encoder device configuration file didn't specify scaling factor,"
-                  "default to 1.0!\n");
+                  "default to %f!\n",
+                  kDefaultScaleFactor);
         }
-        mScalingFactor = scalingFactor.value_or(1.0f);
+        mScalingFactor = scalingFactor.value_or(kDefaultScaleFactor);
         info.addMotionRange(AMOTION_EVENT_AXIS_SCROLL, mSource, -1.0f, 1.0f, 0.0f, 0.0f,
                             res.value_or(0.0f) * mScalingFactor);
     }
diff --git a/services/inputflinger/reader/mapper/accumulator/CursorScrollAccumulator.cpp b/services/inputflinger/reader/mapper/accumulator/CursorScrollAccumulator.cpp
index 06315e2..5373440 100644
--- a/services/inputflinger/reader/mapper/accumulator/CursorScrollAccumulator.cpp
+++ b/services/inputflinger/reader/mapper/accumulator/CursorScrollAccumulator.cpp
@@ -55,14 +55,14 @@
         switch (rawEvent.code) {
             case REL_WHEEL_HI_RES:
                 if (mHaveRelWheelHighRes) {
-                    mRelWheel = rawEvent.value /
-                            static_cast<float>(kEvdevMouseHighResScrollUnitsPerDetent);
+                    mRelWheel =
+                            rawEvent.value / static_cast<float>(kEvdevHighResScrollUnitsPerDetent);
                 }
                 break;
             case REL_HWHEEL_HI_RES:
                 if (mHaveRelHWheelHighRes) {
-                    mRelHWheel = rawEvent.value /
-                            static_cast<float>(kEvdevMouseHighResScrollUnitsPerDetent);
+                    mRelHWheel =
+                            rawEvent.value / static_cast<float>(kEvdevHighResScrollUnitsPerDetent);
                 }
                 break;
             case REL_WHEEL:
diff --git a/services/inputflinger/reader/mapper/accumulator/CursorScrollAccumulator.h b/services/inputflinger/reader/mapper/accumulator/CursorScrollAccumulator.h
index 6990d20..d3373cc 100644
--- a/services/inputflinger/reader/mapper/accumulator/CursorScrollAccumulator.h
+++ b/services/inputflinger/reader/mapper/accumulator/CursorScrollAccumulator.h
@@ -16,8 +16,6 @@
 
 #pragma once
 
-#include <stdint.h>
-
 namespace android {
 
 class InputDeviceContext;
@@ -36,8 +34,6 @@
     inline bool haveRelativeVWheel() const { return mHaveRelWheel; }
     inline bool haveRelativeHWheel() const { return mHaveRelHWheel; }
 
-    inline int32_t getRelativeX() const { return mRelX; }
-    inline int32_t getRelativeY() const { return mRelY; }
     inline float getRelativeVWheel() const { return mRelWheel; }
     inline float getRelativeHWheel() const { return mRelHWheel; }
 
@@ -47,8 +43,6 @@
     bool mHaveRelWheelHighRes;
     bool mHaveRelHWheelHighRes;
 
-    int32_t mRelX;
-    int32_t mRelY;
     float mRelWheel;
     float mRelHWheel;
 
diff --git a/services/inputflinger/tests/RotaryEncoderInputMapper_test.cpp b/services/inputflinger/tests/RotaryEncoderInputMapper_test.cpp
index 2b8071b..366b3dc 100644
--- a/services/inputflinger/tests/RotaryEncoderInputMapper_test.cpp
+++ b/services/inputflinger/tests/RotaryEncoderInputMapper_test.cpp
@@ -22,6 +22,7 @@
 #include <variant>
 
 #include <android-base/logging.h>
+#include <android_companion_virtualdevice_flags.h>
 #include <gtest/gtest.h>
 #include <input/DisplayViewport.h>
 #include <linux/input-event-codes.h>
@@ -109,6 +110,8 @@
 
 } // namespace
 
+namespace vd_flags = android::companion::virtualdevice::flags;
+
 /**
  * Unit tests for RotaryEncoderInputMapper.
  */
@@ -170,4 +173,53 @@
                               WithDisplayId(ui::LogicalDisplayId::INVALID)))));
 }
 
+TEST_F(RotaryEncoderInputMapperTest, ProcessRegularScroll) {
+    createDevice();
+    mMapper = createInputMapper<RotaryEncoderInputMapper>(*mDeviceContext, mReaderConfiguration);
+
+    std::list<NotifyArgs> args;
+    args += process(ARBITRARY_TIME, EV_REL, REL_WHEEL, 1);
+    args += process(ARBITRARY_TIME, EV_SYN, SYN_REPORT, 0);
+
+    EXPECT_THAT(args,
+                ElementsAre(VariantWith<NotifyMotionArgs>(
+                        AllOf(WithSource(AINPUT_SOURCE_ROTARY_ENCODER),
+                              WithMotionAction(AMOTION_EVENT_ACTION_SCROLL), WithScroll(1.0f)))));
+}
+
+TEST_F(RotaryEncoderInputMapperTest, ProcessHighResScroll) {
+    vd_flags::high_resolution_scroll(true);
+    EXPECT_CALL(mMockEventHub, hasRelativeAxis(EVENTHUB_ID, REL_WHEEL_HI_RES))
+            .WillRepeatedly(Return(true));
+    createDevice();
+    mMapper = createInputMapper<RotaryEncoderInputMapper>(*mDeviceContext, mReaderConfiguration);
+
+    std::list<NotifyArgs> args;
+    args += process(ARBITRARY_TIME, EV_REL, REL_WHEEL_HI_RES, 60);
+    args += process(ARBITRARY_TIME, EV_SYN, SYN_REPORT, 0);
+
+    EXPECT_THAT(args,
+                ElementsAre(VariantWith<NotifyMotionArgs>(
+                        AllOf(WithSource(AINPUT_SOURCE_ROTARY_ENCODER),
+                              WithMotionAction(AMOTION_EVENT_ACTION_SCROLL), WithScroll(0.5f)))));
+}
+
+TEST_F(RotaryEncoderInputMapperTest, HighResScrollIgnoresRegularScroll) {
+    vd_flags::high_resolution_scroll(true);
+    EXPECT_CALL(mMockEventHub, hasRelativeAxis(EVENTHUB_ID, REL_WHEEL_HI_RES))
+            .WillRepeatedly(Return(true));
+    createDevice();
+    mMapper = createInputMapper<RotaryEncoderInputMapper>(*mDeviceContext, mReaderConfiguration);
+
+    std::list<NotifyArgs> args;
+    args += process(ARBITRARY_TIME, EV_REL, REL_WHEEL_HI_RES, 60);
+    args += process(ARBITRARY_TIME, EV_REL, REL_WHEEL, 1);
+    args += process(ARBITRARY_TIME, EV_SYN, SYN_REPORT, 0);
+
+    EXPECT_THAT(args,
+                ElementsAre(VariantWith<NotifyMotionArgs>(
+                        AllOf(WithSource(AINPUT_SOURCE_ROTARY_ENCODER),
+                              WithMotionAction(AMOTION_EVENT_ACTION_SCROLL), WithScroll(0.5f)))));
+}
+
 } // namespace android
\ No newline at end of file
diff --git a/services/inputflinger/tests/TestEventMatchers.h b/services/inputflinger/tests/TestEventMatchers.h
index f643fb1..f8e3c22 100644
--- a/services/inputflinger/tests/TestEventMatchers.h
+++ b/services/inputflinger/tests/TestEventMatchers.h
@@ -697,6 +697,12 @@
     return argDistance == distance;
 }
 
+MATCHER_P(WithScroll, scroll, "InputEvent with specified scroll value") {
+    const auto argScroll = arg.pointerCoords[0].getAxisValue(AMOTION_EVENT_AXIS_SCROLL);
+    *result_listener << "expected scroll value " << scroll << ", but got " << argScroll;
+    return argScroll == scroll;
+}
+
 MATCHER_P2(WithScroll, scrollX, scrollY, "InputEvent with specified scroll values") {
     const auto argScrollX = arg.pointerCoords[0].getAxisValue(AMOTION_EVENT_AXIS_HSCROLL);
     const auto argScrollY = arg.pointerCoords[0].getAxisValue(AMOTION_EVENT_AXIS_VSCROLL);