Add PreferStylusOverTouchBlocker and handle multiple devices

We removed PreferStylusOverTouchBlocker previously in order to avoid a
crash. In this CL, we are adding it back in, and handling the case of
input device having "SOURCE_STYLUS", but reporting "finger" tool type.

If there's a stylus event with one of the pointers labeled as 'finger',
let's assume that the device supports simultaneous touch and stylus. For
this situation, simply disable PreferStylusOverTouchBlocker going
forward for these devices, and pass through any events coming from there.

Currently, this happens on emulator. In their touch driver, they
configure stylus properties as well as touch properties, but most of the
events that they send are TOOL_TYPE_FINGER. Previously, this triggered a
crash in PreferStylusOverTouchBlocker.

Bug: 222531989
Test: atest inputflinger_tests
Change-Id: Ifbb08858a4dfebc95c30ca19d6e68533855db7e4
diff --git a/services/inputflinger/InputListener.cpp b/services/inputflinger/InputListener.cpp
index 3a4b6c5..2a3924b 100644
--- a/services/inputflinger/InputListener.cpp
+++ b/services/inputflinger/InputListener.cpp
@@ -202,9 +202,11 @@
         coords += "}";
     }
     return StringPrintf("NotifyMotionArgs(id=%" PRId32 ", eventTime=%" PRId64 ", deviceId=%" PRId32
-                        ", source=%s, action=%s, pointerCount=%" PRIu32 " pointers=%s)",
+                        ", source=%s, action=%s, pointerCount=%" PRIu32
+                        " pointers=%s, flags=0x%08x)",
                         id, eventTime, deviceId, inputEventSourceToString(source).c_str(),
-                        MotionEvent::actionToString(action).c_str(), pointerCount, coords.c_str());
+                        MotionEvent::actionToString(action).c_str(), pointerCount, coords.c_str(),
+                        flags);
 }
 
 void NotifyMotionArgs::notify(InputListenerInterface& listener) const {
diff --git a/services/inputflinger/PreferStylusOverTouchBlocker.cpp b/services/inputflinger/PreferStylusOverTouchBlocker.cpp
index ad639b4..beec2e1 100644
--- a/services/inputflinger/PreferStylusOverTouchBlocker.cpp
+++ b/services/inputflinger/PreferStylusOverTouchBlocker.cpp
@@ -15,78 +15,163 @@
  */
 
 #include "PreferStylusOverTouchBlocker.h"
-
-#include <android-base/stringprintf.h>
-
-using android::base::StringPrintf;
-
-static const char* toString(bool value) {
-    return value ? "true" : "false";
-}
+#include <input/PrintTools.h>
 
 namespace android {
 
-ftl::StaticVector<NotifyMotionArgs, 2> PreferStylusOverTouchBlocker::processMotion(
-        const NotifyMotionArgs& args) {
-    const bool isStylusEvent = isFromSource(args.source, AINPUT_SOURCE_STYLUS);
-    if (isStylusEvent) {
-        for (size_t i = 0; i < args.pointerCount; i++) {
-            // Make sure we are canceling stylus pointers
-            const int32_t toolType = args.pointerProperties[i].toolType;
-            LOG_ALWAYS_FATAL_IF(toolType != AMOTION_EVENT_TOOL_TYPE_STYLUS &&
-                                        toolType != AMOTION_EVENT_TOOL_TYPE_ERASER,
-                                "The pointer %zu has toolType=%i, but the source is STYLUS. If "
-                                "simultaneous touch and stylus is supported, "
-                                "'PreferStylusOverTouchBlocker' should be disabled.",
-                                i, toolType);
+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 (toolType == AMOTION_EVENT_TOOL_TYPE_STYLUS ||
+            toolType == AMOTION_EVENT_TOOL_TYPE_ERASER) {
+            hasStylus = true;
+        }
+        if (toolType == AMOTION_EVENT_TOOL_TYPE_FINGER) {
+            hasTouch = true;
         }
     }
-    const bool isDown = args.action == AMOTION_EVENT_ACTION_DOWN;
+    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
-            mIsStylusDown = true;
-            if (mIsTouchDown && !mCurrentTouchIsCanceled) {
-                // Cancel touch!
-                mCurrentTouchIsCanceled = true;
-                mLastTouchEvent.action = AMOTION_EVENT_ACTION_CANCEL;
-                mLastTouchEvent.flags |= AMOTION_EVENT_FLAG_CANCELED;
-                mLastTouchEvent.eventTime = systemTime(SYSTEM_TIME_MONOTONIC);
-                return {mLastTouchEvent, args};
+            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) {
-            mIsStylusDown = false;
+            mActiveStyli.erase(args.deviceId);
         }
         // Never drop stylus events
         return {args};
     }
 
-    const bool isTouchEvent =
-            isFromSource(args.source, AINPUT_SOURCE_TOUCHSCREEN) && !isStylusEvent;
+    const bool isTouchEvent = hasTouch;
     if (isTouchEvent) {
-        if (mIsStylusDown) {
-            mCurrentTouchIsCanceled = true;
+        // 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 (mCurrentTouchIsCanceled) {
-            if (isUpOrCancel) {
-                mCurrentTouchIsCanceled = false;
-            }
+        if (shouldDrop) {
             return {};
         }
 
-        // Update state
-        mLastTouchEvent = args;
-        if (isDown) {
-            mIsTouchDown = true;
-        }
-        if (isUpOrCancel) {
-            mIsTouchDown = false;
-            mCurrentTouchIsCanceled = false;
+        if (!isUpOrCancel) {
+            mLastTouchEvents[args.deviceId] = args;
         }
         return {args};
     }
@@ -95,12 +180,36 @@
     return {args};
 }
 
-std::string PreferStylusOverTouchBlocker::dump() {
+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 += StringPrintf("mIsTouchDown: %s\n", toString(mIsTouchDown));
-    out += StringPrintf("mIsStylusDown: %s\n", toString(mIsStylusDown));
-    out += StringPrintf("mLastTouchEvent: %s\n", mLastTouchEvent.dump().c_str());
-    out += StringPrintf("mCurrentTouchIsCanceled: %s\n", toString(mCurrentTouchIsCanceled));
+    out += "mActiveStyli: " + dumpSet(mActiveStyli) + "\n";
+    out += "mLastTouchEvents: " + dumpMap(mLastTouchEvents, constToString, dumpArgs) + "\n";
+    out += "mDevicesWithMixedToolType: " + dumpSet(mDevicesWithMixedToolType) + "\n";
+    out += "mCanceledDevices: " + dumpSet(mCanceledDevices) + "\n";
     return out;
 }
 
diff --git a/services/inputflinger/PreferStylusOverTouchBlocker.h b/services/inputflinger/PreferStylusOverTouchBlocker.h
index 3f56161..716dc4d 100644
--- a/services/inputflinger/PreferStylusOverTouchBlocker.h
+++ b/services/inputflinger/PreferStylusOverTouchBlocker.h
@@ -16,66 +16,50 @@
 
 #pragma once
 
-#include <ftl/static_vector.h>
 #include <optional>
+#include <set>
 #include "InputListener.h"
 
 namespace android {
 
 /**
- * When stylus is down, we ignore all touch.
+ * When stylus is down, all touch is ignored.
  * TODO(b/210159205): delete this when simultaneous stylus and touch is supported
  */
 class PreferStylusOverTouchBlocker {
 public:
     /**
-     * Process the provided event and emit up to 2 events in response.
+     * Process the provided event and emit 0 or more events that should be used instead of it.
      * In the majority of cases, the returned result will just be the provided args (array with
      * only 1 element), unmodified.
      *
      * If the gesture should be blocked, the returned result may be:
      *
      * a) An empty array, if the current event should just be ignored completely
-     * b) An array of 2 elements, containing an event with ACTION_CANCEL and the current event.
+     * b) An array of N elements, containing N-1 events with ACTION_CANCEL and the current event.
      *
-     * bool is set to 'true'.
-     * NotifyMotionArgs potentially contains an event that should be used to cancel the existing
-     * gesture.
-     *
-     * If the event should not be blocked, bool contains 'false'.
+     * The returned result is intended to be reinjected into the original event stream in
+     * replacement of the incoming event.
      */
-    ftl::StaticVector<NotifyMotionArgs, 2> processMotion(const NotifyMotionArgs& args);
-    std::string dump();
+    std::vector<NotifyMotionArgs> processMotion(const NotifyMotionArgs& args);
+    std::string dump() const;
+
+    void notifyInputDevicesChanged(const std::vector<InputDeviceInfo>& inputDevices);
+
+    void notifyDeviceReset(const NotifyDeviceResetArgs& args);
 
 private:
-    bool mIsTouchDown = false;
-    bool mIsStylusDown = false;
-    // Provide some default values for the stored MotionEvent to allow printint the event before
-    // any real event is received.
-    NotifyMotionArgs mLastTouchEvent{0 /*id*/,
-                                     0 /*eventTime*/,
-                                     0 /*readTime*/,
-                                     0 /*deviceId*/,
-                                     AINPUT_SOURCE_TOUCHSCREEN,
-                                     0 /*displayId*/,
-                                     0 /*policyFlags*/,
-                                     0 /*action*/,
-                                     0 /*actionButton*/,
-                                     0 /*flags*/,
-                                     0 /*metaState*/,
-                                     0 /*buttonState*/,
-                                     MotionClassification::NONE,
-                                     AMOTION_EVENT_EDGE_FLAG_NONE,
-                                     0 /*pointerCount*/,
-                                     nullptr /*properties*/,
-                                     nullptr /*coords*/,
-                                     0. /*xPrecision*/,
-                                     0. /*yPrecision*/,
-                                     AMOTION_EVENT_INVALID_CURSOR_POSITION,
-                                     AMOTION_EVENT_INVALID_CURSOR_POSITION,
-                                     0 /*downTime*/,
-                                     {}};
-    bool mCurrentTouchIsCanceled = false;
+    // Stores the device id's of styli that are currently down.
+    std::set<int32_t /*deviceId*/> mActiveStyli;
+    // For each device, store the last touch event as long as the touch is down. Upon liftoff,
+    // the entry is erased.
+    std::map<int32_t /*deviceId*/, NotifyMotionArgs> mLastTouchEvents;
+    // Device ids of devices for which the current touch gesture is canceled.
+    std::set<int32_t /*deviceId*/> mCanceledDevices;
+
+    // Device ids of input devices where we encountered simultaneous touch and stylus
+    // events. For these devices, we don't do any event processing (nothing is blocked or altered).
+    std::set<int32_t /*deviceId*/> mDevicesWithMixedToolType;
 };
 
 } // namespace android
\ No newline at end of file
diff --git a/services/inputflinger/UnwantedInteractionBlocker.cpp b/services/inputflinger/UnwantedInteractionBlocker.cpp
index fb3962e..b69e16a 100644
--- a/services/inputflinger/UnwantedInteractionBlocker.cpp
+++ b/services/inputflinger/UnwantedInteractionBlocker.cpp
@@ -368,6 +368,14 @@
 }
 
 void UnwantedInteractionBlocker::notifyMotion(const NotifyMotionArgs* args) {
+    const std::vector<NotifyMotionArgs> processedArgs =
+            mPreferStylusOverTouchBlocker.processMotion(*args);
+    for (const NotifyMotionArgs& loopArgs : processedArgs) {
+        notifyMotionInner(&loopArgs);
+    }
+}
+
+void UnwantedInteractionBlocker::notifyMotionInner(const NotifyMotionArgs* args) {
     auto it = mPalmRejectors.find(args->deviceId);
     const bool sendToPalmRejector = it != mPalmRejectors.end() && isFromTouchscreen(args->source);
     if (!sendToPalmRejector) {
@@ -401,6 +409,7 @@
         mPalmRejectors.emplace(args->deviceId, info);
     }
     mListener.notifyDeviceReset(args);
+    mPreferStylusOverTouchBlocker.notifyDeviceReset(*args);
 }
 
 void UnwantedInteractionBlocker::notifyPointerCaptureChanged(
@@ -437,10 +446,13 @@
         auto const& [deviceId, _] = item;
         return devicesToKeep.find(deviceId) == devicesToKeep.end();
     });
+    mPreferStylusOverTouchBlocker.notifyInputDevicesChanged(inputDevices);
 }
 
 void UnwantedInteractionBlocker::dump(std::string& dump) {
     dump += "UnwantedInteractionBlocker:\n";
+    dump += "  mPreferStylusOverTouchBlocker:\n";
+    dump += addPrefix(mPreferStylusOverTouchBlocker.dump(), "    ");
     dump += StringPrintf("  mEnablePalmRejection: %s\n", toString(mEnablePalmRejection));
     dump += StringPrintf("  isPalmRejectionEnabled (flag value): %s\n",
                          toString(isPalmRejectionEnabled()));
diff --git a/services/inputflinger/UnwantedInteractionBlocker.h b/services/inputflinger/UnwantedInteractionBlocker.h
index 14068fd..8a1cd72 100644
--- a/services/inputflinger/UnwantedInteractionBlocker.h
+++ b/services/inputflinger/UnwantedInteractionBlocker.h
@@ -23,6 +23,8 @@
 #include "ui/events/ozone/evdev/touch_filter/neural_stylus_palm_detection_filter_util.h"
 #include "ui/events/ozone/evdev/touch_filter/palm_detection_filter.h"
 
+#include "PreferStylusOverTouchBlocker.h"
+
 namespace android {
 
 // --- Functions for manipulation of event streams
@@ -88,9 +90,14 @@
     InputListenerInterface& mListener;
     const bool mEnablePalmRejection;
 
+    // When stylus is down, ignore touch
+    PreferStylusOverTouchBlocker mPreferStylusOverTouchBlocker;
+
     // Detect and reject unwanted palms on screen
     // Use a separate palm rejector for every touch device.
     std::map<int32_t /*deviceId*/, PalmRejector> mPalmRejectors;
+    // TODO(b/210159205): delete this when simultaneous stylus and touch is supported
+    void notifyMotionInner(const NotifyMotionArgs* args);
 };
 
 class SlotState {
diff --git a/services/inputflinger/tests/PreferStylusOverTouch_test.cpp b/services/inputflinger/tests/PreferStylusOverTouch_test.cpp
index 70f40aa..8e2ab88 100644
--- a/services/inputflinger/tests/PreferStylusOverTouch_test.cpp
+++ b/services/inputflinger/tests/PreferStylusOverTouch_test.cpp
@@ -20,12 +20,16 @@
 namespace android {
 
 constexpr int32_t TOUCH_DEVICE_ID = 3;
-constexpr int32_t STYLUS_DEVICE_ID = 4;
+constexpr int32_t SECOND_TOUCH_DEVICE_ID = 4;
+constexpr int32_t STYLUS_DEVICE_ID = 5;
+constexpr int32_t SECOND_STYLUS_DEVICE_ID = 6;
 
 constexpr int DOWN = AMOTION_EVENT_ACTION_DOWN;
 constexpr int MOVE = AMOTION_EVENT_ACTION_MOVE;
 constexpr int UP = AMOTION_EVENT_ACTION_UP;
 constexpr int CANCEL = AMOTION_EVENT_ACTION_CANCEL;
+static constexpr int32_t POINTER_1_DOWN =
+        AMOTION_EVENT_ACTION_POINTER_DOWN | (1 << AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT);
 constexpr int32_t TOUCHSCREEN = AINPUT_SOURCE_TOUCHSCREEN;
 constexpr int32_t STYLUS = AINPUT_SOURCE_STYLUS;
 
@@ -78,29 +82,30 @@
 
 class PreferStylusOverTouchTest : public testing::Test {
 protected:
-    void assertNotBlocked(const NotifyMotionArgs& args) {
-        ftl::StaticVector<NotifyMotionArgs, 2> processedArgs = mBlocker.processMotion(args);
-        ASSERT_EQ(1u, processedArgs.size());
-        ASSERT_EQ(args, processedArgs[0]);
+    void assertNotBlocked(const NotifyMotionArgs& args) { assertResponse(args, {args}); }
+
+    void assertDropped(const NotifyMotionArgs& args) { assertResponse(args, {}); }
+
+    void assertResponse(const NotifyMotionArgs& args,
+                        const std::vector<NotifyMotionArgs>& expected) {
+        std::vector<NotifyMotionArgs> receivedArgs = mBlocker.processMotion(args);
+        ASSERT_EQ(expected.size(), receivedArgs.size());
+        for (size_t i = 0; i < expected.size(); i++) {
+            // The 'eventTime' of CANCEL events is dynamically generated. Don't check this field.
+            if (expected[i].action == CANCEL && receivedArgs[i].action == CANCEL) {
+                receivedArgs[i].eventTime = expected[i].eventTime;
+            }
+
+            ASSERT_EQ(expected[i], receivedArgs[i])
+                    << expected[i].dump() << " vs " << receivedArgs[i].dump();
+        }
     }
 
-    void assertDropped(const NotifyMotionArgs& args) {
-        ftl::StaticVector<NotifyMotionArgs, 2> processedArgs = mBlocker.processMotion(args);
-        ASSERT_TRUE(processedArgs.empty());
+    void notifyInputDevicesChanged(const std::vector<InputDeviceInfo>& devices) {
+        mBlocker.notifyInputDevicesChanged(devices);
     }
 
-    void assertCanceled(const NotifyMotionArgs& args,
-                        std::optional<NotifyMotionArgs> canceledArgs) {
-        ftl::StaticVector<NotifyMotionArgs, 2> processedArgs = mBlocker.processMotion(args);
-        ASSERT_EQ(2u, processedArgs.size());
-        NotifyMotionArgs& cancelEvent = processedArgs[0];
-        ASSERT_EQ(CANCEL, cancelEvent.action);
-        ASSERT_EQ(AMOTION_EVENT_FLAG_CANCELED, cancelEvent.flags & AMOTION_EVENT_FLAG_CANCELED);
-        ASSERT_TRUE(isFromSource(cancelEvent.source, TOUCHSCREEN));
-        ASSERT_FALSE(isFromSource(cancelEvent.source, STYLUS));
-
-        ASSERT_EQ(args, processedArgs[1]);
-    }
+    void dump() const { ALOGI("Blocker: \n%s\n", mBlocker.dump().c_str()); }
 
 private:
     PreferStylusOverTouchBlocker mBlocker;
@@ -148,7 +153,8 @@
     args = generateMotionArgs(3 /*downTime*/, 3 /*eventTime*/, DOWN, {{10, 30}}, STYLUS);
     NotifyMotionArgs cancelArgs =
             generateMotionArgs(0 /*downTime*/, 1 /*eventTime*/, CANCEL, {{1, 3}}, TOUCHSCREEN);
-    assertCanceled(args, cancelArgs);
+    cancelArgs.flags |= AMOTION_EVENT_FLAG_CANCELED;
+    assertResponse(args, {cancelArgs, args});
 
     // Both stylus and touch events continue. Stylus should be not blocked, and touch should be
     // blocked
@@ -160,6 +166,26 @@
 }
 
 /**
+ * Stylus goes down after touch gesture.
+ */
+TEST_F(PreferStylusOverTouchTest, StylusDownAfterTouch) {
+    NotifyMotionArgs args;
+
+    args = generateMotionArgs(0 /*downTime*/, 0 /*eventTime*/, DOWN, {{1, 2}}, TOUCHSCREEN);
+    assertNotBlocked(args);
+
+    args = generateMotionArgs(0 /*downTime*/, 1 /*eventTime*/, MOVE, {{1, 3}}, TOUCHSCREEN);
+    assertNotBlocked(args);
+
+    args = generateMotionArgs(0 /*downTime*/, 2 /*eventTime*/, UP, {{1, 3}}, TOUCHSCREEN);
+    assertNotBlocked(args);
+
+    // Stylus goes down
+    args = generateMotionArgs(3 /*downTime*/, 3 /*eventTime*/, DOWN, {{10, 30}}, STYLUS);
+    assertNotBlocked(args);
+}
+
+/**
  * New touch events should be simply blocked (dropped) when stylus is down. No CANCEL event should
  * be generated.
  */
@@ -247,4 +273,230 @@
     assertNotBlocked(args);
 }
 
+/**
+ * If an event with mixed stylus and touch pointers is encountered, it should be ignored. Touches
+ * from such should pass, even if stylus from the same device goes down.
+ */
+TEST_F(PreferStylusOverTouchTest, MixedStylusAndTouchPointersAreIgnored) {
+    NotifyMotionArgs args;
+
+    // Event from a stylus device, but with finger tool type
+    args = generateMotionArgs(1 /*downTime*/, 1 /*eventTime*/, DOWN, {{1, 2}}, STYLUS);
+    // Keep source stylus, but make the tool type touch
+    args.pointerProperties[0].toolType = AMOTION_EVENT_TOOL_TYPE_FINGER;
+    assertNotBlocked(args);
+
+    // Second pointer (stylus pointer) goes down, from the same device
+    args = generateMotionArgs(1 /*downTime*/, 2 /*eventTime*/, POINTER_1_DOWN, {{1, 2}, {10, 20}},
+                              STYLUS);
+    // Keep source stylus, but make the tool type touch
+    args.pointerProperties[0].toolType = AMOTION_EVENT_TOOL_TYPE_STYLUS;
+    assertNotBlocked(args);
+
+    // Second pointer (stylus pointer) goes down, from the same device
+    args = generateMotionArgs(1 /*downTime*/, 3 /*eventTime*/, MOVE, {{2, 3}, {11, 21}}, STYLUS);
+    // Keep source stylus, but make the tool type touch
+    args.pointerProperties[0].toolType = AMOTION_EVENT_TOOL_TYPE_FINGER;
+    assertNotBlocked(args);
+}
+
+/**
+ * When there are two touch devices, stylus down should cancel all current touch streams.
+ */
+TEST_F(PreferStylusOverTouchTest, TouchFromTwoDevicesAndStylus) {
+    NotifyMotionArgs touch1Down =
+            generateMotionArgs(1 /*downTime*/, 1 /*eventTime*/, DOWN, {{1, 2}}, TOUCHSCREEN);
+    assertNotBlocked(touch1Down);
+
+    NotifyMotionArgs touch2Down =
+            generateMotionArgs(2 /*downTime*/, 2 /*eventTime*/, DOWN, {{3, 4}}, TOUCHSCREEN);
+    touch2Down.deviceId = SECOND_TOUCH_DEVICE_ID;
+    assertNotBlocked(touch2Down);
+
+    NotifyMotionArgs stylusDown =
+            generateMotionArgs(3 /*downTime*/, 3 /*eventTime*/, DOWN, {{10, 30}}, STYLUS);
+    NotifyMotionArgs cancelArgs1 = touch1Down;
+    cancelArgs1.action = CANCEL;
+    cancelArgs1.flags |= AMOTION_EVENT_FLAG_CANCELED;
+    NotifyMotionArgs cancelArgs2 = touch2Down;
+    cancelArgs2.action = CANCEL;
+    cancelArgs2.flags |= AMOTION_EVENT_FLAG_CANCELED;
+    assertResponse(stylusDown, {cancelArgs1, cancelArgs2, stylusDown});
+}
+
+/**
+ * Touch should be canceled when stylus goes down. After the stylus lifts up, the touch from that
+ * device should continue to be canceled.
+ * If one of the devices is already canceled, it should remain canceled, but new touches from a
+ * different device should go through.
+ */
+TEST_F(PreferStylusOverTouchTest, AllTouchMustLiftAfterCanceledByStylus) {
+    // First device touches down
+    NotifyMotionArgs touch1Down =
+            generateMotionArgs(1 /*downTime*/, 1 /*eventTime*/, DOWN, {{1, 2}}, TOUCHSCREEN);
+    assertNotBlocked(touch1Down);
+
+    // Stylus goes down - touch should be canceled
+    NotifyMotionArgs stylusDown =
+            generateMotionArgs(2 /*downTime*/, 2 /*eventTime*/, DOWN, {{10, 30}}, STYLUS);
+    NotifyMotionArgs cancelArgs1 = touch1Down;
+    cancelArgs1.action = CANCEL;
+    cancelArgs1.flags |= AMOTION_EVENT_FLAG_CANCELED;
+    assertResponse(stylusDown, {cancelArgs1, stylusDown});
+
+    // Stylus goes up
+    NotifyMotionArgs stylusUp =
+            generateMotionArgs(2 /*downTime*/, 3 /*eventTime*/, UP, {{10, 30}}, STYLUS);
+    assertNotBlocked(stylusUp);
+
+    // Touch from the first device remains blocked
+    NotifyMotionArgs touch1Move =
+            generateMotionArgs(1 /*downTime*/, 4 /*eventTime*/, MOVE, {{2, 3}}, TOUCHSCREEN);
+    assertDropped(touch1Move);
+
+    // Second touch goes down. It should not be blocked because stylus has already lifted.
+    NotifyMotionArgs touch2Down =
+            generateMotionArgs(5 /*downTime*/, 5 /*eventTime*/, DOWN, {{31, 32}}, TOUCHSCREEN);
+    touch2Down.deviceId = SECOND_TOUCH_DEVICE_ID;
+    assertNotBlocked(touch2Down);
+
+    // First device is lifted up. It's already been canceled, so the UP event should be dropped.
+    NotifyMotionArgs touch1Up =
+            generateMotionArgs(1 /*downTime*/, 6 /*eventTime*/, UP, {{2, 3}}, TOUCHSCREEN);
+    assertDropped(touch1Up);
+
+    // Touch from second device touch should continue to work
+    NotifyMotionArgs touch2Move =
+            generateMotionArgs(5 /*downTime*/, 7 /*eventTime*/, MOVE, {{32, 33}}, TOUCHSCREEN);
+    touch2Move.deviceId = SECOND_TOUCH_DEVICE_ID;
+    assertNotBlocked(touch2Move);
+
+    // Second touch lifts up
+    NotifyMotionArgs touch2Up =
+            generateMotionArgs(5 /*downTime*/, 8 /*eventTime*/, UP, {{32, 33}}, TOUCHSCREEN);
+    touch2Up.deviceId = SECOND_TOUCH_DEVICE_ID;
+    assertNotBlocked(touch2Up);
+
+    // Now that all touch has been lifted, new touch from either first or second device should work
+    NotifyMotionArgs touch3Down =
+            generateMotionArgs(9 /*downTime*/, 9 /*eventTime*/, DOWN, {{1, 2}}, TOUCHSCREEN);
+    assertNotBlocked(touch3Down);
+
+    NotifyMotionArgs touch4Down =
+            generateMotionArgs(10 /*downTime*/, 10 /*eventTime*/, DOWN, {{100, 200}}, TOUCHSCREEN);
+    touch4Down.deviceId = SECOND_TOUCH_DEVICE_ID;
+    assertNotBlocked(touch4Down);
+}
+
+/**
+ * When we don't know that a specific device does both stylus and touch, and we only see touch
+ * pointers from it, we should treat it as a touch device. That means, the device events should be
+ * canceled when stylus from another device goes down. When we detect simultaneous touch and stylus
+ * from this device though, we should just pass this device through without canceling anything.
+ *
+ * In this test:
+ * 1. Start by touching down with device 1
+ * 2. Device 2 has stylus going down
+ * 3. Device 1 should be canceled.
+ * 4. When we add stylus pointers to the device 1, they should continue to be canceled.
+ * 5. Device 1 lifts up.
+ * 6. Subsequent events from device 1 should not be canceled even if stylus is down.
+ * 7. If a reset happens, and such device is no longer there, then we should
+ * Therefore, the device 1 is "ignored" and does not participate into "prefer stylus over touch"
+ * behaviour.
+ */
+TEST_F(PreferStylusOverTouchTest, MixedStylusAndTouchDeviceIsCanceledAtFirst) {
+    // Touch from device 1 goes down
+    NotifyMotionArgs touchDown =
+            generateMotionArgs(1 /*downTime*/, 1 /*eventTime*/, DOWN, {{1, 2}}, TOUCHSCREEN);
+    touchDown.source = STYLUS;
+    assertNotBlocked(touchDown);
+
+    // Stylus from device 2 goes down. Touch should be canceled.
+    NotifyMotionArgs args =
+            generateMotionArgs(2 /*downTime*/, 2 /*eventTime*/, DOWN, {{10, 20}}, STYLUS);
+    NotifyMotionArgs cancelTouchArgs = touchDown;
+    cancelTouchArgs.action = CANCEL;
+    cancelTouchArgs.flags |= AMOTION_EVENT_FLAG_CANCELED;
+    assertResponse(args, {cancelTouchArgs, args});
+
+    // Introduce a stylus pointer into the device 1 stream. It should be ignored.
+    args = generateMotionArgs(1 /*downTime*/, 3 /*eventTime*/, POINTER_1_DOWN, {{1, 2}, {3, 4}},
+                              TOUCHSCREEN);
+    args.pointerProperties[1].toolType = AMOTION_EVENT_TOOL_TYPE_STYLUS;
+    args.source = STYLUS;
+    assertDropped(args);
+
+    // Lift up touch from the mixed touch/stylus device
+    args = generateMotionArgs(1 /*downTime*/, 4 /*eventTime*/, CANCEL, {{1, 2}, {3, 4}},
+                              TOUCHSCREEN);
+    args.pointerProperties[1].toolType = AMOTION_EVENT_TOOL_TYPE_STYLUS;
+    args.source = STYLUS;
+    assertDropped(args);
+
+    // Stylus from device 2 is still down. Since the device 1 is now identified as a mixed
+    // touch/stylus device, its events should go through, even if they are touch.
+    args = generateMotionArgs(5 /*downTime*/, 5 /*eventTime*/, DOWN, {{21, 22}}, TOUCHSCREEN);
+    touchDown.source = STYLUS;
+    assertResponse(args, {args});
+
+    // Reconfigure such that only the stylus device remains
+    InputDeviceInfo stylusDevice;
+    stylusDevice.initialize(STYLUS_DEVICE_ID, 1 /*generation*/, 1 /*controllerNumber*/,
+                            {} /*identifier*/, "stylus device", false /*external*/,
+                            false /*hasMic*/);
+    notifyInputDevicesChanged({stylusDevice});
+    // The touchscreen device was removed, so we no longer remember anything about it. We should
+    // again start blocking touch events from it.
+    args = generateMotionArgs(6 /*downTime*/, 6 /*eventTime*/, DOWN, {{1, 2}}, TOUCHSCREEN);
+    args.source = STYLUS;
+    assertDropped(args);
+}
+
+/**
+ * If two styli are active at the same time, touch should be blocked until both of them are lifted.
+ * If one of them lifts, touch should continue to be blocked.
+ */
+TEST_F(PreferStylusOverTouchTest, TouchIsBlockedWhenTwoStyliAreUsed) {
+    NotifyMotionArgs args;
+
+    // First stylus is down
+    assertNotBlocked(generateMotionArgs(0 /*downTime*/, 0 /*eventTime*/, DOWN, {{10, 30}}, STYLUS));
+
+    // Second stylus is down
+    args = generateMotionArgs(1 /*downTime*/, 1 /*eventTime*/, DOWN, {{20, 40}}, STYLUS);
+    args.deviceId = SECOND_STYLUS_DEVICE_ID;
+    assertNotBlocked(args);
+
+    // Touch goes down. It should be ignored.
+    args = generateMotionArgs(2 /*downTime*/, 2 /*eventTime*/, DOWN, {{1, 2}}, TOUCHSCREEN);
+    assertDropped(args);
+
+    // Lift the first stylus
+    args = generateMotionArgs(0 /*downTime*/, 3 /*eventTime*/, UP, {{10, 30}}, STYLUS);
+    assertNotBlocked(args);
+
+    // Touch should continue to be blocked
+    args = generateMotionArgs(2 /*downTime*/, 4 /*eventTime*/, UP, {{1, 2}}, TOUCHSCREEN);
+    assertDropped(args);
+
+    // New touch should be blocked because second stylus is still down
+    args = generateMotionArgs(5 /*downTime*/, 5 /*eventTime*/, DOWN, {{5, 6}}, TOUCHSCREEN);
+    assertDropped(args);
+
+    // Second stylus goes up
+    args = generateMotionArgs(1 /*downTime*/, 6 /*eventTime*/, UP, {{20, 40}}, STYLUS);
+    args.deviceId = SECOND_STYLUS_DEVICE_ID;
+    assertNotBlocked(args);
+
+    // Current touch gesture should continue to be blocked
+    // Touch should continue to be blocked
+    args = generateMotionArgs(5 /*downTime*/, 7 /*eventTime*/, UP, {{5, 6}}, TOUCHSCREEN);
+    assertDropped(args);
+
+    // Now that all styli were lifted, new touch should go through
+    args = generateMotionArgs(8 /*downTime*/, 8 /*eventTime*/, DOWN, {{7, 8}}, TOUCHSCREEN);
+    assertNotBlocked(args);
+}
+
 } // namespace android
diff --git a/services/inputflinger/tests/UnwantedInteractionBlocker_test.cpp b/services/inputflinger/tests/UnwantedInteractionBlocker_test.cpp
index b2f8eb3..e378096 100644
--- a/services/inputflinger/tests/UnwantedInteractionBlocker_test.cpp
+++ b/services/inputflinger/tests/UnwantedInteractionBlocker_test.cpp
@@ -519,6 +519,34 @@
             &(args = generateMotionArgs(0 /*downTime*/, 4 /*eventTime*/, MOVE, {{7, 8, 9}})));
 }
 
+/**
+ * Send a touch event, and then a stylus event. Make sure that both work.
+ */
+TEST_F(UnwantedInteractionBlockerTest, StylusAfterTouchWorks) {
+    NotifyMotionArgs args;
+    mBlocker->notifyInputDevicesChanged({generateTestDeviceInfo()});
+    args = generateMotionArgs(0 /*downTime*/, 0 /*eventTime*/, DOWN, {{1, 2, 3}});
+    mBlocker->notifyMotion(&args);
+    args = generateMotionArgs(0 /*downTime*/, 1 /*eventTime*/, MOVE, {{4, 5, 6}});
+    mBlocker->notifyMotion(&args);
+    args = generateMotionArgs(0 /*downTime*/, 2 /*eventTime*/, UP, {{4, 5, 6}});
+    mBlocker->notifyMotion(&args);
+
+    // Now touch down stylus
+    args = generateMotionArgs(3 /*downTime*/, 3 /*eventTime*/, DOWN, {{10, 20, 30}});
+    args.pointerProperties[0].toolType = AMOTION_EVENT_TOOL_TYPE_STYLUS;
+    args.source |= AINPUT_SOURCE_STYLUS;
+    mBlocker->notifyMotion(&args);
+    args = generateMotionArgs(3 /*downTime*/, 4 /*eventTime*/, MOVE, {{40, 50, 60}});
+    args.pointerProperties[0].toolType = AMOTION_EVENT_TOOL_TYPE_STYLUS;
+    args.source |= AINPUT_SOURCE_STYLUS;
+    mBlocker->notifyMotion(&args);
+    args = generateMotionArgs(3 /*downTime*/, 5 /*eventTime*/, UP, {{40, 50, 60}});
+    args.pointerProperties[0].toolType = AMOTION_EVENT_TOOL_TYPE_STYLUS;
+    args.source |= AINPUT_SOURCE_STYLUS;
+    mBlocker->notifyMotion(&args);
+}
+
 using UnwantedInteractionBlockerTestDeathTest = UnwantedInteractionBlockerTest;
 
 /**