Track hovering pointers explicitly

Before this CL, hovering window was tracked separately inside
InputDispatcher. This window was getting updated in various places.
Inconsistent motion streams, like HOVER_ENTER->DOWN->UP->HOVER_EXIT were
possible.

In this CL, we track hovering pointers inside TouchedWindow. At all
times, the currently tracked pointer must always be in the touch state.
The hovering pointer is removed when HOVER_EXIT is received.

This CL also establishes the foundation for multi-device, multi-pointer
streams, by storing hovering pointers inside TouchedWindow per-device.

Eventually, we can look into separately creating touched targets from
updating the touch state. This approach is partially used in this CL.

TouchState is used to keep track of where the hovering pointer is
currently. The 'addHoveringWindowsLocked' function returns the
equivalent of InputTargets. Eventually, we can change this to return
InputTargets.

Bug: 211379801
Test: m inputflinger_tests && adb sync data && adb shell -t /data/nativetest64/inputflinger_tests/inputflinger_tests
Change-Id: I047926e53b846c96807aed45fb585e031e5b88b9
diff --git a/include/input/PrintTools.h b/include/input/PrintTools.h
index e24344b..a20544b 100644
--- a/include/input/PrintTools.h
+++ b/include/input/PrintTools.h
@@ -16,6 +16,7 @@
 
 #pragma once
 
+#include <bitset>
 #include <map>
 #include <optional>
 #include <set>
@@ -23,6 +24,11 @@
 
 namespace android {
 
+template <size_t N>
+std::string bitsetToString(const std::bitset<N>& bitset) {
+    return bitset.to_string();
+}
+
 template <typename T>
 inline std::string constToString(const T& v) {
     return std::to_string(v);
diff --git a/services/inputflinger/dispatcher/InputDispatcher.cpp b/services/inputflinger/dispatcher/InputDispatcher.cpp
index 4aac377..4d3e6de 100644
--- a/services/inputflinger/dispatcher/InputDispatcher.cpp
+++ b/services/inputflinger/dispatcher/InputDispatcher.cpp
@@ -554,6 +554,65 @@
     return std::nullopt;
 }
 
+/**
+ * Compare the old touch state to the new touch state, and generate the corresponding touched
+ * windows (== input targets).
+ * If a window had the hovering pointer, but now it doesn't, produce HOVER_EXIT for that window.
+ * If the pointer just entered the new window, produce HOVER_ENTER.
+ * For pointers remaining in the window, produce HOVER_MOVE.
+ */
+std::vector<TouchedWindow> getHoveringWindowsLocked(const TouchState* oldState,
+                                                    const TouchState& newTouchState,
+                                                    const MotionEntry& entry) {
+    std::vector<TouchedWindow> out;
+    const int32_t maskedAction = MotionEvent::getActionMasked(entry.action);
+    if (maskedAction != AMOTION_EVENT_ACTION_HOVER_ENTER &&
+        maskedAction != AMOTION_EVENT_ACTION_HOVER_MOVE &&
+        maskedAction != AMOTION_EVENT_ACTION_HOVER_EXIT) {
+        // Not a hover event - don't need to do anything
+        return out;
+    }
+
+    // We should consider all hovering pointers here. But for now, just use the first one
+    const int32_t pointerId = entry.pointerProperties[0].id;
+
+    std::set<sp<WindowInfoHandle>> oldWindows;
+    if (oldState != nullptr) {
+        oldWindows = oldState->getWindowsWithHoveringPointer(entry.deviceId, pointerId);
+    }
+
+    std::set<sp<WindowInfoHandle>> newWindows =
+            newTouchState.getWindowsWithHoveringPointer(entry.deviceId, pointerId);
+
+    // If the pointer is no longer in the new window set, send HOVER_EXIT.
+    for (const sp<WindowInfoHandle>& oldWindow : oldWindows) {
+        if (newWindows.find(oldWindow) == newWindows.end()) {
+            TouchedWindow touchedWindow;
+            touchedWindow.windowHandle = oldWindow;
+            touchedWindow.targetFlags = InputTarget::Flags::DISPATCH_AS_HOVER_EXIT;
+            touchedWindow.pointerIds.markBit(pointerId);
+            out.push_back(touchedWindow);
+        }
+    }
+
+    for (const sp<WindowInfoHandle>& newWindow : newWindows) {
+        TouchedWindow touchedWindow;
+        touchedWindow.windowHandle = newWindow;
+        if (oldWindows.find(newWindow) == oldWindows.end()) {
+            // Any windows that have this pointer now, and didn't have it before, should get
+            // HOVER_ENTER
+            touchedWindow.targetFlags = InputTarget::Flags::DISPATCH_AS_HOVER_ENTER;
+        } else {
+            // This pointer was already sent to the window. Use ACTION_HOVER_MOVE.
+            LOG_ALWAYS_FATAL_IF(maskedAction != AMOTION_EVENT_ACTION_HOVER_MOVE);
+            touchedWindow.targetFlags = InputTarget::Flags::DISPATCH_AS_IS;
+        }
+        touchedWindow.pointerIds.markBit(pointerId);
+        out.push_back(touchedWindow);
+    }
+    return out;
+}
+
 } // namespace
 
 // --- InputDispatcher ---
