CursorInputMapper: share acceleration curves with touchpad

The new touchpad mapper implemented in Android 14 replaced our simple
cursor movement acceleration curves (where the acceleration factor
increased linearly with speed between minimum and maximum values) with
more sophisticated multi-segment curves. However, cursor movement using
mice remained on the old curves. For consistency and to improve pointing
accuracy, use the same curves for mice, too.

This is also a good opportunity to improve the documentation comments
and naming now that I've wrapped my head around the maths a bit better.

Bug: 315313622
Test: atest inputflinger_tests
Test: check pointer movement with a mouse, including changing the
      pointer speed setting and checking that the movement speed changes
Change-Id: Ifcf43f4de6017f06b66f37d5e03a13cc257d92d5
diff --git a/libs/input/AccelerationCurve.cpp b/libs/input/AccelerationCurve.cpp
new file mode 100644
index 0000000..0a92a71
--- /dev/null
+++ b/libs/input/AccelerationCurve.cpp
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <input/AccelerationCurve.h>
+
+#include <array>
+#include <limits>
+
+#include <log/log_main.h>
+
+#define LOG_TAG "AccelerationCurve"
+
+namespace android {
+
+namespace {
+
+// The last segment must have an infinite maximum speed, so that all speeds are covered.
+constexpr std::array<AccelerationCurveSegment, 4> kSegments = {{
+        {32.002, 3.19, 0},
+        {52.83, 4.79, -51.254},
+        {119.124, 7.28, -182.737},
+        {std::numeric_limits<double>::infinity(), 15.04, -1107.556},
+}};
+
+static_assert(kSegments.back().maxPointerSpeedMmPerS == std::numeric_limits<double>::infinity());
+
+constexpr std::array<double, 15> kSensitivityFactors = {1,  2,  4,  6,  7,  8,  9, 10,
+                                                        11, 12, 13, 14, 16, 18, 20};
+
+} // namespace
+
+std::vector<AccelerationCurveSegment> createAccelerationCurveForPointerSensitivity(
+        int32_t sensitivity) {
+    LOG_ALWAYS_FATAL_IF(sensitivity < -7 || sensitivity > 7, "Invalid pointer sensitivity value");
+    std::vector<AccelerationCurveSegment> output;
+    output.reserve(kSegments.size());
+
+    // The curves we want to produce for different sensitivity values are actually the same curve,
+    // just scaled in the Y (gain) axis by a sensitivity factor and a couple of constants.
+    double commonFactor = 0.64 * kSensitivityFactors[sensitivity + 7] / 10;
+    for (AccelerationCurveSegment seg : kSegments) {
+        output.push_back(AccelerationCurveSegment{seg.maxPointerSpeedMmPerS,
+                                                  commonFactor * seg.baseGain,
+                                                  commonFactor * seg.reciprocal});
+    }
+
+    return output;
+}
+
+} // namespace android
\ No newline at end of file
diff --git a/libs/input/Android.bp b/libs/input/Android.bp
index dd8dc8d..c5218f6 100644
--- a/libs/input/Android.bp
+++ b/libs/input/Android.bp
@@ -175,6 +175,7 @@
     ],
     srcs: [
         "android/os/IInputFlinger.aidl",
+        "AccelerationCurve.cpp",
         "Input.cpp",
         "InputDevice.cpp",
         "InputEventLabels.cpp",
diff --git a/libs/input/VelocityControl.cpp b/libs/input/VelocityControl.cpp
index c835a08..edd31e9 100644
--- a/libs/input/VelocityControl.cpp
+++ b/libs/input/VelocityControl.cpp
@@ -15,7 +15,6 @@
  */
 
 #define LOG_TAG "VelocityControl"
-//#define LOG_NDEBUG 0
 
 // Log debug messages about acceleration.
 static constexpr bool DEBUG_ACCELERATION = false;
@@ -23,6 +22,7 @@
 #include <math.h>
 #include <limits.h>
 
+#include <android-base/logging.h>
 #include <input/VelocityControl.h>
 #include <utils/BitSet.h>
 #include <utils/Timers.h>
@@ -37,15 +37,6 @@
     reset();
 }
 
