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(