@@ -2083,8 +2142,6 @@
 
     // Update the touch state as needed based on the properties of the touch event.
     outInjectionResult = InputEventInjectionResult::PENDING;
-    sp<WindowInfoHandle> newHoverWindowHandle(mLastHoverWindowHandle);
-    sp<WindowInfoHandle> newTouchedWindowHandle;
 
     // Copy current touch state into tempTouchState.
     // This state will be used to update mTouchStatesByDisplay at the end of this function.
@@ -2117,7 +2174,7 @@
             outInjectionResult = InputEventInjectionResult::FAILED;
             return touchedWindows; // wrong device
         }
-        tempTouchState.reset();
+        tempTouchState.clearWindowsWithoutPointers();
         tempTouchState.deviceId = entry.deviceId;
         tempTouchState.source = entry.source;
         isSplit = false;
@@ -2130,14 +2187,21 @@
         return touchedWindows; // wrong device
     }
 
+    if (isHoverAction) {
+        // For hover actions, we will treat 'tempTouchState' as a new state, so let's erase
+        // all of the existing hovering pointers and recompute.
+        tempTouchState.clearHoveringPointers();
+    }
+
     if (newGesture || (isSplit && maskedAction == AMOTION_EVENT_ACTION_POINTER_DOWN)) {
         /* Case 1: New splittable pointer going down, or need target for hover or scroll. */
         const auto [x, y] = resolveTouchedPosition(entry);
         const int32_t pointerIndex = getMotionEventActionPointerIndex(action);
         const bool isDown = maskedAction == AMOTION_EVENT_ACTION_DOWN;
         const bool isStylus = isPointerFromStylus(entry, pointerIndex);
-        newTouchedWindowHandle = findTouchedWindowAtLocked(displayId, x, y, &tempTouchState,
-                                                           isStylus, isDown /*addOutsideTargets*/);
+        sp<WindowInfoHandle> newTouchedWindowHandle =
+                findTouchedWindowAtLocked(displayId, x, y, &tempTouchState, isStylus,
+                                          isDown /*addOutsideTargets*/);
 
         // Handle the case where we did not find a window.
         if (newTouchedWindowHandle == nullptr) {
@@ -2172,15 +2236,6 @@
             isSplit = !isFromMouse;
         }
 
-        // Update hover state.
-        if (newTouchedWindowHandle != nullptr) {
-            if (maskedAction == AMOTION_EVENT_ACTION_HOVER_EXIT) {
-                newHoverWindowHandle = nullptr;
-            } else if (isHoverAction) {
-                newHoverWindowHandle = newTouchedWindowHandle;
-            }
-        }
-
         std::vector<sp<WindowInfoHandle>> newTouchedWindows =
                 findTouchedSpyWindowsAtLocked(displayId, x, y, isStylus);
         if (newTouchedWindowHandle != nullptr) {
@@ -2200,6 +2255,18 @@
                 continue;
             }
 
+            if (isHoverAction) {
+                const int32_t pointerId = entry.pointerProperties[0].id;
+                if (maskedAction == AMOTION_EVENT_ACTION_HOVER_EXIT) {
+                    // Pointer left. Remove it
+                    tempTouchState.removeHoveringPointer(entry.deviceId, pointerId);
+                } else {
+                    // The "windowHandle" is the target of this hovering pointer.
+                    tempTouchState.addHoveringPointerToWindow(windowHandle, entry.deviceId,
+                                                              pointerId);
+                }
+            }
+
             // Set target flags.
             ftl::Flags<InputTarget::Flags> targetFlags = InputTarget::Flags::DISPATCH_AS_IS;
 
@@ -2219,7 +2286,9 @@
 
             // Update the temporary touch state.
             BitSet32 pointerIds;
-            pointerIds.markBit(entry.pointerProperties[pointerIndex].id);
+            if (!isHoverAction) {
+                pointerIds.markBit(entry.pointerProperties[pointerIndex].id);
+            }
 
             tempTouchState.addOrUpdateWindow(windowHandle, targetFlags, pointerIds,
                                              entry.eventTime);
@@ -2255,7 +2324,7 @@
             const bool isStylus = isPointerFromStylus(entry, 0 /*pointerIndex*/);
             sp<WindowInfoHandle> oldTouchedWindowHandle =
                     tempTouchState.getFirstForegroundWindowHandle();
-            newTouchedWindowHandle =
+            sp<WindowInfoHandle> newTouchedWindowHandle =
                     findTouchedWindowAtLocked(displayId, x, y, &tempTouchState, isStylus);
 
             // Verify targeted injection.
@@ -2326,36 +2395,11 @@
     }
 
     // Update dispatching for hover enter and exit.