-const VelocityControlParameters& VelocityControl::getParameters() const{
-    return mParameters;
-}
-
-void VelocityControl::setParameters(const VelocityControlParameters& parameters) {
-    mParameters = parameters;
-    reset();
-}
-
 void VelocityControl::reset() {
     mLastMovementTime = LLONG_MIN;
     mRawPositionX = 0;
@@ -54,65 +45,156 @@
 }
 
 void VelocityControl::move(nsecs_t eventTime, float* deltaX, float* deltaY) {
-    if ((deltaX && *deltaX) || (deltaY && *deltaY)) {
-        if (eventTime >= mLastMovementTime + STOP_TIME) {
-            if (DEBUG_ACCELERATION && mLastMovementTime != LLONG_MIN) {
-                ALOGD("VelocityControl: stopped, last movement was %0.3fms ago",
-                           (eventTime - mLastMovementTime) * 0.000001f);
-            }
-            reset();
+    if ((deltaX == nullptr || *deltaX == 0) && (deltaY == nullptr || *deltaY == 0)) {
+        return;
+    }
+    if (eventTime >= mLastMovementTime + STOP_TIME) {
+        ALOGD_IF(DEBUG_ACCELERATION && mLastMovementTime != LLONG_MIN,
+                 "VelocityControl: stopped, last movement was %0.3fms ago",
+                 (eventTime - mLastMovementTime) * 0.000001f);
+        reset();
+    }
+
+    mLastMovementTime = eventTime;
+    if (deltaX) {
+        mRawPositionX += *deltaX;
+    }
+    if (deltaY) {
+        mRawPositionY += *deltaY;
+    }
+    mVelocityTracker.addMovement(eventTime, /*pointerId=*/0, AMOTION_EVENT_AXIS_X, mRawPositionX);
+    mVelocityTracker.addMovement(eventTime, /*pointerId=*/0, AMOTION_EVENT_AXIS_Y, mRawPositionY);
+    scaleDeltas(deltaX, deltaY);
+}
+
+// --- SimpleVelocityControl ---
+
+const VelocityControlParameters& SimpleVelocityControl::getParameters() const {
+    return mParameters;
+}
+
+void SimpleVelocityControl::setParameters(const VelocityControlParameters& parameters) {
+    mParameters = parameters;
+    reset();
+}
+
+void SimpleVelocityControl::scaleDeltas(float* deltaX, float* deltaY) {
+    std::optional<float> vx = mVelocityTracker.getVelocity(AMOTION_EVENT_AXIS_X, 0);
+    std::optional<float> vy = mVelocityTracker.getVelocity(AMOTION_EVENT_AXIS_Y, 0);
+    float scale = mParameters.scale;
+    if (vx.has_value() && vy.has_value()) {
+        float speed = hypotf(*vx, *vy) * scale;
+        if (speed >= mParameters.highThreshold) {
+            // Apply full acceleration above the high speed threshold.
+            scale *= mParameters.acceleration;
+        } else if (speed > mParameters.lowThreshold) {
+            // Linearly interpolate the acceleration to apply between the low and high
+            // speed thresholds.
+            scale *= 1 +
+                    (speed - mParameters.lowThreshold) /
+                            (mParameters.highThreshold - mParameters.lowThreshold) *
+                            (mParameters.acceleration - 1);
         }
 
-        mLastMovementTime = eventTime;
-        if (deltaX) {
-            mRawPositionX += *deltaX;
-        }
-        if (deltaY) {
-            mRawPositionY += *deltaY;
-        }
-        mVelocityTracker.addMovement(eventTime, /*pointerId=*/0, AMOTION_EVENT_AXIS_X,
-                                     mRawPositionX);
-        mVelocityTracker.addMovement(eventTime, /*pointerId=*/0, AMOTION_EVENT_AXIS_Y,
-                                     mRawPositionY);
+        ALOGD_IF(DEBUG_ACCELERATION,
+                 "SimpleVelocityControl(%0.3f, %0.3f, %0.3f, %0.3f): "
+                 "vx=%0.3f, vy=%0.3f, speed=%0.3f, accel=%0.3f",
+                 mParameters.scale, mParameters.lowThreshold, mParameters.highThreshold,
+                 mParameters.acceleration, *vx, *vy, speed, scale / mParameters.scale);
 
-        std::optional<float> vx = mVelocityTracker.getVelocity(AMOTION_EVENT_AXIS_X, 0);
-        std::optional<float> vy = mVelocityTracker.getVelocity(AMOTION_EVENT_AXIS_Y, 0);
-        float scale = mParameters.scale;
-        if (vx && vy) {
-            float speed = hypotf(*vx, *vy) * scale;
-            if (speed >= mParameters.highThreshold) {
-                // Apply full acceleration above the high speed threshold.
-                scale *= mParameters.acceleration;
-            } else if (speed > mParameters.lowThreshold) {
-                // Linearly interpolate the acceleration to apply between the low and high
-                // speed thresholds.
-                scale *= 1 + (speed - mParameters.lowThreshold)
-                        / (mParameters.highThreshold - mParameters.lowThreshold)
-                        * (mParameters.acceleration - 1);
-            }
+    } else {
+        ALOGD_IF(DEBUG_ACCELERATION,
+                 "SimpleVelocityControl(%0.3f, %0.3f, %0.3f, %0.3f): unknown velocity",
+                 mParameters.scale, mParameters.lowThreshold, mParameters.highThreshold,
+                 mParameters.acceleration);
+    }
 
-            if (DEBUG_ACCELERATION) {
-                ALOGD("VelocityControl(%0.3f, %0.3f, %0.3f, %0.3f): "
-                      "vx=%0.3f, vy=%0.3f, speed=%0.3f, accel=%0.3f",
-                      mParameters.scale, mParameters.lowThreshold, mParameters.highThreshold,
-                      mParameters.acceleration, *vx, *vy, speed, scale / mParameters.scale);
-            }
+    if (deltaX != nullptr) {
+        *deltaX *= scale;
+    }
+    if (deltaY != nullptr) {
+        *deltaY *= scale;
+    }
+}
 
-        } else {
-            if (DEBUG_ACCELERATION) {
-                ALOGD("VelocityControl(%0.3f, %0.3f, %0.3f, %0.3f): unknown velocity",
-                        mParameters.scale, mParameters.lowThreshold, mParameters.highThreshold,
-                        mParameters.acceleration);
-            }
-        }
+// --- CurvedVelocityControl ---
 
-        if (deltaX) {
-            *deltaX *= scale;
-        }
-        if (deltaY) {
-            *deltaY *= scale;
+namespace {
+
+/**
+ * The resolution that we assume a mouse to have, in counts per inch.
+ *
+ * Mouse resolutions vary wildly, but 800 CPI is probably the most common. There should be enough
+ * range in the available sensitivity settings to accommodate users of mice with other resolutions.
+ */
+constexpr int32_t MOUSE_CPI = 800;
+
+float countsToMm(float counts) {
+    return counts / MOUSE_CPI * 25.4;
+}
+
+} // namespace
+
+CurvedVelocityControl::CurvedVelocityControl()
+      : mCurveSegments(createAccelerationCurveForPointerSensitivity(0)) {}
+
+void CurvedVelocityControl::setCurve(const std::vector<AccelerationCurveSegment>& curve) {
+    mCurveSegments = curve;
+}
+
+void CurvedVelocityControl::setAccelerationEnabled(bool enabled) {
+    mAccelerationEnabled = enabled;
+}
+
+void CurvedVelocityControl::scaleDeltas(float* deltaX, float* deltaY) {
+    if (!mAccelerationEnabled) {
+        ALOGD_IF(DEBUG_ACCELERATION, "CurvedVelocityControl: acceleration disabled");
+        return;
+    }
+
+    std::optional<float> vx = mVelocityTracker.getVelocity(AMOTION_EVENT_AXIS_X, 0);
+    std::optional<float> vy = mVelocityTracker.getVelocity(AMOTION_EVENT_AXIS_Y, 0);
+
+    float ratio;
+    if (vx.has_value() && vy.has_value()) {
+        float vxMmPerS = countsToMm(*vx);
+        float vyMmPerS = countsToMm(*vy);
+        float speedMmPerS = sqrtf(vxMmPerS * vxMmPerS + vyMmPerS * vyMmPerS);
+
+        const AccelerationCurveSegment& seg = segmentForSpeed(speedMmPerS);
+        ratio = seg.baseGain + seg.reciprocal / speedMmPerS;
+        ALOGD_IF(DEBUG_ACCELERATION,
+                 "CurvedVelocityControl: velocities (%0.3f, %0.3f) → speed %0.3f → ratio %0.3f",
+                 vxMmPerS, vyMmPerS, speedMmPerS, ratio);
+    } else {
+        // We don't have enough data to compute a velocity yet. This happens early in the movement,
+        // when the speed is presumably low, so use the base gain of the first segment of the curve.
+        // (This would behave oddly for curves with a reciprocal term on the first segment, but we
+        // don't have any of those, and they'd be very strange at velocities close to zero anyway.)
+        ratio = mCurveSegments[0].baseGain;
+        ALOGD_IF(DEBUG_ACCELERATION,
+                 "CurvedVelocityControl: unknown velocity, using base gain of first segment (%.3f)",
+                 ratio);
+    }
+
+    if (deltaX != nullptr) {
+        *deltaX *= ratio;
+    }
+    if (deltaY != nullptr) {
+        *deltaY *= ratio;
+    }
+}
+
+const AccelerationCurveSegment& CurvedVelocityControl::segmentForSpeed(float speedMmPerS) {
+    for (const AccelerationCurveSegment& seg : mCurveSegments) {
+        if (speedMmPerS <= seg.maxPointerSpeedMmPerS) {
+            return seg;
         }
     }
+    ALOGE("CurvedVelocityControl: No segment found for speed %.3f; last segment should always have "
+          "a max speed of infinity.",
+          speedMmPerS);
+    return mCurveSegments.back();
 }
 
 } // namespace android
diff --git a/libs/input/input_flags.aconfig b/libs/input/input_flags.aconfig
index 11f6994..1baeb26 100644
--- a/libs/input/input_flags.aconfig
+++ b/libs/input/input_flags.aconfig
@@ -97,3 +97,10 @@
   description: "Remove pointer event tracking in WM after the Pointer Icon Refactor"
   bug: "315321016"
 }
+
+flag {
+  name: "enable_new_mouse_pointer_ballistics"
+  namespace: "input"
+  description: "Change the acceleration curves for mouse pointer movements to match the touchpad ones"
+  bug: "315313622"
+}
diff --git a/libs/input/tests/Android.bp b/libs/input/tests/Android.bp
index 138898f..fa3ea2f 100644
--- a/libs/input/tests/Android.bp
+++ b/libs/input/tests/Android.bp
@@ -25,6 +25,7 @@
         "TfLiteMotionPredictor_test.cpp",
         "TouchResampling_test.cpp",
         "TouchVideoFrame_test.cpp",
+        "VelocityControl_test.cpp",
         "VelocityTracker_test.cpp",
         "VerifiedInputEvent_test.cpp",
     ],
