From ba0a8758240241a852d7fd78603be5c10cb0f05c Mon Sep 17 00:00:00 2001 From: Siarhei Vishniakou Date: Tue, 14 Sep 2021 14:43:25 -0700 Subject: Add ChromeOS palm rejection model This model will be used to block palm presses. It takes in a stream of evdev events, and reports back the pointers which should be considered palm. Bug: 198472780 Test: atest libpalmrejection_test inputflinger_tests Test: "adb shell device_config put input_native_boot palm_rejection_enabled 0" and make sure that "adb shell dumpsys input" shows that there aren't any palm rejectors inside UnwantedInteractionBlocker Change-Id: If979d335af29cf5e93b26336fea56a3a895cc562 --- .../inputflinger/UnwantedInteractionBlocker.cpp | 687 +++++++++++++++++++++ 1 file changed, 687 insertions(+) create mode 100644 services/inputflinger/UnwantedInteractionBlocker.cpp (limited to 'services/inputflinger/UnwantedInteractionBlocker.cpp') diff --git a/services/inputflinger/UnwantedInteractionBlocker.cpp b/services/inputflinger/UnwantedInteractionBlocker.cpp new file mode 100644 index 0000000000..64dbb8ceb4 --- /dev/null +++ b/services/inputflinger/UnwantedInteractionBlocker.cpp @@ -0,0 +1,687 @@ +/* + * Copyright (C) 2022 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. + */ + +#define LOG_TAG "UnwantedInteractionBlocker" +#include "UnwantedInteractionBlocker.h" + +#include +#include +#include +#include +#include + +#include "ui/events/ozone/evdev/touch_filter/neural_stylus_palm_detection_filter.h" +#include "ui/events/ozone/evdev/touch_filter/palm_model/onedevice_train_palm_detection_filter_model.h" + +using android::base::StringPrintf; + +namespace android { + +// Category (=namespace) name for the input settings that are applied at boot time +static const char* INPUT_NATIVE_BOOT = "input_native_boot"; +/** + * Feature flag name. This flag determines whether palm rejection is enabled. To enable, specify + * 'true' (not case sensitive) or '1'. To disable, specify any other value. + */ +static const char* PALM_REJECTION_ENABLED = "palm_rejection_enabled"; + +static std::string toLower(std::string s) { + std::transform(s.begin(), s.end(), s.begin(), [](unsigned char c) { return std::tolower(c); }); + return s; +} + +static bool isFromTouchscreen(int32_t source) { + return isFromSource(source, AINPUT_SOURCE_TOUCHSCREEN); +} + +static ::base::TimeTicks toChromeTimestamp(nsecs_t eventTime) { + return ::base::TimeTicks::UnixEpoch() + + ::base::Milliseconds(static_cast(ns2ms(eventTime))); +} + +/** + * Return true if palm rejection is enabled via the server configurable flags. Return false + * otherwise. + */ +static bool isPalmRejectionEnabled() { + std::string value = toLower( + server_configurable_flags::GetServerConfigurableFlag(INPUT_NATIVE_BOOT, + PALM_REJECTION_ENABLED, "false")); + if (value == "true" || value == "1") { + return true; + } + return false; +} + +static int getLinuxToolType(int32_t toolType) { + switch (toolType) { + case AMOTION_EVENT_TOOL_TYPE_FINGER: + return MT_TOOL_FINGER; + case AMOTION_EVENT_TOOL_TYPE_STYLUS: + return MT_TOOL_PEN; + case AMOTION_EVENT_TOOL_TYPE_PALM: + return MT_TOOL_PALM; + } + ALOGW("Got tool type %" PRId32 ", converting to MT_TOOL_FINGER", toolType); + return MT_TOOL_FINGER; +} + +static std::string addPrefix(std::string str, const std::string& prefix) { + std::stringstream ss; + bool newLineStarted = true; + for (const auto& ch : str) { + if (newLineStarted) { + ss << prefix; + newLineStarted = false; + } + if (ch == '\n') { + newLineStarted = true; + } + ss << ch; + } + return ss.str(); +} + +template +static std::string dumpSet(const std::set& v) { + static_assert(std::is_integral::value, "Only integral types can be printed."); + std::string out; + for (const T& entry : v) { + out += out.empty() ? "{" : ", "; + out += android::base::StringPrintf("%i", entry); + } + return out.empty() ? "{}" : (out + "}"); +} + +template +static std::string dumpMap(const std::map& map) { + static_assert(std::is_integral::value, "Keys should have integral type to be printed."); + static_assert(std::is_integral::value, "Values should have integral type to be printed."); + std::string out; + for (const auto& [k, v] : map) { + if (!out.empty()) { + out += "\n"; + } + out += android::base::StringPrintf("%i : %i", static_cast(k), static_cast(v)); + } + return out; +} + +static std::string dumpDeviceInfo(const AndroidPalmFilterDeviceInfo& info) { + std::string out; + out += StringPrintf("max_x = %.2f\n", info.max_x); + out += StringPrintf("max_y = %.2f\n", info.max_y); + out += StringPrintf("x_res = %.2f\n", info.x_res); + out += StringPrintf("y_res = %.2f\n", info.y_res); + out += StringPrintf("major_radius_res = %.2f\n", info.major_radius_res); + out += StringPrintf("minor_radius_res = %.2f\n", info.minor_radius_res); + out += StringPrintf("minor_radius_supported = %s\n", + info.minor_radius_supported ? "true" : "false"); + out += StringPrintf("touch_major_res = %" PRId32 "\n", info.touch_major_res); + out += StringPrintf("touch_minor_res = %" PRId32 "\n", info.touch_minor_res); + return out; +} + +static int32_t getActionUpForPointerId(const NotifyMotionArgs& args, int32_t pointerId) { + for (size_t i = 0; i < args.pointerCount; i++) { + if (pointerId == args.pointerProperties[i].id) { + return AMOTION_EVENT_ACTION_POINTER_UP | + (i << AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT); + } + } + LOG_ALWAYS_FATAL("Can't find pointerId %" PRId32 " in %s", pointerId, args.dump().c_str()); +} + +/** + * Find the action for individual pointer at the given pointer index. + * This is always equal to MotionEvent::getActionMasked, except for + * POINTER_UP or POINTER_DOWN events. For example, in a POINTER_UP event, the action for + * the active pointer is ACTION_POINTER_UP, while the action for the other pointers is ACTION_MOVE. + */ +static int32_t resolveActionForPointer(uint8_t pointerIndex, int32_t action) { + const int32_t actionMasked = MotionEvent::getActionMasked(action); + if (actionMasked != AMOTION_EVENT_ACTION_POINTER_DOWN && + actionMasked != AMOTION_EVENT_ACTION_POINTER_UP) { + return actionMasked; + } + // This is a POINTER_DOWN or POINTER_UP event + const uint8_t actionIndex = MotionEvent::getActionIndex(action); + if (pointerIndex == actionIndex) { + return actionMasked; + } + // When POINTER_DOWN or POINTER_UP happens, it's actually a MOVE for all of the other + // pointers + return AMOTION_EVENT_ACTION_MOVE; +} + +static const char* toString(bool value) { + return value ? "true" : "false"; +} + +std::string toString(const ::ui::InProgressTouchEvdev& touch) { + return StringPrintf("x=%.1f, y=%.1f, tracking_id=%i, slot=%zu," + " pressure=%.1f, major=%i, minor=%i, " + "tool_type=%i, altered=%s, was_touching=%s, touching=%s", + touch.x, touch.y, touch.tracking_id, touch.slot, touch.pressure, + touch.major, touch.minor, touch.tool_type, toString(touch.altered), + toString(touch.was_touching), toString(touch.touching)); +} + +/** + * Remove the data for the provided pointers from the args. The pointers are identified by their + * pointerId, not by the index inside the array. + * Return the new NotifyMotionArgs struct that has the remaining pointers. + * The only fields that may be different in the returned args from the provided args are: + * - action + * - pointerCount + * - pointerProperties + * - pointerCoords + * Action might change because it contains a pointer index. If another pointer is removed, the + * active pointer index would be shifted. + * Do not call this function for events with POINTER_UP or POINTER_DOWN events when removed pointer + * id is the acting pointer id. + * + * @param args the args from which the pointers should be removed + * @param pointerIds the pointer ids of the pointers that should be removed + */ +NotifyMotionArgs removePointerIds(const NotifyMotionArgs& args, + const std::set& pointerIds) { + const uint8_t actionIndex = MotionEvent::getActionIndex(args.action); + const int32_t actionMasked = MotionEvent::getActionMasked(args.action); + const bool isPointerUpOrDownAction = actionMasked == AMOTION_EVENT_ACTION_POINTER_DOWN || + actionMasked == AMOTION_EVENT_ACTION_POINTER_UP; + + NotifyMotionArgs newArgs{args}; + newArgs.pointerCount = 0; + int32_t newActionIndex = 0; + for (uint32_t i = 0; i < args.pointerCount; i++) { + const int32_t pointerId = args.pointerProperties[i].id; + if (pointerIds.find(pointerId) != pointerIds.end()) { + // skip this pointer + if (isPointerUpOrDownAction && i == actionIndex) { + // The active pointer is being removed, so the action is no longer valid. + // Set the action to 'UNKNOWN' here. The caller is responsible for updating this + // action later to a proper value. + newArgs.action = ACTION_UNKNOWN; + } + continue; + } + newArgs.pointerProperties[newArgs.pointerCount].copyFrom(args.pointerProperties[i]); + newArgs.pointerCoords[newArgs.pointerCount].copyFrom(args.pointerCoords[i]); + if (i == actionIndex) { + newActionIndex = newArgs.pointerCount; + } + newArgs.pointerCount++; + } + // Update POINTER_DOWN or POINTER_UP actions + if (isPointerUpOrDownAction && newArgs.action != ACTION_UNKNOWN) { + newArgs.action = + actionMasked | (newActionIndex << AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT); + // Convert POINTER_DOWN and POINTER_UP to DOWN and UP if there's only 1 pointer remaining + if (newArgs.pointerCount == 1) { + if (actionMasked == AMOTION_EVENT_ACTION_POINTER_DOWN) { + newArgs.action = AMOTION_EVENT_ACTION_DOWN; + } else if (actionMasked == AMOTION_EVENT_ACTION_POINTER_UP) { + newArgs.action = AMOTION_EVENT_ACTION_UP; + } + } + } + return newArgs; +} + +std::optional createPalmFilterDeviceInfo( + const InputDeviceInfo& deviceInfo) { + if (!isFromTouchscreen(deviceInfo.getSources())) { + return std::nullopt; + } + AndroidPalmFilterDeviceInfo out; + const InputDeviceInfo::MotionRange* axisX = + deviceInfo.getMotionRange(AMOTION_EVENT_AXIS_X, AINPUT_SOURCE_TOUCHSCREEN); + if (axisX != nullptr) { + out.max_x = axisX->max; + out.x_res = axisX->resolution; + } else { + ALOGW("Palm rejection is disabled for %s because AXIS_X is not supported", + deviceInfo.getDisplayName().c_str()); + return std::nullopt; + } + const InputDeviceInfo::MotionRange* axisY = + deviceInfo.getMotionRange(AMOTION_EVENT_AXIS_Y, AINPUT_SOURCE_TOUCHSCREEN); + if (axisY != nullptr) { + out.max_y = axisY->max; + out.y_res = axisY->resolution; + } else { + ALOGW("Palm rejection is disabled for %s because AXIS_Y is not supported", + deviceInfo.getDisplayName().c_str()); + return std::nullopt; + } + const InputDeviceInfo::MotionRange* axisMajor = + deviceInfo.getMotionRange(AMOTION_EVENT_AXIS_TOUCH_MAJOR, AINPUT_SOURCE_TOUCHSCREEN); + if (axisMajor != nullptr) { + out.major_radius_res = axisMajor->resolution; + out.touch_major_res = axisMajor->resolution; + } else { + return std::nullopt; + } + const InputDeviceInfo::MotionRange* axisMinor = + deviceInfo.getMotionRange(AMOTION_EVENT_AXIS_TOUCH_MINOR, AINPUT_SOURCE_TOUCHSCREEN); + if (axisMinor != nullptr) { + out.minor_radius_res = axisMinor->resolution; + out.touch_minor_res = axisMinor->resolution; + out.minor_radius_supported = true; + } else { + out.minor_radius_supported = false; + } + + return out; +} + +/** + * Synthesize CANCEL events for any new pointers that should be canceled, while removing pointers + * that have already been canceled. + * The flow of the function is as follows: + * 1. Remove all already canceled pointers + * 2. Cancel all newly suppressed pointers + * 3. Decide what to do with the current event : keep it, or drop it + * The pointers can never be "unsuppressed": once a pointer is canceled, it will never become valid. + */ +std::vector cancelSuppressedPointers( + const NotifyMotionArgs& args, const std::set& oldSuppressedPointerIds, + const std::set& newSuppressedPointerIds) { + LOG_ALWAYS_FATAL_IF(args.pointerCount == 0, "0 pointers in %s", args.dump().c_str()); + + // First, let's remove the old suppressed pointers. They've already been canceled previously. + NotifyMotionArgs oldArgs = removePointerIds(args, oldSuppressedPointerIds); + + // Cancel any newly suppressed pointers. + std::vector out; + const int32_t activePointerId = + args.pointerProperties[MotionEvent::getActionIndex(args.action)].id; + const int32_t actionMasked = MotionEvent::getActionMasked(args.action); + // We will iteratively remove pointers from 'removedArgs'. + NotifyMotionArgs removedArgs{oldArgs}; + for (uint32_t i = 0; i < oldArgs.pointerCount; i++) { + const int32_t pointerId = oldArgs.pointerProperties[i].id; + if (newSuppressedPointerIds.find(pointerId) == newSuppressedPointerIds.end()) { + // This is a pointer that should not be canceled. Move on. + continue; + } + if (pointerId == activePointerId && actionMasked == AMOTION_EVENT_ACTION_POINTER_DOWN) { + // Remove this pointer, but don't cancel it. We'll just not send the POINTER_DOWN event + removedArgs = removePointerIds(removedArgs, {pointerId}); + continue; + } + + if (removedArgs.pointerCount == 1) { + // We are about to remove the last pointer, which means there will be no more gesture + // remaining. This is identical to canceling all pointers, so just send a single CANCEL + // event, without any of the preceding POINTER_UP with FLAG_CANCELED events. + oldArgs.flags |= AMOTION_EVENT_FLAG_CANCELED; + oldArgs.action = AMOTION_EVENT_ACTION_CANCEL; + return {oldArgs}; + } + // Cancel the current pointer + out.push_back(removedArgs); + out.back().flags |= AMOTION_EVENT_FLAG_CANCELED; + out.back().action = getActionUpForPointerId(out.back(), pointerId); + + // Remove the newly canceled pointer from the args + removedArgs = removePointerIds(removedArgs, {pointerId}); + } + + // Now 'removedArgs' contains only pointers that are valid. + if (removedArgs.pointerCount <= 0 || removedArgs.action == ACTION_UNKNOWN) { + return out; + } + out.push_back(removedArgs); + return out; +} + +UnwantedInteractionBlocker::UnwantedInteractionBlocker(InputListenerInterface& listener) + : UnwantedInteractionBlocker(listener, isPalmRejectionEnabled()){}; + +UnwantedInteractionBlocker::UnwantedInteractionBlocker(InputListenerInterface& listener, + bool enablePalmRejection) + : mListener(listener), mEnablePalmRejection(enablePalmRejection) {} + +void UnwantedInteractionBlocker::notifyConfigurationChanged( + const NotifyConfigurationChangedArgs* args) { + mListener.notifyConfigurationChanged(args); +} + +void UnwantedInteractionBlocker::notifyKey(const NotifyKeyArgs* args) { + mListener.notifyKey(args); +} + +void UnwantedInteractionBlocker::notifyMotion(const NotifyMotionArgs* args) { + auto it = mPalmRejectors.find(args->deviceId); + const bool sendToPalmRejector = it != mPalmRejectors.end() && isFromTouchscreen(args->source); + if (!sendToPalmRejector) { + mListener.notifyMotion(args); + return; + } + + const std::vector newMotions = it->second.processMotion(*args); + for (const NotifyMotionArgs& newArgs : newMotions) { + mListener.notifyMotion(&newArgs); + } +} + +void UnwantedInteractionBlocker::notifySwitch(const NotifySwitchArgs* args) { + mListener.notifySwitch(args); +} + +void UnwantedInteractionBlocker::notifySensor(const NotifySensorArgs* args) { + mListener.notifySensor(args); +} + +void UnwantedInteractionBlocker::notifyVibratorState(const NotifyVibratorStateArgs* args) { + mListener.notifyVibratorState(args); +} +void UnwantedInteractionBlocker::notifyDeviceReset(const NotifyDeviceResetArgs* args) { + auto it = mPalmRejectors.find(args->deviceId); + if (it != mPalmRejectors.end()) { + AndroidPalmFilterDeviceInfo info = it->second.getPalmFilterDeviceInfo(); + // Re-create the object instead of resetting it + mPalmRejectors.erase(it); + mPalmRejectors.emplace(args->deviceId, info); + } + mListener.notifyDeviceReset(args); +} + +void UnwantedInteractionBlocker::notifyPointerCaptureChanged( + const NotifyPointerCaptureChangedArgs* args) { + mListener.notifyPointerCaptureChanged(args); +} + +void UnwantedInteractionBlocker::notifyInputDevicesChanged( + const std::vector& inputDevices) { + if (!mEnablePalmRejection) { + // Palm rejection is disabled. Don't create any palm rejector objects. + return; + } + + // Let's see which of the existing devices didn't change, so that we can keep them + // and prevent event stream disruption + std::set devicesToKeep; + for (const InputDeviceInfo& device : inputDevices) { + std::optional info = createPalmFilterDeviceInfo(device); + if (!info) { + continue; + } + + auto [it, emplaced] = mPalmRejectors.try_emplace(device.getId(), *info); + if (!emplaced && *info != it->second.getPalmFilterDeviceInfo()) { + // Re-create the PalmRejector because the device info has changed. + mPalmRejectors.erase(it); + mPalmRejectors.emplace(device.getId(), *info); + } + devicesToKeep.insert(device.getId()); + } + // Delete all devices that we don't need to keep + std::erase_if(mPalmRejectors, [&devicesToKeep](const auto& item) { + auto const& [deviceId, _] = item; + return devicesToKeep.find(deviceId) == devicesToKeep.end(); + }); +} + +void UnwantedInteractionBlocker::dump(std::string& dump) { + dump += "UnwantedInteractionBlocker:\n"; + dump += StringPrintf(" mEnablePalmRejection: %s\n", toString(mEnablePalmRejection)); + dump += StringPrintf(" isPalmRejectionEnabled (flag value): %s\n", + toString(isPalmRejectionEnabled())); + dump += mPalmRejectors.empty() ? " mPalmRejectors: None\n" : " mPalmRejectors:\n"; + for (const auto& [deviceId, palmRejector] : mPalmRejectors) { + dump += StringPrintf(" deviceId = %" PRId32 ":\n", deviceId); + dump += addPrefix(palmRejector.dump(), " "); + } +} + +void UnwantedInteractionBlocker::monitor() {} + +UnwantedInteractionBlocker::~UnwantedInteractionBlocker() {} + +void SlotState::update(const NotifyMotionArgs& args) { + for (size_t i = 0; i < args.pointerCount; i++) { + const int32_t pointerId = args.pointerProperties[i].id; + const int32_t resolvedAction = resolveActionForPointer(i, args.action); + processPointerId(pointerId, resolvedAction); + } +} + +size_t SlotState::findUnusedSlot() const { + size_t unusedSlot = 0; + // Since the collection is ordered, we can rely on the in-order traversal + for (const auto& [slot, trackingId] : mPointerIdsBySlot) { + if (unusedSlot != slot) { + break; + } + unusedSlot++; + } + return unusedSlot; +} + +void SlotState::processPointerId(int pointerId, int32_t actionMasked) { + switch (MotionEvent::getActionMasked(actionMasked)) { + case AMOTION_EVENT_ACTION_DOWN: + case AMOTION_EVENT_ACTION_POINTER_DOWN: + case AMOTION_EVENT_ACTION_HOVER_ENTER: { + // New pointer going down + size_t newSlot = findUnusedSlot(); + mPointerIdsBySlot[newSlot] = pointerId; + mSlotsByPointerId[pointerId] = newSlot; + return; + } + case AMOTION_EVENT_ACTION_MOVE: + case AMOTION_EVENT_ACTION_HOVER_MOVE: { + return; + } + case AMOTION_EVENT_ACTION_CANCEL: + case AMOTION_EVENT_ACTION_POINTER_UP: + case AMOTION_EVENT_ACTION_UP: + case AMOTION_EVENT_ACTION_HOVER_EXIT: { + auto it = mSlotsByPointerId.find(pointerId); + LOG_ALWAYS_FATAL_IF(it == mSlotsByPointerId.end()); + size_t slot = it->second; + // Erase this pointer from both collections + mPointerIdsBySlot.erase(slot); + mSlotsByPointerId.erase(pointerId); + return; + } + } + LOG_ALWAYS_FATAL("Unhandled action : %s", MotionEvent::actionToString(actionMasked).c_str()); + return; +} + +std::optional SlotState::getSlotForPointerId(int32_t pointerId) const { + auto it = mSlotsByPointerId.find(pointerId); + if (it == mSlotsByPointerId.end()) { + return std::nullopt; + } + return it->second; +} + +std::string SlotState::dump() const { + std::string out = "mSlotsByPointerId:\n"; + out += addPrefix(dumpMap(mSlotsByPointerId), " ") + "\n"; + out += "mPointerIdsBySlot:\n"; + out += addPrefix(dumpMap(mPointerIdsBySlot), " ") + "\n"; + return out; +} + +PalmRejector::PalmRejector(const AndroidPalmFilterDeviceInfo& info, + std::unique_ptr<::ui::PalmDetectionFilter> filter) + : mSharedPalmState(std::make_unique<::ui::SharedPalmDetectionFilterState>()), + mDeviceInfo(info), + mPalmDetectionFilter(std::move(filter)) { + if (mPalmDetectionFilter != nullptr) { + // This path is used for testing. Non-testing invocations should let this constructor + // create a real PalmDetectionFilter + return; + } + std::unique_ptr<::ui::NeuralStylusPalmDetectionFilterModel> model = + std::make_unique<::ui::OneDeviceTrainNeuralStylusPalmDetectionFilterModel>( + std::vector()); + mPalmDetectionFilter = + std::make_unique<::ui::NeuralStylusPalmDetectionFilter>(mDeviceInfo, std::move(model), + mSharedPalmState.get()); +} + +std::vector<::ui::InProgressTouchEvdev> getTouches(const NotifyMotionArgs& args, + const AndroidPalmFilterDeviceInfo& deviceInfo, + const SlotState& oldSlotState, + const SlotState& newSlotState) { + std::vector<::ui::InProgressTouchEvdev> touches; + + for (size_t i = 0; i < args.pointerCount; i++) { + const int32_t pointerId = args.pointerProperties[i].id; + touches.emplace_back(::ui::InProgressTouchEvdev()); + touches.back().major = args.pointerCoords[i].getAxisValue(AMOTION_EVENT_AXIS_TOUCH_MAJOR); + touches.back().minor = args.pointerCoords[i].getAxisValue(AMOTION_EVENT_AXIS_TOUCH_MINOR); + touches.back().tool_type = getLinuxToolType(args.pointerProperties[i].toolType); + + // Whether there is new information for the touch. + touches.back().altered = true; + + // Whether the touch was cancelled. Touch events should be ignored till a + // new touch is initiated. + touches.back().was_cancelled = false; + + // Whether the touch is going to be canceled. + touches.back().cancelled = false; + + // Whether the touch is delayed at first appearance. Will not be reported yet. + touches.back().delayed = false; + + // Whether the touch was delayed before. + touches.back().was_delayed = false; + + // Whether the touch is held until end or no longer held. + touches.back().held = false; + + // Whether this touch was held before being sent. + touches.back().was_held = false; + + const int32_t resolvedAction = resolveActionForPointer(i, args.action); + const bool isDown = resolvedAction == AMOTION_EVENT_ACTION_POINTER_DOWN || + resolvedAction == AMOTION_EVENT_ACTION_DOWN; + touches.back().was_touching = !isDown; + + const bool isUpOrCancel = resolvedAction == AMOTION_EVENT_ACTION_CANCEL || + resolvedAction == AMOTION_EVENT_ACTION_UP || + resolvedAction == AMOTION_EVENT_ACTION_POINTER_UP; + + touches.back().x = args.pointerCoords[i].getAxisValue(AMOTION_EVENT_AXIS_X); + touches.back().y = args.pointerCoords[i].getAxisValue(AMOTION_EVENT_AXIS_Y); + + std::optional slot = newSlotState.getSlotForPointerId(pointerId); + if (!slot) { + slot = oldSlotState.getSlotForPointerId(pointerId); + } + LOG_ALWAYS_FATAL_IF(!slot, "Could not find slot for pointer %d", pointerId); + touches.back().slot = *slot; + touches.back().tracking_id = (!isUpOrCancel) ? pointerId : -1; + touches.back().touching = !isUpOrCancel; + + // The fields 'radius_x' and 'radius_x' are not used for palm rejection + touches.back().pressure = args.pointerCoords[i].getAxisValue(AMOTION_EVENT_AXIS_PRESSURE); + touches.back().tool_code = BTN_TOOL_FINGER; + // The field 'orientation' is not used for palm rejection + // The fields 'tilt_x' and 'tilt_y' are not used for palm rejection + touches.back().reported_tool_type = ::ui::EventPointerType::kTouch; + touches.back().stylus_button = false; + } + return touches; +} + +std::vector PalmRejector::processMotion(const NotifyMotionArgs& args) { + if (mPalmDetectionFilter == nullptr) { + return {args}; + } + const bool skipThisEvent = args.action == AMOTION_EVENT_ACTION_HOVER_ENTER || + args.action == AMOTION_EVENT_ACTION_HOVER_MOVE || + args.action == AMOTION_EVENT_ACTION_HOVER_EXIT || + args.action == AMOTION_EVENT_ACTION_BUTTON_PRESS || + args.action == AMOTION_EVENT_ACTION_BUTTON_RELEASE || + args.action == AMOTION_EVENT_ACTION_SCROLL; + if (skipThisEvent) { + // Lets not process hover events, button events, or scroll for now. + return {args}; + } + if (args.action == AMOTION_EVENT_ACTION_DOWN) { + mSuppressedPointerIds.clear(); + } + std::bitset<::ui::kNumTouchEvdevSlots> slotsToHold; + std::bitset<::ui::kNumTouchEvdevSlots> slotsToSuppress; + + // Store the slot state before we call getTouches and update it. This way, we can find + // the slots that have been removed due to the incoming event. + SlotState oldSlotState = mSlotState; + mSlotState.update(args); + std::vector<::ui::InProgressTouchEvdev> touches = + getTouches(args, mDeviceInfo, oldSlotState, mSlotState); + ::base::TimeTicks chromeTimestamp = toChromeTimestamp(args.eventTime); + + mPalmDetectionFilter->Filter(touches, chromeTimestamp, &slotsToHold, &slotsToSuppress); + + // Now that we know which slots should be suppressed, let's convert those to pointer id's. + std::set oldSuppressedIds; + std::swap(oldSuppressedIds, mSuppressedPointerIds); + for (size_t i = 0; i < args.pointerCount; i++) { + const int32_t pointerId = args.pointerProperties[i].id; + std::optional slot = oldSlotState.getSlotForPointerId(pointerId); + if (!slot) { + slot = mSlotState.getSlotForPointerId(pointerId); + LOG_ALWAYS_FATAL_IF(!slot, "Could not find slot for pointer id %" PRId32, pointerId); + } + if (slotsToSuppress.test(*slot)) { + mSuppressedPointerIds.insert(pointerId); + } + } + + std::vector argsWithoutUnwantedPointers = + cancelSuppressedPointers(args, oldSuppressedIds, mSuppressedPointerIds); + for (const NotifyMotionArgs& checkArgs : argsWithoutUnwantedPointers) { + LOG_ALWAYS_FATAL_IF(checkArgs.action == ACTION_UNKNOWN, "%s", checkArgs.dump().c_str()); + } + + if (mSuppressedPointerIds != oldSuppressedIds) { + if (argsWithoutUnwantedPointers.size() != 1 || + argsWithoutUnwantedPointers[0].pointerCount != args.pointerCount) { + ALOGI("Palm detected, removing pointer ids %s from %s", + dumpSet(mSuppressedPointerIds).c_str(), args.dump().c_str()); + } + } + + return argsWithoutUnwantedPointers; +} + +const AndroidPalmFilterDeviceInfo& PalmRejector::getPalmFilterDeviceInfo() { + return mDeviceInfo; +} + +std::string PalmRejector::dump() const { + std::string out; + out += "mDeviceInfo:\n"; + out += addPrefix(dumpDeviceInfo(mDeviceInfo), " "); + out += "mSlotState:\n"; + out += addPrefix(mSlotState.dump(), " "); + out += "mSuppressedPointerIds: "; + out += dumpSet(mSuppressedPointerIds) + "\n"; + return out; +} + +} // namespace android -- cgit v1.2.3-59-g8ed1b From f6db4c395d2fc0821a6a42aaeb88154eb7183aae Mon Sep 17 00:00:00 2001 From: Siarhei Vishniakou Date: Thu, 10 Feb 2022 19:46:34 -0800 Subject: Block touches when stylus is down Since we haven't yet added simultaneous touch and stylus support, let's improve the user experience by blocking all touch when the stylus is down. Bug: 210159205 Test: atest inputflinger_tests Change-Id: Id6a6467d7feb7c7d91770ddbd63b92583832d504 (cherry picked from commit a3c8e51901d17f696838d3e8260464f7437468a5) --- services/inputflinger/Android.bp | 1 + .../inputflinger/PreferStylusOverTouchBlocker.cpp | 107 +++++++++ .../inputflinger/PreferStylusOverTouchBlocker.h | 81 +++++++ .../inputflinger/UnwantedInteractionBlocker.cpp | 13 +- services/inputflinger/UnwantedInteractionBlocker.h | 6 + services/inputflinger/tests/Android.bp | 1 + .../tests/PreferStylusOverTouch_test.cpp | 250 +++++++++++++++++++++ 7 files changed, 458 insertions(+), 1 deletion(-) create mode 100644 services/inputflinger/PreferStylusOverTouchBlocker.cpp create mode 100644 services/inputflinger/PreferStylusOverTouchBlocker.h create mode 100644 services/inputflinger/tests/PreferStylusOverTouch_test.cpp (limited to 'services/inputflinger/UnwantedInteractionBlocker.cpp') diff --git a/services/inputflinger/Android.bp b/services/inputflinger/Android.bp index ed9bfd272f..41878e3487 100644 --- a/services/inputflinger/Android.bp +++ b/services/inputflinger/Android.bp @@ -58,6 +58,7 @@ filegroup { srcs: [ "InputClassifier.cpp", "InputCommonConverter.cpp", + "PreferStylusOverTouchBlocker.cpp", "UnwantedInteractionBlocker.cpp", "InputManager.cpp", ], diff --git a/services/inputflinger/PreferStylusOverTouchBlocker.cpp b/services/inputflinger/PreferStylusOverTouchBlocker.cpp new file mode 100644 index 0000000000..ad639b4ef8 --- /dev/null +++ b/services/inputflinger/PreferStylusOverTouchBlocker.cpp @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2022 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 "PreferStylusOverTouchBlocker.h" + +#include + +using android::base::StringPrintf; + +static const char* toString(bool value) { + return value ? "true" : "false"; +} + +namespace android { + +ftl::StaticVector PreferStylusOverTouchBlocker::processMotion( + const NotifyMotionArgs& args) { + const bool isStylusEvent = isFromSource(args.source, AINPUT_SOURCE_STYLUS); + if (isStylusEvent) { + for (size_t i = 0; i < args.pointerCount; i++) { + // Make sure we are canceling stylus pointers + const int32_t toolType = args.pointerProperties[i].toolType; + LOG_ALWAYS_FATAL_IF(toolType != AMOTION_EVENT_TOOL_TYPE_STYLUS && + toolType != AMOTION_EVENT_TOOL_TYPE_ERASER, + "The pointer %zu has toolType=%i, but the source is STYLUS. If " + "simultaneous touch and stylus is supported, " + "'PreferStylusOverTouchBlocker' should be disabled.", + i, toolType); + } + } + const bool isDown = args.action == AMOTION_EVENT_ACTION_DOWN; + const bool isUpOrCancel = + args.action == AMOTION_EVENT_ACTION_UP || args.action == AMOTION_EVENT_ACTION_CANCEL; + if (isStylusEvent) { + if (isDown) { + // Reject all touch while stylus is down + mIsStylusDown = true; + if (mIsTouchDown && !mCurrentTouchIsCanceled) { + // Cancel touch! + mCurrentTouchIsCanceled = true; + mLastTouchEvent.action = AMOTION_EVENT_ACTION_CANCEL; + mLastTouchEvent.flags |= AMOTION_EVENT_FLAG_CANCELED; + mLastTouchEvent.eventTime = systemTime(SYSTEM_TIME_MONOTONIC); + return {mLastTouchEvent, args}; + } + } + if (isUpOrCancel) { + mIsStylusDown = false; + } + // Never drop stylus events + return {args}; + } + + const bool isTouchEvent = + isFromSource(args.source, AINPUT_SOURCE_TOUCHSCREEN) && !isStylusEvent; + if (isTouchEvent) { + if (mIsStylusDown) { + mCurrentTouchIsCanceled = true; + } + // If we already canceled the current gesture, then continue to drop events from it, even if + // the stylus has been lifted. + if (mCurrentTouchIsCanceled) { + if (isUpOrCancel) { + mCurrentTouchIsCanceled = false; + } + return {}; + } + + // Update state + mLastTouchEvent = args; + if (isDown) { + mIsTouchDown = true; + } + if (isUpOrCancel) { + mIsTouchDown = false; + mCurrentTouchIsCanceled = false; + } + return {args}; + } + + // Not a touch or stylus event + return {args}; +} + +std::string PreferStylusOverTouchBlocker::dump() { + std::string out; + out += StringPrintf("mIsTouchDown: %s\n", toString(mIsTouchDown)); + out += StringPrintf("mIsStylusDown: %s\n", toString(mIsStylusDown)); + out += StringPrintf("mLastTouchEvent: %s\n", mLastTouchEvent.dump().c_str()); + out += StringPrintf("mCurrentTouchIsCanceled: %s\n", toString(mCurrentTouchIsCanceled)); + return out; +} + +} // namespace android diff --git a/services/inputflinger/PreferStylusOverTouchBlocker.h b/services/inputflinger/PreferStylusOverTouchBlocker.h new file mode 100644 index 0000000000..3f5616190b --- /dev/null +++ b/services/inputflinger/PreferStylusOverTouchBlocker.h @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2022 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 +#include +#include "InputListener.h" + +namespace android { + +/** + * When stylus is down, we ignore all touch. + * TODO(b/210159205): delete this when simultaneous stylus and touch is supported + */ +class PreferStylusOverTouchBlocker { +public: + /** + * Process the provided event and emit up to 2 events in response. + * In the majority of cases, the returned result will just be the provided args (array with + * only 1 element), unmodified. + * + * If the gesture should be blocked, the returned result may be: + * + * a) An empty array, if the current event should just be ignored completely + * b) An array of 2 elements, containing an event with ACTION_CANCEL and the current event. + * + * bool is set to 'true'. + * NotifyMotionArgs potentially contains an event that should be used to cancel the existing + * gesture. + * + * If the event should not be blocked, bool contains 'false'. + */ + ftl::StaticVector processMotion(const NotifyMotionArgs& args); + std::string dump(); + +private: + bool mIsTouchDown = false; + bool mIsStylusDown = false; + // Provide some default values for the stored MotionEvent to allow printint the event before + // any real event is received. + NotifyMotionArgs mLastTouchEvent{0 /*id*/, + 0 /*eventTime*/, + 0 /*readTime*/, + 0 /*deviceId*/, + AINPUT_SOURCE_TOUCHSCREEN, + 0 /*displayId*/, + 0 /*policyFlags*/, + 0 /*action*/, + 0 /*actionButton*/, + 0 /*flags*/, + 0 /*metaState*/, + 0 /*buttonState*/, + MotionClassification::NONE, + AMOTION_EVENT_EDGE_FLAG_NONE, + 0 /*pointerCount*/, + nullptr /*properties*/, + nullptr /*coords*/, + 0. /*xPrecision*/, + 0. /*yPrecision*/, + AMOTION_EVENT_INVALID_CURSOR_POSITION, + AMOTION_EVENT_INVALID_CURSOR_POSITION, + 0 /*downTime*/, + {}}; + bool mCurrentTouchIsCanceled = false; +}; + +} // namespace android \ No newline at end of file diff --git a/services/inputflinger/UnwantedInteractionBlocker.cpp b/services/inputflinger/UnwantedInteractionBlocker.cpp index 64dbb8ceb4..f904bfd82e 100644 --- a/services/inputflinger/UnwantedInteractionBlocker.cpp +++ b/services/inputflinger/UnwantedInteractionBlocker.cpp @@ -44,7 +44,8 @@ static std::string toLower(std::string s) { } static bool isFromTouchscreen(int32_t source) { - return isFromSource(source, AINPUT_SOURCE_TOUCHSCREEN); + return isFromSource(source, AINPUT_SOURCE_TOUCHSCREEN) && + !isFromSource(source, AINPUT_SOURCE_STYLUS); } static ::base::TimeTicks toChromeTimestamp(nsecs_t eventTime) { @@ -367,6 +368,14 @@ void UnwantedInteractionBlocker::notifyKey(const NotifyKeyArgs* args) { } void UnwantedInteractionBlocker::notifyMotion(const NotifyMotionArgs* args) { + ftl::StaticVector processedArgs = + mPreferStylusOverTouchBlocker.processMotion(*args); + for (const NotifyMotionArgs& loopArgs : processedArgs) { + notifyMotionInner(&loopArgs); + } +} + +void UnwantedInteractionBlocker::notifyMotionInner(const NotifyMotionArgs* args) { auto it = mPalmRejectors.find(args->deviceId); const bool sendToPalmRejector = it != mPalmRejectors.end() && isFromTouchscreen(args->source); if (!sendToPalmRejector) { @@ -440,6 +449,8 @@ void UnwantedInteractionBlocker::notifyInputDevicesChanged( void UnwantedInteractionBlocker::dump(std::string& dump) { dump += "UnwantedInteractionBlocker:\n"; + dump += " mPreferStylusOverTouchBlocker:\n"; + dump += addPrefix(mPreferStylusOverTouchBlocker.dump(), " "); dump += StringPrintf(" mEnablePalmRejection: %s\n", toString(mEnablePalmRejection)); dump += StringPrintf(" isPalmRejectionEnabled (flag value): %s\n", toString(isPalmRejectionEnabled())); diff --git a/services/inputflinger/UnwantedInteractionBlocker.h b/services/inputflinger/UnwantedInteractionBlocker.h index 14068fd878..9e41995ac4 100644 --- a/services/inputflinger/UnwantedInteractionBlocker.h +++ b/services/inputflinger/UnwantedInteractionBlocker.h @@ -23,6 +23,8 @@ #include "ui/events/ozone/evdev/touch_filter/neural_stylus_palm_detection_filter_util.h" #include "ui/events/ozone/evdev/touch_filter/palm_detection_filter.h" +#include "PreferStylusOverTouchBlocker.h" + namespace android { // --- Functions for manipulation of event streams @@ -88,9 +90,13 @@ private: InputListenerInterface& mListener; const bool mEnablePalmRejection; + // When stylus is down, ignore touch + PreferStylusOverTouchBlocker mPreferStylusOverTouchBlocker; // Detect and reject unwanted palms on screen // Use a separate palm rejector for every touch device. std::map mPalmRejectors; + // TODO(b/210159205): delete this when simultaneous stylus and touch is supported + void notifyMotionInner(const NotifyMotionArgs* args); }; class SlotState { diff --git a/services/inputflinger/tests/Android.bp b/services/inputflinger/tests/Android.bp index 9d200bdeff..76a7c19086 100644 --- a/services/inputflinger/tests/Android.bp +++ b/services/inputflinger/tests/Android.bp @@ -47,6 +47,7 @@ cc_test { "InputReader_test.cpp", "InputFlingerService_test.cpp", "LatencyTracker_test.cpp", + "PreferStylusOverTouch_test.cpp", "TestInputListener.cpp", "UinputDevice.cpp", "UnwantedInteractionBlocker_test.cpp", diff --git a/services/inputflinger/tests/PreferStylusOverTouch_test.cpp b/services/inputflinger/tests/PreferStylusOverTouch_test.cpp new file mode 100644 index 0000000000..70f40aa028 --- /dev/null +++ b/services/inputflinger/tests/PreferStylusOverTouch_test.cpp @@ -0,0 +1,250 @@ +/* + * Copyright (C) 2022 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 +#include "../PreferStylusOverTouchBlocker.h" + +namespace android { + +constexpr int32_t TOUCH_DEVICE_ID = 3; +constexpr int32_t STYLUS_DEVICE_ID = 4; + +constexpr int DOWN = AMOTION_EVENT_ACTION_DOWN; +constexpr int MOVE = AMOTION_EVENT_ACTION_MOVE; +constexpr int UP = AMOTION_EVENT_ACTION_UP; +constexpr int CANCEL = AMOTION_EVENT_ACTION_CANCEL; +constexpr int32_t TOUCHSCREEN = AINPUT_SOURCE_TOUCHSCREEN; +constexpr int32_t STYLUS = AINPUT_SOURCE_STYLUS; + +struct PointerData { + float x; + float y; +}; + +static NotifyMotionArgs generateMotionArgs(nsecs_t downTime, nsecs_t eventTime, int32_t action, + const std::vector& points, + uint32_t source) { + size_t pointerCount = points.size(); + if (action == DOWN || action == UP) { + EXPECT_EQ(1U, pointerCount) << "Actions DOWN and UP can only contain a single pointer"; + } + + PointerProperties pointerProperties[pointerCount]; + PointerCoords pointerCoords[pointerCount]; + + const int32_t deviceId = isFromSource(source, TOUCHSCREEN) ? TOUCH_DEVICE_ID : STYLUS_DEVICE_ID; + const int32_t toolType = isFromSource(source, TOUCHSCREEN) ? AMOTION_EVENT_TOOL_TYPE_FINGER + : AMOTION_EVENT_TOOL_TYPE_STYLUS; + for (size_t i = 0; i < pointerCount; i++) { + pointerProperties[i].clear(); + pointerProperties[i].id = i; + pointerProperties[i].toolType = toolType; + + pointerCoords[i].clear(); + pointerCoords[i].setAxisValue(AMOTION_EVENT_AXIS_X, points[i].x); + pointerCoords[i].setAxisValue(AMOTION_EVENT_AXIS_Y, points[i].y); + } + + // Currently, can't have STYLUS source without it also being a TOUCH source. Update the source + // accordingly. + if (isFromSource(source, STYLUS)) { + source |= TOUCHSCREEN; + } + + // Define a valid motion event. + NotifyMotionArgs args(/* id */ 0, eventTime, 0 /*readTime*/, deviceId, source, 0 /*displayId*/, + POLICY_FLAG_PASS_TO_USER, action, /* actionButton */ 0, + /* flags */ 0, AMETA_NONE, /* buttonState */ 0, + MotionClassification::NONE, AMOTION_EVENT_EDGE_FLAG_NONE, pointerCount, + pointerProperties, pointerCoords, /* xPrecision */ 0, /* yPrecision */ 0, + AMOTION_EVENT_INVALID_CURSOR_POSITION, + AMOTION_EVENT_INVALID_CURSOR_POSITION, downTime, /* videoFrames */ {}); + + return args; +} + +class PreferStylusOverTouchTest : public testing::Test { +protected: + void assertNotBlocked(const NotifyMotionArgs& args) { + ftl::StaticVector processedArgs = mBlocker.processMotion(args); + ASSERT_EQ(1u, processedArgs.size()); + ASSERT_EQ(args, processedArgs[0]); + } + + void assertDropped(const NotifyMotionArgs& args) { + ftl::StaticVector processedArgs = mBlocker.processMotion(args); + ASSERT_TRUE(processedArgs.empty()); + } + + void assertCanceled(const NotifyMotionArgs& args, + std::optional canceledArgs) { + ftl::StaticVector processedArgs = mBlocker.processMotion(args); + ASSERT_EQ(2u, processedArgs.size()); + NotifyMotionArgs& cancelEvent = processedArgs[0]; + ASSERT_EQ(CANCEL, cancelEvent.action); + ASSERT_EQ(AMOTION_EVENT_FLAG_CANCELED, cancelEvent.flags & AMOTION_EVENT_FLAG_CANCELED); + ASSERT_TRUE(isFromSource(cancelEvent.source, TOUCHSCREEN)); + ASSERT_FALSE(isFromSource(cancelEvent.source, STYLUS)); + + ASSERT_EQ(args, processedArgs[1]); + } + +private: + PreferStylusOverTouchBlocker mBlocker; +}; + +TEST_F(PreferStylusOverTouchTest, TouchGestureIsNotBlocked) { + NotifyMotionArgs args; + + args = generateMotionArgs(0 /*downTime*/, 0 /*eventTime*/, DOWN, {{1, 2}}, TOUCHSCREEN); + assertNotBlocked(args); + + args = generateMotionArgs(0 /*downTime*/, 1 /*eventTime*/, MOVE, {{1, 3}}, TOUCHSCREEN); + assertNotBlocked(args); + + args = generateMotionArgs(0 /*downTime*/, 2 /*eventTime*/, UP, {{1, 3}}, TOUCHSCREEN); + assertNotBlocked(args); +} + +TEST_F(PreferStylusOverTouchTest, StylusGestureIsNotBlocked) { + NotifyMotionArgs args; + + args = generateMotionArgs(0 /*downTime*/, 0 /*eventTime*/, DOWN, {{1, 2}}, STYLUS); + assertNotBlocked(args); + + args = generateMotionArgs(0 /*downTime*/, 1 /*eventTime*/, MOVE, {{1, 3}}, STYLUS); + assertNotBlocked(args); + + args = generateMotionArgs(0 /*downTime*/, 2 /*eventTime*/, UP, {{1, 3}}, STYLUS); + assertNotBlocked(args); +} + +/** + * Existing touch gesture should be canceled when stylus goes down. There should be an ACTION_CANCEL + * event generated. + */ +TEST_F(PreferStylusOverTouchTest, TouchIsCanceledWhenStylusGoesDown) { + NotifyMotionArgs args; + + args = generateMotionArgs(0 /*downTime*/, 0 /*eventTime*/, DOWN, {{1, 2}}, TOUCHSCREEN); + assertNotBlocked(args); + + args = generateMotionArgs(0 /*downTime*/, 1 /*eventTime*/, MOVE, {{1, 3}}, TOUCHSCREEN); + assertNotBlocked(args); + + args = generateMotionArgs(3 /*downTime*/, 3 /*eventTime*/, DOWN, {{10, 30}}, STYLUS); + NotifyMotionArgs cancelArgs = + generateMotionArgs(0 /*downTime*/, 1 /*eventTime*/, CANCEL, {{1, 3}}, TOUCHSCREEN); + assertCanceled(args, cancelArgs); + + // Both stylus and touch events continue. Stylus should be not blocked, and touch should be + // blocked + args = generateMotionArgs(3 /*downTime*/, 4 /*eventTime*/, MOVE, {{10, 31}}, STYLUS); + assertNotBlocked(args); + + args = generateMotionArgs(0 /*downTime*/, 5 /*eventTime*/, MOVE, {{1, 4}}, TOUCHSCREEN); + assertDropped(args); +} + +/** + * New touch events should be simply blocked (dropped) when stylus is down. No CANCEL event should + * be generated. + */ +TEST_F(PreferStylusOverTouchTest, NewTouchIsBlockedWhenStylusIsDown) { + NotifyMotionArgs args; + constexpr nsecs_t stylusDownTime = 0; + constexpr nsecs_t touchDownTime = 1; + + args = generateMotionArgs(stylusDownTime, 0 /*eventTime*/, DOWN, {{10, 30}}, STYLUS); + assertNotBlocked(args); + + args = generateMotionArgs(touchDownTime, 1 /*eventTime*/, DOWN, {{1, 2}}, TOUCHSCREEN); + assertDropped(args); + + // Stylus should continue to work + args = generateMotionArgs(stylusDownTime, 2 /*eventTime*/, MOVE, {{10, 31}}, STYLUS); + assertNotBlocked(args); + + // Touch should continue to be blocked + args = generateMotionArgs(touchDownTime, 1 /*eventTime*/, MOVE, {{1, 3}}, TOUCHSCREEN); + assertDropped(args); + + args = generateMotionArgs(0 /*downTime*/, 5 /*eventTime*/, MOVE, {{1, 4}}, TOUCHSCREEN); + assertDropped(args); +} + +/** + * New touch events should be simply blocked (dropped) when stylus is down. No CANCEL event should + * be generated. + */ +TEST_F(PreferStylusOverTouchTest, NewTouchWorksAfterStylusIsLifted) { + NotifyMotionArgs args; + constexpr nsecs_t stylusDownTime = 0; + constexpr nsecs_t touchDownTime = 4; + + // Stylus goes down and up + args = generateMotionArgs(stylusDownTime, 0 /*eventTime*/, DOWN, {{10, 30}}, STYLUS); + assertNotBlocked(args); + + args = generateMotionArgs(stylusDownTime, 2 /*eventTime*/, MOVE, {{10, 31}}, STYLUS); + assertNotBlocked(args); + + args = generateMotionArgs(stylusDownTime, 3 /*eventTime*/, UP, {{10, 31}}, STYLUS); + assertNotBlocked(args); + + // New touch goes down. It should not be blocked + args = generateMotionArgs(touchDownTime, touchDownTime, DOWN, {{1, 2}}, TOUCHSCREEN); + assertNotBlocked(args); + + args = generateMotionArgs(touchDownTime, 5 /*eventTime*/, MOVE, {{1, 3}}, TOUCHSCREEN); + assertNotBlocked(args); + + args = generateMotionArgs(touchDownTime, 6 /*eventTime*/, UP, {{1, 3}}, TOUCHSCREEN); + assertNotBlocked(args); +} + +/** + * Once a touch gesture is canceled, it should continue to be canceled, even if the stylus has been + * lifted. + */ +TEST_F(PreferStylusOverTouchTest, AfterStylusIsLiftedCurrentTouchIsBlocked) { + NotifyMotionArgs args; + constexpr nsecs_t stylusDownTime = 0; + constexpr nsecs_t touchDownTime = 1; + + assertNotBlocked(generateMotionArgs(stylusDownTime, 0 /*eventTime*/, DOWN, {{10, 30}}, STYLUS)); + + args = generateMotionArgs(touchDownTime, 1 /*eventTime*/, DOWN, {{1, 2}}, TOUCHSCREEN); + assertDropped(args); + + // Lift the stylus + args = generateMotionArgs(stylusDownTime, 2 /*eventTime*/, UP, {{10, 30}}, STYLUS); + assertNotBlocked(args); + + // Touch should continue to be blocked + args = generateMotionArgs(touchDownTime, 3 /*eventTime*/, MOVE, {{1, 3}}, TOUCHSCREEN); + assertDropped(args); + + args = generateMotionArgs(touchDownTime, 4 /*eventTime*/, UP, {{1, 3}}, TOUCHSCREEN); + assertDropped(args); + + // New touch should go through, though. + constexpr nsecs_t newTouchDownTime = 5; + args = generateMotionArgs(newTouchDownTime, 5 /*eventTime*/, DOWN, {{10, 20}}, TOUCHSCREEN); + assertNotBlocked(args); +} + +} // namespace android -- cgit v1.2.3-59-g8ed1b From e844e01f78f98d54676d07f3a44f46ae410b9b7b Mon Sep 17 00:00:00 2001 From: Siarhei Vishniakou Date: Tue, 8 Mar 2022 11:06:34 -0800 Subject: Remove PreferStylusOverTouchBlocker To prevent crash on emulator, let's disable PreferStylusOverTouchBlocker. A proper solution might require a re-work of how it operates. Test: atest inputflinger_tests Bug: 222531989 Change-Id: I5481e6baf284be8f254b6caec45be0478f1fe4a7 (cherry picked from commit 2756839ae9e898d31b138be0f4e7151b76288abb) --- services/inputflinger/UnwantedInteractionBlocker.cpp | 10 ---------- services/inputflinger/UnwantedInteractionBlocker.h | 6 ------ .../inputflinger/tests/UnwantedInteractionBlocker_test.cpp | 8 ++++++++ 3 files changed, 8 insertions(+), 16 deletions(-) (limited to 'services/inputflinger/UnwantedInteractionBlocker.cpp') diff --git a/services/inputflinger/UnwantedInteractionBlocker.cpp b/services/inputflinger/UnwantedInteractionBlocker.cpp index f904bfd82e..fb3962e544 100644 --- a/services/inputflinger/UnwantedInteractionBlocker.cpp +++ b/services/inputflinger/UnwantedInteractionBlocker.cpp @@ -368,14 +368,6 @@ void UnwantedInteractionBlocker::notifyKey(const NotifyKeyArgs* args) { } void UnwantedInteractionBlocker::notifyMotion(const NotifyMotionArgs* args) { - ftl::StaticVector processedArgs = - mPreferStylusOverTouchBlocker.processMotion(*args); - for (const NotifyMotionArgs& loopArgs : processedArgs) { - notifyMotionInner(&loopArgs); - } -} - -void UnwantedInteractionBlocker::notifyMotionInner(const NotifyMotionArgs* args) { auto it = mPalmRejectors.find(args->deviceId); const bool sendToPalmRejector = it != mPalmRejectors.end() && isFromTouchscreen(args->source); if (!sendToPalmRejector) { @@ -449,8 +441,6 @@ void UnwantedInteractionBlocker::notifyInputDevicesChanged( void UnwantedInteractionBlocker::dump(std::string& dump) { dump += "UnwantedInteractionBlocker:\n"; - dump += " mPreferStylusOverTouchBlocker:\n"; - dump += addPrefix(mPreferStylusOverTouchBlocker.dump(), " "); dump += StringPrintf(" mEnablePalmRejection: %s\n", toString(mEnablePalmRejection)); dump += StringPrintf(" isPalmRejectionEnabled (flag value): %s\n", toString(isPalmRejectionEnabled())); diff --git a/services/inputflinger/UnwantedInteractionBlocker.h b/services/inputflinger/UnwantedInteractionBlocker.h index 9e41995ac4..14068fd878 100644 --- a/services/inputflinger/UnwantedInteractionBlocker.h +++ b/services/inputflinger/UnwantedInteractionBlocker.h @@ -23,8 +23,6 @@ #include "ui/events/ozone/evdev/touch_filter/neural_stylus_palm_detection_filter_util.h" #include "ui/events/ozone/evdev/touch_filter/palm_detection_filter.h" -#include "PreferStylusOverTouchBlocker.h" - namespace android { // --- Functions for manipulation of event streams @@ -90,13 +88,9 @@ private: InputListenerInterface& mListener; const bool mEnablePalmRejection; - // When stylus is down, ignore touch - PreferStylusOverTouchBlocker mPreferStylusOverTouchBlocker; // Detect and reject unwanted palms on screen // Use a separate palm rejector for every touch device. std::map mPalmRejectors; - // TODO(b/210159205): delete this when simultaneous stylus and touch is supported - void notifyMotionInner(const NotifyMotionArgs* args); }; class SlotState { diff --git a/services/inputflinger/tests/UnwantedInteractionBlocker_test.cpp b/services/inputflinger/tests/UnwantedInteractionBlocker_test.cpp index a3220cc63a..b2f8eb37f0 100644 --- a/services/inputflinger/tests/UnwantedInteractionBlocker_test.cpp +++ b/services/inputflinger/tests/UnwantedInteractionBlocker_test.cpp @@ -490,6 +490,14 @@ TEST_F(UnwantedInteractionBlockerTest, NoCrashWhenResetHappens) { &(args = generateMotionArgs(0 /*downTime*/, 4 /*eventTime*/, DOWN, {{7, 8, 9}}))); } +TEST_F(UnwantedInteractionBlockerTest, NoCrashWhenStylusSourceWithFingerToolIsReceived) { + mBlocker->notifyInputDevicesChanged({generateTestDeviceInfo()}); + NotifyMotionArgs args = generateMotionArgs(0 /*downTime*/, 1 /*eventTime*/, DOWN, {{1, 2, 3}}); + args.pointerProperties[0].toolType = AMOTION_EVENT_TOOL_TYPE_FINGER; + args.source = AINPUT_SOURCE_STYLUS; + mBlocker->notifyMotion(&args); +} + /** * If input devices have changed, but the important device info that's used by the * UnwantedInteractionBlocker has not changed, there should not be a reset. -- cgit v1.2.3-59-g8ed1b From 814ace3f256109b59ee9dc1d881887c4cb8b17ae Mon Sep 17 00:00:00 2001 From: Siarhei Vishniakou Date: Fri, 4 Mar 2022 15:12:16 -0800 Subject: Add PreferStylusOverTouchBlocker and handle multiple devices We removed PreferStylusOverTouchBlocker previously in order to avoid a crash. In this CL, we are adding it back in, and handling the case of input device having "SOURCE_STYLUS", but reporting "finger" tool type. If there's a stylus event with one of the pointers labeled as 'finger', let's assume that the device supports simultaneous touch and stylus. For this situation, simply disable PreferStylusOverTouchBlocker going forward for these devices, and pass through any events coming from there. Currently, this happens on emulator. In their touch driver, they configure stylus properties as well as touch properties, but most of the events that they send are TOOL_TYPE_FINGER. Previously, this triggered a crash in PreferStylusOverTouchBlocker. Bug: 222531989 Test: atest inputflinger_tests Change-Id: Ifbb08858a4dfebc95c30ca19d6e68533855db7e4 (cherry picked from commit a6a660fc0aa74ea4f5930b74523cf1893b2f9282) --- include/input/PrintTools.h | 61 +++++ libs/input/Android.bp | 4 + libs/input/PrintTools.cpp | 27 ++ services/inputflinger/InputListener.cpp | 6 +- .../inputflinger/PreferStylusOverTouchBlocker.cpp | 207 +++++++++++---- .../inputflinger/PreferStylusOverTouchBlocker.h | 62 ++--- .../inputflinger/UnwantedInteractionBlocker.cpp | 12 + services/inputflinger/UnwantedInteractionBlocker.h | 7 + .../tests/PreferStylusOverTouch_test.cpp | 294 +++++++++++++++++++-- .../tests/UnwantedInteractionBlocker_test.cpp | 28 ++ 10 files changed, 597 insertions(+), 111 deletions(-) create mode 100644 include/input/PrintTools.h create mode 100644 libs/input/PrintTools.cpp (limited to 'services/inputflinger/UnwantedInteractionBlocker.cpp') diff --git a/include/input/PrintTools.h b/include/input/PrintTools.h new file mode 100644 index 0000000000..7c3b29b55f --- /dev/null +++ b/include/input/PrintTools.h @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2022 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 +#include +#include + +namespace android { + +template +std::string constToString(const T& v) { + return std::to_string(v); +} + +/** + * Convert a set of integral types to string. + */ +template +std::string dumpSet(const std::set& v, std::string (*toString)(const T&) = constToString) { + std::string out; + for (const T& entry : v) { + out += out.empty() ? "{" : ", "; + out += toString(entry); + } + return out.empty() ? "{}" : (out + "}"); +} + +/** + * Convert a map to string. Both keys and values of the map should be integral type. + */ +template +std::string dumpMap(const std::map& map, std::string (*keyToString)(const K&) = constToString, + std::string (*valueToString)(const V&) = constToString) { + std::string out; + for (const auto& [k, v] : map) { + if (!out.empty()) { + out += "\n"; + } + out += keyToString(k) + ":" + valueToString(v); + } + return out; +} + +const char* toString(bool value); + +} // namespace android \ No newline at end of file diff --git a/libs/input/Android.bp b/libs/input/Android.bp index 930d8194d5..b188ea2921 100644 --- a/libs/input/Android.bp +++ b/libs/input/Android.bp @@ -49,6 +49,7 @@ cc_library { "Keyboard.cpp", "KeyCharacterMap.cpp", "KeyLayoutMap.cpp", + "PrintTools.cpp", "PropertyMap.cpp", "TouchVideoFrame.cpp", "VelocityControl.cpp", @@ -104,6 +105,9 @@ cc_library { sanitize: { misc_undefined: ["integer"], + diag: { + misc_undefined: ["integer"], + }, }, }, host: { diff --git a/libs/input/PrintTools.cpp b/libs/input/PrintTools.cpp new file mode 100644 index 0000000000..5d6ae4ed91 --- /dev/null +++ b/libs/input/PrintTools.cpp @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2022 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. + */ + +#define LOG_TAG "PrintTools" + +#include + +namespace android { + +const char* toString(bool value) { + return value ? "true" : "false"; +} + +} // namespace android diff --git a/services/inputflinger/InputListener.cpp b/services/inputflinger/InputListener.cpp index 73b63e3141..48cc413098 100644 --- a/services/inputflinger/InputListener.cpp +++ b/services/inputflinger/InputListener.cpp @@ -202,9 +202,11 @@ std::string NotifyMotionArgs::dump() const { coords += "}"; } return StringPrintf("NotifyMotionArgs(id=%" PRId32 ", eventTime=%" PRId64 ", deviceId=%" PRId32 - ", source=%s, action=%s, pointerCount=%" PRIu32 " pointers=%s)", + ", source=%s, action=%s, pointerCount=%" PRIu32 + " pointers=%s, flags=0x%08x)", id, eventTime, deviceId, inputEventSourceToString(source).c_str(), - MotionEvent::actionToString(action).c_str(), pointerCount, coords.c_str()); + MotionEvent::actionToString(action).c_str(), pointerCount, coords.c_str(), + flags); } void NotifyMotionArgs::notify(InputListenerInterface& listener) const { diff --git a/services/inputflinger/PreferStylusOverTouchBlocker.cpp b/services/inputflinger/PreferStylusOverTouchBlocker.cpp index ad639b4ef8..beec2e162e 100644 --- a/services/inputflinger/PreferStylusOverTouchBlocker.cpp +++ b/services/inputflinger/PreferStylusOverTouchBlocker.cpp @@ -15,78 +15,163 @@ */ #include "PreferStylusOverTouchBlocker.h" +#include -#include +namespace android { -using android::base::StringPrintf; +static std::pair checkToolType(const NotifyMotionArgs& args) { + bool hasStylus = false; + bool hasTouch = false; + for (size_t i = 0; i < args.pointerCount; i++) { + // Make sure we are canceling stylus pointers + const int32_t toolType = args.pointerProperties[i].toolType; + if (toolType == AMOTION_EVENT_TOOL_TYPE_STYLUS || + toolType == AMOTION_EVENT_TOOL_TYPE_ERASER) { + hasStylus = true; + } + if (toolType == AMOTION_EVENT_TOOL_TYPE_FINGER) { + hasTouch = true; + } + } + return std::make_pair(hasTouch, hasStylus); +} -static const char* toString(bool value) { - return value ? "true" : "false"; +/** + * Intersect two sets in-place, storing the result in 'set1'. + * Find elements in set1 that are not present in set2 and delete them, + * relying on the fact that the two sets are ordered. + */ +template +static void intersectInPlace(std::set& set1, const std::set& set2) { + typename std::set::iterator it1 = set1.begin(); + typename std::set::const_iterator it2 = set2.begin(); + while (it1 != set1.end() && it2 != set2.end()) { + const T& element1 = *it1; + const T& element2 = *it2; + if (element1 < element2) { + // This element is not present in set2. Remove it from set1. + it1 = set1.erase(it1); + continue; + } + if (element2 < element1) { + it2++; + } + if (element1 == element2) { + it1++; + it2++; + } + } + // Remove the rest of the elements in set1 because set2 is already exhausted. + set1.erase(it1, set1.end()); } -namespace android { +/** + * Same as above, but prune a map + */ +template +static void intersectInPlace(std::map& map, const std::set& set2) { + typename std::map::iterator it1 = map.begin(); + typename std::set::const_iterator it2 = set2.begin(); + while (it1 != map.end() && it2 != set2.end()) { + const auto& [key, _] = *it1; + const K& element2 = *it2; + if (key < element2) { + // This element is not present in set2. Remove it from map. + it1 = map.erase(it1); + continue; + } + if (element2 < key) { + it2++; + } + if (key == element2) { + it1++; + it2++; + } + } + // Remove the rest of the elements in map because set2 is already exhausted. + map.erase(it1, map.end()); +} + +// -------------------------------- PreferStylusOverTouchBlocker ----------------------------------- -ftl::StaticVector PreferStylusOverTouchBlocker::processMotion( +std::vector PreferStylusOverTouchBlocker::processMotion( const NotifyMotionArgs& args) { - const bool isStylusEvent = isFromSource(args.source, AINPUT_SOURCE_STYLUS); - if (isStylusEvent) { - for (size_t i = 0; i < args.pointerCount; i++) { - // Make sure we are canceling stylus pointers - const int32_t toolType = args.pointerProperties[i].toolType; - LOG_ALWAYS_FATAL_IF(toolType != AMOTION_EVENT_TOOL_TYPE_STYLUS && - toolType != AMOTION_EVENT_TOOL_TYPE_ERASER, - "The pointer %zu has toolType=%i, but the source is STYLUS. If " - "simultaneous touch and stylus is supported, " - "'PreferStylusOverTouchBlocker' should be disabled.", - i, toolType); + const auto [hasTouch, hasStylus] = checkToolType(args); + const bool isUpOrCancel = + args.action == AMOTION_EVENT_ACTION_UP || args.action == AMOTION_EVENT_ACTION_CANCEL; + + if (hasTouch && hasStylus) { + mDevicesWithMixedToolType.insert(args.deviceId); + } + // Handle the case where mixed touch and stylus pointers are reported. Add this device to the + // ignore list, since it clearly supports simultaneous touch and stylus. + if (mDevicesWithMixedToolType.find(args.deviceId) != mDevicesWithMixedToolType.end()) { + // This event comes from device with mixed stylus and touch event. Ignore this device. + if (mCanceledDevices.find(args.deviceId) != mCanceledDevices.end()) { + // If we started to cancel events from this device, continue to do so to keep + // the stream consistent. It should happen at most once per "mixed" device. + if (isUpOrCancel) { + mCanceledDevices.erase(args.deviceId); + mLastTouchEvents.erase(args.deviceId); + } + return {}; } + return {args}; } + + const bool isStylusEvent = hasStylus; const bool isDown = args.action == AMOTION_EVENT_ACTION_DOWN; - const bool isUpOrCancel = - args.action == AMOTION_EVENT_ACTION_UP || args.action == AMOTION_EVENT_ACTION_CANCEL; + if (isStylusEvent) { if (isDown) { // Reject all touch while stylus is down - mIsStylusDown = true; - if (mIsTouchDown && !mCurrentTouchIsCanceled) { - // Cancel touch! - mCurrentTouchIsCanceled = true; - mLastTouchEvent.action = AMOTION_EVENT_ACTION_CANCEL; - mLastTouchEvent.flags |= AMOTION_EVENT_FLAG_CANCELED; - mLastTouchEvent.eventTime = systemTime(SYSTEM_TIME_MONOTONIC); - return {mLastTouchEvent, args}; + mActiveStyli.insert(args.deviceId); + + // Cancel all current touch! + std::vector result; + for (auto& [deviceId, lastTouchEvent] : mLastTouchEvents) { + if (mCanceledDevices.find(deviceId) != mCanceledDevices.end()) { + // Already canceled, go to next one. + continue; + } + // Not yet canceled. Cancel it. + lastTouchEvent.action = AMOTION_EVENT_ACTION_CANCEL; + lastTouchEvent.flags |= AMOTION_EVENT_FLAG_CANCELED; + lastTouchEvent.eventTime = systemTime(SYSTEM_TIME_MONOTONIC); + result.push_back(lastTouchEvent); + mCanceledDevices.insert(deviceId); } + result.push_back(args); + return result; } if (isUpOrCancel) { - mIsStylusDown = false; + mActiveStyli.erase(args.deviceId); } // Never drop stylus events return {args}; } - const bool isTouchEvent = - isFromSource(args.source, AINPUT_SOURCE_TOUCHSCREEN) && !isStylusEvent; + const bool isTouchEvent = hasTouch; if (isTouchEvent) { - if (mIsStylusDown) { - mCurrentTouchIsCanceled = true; + // Suppress the current gesture if any stylus is still down + if (!mActiveStyli.empty()) { + mCanceledDevices.insert(args.deviceId); + } + + const bool shouldDrop = mCanceledDevices.find(args.deviceId) != mCanceledDevices.end(); + if (isUpOrCancel) { + mCanceledDevices.erase(args.deviceId); + mLastTouchEvents.erase(args.deviceId); } + // If we already canceled the current gesture, then continue to drop events from it, even if // the stylus has been lifted. - if (mCurrentTouchIsCanceled) { - if (isUpOrCancel) { - mCurrentTouchIsCanceled = false; - } + if (shouldDrop) { return {}; } - // Update state - mLastTouchEvent = args; - if (isDown) { - mIsTouchDown = true; - } - if (isUpOrCancel) { - mIsTouchDown = false; - mCurrentTouchIsCanceled = false; + if (!isUpOrCancel) { + mLastTouchEvents[args.deviceId] = args; } return {args}; } @@ -95,12 +180,36 @@ ftl::StaticVector PreferStylusOverTouchBlocker::processMoti return {args}; } -std::string PreferStylusOverTouchBlocker::dump() { +void PreferStylusOverTouchBlocker::notifyInputDevicesChanged( + const std::vector& inputDevices) { + std::set presentDevices; + for (const InputDeviceInfo& device : inputDevices) { + presentDevices.insert(device.getId()); + } + // Only keep the devices that are still present. + intersectInPlace(mDevicesWithMixedToolType, presentDevices); + intersectInPlace(mLastTouchEvents, presentDevices); + intersectInPlace(mCanceledDevices, presentDevices); + intersectInPlace(mActiveStyli, presentDevices); +} + +void PreferStylusOverTouchBlocker::notifyDeviceReset(const NotifyDeviceResetArgs& args) { + mDevicesWithMixedToolType.erase(args.deviceId); + mLastTouchEvents.erase(args.deviceId); + mCanceledDevices.erase(args.deviceId); + mActiveStyli.erase(args.deviceId); +} + +static std::string dumpArgs(const NotifyMotionArgs& args) { + return args.dump(); +} + +std::string PreferStylusOverTouchBlocker::dump() const { std::string out; - out += StringPrintf("mIsTouchDown: %s\n", toString(mIsTouchDown)); - out += StringPrintf("mIsStylusDown: %s\n", toString(mIsStylusDown)); - out += StringPrintf("mLastTouchEvent: %s\n", mLastTouchEvent.dump().c_str()); - out += StringPrintf("mCurrentTouchIsCanceled: %s\n", toString(mCurrentTouchIsCanceled)); + out += "mActiveStyli: " + dumpSet(mActiveStyli) + "\n"; + out += "mLastTouchEvents: " + dumpMap(mLastTouchEvents, constToString, dumpArgs) + "\n"; + out += "mDevicesWithMixedToolType: " + dumpSet(mDevicesWithMixedToolType) + "\n"; + out += "mCanceledDevices: " + dumpSet(mCanceledDevices) + "\n"; return out; } diff --git a/services/inputflinger/PreferStylusOverTouchBlocker.h b/services/inputflinger/PreferStylusOverTouchBlocker.h index 3f5616190b..716dc4d351 100644 --- a/services/inputflinger/PreferStylusOverTouchBlocker.h +++ b/services/inputflinger/PreferStylusOverTouchBlocker.h @@ -16,66 +16,50 @@ #pragma once -#include #include +#include #include "InputListener.h" namespace android { /** - * When stylus is down, we ignore all touch. + * When stylus is down, all touch is ignored. * TODO(b/210159205): delete this when simultaneous stylus and touch is supported */ class PreferStylusOverTouchBlocker { public: /** - * Process the provided event and emit up to 2 events in response. + * Process the provided event and emit 0 or more events that should be used instead of it. * In the majority of cases, the returned result will just be the provided args (array with * only 1 element), unmodified. * * If the gesture should be blocked, the returned result may be: * * a) An empty array, if the current event should just be ignored completely - * b) An array of 2 elements, containing an event with ACTION_CANCEL and the current event. + * b) An array of N elements, containing N-1 events with ACTION_CANCEL and the current event. * - * bool is set to 'true'. - * NotifyMotionArgs potentially contains an event that should be used to cancel the existing - * gesture. - * - * If the event should not be blocked, bool contains 'false'. + * The returned result is intended to be reinjected into the original event stream in + * replacement of the incoming event. */ - ftl::StaticVector processMotion(const NotifyMotionArgs& args); - std::string dump(); + std::vector processMotion(const NotifyMotionArgs& args); + std::string dump() const; + + void notifyInputDevicesChanged(const std::vector& inputDevices); + + void notifyDeviceReset(const NotifyDeviceResetArgs& args); private: - bool mIsTouchDown = false; - bool mIsStylusDown = false; - // Provide some default values for the stored MotionEvent to allow printint the event before - // any real event is received. - NotifyMotionArgs mLastTouchEvent{0 /*id*/, - 0 /*eventTime*/, - 0 /*readTime*/, - 0 /*deviceId*/, - AINPUT_SOURCE_TOUCHSCREEN, - 0 /*displayId*/, - 0 /*policyFlags*/, - 0 /*action*/, - 0 /*actionButton*/, - 0 /*flags*/, - 0 /*metaState*/, - 0 /*buttonState*/, - MotionClassification::NONE, - AMOTION_EVENT_EDGE_FLAG_NONE, - 0 /*pointerCount*/, - nullptr /*properties*/, - nullptr /*coords*/, - 0. /*xPrecision*/, - 0. /*yPrecision*/, - AMOTION_EVENT_INVALID_CURSOR_POSITION, - AMOTION_EVENT_INVALID_CURSOR_POSITION, - 0 /*downTime*/, - {}}; - bool mCurrentTouchIsCanceled = false; + // Stores the device id's of styli that are currently down. + std::set mActiveStyli; + // For each device, store the last touch event as long as the touch is down. Upon liftoff, + // the entry is erased. + std::map mLastTouchEvents; + // Device ids of devices for which the current touch gesture is canceled. + std::set mCanceledDevices; + + // Device ids of input devices where we encountered simultaneous touch and stylus + // events. For these devices, we don't do any event processing (nothing is blocked or altered). + std::set mDevicesWithMixedToolType; }; } // namespace android \ No newline at end of file diff --git a/services/inputflinger/UnwantedInteractionBlocker.cpp b/services/inputflinger/UnwantedInteractionBlocker.cpp index fb3962e544..b69e16ac85 100644 --- a/services/inputflinger/UnwantedInteractionBlocker.cpp +++ b/services/inputflinger/UnwantedInteractionBlocker.cpp @@ -368,6 +368,14 @@ void UnwantedInteractionBlocker::notifyKey(const NotifyKeyArgs* args) { } void UnwantedInteractionBlocker::notifyMotion(const NotifyMotionArgs* args) { + const std::vector processedArgs = + mPreferStylusOverTouchBlocker.processMotion(*args); + for (const NotifyMotionArgs& loopArgs : processedArgs) { + notifyMotionInner(&loopArgs); + } +} + +void UnwantedInteractionBlocker::notifyMotionInner(const NotifyMotionArgs* args) { auto it = mPalmRejectors.find(args->deviceId); const bool sendToPalmRejector = it != mPalmRejectors.end() && isFromTouchscreen(args->source); if (!sendToPalmRejector) { @@ -401,6 +409,7 @@ void UnwantedInteractionBlocker::notifyDeviceReset(const NotifyDeviceResetArgs* mPalmRejectors.emplace(args->deviceId, info); } mListener.notifyDeviceReset(args); + mPreferStylusOverTouchBlocker.notifyDeviceReset(*args); } void UnwantedInteractionBlocker::notifyPointerCaptureChanged( @@ -437,10 +446,13 @@ void UnwantedInteractionBlocker::notifyInputDevicesChanged( auto const& [deviceId, _] = item; return devicesToKeep.find(deviceId) == devicesToKeep.end(); }); + mPreferStylusOverTouchBlocker.notifyInputDevicesChanged(inputDevices); } void UnwantedInteractionBlocker::dump(std::string& dump) { dump += "UnwantedInteractionBlocker:\n"; + dump += " mPreferStylusOverTouchBlocker:\n"; + dump += addPrefix(mPreferStylusOverTouchBlocker.dump(), " "); dump += StringPrintf(" mEnablePalmRejection: %s\n", toString(mEnablePalmRejection)); dump += StringPrintf(" isPalmRejectionEnabled (flag value): %s\n", toString(isPalmRejectionEnabled())); diff --git a/services/inputflinger/UnwantedInteractionBlocker.h b/services/inputflinger/UnwantedInteractionBlocker.h index 14068fd878..8a1cd7265e 100644 --- a/services/inputflinger/UnwantedInteractionBlocker.h +++ b/services/inputflinger/UnwantedInteractionBlocker.h @@ -23,6 +23,8 @@ #include "ui/events/ozone/evdev/touch_filter/neural_stylus_palm_detection_filter_util.h" #include "ui/events/ozone/evdev/touch_filter/palm_detection_filter.h" +#include "PreferStylusOverTouchBlocker.h" + namespace android { // --- Functions for manipulation of event streams @@ -88,9 +90,14 @@ private: InputListenerInterface& mListener; const bool mEnablePalmRejection; + // When stylus is down, ignore touch + PreferStylusOverTouchBlocker mPreferStylusOverTouchBlocker; + // Detect and reject unwanted palms on screen // Use a separate palm rejector for every touch device. std::map mPalmRejectors; + // TODO(b/210159205): delete this when simultaneous stylus and touch is supported + void notifyMotionInner(const NotifyMotionArgs* args); }; class SlotState { diff --git a/services/inputflinger/tests/PreferStylusOverTouch_test.cpp b/services/inputflinger/tests/PreferStylusOverTouch_test.cpp index 70f40aa028..8e2ab88e80 100644 --- a/services/inputflinger/tests/PreferStylusOverTouch_test.cpp +++ b/services/inputflinger/tests/PreferStylusOverTouch_test.cpp @@ -20,12 +20,16 @@ namespace android { constexpr int32_t TOUCH_DEVICE_ID = 3; -constexpr int32_t STYLUS_DEVICE_ID = 4; +constexpr int32_t SECOND_TOUCH_DEVICE_ID = 4; +constexpr int32_t STYLUS_DEVICE_ID = 5; +constexpr int32_t SECOND_STYLUS_DEVICE_ID = 6; constexpr int DOWN = AMOTION_EVENT_ACTION_DOWN; constexpr int MOVE = AMOTION_EVENT_ACTION_MOVE; constexpr int UP = AMOTION_EVENT_ACTION_UP; constexpr int CANCEL = AMOTION_EVENT_ACTION_CANCEL; +static constexpr int32_t POINTER_1_DOWN = + AMOTION_EVENT_ACTION_POINTER_DOWN | (1 << AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT); constexpr int32_t TOUCHSCREEN = AINPUT_SOURCE_TOUCHSCREEN; constexpr int32_t STYLUS = AINPUT_SOURCE_STYLUS; @@ -78,29 +82,30 @@ static NotifyMotionArgs generateMotionArgs(nsecs_t downTime, nsecs_t eventTime, class PreferStylusOverTouchTest : public testing::Test { protected: - void assertNotBlocked(const NotifyMotionArgs& args) { - ftl::StaticVector processedArgs = mBlocker.processMotion(args); - ASSERT_EQ(1u, processedArgs.size()); - ASSERT_EQ(args, processedArgs[0]); + void assertNotBlocked(const NotifyMotionArgs& args) { assertResponse(args, {args}); } + + void assertDropped(const NotifyMotionArgs& args) { assertResponse(args, {}); } + + void assertResponse(const NotifyMotionArgs& args, + const std::vector& expected) { + std::vector receivedArgs = mBlocker.processMotion(args); + ASSERT_EQ(expected.size(), receivedArgs.size()); + for (size_t i = 0; i < expected.size(); i++) { + // The 'eventTime' of CANCEL events is dynamically generated. Don't check this field. + if (expected[i].action == CANCEL && receivedArgs[i].action == CANCEL) { + receivedArgs[i].eventTime = expected[i].eventTime; + } + + ASSERT_EQ(expected[i], receivedArgs[i]) + << expected[i].dump() << " vs " << receivedArgs[i].dump(); + } } - void assertDropped(const NotifyMotionArgs& args) { - ftl::StaticVector processedArgs = mBlocker.processMotion(args); - ASSERT_TRUE(processedArgs.empty()); + void notifyInputDevicesChanged(const std::vector& devices) { + mBlocker.notifyInputDevicesChanged(devices); } - void assertCanceled(const NotifyMotionArgs& args, - std::optional canceledArgs) { - ftl::StaticVector processedArgs = mBlocker.processMotion(args); - ASSERT_EQ(2u, processedArgs.size()); - NotifyMotionArgs& cancelEvent = processedArgs[0]; - ASSERT_EQ(CANCEL, cancelEvent.action); - ASSERT_EQ(AMOTION_EVENT_FLAG_CANCELED, cancelEvent.flags & AMOTION_EVENT_FLAG_CANCELED); - ASSERT_TRUE(isFromSource(cancelEvent.source, TOUCHSCREEN)); - ASSERT_FALSE(isFromSource(cancelEvent.source, STYLUS)); - - ASSERT_EQ(args, processedArgs[1]); - } + void dump() const { ALOGI("Blocker: \n%s\n", mBlocker.dump().c_str()); } private: PreferStylusOverTouchBlocker mBlocker; @@ -148,7 +153,8 @@ TEST_F(PreferStylusOverTouchTest, TouchIsCanceledWhenStylusGoesDown) { args = generateMotionArgs(3 /*downTime*/, 3 /*eventTime*/, DOWN, {{10, 30}}, STYLUS); NotifyMotionArgs cancelArgs = generateMotionArgs(0 /*downTime*/, 1 /*eventTime*/, CANCEL, {{1, 3}}, TOUCHSCREEN); - assertCanceled(args, cancelArgs); + cancelArgs.flags |= AMOTION_EVENT_FLAG_CANCELED; + assertResponse(args, {cancelArgs, args}); // Both stylus and touch events continue. Stylus should be not blocked, and touch should be // blocked @@ -159,6 +165,26 @@ TEST_F(PreferStylusOverTouchTest, TouchIsCanceledWhenStylusGoesDown) { assertDropped(args); } +/** + * Stylus goes down after touch gesture. + */ +TEST_F(PreferStylusOverTouchTest, StylusDownAfterTouch) { + NotifyMotionArgs args; + + args = generateMotionArgs(0 /*downTime*/, 0 /*eventTime*/, DOWN, {{1, 2}}, TOUCHSCREEN); + assertNotBlocked(args); + + args = generateMotionArgs(0 /*downTime*/, 1 /*eventTime*/, MOVE, {{1, 3}}, TOUCHSCREEN); + assertNotBlocked(args); + + args = generateMotionArgs(0 /*downTime*/, 2 /*eventTime*/, UP, {{1, 3}}, TOUCHSCREEN); + assertNotBlocked(args); + + // Stylus goes down + args = generateMotionArgs(3 /*downTime*/, 3 /*eventTime*/, DOWN, {{10, 30}}, STYLUS); + assertNotBlocked(args); +} + /** * New touch events should be simply blocked (dropped) when stylus is down. No CANCEL event should * be generated. @@ -247,4 +273,230 @@ TEST_F(PreferStylusOverTouchTest, AfterStylusIsLiftedCurrentTouchIsBlocked) { assertNotBlocked(args); } +/** + * If an event with mixed stylus and touch pointers is encountered, it should be ignored. Touches + * from such should pass, even if stylus from the same device goes down. + */ +TEST_F(PreferStylusOverTouchTest, MixedStylusAndTouchPointersAreIgnored) { + NotifyMotionArgs args; + + // Event from a stylus device, but with finger tool type + args = generateMotionArgs(1 /*downTime*/, 1 /*eventTime*/, DOWN, {{1, 2}}, STYLUS); + // Keep source stylus, but make the tool type touch + args.pointerProperties[0].toolType = AMOTION_EVENT_TOOL_TYPE_FINGER; + assertNotBlocked(args); + + // Second pointer (stylus pointer) goes down, from the same device + args = generateMotionArgs(1 /*downTime*/, 2 /*eventTime*/, POINTER_1_DOWN, {{1, 2}, {10, 20}}, + STYLUS); + // Keep source stylus, but make the tool type touch + args.pointerProperties[0].toolType = AMOTION_EVENT_TOOL_TYPE_STYLUS; + assertNotBlocked(args); + + // Second pointer (stylus pointer) goes down, from the same device + args = generateMotionArgs(1 /*downTime*/, 3 /*eventTime*/, MOVE, {{2, 3}, {11, 21}}, STYLUS); + // Keep source stylus, but make the tool type touch + args.pointerProperties[0].toolType = AMOTION_EVENT_TOOL_TYPE_FINGER; + assertNotBlocked(args); +} + +/** + * When there are two touch devices, stylus down should cancel all current touch streams. + */ +TEST_F(PreferStylusOverTouchTest, TouchFromTwoDevicesAndStylus) { + NotifyMotionArgs touch1Down = + generateMotionArgs(1 /*downTime*/, 1 /*eventTime*/, DOWN, {{1, 2}}, TOUCHSCREEN); + assertNotBlocked(touch1Down); + + NotifyMotionArgs touch2Down = + generateMotionArgs(2 /*downTime*/, 2 /*eventTime*/, DOWN, {{3, 4}}, TOUCHSCREEN); + touch2Down.deviceId = SECOND_TOUCH_DEVICE_ID; + assertNotBlocked(touch2Down); + + NotifyMotionArgs stylusDown = + generateMotionArgs(3 /*downTime*/, 3 /*eventTime*/, DOWN, {{10, 30}}, STYLUS); + NotifyMotionArgs cancelArgs1 = touch1Down; + cancelArgs1.action = CANCEL; + cancelArgs1.flags |= AMOTION_EVENT_FLAG_CANCELED; + NotifyMotionArgs cancelArgs2 = touch2Down; + cancelArgs2.action = CANCEL; + cancelArgs2.flags |= AMOTION_EVENT_FLAG_CANCELED; + assertResponse(stylusDown, {cancelArgs1, cancelArgs2, stylusDown}); +} + +/** + * Touch should be canceled when stylus goes down. After the stylus lifts up, the touch from that + * device should continue to be canceled. + * If one of the devices is already canceled, it should remain canceled, but new touches from a + * different device should go through. + */ +TEST_F(PreferStylusOverTouchTest, AllTouchMustLiftAfterCanceledByStylus) { + // First device touches down + NotifyMotionArgs touch1Down = + generateMotionArgs(1 /*downTime*/, 1 /*eventTime*/, DOWN, {{1, 2}}, TOUCHSCREEN); + assertNotBlocked(touch1Down); + + // Stylus goes down - touch should be canceled + NotifyMotionArgs stylusDown = + generateMotionArgs(2 /*downTime*/, 2 /*eventTime*/, DOWN, {{10, 30}}, STYLUS); + NotifyMotionArgs cancelArgs1 = touch1Down; + cancelArgs1.action = CANCEL; + cancelArgs1.flags |= AMOTION_EVENT_FLAG_CANCELED; + assertResponse(stylusDown, {cancelArgs1, stylusDown}); + + // Stylus goes up + NotifyMotionArgs stylusUp = + generateMotionArgs(2 /*downTime*/, 3 /*eventTime*/, UP, {{10, 30}}, STYLUS); + assertNotBlocked(stylusUp); + + // Touch from the first device remains blocked + NotifyMotionArgs touch1Move = + generateMotionArgs(1 /*downTime*/, 4 /*eventTime*/, MOVE, {{2, 3}}, TOUCHSCREEN); + assertDropped(touch1Move); + + // Second touch goes down. It should not be blocked because stylus has already lifted. + NotifyMotionArgs touch2Down = + generateMotionArgs(5 /*downTime*/, 5 /*eventTime*/, DOWN, {{31, 32}}, TOUCHSCREEN); + touch2Down.deviceId = SECOND_TOUCH_DEVICE_ID; + assertNotBlocked(touch2Down); + + // First device is lifted up. It's already been canceled, so the UP event should be dropped. + NotifyMotionArgs touch1Up = + generateMotionArgs(1 /*downTime*/, 6 /*eventTime*/, UP, {{2, 3}}, TOUCHSCREEN); + assertDropped(touch1Up); + + // Touch from second device touch should continue to work + NotifyMotionArgs touch2Move = + generateMotionArgs(5 /*downTime*/, 7 /*eventTime*/, MOVE, {{32, 33}}, TOUCHSCREEN); + touch2Move.deviceId = SECOND_TOUCH_DEVICE_ID; + assertNotBlocked(touch2Move); + + // Second touch lifts up + NotifyMotionArgs touch2Up = + generateMotionArgs(5 /*downTime*/, 8 /*eventTime*/, UP, {{32, 33}}, TOUCHSCREEN); + touch2Up.deviceId = SECOND_TOUCH_DEVICE_ID; + assertNotBlocked(touch2Up); + + // Now that all touch has been lifted, new touch from either first or second device should work + NotifyMotionArgs touch3Down = + generateMotionArgs(9 /*downTime*/, 9 /*eventTime*/, DOWN, {{1, 2}}, TOUCHSCREEN); + assertNotBlocked(touch3Down); + + NotifyMotionArgs touch4Down = + generateMotionArgs(10 /*downTime*/, 10 /*eventTime*/, DOWN, {{100, 200}}, TOUCHSCREEN); + touch4Down.deviceId = SECOND_TOUCH_DEVICE_ID; + assertNotBlocked(touch4Down); +} + +/** + * When we don't know that a specific device does both stylus and touch, and we only see touch + * pointers from it, we should treat it as a touch device. That means, the device events should be + * canceled when stylus from another device goes down. When we detect simultaneous touch and stylus + * from this device though, we should just pass this device through without canceling anything. + * + * In this test: + * 1. Start by touching down with device 1 + * 2. Device 2 has stylus going down + * 3. Device 1 should be canceled. + * 4. When we add stylus pointers to the device 1, they should continue to be canceled. + * 5. Device 1 lifts up. + * 6. Subsequent events from device 1 should not be canceled even if stylus is down. + * 7. If a reset happens, and such device is no longer there, then we should + * Therefore, the device 1 is "ignored" and does not participate into "prefer stylus over touch" + * behaviour. + */ +TEST_F(PreferStylusOverTouchTest, MixedStylusAndTouchDeviceIsCanceledAtFirst) { + // Touch from device 1 goes down + NotifyMotionArgs touchDown = + generateMotionArgs(1 /*downTime*/, 1 /*eventTime*/, DOWN, {{1, 2}}, TOUCHSCREEN); + touchDown.source = STYLUS; + assertNotBlocked(touchDown); + + // Stylus from device 2 goes down. Touch should be canceled. + NotifyMotionArgs args = + generateMotionArgs(2 /*downTime*/, 2 /*eventTime*/, DOWN, {{10, 20}}, STYLUS); + NotifyMotionArgs cancelTouchArgs = touchDown; + cancelTouchArgs.action = CANCEL; + cancelTouchArgs.flags |= AMOTION_EVENT_FLAG_CANCELED; + assertResponse(args, {cancelTouchArgs, args}); + + // Introduce a stylus pointer into the device 1 stream. It should be ignored. + args = generateMotionArgs(1 /*downTime*/, 3 /*eventTime*/, POINTER_1_DOWN, {{1, 2}, {3, 4}}, + TOUCHSCREEN); + args.pointerProperties[1].toolType = AMOTION_EVENT_TOOL_TYPE_STYLUS; + args.source = STYLUS; + assertDropped(args); + + // Lift up touch from the mixed touch/stylus device + args = generateMotionArgs(1 /*downTime*/, 4 /*eventTime*/, CANCEL, {{1, 2}, {3, 4}}, + TOUCHSCREEN); + args.pointerProperties[1].toolType = AMOTION_EVENT_TOOL_TYPE_STYLUS; + args.source = STYLUS; + assertDropped(args); + + // Stylus from device 2 is still down. Since the device 1 is now identified as a mixed + // touch/stylus device, its events should go through, even if they are touch. + args = generateMotionArgs(5 /*downTime*/, 5 /*eventTime*/, DOWN, {{21, 22}}, TOUCHSCREEN); + touchDown.source = STYLUS; + assertResponse(args, {args}); + + // Reconfigure such that only the stylus device remains + InputDeviceInfo stylusDevice; + stylusDevice.initialize(STYLUS_DEVICE_ID, 1 /*generation*/, 1 /*controllerNumber*/, + {} /*identifier*/, "stylus device", false /*external*/, + false /*hasMic*/); + notifyInputDevicesChanged({stylusDevice}); + // The touchscreen device was removed, so we no longer remember anything about it. We should + // again start blocking touch events from it. + args = generateMotionArgs(6 /*downTime*/, 6 /*eventTime*/, DOWN, {{1, 2}}, TOUCHSCREEN); + args.source = STYLUS; + assertDropped(args); +} + +/** + * If two styli are active at the same time, touch should be blocked until both of them are lifted. + * If one of them lifts, touch should continue to be blocked. + */ +TEST_F(PreferStylusOverTouchTest, TouchIsBlockedWhenTwoStyliAreUsed) { + NotifyMotionArgs args; + + // First stylus is down + assertNotBlocked(generateMotionArgs(0 /*downTime*/, 0 /*eventTime*/, DOWN, {{10, 30}}, STYLUS)); + + // Second stylus is down + args = generateMotionArgs(1 /*downTime*/, 1 /*eventTime*/, DOWN, {{20, 40}}, STYLUS); + args.deviceId = SECOND_STYLUS_DEVICE_ID; + assertNotBlocked(args); + + // Touch goes down. It should be ignored. + args = generateMotionArgs(2 /*downTime*/, 2 /*eventTime*/, DOWN, {{1, 2}}, TOUCHSCREEN); + assertDropped(args); + + // Lift the first stylus + args = generateMotionArgs(0 /*downTime*/, 3 /*eventTime*/, UP, {{10, 30}}, STYLUS); + assertNotBlocked(args); + + // Touch should continue to be blocked + args = generateMotionArgs(2 /*downTime*/, 4 /*eventTime*/, UP, {{1, 2}}, TOUCHSCREEN); + assertDropped(args); + + // New touch should be blocked because second stylus is still down + args = generateMotionArgs(5 /*downTime*/, 5 /*eventTime*/, DOWN, {{5, 6}}, TOUCHSCREEN); + assertDropped(args); + + // Second stylus goes up + args = generateMotionArgs(1 /*downTime*/, 6 /*eventTime*/, UP, {{20, 40}}, STYLUS); + args.deviceId = SECOND_STYLUS_DEVICE_ID; + assertNotBlocked(args); + + // Current touch gesture should continue to be blocked + // Touch should continue to be blocked + args = generateMotionArgs(5 /*downTime*/, 7 /*eventTime*/, UP, {{5, 6}}, TOUCHSCREEN); + assertDropped(args); + + // Now that all styli were lifted, new touch should go through + args = generateMotionArgs(8 /*downTime*/, 8 /*eventTime*/, DOWN, {{7, 8}}, TOUCHSCREEN); + assertNotBlocked(args); +} + } // namespace android diff --git a/services/inputflinger/tests/UnwantedInteractionBlocker_test.cpp b/services/inputflinger/tests/UnwantedInteractionBlocker_test.cpp index b2f8eb37f0..e378096df5 100644 --- a/services/inputflinger/tests/UnwantedInteractionBlocker_test.cpp +++ b/services/inputflinger/tests/UnwantedInteractionBlocker_test.cpp @@ -519,6 +519,34 @@ TEST_F(UnwantedInteractionBlockerTest, NoResetIfDeviceInfoChanges) { &(args = generateMotionArgs(0 /*downTime*/, 4 /*eventTime*/, MOVE, {{7, 8, 9}}))); } +/** + * Send a touch event, and then a stylus event. Make sure that both work. + */ +TEST_F(UnwantedInteractionBlockerTest, StylusAfterTouchWorks) { + NotifyMotionArgs args; + mBlocker->notifyInputDevicesChanged({generateTestDeviceInfo()}); + args = generateMotionArgs(0 /*downTime*/, 0 /*eventTime*/, DOWN, {{1, 2, 3}}); + mBlocker->notifyMotion(&args); + args = generateMotionArgs(0 /*downTime*/, 1 /*eventTime*/, MOVE, {{4, 5, 6}}); + mBlocker->notifyMotion(&args); + args = generateMotionArgs(0 /*downTime*/, 2 /*eventTime*/, UP, {{4, 5, 6}}); + mBlocker->notifyMotion(&args); + + // Now touch down stylus + args = generateMotionArgs(3 /*downTime*/, 3 /*eventTime*/, DOWN, {{10, 20, 30}}); + args.pointerProperties[0].toolType = AMOTION_EVENT_TOOL_TYPE_STYLUS; + args.source |= AINPUT_SOURCE_STYLUS; + mBlocker->notifyMotion(&args); + args = generateMotionArgs(3 /*downTime*/, 4 /*eventTime*/, MOVE, {{40, 50, 60}}); + args.pointerProperties[0].toolType = AMOTION_EVENT_TOOL_TYPE_STYLUS; + args.source |= AINPUT_SOURCE_STYLUS; + mBlocker->notifyMotion(&args); + args = generateMotionArgs(3 /*downTime*/, 5 /*eventTime*/, UP, {{40, 50, 60}}); + args.pointerProperties[0].toolType = AMOTION_EVENT_TOOL_TYPE_STYLUS; + args.source |= AINPUT_SOURCE_STYLUS; + mBlocker->notifyMotion(&args); +} + using UnwantedInteractionBlockerTestDeathTest = UnwantedInteractionBlockerTest; /** -- cgit v1.2.3-59-g8ed1b From a91d8576d9fbb5c2fecbcad8a4f3eef0d2fcc30f Mon Sep 17 00:00:00 2001 From: Siarhei Vishniakou Date: Tue, 17 May 2022 05:03:42 -0700 Subject: Add lock to protect UnwantedInteractionBlocker The call to 'dump' may come from any thread, and therefore could cause a crash. Add a lock to protect this input stage. To run the test: adb shell -t "/data/nativetest64/inputflinger_tests/inputflinger_tests --gtest_filter='*Dump*' --gtest_repeat=100000 --gtest_break_on_failure" Before this patch, the test failed after ~5K - ~13K iterations (took 10-20 seconds to crash). Bug: 232645962 Test: m inputflinger_tests && adb sync data && Change-Id: I2a199690450bc5bb4a8576aa59075e99d37a531b (cherry picked from commit 9f330c542b48dc6edba9aeaff3b3f4bf305713f3) --- include/input/PrintTools.h | 9 ++ libs/input/PrintTools.cpp | 17 +++ services/inputflinger/InputClassifier.cpp | 62 ++++++---- services/inputflinger/InputClassifier.h | 6 +- services/inputflinger/InputManager.cpp | 13 +- services/inputflinger/InputManager.h | 6 +- .../inputflinger/UnwantedInteractionBlocker.cpp | 133 +++++++++------------ services/inputflinger/UnwantedInteractionBlocker.h | 11 +- .../include/UnwantedInteractionBlockerInterface.h | 4 +- .../tests/UnwantedInteractionBlocker_test.cpp | 22 ++++ 10 files changed, 171 insertions(+), 112 deletions(-) (limited to 'services/inputflinger/UnwantedInteractionBlocker.cpp') diff --git a/include/input/PrintTools.h b/include/input/PrintTools.h index 7c3b29b55f..0a75278494 100644 --- a/include/input/PrintTools.h +++ b/include/input/PrintTools.h @@ -58,4 +58,13 @@ std::string dumpMap(const std::map& map, std::string (*keyToString)(const const char* toString(bool value); +/** + * Add "prefix" to the beginning of each line in the provided string + * "str". + * The string 'str' is typically multi-line. + * The most common use case for this function is to add some padding + * when dumping state. + */ +std::string addLinePrefix(std::string str, const std::string& prefix); + } // namespace android \ No newline at end of file diff --git a/libs/input/PrintTools.cpp b/libs/input/PrintTools.cpp index 5d6ae4ed91..01f6bf514b 100644 --- a/libs/input/PrintTools.cpp +++ b/libs/input/PrintTools.cpp @@ -17,6 +17,7 @@ #define LOG_TAG "PrintTools" #include +#include namespace android { @@ -24,4 +25,20 @@ const char* toString(bool value) { return value ? "true" : "false"; } +std::string addLinePrefix(std::string str, const std::string& prefix) { + std::stringstream ss; + bool newLineStarted = true; + for (const auto& ch : str) { + if (newLineStarted) { + ss << prefix; + newLineStarted = false; + } + if (ch == '\n') { + newLineStarted = true; + } + ss << ch; + } + return ss.str(); +} + } // namespace android diff --git a/services/inputflinger/InputClassifier.cpp b/services/inputflinger/InputClassifier.cpp index 3ea0986d41..8ce2f35d7b 100644 --- a/services/inputflinger/InputClassifier.cpp +++ b/services/inputflinger/InputClassifier.cpp @@ -367,7 +367,7 @@ void MotionClassifier::dump(std::string& dump) { // --- InputClassifier --- -InputClassifier::InputClassifier(InputListenerInterface& listener) : mListener(listener) {} +InputClassifier::InputClassifier(InputListenerInterface& listener) : mQueuedListener(listener) {} void InputClassifier::onBinderDied(void* cookie) { InputClassifier* classifier = static_cast(cookie); @@ -417,55 +417,67 @@ void InputClassifier::setMotionClassifierEnabled(bool enabled) { void InputClassifier::notifyConfigurationChanged(const NotifyConfigurationChangedArgs* args) { // pass through - mListener.notifyConfigurationChanged(args); + mQueuedListener.notifyConfigurationChanged(args); + mQueuedListener.flush(); } void InputClassifier::notifyKey(const NotifyKeyArgs* args) { // pass through - mListener.notifyKey(args); + mQueuedListener.notifyKey(args); + mQueuedListener.flush(); } void InputClassifier::notifyMotion(const NotifyMotionArgs* args) { - std::scoped_lock lock(mLock); - // MotionClassifier is only used for touch events, for now - const bool sendToMotionClassifier = mMotionClassifier && isTouchEvent(*args); - if (!sendToMotionClassifier) { - mListener.notifyMotion(args); - return; - } - - NotifyMotionArgs newArgs(*args); - newArgs.classification = mMotionClassifier->classify(newArgs); - mListener.notifyMotion(&newArgs); + { // acquire lock + std::scoped_lock lock(mLock); + // MotionClassifier is only used for touch events, for now + const bool sendToMotionClassifier = mMotionClassifier && isTouchEvent(*args); + if (!sendToMotionClassifier) { + mQueuedListener.notifyMotion(args); + } else { + NotifyMotionArgs newArgs(*args); + newArgs.classification = mMotionClassifier->classify(newArgs); + mQueuedListener.notifyMotion(&newArgs); + } + } // release lock + mQueuedListener.flush(); } void InputClassifier::notifySensor(const NotifySensorArgs* args) { // pass through - mListener.notifySensor(args); + mQueuedListener.notifySensor(args); + mQueuedListener.flush(); } void InputClassifier::notifyVibratorState(const NotifyVibratorStateArgs* args) { // pass through - mListener.notifyVibratorState(args); + mQueuedListener.notifyVibratorState(args); + mQueuedListener.flush(); } void InputClassifier::notifySwitch(const NotifySwitchArgs* args) { // pass through - mListener.notifySwitch(args); + mQueuedListener.notifySwitch(args); + mQueuedListener.flush(); } void InputClassifier::notifyDeviceReset(const NotifyDeviceResetArgs* args) { - std::scoped_lock lock(mLock); - if (mMotionClassifier) { - mMotionClassifier->reset(*args); - } + { // acquire lock + std::scoped_lock lock(mLock); + if (mMotionClassifier) { + mMotionClassifier->reset(*args); + } + } // release lock + // continue to next stage - mListener.notifyDeviceReset(args); + mQueuedListener.notifyDeviceReset(args); + mQueuedListener.flush(); } void InputClassifier::notifyPointerCaptureChanged(const NotifyPointerCaptureChangedArgs* args) { // pass through - mListener.notifyPointerCaptureChanged(args); + mQueuedListener.notifyPointerCaptureChanged(args); + mQueuedListener.flush(); } void InputClassifier::setMotionClassifierLocked( @@ -490,6 +502,10 @@ void InputClassifier::dump(std::string& dump) { dump += "\n"; } +void InputClassifier::monitor() { + std::scoped_lock lock(mLock); +} + InputClassifier::~InputClassifier() { } diff --git a/services/inputflinger/InputClassifier.h b/services/inputflinger/InputClassifier.h index e2a0bc26f6..56cf760256 100644 --- a/services/inputflinger/InputClassifier.h +++ b/services/inputflinger/InputClassifier.h @@ -96,6 +96,9 @@ public: */ virtual void dump(std::string& dump) = 0; + /* Called by the heatbeat to ensures that the classifier has not deadlocked. */ + virtual void monitor() = 0; + InputClassifierInterface() { } virtual ~InputClassifierInterface() { } }; @@ -247,6 +250,7 @@ public: void notifyPointerCaptureChanged(const NotifyPointerCaptureChangedArgs* args) override; void dump(std::string& dump) override; + void monitor() override; ~InputClassifier(); @@ -257,7 +261,7 @@ private: // Protect access to mMotionClassifier, since it may become null via a hidl callback std::mutex mLock; // The next stage to pass input events to - InputListenerInterface& mListener; + QueuedInputListener mQueuedListener; std::unique_ptr mMotionClassifier GUARDED_BY(mLock); std::future mInitializeMotionClassifier GUARDED_BY(mLock); diff --git a/services/inputflinger/InputManager.cpp b/services/inputflinger/InputManager.cpp index 7b03631e1f..9767cd9b71 100644 --- a/services/inputflinger/InputManager.cpp +++ b/services/inputflinger/InputManager.cpp @@ -62,8 +62,8 @@ InputManager::InputManager( const sp& dispatcherPolicy) { mDispatcher = createInputDispatcher(dispatcherPolicy); mClassifier = std::make_unique(*mDispatcher); - mUnwantedInteractionBlocker = std::make_unique(*mClassifier); - mReader = createInputReader(readerPolicy, *mUnwantedInteractionBlocker); + mBlocker = std::make_unique(*mClassifier); + mReader = createInputReader(readerPolicy, *mBlocker); } InputManager::~InputManager() { @@ -111,7 +111,7 @@ InputReaderInterface& InputManager::getReader() { } UnwantedInteractionBlockerInterface& InputManager::getUnwantedInteractionBlocker() { - return *mUnwantedInteractionBlocker; + return *mBlocker; } InputClassifierInterface& InputManager::getClassifier() { @@ -122,6 +122,13 @@ InputDispatcherInterface& InputManager::getDispatcher() { return *mDispatcher; } +void InputManager::monitor() { + mReader->monitor(); + mBlocker->monitor(); + mClassifier->monitor(); + mDispatcher->monitor(); +} + // Used by tests only. binder::Status InputManager::createInputChannel(const std::string& name, InputChannel* outChannel) { IPCThreadState* ipc = IPCThreadState::self(); diff --git a/services/inputflinger/InputManager.h b/services/inputflinger/InputManager.h index 35d2b0fa19..8aad35bf1e 100644 --- a/services/inputflinger/InputManager.h +++ b/services/inputflinger/InputManager.h @@ -90,6 +90,9 @@ public: /* Gets the input dispatcher. */ virtual InputDispatcherInterface& getDispatcher() = 0; + + /* Check that the input stages have not deadlocked. */ + virtual void monitor() = 0; }; class InputManager : public InputManagerInterface, public BnInputFlinger { @@ -108,6 +111,7 @@ public: UnwantedInteractionBlockerInterface& getUnwantedInteractionBlocker() override; InputClassifierInterface& getClassifier() override; InputDispatcherInterface& getDispatcher() override; + void monitor() override; status_t dump(int fd, const Vector& args) override; binder::Status createInputChannel(const std::string& name, InputChannel* outChannel) override; @@ -117,7 +121,7 @@ public: private: std::unique_ptr mReader; - std::unique_ptr mUnwantedInteractionBlocker; + std::unique_ptr mBlocker; std::unique_ptr mClassifier; diff --git a/services/inputflinger/UnwantedInteractionBlocker.cpp b/services/inputflinger/UnwantedInteractionBlocker.cpp index b69e16ac85..f57ff33d50 100644 --- a/services/inputflinger/UnwantedInteractionBlocker.cpp +++ b/services/inputflinger/UnwantedInteractionBlocker.cpp @@ -18,6 +18,7 @@ #include "UnwantedInteractionBlocker.h" #include +#include #include #include #include @@ -80,47 +81,6 @@ static int getLinuxToolType(int32_t toolType) { return MT_TOOL_FINGER; } -static std::string addPrefix(std::string str, const std::string& prefix) { - std::stringstream ss; - bool newLineStarted = true; - for (const auto& ch : str) { - if (newLineStarted) { - ss << prefix; - newLineStarted = false; - } - if (ch == '\n') { - newLineStarted = true; - } - ss << ch; - } - return ss.str(); -} - -template -static std::string dumpSet(const std::set& v) { - static_assert(std::is_integral::value, "Only integral types can be printed."); - std::string out; - for (const T& entry : v) { - out += out.empty() ? "{" : ", "; - out += android::base::StringPrintf("%i", entry); - } - return out.empty() ? "{}" : (out + "}"); -} - -template -static std::string dumpMap(const std::map& map) { - static_assert(std::is_integral::value, "Keys should have integral type to be printed."); - static_assert(std::is_integral::value, "Values should have integral type to be printed."); - std::string out; - for (const auto& [k, v] : map) { - if (!out.empty()) { - out += "\n"; - } - out += android::base::StringPrintf("%i : %i", static_cast(k), static_cast(v)); - } - return out; -} - static std::string dumpDeviceInfo(const AndroidPalmFilterDeviceInfo& info) { std::string out; out += StringPrintf("max_x = %.2f\n", info.max_x); @@ -168,10 +128,6 @@ static int32_t resolveActionForPointer(uint8_t pointerIndex, int32_t action) { return AMOTION_EVENT_ACTION_MOVE; } -static const char* toString(bool value) { - return value ? "true" : "false"; -} - std::string toString(const ::ui::InProgressTouchEvdev& touch) { return StringPrintf("x=%.1f, y=%.1f, tracking_id=%i, slot=%zu," " pressure=%.1f, major=%i, minor=%i, " @@ -356,69 +312,87 @@ UnwantedInteractionBlocker::UnwantedInteractionBlocker(InputListenerInterface& l UnwantedInteractionBlocker::UnwantedInteractionBlocker(InputListenerInterface& listener, bool enablePalmRejection) - : mListener(listener), mEnablePalmRejection(enablePalmRejection) {} + : mQueuedListener(listener), mEnablePalmRejection(enablePalmRejection) {} void UnwantedInteractionBlocker::notifyConfigurationChanged( const NotifyConfigurationChangedArgs* args) { - mListener.notifyConfigurationChanged(args); + mQueuedListener.notifyConfigurationChanged(args); + mQueuedListener.flush(); } void UnwantedInteractionBlocker::notifyKey(const NotifyKeyArgs* args) { - mListener.notifyKey(args); + mQueuedListener.notifyKey(args); + mQueuedListener.flush(); } void UnwantedInteractionBlocker::notifyMotion(const NotifyMotionArgs* args) { - const std::vector processedArgs = - mPreferStylusOverTouchBlocker.processMotion(*args); - for (const NotifyMotionArgs& loopArgs : processedArgs) { - notifyMotionInner(&loopArgs); - } + { // acquire lock + std::scoped_lock lock(mLock); + const std::vector processedArgs = + mPreferStylusOverTouchBlocker.processMotion(*args); + for (const NotifyMotionArgs& loopArgs : processedArgs) { + notifyMotionLocked(&loopArgs); + } + } // release lock + + // Call out to the next stage without holding the lock + mQueuedListener.flush(); } -void UnwantedInteractionBlocker::notifyMotionInner(const NotifyMotionArgs* args) { +void UnwantedInteractionBlocker::notifyMotionLocked(const NotifyMotionArgs* args) { auto it = mPalmRejectors.find(args->deviceId); const bool sendToPalmRejector = it != mPalmRejectors.end() && isFromTouchscreen(args->source); if (!sendToPalmRejector) { - mListener.notifyMotion(args); + mQueuedListener.notifyMotion(args); return; } - const std::vector newMotions = it->second.processMotion(*args); - for (const NotifyMotionArgs& newArgs : newMotions) { - mListener.notifyMotion(&newArgs); + std::vector processedArgs = it->second.processMotion(*args); + for (const NotifyMotionArgs& loopArgs : processedArgs) { + mQueuedListener.notifyMotion(&loopArgs); } } void UnwantedInteractionBlocker::notifySwitch(const NotifySwitchArgs* args) { - mListener.notifySwitch(args); + mQueuedListener.notifySwitch(args); + mQueuedListener.flush(); } void UnwantedInteractionBlocker::notifySensor(const NotifySensorArgs* args) { - mListener.notifySensor(args); + mQueuedListener.notifySensor(args); + mQueuedListener.flush(); } void UnwantedInteractionBlocker::notifyVibratorState(const NotifyVibratorStateArgs* args) { - mListener.notifyVibratorState(args); + mQueuedListener.notifyVibratorState(args); + mQueuedListener.flush(); } void UnwantedInteractionBlocker::notifyDeviceReset(const NotifyDeviceResetArgs* args) { - auto it = mPalmRejectors.find(args->deviceId); - if (it != mPalmRejectors.end()) { - AndroidPalmFilterDeviceInfo info = it->second.getPalmFilterDeviceInfo(); - // Re-create the object instead of resetting it - mPalmRejectors.erase(it); - mPalmRejectors.emplace(args->deviceId, info); - } - mListener.notifyDeviceReset(args); - mPreferStylusOverTouchBlocker.notifyDeviceReset(*args); + { // acquire lock + std::scoped_lock lock(mLock); + auto it = mPalmRejectors.find(args->deviceId); + if (it != mPalmRejectors.end()) { + AndroidPalmFilterDeviceInfo info = it->second.getPalmFilterDeviceInfo(); + // Re-create the object instead of resetting it + mPalmRejectors.erase(it); + mPalmRejectors.emplace(args->deviceId, info); + } + mQueuedListener.notifyDeviceReset(args); + mPreferStylusOverTouchBlocker.notifyDeviceReset(*args); + } // release lock + // Send events to the next stage without holding the lock + mQueuedListener.flush(); } void UnwantedInteractionBlocker::notifyPointerCaptureChanged( const NotifyPointerCaptureChangedArgs* args) { - mListener.notifyPointerCaptureChanged(args); + mQueuedListener.notifyPointerCaptureChanged(args); + mQueuedListener.flush(); } void UnwantedInteractionBlocker::notifyInputDevicesChanged( const std::vector& inputDevices) { + std::scoped_lock lock(mLock); if (!mEnablePalmRejection) { // Palm rejection is disabled. Don't create any palm rejector objects. return; @@ -450,20 +424,23 @@ void UnwantedInteractionBlocker::notifyInputDevicesChanged( } void UnwantedInteractionBlocker::dump(std::string& dump) { + std::scoped_lock lock(mLock); dump += "UnwantedInteractionBlocker:\n"; dump += " mPreferStylusOverTouchBlocker:\n"; - dump += addPrefix(mPreferStylusOverTouchBlocker.dump(), " "); + dump += addLinePrefix(mPreferStylusOverTouchBlocker.dump(), " "); dump += StringPrintf(" mEnablePalmRejection: %s\n", toString(mEnablePalmRejection)); dump += StringPrintf(" isPalmRejectionEnabled (flag value): %s\n", toString(isPalmRejectionEnabled())); dump += mPalmRejectors.empty() ? " mPalmRejectors: None\n" : " mPalmRejectors:\n"; for (const auto& [deviceId, palmRejector] : mPalmRejectors) { dump += StringPrintf(" deviceId = %" PRId32 ":\n", deviceId); - dump += addPrefix(palmRejector.dump(), " "); + dump += addLinePrefix(palmRejector.dump(), " "); } } -void UnwantedInteractionBlocker::monitor() {} +void UnwantedInteractionBlocker::monitor() { + std::scoped_lock lock(mLock); +} UnwantedInteractionBlocker::~UnwantedInteractionBlocker() {} @@ -529,9 +506,9 @@ std::optional SlotState::getSlotForPointerId(int32_t pointerId) const { std::string SlotState::dump() const { std::string out = "mSlotsByPointerId:\n"; - out += addPrefix(dumpMap(mSlotsByPointerId), " ") + "\n"; + out += addLinePrefix(dumpMap(mSlotsByPointerId), " ") + "\n"; out += "mPointerIdsBySlot:\n"; - out += addPrefix(dumpMap(mPointerIdsBySlot), " ") + "\n"; + out += addLinePrefix(dumpMap(mPointerIdsBySlot), " ") + "\n"; return out; } @@ -689,9 +666,9 @@ const AndroidPalmFilterDeviceInfo& PalmRejector::getPalmFilterDeviceInfo() { std::string PalmRejector::dump() const { std::string out; out += "mDeviceInfo:\n"; - out += addPrefix(dumpDeviceInfo(mDeviceInfo), " "); + out += addLinePrefix(dumpDeviceInfo(mDeviceInfo), " "); out += "mSlotState:\n"; - out += addPrefix(mSlotState.dump(), " "); + out += addLinePrefix(mSlotState.dump(), " "); out += "mSuppressedPointerIds: "; out += dumpSet(mSuppressedPointerIds) + "\n"; return out; diff --git a/services/inputflinger/UnwantedInteractionBlocker.h b/services/inputflinger/UnwantedInteractionBlocker.h index 8a1cd7265e..a43376419f 100644 --- a/services/inputflinger/UnwantedInteractionBlocker.h +++ b/services/inputflinger/UnwantedInteractionBlocker.h @@ -19,6 +19,7 @@ #include #include +#include #include "include/UnwantedInteractionBlockerInterface.h" #include "ui/events/ozone/evdev/touch_filter/neural_stylus_palm_detection_filter_util.h" #include "ui/events/ozone/evdev/touch_filter/palm_detection_filter.h" @@ -86,18 +87,20 @@ public: ~UnwantedInteractionBlocker(); private: + std::mutex mLock; // The next stage to pass input events to - InputListenerInterface& mListener; + + QueuedInputListener mQueuedListener; const bool mEnablePalmRejection; // When stylus is down, ignore touch - PreferStylusOverTouchBlocker mPreferStylusOverTouchBlocker; + PreferStylusOverTouchBlocker mPreferStylusOverTouchBlocker GUARDED_BY(mLock); // Detect and reject unwanted palms on screen // Use a separate palm rejector for every touch device. - std::map mPalmRejectors; + std::map mPalmRejectors GUARDED_BY(mLock); // TODO(b/210159205): delete this when simultaneous stylus and touch is supported - void notifyMotionInner(const NotifyMotionArgs* args); + void notifyMotionLocked(const NotifyMotionArgs* args) REQUIRES(mLock); }; class SlotState { diff --git a/services/inputflinger/include/UnwantedInteractionBlockerInterface.h b/services/inputflinger/include/UnwantedInteractionBlockerInterface.h index 2327266563..1a6f8472a5 100644 --- a/services/inputflinger/include/UnwantedInteractionBlockerInterface.h +++ b/services/inputflinger/include/UnwantedInteractionBlockerInterface.h @@ -39,11 +39,11 @@ public: /** * Dump the state of the interaction blocker. - * This method may be called on any thread (usually by the input manager). + * This method may be called on any thread (usually by the input manager on a binder thread). */ virtual void dump(std::string& dump) = 0; - /* Called by the heatbeat to ensures that the dispatcher has not deadlocked. */ + /* Called by the heatbeat to ensures that the blocker has not deadlocked. */ virtual void monitor() = 0; UnwantedInteractionBlockerInterface() {} diff --git a/services/inputflinger/tests/UnwantedInteractionBlocker_test.cpp b/services/inputflinger/tests/UnwantedInteractionBlocker_test.cpp index e378096df5..0062f426d7 100644 --- a/services/inputflinger/tests/UnwantedInteractionBlocker_test.cpp +++ b/services/inputflinger/tests/UnwantedInteractionBlocker_test.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #include "TestInputListener.h" @@ -547,6 +548,27 @@ TEST_F(UnwantedInteractionBlockerTest, StylusAfterTouchWorks) { mBlocker->notifyMotion(&args); } +/** + * Call dump, and on another thread, try to send some motions. The blocker should + * not crash. On 2022 hardware, this test requires ~ 13K executions (about 20 seconds) to reproduce + * the original bug. This is meant to be run with "--gtest_repeat=100000 --gtest_break_on_failure" + * options + */ +TEST_F(UnwantedInteractionBlockerTest, DumpCanBeAccessedOnAnotherThread) { + mBlocker->notifyInputDevicesChanged({generateTestDeviceInfo()}); + NotifyMotionArgs args1 = generateMotionArgs(0 /*downTime*/, 0 /*eventTime*/, DOWN, {{1, 2, 3}}); + mBlocker->notifyMotion(&args1); + std::thread dumpThread([this]() { + std::string dump; + mBlocker->dump(dump); + }); + NotifyMotionArgs args2 = generateMotionArgs(0 /*downTime*/, 1 /*eventTime*/, MOVE, {{4, 5, 6}}); + mBlocker->notifyMotion(&args2); + NotifyMotionArgs args3 = generateMotionArgs(0 /*downTime*/, 2 /*eventTime*/, UP, {{4, 5, 6}}); + mBlocker->notifyMotion(&args3); + dumpThread.join(); +} + using UnwantedInteractionBlockerTestDeathTest = UnwantedInteractionBlockerTest; /** -- cgit v1.2.3-59-g8ed1b