-    if (newHoverWindowHandle != mLastHoverWindowHandle) {
-        // Let the previous window know that the hover sequence is over, unless we already did
-        // it when dispatching it as is to newTouchedWindowHandle.
-        if (mLastHoverWindowHandle != nullptr &&
-            (maskedAction != AMOTION_EVENT_ACTION_HOVER_EXIT ||
-             mLastHoverWindowHandle != newTouchedWindowHandle)) {
-            if (DEBUG_HOVER) {
-                ALOGD("Sending hover exit event to window %s.",
-                      mLastHoverWindowHandle->getName().c_str());
-            }
-            tempTouchState.addOrUpdateWindow(mLastHoverWindowHandle,
-                                             InputTarget::Flags::DISPATCH_AS_HOVER_EXIT,
-                                             BitSet32(0));
-        }
-
-        // Let the new window know that the hover sequence is starting, unless we already did it
-        // when dispatching it as is to newTouchedWindowHandle.
-        if (newHoverWindowHandle != nullptr &&
-            (maskedAction != AMOTION_EVENT_ACTION_HOVER_ENTER ||
-             newHoverWindowHandle != newTouchedWindowHandle)) {
-            if (DEBUG_HOVER) {
-                ALOGD("Sending hover enter event to window %s.",
-                      newHoverWindowHandle->getName().c_str());
-            }
-            tempTouchState.addOrUpdateWindow(newHoverWindowHandle,
-                                             InputTarget::Flags::DISPATCH_AS_HOVER_ENTER,
-                                             BitSet32(0));
-        }
+    {
+        std::vector<TouchedWindow> hoveringWindows =
+                getHoveringWindowsLocked(oldState, tempTouchState, entry);
+        touchedWindows.insert(touchedWindows.end(), hoveringWindows.begin(), hoveringWindows.end());
     }
-
     // Ensure that we have at least one foreground window or at least one window that cannot be a
     // foreground target. If we only have windows that are not receiving foreground touches (e.g. we
     // only have windows getting ACTION_OUTSIDE), then drop the event, because there is no window
@@ -2445,10 +2489,13 @@
         }
     }
 
