TouchpadInputMapper: support touchpad capture

This allows an app to capture the touchpad and receive "raw" touch
information from it. Since some specifics (such as how size is
calculated) aren't well-defined in documentation or tests currently,
when in doubt I tried to match the behaviour of MultiTouchInputMapper in
captured mode.

Bug: b/259547750
Test: atest inputflinger_tests
Test: use a test app to dump captured events, manually verify and
      compare with old behaviour
Change-Id: I482cce6d16ed6f947ce86e8453ddb2c9789d4d00
diff --git a/services/inputflinger/reader/Android.bp b/services/inputflinger/reader/Android.bp
index 132c3a1..b0edb57 100644
--- a/services/inputflinger/reader/Android.bp
+++ b/services/inputflinger/reader/Android.bp
@@ -42,6 +42,7 @@
         "Macros.cpp",
         "TouchVideoDevice.cpp",
         "controller/PeripheralController.cpp",
+        "mapper/CapturedTouchpadEventConverter.cpp",
         "mapper/CursorInputMapper.cpp",
         "mapper/ExternalStylusInputMapper.cpp",
         "mapper/InputMapper.cpp",
diff --git a/services/inputflinger/reader/mapper/CapturedTouchpadEventConverter.cpp b/services/inputflinger/reader/mapper/CapturedTouchpadEventConverter.cpp
new file mode 100644
index 0000000..023372a
--- /dev/null
+++ b/services/inputflinger/reader/mapper/CapturedTouchpadEventConverter.cpp
@@ -0,0 +1,302 @@
+/*
+ * Copyright 2023 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 "CapturedTouchpadEventConverter.h"
+
+#include <sstream>
+
+#include <android-base/stringprintf.h>
+#include <gui/constants.h>
+#include <input/PrintTools.h>
+#include <linux/input-event-codes.h>
+#include <log/log_main.h>
+
+namespace android {
+
+namespace {
+
+int32_t actionWithIndex(int32_t action, int32_t index) {
+    return action | (index << AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT);
+}
+
+template <typename T>
+size_t firstUnmarkedBit(T set) {
+    // TODO: replace with std::countr_one from <bit> when that's available
+    LOG_ALWAYS_FATAL_IF(set.all());
+    size_t i = 0;
+    while (set.test(i)) {
+        i++;
+    }
+    return i;
+}
+
+} // namespace
+
+CapturedTouchpadEventConverter::CapturedTouchpadEventConverter(
+        InputReaderContext& readerContext, const InputDeviceContext& deviceContext,
+        MultiTouchMotionAccumulator& motionAccumulator, int32_t deviceId)
+      : mDeviceId(deviceId),
+        mReaderContext(readerContext),
+        mDeviceContext(deviceContext),
+        mMotionAccumulator(motionAccumulator),
+        mHasTouchMinor(deviceContext.hasAbsoluteAxis(ABS_MT_TOUCH_MINOR)),
+        mHasToolMinor(deviceContext.hasAbsoluteAxis(ABS_MT_WIDTH_MINOR)) {
+    RawAbsoluteAxisInfo orientationInfo;
+    deviceContext.getAbsoluteAxisInfo(ABS_MT_ORIENTATION, &orientationInfo);
+    if (orientationInfo.valid) {
+        if (orientationInfo.maxValue > 0) {
+            mOrientationScale = M_PI_2 / orientationInfo.maxValue;
+        } else if (orientationInfo.minValue < 0) {
+            mOrientationScale = -M_PI_2 / orientationInfo.minValue;
+        }
+    }
+
+    // TODO(b/275369880): support touch.pressure.calibration and .scale properties when captured.
+    RawAbsoluteAxisInfo pressureInfo;
+    deviceContext.getAbsoluteAxisInfo(ABS_MT_PRESSURE, &pressureInfo);
+    if (pressureInfo.valid && pressureInfo.maxValue > 0) {
+        mPressureScale = 1.0 / pressureInfo.maxValue;
+    }
+
+    RawAbsoluteAxisInfo touchMajorInfo, toolMajorInfo;
+    deviceContext.getAbsoluteAxisInfo(ABS_MT_TOUCH_MAJOR, &touchMajorInfo);
+    deviceContext.getAbsoluteAxisInfo(ABS_MT_WIDTH_MAJOR, &toolMajorInfo);
+    mHasTouchMajor = touchMajorInfo.valid;
+    mHasToolMajor = toolMajorInfo.valid;
+    if (mHasTouchMajor && touchMajorInfo.maxValue != 0) {
+        mSizeScale = 1.0f / touchMajorInfo.maxValue;
+    } else if (mHasToolMajor && toolMajorInfo.maxValue != 0) {
+        mSizeScale = 1.0f / toolMajorInfo.maxValue;
+    }
+}
+
+std::string CapturedTouchpadEventConverter::dump() const {
+    std::stringstream out;
+    out << "Orientation scale: " << mOrientationScale << "\n";
+    out << "Pressure scale: " << mPressureScale << "\n";
+    out << "Size scale: " << mSizeScale << "\n";
+
+    out << "Dimension axes:";
+    if (mHasTouchMajor) out << " touch major";
+    if (mHasTouchMinor) out << ", touch minor";
+    if (mHasToolMajor) out << ", tool major";
+    if (mHasToolMinor) out << ", tool minor";
+    out << "\n";
+
+    out << "Down time: " << mDownTime << "\n";
+    out << StringPrintf("Button state: 0x%08x\n", mButtonState);
+
+    out << StringPrintf("Pointer IDs in use: %s\n", mPointerIdsInUse.to_string().c_str());
+
+    out << "Pointer IDs for slot numbers:\n";
+    out << addLinePrefix(dumpMap(mPointerIdForSlotNumber), "  ") << "\n";
+    return out.str();
+}
+
+void CapturedTouchpadEventConverter::populateMotionRanges(InputDeviceInfo& info) const {
+    tryAddRawMotionRange(/*byref*/ info, AMOTION_EVENT_AXIS_X, ABS_MT_POSITION_X);
+    tryAddRawMotionRange(/*byref*/ info, AMOTION_EVENT_AXIS_Y, ABS_MT_POSITION_Y);
+    tryAddRawMotionRange(/*byref*/ info, AMOTION_EVENT_AXIS_TOUCH_MAJOR, ABS_MT_TOUCH_MAJOR);
+    tryAddRawMotionRange(/*byref*/ info, AMOTION_EVENT_AXIS_TOUCH_MINOR, ABS_MT_TOUCH_MINOR);
+    tryAddRawMotionRange(/*byref*/ info, AMOTION_EVENT_AXIS_TOOL_MAJOR, ABS_MT_WIDTH_MAJOR);
+    tryAddRawMotionRange(/*byref*/ info, AMOTION_EVENT_AXIS_TOOL_MINOR, ABS_MT_WIDTH_MINOR);
+
+    RawAbsoluteAxisInfo pressureInfo;
+    mDeviceContext.getAbsoluteAxisInfo(ABS_MT_PRESSURE, &pressureInfo);
+    if (pressureInfo.valid) {
+        info.addMotionRange(AMOTION_EVENT_AXIS_PRESSURE, SOURCE, 0, 1, 0, 0, 0);
+    }
+
+    RawAbsoluteAxisInfo orientationInfo;
+    mDeviceContext.getAbsoluteAxisInfo(ABS_MT_ORIENTATION, &orientationInfo);
+    if (orientationInfo.valid && (orientationInfo.maxValue > 0 || orientationInfo.minValue < 0)) {
+        info.addMotionRange(AMOTION_EVENT_AXIS_ORIENTATION, SOURCE, -M_PI_2, M_PI_2, 0, 0, 0);
+    }
+
+    if (mHasTouchMajor || mHasToolMajor) {
+        info.addMotionRange(AMOTION_EVENT_AXIS_SIZE, SOURCE, 0, 1, 0, 0, 0);
+    }
+}
+
+void CapturedTouchpadEventConverter::tryAddRawMotionRange(InputDeviceInfo& deviceInfo,
+                                                          int32_t androidAxis,
+                                                          int32_t evdevAxis) const {
+    RawAbsoluteAxisInfo info;
+    mDeviceContext.getAbsoluteAxisInfo(evdevAxis, &info);
+    if (info.valid) {
+        deviceInfo.addMotionRange(androidAxis, SOURCE, info.minValue, info.maxValue, info.flat,
+                                  info.fuzz, info.resolution);
+    }
+}
+
+void CapturedTouchpadEventConverter::reset() {
+    mCursorButtonAccumulator.reset(mDeviceContext);
+    mDownTime = 0;
+    mPointerIdsInUse.reset();
+    mPointerIdForSlotNumber.clear();
+}
+
+std::list<NotifyArgs> CapturedTouchpadEventConverter::process(const RawEvent& rawEvent) {
+    std::list<NotifyArgs> out;
+    if (rawEvent.type == EV_SYN && rawEvent.code == SYN_REPORT) {
+        out = sync(rawEvent.when, rawEvent.readTime);
+        mMotionAccumulator.finishSync();
+    }
+
+    mCursorButtonAccumulator.process(&rawEvent);
+    mMotionAccumulator.process(&rawEvent);
+    return out;
+}
+
+std::list<NotifyArgs> CapturedTouchpadEventConverter::sync(nsecs_t when, nsecs_t readTime) {
+    // TODO(b/259547750): filter out touches marked as palms (using MT_TOOL_PALM).
+    std::list<NotifyArgs> out;
+    std::vector<PointerCoords> coords;
+    std::vector<PointerProperties> properties;
+    std::map<size_t, size_t> coordsIndexForSlotNumber;
+
+    // For all the touches that were already down, send a MOVE event with their updated coordinates.
+    // A convention of the MotionEvent API is that pointer coordinates in UP events match the
+    // pointer's coordinates from the previous MOVE, so we still include touches here even if
+    // they've been lifted in this evdev frame.
+    if (!mPointerIdForSlotNumber.empty()) {
+        for (const auto [slotNumber, pointerId] : mPointerIdForSlotNumber) {
+            // Note that we don't check whether the touch has actually moved — it's rare for a touch
+            // to stay perfectly still between frames, and if it does the worst that can happen is
+            // an extra MOVE event, so it's not worth the overhead of checking for changes.
+            coordsIndexForSlotNumber[slotNumber] = coords.size();
+            coords.push_back(makePointerCoordsForSlot(mMotionAccumulator.getSlot(slotNumber)));
+            properties.push_back({.id = pointerId, .toolType = ToolType::FINGER});
+        }
+        out.push_back(
+                makeMotionArgs(when, readTime, AMOTION_EVENT_ACTION_MOVE, coords, properties));
+    }
+
+    std::vector<size_t> upSlots, downSlots;
+    for (size_t i = 0; i < mMotionAccumulator.getSlotCount(); i++) {
+        const bool isInUse = mMotionAccumulator.getSlot(i).isInUse();
+        const bool wasInUse = mPointerIdForSlotNumber.find(i) != mPointerIdForSlotNumber.end();
+        if (isInUse && !wasInUse) {
+            downSlots.push_back(i);
+        } else if (!isInUse && wasInUse) {
+            upSlots.push_back(i);
+        }
+    }
+
+    // For any touches that were lifted, send UP or POINTER_UP events.
+    for (size_t slotNumber : upSlots) {
+        const size_t indexToRemove = coordsIndexForSlotNumber.at(slotNumber);
+        const int32_t action = coords.size() == 1
+                ? AMOTION_EVENT_ACTION_UP
+                : actionWithIndex(AMOTION_EVENT_ACTION_POINTER_UP, indexToRemove);
+        out.push_back(makeMotionArgs(when, readTime, action, coords, properties));
+
+        freePointerIdForSlot(slotNumber);
+        coords.erase(coords.begin() + indexToRemove);
+        properties.erase(properties.begin() + indexToRemove);
+        // Now that we've removed some coords and properties, we might have to update the slot
+        // number to coords index mapping.
+        coordsIndexForSlotNumber.erase(slotNumber);
+        for (auto& [_, index] : coordsIndexForSlotNumber) {
+            if (index > indexToRemove) {
+                index--;
+            }
+        }
+    }
+
+    // For new touches, send DOWN or POINTER_DOWN events.
+    for (size_t slotNumber : downSlots) {
+        const size_t coordsIndex = coords.size();
+        const int32_t action = coords.empty()
+                ? AMOTION_EVENT_ACTION_DOWN
+                : actionWithIndex(AMOTION_EVENT_ACTION_POINTER_DOWN, coordsIndex);
+
+        coordsIndexForSlotNumber[slotNumber] = coordsIndex;
+        coords.push_back(makePointerCoordsForSlot(mMotionAccumulator.getSlot(slotNumber)));
+        properties.push_back(
+                {.id = allocatePointerIdToSlot(slotNumber), .toolType = ToolType::FINGER});
+
+        out.push_back(makeMotionArgs(when, readTime, action, coords, properties));
+    }
+
+    const uint32_t newButtonState = mCursorButtonAccumulator.getButtonState();
+    for (uint32_t button = 1; button <= AMOTION_EVENT_BUTTON_FORWARD; button <<= 1) {
+        if (newButtonState & button && !(mButtonState & button)) {
+            mButtonState |= button;
+            out.push_back(makeMotionArgs(when, readTime, AMOTION_EVENT_ACTION_BUTTON_PRESS, coords,
+                                         properties, /*actionButton=*/button));
+        } else if (!(newButtonState & button) && mButtonState & button) {
+            mButtonState &= ~button;
+            out.push_back(makeMotionArgs(when, readTime, AMOTION_EVENT_ACTION_BUTTON_RELEASE,
+                                         coords, properties, /*actionButton=*/button));
+        }
+    }
+    return out;
+}
+
+NotifyMotionArgs CapturedTouchpadEventConverter::makeMotionArgs(
+        nsecs_t when, nsecs_t readTime, int32_t action, const std::vector<PointerCoords>& coords,
+        const std::vector<PointerProperties>& properties, int32_t actionButton) {
+    LOG_ALWAYS_FATAL_IF(coords.size() != properties.size(),
+                        "Mismatched coords and properties arrays.");
+    return NotifyMotionArgs(mReaderContext.getNextId(), when, readTime, mDeviceId, SOURCE,
+                            ADISPLAY_ID_NONE, /*policyFlags=*/POLICY_FLAG_WAKE, action,
+                            /*actionButton=*/actionButton, /*flags=*/0,
+                            mReaderContext.getGlobalMetaState(), mButtonState,
+                            MotionClassification::NONE, AMOTION_EVENT_EDGE_FLAG_NONE, coords.size(),
+                            properties.data(), coords.data(), /*xPrecision=*/1.0f,
+                            /*yPrecision=*/1.0f, AMOTION_EVENT_INVALID_CURSOR_POSITION,
+                            AMOTION_EVENT_INVALID_CURSOR_POSITION, mDownTime, /*videoFrames=*/{});
+}
+
+PointerCoords CapturedTouchpadEventConverter::makePointerCoordsForSlot(
+        const MultiTouchMotionAccumulator::Slot& slot) const {
+    PointerCoords coords;
+    coords.clear();
+    coords.setAxisValue(AMOTION_EVENT_AXIS_X, slot.getX());
+    coords.setAxisValue(AMOTION_EVENT_AXIS_Y, slot.getY());
+    coords.setAxisValue(AMOTION_EVENT_AXIS_TOUCH_MAJOR, slot.getTouchMajor());
+    coords.setAxisValue(AMOTION_EVENT_AXIS_TOUCH_MINOR, slot.getTouchMinor());
+    coords.setAxisValue(AMOTION_EVENT_AXIS_TOOL_MAJOR, slot.getToolMajor());
+    coords.setAxisValue(AMOTION_EVENT_AXIS_TOOL_MINOR, slot.getToolMinor());
+    coords.setAxisValue(AMOTION_EVENT_AXIS_ORIENTATION, slot.getOrientation() * mOrientationScale);
+    coords.setAxisValue(AMOTION_EVENT_AXIS_PRESSURE, slot.getPressure() * mPressureScale);
+    float size = 0;
+    // TODO(b/275369880): support touch.size.calibration and .isSummed properties when captured.
+    if (mHasTouchMajor) {
+        size = mHasTouchMinor ? (slot.getTouchMajor() + slot.getTouchMinor()) / 2
+                              : slot.getTouchMajor();
+    } else if (mHasToolMajor) {
+        size = mHasToolMinor ? (slot.getToolMajor() + slot.getToolMinor()) / 2
+                             : slot.getToolMajor();
+    }
+    coords.setAxisValue(AMOTION_EVENT_AXIS_SIZE, size * mSizeScale);
+    return coords;
+}
+
+int32_t CapturedTouchpadEventConverter::allocatePointerIdToSlot(size_t slotNumber) {
+    const int32_t pointerId = firstUnmarkedBit(mPointerIdsInUse);
+    mPointerIdsInUse.set(pointerId);
+    mPointerIdForSlotNumber[slotNumber] = pointerId;
+    return pointerId;
+}
+
+void CapturedTouchpadEventConverter::freePointerIdForSlot(size_t slotNumber) {
+    mPointerIdsInUse.reset(mPointerIdForSlotNumber.at(slotNumber));
+    mPointerIdForSlotNumber.erase(slotNumber);
+}
+
+} // namespace android
diff --git a/services/inputflinger/reader/mapper/CapturedTouchpadEventConverter.h b/services/inputflinger/reader/mapper/CapturedTouchpadEventConverter.h
new file mode 100644
index 0000000..d81692d
--- /dev/null
+++ b/services/inputflinger/reader/mapper/CapturedTouchpadEventConverter.h
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2023 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.
+ */
+
+#pragma once
+
+#include <bitset>
+#include <list>
+#include <map>
+#include <set>
+#include <string>
+#include <vector>
+
+#include <android/input.h>
+#include <input/Input.h>
+#include <utils/Timers.h>
+
+#include "EventHub.h"
+#include "InputDevice.h"
+#include "accumulator/CursorButtonAccumulator.h"
+#include "accumulator/MultiTouchMotionAccumulator.h"
+#include "accumulator/TouchButtonAccumulator.h"
+
+namespace android {
+
+class CapturedTouchpadEventConverter {
+public:
+    explicit CapturedTouchpadEventConverter(InputReaderContext& readerContext,
+                                            const InputDeviceContext& deviceContext,
+                                            MultiTouchMotionAccumulator& motionAccumulator,
+                                            int32_t deviceId);
+    std::string dump() const;
+    void populateMotionRanges(InputDeviceInfo& info) const;
+    void reset();
+    [[nodiscard]] std::list<NotifyArgs> process(const RawEvent& rawEvent);
+
+private:
+    void tryAddRawMotionRange(InputDeviceInfo& deviceInfo, int32_t androidAxis,
+                              int32_t evdevAxis) const;
+    [[nodiscard]] std::list<NotifyArgs> sync(nsecs_t when, nsecs_t readTime);
+    [[nodiscard]] NotifyMotionArgs makeMotionArgs(nsecs_t when, nsecs_t readTime, int32_t action,
+                                                  const std::vector<PointerCoords>& coords,
+                                                  const std::vector<PointerProperties>& properties,
+                                                  int32_t actionButton = 0);
+    PointerCoords makePointerCoordsForSlot(const MultiTouchMotionAccumulator::Slot& slot) const;
+    int32_t allocatePointerIdToSlot(size_t slotNumber);
+    void freePointerIdForSlot(size_t slotNumber);
+
+    const int32_t mDeviceId;
+    InputReaderContext& mReaderContext;
+    const InputDeviceContext& mDeviceContext;
+    CursorButtonAccumulator mCursorButtonAccumulator;
+    MultiTouchMotionAccumulator& mMotionAccumulator;
+
+    float mOrientationScale = 0;
+    float mPressureScale = 1;
+    float mSizeScale = 0;
+    bool mHasTouchMajor;
+    const bool mHasTouchMinor;
+    bool mHasToolMajor;
+    const bool mHasToolMinor;
+    nsecs_t mDownTime = 0;
+    uint32_t mButtonState = 0;
+
+    std::bitset<MAX_POINTER_ID + 1> mPointerIdsInUse;
+    std::map<size_t, int32_t> mPointerIdForSlotNumber;
+
+    static constexpr uint32_t SOURCE = AINPUT_SOURCE_TOUCHPAD;
+};
+
+} // namespace android
diff --git a/services/inputflinger/reader/mapper/TouchpadInputMapper.cpp b/services/inputflinger/reader/mapper/TouchpadInputMapper.cpp
index 8753b48..a5da3cd 100644
--- a/services/inputflinger/reader/mapper/TouchpadInputMapper.cpp
+++ b/services/inputflinger/reader/mapper/TouchpadInputMapper.cpp
@@ -16,9 +16,11 @@
 
 #include "../Macros.h"
 
