Merge "Generate down events when transferring touch focus"
diff --git a/services/inputflinger/dispatcher/InputDispatcher.cpp b/services/inputflinger/dispatcher/InputDispatcher.cpp
index b2b5145..f2b95e7 100644
--- a/services/inputflinger/dispatcher/InputDispatcher.cpp
+++ b/services/inputflinger/dispatcher/InputDispatcher.cpp
@@ -2686,6 +2686,19 @@
           connection->getInputChannelName().c_str(), cancelationEvents.size(), options.reason,
           options.mode);
 #endif
+
+    InputTarget target;
+    sp<InputWindowHandle> windowHandle =
+            getWindowHandleLocked(connection->inputChannel->getConnectionToken());
+    if (windowHandle != nullptr) {
+        const InputWindowInfo* windowInfo = windowHandle->getInfo();
+        target.setDefaultPointerInfo(-windowInfo->frameLeft, -windowInfo->frameTop,
+                                     windowInfo->windowXScale, windowInfo->windowYScale);
+        target.globalScaleFactor = windowInfo->globalScaleFactor;
+    }
+    target.inputChannel = connection->inputChannel;
+    target.flags = InputTarget::FLAG_DISPATCH_AS_IS;
+
     for (size_t i = 0; i < cancelationEvents.size(); i++) {
         EventEntry* cancelationEventEntry = cancelationEvents[i];
         switch (cancelationEventEntry->type) {
@@ -2711,18 +2724,6 @@
             }
         }
 
-        InputTarget target;
-        sp<InputWindowHandle> windowHandle =
-                getWindowHandleLocked(connection->inputChannel->getConnectionToken());
-        if (windowHandle != nullptr) {
-            const InputWindowInfo* windowInfo = windowHandle->getInfo();
-            target.setDefaultPointerInfo(-windowInfo->frameLeft, -windowInfo->frameTop,
-                                         windowInfo->windowXScale, windowInfo->windowYScale);
-            target.globalScaleFactor = windowInfo->globalScaleFactor;
-        }
-        target.inputChannel = connection->inputChannel;
-        target.flags = InputTarget::FLAG_DISPATCH_AS_IS;
-
         enqueueDispatchEntryLocked(connection, cancelationEventEntry, // increments ref
                                    target, InputTarget::FLAG_DISPATCH_AS_IS);
 
@@ -2732,6 +2733,65 @@
     startDispatchCycleLocked(currentTime, connection);
 }
 