-    // Success!  Output targets.
-    touchedWindows = tempTouchState.windows;
-    outInjectionResult = InputEventInjectionResult::SUCCEEDED;
+    // Success!  Output targets for everything except hovers.
+    if (!isHoverAction) {
+        touchedWindows.insert(touchedWindows.end(), tempTouchState.windows.begin(),
+                              tempTouchState.windows.end());
+    }
 
+    outInjectionResult = InputEventInjectionResult::SUCCEEDED;
     // Drop the outside or hover touch windows since we will not care about them
     // in the next iteration.
     tempTouchState.filterNonAsIsTouchWindows();
@@ -2469,14 +2516,16 @@
                      "Conflicting pointer actions: Hover received while pointer was down.");
             *outConflictingPointerActions = true;
         }
-        tempTouchState.reset();
         if (maskedAction == AMOTION_EVENT_ACTION_HOVER_ENTER ||
             maskedAction == AMOTION_EVENT_ACTION_HOVER_MOVE) {
             tempTouchState.deviceId = entry.deviceId;
             tempTouchState.source = entry.source;
         }
-    } else if (maskedAction == AMOTION_EVENT_ACTION_UP ||
-               maskedAction == AMOTION_EVENT_ACTION_CANCEL) {
+    } else if (maskedAction == AMOTION_EVENT_ACTION_UP) {
+        // Pointer went up.
+        tempTouchState.removeTouchedPointer(entry.pointerProperties[0].id);
+        tempTouchState.clearWindowsWithoutPointers();
+    } else if (maskedAction == AMOTION_EVENT_ACTION_CANCEL) {
         // All pointers up or canceled.
         tempTouchState.reset();
     } else if (maskedAction == AMOTION_EVENT_ACTION_DOWN) {
@@ -2515,9 +2564,6 @@
         mTouchStatesByDisplay.erase(displayId);
     }
 
-    // Update hover state.
-    mLastHoverWindowHandle = newHoverWindowHandle;
-
     return touchedWindows;
 }
 
@@ -4809,14 +4855,6 @@
     updateWindowHandlesForDisplayLocked(windowInfoHandles, displayId);
 
     const std::vector<sp<WindowInfoHandle>>& windowHandles = getWindowHandlesLocked(displayId);
-    if (mLastHoverWindowHandle) {
-        const WindowInfo* lastHoverWindowInfo = mLastHoverWindowHandle->getInfo();
-        if (lastHoverWindowInfo->displayId == displayId &&
-            std::find(windowHandles.begin(), windowHandles.end(), mLastHoverWindowHandle) ==
-                    windowHandles.end()) {
-            mLastHoverWindowHandle = nullptr;
-        }
-    }
 
     std::optional<FocusResolver::FocusChanges> changes =
             mFocusResolver.setInputWindows(displayId, windowHandles);
@@ -5264,7 +5302,6 @@
 
     mAnrTracker.clear();
     mTouchStatesByDisplay.clear();
-    mLastHoverWindowHandle.clear();
     mReplacedKeys.clear();
 }
 
@@ -6454,7 +6491,6 @@
         synthesizeCancelationEventsForAllConnectionsLocked(options);
 
         mTouchStatesByDisplay.clear();
-        mLastHoverWindowHandle.clear();
     }
     // Wake up poll loop since there might be work to do.
     mLooper->wake();
diff --git a/services/inputflinger/dispatcher/InputDispatcher.h b/services/inputflinger/dispatcher/InputDispatcher.h
index 5efb39e..3c7ddfa 100644
--- a/services/inputflinger/dispatcher/InputDispatcher.h
+++ b/services/inputflinger/dispatcher/InputDispatcher.h
@@ -530,9 +530,6 @@
     // prevent unneeded wakeups.
     AnrTracker mAnrTracker GUARDED_BY(mLock);
 
-    // Contains the last window which received a hover event.
-    sp<android::gui::WindowInfoHandle> mLastHoverWindowHandle GUARDED_BY(mLock);
-
     void cancelEventsForAnrLocked(const sp<Connection>& connection) REQUIRES(mLock);
     // If a focused application changes, we should stop counting down the "no focused window" time,
     // because we will have no way of knowing when the previous application actually added a window.
