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 <input/PrintTools.h>
19
20 namespace android {
21
checkToolType(const NotifyMotionArgs & args)22 static std::pair<bool, bool> checkToolType(const NotifyMotionArgs& args) {
23 bool hasStylus = false;
24 bool hasTouch = false;
25 for (size_t i = 0; i < args.pointerCount; i++) {
26 // Make sure we are canceling stylus pointers
27 const int32_t toolType = args.pointerProperties[i].toolType;
28 if (toolType == AMOTION_EVENT_TOOL_TYPE_STYLUS ||
29 toolType == AMOTION_EVENT_TOOL_TYPE_ERASER) {
30 hasStylus = true;
31 }
32 if (toolType == AMOTION_EVENT_TOOL_TYPE_FINGER) {
33 hasTouch = true;
34 }
35 }
36 return std::make_pair(hasTouch, hasStylus);
37 }
38
39 /**
40 * Intersect two sets in-place, storing the result in 'set1'.
41 * Find elements in set1 that are not present in set2 and delete them,
42 * relying on the fact that the two sets are ordered.
43 */
44 template <typename T>
intersectInPlace(std::set<T> & set1,const std::set<T> & set2)45 static void intersectInPlace(std::set<T>& set1, const std::set<T>& set2) {
46 typename std::set<T>::iterator it1 = set1.begin();
47 typename std::set<T>::const_iterator it2 = set2.begin();
48 while (it1 != set1.end() && it2 != set2.end()) {
49 const T& element1 = *it1;
50 const T& element2 = *it2;
51 if (element1 < element2) {
52 // This element is not present in set2. Remove it from set1.
53 it1 = set1.erase(it1);
54 continue;
55 }
56 if (element2 < element1) {
57 it2++;
58 }
59 if (element1 == element2) {
60 it1++;
61 it2++;
62 }
63 }
64 // Remove the rest of the elements in set1 because set2 is already exhausted.
65 set1.erase(it1, set1.end());
66 }
67
68 /**
69 * Same as above, but prune a map
70 */
71 template <typename K, class V>
intersectInPlace(std::map<K,V> & map,const std::set<K> & set2)72 static void intersectInPlace(std::map<K, V>& map, const std::set<K>& set2) {
73 typename std::map<K, V>::iterator it1 = map.begin();
74 typename std::set<K>::const_iterator it2 = set2.begin();
75 while (it1 != map.end() && it2 != set2.end()) {
76 const auto& [key, _] = *it1;
77 const K& element2 = *it2;
78 if (key < element2) {
79 // This element is not present in set2. Remove it from map.
80 it1 = map.erase(it1);
81 continue;
82 }
83 if (element2 < key) {
84 it2++;
85 }
86 if (key == element2) {
87 it1++;
88 it2++;
89 }
90 }
91 // Remove the rest of the elements in map because set2 is already exhausted.
92 map.erase(it1, map.end());
93 }
94
95 // -------------------------------- PreferStylusOverTouchBlocker -----------------------------------
96
processMotion(const NotifyMotionArgs & args)97 std::vector<NotifyMotionArgs> PreferStylusOverTouchBlocker::processMotion(
98 const NotifyMotionArgs& args) {
99 const auto [hasTouch, hasStylus] = checkToolType(args);
100 const bool isUpOrCancel =
101 args.action == AMOTION_EVENT_ACTION_UP || args.action == AMOTION_EVENT_ACTION_CANCEL;
102
103 if (hasTouch && hasStylus) {
104 mDevicesWithMixedToolType.insert(args.deviceId);
105 }
106 // Handle the case where mixed touch and stylus pointers are reported. Add this device to the
107 // ignore list, since it clearly supports simultaneous touch and stylus.
108 if (mDevicesWithMixedToolType.find(args.deviceId) != mDevicesWithMixedToolType.end()) {
109 // This event comes from device with mixed stylus and touch event. Ignore this device.
110 if (mCanceledDevices.find(args.deviceId) != mCanceledDevices.end()) {
111 // If we started to cancel events from this device, continue to do so to keep
112 // the stream consistent. It should happen at most once per "mixed" device.
113 if (isUpOrCancel) {
114 mCanceledDevices.erase(args.deviceId);
115 mLastTouchEvents.erase(args.deviceId);
116 }
117 return {};
118 }
119 return {args};
120 }
121
122 const bool isStylusEvent = hasStylus;
123 const bool isDown = args.action == AMOTION_EVENT_ACTION_DOWN;
124
125 if (isStylusEvent) {
126 if (isDown) {
127 // Reject all touch while stylus is down
128 mActiveStyli.insert(args.deviceId);
129
130 // Cancel all current touch!
131 std::vector<NotifyMotionArgs> result;
132 for (auto& [deviceId, lastTouchEvent] : mLastTouchEvents) {
133 if (mCanceledDevices.find(deviceId) != mCanceledDevices.end()) {
134 // Already canceled, go to next one.
135 continue;
136 }
137 // Not yet canceled. Cancel it.
138 lastTouchEvent.action = AMOTION_EVENT_ACTION_CANCEL;
139 lastTouchEvent.flags |= AMOTION_EVENT_FLAG_CANCELED;
140 lastTouchEvent.eventTime = systemTime(SYSTEM_TIME_MONOTONIC);
141 result.push_back(lastTouchEvent);
142 mCanceledDevices.insert(deviceId);
143 }
144 result.push_back(args);
145 return result;
146 }
147 if (isUpOrCancel) {
148 mActiveStyli.erase(args.deviceId);
149 }
150 // Never drop stylus events
151 return {args};
152 }
153
154 const bool isTouchEvent = hasTouch;
155 if (isTouchEvent) {
156 // Suppress the current gesture if any stylus is still down
157 if (!mActiveStyli.empty()) {
158 mCanceledDevices.insert(args.deviceId);
159 }
160
161 const bool shouldDrop = mCanceledDevices.find(args.deviceId) != mCanceledDevices.end();
162 if (isUpOrCancel) {
163 mCanceledDevices.erase(args.deviceId);
164 mLastTouchEvents.erase(args.deviceId);
165 }
166
167 // If we already canceled the current gesture, then continue to drop events from it, even if
168 // the stylus has been lifted.
169 if (shouldDrop) {
170 return {};
171 }
172
173 if (!isUpOrCancel) {
174 mLastTouchEvents[args.deviceId] = args;
175 }
176 return {args};
177 }
178
179 // Not a touch or stylus event
180 return {args};
181 }
182
notifyInputDevicesChanged(const std::vector<InputDeviceInfo> & inputDevices)183 void PreferStylusOverTouchBlocker::notifyInputDevicesChanged(
184 const std::vector<InputDeviceInfo>& inputDevices) {
185 std::set<int32_t> presentDevices;
186 for (const InputDeviceInfo& device : inputDevices) {
187 presentDevices.insert(device.getId());
188 }
189 // Only keep the devices that are still present.
190 intersectInPlace(mDevicesWithMixedToolType, presentDevices);
191 intersectInPlace(mLastTouchEvents, presentDevices);
192 intersectInPlace(mCanceledDevices, presentDevices);
193 intersectInPlace(mActiveStyli, presentDevices);
194 }
195
notifyDeviceReset(const NotifyDeviceResetArgs & args)196 void PreferStylusOverTouchBlocker::notifyDeviceReset(const NotifyDeviceResetArgs& args) {
197 mDevicesWithMixedToolType.erase(args.deviceId);
198 mLastTouchEvents.erase(args.deviceId);
199 mCanceledDevices.erase(args.deviceId);
200 mActiveStyli.erase(args.deviceId);
201 }
202
dumpArgs(const NotifyMotionArgs & args)203 static std::string dumpArgs(const NotifyMotionArgs& args) {
204 return args.dump();
205 }
206
dump() const207 std::string PreferStylusOverTouchBlocker::dump() const {
208 std::string out;
209 out += "mActiveStyli: " + dumpSet(mActiveStyli) + "\n";
210 out += "mLastTouchEvents: " + dumpMap(mLastTouchEvents, constToString, dumpArgs) + "\n";
211 out += "mDevicesWithMixedToolType: " + dumpSet(mDevicesWithMixedToolType) + "\n";
212 out += "mCanceledDevices: " + dumpSet(mCanceledDevices) + "\n";
213 return out;
214 }
215
216 } // namespace android
217