| /* | |
| * 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 <optional> | |
| #include <sstream> | |
| #include <android-base/stringprintf.h> | |
| #include <com_android_input_flags.h> | |
| #include <input/PrintTools.h> | |
| #include <linux/input-event-codes.h> | |
| #include <log/log_main.h> | |
| namespace input_flags = com::android::input::flags; | |
| namespace android { | |
| namespace { | |
| static constexpr uint32_t SOURCE = AINPUT_SOURCE_TOUCHPAD; | |
| 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; | |
| } | |
| void addRawMotionRange(InputDeviceInfo& deviceInfo, int32_t androidAxis, | |
| RawAbsoluteAxisInfo& evdevAxis) { | |
| deviceInfo.addMotionRange(androidAxis, SOURCE, evdevAxis.minValue, evdevAxis.maxValue, | |
| evdevAxis.flat, evdevAxis.fuzz, evdevAxis.resolution); | |
| } | |
| } // 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)) { | |
| if (std::optional<RawAbsoluteAxisInfo> orientation = | |
| deviceContext.getAbsoluteAxisInfo(ABS_MT_ORIENTATION); | |
| orientation) { | |
| if (orientation->maxValue > 0) { | |
| mOrientationScale = M_PI_2 / orientation->maxValue; | |
| } else if (orientation->minValue < 0) { | |
| mOrientationScale = -M_PI_2 / orientation->minValue; | |
| } | |
| } | |
| // TODO(b/275369880): support touch.pressure.calibration and .scale properties when captured. | |
| if (std::optional<RawAbsoluteAxisInfo> pressure = | |
| deviceContext.getAbsoluteAxisInfo(ABS_MT_PRESSURE); | |
| pressure && pressure->maxValue > 0) { | |
| mPressureScale = 1.0 / pressure->maxValue; | |
| } | |
| std::optional<RawAbsoluteAxisInfo> touchMajor = | |
| deviceContext.getAbsoluteAxisInfo(ABS_MT_TOUCH_MAJOR); | |
| std::optional<RawAbsoluteAxisInfo> toolMajor = | |
| deviceContext.getAbsoluteAxisInfo(ABS_MT_WIDTH_MAJOR); | |
| mHasTouchMajor = touchMajor.has_value(); | |
| mHasToolMajor = toolMajor.has_value(); | |
| if (mHasTouchMajor && touchMajor->maxValue != 0) { | |
| mSizeScale = 1.0f / touchMajor->maxValue; | |
| } else if (mHasToolMajor && toolMajor->maxValue != 0) { | |
| mSizeScale = 1.0f / toolMajor->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 { | |
| if (input_flags::include_relative_axis_values_for_captured_touchpads()) { | |
| tryAddRawMotionRangeWithRelative(/*byref*/ info, AMOTION_EVENT_AXIS_X, | |
| AMOTION_EVENT_AXIS_RELATIVE_X, ABS_MT_POSITION_X); | |
| tryAddRawMotionRangeWithRelative(/*byref*/ info, AMOTION_EVENT_AXIS_Y, | |
| AMOTION_EVENT_AXIS_RELATIVE_Y, ABS_MT_POSITION_Y); | |
| } else { | |
| 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); | |
| if (mDeviceContext.hasAbsoluteAxis(ABS_MT_PRESSURE)) { | |
| info.addMotionRange(AMOTION_EVENT_AXIS_PRESSURE, SOURCE, 0, 1, 0, 0, 0); | |
| } | |
| if (std::optional<RawAbsoluteAxisInfo> orientation = | |
| mDeviceContext.getAbsoluteAxisInfo(ABS_MT_ORIENTATION); | |
| orientation && (orientation->maxValue > 0 || orientation->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 { | |
| std::optional<RawAbsoluteAxisInfo> info = mDeviceContext.getAbsoluteAxisInfo(evdevAxis); | |
| if (info) { | |
| addRawMotionRange(/*byref*/ deviceInfo, androidAxis, *info); | |
| } | |
| } | |
| void CapturedTouchpadEventConverter::tryAddRawMotionRangeWithRelative(InputDeviceInfo& deviceInfo, | |
| int32_t androidAxis, | |
| int32_t androidRelativeAxis, | |
| int32_t evdevAxis) const { | |
| std::optional<RawAbsoluteAxisInfo> axisInfo = mDeviceContext.getAbsoluteAxisInfo(evdevAxis); | |
| if (axisInfo) { | |
| addRawMotionRange(/*byref*/ deviceInfo, androidAxis, *axisInfo); | |
| // The largest movement we could possibly report on a relative axis is from the minimum to | |
| // the maximum (or vice versa) of the absolute axis. | |
| float range = axisInfo->maxValue - axisInfo->minValue; | |
| deviceInfo.addMotionRange(androidRelativeAxis, SOURCE, -range, range, axisInfo->flat, | |
| axisInfo->fuzz, axisInfo->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) { | |
| std::list<NotifyArgs> out; | |
| std::vector<PointerCoords> coords; | |
| std::vector<PointerProperties> properties; | |
| std::map<size_t /*slotNumber*/, size_t /*coordsIndex*/> 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(slotNumber)); | |
| properties.push_back({.id = pointerId, .toolType = ToolType::FINGER}); | |
| } | |
| out.push_back( | |
| makeMotionArgs(when, readTime, AMOTION_EVENT_ACTION_MOVE, coords, properties)); | |
| if (input_flags::include_relative_axis_values_for_captured_touchpads()) { | |
| // For any further events we send from this sync, the pointers won't have moved relative | |
| // to the positions we just reported in this MOVE event, so zero out the relative axes. | |
| for (PointerCoords& pointer : coords) { | |
| pointer.setAxisValue(AMOTION_EVENT_AXIS_RELATIVE_X, 0); | |
| pointer.setAxisValue(AMOTION_EVENT_AXIS_RELATIVE_Y, 0); | |
| } | |
| } | |
| } | |
| std::vector<size_t> upSlots, downSlots; | |
| for (size_t i = 0; i < mMotionAccumulator.getSlotCount(); i++) { | |
| const MultiTouchMotionAccumulator::Slot& slot = mMotionAccumulator.getSlot(i); | |
| // Some touchpads continue to report contacts even after they've identified them as palms. | |
| // We don't currently have a way to mark these as palms when reporting to apps, so don't | |
| // report them at all. | |
| const bool isInUse = slot.isInUse() && slot.getToolType() != ToolType::PALM; | |
| const bool wasInUse = mPointerIdForSlotNumber.find(i) != mPointerIdForSlotNumber.end(); | |
| if (isInUse && !wasInUse) { | |
| downSlots.push_back(i); | |
| } else if (!isInUse && wasInUse) { | |
| upSlots.push_back(i); | |
| } | |
| } | |
| // Send BUTTON_RELEASE events. (This has to happen before any UP events to avoid sending | |
| // BUTTON_RELEASE events without any pointers.) | |
| uint32_t newButtonState; | |
| if (coords.size() - upSlots.size() + downSlots.size() == 0) { | |
| // If there won't be any pointers down after this evdev sync, we won't be able to send | |
| // button updates on their own, as motion events without pointers are invalid. To avoid | |
| // erroneously reporting buttons being held for long periods, send BUTTON_RELEASE events for | |
| // all pressed buttons when the last pointer is lifted. | |
| // | |
| // This also prevents us from sending BUTTON_PRESS events too early in the case of touchpads | |
| // which report a button press one evdev sync before reporting a touch going down. | |
| newButtonState = 0; | |
| } else { | |
| 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_RELEASE, | |
| coords, properties, /*actionButton=*/button)); | |
| } | |
| } | |
| // 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 bool cancel = mMotionAccumulator.getSlot(slotNumber).getToolType() == ToolType::PALM; | |
| int32_t action; | |
| if (coords.size() == 1) { | |
| action = cancel ? AMOTION_EVENT_ACTION_CANCEL : AMOTION_EVENT_ACTION_UP; | |
| } else { | |
| action = actionWithIndex(AMOTION_EVENT_ACTION_POINTER_UP, indexToRemove); | |
| } | |
| out.push_back(makeMotionArgs(when, readTime, action, coords, properties, /*actionButton=*/0, | |
| /*flags=*/cancel ? AMOTION_EVENT_FLAG_CANCELED : 0)); | |
| freePointerIdForSlot(slotNumber); | |
| if (input_flags::include_relative_axis_values_for_captured_touchpads()) { | |
| mPreviousCoordsForSlotNumber.erase(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(slotNumber)); | |
| properties.push_back( | |
| {.id = allocatePointerIdToSlot(slotNumber), .toolType = ToolType::FINGER}); | |
| out.push_back(makeMotionArgs(when, readTime, action, coords, properties)); | |
| } | |
| 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)); | |
| } | |
| } | |
| 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, int32_t flags) { | |
| LOG_ALWAYS_FATAL_IF(coords.size() != properties.size(), | |
| "Mismatched coords and properties arrays."); | |
| return NotifyMotionArgs(mReaderContext.getNextId(), when, readTime, mDeviceId, SOURCE, | |
| ui::LogicalDisplayId::INVALID, /*policyFlags=*/POLICY_FLAG_WAKE, action, | |
| /*actionButton=*/actionButton, flags, | |
| 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(size_t slotNumber) { | |
| const MultiTouchMotionAccumulator::Slot& slot = mMotionAccumulator.getSlot(slotNumber); | |
| PointerCoords coords; | |
| coords.clear(); | |
| coords.setAxisValue(AMOTION_EVENT_AXIS_X, slot.getX()); | |
| coords.setAxisValue(AMOTION_EVENT_AXIS_Y, slot.getY()); | |
| if (input_flags::include_relative_axis_values_for_captured_touchpads()) { | |
| if (auto it = mPreviousCoordsForSlotNumber.find(slotNumber); | |
| it != mPreviousCoordsForSlotNumber.end()) { | |
| auto [oldX, oldY] = it->second; | |
| coords.setAxisValue(AMOTION_EVENT_AXIS_RELATIVE_X, slot.getX() - oldX); | |
| coords.setAxisValue(AMOTION_EVENT_AXIS_RELATIVE_Y, slot.getY() - oldY); | |
| } | |
| mPreviousCoordsForSlotNumber[slotNumber] = std::make_pair(slot.getX(), 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 |