diff --git a/services/inputflinger/dispatcher/TouchState.cpp b/services/inputflinger/dispatcher/TouchState.cpp
index c21af9e..f120fc9 100644
--- a/services/inputflinger/dispatcher/TouchState.cpp
+++ b/services/inputflinger/dispatcher/TouchState.cpp
@@ -31,10 +31,30 @@
     *this = TouchState();
 }
 
+void TouchState::removeTouchedPointer(int32_t pointerId) {
+    for (TouchedWindow& touchedWindow : windows) {
+        touchedWindow.pointerIds.clearBit(pointerId);
+    }
+}
+
+void TouchState::clearHoveringPointers() {
+    for (TouchedWindow& touchedWindow : windows) {
+        touchedWindow.clearHoveringPointers();
+    }
+}
+
+void TouchState::clearWindowsWithoutPointers() {
+    std::erase_if(windows, [](const TouchedWindow& w) {
+        return w.pointerIds.isEmpty() && !w.hasHoveringPointers();
+    });
+}
+
 void TouchState::addOrUpdateWindow(const sp<WindowInfoHandle>& windowHandle,
                                    ftl::Flags<InputTarget::Flags> targetFlags, BitSet32 pointerIds,
                                    std::optional<nsecs_t> eventTime) {
     for (TouchedWindow& touchedWindow : windows) {
+        // We do not compare windows by token here because two windows that share the same token
+        // may have a different transform
         if (touchedWindow.windowHandle == windowHandle) {
             touchedWindow.targetFlags |= targetFlags;
             if (targetFlags.test(InputTarget::Flags::DISPATCH_AS_SLIPPERY_EXIT)) {
@@ -59,6 +79,21 @@
     windows.push_back(touchedWindow);
 }
 
+void TouchState::addHoveringPointerToWindow(const sp<WindowInfoHandle>& windowHandle,
+                                            int32_t hoveringDeviceId, int32_t hoveringPointerId) {
+    for (TouchedWindow& touchedWindow : windows) {
+        if (touchedWindow.windowHandle == windowHandle) {
+            touchedWindow.addHoveringPointer(hoveringDeviceId, hoveringPointerId);
+            return;
+        }
+    }
+
+    TouchedWindow touchedWindow;
+    touchedWindow.windowHandle = windowHandle;
+    touchedWindow.addHoveringPointer(hoveringDeviceId, hoveringPointerId);
+    windows.push_back(touchedWindow);
+}
+
 void TouchState::removeWindowByToken(const sp<IBinder>& token) {
     for (size_t i = 0; i < windows.size(); i++) {
         if (windows[i].windowHandle->getToken() == token) {
@@ -145,6 +180,26 @@
                        [](const TouchedWindow& window) { return !window.pointerIds.isEmpty(); });
 }
 
+std::set<sp<WindowInfoHandle>> TouchState::getWindowsWithHoveringPointer(int32_t hoveringDeviceId,
+                                                                         int32_t pointerId) const {
+    std::set<sp<WindowInfoHandle>> out;
+    for (const TouchedWindow& window : windows) {
+        if (window.hasHoveringPointer(hoveringDeviceId, pointerId)) {
+            out.insert(window.windowHandle);
+        }
+    }
+    return out;
+}
+
+void TouchState::removeHoveringPointer(int32_t hoveringDeviceId, int32_t hoveringPointerId) {
+    for (TouchedWindow& window : windows) {
+        window.removeHoveringPointer(hoveringDeviceId, hoveringPointerId);
+    }
+    std::erase_if(windows, [](const TouchedWindow& w) {
+        return w.pointerIds.isEmpty() && !w.hasHoveringPointers();
+    });
+}
+
 std::string TouchState::dump() const {
     std::string out;
     out += StringPrintf("deviceId=%d, source=%s\n", deviceId,
diff --git a/services/inputflinger/dispatcher/TouchState.h b/services/inputflinger/dispatcher/TouchState.h
index 77c1cdf..b75e6ef 100644
--- a/services/inputflinger/dispatcher/TouchState.h
+++ b/services/inputflinger/dispatcher/TouchState.h
@@ -16,6 +16,7 @@
 
 #pragma once
 
+#include <set>
 #include "TouchedWindow.h"
 
 namespace android {
@@ -39,9 +40,16 @@
     TouchState& operator=(const TouchState&) = default;
 
     void reset();
+    void clearWindowsWithoutPointers();
+
+    void removeTouchedPointer(int32_t pointerId);
     void addOrUpdateWindow(const sp<android::gui::WindowInfoHandle>& windowHandle,
                            ftl::Flags<InputTarget::Flags> targetFlags, BitSet32 pointerIds,
                            std::optional<nsecs_t> eventTime = std::nullopt);
+    void addHoveringPointerToWindow(const sp<android::gui::WindowInfoHandle>& windowHandle,
+                                    int32_t deviceId, int32_t hoveringPointerId);
+    void removeHoveringPointer(int32_t deviceId, int32_t hoveringPointerId);
+    void clearHoveringPointers();
     void removeWindowByToken(const sp<IBinder>& token);
     void filterNonAsIsTouchWindows();
 
@@ -56,6 +64,9 @@
     sp<android::gui::WindowInfoHandle> getWallpaperWindow() const;
     // Whether any of the windows are currently being touched
     bool isDown() const;
+
+    std::set<sp<android::gui::WindowInfoHandle>> getWindowsWithHoveringPointer(
+            int32_t deviceId, int32_t pointerId) const;
     std::string dump() const;
 };
 
diff --git a/services/inputflinger/dispatcher/TouchedWindow.cpp b/services/inputflinger/dispatcher/TouchedWindow.cpp
index af74598..3704edd 100644
--- a/services/inputflinger/dispatcher/TouchedWindow.cpp
+++ b/services/inputflinger/dispatcher/TouchedWindow.cpp
@@ -25,11 +25,49 @@
 
 namespace inputdispatcher {
 
+bool TouchedWindow::hasHoveringPointers() const {
+    return !mHoveringPointerIdsByDevice.empty();
+}
+
+void TouchedWindow::clearHoveringPointers() {
+    mHoveringPointerIdsByDevice.clear();
+}
+
+bool TouchedWindow::hasHoveringPointer(int32_t deviceId, int32_t pointerId) const {
+    auto it = mHoveringPointerIdsByDevice.find(deviceId);
+    if (it == mHoveringPointerIdsByDevice.end()) {
+        return false;
+    }
+    return it->second.test(pointerId);
+}
+
+void TouchedWindow::addHoveringPointer(int32_t deviceId, int32_t pointerId) {
+    const auto [it, _] = mHoveringPointerIdsByDevice.insert({deviceId, {}});
+    it->second.set(pointerId);
+}
+
+void TouchedWindow::removeHoveringPointer(int32_t deviceId, int32_t pointerId) {
+    const auto it = mHoveringPointerIdsByDevice.find(deviceId);
+    if (it == mHoveringPointerIdsByDevice.end()) {
+        return;
+    }
+    it->second.set(pointerId, false);
+
+    if (it->second.none()) {
+        mHoveringPointerIdsByDevice.erase(deviceId);
+    }
+}
+
 std::string TouchedWindow::dump() const {
-    return StringPrintf("name='%s', pointerIds=0x%0x, "
-                        "targetFlags=%s, firstDownTimeInTarget=%s\n",
+    std::string out;
+    std::string hoveringPointers =
+            dumpMap(mHoveringPointerIdsByDevice, constToString, bitsetToString);
+    out += StringPrintf("name='%s', pointerIds=0x%0x, targetFlags=%s, firstDownTimeInTarget=%s, "
+                        "mHoveringPointerIdsByDevice=%s\n",
                         windowHandle->getName().c_str(), pointerIds.value,
-                        targetFlags.string().c_str(), toString(firstDownTimeInTarget).c_str());
+                        targetFlags.string().c_str(), toString(firstDownTimeInTarget).c_str(),
+                        hoveringPointers.c_str());
+    return out;
 }
 
 } // namespace inputdispatcher
diff --git a/services/inputflinger/dispatcher/TouchedWindow.h b/services/inputflinger/dispatcher/TouchedWindow.h
index dd08323..add6b61 100644
--- a/services/inputflinger/dispatcher/TouchedWindow.h
+++ b/services/inputflinger/dispatcher/TouchedWindow.h
@@ -17,7 +17,9 @@
 #pragma once
 
 #include <gui/WindowInfo.h>
+#include <input/Input.h>
 #include <utils/BitSet.h>
+#include <bitset>
 #include "InputTarget.h"
 
 namespace android {
@@ -33,7 +35,17 @@
     // Time at which the first action down occurred on this window.
     // NOTE: This is not initialized in case of HOVER entry/exit and DISPATCH_AS_OUTSIDE scenario.
     std::optional<nsecs_t> firstDownTimeInTarget;
+
+    bool hasHoveringPointers() const;
+
+    bool hasHoveringPointer(int32_t deviceId, int32_t pointerId) const;
+    void addHoveringPointer(int32_t deviceId, int32_t pointerId);
+    void removeHoveringPointer(int32_t deviceId, int32_t pointerId);
+    void clearHoveringPointers();
     std::string dump() const;
+
+private:
+    std::map<int32_t /*deviceId*/, std::bitset<MAX_POINTERS>> mHoveringPointerIdsByDevice;
 };
 
 } // namespace inputdispatcher
diff --git a/services/inputflinger/tests/InputDispatcher_test.cpp b/services/inputflinger/tests/InputDispatcher_test.cpp
index 41c174a..9881cd4 100644
--- a/services/inputflinger/tests/InputDispatcher_test.cpp
+++ b/services/inputflinger/tests/InputDispatcher_test.cpp
@@ -2135,7 +2135,6 @@
                                                          .y(400))
                                         .build()));
     windowRight->consumeMotionEvent(WithMotionAction(AMOTION_EVENT_ACTION_HOVER_ENTER));
-    windowRight->consumeMotionEvent(WithMotionAction(AMOTION_EVENT_ACTION_HOVER_MOVE));
 
     // Move cursor into left window
     ASSERT_EQ(InputEventInjectionResult::SUCCEEDED,
@@ -2148,7 +2147,6 @@
                                         .build()));
     windowRight->consumeMotionEvent(WithMotionAction(AMOTION_EVENT_ACTION_HOVER_EXIT));
     windowLeft->consumeMotionEvent(WithMotionAction(AMOTION_EVENT_ACTION_HOVER_ENTER));
-    windowLeft->consumeMotionEvent(WithMotionAction(AMOTION_EVENT_ACTION_HOVER_MOVE));
 
     // Inject a series of mouse events for a mouse click
     ASSERT_EQ(InputEventInjectionResult::SUCCEEDED,
@@ -2206,7 +2204,6 @@
                                         .build()));
     windowLeft->consumeMotionEvent(WithMotionAction(AMOTION_EVENT_ACTION_HOVER_EXIT));
     windowRight->consumeMotionEvent(WithMotionAction(AMOTION_EVENT_ACTION_HOVER_ENTER));
