Enable cursor to transition across multiple displays
This CL enables cursor to move between displays. It uses a fake
topology that assumes all available displays are connected in the
following order:
default-display (top-edge) -> next-display (right-edge)
-> next-display (right-edge) ...
Test: atest inputflinger_tests
Test: verify cursor can move between displays as expected
Bug: 367659738
Bug: 367660694
Flag: com.android.input.flags.connected_displays_cursor
Change-Id: I8b3b7c3c0e68dca1e7ce281cb7ff6efab263a500
diff --git a/services/inputflinger/PointerChoreographer.cpp b/services/inputflinger/PointerChoreographer.cpp
index 38e5974..2434d2b 100644
--- a/services/inputflinger/PointerChoreographer.cpp
+++ b/services/inputflinger/PointerChoreographer.cpp
@@ -205,20 +205,30 @@
}
NotifyMotionArgs PointerChoreographer::processMotion(const NotifyMotionArgs& args) {
- std::scoped_lock _l(getLock());
+ NotifyMotionArgs newArgs(args);
+ PointerDisplayChange pointerDisplayChange;
+ { // acquire lock
+ std::scoped_lock _l(getLock());
+ if (isFromMouse(args)) {
+ newArgs = processMouseEventLocked(args);
+ pointerDisplayChange = calculatePointerDisplayChangeToNotify();
+ } else if (isFromTouchpad(args)) {
+ newArgs = processTouchpadEventLocked(args);
+ pointerDisplayChange = calculatePointerDisplayChangeToNotify();
+ } else if (isFromDrawingTablet(args)) {
+ processDrawingTabletEventLocked(args);
+ } else if (mStylusPointerIconEnabled && isStylusHoverEvent(args)) {
+ processStylusHoverEventLocked(args);
+ } else if (isFromSource(args.source, AINPUT_SOURCE_TOUCHSCREEN)) {
+ processTouchscreenAndStylusEventLocked(args);
+ }
+ } // release lock
- if (isFromMouse(args)) {
- return processMouseEventLocked(args);
- } else if (isFromTouchpad(args)) {
- return processTouchpadEventLocked(args);
- } else if (isFromDrawingTablet(args)) {
- processDrawingTabletEventLocked(args);
- } else if (mStylusPointerIconEnabled && isStylusHoverEvent(args)) {
- processStylusHoverEventLocked(args);
- } else if (isFromSource(args.source, AINPUT_SOURCE_TOUCHSCREEN)) {
- processTouchscreenAndStylusEventLocked(args);
+ if (pointerDisplayChange) {
+ // pointer display may have changed if mouse crossed display boundary
+ notifyPointerDisplayChange(pointerDisplayChange, mPolicy);
}
- return args;
+ return newArgs;
}
NotifyMotionArgs PointerChoreographer::processMouseEventLocked(const NotifyMotionArgs& args) {
@@ -245,7 +255,8 @@
// This is a relative mouse, so move the cursor by the specified amount.
processPointerDeviceMotionEventLocked(/*byref*/ newArgs, /*byref*/ pc);
}
- if (canUnfadeOnDisplay(displayId)) {
+ // Note displayId may have changed if the cursor moved to a different display
+ if (canUnfadeOnDisplay(newArgs.displayId)) {
pc.unfade(PointerControllerInterface::Transition::IMMEDIATE);
}
return newArgs;
@@ -272,7 +283,9 @@
newArgs.xCursorPosition = x;
newArgs.yCursorPosition = y;
}
- if (canUnfadeOnDisplay(displayId)) {
+
+ // Note displayId may have changed if the cursor moved to a different display
+ if (canUnfadeOnDisplay(newArgs.displayId)) {
pc.unfade(PointerControllerInterface::Transition::IMMEDIATE);
}
return newArgs;
@@ -283,7 +296,14 @@
const float deltaX = newArgs.pointerCoords[0].getAxisValue(AMOTION_EVENT_AXIS_RELATIVE_X);
const float deltaY = newArgs.pointerCoords[0].getAxisValue(AMOTION_EVENT_AXIS_RELATIVE_Y);
- pc.move(deltaX, deltaY);
+ FloatPoint unconsumedDelta = pc.move(deltaX, deltaY);
+ if (com::android::input::flags::connected_displays_cursor() &&
+ (std::abs(unconsumedDelta.x) > 0 || std::abs(unconsumedDelta.y) > 0)) {
+ handleUnconsumedDeltaLocked(pc, unconsumedDelta);
+ // pointer may have moved to a different viewport
+ newArgs.displayId = pc.getDisplayId();
+ }
+
const auto [x, y] = pc.getPosition();
newArgs.pointerCoords[0].setAxisValue(AMOTION_EVENT_AXIS_X, x);
newArgs.pointerCoords[0].setAxisValue(AMOTION_EVENT_AXIS_Y, y);
@@ -291,6 +311,34 @@
newArgs.yCursorPosition = y;
}
+void PointerChoreographer::handleUnconsumedDeltaLocked(PointerControllerInterface& pc,
+ const FloatPoint& unconsumedDelta) {
+ const ui::LogicalDisplayId sourceDisplayId = pc.getDisplayId();
+ const auto& sourceViewport = *findViewportByIdLocked(sourceDisplayId);
+ std::optional<const DisplayViewport*> destination =
+ findDestinationDisplayLocked(sourceViewport, unconsumedDelta);
+ if (!destination) {
+ // no adjacent display
+ return;
+ }
+
+ const DisplayViewport* destinationViewport = *destination;
+
+ if (mMousePointersByDisplay.find(destinationViewport->displayId) !=
+ mMousePointersByDisplay.end()) {
+ LOG(FATAL) << "A cursor already exists on destination display"
+ << destinationViewport->displayId;
+ }
+ mDefaultMouseDisplayId = destinationViewport->displayId;
+ auto pcNode = mMousePointersByDisplay.extract(sourceDisplayId);
+ pcNode.key() = destinationViewport->displayId;
+ mMousePointersByDisplay.insert(std::move(pcNode));
+
+ // This will place cursor at the center of the target display for now
+ // TODO (b/367660694) place the cursor at the appropriate position in destination display
+ pc.setDisplayViewport(*destinationViewport);
+}
+
void PointerChoreographer::processDrawingTabletEventLocked(const android::NotifyMotionArgs& args) {
if (args.displayId == ui::LogicalDisplayId::INVALID) {
return;
@@ -436,7 +484,8 @@
}
void PointerChoreographer::onControllerAddedOrRemovedLocked() {
- if (!com::android::input::flags::hide_pointer_indicators_for_secure_windows()) {
+ if (!com::android::input::flags::hide_pointer_indicators_for_secure_windows() &&
+ !com::android::input::flags::connected_displays_cursor()) {
return;
}
bool requireListener = !mTouchPointersByDevice.empty() || !mMousePointersByDisplay.empty() ||
@@ -502,6 +551,13 @@
mNextListener.notify(args);
}
+void PointerChoreographer::setDisplayTopology(
+ const std::unordered_map<ui::LogicalDisplayId, std::vector<AdjacentDisplay>>&
+ displayTopology) {
+ std::scoped_lock _l(getLock());
+ mTopology = displayTopology;
+}
+
void PointerChoreographer::dump(std::string& dump) {
std::scoped_lock _l(getLock());
@@ -873,6 +929,104 @@
return ConstructorDelegate(std::move(ctor));
}
+void PointerChoreographer::populateFakeDisplayTopologyLocked(
+ const std::vector<gui::DisplayInfo>& displayInfos) {
+ if (!com::android::input::flags::connected_displays_cursor()) {
+ return;
+ }
+
+ if (displayInfos.size() == mTopology.size()) {
+ bool displaysChanged = false;
+ for (const auto& displayInfo : displayInfos) {
+ if (mTopology.find(displayInfo.displayId) == mTopology.end()) {
+ displaysChanged = true;
+ break;
+ }
+ }
+
+ if (!displaysChanged) {
+ return;
+ }
+ }
+
+ // create a fake topology assuming following order
+ // default-display (top-edge) -> next-display (right-edge) -> next-display (right-edge) ...
+ // ┌─────────┬─────────┐
+ // │ next │ next 2 │ ...
+ // ├─────────┼─────────┘
+ // │ default │
+ // └─────────┘
+ mTopology.clear();
+
+ // treat default display as base, in real topology it should be the primary-display
+ ui::LogicalDisplayId previousDisplay = ui::LogicalDisplayId::DEFAULT;
+ for (const auto& displayInfo : displayInfos) {
+ if (displayInfo.displayId == ui::LogicalDisplayId::DEFAULT) {
+ continue;
+ }
+ if (previousDisplay == ui::LogicalDisplayId::DEFAULT) {
+ mTopology[previousDisplay].push_back({displayInfo.displayId, DisplayPosition::TOP, 0});
+ mTopology[displayInfo.displayId].push_back(
+ {previousDisplay, DisplayPosition::BOTTOM, 0});
+ } else {
+ mTopology[previousDisplay].push_back(
+ {displayInfo.displayId, DisplayPosition::RIGHT, 0});
+ mTopology[displayInfo.displayId].push_back({previousDisplay, DisplayPosition::LEFT, 0});
+ }
+ previousDisplay = displayInfo.displayId;
+ }
+
+ // update default pointer display. In real topology it should be the primary-display
+ if (mTopology.find(mDefaultMouseDisplayId) == mTopology.end()) {
+ mDefaultMouseDisplayId = ui::LogicalDisplayId::DEFAULT;
+ }
+}
+
+std::optional<const DisplayViewport*> PointerChoreographer::findDestinationDisplayLocked(
+ const DisplayViewport& sourceViewport, const FloatPoint& unconsumedDelta) const {
+ DisplayPosition sourceBoundary;
+ if (unconsumedDelta.x > 0) {
+ sourceBoundary = DisplayPosition::RIGHT;
+ } else if (unconsumedDelta.x < 0) {
+ sourceBoundary = DisplayPosition::LEFT;
+ } else if (unconsumedDelta.y > 0) {
+ sourceBoundary = DisplayPosition::BOTTOM;
+ } else {
+ sourceBoundary = DisplayPosition::TOP;
+ }
+
+ // Choreographer works in un-rotate coordinate space so we need to rotate boundary by viewport
+ // orientation to find the rotated boundary
+ constexpr int MOD = ftl::to_underlying(ui::Rotation::ftl_last) + 1;
+ sourceBoundary = static_cast<DisplayPosition>(
+ (ftl::to_underlying(sourceBoundary) + ftl::to_underlying(sourceViewport.orientation)) %
+ MOD);
+
+ const auto& destination = mTopology.find(sourceViewport.displayId);
+ if (destination == mTopology.end()) {
+ // Topology is likely out of sync with viewport info, wait for it to be updated
+ LOG(WARNING) << "Source display missing from topology " << sourceViewport.displayId;
+ return std::nullopt;
+ }
+
+ for (const auto& adjacentDisplay : destination->second) {
+ if (adjacentDisplay.position != sourceBoundary) {
+ continue;
+ }
+ const DisplayViewport* destinationViewport =
+ findViewportByIdLocked(adjacentDisplay.displayId);
+ if (destinationViewport == nullptr) {
+ // Topology is likely out of sync with viewport info, wait for them to be updated
+ LOG(WARNING) << "Cannot find viewport for adjacent display "
+ << adjacentDisplay.displayId << "of source display "
+ << sourceViewport.displayId;
+ break;
+ }
+ return destinationViewport;
+ }
+ return std::nullopt;
+}
+
// --- PointerChoreographer::PointerChoreographerDisplayInfoListener ---
void PointerChoreographer::PointerChoreographerDisplayInfoListener::onWindowInfosChanged(
@@ -883,12 +1037,14 @@
}
auto newPrivacySensitiveDisplays =
getPrivacySensitiveDisplaysFromWindowInfos(windowInfosUpdate.windowInfos);
+
+ // PointerChoreographer uses Listener's lock.
+ base::ScopedLockAssertion assumeLocked(mPointerChoreographer->getLock());
if (newPrivacySensitiveDisplays != mPrivacySensitiveDisplays) {
mPrivacySensitiveDisplays = std::move(newPrivacySensitiveDisplays);
- // PointerChoreographer uses Listener's lock.
- base::ScopedLockAssertion assumeLocked(mPointerChoreographer->getLock());
mPointerChoreographer->onPrivacySensitiveDisplaysChangedLocked(mPrivacySensitiveDisplays);
}
+ mPointerChoreographer->populateFakeDisplayTopologyLocked(windowInfosUpdate.displayInfos);
}
void PointerChoreographer::PointerChoreographerDisplayInfoListener::setInitialDisplayInfosLocked(