+void InputDispatcher::synthesizePointerDownEventsForConnectionLocked(
+        const sp<Connection>& connection) {
+    if (connection->status == Connection::STATUS_BROKEN) {
+        return;
+    }
+
+    nsecs_t currentTime = now();
+
+    std::vector<EventEntry*> downEvents =
+            connection->inputState.synthesizePointerDownEvents(currentTime);
+
+    if (downEvents.empty()) {
+        return;
+    }
+
+#if DEBUG_OUTBOUND_EVENT_DETAILS
+        ALOGD("channel '%s' ~ Synthesized %zu down events to ensure consistent event stream.",
+              connection->getInputChannelName().c_str(), downEvents.size());
+#endif
+
+    InputTarget target;
+    sp<InputWindowHandle> windowHandle =
+            getWindowHandleLocked(connection->inputChannel->getConnectionToken());
+    if (windowHandle != nullptr) {
+        const InputWindowInfo* windowInfo = windowHandle->getInfo();
+        target.setDefaultPointerInfo(-windowInfo->frameLeft, -windowInfo->frameTop,
+                                     windowInfo->windowXScale, windowInfo->windowYScale);
+        target.globalScaleFactor = windowInfo->globalScaleFactor;
+    }
+    target.inputChannel = connection->inputChannel;
+    target.flags = InputTarget::FLAG_DISPATCH_AS_IS;
+
+    for (EventEntry* downEventEntry : downEvents) {
+        switch (downEventEntry->type) {
+            case EventEntry::Type::MOTION: {
+                logOutboundMotionDetails("down - ",
+                        static_cast<const MotionEntry&>(*downEventEntry));
+                break;
+            }
+
+            case EventEntry::Type::KEY:
+            case EventEntry::Type::FOCUS:
+            case EventEntry::Type::CONFIGURATION_CHANGED:
+            case EventEntry::Type::DEVICE_RESET: {
+                LOG_ALWAYS_FATAL("%s event should not be found inside Connections's queue",
+                                     EventEntry::typeToString(downEventEntry->type));
+                break;
+            }
+        }
+
+        enqueueDispatchEntryLocked(connection, downEventEntry, // increments ref
+                                   target, InputTarget::FLAG_DISPATCH_AS_IS);
+
+        downEventEntry->release();
+    }
+
+    startDispatchCycleLocked(currentTime, connection);
+}
+
 MotionEntry* InputDispatcher::splitMotionEvent(const MotionEntry& originalMotionEntry,
                                                BitSet32 pointerIds) {
     ALOG_ASSERT(pointerIds.value != 0);
@@ -3770,11 +3830,12 @@
         sp<Connection> fromConnection = getConnectionLocked(fromToken);
         sp<Connection> toConnection = getConnectionLocked(toToken);
         if (fromConnection != nullptr && toConnection != nullptr) {
-            fromConnection->inputState.copyPointerStateTo(toConnection->inputState);
+            fromConnection->inputState.mergePointerStateTo(toConnection->inputState);
             CancelationOptions
                     options(CancelationOptions::CANCEL_POINTER_EVENTS,
                             "transferring touch focus from this window to another window");
             synthesizeCancelationEventsForConnectionLocked(fromConnection, options);
+            synthesizePointerDownEventsForConnectionLocked(toConnection);
         }
 
         if (DEBUG_FOCUS) {
diff --git a/services/inputflinger/dispatcher/InputDispatcher.h b/services/inputflinger/dispatcher/InputDispatcher.h
index 93de18d..d2aea80 100644
--- a/services/inputflinger/dispatcher/InputDispatcher.h
+++ b/services/inputflinger/dispatcher/InputDispatcher.h
@@ -417,6 +417,9 @@
                                                         const CancelationOptions& options)
             REQUIRES(mLock);
 
+    void synthesizePointerDownEventsForConnectionLocked(const sp<Connection>& connection)
+            REQUIRES(mLock);
+
     // Splitting motion events across windows.
     MotionEntry* splitMotionEvent(const MotionEntry& originalMotionEntry, BitSet32 pointerIds);
 
diff --git a/services/inputflinger/dispatcher/InputState.cpp b/services/inputflinger/dispatcher/InputState.cpp
index c43e304..053598a 100644
--- a/services/inputflinger/dispatcher/InputState.cpp
+++ b/services/inputflinger/dispatcher/InputState.cpp
@@ -145,10 +145,13 @@
                 // Joysticks and trackballs can send MOVE events without corresponding DOWN or UP.
                 return true;
             }
+
             if (index >= 0) {
                 MotionMemento& memento = mMotionMementos[index];
-                memento.setPointers(entry);
-                return true;
+                if (memento.firstNewPointerIdx < 0) {
+                    memento.setPointers(entry);
+                    return true;
+                }
             }
 #if DEBUG_OUTBOUND_EVENT_DETAILS
             ALOGD("Dropping inconsistent motion pointer up/down or move event: "
@@ -249,6 +252,17 @@
     }
 }
 
+void InputState::MotionMemento::mergePointerStateTo(MotionMemento& other) const {
+    for (uint32_t i = 0; i < pointerCount; i++) {
+        if (other.firstNewPointerIdx < 0) {
+            other.firstNewPointerIdx = other.pointerCount;
+        }
+        other.pointerProperties[other.pointerCount].copyFrom(pointerProperties[i]);
+        other.pointerCoords[other.pointerCount].copyFrom(pointerCoords[i]);
+        other.pointerCount++;
+    }
+}
+
 std::vector<EventEntry*> InputState::synthesizeCancelationEvents(
         nsecs_t currentTime, const CancelationOptions& options) {
     std::vector<EventEntry*> events;
@@ -282,27 +296,87 @@
     return events;
 }
 
