| /* | |
| * 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 (isStylusToolType(toolType)) { | |
| 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 |