-    windowRight->consumeMotionEvent(WithMotionAction(AMOTION_EVENT_ACTION_HOVER_MOVE));
 
     // No more events
     windowLeft->assertNoEvents();
@@ -2268,7 +2265,6 @@
                                                          .y(400))
                                         .build()));
     window->consumeMotionEvent(WithMotionAction(AMOTION_EVENT_ACTION_HOVER_ENTER));
-
     // Inject a series of mouse events for a mouse click
     ASSERT_EQ(InputEventInjectionResult::SUCCEEDED,
               injectMotionEvent(mDispatcher,
@@ -2326,8 +2322,38 @@
 }
 
 /**
+ * Hover over a window, and then remove that window. Make sure that HOVER_EXIT for that event
+ * is generated.
+ */
+TEST_F(InputDispatcherTest, HoverExitIsSentToRemovedWindow) {
+    std::shared_ptr<FakeApplicationHandle> application = std::make_shared<FakeApplicationHandle>();
+    sp<FakeWindowHandle> window =
+            sp<FakeWindowHandle>::make(application, mDispatcher, "Window", ADISPLAY_ID_DEFAULT);
+    window->setFrame(Rect(0, 0, 1200, 800));
+
+    mDispatcher->setFocusedApplication(ADISPLAY_ID_DEFAULT, application);
+
+    mDispatcher->setInputWindows({{ADISPLAY_ID_DEFAULT, {window}}});
+
+    ASSERT_EQ(InputEventInjectionResult::SUCCEEDED,
+              injectMotionEvent(mDispatcher,
+                                MotionEventBuilder(AMOTION_EVENT_ACTION_HOVER_ENTER,
+                                                   AINPUT_SOURCE_MOUSE)
+                                        .pointer(PointerBuilder(0, AMOTION_EVENT_TOOL_TYPE_MOUSE)
+                                                         .x(300)
+                                                         .y(400))
+                                        .build()));
+    window->consumeMotionEvent(WithMotionAction(AMOTION_EVENT_ACTION_HOVER_ENTER));
+
+    // Remove the window, but keep the channel.
+    mDispatcher->setInputWindows({{ADISPLAY_ID_DEFAULT, {}}});
+    window->consumeMotionEvent(WithMotionAction(AMOTION_EVENT_ACTION_HOVER_EXIT));
+}
+
+/**
  * Inject a mouse hover event followed by a tap from touchscreen.
- * In the current implementation, the tap does not cause a HOVER_EXIT event.
+ * The tap causes a HOVER_EXIT event to be generated because the current event
+ * stream's source has been switched.
  */
 TEST_F(InputDispatcherTest, MouseHoverAndTouchTap) {
     std::shared_ptr<FakeApplicationHandle> application = std::make_shared<FakeApplicationHandle>();
@@ -2347,15 +2373,16 @@
     ASSERT_NO_FATAL_FAILURE(
             window->consumeMotionEvent(AllOf(WithMotionAction(AMOTION_EVENT_ACTION_HOVER_ENTER),
                                              WithSource(AINPUT_SOURCE_MOUSE))));
-    ASSERT_NO_FATAL_FAILURE(
-            window->consumeMotionEvent(AllOf(WithMotionAction(AMOTION_EVENT_ACTION_HOVER_MOVE),
-                                             WithSource(AINPUT_SOURCE_MOUSE))));
 
     // Tap on the window
     motionArgs = generateMotionArgs(AMOTION_EVENT_ACTION_DOWN, AINPUT_SOURCE_TOUCHSCREEN,
                                     ADISPLAY_ID_DEFAULT, {{10, 10}});
     mDispatcher->notifyMotion(&motionArgs);
     ASSERT_NO_FATAL_FAILURE(
+            window->consumeMotionEvent(AllOf(WithMotionAction(AMOTION_EVENT_ACTION_HOVER_EXIT),
+                                             WithSource(AINPUT_SOURCE_MOUSE))));
+
+    ASSERT_NO_FATAL_FAILURE(
             window->consumeMotionEvent(AllOf(WithMotionAction(AMOTION_EVENT_ACTION_DOWN),
                                              WithSource(AINPUT_SOURCE_TOUCHSCREEN))));
 
@@ -2393,7 +2420,6 @@
                                                          .y(600))
                                         .build()));
     windowDefaultDisplay->consumeMotionEvent(WithMotionAction(AMOTION_EVENT_ACTION_HOVER_ENTER));
-    windowDefaultDisplay->consumeMotionEvent(WithMotionAction(AMOTION_EVENT_ACTION_HOVER_MOVE));
 
     // Remove all windows in secondary display and check that no event happens on window in
     // primary display.