+std::vector<EventEntry*> InputState::synthesizePointerDownEvents(nsecs_t currentTime) {
+    std::vector<EventEntry*> events;
+    for (MotionMemento& memento : mMotionMementos) {
+        if (!(memento.source & AINPUT_SOURCE_CLASS_POINTER)) {
+            continue;
+        }
+
+        if (memento.firstNewPointerIdx < 0) {
+            continue;
+        }
+
+        uint32_t pointerCount = 0;
+        PointerProperties pointerProperties[MAX_POINTERS];
+        PointerCoords pointerCoords[MAX_POINTERS];
+
+        // We will deliver all pointers the target already knows about
+        for (uint32_t i = 0; i < static_cast<uint32_t>(memento.firstNewPointerIdx); i++) {
+            pointerProperties[i].copyFrom(memento.pointerProperties[i]);
+            pointerCoords[i].copyFrom(memento.pointerCoords[i]);
+            pointerCount++;
+        }
+
+        // We will send explicit events for all pointers the target doesn't know about
+        for (uint32_t i = static_cast<uint32_t>(memento.firstNewPointerIdx);
+                i < memento.pointerCount; i++) {
+
+            pointerProperties[i].copyFrom(memento.pointerProperties[i]);
+            pointerCoords[i].copyFrom(memento.pointerCoords[i]);
+            pointerCount++;
+
+            // Down only if the first pointer, pointer down otherwise
+            const int32_t action = (pointerCount <= 1)
+                    ? AMOTION_EVENT_ACTION_DOWN
+                    : AMOTION_EVENT_ACTION_POINTER_DOWN
+                            | (i << AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT);
+
+            events.push_back(new MotionEntry(SYNTHESIZED_EVENT_SEQUENCE_NUM, currentTime,
+                                             memento.deviceId, memento.source, memento.displayId,
+                                             memento.policyFlags, action, 0 /*actionButton*/,
+                                             memento.flags, AMETA_NONE, 0 /*buttonState*/,
+                                             MotionClassification::NONE,
+                                             AMOTION_EVENT_EDGE_FLAG_NONE, memento.xPrecision,
+                                             memento.yPrecision, memento.xCursorPosition,
+                                             memento.yCursorPosition, memento.downTime,
+                                             pointerCount, pointerProperties,
+                                             pointerCoords, 0 /*xOffset*/, 0 /*yOffset*/));
+        }
+
+        memento.firstNewPointerIdx = INVALID_POINTER_INDEX;
+    }
+
+    return events;
+}
+
 void InputState::clear() {
     mKeyMementos.clear();
     mMotionMementos.clear();
     mFallbackKeys.clear();
 }
 
