Ensure canceled touch stream uses previous display id

We are observing inconsistent event streams being sent out of
TouchInputMapper. Whenever display id associated with a specific device
changes while touch is in progress, the generated ACTION_CANCEL event
has the new display id instead of the original one.

This causes issues later in the pipeline. In the case when a11y is ON,
the events are sent to a11y and get later re-injected into
InputDispatcher. The InputDispatcher will not resolve the correct
verifier, and as a result will incorrectly reject the event.

In this CL, we are storing the previous display id before
reconfiguration occurs, and then using this stored display id to abort
touches just before reset() is called.

In reset(), we are also canceling touches. That behaviour has to be
maintained, since reset() is a public API that can be called from other
places.

Bug: 378308551
Test: TEST=inputflinger_tests; m $TEST && $ANDROID_HOST_OUT/nativetest64/$TEST/$TEST --gtest_filter="*ChangeAssociatedDisplayIdWhenTouchIsActive*"
Flag: EXEMPT bugfix
Change-Id: I275bdd03929be6dd024c250c1276c05622a0e2ce
diff --git a/services/inputflinger/reader/mapper/TouchInputMapper.cpp b/services/inputflinger/reader/mapper/TouchInputMapper.cpp
index 5cfda03..8deff6b 100644
--- a/services/inputflinger/reader/mapper/TouchInputMapper.cpp
+++ b/services/inputflinger/reader/mapper/TouchInputMapper.cpp
@@ -302,6 +302,8 @@
                                                     ConfigurationChanges changes) {
     std::list<NotifyArgs> out = InputMapper::reconfigure(when, config, changes);
 
+    std::optional<ui::LogicalDisplayId> previousDisplayId = getAssociatedDisplayId();
+
     mConfig = config;
 
     // Full configuration should happen the first time configure is called and
@@ -350,6 +352,8 @@
     }
 
     if (changes.any() && resetNeeded) {
+        // Touches should be aborted using the previous display id, so that the stream is consistent
+        out += abortTouches(when, when, /*policyFlags=*/0, previousDisplayId);
         out += reset(when);
 
         // Send reset, unless this is the first time the device has been configured,
@@ -1657,6 +1661,10 @@
             mParameters.hasAssociatedDisplay;
 }
 
+ui::LogicalDisplayId TouchInputMapper::resolveDisplayId() const {
+    return getAssociatedDisplayId().value_or(ui::LogicalDisplayId::INVALID);
+};
+
 void TouchInputMapper::applyExternalStylusButtonState(nsecs_t when) {
     if (mDeviceMode == DeviceMode::DIRECT && hasExternalStylus()) {
         // If any of the external buttons are already pressed by the touch device, ignore them.
@@ -1928,8 +1936,9 @@
                          keyEventFlags, keyCode, scanCode, metaState, downTime);
 }
 
