| /* |
| * 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 <com_android_input_flags.h> |
| #include <input/PrintTools.h> |
| |
| namespace input_flags = com::android::input::flags; |
| |
| namespace android { |
| |
| const bool BLOCK_TOUCH_WHEN_STYLUS_HOVER = !input_flags::disable_reject_touch_on_stylus_hover(); |
| |
| static std::pair<bool, bool> checkToolType(const NotifyMotionArgs& args) { |
| bool hasStylus = false; |
| bool hasTouch = false; |
| for (size_t i = 0; i < args.getPointerCount(); i++) { |
| // Make sure we are canceling stylus pointers |
| const ToolType toolType = args.pointerProperties[i].toolType; |
| if (isStylusToolType(toolType)) { |
| hasStylus = true; |
| } |
| if (toolType == ToolType::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 isDisengageOrCancel = BLOCK_TOUCH_WHEN_STYLUS_HOVER |
| ? (args.action == AMOTION_EVENT_ACTION_HOVER_EXIT || |
| args.action == AMOTION_EVENT_ACTION_UP || args.action == AMOTION_EVENT_ACTION_CANCEL) |
| : (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 (isDisengageOrCancel) { |
| mCanceledDevices.erase(args.deviceId); |
| mLastTouchEvents.erase(args.deviceId); |
| } |
| return {}; |
| } |
| return {args}; |
| } |
| |
| const bool isStylusEvent = hasStylus; |
| const bool isEngage = BLOCK_TOUCH_WHEN_STYLUS_HOVER |
| ? (args.action == AMOTION_EVENT_ACTION_DOWN || |
| args.action == AMOTION_EVENT_ACTION_HOVER_ENTER) |
| : (args.action == AMOTION_EVENT_ACTION_DOWN); |
| |
| if (isStylusEvent) { |
| if (isEngage) { |
| // 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 (isDisengageOrCancel) { |
| 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 (isDisengageOrCancel) { |
| 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 (!isDisengageOrCancel) { |
| 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 |