+#include <chrono>
 #include <limits>
 #include <optional>
 
+#include <android-base/stringprintf.h>
 #include <android/input.h>
 #include <ftl/enum.h>
 #include <input/PrintTools.h>
@@ -174,8 +176,18 @@
       : InputMapper(deviceContext, readerConfig),
         mGestureInterpreter(NewGestureInterpreter(), DeleteGestureInterpreter),
         mPointerController(getContext()->getPointerController(getDeviceId())),
-        mStateConverter(deviceContext),
-        mGestureConverter(*getContext(), deviceContext, getDeviceId()) {
+        mStateConverter(deviceContext, mMotionAccumulator),
+        mGestureConverter(*getContext(), deviceContext, getDeviceId()),
+        mCapturedEventConverter(*getContext(), deviceContext, mMotionAccumulator, getDeviceId()) {
+    RawAbsoluteAxisInfo slotAxisInfo;
+    deviceContext.getAbsoluteAxisInfo(ABS_MT_SLOT, &slotAxisInfo);
+    if (!slotAxisInfo.valid || slotAxisInfo.maxValue <= 0) {
+        ALOGW("Touchpad \"%s\" doesn't have a valid ABS_MT_SLOT axis, and probably won't work "
+              "properly.",
+              deviceContext.getName().c_str());
+    }
+    mMotionAccumulator.configure(deviceContext, slotAxisInfo.maxValue + 1, true);
+
     mGestureInterpreter->Initialize(GESTURES_DEVCLASS_TOUCHPAD);
     mGestureInterpreter->SetHardwareProperties(createHardwareProperties(deviceContext));
     // Even though we don't explicitly delete copy/move semantics, it's safe to
@@ -209,15 +221,28 @@
 
 void TouchpadInputMapper::populateDeviceInfo(InputDeviceInfo& info) {
     InputMapper::populateDeviceInfo(info);
-    mGestureConverter.populateMotionRanges(info);
+    if (mPointerCaptured) {
+        mCapturedEventConverter.populateMotionRanges(info);
+    } else {
+        mGestureConverter.populateMotionRanges(info);
+    }
 }
 
 void TouchpadInputMapper::dump(std::string& dump) {
     dump += INDENT2 "Touchpad Input Mapper:\n";
+    if (mProcessing) {
+        dump += INDENT3 "Currently processing a hardware state\n";
+    }
+    if (mResettingInterpreter) {
+        dump += INDENT3 "Currently resetting gesture interpreter\n";
+    }
+    dump += StringPrintf(INDENT3 "Pointer captured: %s\n", toString(mPointerCaptured));
     dump += INDENT3 "Gesture converter:\n";
     dump += addLinePrefix(mGestureConverter.dump(), INDENT4);
     dump += INDENT3 "Gesture properties:\n";
     dump += addLinePrefix(mPropertyProvider.dump(), INDENT4);
+    dump += INDENT3 "Captured event converter:\n";
+    dump += addLinePrefix(mCapturedEventConverter.dump(), INDENT4);
 }
 
 std::list<NotifyArgs> TouchpadInputMapper::reconfigure(nsecs_t when,
@@ -252,17 +277,50 @@
         mPropertyProvider.getProperty("Button Right Click Zone Enable")
                 .setBoolValues({config.touchpadRightClickZoneEnabled});
     }
-    return {};
+    std::list<NotifyArgs> out;
+    if ((!changes.any() && config.pointerCaptureRequest.enable) ||
+        changes.test(InputReaderConfiguration::Change::POINTER_CAPTURE)) {
+        mPointerCaptured = config.pointerCaptureRequest.enable;
+        // The motion ranges are going to change, so bump the generation to clear the cached ones.
+        bumpGeneration();
+        if (mPointerCaptured) {
+            // The touchpad is being captured, so we need to tidy up any fake fingers etc. that are
+            // still being reported for a gesture in progress.
+            out += reset(when);
+            mPointerController->fade(PointerControllerInterface::Transition::IMMEDIATE);
+        } else {
+            // We're transitioning from captured to uncaptured.
+            mCapturedEventConverter.reset();
+        }
+        if (changes.any()) {
+            out.push_back(NotifyDeviceResetArgs(getContext()->getNextId(), when, getDeviceId()));
+        }
+    }
+    return out;
 }
 
 std::list<NotifyArgs> TouchpadInputMapper::reset(nsecs_t when) {
     mStateConverter.reset();
+    resetGestureInterpreter(when);
     std::list<NotifyArgs> out = mGestureConverter.reset(when);
     out += InputMapper::reset(when);
     return out;
 }
 
+void TouchpadInputMapper::resetGestureInterpreter(nsecs_t when) {
+    // The GestureInterpreter has no official reset method, but sending a HardwareState with no
+    // fingers down or buttons pressed should get it into a clean state.
+    HardwareState state;
+    state.timestamp = std::chrono::duration<stime_t>(std::chrono::nanoseconds(when)).count();
+    mResettingInterpreter = true;
+    mGestureInterpreter->PushHardwareState(&state);
+    mResettingInterpreter = false;
+}
+
 std::list<NotifyArgs> TouchpadInputMapper::process(const RawEvent* rawEvent) {
+    if (mPointerCaptured) {
+        return mCapturedEventConverter.process(*rawEvent);
+    }
     std::optional<SelfContainedHardwareState> state = mStateConverter.processRawEvent(rawEvent);
     if (state) {
         return sendHardwareState(rawEvent->when, rawEvent->readTime, *state);
@@ -283,6 +341,11 @@
 
 void TouchpadInputMapper::consumeGesture(const Gesture* gesture) {
     ALOGD_IF(DEBUG_TOUCHPAD_GESTURES, "Gesture ready: %s", gesture->String().c_str());
+    if (mResettingInterpreter) {
+        // We already handle tidying up fake fingers etc. in GestureConverter::reset, so we should
+        // ignore any gestures produced from the interpreter while we're resetting it.
+        return;
+    }
     if (!mProcessing) {
         ALOGE("Received gesture outside of the normal processing flow; ignoring it.");
         return;
diff --git a/services/inputflinger/reader/mapper/TouchpadInputMapper.h b/services/inputflinger/reader/mapper/TouchpadInputMapper.h
index 268b275..3128d18 100644
--- a/services/inputflinger/reader/mapper/TouchpadInputMapper.h
+++ b/services/inputflinger/reader/mapper/TouchpadInputMapper.h
@@ -21,12 +21,15 @@
 #include <vector>
 
 #include <PointerControllerInterface.h>
+#include <utils/Timers.h>
 
+#include "CapturedTouchpadEventConverter.h"
 #include "EventHub.h"
 #include "InputDevice.h"
 #include "InputMapper.h"
 #include "InputReaderBase.h"
 #include "NotifyArgs.h"
+#include "accumulator/MultiTouchMotionAccumulator.h"
 #include "gestures/GestureConverter.h"
 #include "gestures/HardwareStateConverter.h"
 #include "gestures/PropertyProvider.h"
@@ -54,6 +57,7 @@
     void consumeGesture(const Gesture* gesture);
 
 private:
+    void resetGestureInterpreter(nsecs_t when);
     [[nodiscard]] std::list<NotifyArgs> sendHardwareState(nsecs_t when, nsecs_t readTime,
                                                           SelfContainedHardwareState schs);
     [[nodiscard]] std::list<NotifyArgs> processGestures(nsecs_t when, nsecs_t readTime);
@@ -64,10 +68,19 @@
 
     PropertyProvider mPropertyProvider;
 
+    // The MultiTouchMotionAccumulator is shared between the HardwareStateConverter and
+    // CapturedTouchpadEventConverter, so that if the touchpad is captured or released while touches
+    // are down, the relevant converter can still benefit from the current axis values stored in the
+    // accumulator.
+    MultiTouchMotionAccumulator mMotionAccumulator;
+
     HardwareStateConverter mStateConverter;
     GestureConverter mGestureConverter;
+    CapturedTouchpadEventConverter mCapturedEventConverter;
 
+    bool mPointerCaptured = false;
     bool mProcessing = false;
+    bool mResettingInterpreter = false;
     std::vector<Gesture> mGesturesToProcess;
 };
 
diff --git a/services/inputflinger/reader/mapper/gestures/HardwareStateConverter.cpp b/services/inputflinger/reader/mapper/gestures/HardwareStateConverter.cpp
index 8841b6e..6780dce 100644
--- a/services/inputflinger/reader/mapper/gestures/HardwareStateConverter.cpp
+++ b/services/inputflinger/reader/mapper/gestures/HardwareStateConverter.cpp
@@ -26,16 +26,11 @@
 
 namespace android {
 
-HardwareStateConverter::HardwareStateConverter(const InputDeviceContext& deviceContext)
-      : mDeviceContext(deviceContext), mTouchButtonAccumulator(deviceContext) {
-    RawAbsoluteAxisInfo slotAxisInfo;
-    deviceContext.getAbsoluteAxisInfo(ABS_MT_SLOT, &slotAxisInfo);
-    if (!slotAxisInfo.valid || slotAxisInfo.maxValue <= 0) {
-        ALOGW("Touchpad \"%s\" doesn't have a valid ABS_MT_SLOT axis, and probably won't work "
-              "properly.",
-              deviceContext.getName().c_str());
-    }
-    mMotionAccumulator.configure(deviceContext, slotAxisInfo.maxValue + 1, true);
+HardwareStateConverter::HardwareStateConverter(const InputDeviceContext& deviceContext,
+                                               MultiTouchMotionAccumulator& motionAccumulator)
+      : mDeviceContext(deviceContext),
+        mMotionAccumulator(motionAccumulator),
+        mTouchButtonAccumulator(deviceContext) {
     mTouchButtonAccumulator.configure();
 }
 
diff --git a/services/inputflinger/reader/mapper/gestures/HardwareStateConverter.h b/services/inputflinger/reader/mapper/gestures/HardwareStateConverter.h
index c314b0d..633448e 100644
--- a/services/inputflinger/reader/mapper/gestures/HardwareStateConverter.h
+++ b/services/inputflinger/reader/mapper/gestures/HardwareStateConverter.h
@@ -41,7 +41,8 @@
 // Converts RawEvents into the HardwareState structs used by the gestures library.
 class HardwareStateConverter {
 public:
-    HardwareStateConverter(const InputDeviceContext& deviceContext);
+    HardwareStateConverter(const InputDeviceContext& deviceContext,
+                           MultiTouchMotionAccumulator& motionAccumulator);
 
     std::optional<SelfContainedHardwareState> processRawEvent(const RawEvent* event);
     void reset();
@@ -51,7 +52,7 @@
 
     const InputDeviceContext& mDeviceContext;
     CursorButtonAccumulator mCursorButtonAccumulator;
-    MultiTouchMotionAccumulator mMotionAccumulator;
+    MultiTouchMotionAccumulator& mMotionAccumulator;
     TouchButtonAccumulator mTouchButtonAccumulator;
     int32_t mMscTimestamp = 0;
 };