-std::list<NotifyArgs> TouchInputMapper::abortTouches(nsecs_t when, nsecs_t readTime,
-                                                     uint32_t policyFlags) {
+std::list<NotifyArgs> TouchInputMapper::abortTouches(
+        nsecs_t when, nsecs_t readTime, uint32_t policyFlags,
+        std::optional<ui::LogicalDisplayId> currentGestureDisplayId) {
     std::list<NotifyArgs> out;
     if (mCurrentMotionAborted) {
         // Current motion event was already aborted.
@@ -1940,6 +1949,7 @@
         int32_t metaState = getContext()->getGlobalMetaState();
         int32_t buttonState = mCurrentCookedState.buttonState;
         out.push_back(dispatchMotion(when, readTime, policyFlags, mSource,
+                                     currentGestureDisplayId.value_or(resolveDisplayId()),
                                      AMOTION_EVENT_ACTION_CANCEL, 0, AMOTION_EVENT_FLAG_CANCELED,
                                      metaState, buttonState, AMOTION_EVENT_EDGE_FLAG_NONE,
                                      mCurrentCookedState.cookedPointerData.pointerProperties,
@@ -1994,14 +2004,15 @@
         if (!currentIdBits.isEmpty()) {
             // No pointer id changes so this is a move event.
             // The listener takes care of batching moves so we don't have to deal with that here.
-            out.push_back(
-                    dispatchMotion(when, readTime, policyFlags, mSource, AMOTION_EVENT_ACTION_MOVE,
-                                   0, 0, metaState, buttonState, AMOTION_EVENT_EDGE_FLAG_NONE,
-                                   mCurrentCookedState.cookedPointerData.pointerProperties,
-                                   mCurrentCookedState.cookedPointerData.pointerCoords,
-                                   mCurrentCookedState.cookedPointerData.idToIndex, currentIdBits,
-                                   -1, mOrientedXPrecision, mOrientedYPrecision, mDownTime,
-                                   MotionClassification::NONE));
+            out.push_back(dispatchMotion(when, readTime, policyFlags, mSource, resolveDisplayId(),
+                                         AMOTION_EVENT_ACTION_MOVE, 0, 0, metaState, buttonState,
+                                         AMOTION_EVENT_EDGE_FLAG_NONE,
+                                         mCurrentCookedState.cookedPointerData.pointerProperties,
+                                         mCurrentCookedState.cookedPointerData.pointerCoords,
+                                         mCurrentCookedState.cookedPointerData.idToIndex,
+                                         currentIdBits, -1, mOrientedXPrecision,
+                                         mOrientedYPrecision, mDownTime,
+                                         MotionClassification::NONE));
         }
     } else {
         // There may be pointers going up and pointers going down and pointers moving
@@ -2031,7 +2042,7 @@
             if (isCanceled) {
                 ALOGI("Canceling pointer %d for the palm event was detected.", upId);
             }
-            out.push_back(dispatchMotion(when, readTime, policyFlags, mSource,
+            out.push_back(dispatchMotion(when, readTime, policyFlags, mSource, resolveDisplayId(),
                                          AMOTION_EVENT_ACTION_POINTER_UP, 0,
                                          isCanceled ? AMOTION_EVENT_FLAG_CANCELED : 0, metaState,
                                          buttonState, 0,
@@ -2050,7 +2061,7 @@
         // events, they do not generally handle them except when presented in a move event.
         if (moveNeeded && !moveIdBits.isEmpty()) {
             ALOG_ASSERT(moveIdBits.value == dispatchedIdBits.value);
-            out.push_back(dispatchMotion(when, readTime, policyFlags, mSource,
+            out.push_back(dispatchMotion(when, readTime, policyFlags, mSource, resolveDisplayId(),
                                          AMOTION_EVENT_ACTION_MOVE, 0, 0, metaState, buttonState, 0,
                                          mCurrentCookedState.cookedPointerData.pointerProperties,
                                          mCurrentCookedState.cookedPointerData.pointerCoords,
@@ -2071,7 +2082,7 @@
             }
 
             out.push_back(
-                    dispatchMotion(when, readTime, policyFlags, mSource,
+                    dispatchMotion(when, readTime, policyFlags, mSource, resolveDisplayId(),
                                    AMOTION_EVENT_ACTION_POINTER_DOWN, 0, 0, metaState, buttonState,
                                    0, mCurrentCookedState.cookedPointerData.pointerProperties,
                                    mCurrentCookedState.cookedPointerData.pointerCoords,
@@ -2090,7 +2101,7 @@
         (mCurrentCookedState.cookedPointerData.hoveringIdBits.isEmpty() ||
          !mCurrentCookedState.cookedPointerData.touchingIdBits.isEmpty())) {
         int32_t metaState = getContext()->getGlobalMetaState();
-        out.push_back(dispatchMotion(when, readTime, policyFlags, mSource,
+        out.push_back(dispatchMotion(when, readTime, policyFlags, mSource, resolveDisplayId(),
                                      AMOTION_EVENT_ACTION_HOVER_EXIT, 0, 0, metaState,
                                      mLastCookedState.buttonState, 0,
                                      mLastCookedState.cookedPointerData.pointerProperties,
@@ -2111,7 +2122,7 @@
         !mCurrentCookedState.cookedPointerData.hoveringIdBits.isEmpty()) {
         int32_t metaState = getContext()->getGlobalMetaState();
         if (!mSentHoverEnter) {
-            out.push_back(dispatchMotion(when, readTime, policyFlags, mSource,
+            out.push_back(dispatchMotion(when, readTime, policyFlags, mSource, resolveDisplayId(),
                                          AMOTION_EVENT_ACTION_HOVER_ENTER, 0, 0, metaState,
                                          mCurrentRawState.buttonState, 0,
                                          mCurrentCookedState.cookedPointerData.pointerProperties,
@@ -2123,7 +2134,7 @@
             mSentHoverEnter = true;
         }
 
-        out.push_back(dispatchMotion(when, readTime, policyFlags, mSource,
+        out.push_back(dispatchMotion(when, readTime, policyFlags, mSource, resolveDisplayId(),
                                      AMOTION_EVENT_ACTION_HOVER_MOVE, 0, 0, metaState,
                                      mCurrentRawState.buttonState, 0,
                                      mCurrentCookedState.cookedPointerData.pointerProperties,
@@ -2146,7 +2157,7 @@
     while (!releasedButtons.isEmpty()) {
         int32_t actionButton = BitSet32::valueForBit(releasedButtons.clearFirstMarkedBit());
         buttonState &= ~actionButton;
-        out.push_back(dispatchMotion(when, readTime, policyFlags, mSource,
+        out.push_back(dispatchMotion(when, readTime, policyFlags, mSource, resolveDisplayId(),
                                      AMOTION_EVENT_ACTION_BUTTON_RELEASE, actionButton, 0,
                                      metaState, buttonState, 0,
                                      mLastCookedState.cookedPointerData.pointerProperties,
@@ -2168,7 +2179,7 @@
     while (!pressedButtons.isEmpty()) {
         int32_t actionButton = BitSet32::valueForBit(pressedButtons.clearFirstMarkedBit());
         buttonState |= actionButton;
-        out.push_back(dispatchMotion(when, readTime, policyFlags, mSource,
+        out.push_back(dispatchMotion(when, readTime, policyFlags, mSource, resolveDisplayId(),
                                      AMOTION_EVENT_ACTION_BUTTON_PRESS, actionButton, 0, metaState,
                                      buttonState, 0,
                                      mCurrentCookedState.cookedPointerData.pointerProperties,
@@ -2192,7 +2203,7 @@
     while (!releasedButtons.isEmpty()) {
         int32_t actionButton = BitSet32::valueForBit(releasedButtons.clearFirstMarkedBit());
         buttonState &= ~actionButton;
-        out.push_back(dispatchMotion(when, readTime, policyFlags, mSource,
+        out.push_back(dispatchMotion(when, readTime, policyFlags, mSource, resolveDisplayId(),
                                      AMOTION_EVENT_ACTION_BUTTON_RELEASE, actionButton, 0,
                                      metaState, buttonState, 0,
                                      mPointerGesture.lastGestureProperties,
@@ -2216,7 +2227,7 @@
     while (!pressedButtons.isEmpty()) {
         int32_t actionButton = BitSet32::valueForBit(pressedButtons.clearFirstMarkedBit());
         buttonState |= actionButton;
-        out.push_back(dispatchMotion(when, readTime, policyFlags, mSource,
+        out.push_back(dispatchMotion(when, readTime, policyFlags, mSource, resolveDisplayId(),
                                      AMOTION_EVENT_ACTION_BUTTON_PRESS, actionButton, 0, metaState,
                                      buttonState, 0, mPointerGesture.currentGestureProperties,
                                      mPointerGesture.currentGestureCoords,
@@ -2564,7 +2575,7 @@
     if (!dispatchedGestureIdBits.isEmpty()) {
         if (cancelPreviousGesture) {
             const uint32_t cancelFlags = flags | AMOTION_EVENT_FLAG_CANCELED;
-            out.push_back(dispatchMotion(when, readTime, policyFlags, mSource,
+            out.push_back(dispatchMotion(when, readTime, policyFlags, mSource, resolveDisplayId(),
                                          AMOTION_EVENT_ACTION_CANCEL, 0, cancelFlags, metaState,
                                          buttonState, AMOTION_EVENT_EDGE_FLAG_NONE,
                                          mPointerGesture.lastGestureProperties,
@@ -2591,8 +2602,9 @@
                 }
                 const uint32_t id = upGestureIdBits.clearFirstMarkedBit();
                 out.push_back(dispatchMotion(when, readTime, policyFlags, mSource,
-                                             AMOTION_EVENT_ACTION_POINTER_UP, 0, flags, metaState,
-                                             buttonState, AMOTION_EVENT_EDGE_FLAG_NONE,
+                                             resolveDisplayId(), AMOTION_EVENT_ACTION_POINTER_UP, 0,
+                                             flags, metaState, buttonState,
+                                             AMOTION_EVENT_EDGE_FLAG_NONE,
                                              mPointerGesture.lastGestureProperties,
                                              mPointerGesture.lastGestureCoords,
                                              mPointerGesture.lastGestureIdToIndex,
@@ -2606,13 +2618,14 @@
 
     // Send motion events for all pointers that moved.
     if (moveNeeded) {
-        out.push_back(
-                dispatchMotion(when, readTime, policyFlags, mSource, AMOTION_EVENT_ACTION_MOVE, 0,
-                               flags, metaState, buttonState, AMOTION_EVENT_EDGE_FLAG_NONE,
-                               mPointerGesture.currentGestureProperties,
-                               mPointerGesture.currentGestureCoords,
-                               mPointerGesture.currentGestureIdToIndex, dispatchedGestureIdBits, -1,
-                               0, 0, mPointerGesture.downTime, classification));
+        out.push_back(dispatchMotion(when, readTime, policyFlags, mSource, resolveDisplayId(),
+                                     AMOTION_EVENT_ACTION_MOVE, 0, flags, metaState, buttonState,
+                                     AMOTION_EVENT_EDGE_FLAG_NONE,
+                                     mPointerGesture.currentGestureProperties,
+                                     mPointerGesture.currentGestureCoords,
+                                     mPointerGesture.currentGestureIdToIndex,
+                                     dispatchedGestureIdBits, -1, 0, 0, mPointerGesture.downTime,
+                                     classification));
     }
 
     // Send motion events for all pointers that went down.
@@ -2627,7 +2640,7 @@
                 mPointerGesture.downTime = when;
             }
 
-            out.push_back(dispatchMotion(when, readTime, policyFlags, mSource,
+            out.push_back(dispatchMotion(when, readTime, policyFlags, mSource, resolveDisplayId(),
                                          AMOTION_EVENT_ACTION_POINTER_DOWN, 0, flags, metaState,
                                          buttonState, 0, mPointerGesture.currentGestureProperties,
                                          mPointerGesture.currentGestureCoords,
@@ -2645,7 +2658,7 @@
 
     // Send motion events for hover.
     if (mPointerGesture.currentGestureMode == PointerGesture::Mode::HOVER) {
-        out.push_back(dispatchMotion(when, readTime, policyFlags, mSource,
+        out.push_back(dispatchMotion(when, readTime, policyFlags, mSource, resolveDisplayId(),
                                      AMOTION_EVENT_ACTION_HOVER_MOVE, 0, flags, metaState,
                                      buttonState, AMOTION_EVENT_EDGE_FLAG_NONE,
                                      mPointerGesture.currentGestureProperties,
@@ -2704,7 +2717,7 @@
     if (!mPointerGesture.lastGestureIdBits.isEmpty()) {
         int32_t metaState = getContext()->getGlobalMetaState();
         int32_t buttonState = mCurrentRawState.buttonState;
-        out.push_back(dispatchMotion(when, readTime, policyFlags, mSource,
+        out.push_back(dispatchMotion(when, readTime, policyFlags, mSource, resolveDisplayId(),
                                      AMOTION_EVENT_ACTION_CANCEL, 0, AMOTION_EVENT_FLAG_CANCELED,
                                      metaState, buttonState, AMOTION_EVENT_EDGE_FLAG_NONE,
                                      mPointerGesture.lastGestureProperties,
@@ -3498,8 +3511,7 @@
         hovering = false;
     }
 
-    return dispatchPointerSimple(when, readTime, policyFlags, down, hovering,
-                                 getAssociatedDisplayId().value_or(ui::LogicalDisplayId::INVALID));
+    return dispatchPointerSimple(when, readTime, policyFlags, down, hovering, resolveDisplayId());
 }
 
 std::list<NotifyArgs> TouchInputMapper::abortPointerMouse(nsecs_t when, nsecs_t readTime,
@@ -3662,9 +3674,10 @@
 }
 
 NotifyMotionArgs TouchInputMapper::dispatchMotion(
-        nsecs_t when, nsecs_t readTime, uint32_t policyFlags, uint32_t source, int32_t action,
-        int32_t actionButton, int32_t flags, int32_t metaState, int32_t buttonState,
-        int32_t edgeFlags, const PropertiesArray& properties, const CoordsArray& coords,
+        nsecs_t when, nsecs_t readTime, uint32_t policyFlags, uint32_t source,
+        ui::LogicalDisplayId displayId, int32_t action, int32_t actionButton, int32_t flags,
+        int32_t metaState, int32_t buttonState, int32_t edgeFlags,
+        const PropertiesArray& properties, const CoordsArray& coords,
         const IdToIndexArray& idToIndex, BitSet32 idBits, int32_t changedId, float xPrecision,
         float yPrecision, nsecs_t downTime, MotionClassification classification) const {
     std::vector<PointerCoords> pointerCoords;
@@ -3714,9 +3727,6 @@
         }
     }
 
-    const ui::LogicalDisplayId displayId =
-            getAssociatedDisplayId().value_or(ui::LogicalDisplayId::INVALID);
-
     float xCursorPosition = AMOTION_EVENT_INVALID_CURSOR_POSITION;
     float yCursorPosition = AMOTION_EVENT_INVALID_CURSOR_POSITION;
     if (mDeviceMode == DeviceMode::POINTER) {
@@ -3739,7 +3749,7 @@
 std::list<NotifyArgs> TouchInputMapper::cancelTouch(nsecs_t when, nsecs_t readTime) {
     std::list<NotifyArgs> out;
     out += abortPointerUsage(when, readTime, /*policyFlags=*/0);
-    out += abortTouches(when, readTime, /* policyFlags=*/0);
+    out += abortTouches(when, readTime, /* policyFlags=*/0, std::nullopt);
     return out;
 }
 
diff --git a/services/inputflinger/reader/mapper/TouchInputMapper.h b/services/inputflinger/reader/mapper/TouchInputMapper.h
index 96fc61b..4ef0be8 100644
--- a/services/inputflinger/reader/mapper/TouchInputMapper.h
+++ b/services/inputflinger/reader/mapper/TouchInputMapper.h
@@ -238,7 +238,7 @@
         // DeviceType::TOUCH_SCREEN, and will otherwise use DeviceType::POINTER by default.
         // This can be overridden by IDC files, using the `touch.deviceType` config.
         DeviceType deviceType;
-        bool hasAssociatedDisplay;
+        bool hasAssociatedDisplay = false;
         bool associatedDisplayIsExternal;
         bool orientationAware;
 
@@ -779,8 +779,9 @@
                                                                      nsecs_t readTime);
     const BitSet32& findActiveIdBits(const CookedPointerData& cookedPointerData);
     void cookPointerData();
-    [[nodiscard]] std::list<NotifyArgs> abortTouches(nsecs_t when, nsecs_t readTime,
-                                                     uint32_t policyFlags);
+    [[nodiscard]] std::list<NotifyArgs> abortTouches(
+            nsecs_t when, nsecs_t readTime, uint32_t policyFlags,
+            std::optional<ui::LogicalDisplayId> gestureDisplayId);
 
     [[nodiscard]] std::list<NotifyArgs> dispatchPointerUsage(nsecs_t when, nsecs_t readTime,
                                                              uint32_t policyFlags,
@@ -836,15 +837,18 @@
     // method will take care of setting the index and transmuting the action to DOWN or UP
     // it is the first / last pointer to go down / up.
     [[nodiscard]] NotifyMotionArgs dispatchMotion(
-            nsecs_t when, nsecs_t readTime, uint32_t policyFlags, uint32_t source, int32_t action,
-            int32_t actionButton, int32_t flags, int32_t metaState, int32_t buttonState,
-            int32_t edgeFlags, const PropertiesArray& properties, const CoordsArray& coords,
+            nsecs_t when, nsecs_t readTime, uint32_t policyFlags, uint32_t source,
+            ui::LogicalDisplayId displayId, int32_t action, int32_t actionButton, int32_t flags,
+            int32_t metaState, int32_t buttonState, int32_t edgeFlags,
+            const PropertiesArray& properties, const CoordsArray& coords,
             const IdToIndexArray& idToIndex, BitSet32 idBits, int32_t changedId, float xPrecision,
             float yPrecision, nsecs_t downTime, MotionClassification classification) const;
 
     // Returns if this touch device is a touch screen with an associated display.
     bool isTouchScreen();
 
+    ui::LogicalDisplayId resolveDisplayId() const;
+
     bool isPointInsidePhysicalFrame(int32_t x, int32_t y) const;
     const VirtualKey* findVirtualKeyHit(int32_t x, int32_t y);
 
diff --git a/services/inputflinger/tests/MultiTouchInputMapper_test.cpp b/services/inputflinger/tests/MultiTouchInputMapper_test.cpp
index e8cca5f..cc3d123 100644
--- a/services/inputflinger/tests/MultiTouchInputMapper_test.cpp
+++ b/services/inputflinger/tests/MultiTouchInputMapper_test.cpp
@@ -97,6 +97,17 @@
         EXPECT_CALL(mMockEventHub, hasInputProperty(EVENTHUB_ID, _)).WillRepeatedly(Return(false));
         EXPECT_CALL(mMockEventHub, hasInputProperty(EVENTHUB_ID, INPUT_PROP_DIRECT))
                 .WillRepeatedly(Return(true));
+        // The following EXPECT_CALL lines are not load-bearing, but without them gtest prints
+        // warnings about "uninteresting mocked call", which are distracting when developing the
+        // tests because this text is interleaved with logs of interest.
+        EXPECT_CALL(mMockEventHub, getVirtualKeyDefinitions(EVENTHUB_ID, _))
+                .WillRepeatedly(Return());
+        EXPECT_CALL(mMockEventHub, hasRelativeAxis(EVENTHUB_ID, _))
+                .WillRepeatedly(testing::Return(false));
+        EXPECT_CALL(mMockEventHub, getVideoFrames(EVENTHUB_ID))
+                .WillRepeatedly(testing::Return(std::vector<TouchVideoFrame>{}));
+        EXPECT_CALL(mMockInputReaderContext, getExternalStylusDevices(_)).WillRepeatedly(Return());
+        EXPECT_CALL(mMockInputReaderContext, getGlobalMetaState()).WillRepeatedly(Return(0));
 
         // Axes that the device has
         setupAxis(ABS_MT_SLOT, /*valid=*/true, /*min=*/0, /*max=*/SLOT_COUNT - 1, /*resolution=*/0);
@@ -164,24 +175,97 @@
                 });
     }
 
-    std::list<NotifyArgs> processPosition(int32_t x, int32_t y) {
+    [[nodiscard]] std::list<NotifyArgs> processPosition(int32_t x, int32_t y) {
         std::list<NotifyArgs> args;
         args += process(EV_ABS, ABS_MT_POSITION_X, x);
         args += process(EV_ABS, ABS_MT_POSITION_Y, y);
         return args;
     }
 
-    std::list<NotifyArgs> processId(int32_t id) { return process(EV_ABS, ABS_MT_TRACKING_ID, id); }
+    [[nodiscard]] std::list<NotifyArgs> processId(int32_t id) {
+        return process(EV_ABS, ABS_MT_TRACKING_ID, id);
+    }
 
-    std::list<NotifyArgs> processKey(int32_t code, int32_t value) {
+    [[nodiscard]] std::list<NotifyArgs> processKey(int32_t code, int32_t value) {
         return process(EV_KEY, code, value);
     }
 
-    std::list<NotifyArgs> processSlot(int32_t slot) { return process(EV_ABS, ABS_MT_SLOT, slot); }
+    [[nodiscard]] std::list<NotifyArgs> processSlot(int32_t slot) {
+        return process(EV_ABS, ABS_MT_SLOT, slot);
+    }
 
-    std::list<NotifyArgs> processSync() { return process(EV_SYN, SYN_REPORT, 0); }
+    [[nodiscard]] std::list<NotifyArgs> processSync() { return process(EV_SYN, SYN_REPORT, 0); }
 };
 
+/**
+ * While a gesture is active, change the display that the device is associated with. Make sure that
+ * the CANCEL event that's generated has the display id of the original DOWN event, rather than the
+ * new display id.
+ */
+TEST_F(MultiTouchInputMapperUnitTest, ChangeAssociatedDisplayIdWhenTouchIsActive) {
+    std::list<NotifyArgs> args;
+
+    // Add a second viewport that later will be associated with our device.
+    DisplayViewport secondViewport =
+            createViewport(SECOND_DISPLAY_ID, DISPLAY_WIDTH, DISPLAY_HEIGHT, ui::ROTATION_0,
+                           /*isActive=*/true, "local:1", NO_PORT, ViewportType::EXTERNAL);
+    mFakePolicy->addDisplayViewport(secondViewport);
+    std::optional<DisplayViewport> firstViewport =
+            mFakePolicy->getDisplayViewportByUniqueId("local:0");
+
+    // InputReaderConfiguration contains information about how devices are associated with displays.
+    // The mapper receives this information. However, it doesn't actually parse it - that's done by
+    // InputDevice. The mapper asks InputDevice about the associated viewport, so that's what we
+    // need to mock here to simulate association. This abstraction is confusing and should be
+    // refactored.
+
+    // Start with the first viewport
+    ON_CALL((*mDevice), getAssociatedViewport).WillByDefault(Return(firstViewport));
+    args += mMapper->reconfigure(systemTime(SYSTEM_TIME_MONOTONIC), mReaderConfiguration,
+                                 InputReaderConfiguration::Change::DISPLAY_INFO);
+
+    int32_t x1 = 100, y1 = 125;
+    args += processKey(BTN_TOUCH, 1);
+    args += processPosition(x1, y1);
+    args += processId(1);
+    args += processSync();
+    ASSERT_THAT(args,
+                ElementsAre(VariantWith<NotifyMotionArgs>(
+                        AllOf(WithMotionAction(ACTION_DOWN), WithDisplayId(DISPLAY_ID)))));
+    args.clear();
+
+    // Now associate with the second viewport, and reconfigure.
+    ON_CALL((*mDevice), getAssociatedViewport).WillByDefault(Return(secondViewport));
+    args += mMapper->reconfigure(systemTime(SYSTEM_TIME_MONOTONIC), mReaderConfiguration,
+                                 InputReaderConfiguration::Change::DISPLAY_INFO);
+    assertNotifyArgs(args,
+                     VariantWith<NotifyMotionArgs>(
+                             AllOf(WithMotionAction(ACTION_CANCEL), WithDisplayId(DISPLAY_ID))),
+                     VariantWith<NotifyDeviceResetArgs>(WithDeviceId(DEVICE_ID)));
+
+    // The remainder of the gesture is ignored
+    // Move.
+    x1 += 10;
+    y1 += 15;
+    args = processPosition(x1, y1);
+    args += processSync();
+    // Up
+    args += processKey(BTN_TOUCH, 0);
+    args += processId(-1);
+    args += processSync();
+
+    ASSERT_THAT(args, IsEmpty());
+
+    // New touch is delivered with the new display id.
+    args += processId(2);
+    args += processKey(BTN_TOUCH, 1);
+    args += processPosition(x1 + 20, y1 + 40);
+    args += processSync();
+    assertNotifyArgs(args,
+                     VariantWith<NotifyMotionArgs>(AllOf(WithMotionAction(ACTION_DOWN),
+                                                         WithDisplayId(SECOND_DISPLAY_ID))));
+}
+
 // This test simulates a multi-finger gesture with unexpected reset in between. This might happen
 // due to buffer overflow and device with report a SYN_DROPPED. In this case we expect mapper to be
 // reset, MT slot state to be re-populated and the gesture should be cancelled and restarted.
@@ -191,7 +275,7 @@
     // Two fingers down at once.
     constexpr int32_t FIRST_TRACKING_ID = 1, SECOND_TRACKING_ID = 2;
     int32_t x1 = 100, y1 = 125, x2 = 200, y2 = 225;
-    processKey(BTN_TOUCH, 1);
+    args += processKey(BTN_TOUCH, 1);
     args += processPosition(x1, y1);
     args += processId(FIRST_TRACKING_ID);
     args += processSlot(1);
@@ -199,7 +283,7 @@
     args += processId(SECOND_TRACKING_ID);
     ASSERT_THAT(args, IsEmpty());
 
-    args = processSync();
+    args += processSync();
     ASSERT_THAT(args,
                 ElementsAre(VariantWith<NotifyMotionArgs>(
                                     WithMotionAction(AMOTION_EVENT_ACTION_DOWN)),
@@ -272,8 +356,8 @@
                 ElementsAre(VariantWith<NotifyMotionArgs>(WithMotionAction(ACTION_POINTER_0_UP))));
 
     // Second finger up.
-    processKey(BTN_TOUCH, 0);
-    args = processSlot(1);
+    args = processKey(BTN_TOUCH, 0);
+    args += processSlot(1);
     args += processId(-1);
     ASSERT_THAT(args, IsEmpty());
 
@@ -317,12 +401,12 @@
                                 InputReaderConfiguration::Change::DISPLAY_INFO);
 
     assertNotifyArgs(args,
-                     VariantWith<NotifyMotionArgs>(AllOf(WithMotionAction(ACTION_CANCEL),
-                                                         WithDisplayId(SECOND_DISPLAY_ID))),
+                     VariantWith<NotifyMotionArgs>(
+                             AllOf(WithMotionAction(ACTION_CANCEL), WithDisplayId(DISPLAY_ID))),
                      VariantWith<NotifyDeviceResetArgs>(WithDeviceId(DEVICE_ID)));
     // Lift up the old pointer.
-    processKey(BTN_TOUCH, 0);
-    args = processId(-1);
+    args = processKey(BTN_TOUCH, 0);
+    args += processId(-1);
     args += processSync();
 
     // Send new pointer