diff options
-rw-r--r-- | include/input/PrintTools.h | 61 | ||||
-rw-r--r-- | libs/input/Android.bp | 4 | ||||
-rw-r--r-- | libs/input/PrintTools.cpp | 27 | ||||
-rw-r--r-- | services/inputflinger/Android.bp | 1 | ||||
-rw-r--r-- | services/inputflinger/InputListener.cpp | 6 | ||||
-rw-r--r-- | services/inputflinger/PreferStylusOverTouchBlocker.cpp | 216 | ||||
-rw-r--r-- | services/inputflinger/PreferStylusOverTouchBlocker.h | 65 | ||||
-rw-r--r-- | services/inputflinger/UnwantedInteractionBlocker.cpp | 15 | ||||
-rw-r--r-- | services/inputflinger/UnwantedInteractionBlocker.h | 7 | ||||
-rw-r--r-- | services/inputflinger/tests/Android.bp | 1 | ||||
-rw-r--r-- | services/inputflinger/tests/PreferStylusOverTouch_test.cpp | 502 | ||||
-rw-r--r-- | services/inputflinger/tests/UnwantedInteractionBlocker_test.cpp | 36 |
12 files changed, 938 insertions, 3 deletions
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 <map> +#include <set> +#include <string> + +namespace android { + +template <typename T> +std::string constToString(const T& v) { + return std::to_string(v); +} + +/** + * Convert a set of integral types to string. + */ +template <typename T> +std::string dumpSet(const std::set<T>& 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 <typename K, typename V> +std::string dumpMap(const std::map<K, V>& 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 18fb7c1bfb..1d4fc1fc04 100644 --- a/libs/input/Android.bp +++ b/libs/input/Android.bp @@ -50,6 +50,7 @@ cc_library { "Keyboard.cpp", "KeyCharacterMap.cpp", "KeyLayoutMap.cpp", + "PrintTools.cpp", "PropertyMap.cpp", "TouchVideoFrame.cpp", "VelocityControl.cpp", @@ -102,6 +103,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 <input/PrintTools.h> + +namespace android { + +const char* toString(bool value) { + return value ? "true" : "false"; +} + +} // namespace android 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/InputListener.cpp b/services/inputflinger/InputListener.cpp index 3a4b6c599f..2a3924b5f2 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 new file mode 100644 index 0000000000..beec2e162e --- /dev/null +++ b/services/inputflinger/PreferStylusOverTouchBlocker.cpp @@ -0,0 +1,216 @@ +/* + * 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 <input/PrintTools.h> + +namespace android { + +static std::pair<bool, bool> 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); +} + +/** + * 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 <typename T> +static void intersectInPlace(std::set<T>& set1, const std::set<T>& set2) { + typename std::set<T>::iterator it1 = set1.begin(); + typename std::set<T>::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()); +} + +/** + * Same as above, but prune a map + */ +template <typename K, class V> +static void intersectInPlace(std::map<K, V>& map, const std::set<K>& set2) { + typename std::map<K, V>::iterator it1 = map.begin(); + typename std::set<K>::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 ----------------------------------- + +std::vector<NotifyMotionArgs> PreferStylusOverTouchBlocker::processMotion( + const NotifyMotionArgs& args) { + 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; + + if (isStylusEvent) { + if (isDown) { + // Reject all touch while stylus is down + mActiveStyli.insert(args.deviceId); + + // Cancel all current touch! + std::vector<NotifyMotionArgs> 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) { + mActiveStyli.erase(args.deviceId); + } + // Never drop stylus events + return {args}; + } + + const bool isTouchEvent = hasTouch; + if (isTouchEvent) { + // 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 (shouldDrop) { + return {}; + } + + if (!isUpOrCancel) { + mLastTouchEvents[args.deviceId] = args; + } + return {args}; + } + + // Not a touch or stylus event + return {args}; +} + +void PreferStylusOverTouchBlocker::notifyInputDevicesChanged( + const std::vector<InputDeviceInfo>& inputDevices) { + std::set<int32_t> 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 += "mActiveStyli: " + dumpSet(mActiveStyli) + "\n"; + out += "mLastTouchEvents: " + dumpMap(mLastTouchEvents, constToString, dumpArgs) + "\n"; + out += "mDevicesWithMixedToolType: " + dumpSet(mDevicesWithMixedToolType) + "\n"; + out += "mCanceledDevices: " + dumpSet(mCanceledDevices) + "\n"; + return out; +} + +} // namespace android diff --git a/services/inputflinger/PreferStylusOverTouchBlocker.h b/services/inputflinger/PreferStylusOverTouchBlocker.h new file mode 100644 index 0000000000..716dc4d351 --- /dev/null +++ b/services/inputflinger/PreferStylusOverTouchBlocker.h @@ -0,0 +1,65 @@ +/* + * 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 <optional> +#include <set> +#include "InputListener.h" + +namespace android { + +/** + * 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 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 N elements, containing N-1 events with ACTION_CANCEL and the current event. + * + * The returned result is intended to be reinjected into the original event stream in + * replacement of the incoming event. + */ + std::vector<NotifyMotionArgs> processMotion(const NotifyMotionArgs& args); + std::string dump() const; + + void notifyInputDevicesChanged(const std::vector<InputDeviceInfo>& inputDevices); + + void notifyDeviceReset(const NotifyDeviceResetArgs& args); + +private: + // Stores the device id's of styli that are currently down. + std::set<int32_t /*deviceId*/> mActiveStyli; + // For each device, store the last touch event as long as the touch is down. Upon liftoff, + // the entry is erased. + std::map<int32_t /*deviceId*/, NotifyMotionArgs> mLastTouchEvents; + // Device ids of devices for which the current touch gesture is canceled. + std::set<int32_t /*deviceId*/> 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<int32_t /*deviceId*/> mDevicesWithMixedToolType; +}; + +} // namespace android
\ No newline at end of file diff --git a/services/inputflinger/UnwantedInteractionBlocker.cpp b/services/inputflinger/UnwantedInteractionBlocker.cpp index 64dbb8ceb4..b69e16ac85 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) { + const std::vector<NotifyMotionArgs> 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) { @@ -400,6 +409,7 @@ void UnwantedInteractionBlocker::notifyDeviceReset(const NotifyDeviceResetArgs* mPalmRejectors.emplace(args->deviceId, info); } mListener.notifyDeviceReset(args); + mPreferStylusOverTouchBlocker.notifyDeviceReset(*args); } void UnwantedInteractionBlocker::notifyPointerCaptureChanged( @@ -436,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<int32_t /*deviceId*/, PalmRejector> 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..8e2ab88e80 --- /dev/null +++ b/services/inputflinger/tests/PreferStylusOverTouch_test.cpp @@ -0,0 +1,502 @@ +/* + * 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 <gtest/gtest.h> +#include "../PreferStylusOverTouchBlocker.h" + +namespace android { + +constexpr int32_t TOUCH_DEVICE_ID = 3; +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; + +struct PointerData { + float x; + float y; +}; + +static NotifyMotionArgs generateMotionArgs(nsecs_t downTime, nsecs_t eventTime, int32_t action, + const std::vector<PointerData>& 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) { assertResponse(args, {args}); } + + void assertDropped(const NotifyMotionArgs& args) { assertResponse(args, {}); } + + void assertResponse(const NotifyMotionArgs& args, + const std::vector<NotifyMotionArgs>& expected) { + std::vector<NotifyMotionArgs> 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 notifyInputDevicesChanged(const std::vector<InputDeviceInfo>& devices) { + mBlocker.notifyInputDevicesChanged(devices); + } + + void dump() const { ALOGI("Blocker: \n%s\n", mBlocker.dump().c_str()); } + +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); + 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 + args = generateMotionArgs(3 /*downTime*/, 4 /*eventTime*/, MOVE, {{10, 31}}, STYLUS); + assertNotBlocked(args); + + args = generateMotionArgs(0 /*downTime*/, 5 /*eventTime*/, MOVE, {{1, 4}}, TOUCHSCREEN); + 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. + */ +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); +} + +/** + * 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 a3220cc63a..e378096df5 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. @@ -511,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; /** |