Generate HOVER_EXIT if touchable region changes
The dispatcher crashes whenever we try adding pointers to a target,
but the target already had those pointers under a different mode.
Only one dispatch mode is allowed for each event, per target.
That's generally true, but there were instances where we ended up
with multiple dispatch modes for the same event.
For example, if a window changes bounds (for example, becomes hidden, or
the touchable region shrinks), the pointer would no longer be inside
that window. And if this window is also watching for outside touch,
then an ACTION_DOWN event of the mouse would cause 2 things to happen:
1) This would cause HOVER_EXIT, since the pointer is no longer in that
window
2) This would case ACTION_OUTSIDE, since the pointer is tapped outside
of the window's bounds.
This previously led to a crash in dispatcher because there would be two
different dispatch modes for the same window for the same ACTION_DOWN
event.
Update being done here:
When a window resizes, it's possible that the hovering pointer is no
longer inside that window's bounds. We should generate a HOVER_EXIT
event at that point; this will ensure that the next mouse event is still
maintaining the contract that it's only dispatched with a single mode.
Bug: 346121425
Test: TEST=inputflinger_tests; m $TEST && $ANDROID_HOST_OUT/nativetest64/$TEST/$TEST --gtest_filter="*MouseClickUnderShrinkingTrustedOverlay"
Flag: EXEMPT bugfix
Merged-In: Ia5e5ac0421b127231d36ed375538eaebfb8994ac
Change-Id: Ia5e5ac0421b127231d36ed375538eaebfb8994ac
diff --git a/services/inputflinger/dispatcher/CancelationOptions.h b/services/inputflinger/dispatcher/CancelationOptions.h
index 4a0889f..568d348 100644
--- a/services/inputflinger/dispatcher/CancelationOptions.h
+++ b/services/inputflinger/dispatcher/CancelationOptions.h
@@ -32,7 +32,8 @@
CANCEL_POINTER_EVENTS = 1,
CANCEL_NON_POINTER_EVENTS = 2,
CANCEL_FALLBACK_EVENTS = 3,
- ftl_last = CANCEL_FALLBACK_EVENTS,
+ CANCEL_HOVER_EVENTS = 4,
+ ftl_last = CANCEL_HOVER_EVENTS
};
// The criterion to use to determine which events should be canceled.
diff --git a/services/inputflinger/dispatcher/InputDispatcher.cpp b/services/inputflinger/dispatcher/InputDispatcher.cpp
index 2161e09..7eb7e36 100644
--- a/services/inputflinger/dispatcher/InputDispatcher.cpp
+++ b/services/inputflinger/dispatcher/InputDispatcher.cpp
@@ -746,7 +746,8 @@
}
touchedWindow.dispatchMode = InputTarget::DispatchMode::AS_IS;
}
- touchedWindow.addHoveringPointer(entry.deviceId, pointer);
+ const auto [x, y] = resolveTouchedPosition(entry);
+ touchedWindow.addHoveringPointer(entry.deviceId, pointer, x, y);
if (canReceiveForegroundTouches(*newWindow->getInfo())) {
touchedWindow.targetFlags |= InputTarget::Flags::FOREGROUND;
}
@@ -873,6 +874,8 @@
return {false, true};
case CancelationOptions::Mode::CANCEL_FALLBACK_EVENTS:
return {false, true};
+ case CancelationOptions::Mode::CANCEL_HOVER_EVENTS:
+ return {true, false};
}
}
@@ -2511,7 +2514,8 @@
if (isHoverAction) {
// The "windowHandle" is the target of this hovering pointer.
- tempTouchState.addHoveringPointerToWindow(windowHandle, entry.deviceId, pointer);
+ tempTouchState.addHoveringPointerToWindow(windowHandle, entry.deviceId, pointer, x,
+ y);
}
// Set target flags.
@@ -5437,6 +5441,31 @@
}
}
+ // Check if the hovering should stop because the window is no longer eligible to receive it
+ // (for example, if the touchable region changed)
+ if (const auto& it = mTouchStatesByDisplay.find(displayId); it != mTouchStatesByDisplay.end()) {
+ TouchState& state = it->second;
+ for (TouchedWindow& touchedWindow : state.windows) {
+ std::vector<DeviceId> erasedDevices = touchedWindow.eraseHoveringPointersIf(
+ [this, displayId, &touchedWindow](const PointerProperties& properties, float x,
+ float y) REQUIRES(mLock) {
+ const bool isStylus = properties.toolType == ToolType::STYLUS;
+ const ui::Transform displayTransform = getTransformLocked(displayId);
+ const bool stillAcceptsTouch =
+ windowAcceptsTouchAt(*touchedWindow.windowHandle->getInfo(),
+ displayId, x, y, isStylus, displayTransform);
+ return !stillAcceptsTouch;
+ });
+
+ for (DeviceId deviceId : erasedDevices) {
+ CancelationOptions options(CancelationOptions::Mode::CANCEL_HOVER_EVENTS,
+ "WindowInfo changed", traceContext.getTracker());
+ options.deviceId = deviceId;
+ synthesizeCancelationEventsForWindowLocked(touchedWindow.windowHandle, options);
+ }
+ }
+ }
+
// Release information for windows that are no longer present.
// This ensures that unused input channels are released promptly.
// Otherwise, they might stick around until the window handle is destroyed
diff --git a/services/inputflinger/dispatcher/InputState.cpp b/services/inputflinger/dispatcher/InputState.cpp
index dfbe02f..e283fc3 100644
--- a/services/inputflinger/dispatcher/InputState.cpp
+++ b/services/inputflinger/dispatcher/InputState.cpp
@@ -638,6 +638,8 @@
return memento.source & AINPUT_SOURCE_CLASS_POINTER;
case CancelationOptions::Mode::CANCEL_NON_POINTER_EVENTS:
return !(memento.source & AINPUT_SOURCE_CLASS_POINTER);
+ case CancelationOptions::Mode::CANCEL_HOVER_EVENTS:
+ return memento.hovering;
default:
return false;
}
diff --git a/services/inputflinger/dispatcher/TouchState.cpp b/services/inputflinger/dispatcher/TouchState.cpp
index 0c9ad3c..2bf63be 100644
--- a/services/inputflinger/dispatcher/TouchState.cpp
+++ b/services/inputflinger/dispatcher/TouchState.cpp
@@ -112,17 +112,18 @@
}
void TouchState::addHoveringPointerToWindow(const sp<WindowInfoHandle>& windowHandle,
- DeviceId deviceId, const PointerProperties& pointer) {
+ DeviceId deviceId, const PointerProperties& pointer,
+ float x, float y) {
for (TouchedWindow& touchedWindow : windows) {
if (touchedWindow.windowHandle == windowHandle) {
- touchedWindow.addHoveringPointer(deviceId, pointer);
+ touchedWindow.addHoveringPointer(deviceId, pointer, x, y);
return;
}
}
TouchedWindow touchedWindow;
touchedWindow.windowHandle = windowHandle;
- touchedWindow.addHoveringPointer(deviceId, pointer);
+ touchedWindow.addHoveringPointer(deviceId, pointer, x, y);
windows.push_back(touchedWindow);
}
diff --git a/services/inputflinger/dispatcher/TouchState.h b/services/inputflinger/dispatcher/TouchState.h
index 9d4bb3d..3fbe584 100644
--- a/services/inputflinger/dispatcher/TouchState.h
+++ b/services/inputflinger/dispatcher/TouchState.h
@@ -49,7 +49,8 @@
DeviceId deviceId, const std::vector<PointerProperties>& touchingPointers,
std::optional<nsecs_t> firstDownTimeInTarget = std::nullopt);
void addHoveringPointerToWindow(const sp<android::gui::WindowInfoHandle>& windowHandle,
- DeviceId deviceId, const PointerProperties& pointer);
+ DeviceId deviceId, const PointerProperties& pointer, float x,
+ float y);
void removeHoveringPointer(DeviceId deviceId, int32_t pointerId);
void clearHoveringPointers(DeviceId deviceId);
diff --git a/services/inputflinger/dispatcher/TouchedWindow.cpp b/services/inputflinger/dispatcher/TouchedWindow.cpp
index 1f86f66..fa5be1a 100644
--- a/services/inputflinger/dispatcher/TouchedWindow.cpp
+++ b/services/inputflinger/dispatcher/TouchedWindow.cpp
@@ -36,6 +36,13 @@
}) != pointers.end();
}
+bool hasPointerId(const std::vector<TouchedWindow::HoveringPointer>& pointers, int32_t pointerId) {
+ return std::find_if(pointers.begin(), pointers.end(),
+ [&pointerId](const TouchedWindow::HoveringPointer& pointer) {
+ return pointer.properties.id == pointerId;
+ }) != pointers.end();
+}
+
} // namespace
bool TouchedWindow::hasHoveringPointers() const {
@@ -78,16 +85,18 @@
return hasPointerId(state.hoveringPointers, pointerId);
}
-void TouchedWindow::addHoveringPointer(DeviceId deviceId, const PointerProperties& pointer) {
- std::vector<PointerProperties>& hoveringPointers = mDeviceStates[deviceId].hoveringPointers;
+void TouchedWindow::addHoveringPointer(DeviceId deviceId, const PointerProperties& properties,
+ float x, float y) {
+ std::vector<HoveringPointer>& hoveringPointers = mDeviceStates[deviceId].hoveringPointers;
const size_t initialSize = hoveringPointers.size();
- std::erase_if(hoveringPointers, [&pointer](const PointerProperties& properties) {
- return properties.id == pointer.id;
+ std::erase_if(hoveringPointers, [&properties](const HoveringPointer& pointer) {
+ return pointer.properties.id == properties.id;
});
if (hoveringPointers.size() != initialSize) {
- LOG(ERROR) << __func__ << ": " << pointer << ", device " << deviceId << " was in " << *this;
+ LOG(ERROR) << __func__ << ": " << properties << ", device " << deviceId << " was in "
+ << *this;
}
- hoveringPointers.push_back(pointer);
+ hoveringPointers.push_back({properties, x, y});
}
Result<void> TouchedWindow::addTouchingPointers(DeviceId deviceId,
@@ -173,8 +182,8 @@
return true;
}
}
- for (const PointerProperties& properties : state.hoveringPointers) {
- if (properties.toolType == ToolType::STYLUS) {
+ for (const HoveringPointer& pointer : state.hoveringPointers) {
+ if (pointer.properties.toolType == ToolType::STYLUS) {
return true;
}
}
@@ -270,8 +279,8 @@
}
DeviceState& state = stateIt->second;
- std::erase_if(state.hoveringPointers, [&pointerId](const PointerProperties& properties) {
- return properties.id == pointerId;
+ std::erase_if(state.hoveringPointers, [&pointerId](const HoveringPointer& pointer) {
+ return pointer.properties.id == pointerId;
});
if (!state.hasPointers()) {
@@ -279,6 +288,22 @@
}
}
+std::vector<DeviceId> TouchedWindow::eraseHoveringPointersIf(
+ std::function<bool(const PointerProperties&, float /*x*/, float /*y*/)> condition) {
+ std::vector<DeviceId> erasedDevices;
+ for (auto& [deviceId, state] : mDeviceStates) {
+ std::erase_if(state.hoveringPointers, [&](const HoveringPointer& pointer) {
+ if (condition(pointer.properties, pointer.x, pointer.y)) {
+ erasedDevices.push_back(deviceId);
+ return true;
+ }
+ return false;
+ });
+ }
+
+ return erasedDevices;
+}
+
void TouchedWindow::removeAllHoveringPointersForDevice(DeviceId deviceId) {
const auto stateIt = mDeviceStates.find(deviceId);
if (stateIt == mDeviceStates.end()) {
@@ -312,6 +337,11 @@
return out;
}
+std::ostream& operator<<(std::ostream& out, const TouchedWindow::HoveringPointer& pointer) {
+ out << pointer.properties << " at (" << pointer.x << ", " << pointer.y << ")";
+ return out;
+}
+
std::ostream& operator<<(std::ostream& out, const TouchedWindow& window) {
out << window.dump();
return out;
diff --git a/services/inputflinger/dispatcher/TouchedWindow.h b/services/inputflinger/dispatcher/TouchedWindow.h
index 4f0ad16..c38681e 100644
--- a/services/inputflinger/dispatcher/TouchedWindow.h
+++ b/services/inputflinger/dispatcher/TouchedWindow.h
@@ -38,7 +38,7 @@
bool hasHoveringPointers() const;
bool hasHoveringPointers(DeviceId deviceId) const;
bool hasHoveringPointer(DeviceId deviceId, int32_t pointerId) const;
- void addHoveringPointer(DeviceId deviceId, const PointerProperties& pointer);
+ void addHoveringPointer(DeviceId deviceId, const PointerProperties& pointer, float x, float y);
void removeHoveringPointer(DeviceId deviceId, int32_t pointerId);
// Touching
@@ -69,6 +69,15 @@
void clearHoveringPointers(DeviceId deviceId);
std::string dump() const;
+ struct HoveringPointer {
+ PointerProperties properties;
+ float x;
+ float y;
+ };
+
+ std::vector<DeviceId> eraseHoveringPointersIf(
+ std::function<bool(const PointerProperties&, float /*x*/, float /*y*/)> condition);
+
private:
struct DeviceState {
std::vector<PointerProperties> touchingPointers;
@@ -78,7 +87,7 @@
// NOTE: This is not initialized in case of HOVER entry/exit and DISPATCH_AS_OUTSIDE
// scenario.
std::optional<nsecs_t> downTimeInTarget;
- std::vector<PointerProperties> hoveringPointers;
+ std::vector<HoveringPointer> hoveringPointers;
bool hasPointers() const { return !touchingPointers.empty() || !hoveringPointers.empty(); };
};
diff --git a/services/inputflinger/tests/InputDispatcher_test.cpp b/services/inputflinger/tests/InputDispatcher_test.cpp
index 73ab0da..48930ef 100644
--- a/services/inputflinger/tests/InputDispatcher_test.cpp
+++ b/services/inputflinger/tests/InputDispatcher_test.cpp
@@ -1923,6 +1923,99 @@
window->consumeMotionEvent(WithMotionAction(ACTION_SCROLL));
}
+/**
+ * Two windows: a trusted overlay and a regular window underneath. Both windows are visible.
+ * Mouse is hovered, and the hover event should only go to the overlay.
+ * However, next, the touchable region of the trusted overlay shrinks. The mouse position hasn't
+ * changed, but the cursor would now end up hovering above the regular window underneatch.
+ * If the mouse is now clicked, this would generate an ACTION_DOWN event, which would go to the
+ * regular window. However, the trusted overlay is also watching for outside touch.
+ * The trusted overlay should get two events:
+ * 1) The ACTION_OUTSIDE event, since the click is now not inside its touchable region
+ * 2) The HOVER_EXIT event, since the mouse pointer is no longer hovering inside this window
+ *
+ * This test reproduces a crash where there is an overlap between dispatch modes for the trusted
+ * overlay touch target, since the event is causing both an ACTION_OUTSIDE, and as a HOVER_EXIT.
+ */
+TEST_F(InputDispatcherTest, MouseClickUnderShrinkingTrustedOverlay) {
+ std::shared_ptr<FakeApplicationHandle> app = std::make_shared<FakeApplicationHandle>();
+ sp<FakeWindowHandle> overlay = sp<FakeWindowHandle>::make(app, mDispatcher, "Trusted overlay",
+ ui::LogicalDisplayId::DEFAULT);
+ overlay->setTrustedOverlay(true);
+ overlay->setWatchOutsideTouch(true);
+ overlay->setFrame(Rect(0, 0, 200, 200));
+
+ sp<FakeWindowHandle> window = sp<FakeWindowHandle>::make(app, mDispatcher, "Regular window",
+ ui::LogicalDisplayId::DEFAULT);
+ window->setFrame(Rect(0, 0, 200, 200));
+
+ mDispatcher->onWindowInfosChanged({{*overlay->getInfo(), *window->getInfo()}, {}, 0, 0});
+ // Hover the mouse into the overlay
+ mDispatcher->notifyMotion(MotionArgsBuilder(ACTION_HOVER_MOVE, AINPUT_SOURCE_MOUSE)
+ .pointer(PointerBuilder(0, ToolType::MOUSE).x(100).y(110))
+ .build());
+ overlay->consumeMotionEvent(WithMotionAction(ACTION_HOVER_ENTER));
+
+ // Now, shrink the touchable region of the overlay! This will cause the cursor to suddenly have
+ // the regular window as the touch target
+ overlay->setTouchableRegion(Region({0, 0, 0, 0}));
+ mDispatcher->onWindowInfosChanged({{*overlay->getInfo(), *window->getInfo()}, {}, 0, 0});
+
+ // Now we can click with the mouse. The click should go into the regular window
+ mDispatcher->notifyMotion(MotionArgsBuilder(ACTION_DOWN, AINPUT_SOURCE_MOUSE)
+ .pointer(PointerBuilder(0, ToolType::MOUSE).x(100).y(110))
+ .build());
+ overlay->consumeMotionEvent(WithMotionAction(ACTION_HOVER_EXIT));
+ overlay->consumeMotionEvent(WithMotionAction(ACTION_OUTSIDE));
+ window->consumeMotionEvent(WithMotionAction(ACTION_DOWN));
+}
+
+/**
+ * Similar to above, but also has a spy on top that also catches the HOVER
+ * events. Also, instead of ACTION_DOWN, we are continuing to send the hovering
+ * stream to ensure that the spy receives hover events correctly.
+ */
+TEST_F(InputDispatcherTest, MouseClickUnderShrinkingTrustedOverlayWithSpy) {
+ std::shared_ptr<FakeApplicationHandle> app = std::make_shared<FakeApplicationHandle>();
+ sp<FakeWindowHandle> spyWindow =
+ sp<FakeWindowHandle>::make(app, mDispatcher, "Spy", ui::LogicalDisplayId::DEFAULT);
+ spyWindow->setFrame(Rect(0, 0, 200, 200));
+ spyWindow->setTrustedOverlay(true);
+ spyWindow->setSpy(true);
+ sp<FakeWindowHandle> overlay = sp<FakeWindowHandle>::make(app, mDispatcher, "Trusted overlay",
+ ui::LogicalDisplayId::DEFAULT);
+ overlay->setTrustedOverlay(true);
+ overlay->setWatchOutsideTouch(true);
+ overlay->setFrame(Rect(0, 0, 200, 200));
+
+ sp<FakeWindowHandle> window = sp<FakeWindowHandle>::make(app, mDispatcher, "Regular window",
+ ui::LogicalDisplayId::DEFAULT);
+ window->setFrame(Rect(0, 0, 200, 200));
+
+ mDispatcher->onWindowInfosChanged(
+ {{*spyWindow->getInfo(), *overlay->getInfo(), *window->getInfo()}, {}, 0, 0});
+ // Hover the mouse into the overlay
+ mDispatcher->notifyMotion(MotionArgsBuilder(ACTION_HOVER_MOVE, AINPUT_SOURCE_MOUSE)
+ .pointer(PointerBuilder(0, ToolType::MOUSE).x(100).y(110))
+ .build());
+ spyWindow->consumeMotionEvent(WithMotionAction(ACTION_HOVER_ENTER));
+ overlay->consumeMotionEvent(WithMotionAction(ACTION_HOVER_ENTER));
+
+ // Now, shrink the touchable region of the overlay! This will cause the cursor to suddenly have
+ // the regular window as the touch target
+ overlay->setTouchableRegion(Region({0, 0, 0, 0}));
+ mDispatcher->onWindowInfosChanged(
+ {{*spyWindow->getInfo(), *overlay->getInfo(), *window->getInfo()}, {}, 0, 0});
+
+ // Now we can click with the mouse. The click should go into the regular window
+ mDispatcher->notifyMotion(MotionArgsBuilder(ACTION_HOVER_MOVE, AINPUT_SOURCE_MOUSE)
+ .pointer(PointerBuilder(0, ToolType::MOUSE).x(110).y(110))
+ .build());
+ spyWindow->consumeMotionEvent(WithMotionAction(ACTION_HOVER_MOVE));
+ overlay->consumeMotionEvent(WithMotionAction(ACTION_HOVER_EXIT));
+ window->consumeMotionEvent(WithMotionAction(ACTION_HOVER_ENTER));
+}
+
using InputDispatcherMultiDeviceTest = InputDispatcherTest;
/**