InputDispatcher: Implement stylus interceptor
A stylus interceptor is a window that receives all stylus events within
its touchable bounds, while letting all other events be dispatched to
windows behind it. This makes it possible for the framework to create a
stylus interceptor on top of another app to implement handwriting
recognition.
The feature has no effect when the window flag FLAG_NOT_TOUCHABLE is
not set.
The feature has no effect when the window is not a trusted overlay.
Test: atest inputflinger_tests
Change-Id: Id4833840aa3088d21333d3ea08beffbded4debbc
diff --git a/libs/gui/WindowInfo.cpp b/libs/gui/WindowInfo.cpp
index e92be01..8d356aa 100644
--- a/libs/gui/WindowInfo.cpp
+++ b/libs/gui/WindowInfo.cpp
@@ -47,6 +47,10 @@
return inputFeatures.test(Feature::SPY);
}
+bool WindowInfo::interceptsStylus() const {
+ return inputFeatures.test(Feature::INTERCEPTS_STYLUS);
+}
+
bool WindowInfo::overlaps(const WindowInfo* other) const {
return frameLeft < other->frameRight && frameRight > other->frameLeft &&
frameTop < other->frameBottom && frameBottom > other->frameTop;
diff --git a/libs/gui/include/gui/WindowInfo.h b/libs/gui/include/gui/WindowInfo.h
index 808afe4..0273717 100644
--- a/libs/gui/include/gui/WindowInfo.h
+++ b/libs/gui/include/gui/WindowInfo.h
@@ -138,6 +138,7 @@
DROP_INPUT = 1u << 3,
DROP_INPUT_IF_OBSCURED = 1u << 4,
SPY = 1u << 5,
+ INTERCEPTS_STYLUS = 1u << 6,
};
/* These values are filled in by the WM and passed through SurfaceFlinger
@@ -218,6 +219,8 @@
bool isSpy() const;
+ bool interceptsStylus() const;
+
bool overlaps(const WindowInfo* other) const;
bool operator==(const WindowInfo& inputChannel) const;
diff --git a/services/inputflinger/dispatcher/InputDispatcher.cpp b/services/inputflinger/dispatcher/InputDispatcher.cpp
index 6ff2450..98a73d4 100644
--- a/services/inputflinger/dispatcher/InputDispatcher.cpp
+++ b/services/inputflinger/dispatcher/InputDispatcher.cpp
@@ -530,12 +530,14 @@
}
// Returns true if the given window can accept pointer events at the given display location.
-bool windowAcceptsTouchAt(const WindowInfo& windowInfo, int32_t displayId, int32_t x, int32_t y) {
+bool windowAcceptsTouchAt(const WindowInfo& windowInfo, int32_t displayId, int32_t x, int32_t y,
+ bool isStylus) {
if (windowInfo.displayId != displayId || !windowInfo.visible) {
return false;
}
const auto flags = windowInfo.flags;
- if (flags.test(WindowInfo::Flag::NOT_TOUCHABLE)) {
+ const bool windowCanInterceptTouch = isStylus && windowInfo.interceptsStylus();
+ if (flags.test(WindowInfo::Flag::NOT_TOUCHABLE) && !windowCanInterceptTouch) {
return false;
}
const bool isModalWindow = !flags.test(WindowInfo::Flag::NOT_FOCUSABLE) &&
@@ -546,6 +548,12 @@
return true;
}
+bool isPointerFromStylus(const MotionEntry& entry, int32_t pointerIndex) {
+ return isFromSource(entry.source, AINPUT_SOURCE_STYLUS) &&
+ (entry.pointerProperties[pointerIndex].toolType == AMOTION_EVENT_TOOL_TYPE_STYLUS ||
+ entry.pointerProperties[pointerIndex].toolType == AMOTION_EVENT_TOOL_TYPE_ERASER);
+}
+
} // namespace
// --- InputDispatcher ---
@@ -939,8 +947,10 @@
motionEntry.pointerCoords[0].getAxisValue(AMOTION_EVENT_AXIS_X));
int32_t y = static_cast<int32_t>(
motionEntry.pointerCoords[0].getAxisValue(AMOTION_EVENT_AXIS_Y));
+
+ const bool isStylus = isPointerFromStylus(motionEntry, 0 /*pointerIndex*/);
sp<WindowInfoHandle> touchedWindowHandle =
- findTouchedWindowAtLocked(displayId, x, y, nullptr);
+ findTouchedWindowAtLocked(displayId, x, y, nullptr, isStylus);
if (touchedWindowHandle != nullptr &&
touchedWindowHandle->getApplicationToken() !=
mAwaitedFocusedApplication->getApplicationToken()) {
@@ -1045,6 +1055,7 @@
sp<WindowInfoHandle> InputDispatcher::findTouchedWindowAtLocked(int32_t displayId, int32_t x,
int32_t y, TouchState* touchState,
+ bool isStylus,
bool addOutsideTargets,
bool ignoreDragWindow) {
if (addOutsideTargets && touchState == nullptr) {
@@ -1058,7 +1069,7 @@
}
const WindowInfo& info = *windowHandle->getInfo();
- if (!info.isSpy() && windowAcceptsTouchAt(info, displayId, x, y)) {
+ if (!info.isSpy() && windowAcceptsTouchAt(info, displayId, x, y, isStylus)) {
return windowHandle;
}
@@ -1070,16 +1081,15 @@
return nullptr;
}
-std::vector<sp<WindowInfoHandle>> InputDispatcher::findTouchedSpyWindowsAtLocked(int32_t displayId,
- int32_t x,
- int32_t y) const {
+std::vector<sp<WindowInfoHandle>> InputDispatcher::findTouchedSpyWindowsAtLocked(
+ int32_t displayId, int32_t x, int32_t y, bool isStylus) const {
// Traverse windows from front to back and gather the touched spy windows.
std::vector<sp<WindowInfoHandle>> spyWindows;
const auto& windowHandles = getWindowHandlesLocked(displayId);
for (const sp<WindowInfoHandle>& windowHandle : windowHandles) {
const WindowInfo& info = *windowHandle->getInfo();
- if (!windowAcceptsTouchAt(info, displayId, x, y)) {
+ if (!windowAcceptsTouchAt(info, displayId, x, y, isStylus)) {
continue;
}
if (!info.isSpy()) {
@@ -2062,8 +2072,9 @@
y = int32_t(entry.pointerCoords[pointerIndex].getAxisValue(AMOTION_EVENT_AXIS_Y));
}
const bool isDown = maskedAction == AMOTION_EVENT_ACTION_DOWN;
+ const bool isStylus = isPointerFromStylus(entry, pointerIndex);
newTouchedWindowHandle = findTouchedWindowAtLocked(displayId, x, y, &tempTouchState,
- isDown /*addOutsideTargets*/);
+ isStylus, isDown /*addOutsideTargets*/);
// Handle the case where we did not find a window.
if (newTouchedWindowHandle == nullptr) {
@@ -2100,7 +2111,7 @@
}
std::vector<sp<WindowInfoHandle>> newTouchedWindows =
- findTouchedSpyWindowsAtLocked(displayId, x, y);
+ findTouchedSpyWindowsAtLocked(displayId, x, y, isStylus);
if (newTouchedWindowHandle != nullptr) {
// Process the foreground window first so that it is the first to receive the event.
newTouchedWindows.insert(newTouchedWindows.begin(), newTouchedWindowHandle);
@@ -2213,9 +2224,11 @@
const int32_t x = int32_t(entry.pointerCoords[0].getAxisValue(AMOTION_EVENT_AXIS_X));
const int32_t y = int32_t(entry.pointerCoords[0].getAxisValue(AMOTION_EVENT_AXIS_Y));
+ const bool isStylus = isPointerFromStylus(entry, 0 /*pointerIndex*/);
sp<WindowInfoHandle> oldTouchedWindowHandle =
tempTouchState.getFirstForegroundWindowHandle();
- newTouchedWindowHandle = findTouchedWindowAtLocked(displayId, x, y, &tempTouchState);
+ newTouchedWindowHandle =
+ findTouchedWindowAtLocked(displayId, x, y, &tempTouchState, isStylus);
// Drop touch events if requested by input feature
if (newTouchedWindowHandle != nullptr &&
@@ -2477,8 +2490,12 @@
}
void InputDispatcher::finishDragAndDrop(int32_t displayId, float x, float y) {
+ // Prevent stylus interceptor windows from affecting drag and drop behavior for now, until we
+ // have an explicit reason to support it.
+ constexpr bool isStylus = false;
+
const sp<WindowInfoHandle> dropWindow =
- findTouchedWindowAtLocked(displayId, x, y, nullptr /*touchState*/,
+ findTouchedWindowAtLocked(displayId, x, y, nullptr /*touchState*/, isStylus,
false /*addOutsideTargets*/, true /*ignoreDragWindow*/);
if (dropWindow) {
vec2 local = dropWindow->getInfo()->transform.transform(x, y);
@@ -2511,8 +2528,12 @@
return;
}
+ // Prevent stylus interceptor windows from affecting drag and drop behavior for now, until
+ // we have an explicit reason to support it.
+ constexpr bool isStylus = false;
+
const sp<WindowInfoHandle> hoverWindowHandle =
- findTouchedWindowAtLocked(entry.displayId, x, y, nullptr /*touchState*/,
+ findTouchedWindowAtLocked(entry.displayId, x, y, nullptr /*touchState*/, isStylus,
false /*addOutsideTargets*/, true /*ignoreDragWindow*/);
// enqueue drag exit if needed.
if (hoverWindowHandle != mDragState->dragHoverWindowHandle &&
@@ -4685,15 +4706,22 @@
ALOGD("setInputWindows displayId=%" PRId32 " %s", displayId, windowList.c_str());
}
- // Ensure all tokens are null if the window has feature NO_INPUT_CHANNEL
+ // Check preconditions for new input windows
for (const sp<WindowInfoHandle>& window : windowInfoHandles) {
- const bool noInputWindow =
- window->getInfo()->inputFeatures.test(WindowInfo::Feature::NO_INPUT_CHANNEL);
+ const WindowInfo& info = *window->getInfo();
+
+ // Ensure all tokens are null if the window has feature NO_INPUT_CHANNEL
+ const bool noInputWindow = info.inputFeatures.test(WindowInfo::Feature::NO_INPUT_CHANNEL);
if (noInputWindow && window->getToken() != nullptr) {
ALOGE("%s has feature NO_INPUT_WINDOW, but a non-null token. Clearing",
window->getName().c_str());
window->releaseChannel();
}
+
+ // Ensure all stylus interceptors are trusted overlays
+ LOG_ALWAYS_FATAL_IF(info.interceptsStylus() && !info.trustedOverlay,
+ "%s has feature INTERCEPTS_STYLUS, but is not a trusted overlay.",
+ window->getName().c_str());
}
// Copy old handles for release if they are no longer present.
diff --git a/services/inputflinger/dispatcher/InputDispatcher.h b/services/inputflinger/dispatcher/InputDispatcher.h
index fb9c81b..f8ab7ee 100644
--- a/services/inputflinger/dispatcher/InputDispatcher.h
+++ b/services/inputflinger/dispatcher/InputDispatcher.h
@@ -234,16 +234,12 @@
// to transfer focus to a new application.
std::shared_ptr<EventEntry> mNextUnblockedEvent GUARDED_BY(mLock);
- sp<android::gui::WindowInfoHandle> findTouchedWindowAtLocked(int32_t displayId, int32_t x,
- int32_t y, TouchState* touchState,
- bool addOutsideTargets = false,
- bool ignoreDragWindow = false)
- REQUIRES(mLock);
+ sp<android::gui::WindowInfoHandle> findTouchedWindowAtLocked(
+ int32_t displayId, int32_t x, int32_t y, TouchState* touchState, bool isStylus = false,
+ bool addOutsideTargets = false, bool ignoreDragWindow = false) REQUIRES(mLock);
- std::vector<sp<android::gui::WindowInfoHandle>> findTouchedSpyWindowsAtLocked(int32_t displayId,
- int32_t x,
- int32_t y) const
- REQUIRES(mLock);
+ std::vector<sp<android::gui::WindowInfoHandle>> findTouchedSpyWindowsAtLocked(
+ int32_t displayId, int32_t x, int32_t y, bool isStylus) const REQUIRES(mLock);
sp<Connection> getConnectionLocked(const sp<IBinder>& inputConnectionToken) const
REQUIRES(mLock);
diff --git a/services/inputflinger/tests/InputDispatcher_test.cpp b/services/inputflinger/tests/InputDispatcher_test.cpp
index ba70929..b2d0fb8 100644
--- a/services/inputflinger/tests/InputDispatcher_test.cpp
+++ b/services/inputflinger/tests/InputDispatcher_test.cpp
@@ -1003,6 +1003,7 @@
mInfo.ownerPid = INJECTOR_PID;
mInfo.ownerUid = INJECTOR_UID;
mInfo.displayId = displayId;
+ mInfo.trustedOverlay = false;
}
sp<FakeWindowHandle> clone(
@@ -1054,7 +1055,9 @@
void setFlags(Flags<WindowInfo::Flag> flags) { mInfo.flags = flags; }
- void setInputFeatures(WindowInfo::Feature features) { mInfo.inputFeatures = features; }
+ void setInputFeatures(Flags<WindowInfo::Feature> features) { mInfo.inputFeatures = features; }
+
+ void setTrustedOverlay(bool trustedOverlay) { mInfo.trustedOverlay = trustedOverlay; }
void setWindowTransform(float dsdx, float dtdx, float dtdy, float dsdy) {
mInfo.transform.set(dsdx, dtdx, dtdy, dsdy);
@@ -6600,4 +6603,97 @@
spyRight->consumeMotionDown();
}
+class InputDispatcherStylusInterceptorTest : public InputDispatcherTest {
+public:
+ std::pair<sp<FakeWindowHandle>, sp<FakeWindowHandle>> setupStylusOverlayScenario() {
+ std::shared_ptr<FakeApplicationHandle> overlayApplication =
+ std::make_shared<FakeApplicationHandle>();
+ sp<FakeWindowHandle> overlay =
+ new FakeWindowHandle(overlayApplication, mDispatcher, "Stylus interceptor window",
+ ADISPLAY_ID_DEFAULT);
+ overlay->setFocusable(false);
+ overlay->setOwnerInfo(111, 111);
+ overlay->setFlags(WindowInfo::Flag::NOT_TOUCHABLE | WindowInfo::Flag::SPLIT_TOUCH);
+ overlay->setInputFeatures(WindowInfo::Feature::INTERCEPTS_STYLUS);
+ overlay->setTrustedOverlay(true);
+
+ std::shared_ptr<FakeApplicationHandle> application =
+ std::make_shared<FakeApplicationHandle>();
+ sp<FakeWindowHandle> window =
+ new FakeWindowHandle(application, mDispatcher, "Application window",
+ ADISPLAY_ID_DEFAULT);
+ window->setFocusable(true);
+ window->setOwnerInfo(222, 222);
+ window->setFlags(WindowInfo::Flag::SPLIT_TOUCH);
+
+ mDispatcher->setFocusedApplication(ADISPLAY_ID_DEFAULT, application);
+ mDispatcher->setInputWindows({{ADISPLAY_ID_DEFAULT, {overlay, window}}});
+ setFocusedWindow(window);
+ window->consumeFocusEvent(true /*hasFocus*/, true /*inTouchMode*/);
+ return {std::move(overlay), std::move(window)};
+ }
+
+ void sendFingerEvent(int32_t action) {
+ NotifyMotionArgs motionArgs =
+ generateMotionArgs(action, AINPUT_SOURCE_TOUCHSCREEN | AINPUT_SOURCE_STYLUS,
+ ADISPLAY_ID_DEFAULT, {PointF{20, 20}});
+ mDispatcher->notifyMotion(&motionArgs);
+ }
+
+ void sendStylusEvent(int32_t action) {
+ NotifyMotionArgs motionArgs =
+ generateMotionArgs(action, AINPUT_SOURCE_TOUCHSCREEN | AINPUT_SOURCE_STYLUS,
+ ADISPLAY_ID_DEFAULT, {PointF{30, 40}});
+ motionArgs.pointerProperties[0].toolType = AMOTION_EVENT_TOOL_TYPE_STYLUS;
+ mDispatcher->notifyMotion(&motionArgs);
+ }
+};
+
+TEST_F(InputDispatcherStylusInterceptorTest, UntrustedOverlay_AbortsDispatcher) {
+ auto [overlay, window] = setupStylusOverlayScenario();
+ overlay->setTrustedOverlay(false);
+ // Configuring an untrusted overlay as a stylus interceptor should cause Dispatcher to abort.
+ ASSERT_DEATH(mDispatcher->setInputWindows({{ADISPLAY_ID_DEFAULT, {overlay, window}}}),
+ ".* not a trusted overlay");
+}
+
+TEST_F(InputDispatcherStylusInterceptorTest, ConsmesOnlyStylusEvents) {
+ auto [overlay, window] = setupStylusOverlayScenario();
+ mDispatcher->setInputWindows({{ADISPLAY_ID_DEFAULT, {overlay, window}}});
+
+ sendStylusEvent(AMOTION_EVENT_ACTION_DOWN);
+ overlay->consumeMotionDown();
+ sendStylusEvent(AMOTION_EVENT_ACTION_UP);
+ overlay->consumeMotionUp();
+
+ sendFingerEvent(AMOTION_EVENT_ACTION_DOWN);
+ window->consumeMotionDown();
+ sendFingerEvent(AMOTION_EVENT_ACTION_UP);
+ window->consumeMotionUp();
+
+ overlay->assertNoEvents();
+ window->assertNoEvents();
+}
+
+TEST_F(InputDispatcherStylusInterceptorTest, SpyWindowStylusInterceptor) {
+ auto [overlay, window] = setupStylusOverlayScenario();
+ overlay->setInputFeatures(overlay->getInfo()->inputFeatures | WindowInfo::Feature::SPY);
+ mDispatcher->setInputWindows({{ADISPLAY_ID_DEFAULT, {overlay, window}}});
+
+ sendStylusEvent(AMOTION_EVENT_ACTION_DOWN);
+ overlay->consumeMotionDown();
+ window->consumeMotionDown();
+ sendStylusEvent(AMOTION_EVENT_ACTION_UP);
+ overlay->consumeMotionUp();
+ window->consumeMotionUp();
+
+ sendFingerEvent(AMOTION_EVENT_ACTION_DOWN);
+ window->consumeMotionDown();
+ sendFingerEvent(AMOTION_EVENT_ACTION_UP);
+ window->consumeMotionUp();
+
+ overlay->assertNoEvents();
+ window->assertNoEvents();
+}
+
} // namespace android::inputdispatcher