diff --git a/libs/input/tests/VelocityControl_test.cpp b/libs/input/tests/VelocityControl_test.cpp
new file mode 100644
index 0000000..63d64c6
--- /dev/null
+++ b/libs/input/tests/VelocityControl_test.cpp
@@ -0,0 +1,129 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <input/VelocityControl.h>
+
+#include <limits>
+
+#include <gtest/gtest.h>
+#include <input/AccelerationCurve.h>
+#include <utils/Timers.h>
+
+namespace android {
+
+namespace {
+
+constexpr float EPSILON = 0.001;
+constexpr float COUNTS_PER_MM = 800 / 25.4;
+
+} // namespace
+
+class CurvedVelocityControlTest : public testing::Test {
+protected:
+    CurvedVelocityControl mCtrl;
+
+    void moveWithoutCheckingResult(nsecs_t eventTime, float deltaX, float deltaY) {
+        mCtrl.move(eventTime, &deltaX, &deltaY);
+    }
+
+    void moveAndCheckRatio(nsecs_t eventTime, const float deltaX, const float deltaY,
+                           float expectedRatio) {
+        float newDeltaX = deltaX, newDeltaY = deltaY;
+        mCtrl.move(eventTime, &newDeltaX, &newDeltaY);
+        ASSERT_NEAR(expectedRatio * deltaX, newDeltaX, EPSILON)
+                << "Expected ratio of " << expectedRatio << " in X, but actual ratio was "
+                << newDeltaX / deltaX;
+        ASSERT_NEAR(expectedRatio * deltaY, newDeltaY, EPSILON)
+                << "Expected ratio of " << expectedRatio << " in Y, but actual ratio was "
+                << newDeltaY / deltaY;
+    }
+};
+
+TEST_F(CurvedVelocityControlTest, SegmentSelection) {
+    // To make the maths simple, use a "curve" that's actually just a sequence of steps.
+    mCtrl.setCurve({
+            {10, 2, 0},
+            {20, 3, 0},
+            {30, 4, 0},
+            {std::numeric_limits<double>::infinity(), 5, 0},
+    });
+
+    // Establish a velocity of 16 mm/s.
+    moveWithoutCheckingResult(0, 0, 0);
+    moveWithoutCheckingResult(10'000'000, 0.16 * COUNTS_PER_MM, 0);
+    moveWithoutCheckingResult(20'000'000, 0.16 * COUNTS_PER_MM, 0);
+    moveWithoutCheckingResult(30'000'000, 0.16 * COUNTS_PER_MM, 0);
+    ASSERT_NO_FATAL_FAILURE(
+            moveAndCheckRatio(40'000'000, 0.16 * COUNTS_PER_MM, 0, /*expectedRatio=*/3));
+
+    // Establish a velocity of 50 mm/s.
+    mCtrl.reset();
+    moveWithoutCheckingResult(100'000'000, 0, 0);
+    moveWithoutCheckingResult(110'000'000, 0.50 * COUNTS_PER_MM, 0);
+    moveWithoutCheckingResult(120'000'000, 0.50 * COUNTS_PER_MM, 0);
+    moveWithoutCheckingResult(130'000'000, 0.50 * COUNTS_PER_MM, 0);
+    ASSERT_NO_FATAL_FAILURE(
+            moveAndCheckRatio(140'000'000, 0.50 * COUNTS_PER_MM, 0, /*expectedRatio=*/5));
+}
+
+TEST_F(CurvedVelocityControlTest, RatioDefaultsToFirstSegmentWhenVelocityIsUnknown) {
+    mCtrl.setCurve({
+            {10, 3, 0},
+            {20, 2, 0},
+            {std::numeric_limits<double>::infinity(), 4, 0},
+    });
+
+    // Only send two moves, which won't be enough for VelocityTracker to calculate a velocity from.
+    moveWithoutCheckingResult(0, 0, 0);
+    ASSERT_NO_FATAL_FAILURE(
+            moveAndCheckRatio(10'000'000, 0.25 * COUNTS_PER_MM, 0, /*expectedRatio=*/3));
+}
+
+TEST_F(CurvedVelocityControlTest, VelocityCalculatedUsingBothAxes) {
+    mCtrl.setCurve({
+            {8.0, 3, 0},
+            {8.1, 2, 0},
+            {std::numeric_limits<double>::infinity(), 4, 0},
+    });
+
+    // Establish a velocity of 8.06 (= √65 = √(7²+4²)) mm/s between the two axes.
+    moveWithoutCheckingResult(0, 0, 0);
+    moveWithoutCheckingResult(10'000'000, 0.07 * COUNTS_PER_MM, 0.04 * COUNTS_PER_MM);
+    moveWithoutCheckingResult(20'000'000, 0.07 * COUNTS_PER_MM, 0.04 * COUNTS_PER_MM);
+    moveWithoutCheckingResult(30'000'000, 0.07 * COUNTS_PER_MM, 0.04 * COUNTS_PER_MM);
+    ASSERT_NO_FATAL_FAILURE(moveAndCheckRatio(40'000'000, 0.07 * COUNTS_PER_MM,
+                                              0.04 * COUNTS_PER_MM,
+                                              /*expectedRatio=*/2));
+}
+
+TEST_F(CurvedVelocityControlTest, ReciprocalTerm) {
+    mCtrl.setCurve({
+            {10, 2, 0},
+            {20, 3, -10},
+            {std::numeric_limits<double>::infinity(), 3, 0},
+    });
+
+    // Establish a velocity of 15 mm/s.
+    moveWithoutCheckingResult(0, 0, 0);
+    moveWithoutCheckingResult(10'000'000, 0, 0.15 * COUNTS_PER_MM);
+    moveWithoutCheckingResult(20'000'000, 0, 0.15 * COUNTS_PER_MM);
+    moveWithoutCheckingResult(30'000'000, 0, 0.15 * COUNTS_PER_MM);
+    // Expected ratio is 3 - 10 / 15 = 2.33333...
+    ASSERT_NO_FATAL_FAILURE(
+            moveAndCheckRatio(40'000'000, 0, 0.15 * COUNTS_PER_MM, /*expectedRatio=*/2.33333));
+}
+
+} // namespace android
\ No newline at end of file