/* * 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 namespace android { 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 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 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()); } /** * 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 ----------------------------------- std::vector 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 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& 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 += "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