• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2022 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 #include "PreferStylusOverTouchBlocker.h"
18 #include <com_android_input_flags.h>
19 #include <input/PrintTools.h>
20 
21 namespace input_flags = com::android::input::flags;
22 
23 namespace android {
24 
25 const bool BLOCK_TOUCH_WHEN_STYLUS_HOVER = !input_flags::disable_reject_touch_on_stylus_hover();
26 
checkToolType(const NotifyMotionArgs & args)27 static std::pair<bool, bool> checkToolType(const NotifyMotionArgs& args) {
28     bool hasStylus = false;
29     bool hasTouch = false;
30     for (size_t i = 0; i < args.getPointerCount(); i++) {
31         // Make sure we are canceling stylus pointers
32         const ToolType toolType = args.pointerProperties[i].toolType;
33         if (isStylusToolType(toolType)) {
34             hasStylus = true;
35         }
36         if (toolType == ToolType::FINGER) {
37             hasTouch = true;
38         }
39     }
40     return std::make_pair(hasTouch, hasStylus);
41 }
42 
43 /**
44  * Intersect two sets in-place, storing the result in 'set1'.
45  * Find elements in set1 that are not present in set2 and delete them,
46  * relying on the fact that the two sets are ordered.
47  */
48 template <typename T>
intersectInPlace(std::set<T> & set1,const std::set<T> & set2)49 static void intersectInPlace(std::set<T>& set1, const std::set<T>& set2) {
50     typename std::set<T>::iterator it1 = set1.begin();
51     typename std::set<T>::const_iterator it2 = set2.begin();
52     while (it1 != set1.end() && it2 != set2.end()) {
53         const T& element1 = *it1;
54         const T& element2 = *it2;
55         if (element1 < element2) {
56             // This element is not present in set2. Remove it from set1.
57             it1 = set1.erase(it1);
58             continue;
59         }
60         if (element2 < element1) {
61             it2++;
62         }
63         if (element1 == element2) {
64             it1++;
65             it2++;
66         }
67     }
68     // Remove the rest of the elements in set1 because set2 is already exhausted.
69     set1.erase(it1, set1.end());
70 }
71 
72 /**
73  * Same as above, but prune a map
74  */
75 template <typename K, class V>
intersectInPlace(std::map<K,V> & map,const std::set<K> & set2)76 static void intersectInPlace(std::map<K, V>& map, const std::set<K>& set2) {
77     typename std::map<K, V>::iterator it1 = map.begin();
78     typename std::set<K>::const_iterator it2 = set2.begin();
79     while (it1 != map.end() && it2 != set2.end()) {
80         const auto& [key, _] = *it1;
81         const K& element2 = *it2;
82         if (key < element2) {
83             // This element is not present in set2. Remove it from map.
84             it1 = map.erase(it1);
85             continue;
86         }
87         if (element2 < key) {
88             it2++;
89         }
90         if (key == element2) {
91             it1++;
92             it2++;
93         }
94     }
95     // Remove the rest of the elements in map because set2 is already exhausted.
96     map.erase(it1, map.end());
97 }
98 
99 // -------------------------------- PreferStylusOverTouchBlocker -----------------------------------
100 
processMotion(const NotifyMotionArgs & args)101 std::vector<NotifyMotionArgs> PreferStylusOverTouchBlocker::processMotion(
102         const NotifyMotionArgs& args) {
103     const auto [hasTouch, hasStylus] = checkToolType(args);
104     const bool isDisengageOrCancel = BLOCK_TOUCH_WHEN_STYLUS_HOVER
105             ? (args.action == AMOTION_EVENT_ACTION_HOVER_EXIT ||
106                args.action == AMOTION_EVENT_ACTION_UP || args.action == AMOTION_EVENT_ACTION_CANCEL)
107             : (args.action == AMOTION_EVENT_ACTION_UP ||
108                args.action == AMOTION_EVENT_ACTION_CANCEL);
109 
110     if (hasTouch && hasStylus) {
111         mDevicesWithMixedToolType.insert(args.deviceId);
112     }
113     // Handle the case where mixed touch and stylus pointers are reported. Add this device to the
114     // ignore list, since it clearly supports simultaneous touch and stylus.
115     if (mDevicesWithMixedToolType.find(args.deviceId) != mDevicesWithMixedToolType.end()) {
116         // This event comes from device with mixed stylus and touch event. Ignore this device.
117         if (mCanceledDevices.find(args.deviceId) != mCanceledDevices.end()) {
118             // If we started to cancel events from this device, continue to do so to keep
119             // the stream consistent. It should happen at most once per "mixed" device.
120             if (isDisengageOrCancel) {
121                 mCanceledDevices.erase(args.deviceId);
122                 mLastTouchEvents.erase(args.deviceId);
123             }
124             return {};
125         }
126         return {args};
127     }
128 
129     const bool isStylusEvent = hasStylus;
130     const bool isEngage = BLOCK_TOUCH_WHEN_STYLUS_HOVER
131             ? (args.action == AMOTION_EVENT_ACTION_DOWN ||
132                args.action == AMOTION_EVENT_ACTION_HOVER_ENTER)
133             : (args.action == AMOTION_EVENT_ACTION_DOWN);
134 
135     if (isStylusEvent) {
136         if (isEngage) {
137             // Reject all touch while stylus is down
138             mActiveStyli.insert(args.deviceId);
139 
140             // Cancel all current touch!
141             std::vector<NotifyMotionArgs> result;
142             for (auto& [deviceId, lastTouchEvent] : mLastTouchEvents) {
143                 if (mCanceledDevices.find(deviceId) != mCanceledDevices.end()) {
144                     // Already canceled, go to next one.
145                     continue;
146                 }
147                 // Not yet canceled. Cancel it.
148                 lastTouchEvent.action = AMOTION_EVENT_ACTION_CANCEL;
149                 lastTouchEvent.flags |= AMOTION_EVENT_FLAG_CANCELED;
150                 lastTouchEvent.eventTime = systemTime(SYSTEM_TIME_MONOTONIC);
151                 result.push_back(lastTouchEvent);
152                 mCanceledDevices.insert(deviceId);
153             }
154             result.push_back(args);
155             return result;
156         }
157         if (isDisengageOrCancel) {
158             mActiveStyli.erase(args.deviceId);
159         }
160         // Never drop stylus events
161         return {args};
162     }
163 
164     const bool isTouchEvent = hasTouch;
165     if (isTouchEvent) {
166         // Suppress the current gesture if any stylus is still down
167         if (!mActiveStyli.empty()) {
168             mCanceledDevices.insert(args.deviceId);
169         }
170 
171         const bool shouldDrop = mCanceledDevices.find(args.deviceId) != mCanceledDevices.end();
172         if (isDisengageOrCancel) {
173             mCanceledDevices.erase(args.deviceId);
174             mLastTouchEvents.erase(args.deviceId);
175         }
176 
177         // If we already canceled the current gesture, then continue to drop events from it, even if
178         // the stylus has been lifted.
179         if (shouldDrop) {
180             return {};
181         }
182 
183         if (!isDisengageOrCancel) {
184             mLastTouchEvents[args.deviceId] = args;
185         }
186         return {args};
187     }
188 
189     // Not a touch or stylus event
190     return {args};
191 }
192 
notifyInputDevicesChanged(const std::vector<InputDeviceInfo> & inputDevices)193 void PreferStylusOverTouchBlocker::notifyInputDevicesChanged(
194         const std::vector<InputDeviceInfo>& inputDevices) {
195     std::set<int32_t> presentDevices;
196     for (const InputDeviceInfo& device : inputDevices) {
197         presentDevices.insert(device.getId());
198     }
199     // Only keep the devices that are still present.
200     intersectInPlace(mDevicesWithMixedToolType, presentDevices);
201     intersectInPlace(mLastTouchEvents, presentDevices);
202     intersectInPlace(mCanceledDevices, presentDevices);
203     intersectInPlace(mActiveStyli, presentDevices);
204 }
205 
notifyDeviceReset(const NotifyDeviceResetArgs & args)206 void PreferStylusOverTouchBlocker::notifyDeviceReset(const NotifyDeviceResetArgs& args) {
207     mDevicesWithMixedToolType.erase(args.deviceId);
208     mLastTouchEvents.erase(args.deviceId);
209     mCanceledDevices.erase(args.deviceId);
210     mActiveStyli.erase(args.deviceId);
211 }
212 
dumpArgs(const NotifyMotionArgs & args)213 static std::string dumpArgs(const NotifyMotionArgs& args) {
214     return args.dump();
215 }
216 
dump() const217 std::string PreferStylusOverTouchBlocker::dump() const {
218     std::string out;
219     out += "mActiveStyli: " + dumpSet(mActiveStyli) + "\n";
220     out += "mLastTouchEvents: " + dumpMap(mLastTouchEvents, constToString, dumpArgs) + "\n";
221     out += "mDevicesWithMixedToolType: " + dumpSet(mDevicesWithMixedToolType) + "\n";
222     out += "mCanceledDevices: " + dumpSet(mCanceledDevices) + "\n";
223     return out;
224 }
225 
226 } // namespace android
227