-void InputState::copyPointerStateTo(InputState& other) const {
+void InputState::mergePointerStateTo(InputState& other) {
     for (size_t i = 0; i < mMotionMementos.size(); i++) {
-        const MotionMemento& memento = mMotionMementos[i];
+        MotionMemento& memento = mMotionMementos[i];
+        // Since we support split pointers we need to merge touch events
+        // from the same source + device + screen.
         if (memento.source & AINPUT_SOURCE_CLASS_POINTER) {
-            for (size_t j = 0; j < other.mMotionMementos.size();) {
-                const MotionMemento& otherMemento = other.mMotionMementos[j];
+            bool merged = false;
+            for (size_t j = 0; j < other.mMotionMementos.size(); j++) {
+                MotionMemento& otherMemento = other.mMotionMementos[j];
                 if (memento.deviceId == otherMemento.deviceId &&
                     memento.source == otherMemento.source &&
                     memento.displayId == otherMemento.displayId) {
-                    other.mMotionMementos.erase(other.mMotionMementos.begin() + j);
-                } else {
-                    j += 1;
+                    memento.mergePointerStateTo(otherMemento);
+                    merged = true;
+                    break;
                 }
             }
-            other.mMotionMementos.push_back(memento);
+            if (!merged) {
+                memento.firstNewPointerIdx = 0;
+                other.mMotionMementos.push_back(memento);
+            }
         }
     }
 }
diff --git a/services/inputflinger/dispatcher/InputState.h b/services/inputflinger/dispatcher/InputState.h
index a93f486..08266ae 100644
--- a/services/inputflinger/dispatcher/InputState.h
+++ b/services/inputflinger/dispatcher/InputState.h
@@ -24,6 +24,8 @@
 
 namespace android::inputdispatcher {
 
+static constexpr int32_t INVALID_POINTER_INDEX = -1;
+
 /* Tracks dispatched key and motion event state so that cancellation events can be
  * synthesized when events are dropped. */
 class InputState {
@@ -52,11 +54,14 @@
     std::vector<EventEntry*> synthesizeCancelationEvents(nsecs_t currentTime,
                                                          const CancelationOptions& options);
 
+    // Synthesizes down events for the current state.
+    std::vector<EventEntry*> synthesizePointerDownEvents(nsecs_t currentTime);
+
     // Clears the current state.
     void clear();
 
-    // Copies pointer-related parts of the input state to another instance.
-    void copyPointerStateTo(InputState& other) const;
+    // Merges pointer-related parts of the input state into another instance.
+    void mergePointerStateTo(InputState& other);
 
     // Gets the fallback key associated with a keycode.
     // Returns -1 if none.
@@ -97,10 +102,13 @@
         uint32_t pointerCount;
         PointerProperties pointerProperties[MAX_POINTERS];
         PointerCoords pointerCoords[MAX_POINTERS];
+        // Track for which pointers the target doesn't know about.
+        int32_t firstNewPointerIdx = INVALID_POINTER_INDEX;
         bool hovering;
         uint32_t policyFlags;
 
         void setPointers(const MotionEntry& entry);
+        void mergePointerStateTo(MotionMemento& other) const;
     };
 
     std::vector<KeyMemento> mKeyMementos;
diff --git a/services/inputflinger/tests/InputDispatcher_test.cpp b/services/inputflinger/tests/InputDispatcher_test.cpp
index 094452a..27db8f5 100644
--- a/services/inputflinger/tests/InputDispatcher_test.cpp
+++ b/services/inputflinger/tests/InputDispatcher_test.cpp
@@ -594,12 +594,40 @@
                      expectedFlags);
     }
 
-    void consumeMotionDown(int32_t expectedDisplayId, int32_t expectedFlags = 0) {
+    void consumeMotionCancel(int32_t expectedDisplayId = ADISPLAY_ID_DEFAULT,
+            int32_t expectedFlags = 0) {
+        consumeEvent(AINPUT_EVENT_TYPE_MOTION, AMOTION_EVENT_ACTION_CANCEL, expectedDisplayId,
+                     expectedFlags);
+    }
+
+    void consumeMotionMove(int32_t expectedDisplayId = ADISPLAY_ID_DEFAULT,
+            int32_t expectedFlags = 0) {
+        consumeEvent(AINPUT_EVENT_TYPE_MOTION, AMOTION_EVENT_ACTION_MOVE, expectedDisplayId,
+                     expectedFlags);
+    }
+
+    void consumeMotionDown(int32_t expectedDisplayId = ADISPLAY_ID_DEFAULT,
+            int32_t expectedFlags = 0) {
         consumeEvent(AINPUT_EVENT_TYPE_MOTION, AMOTION_EVENT_ACTION_DOWN, expectedDisplayId,
                      expectedFlags);
     }
 
-    void consumeMotionUp(int32_t expectedDisplayId, int32_t expectedFlags = 0) {
+    void consumeMotionPointerDown(int32_t pointerIdx,
+            int32_t expectedDisplayId = ADISPLAY_ID_DEFAULT, int32_t expectedFlags = 0) {
+        int32_t action = AMOTION_EVENT_ACTION_POINTER_DOWN
+                | (pointerIdx << AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT);
+        consumeEvent(AINPUT_EVENT_TYPE_MOTION, action, expectedDisplayId, expectedFlags);
+    }
+
+    void consumeMotionPointerUp(int32_t pointerIdx, int32_t expectedDisplayId = ADISPLAY_ID_DEFAULT,
+            int32_t expectedFlags = 0) {
+        int32_t action = AMOTION_EVENT_ACTION_POINTER_UP
+                | (pointerIdx << AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT);
+        consumeEvent(AINPUT_EVENT_TYPE_MOTION, action, expectedDisplayId, expectedFlags);
+    }
+
+    void consumeMotionUp(int32_t expectedDisplayId = ADISPLAY_ID_DEFAULT,
+            int32_t expectedFlags = 0) {
         consumeEvent(AINPUT_EVENT_TYPE_MOTION, AMOTION_EVENT_ACTION_UP, expectedDisplayId,
                      expectedFlags);
     }
@@ -923,6 +951,161 @@
                          0 /*expectedFlags*/);
 }
 
+TEST_F(InputDispatcherTest, TransferTouchFocus_OnePointer) {
+    sp<FakeApplicationHandle> application = new FakeApplicationHandle();
+
+    // Create a couple of windows
+    sp<FakeWindowHandle> firstWindow = new FakeWindowHandle(application, mDispatcher,
+            "First Window", ADISPLAY_ID_DEFAULT);
+    sp<FakeWindowHandle> secondWindow = new FakeWindowHandle(application, mDispatcher,
+            "Second Window", ADISPLAY_ID_DEFAULT);
+
+    // Add the windows to the dispatcher
+    mDispatcher->setInputWindows({firstWindow, secondWindow}, ADISPLAY_ID_DEFAULT);
+
+    // Send down to the first window
+    NotifyMotionArgs downMotionArgs = generateMotionArgs(AMOTION_EVENT_ACTION_DOWN,
+            AINPUT_SOURCE_TOUCHSCREEN, ADISPLAY_ID_DEFAULT);
+    mDispatcher->notifyMotion(&downMotionArgs);
+    // Only the first window should get the down event
+    firstWindow->consumeMotionDown();
+    secondWindow->assertNoEvents();
+
+    // Transfer touch focus to the second window
+    mDispatcher->transferTouchFocus(firstWindow->getToken(), secondWindow->getToken());
+    // The first window gets cancel and the second gets down
+    firstWindow->consumeMotionCancel();
+    secondWindow->consumeMotionDown();
+
+    // Send up event to the second window
+    NotifyMotionArgs upMotionArgs = generateMotionArgs(AMOTION_EVENT_ACTION_UP,
+            AINPUT_SOURCE_TOUCHSCREEN, ADISPLAY_ID_DEFAULT);
+    mDispatcher->notifyMotion(&upMotionArgs);
+    // The first  window gets no events and the second gets up
+    firstWindow->assertNoEvents();
+    secondWindow->consumeMotionUp();
+}
+
+TEST_F(InputDispatcherTest, TransferTouchFocus_TwoPointerNoSplitTouch) {
+    sp<FakeApplicationHandle> application = new FakeApplicationHandle();
+
+    PointF touchPoint = {10, 10};
+
+    // Create a couple of windows
+    sp<FakeWindowHandle> firstWindow = new FakeWindowHandle(application, mDispatcher,
+            "First Window", ADISPLAY_ID_DEFAULT);
+    sp<FakeWindowHandle> secondWindow = new FakeWindowHandle(application, mDispatcher,
+            "Second Window", ADISPLAY_ID_DEFAULT);
+
+    // Add the windows to the dispatcher
+    mDispatcher->setInputWindows({firstWindow, secondWindow}, ADISPLAY_ID_DEFAULT);
+
+    // Send down to the first window
+    NotifyMotionArgs downMotionArgs = generateMotionArgs(AMOTION_EVENT_ACTION_DOWN,
+            AINPUT_SOURCE_TOUCHSCREEN, ADISPLAY_ID_DEFAULT, {touchPoint});
+    mDispatcher->notifyMotion(&downMotionArgs);
+    // Only the first window should get the down event
+    firstWindow->consumeMotionDown();
+    secondWindow->assertNoEvents();
+
+    // Send pointer down to the first window
+    NotifyMotionArgs pointerDownMotionArgs = generateMotionArgs(AMOTION_EVENT_ACTION_POINTER_DOWN
+            | (1 << AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT),
+            AINPUT_SOURCE_TOUCHSCREEN, ADISPLAY_ID_DEFAULT, {touchPoint, touchPoint});
+    mDispatcher->notifyMotion(&pointerDownMotionArgs);
+    // Only the first window should get the pointer down event
+    firstWindow->consumeMotionPointerDown(1);
+    secondWindow->assertNoEvents();
+
+    // Transfer touch focus to the second window
+    mDispatcher->transferTouchFocus(firstWindow->getToken(), secondWindow->getToken());
+    // The first window gets cancel and the second gets down and pointer down
+    firstWindow->consumeMotionCancel();
+    secondWindow->consumeMotionDown();
+    secondWindow->consumeMotionPointerDown(1);
+
+    // Send pointer up to the second window
+    NotifyMotionArgs pointerUpMotionArgs = generateMotionArgs(AMOTION_EVENT_ACTION_POINTER_UP
+            | (1 << AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT),
+            AINPUT_SOURCE_TOUCHSCREEN, ADISPLAY_ID_DEFAULT, {touchPoint, touchPoint});
+    mDispatcher->notifyMotion(&pointerUpMotionArgs);
+    // The first window gets nothing and the second gets pointer up
+    firstWindow->assertNoEvents();
+    secondWindow->consumeMotionPointerUp(1);
+
+    // Send up event to the second window
+    NotifyMotionArgs upMotionArgs = generateMotionArgs(AMOTION_EVENT_ACTION_UP,
+            AINPUT_SOURCE_TOUCHSCREEN, ADISPLAY_ID_DEFAULT);
+    mDispatcher->notifyMotion(&upMotionArgs);
+    // The first window gets nothing and the second gets up
+    firstWindow->assertNoEvents();
+    secondWindow->consumeMotionUp();
+}
+
+TEST_F(InputDispatcherTest, TransferTouchFocus_TwoPointersSplitTouch) {
+    sp<FakeApplicationHandle> application = new FakeApplicationHandle();
+
+    // Create a non touch modal window that supports split touch
+    sp<FakeWindowHandle> firstWindow = new FakeWindowHandle(application, mDispatcher,
+            "First Window", ADISPLAY_ID_DEFAULT);
+    firstWindow->setFrame(Rect(0, 0, 600, 400));
+    firstWindow->setLayoutParamFlags(InputWindowInfo::FLAG_NOT_TOUCH_MODAL
+            | InputWindowInfo::FLAG_SPLIT_TOUCH);
+
+    // Create a non touch modal window that supports split touch
+    sp<FakeWindowHandle> secondWindow = new FakeWindowHandle(application, mDispatcher,
+            "Second Window", ADISPLAY_ID_DEFAULT);
+    secondWindow->setFrame(Rect(0, 400, 600, 800));
+    secondWindow->setLayoutParamFlags(InputWindowInfo::FLAG_NOT_TOUCH_MODAL
+            | InputWindowInfo::FLAG_SPLIT_TOUCH);
+
+    // Add the windows to the dispatcher
+    mDispatcher->setInputWindows({firstWindow, secondWindow}, ADISPLAY_ID_DEFAULT);
+
+    PointF pointInFirst = {300, 200};
+    PointF pointInSecond = {300, 600};
+
+    // Send down to the first window
+    NotifyMotionArgs firstDownMotionArgs = generateMotionArgs(AMOTION_EVENT_ACTION_DOWN,
+            AINPUT_SOURCE_TOUCHSCREEN, ADISPLAY_ID_DEFAULT, {pointInFirst});
+    mDispatcher->notifyMotion(&firstDownMotionArgs);
+    // Only the first window should get the down event
+    firstWindow->consumeMotionDown();
+    secondWindow->assertNoEvents();
+
+    // Send down to the second window
+    NotifyMotionArgs secondDownMotionArgs = generateMotionArgs(AMOTION_EVENT_ACTION_POINTER_DOWN
+            | (1 << AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT),
+            AINPUT_SOURCE_TOUCHSCREEN, ADISPLAY_ID_DEFAULT, {pointInFirst, pointInSecond});
+    mDispatcher->notifyMotion(&secondDownMotionArgs);
+    // The first window gets a move and the second a down
+    firstWindow->consumeMotionMove();
+    secondWindow->consumeMotionDown();
+
+    // Transfer touch focus to the second window
+    mDispatcher->transferTouchFocus(firstWindow->getToken(), secondWindow->getToken());
+    // The first window gets cancel and the new gets pointer down (it already saw down)
+    firstWindow->consumeMotionCancel();
+    secondWindow->consumeMotionPointerDown(1);
+
+    // Send pointer up to the second window
+    NotifyMotionArgs pointerUpMotionArgs = generateMotionArgs(AMOTION_EVENT_ACTION_POINTER_UP
+            | (1 << AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT),
+            AINPUT_SOURCE_TOUCHSCREEN, ADISPLAY_ID_DEFAULT, {pointInFirst, pointInSecond});
+    mDispatcher->notifyMotion(&pointerUpMotionArgs);
+    // The first window gets nothing and the second gets pointer up
+    firstWindow->assertNoEvents();
+    secondWindow->consumeMotionPointerUp(1);
+
+    // Send up event to the second window
+    NotifyMotionArgs upMotionArgs = generateMotionArgs(AMOTION_EVENT_ACTION_UP,
+            AINPUT_SOURCE_TOUCHSCREEN, ADISPLAY_ID_DEFAULT);
+    mDispatcher->notifyMotion(&upMotionArgs);
+    // The first window gets nothing and the second gets up
+    firstWindow->assertNoEvents();
+    secondWindow->consumeMotionUp();
+}
+
 TEST_F(InputDispatcherTest, FocusedWindow_ReceivesFocusEventAndKeyEvent) {
     sp<FakeApplicationHandle> application = new FakeApplicationHandle();
     sp<FakeWindowHandle> window =