Merge "ultrahdr: Avoid possible failure during gainmap compression" into udc-dev
diff --git a/include/input/Input.h b/include/input/Input.h
index fe0c775..527a477 100644
--- a/include/input/Input.h
+++ b/include/input/Input.h
@@ -242,6 +242,19 @@
     ftl_last = PALM,
 };
 
+/**
+ * The state of the key. This should have 1:1 correspondence with the values of anonymous enum
+ * defined in input.h
+ */
+enum class KeyState {
+    UNKNOWN = AKEY_STATE_UNKNOWN,
+    UP = AKEY_STATE_UP,
+    DOWN = AKEY_STATE_DOWN,
+    VIRTUAL = AKEY_STATE_VIRTUAL,
+    ftl_first = UNKNOWN,
+    ftl_last = VIRTUAL,
+};
+
 bool isStylusToolType(ToolType toolType);
 
 /*
diff --git a/libs/ui/Gralloc5.cpp b/libs/ui/Gralloc5.cpp
index 2106839..c3b2d3d 100644
--- a/libs/ui/Gralloc5.cpp
+++ b/libs/ui/Gralloc5.cpp
@@ -343,14 +343,17 @@
             return BAD_VALUE;
         }
     }
-    {
-        auto value = getStandardMetadata<StandardMetadataType::USAGE>(mMapper, bufferHandle);
-        if (static_cast<BufferUsage>(usage) != value) {
-            ALOGW("Usage didn't match, expected %" PRIu64 " got %" PRId64, usage,
-                  static_cast<int64_t>(value.value_or(BufferUsage::CPU_READ_NEVER)));
-            return BAD_VALUE;
-        }
-    }
+    // TODO: This can false-positive fail if the allocator adjusted the USAGE bits internally
+    //       Investigate further & re-enable or remove, but for now ignoring usage should be OK
+    (void)usage;
+    // {
+    //     auto value = getStandardMetadata<StandardMetadataType::USAGE>(mMapper, bufferHandle);
+    //     if (static_cast<BufferUsage>(usage) != value) {
+    //         ALOGW("Usage didn't match, expected %" PRIu64 " got %" PRId64, usage,
+    //               static_cast<int64_t>(value.value_or(BufferUsage::CPU_READ_NEVER)));
+    //         return BAD_VALUE;
+    //     }
+    // }
     {
         auto value = getStandardMetadata<StandardMetadataType::STRIDE>(mMapper, bufferHandle);
         if (stride != value) {
diff --git a/services/inputflinger/dispatcher/InputDispatcher.cpp b/services/inputflinger/dispatcher/InputDispatcher.cpp
index bdd45dc..0cc7cfb 100644
--- a/services/inputflinger/dispatcher/InputDispatcher.cpp
+++ b/services/inputflinger/dispatcher/InputDispatcher.cpp
@@ -5659,14 +5659,6 @@
     } else {
         dump += INDENT "Displays: <none>\n";
     }
-    dump += INDENT "Window Infos:\n";
-    dump += StringPrintf(INDENT2 "vsync id: %" PRId64 "\n", mWindowInfosVsyncId);
-    dump += StringPrintf(INDENT2 "timestamp (ns): %" PRId64 "\n", mWindowInfosTimestamp);
-    dump += "\n";
-    dump += StringPrintf(INDENT2 "max update delay (ns): %" PRId64 "\n", mMaxWindowInfosDelay);
-    dump += StringPrintf(INDENT2 "max update delay vsync id: %" PRId64 "\n",
-                         mMaxWindowInfosDelayVsyncId);
-    dump += "\n";
 
     if (!mGlobalMonitorsByDisplay.empty()) {
         for (const auto& [displayId, monitors] : mGlobalMonitorsByDisplay) {
@@ -6708,15 +6700,6 @@
         for (const auto& [displayId, handles] : handlesPerDisplay) {
             setInputWindowsLocked(handles, displayId);
         }
-
-        mWindowInfosVsyncId = update.vsyncId;
-        mWindowInfosTimestamp = update.timestamp;
-
-        int64_t delay = systemTime() - update.timestamp;
-        if (delay > mMaxWindowInfosDelay) {
-            mMaxWindowInfosDelay = delay;
-            mMaxWindowInfosDelayVsyncId = update.vsyncId;
-        }
     }
     // Wake up poll loop since it may need to make new input dispatching choices.
     mLooper->wake();
diff --git a/services/inputflinger/dispatcher/InputDispatcher.h b/services/inputflinger/dispatcher/InputDispatcher.h
index 0e9cfef..8ca01b7 100644
--- a/services/inputflinger/dispatcher/InputDispatcher.h
+++ b/services/inputflinger/dispatcher/InputDispatcher.h
@@ -204,11 +204,6 @@
 
     const IdGenerator mIdGenerator;
 
-    int64_t mWindowInfosVsyncId GUARDED_BY(mLock);
-    int64_t mWindowInfosTimestamp GUARDED_BY(mLock);
-    int64_t mMaxWindowInfosDelay GUARDED_BY(mLock) = -1;
-    int64_t mMaxWindowInfosDelayVsyncId GUARDED_BY(mLock) = -1;
-
     // With each iteration, InputDispatcher nominally processes one queued event,
     // a timeout, or a response from an input consumer.
     // This method should only be called on the input dispatcher's own thread.
diff --git a/services/inputflinger/reader/InputDevice.cpp b/services/inputflinger/reader/InputDevice.cpp
index c6b93a5..0a64a1c 100644
--- a/services/inputflinger/reader/InputDevice.cpp
+++ b/services/inputflinger/reader/InputDevice.cpp
@@ -151,22 +151,21 @@
         return;
     }
     std::unique_ptr<InputDeviceContext> contextPtr(new InputDeviceContext(*this, eventHubId));
-    mDevices.insert(
-            {eventHubId,
-             std::make_pair<std::unique_ptr<InputDeviceContext>,
-                            std::vector<std::unique_ptr<InputMapper>>>(std::move(contextPtr), {})});
+    std::vector<std::unique_ptr<InputMapper>> mappers;
+
+    mDevices.insert({eventHubId, std::make_pair(std::move(contextPtr), std::move(mappers))});
 }
 
-void InputDevice::populateMappers(int32_t eventHubId,
-                                  const InputReaderConfiguration& readerConfig) {
-    auto targetDevice = mDevices.find(eventHubId);
-    LOG_ALWAYS_FATAL_IF(targetDevice == mDevices.end(),
-                        "InputDevice::populateMappers(): missing device with eventHubId %d ",
-                        eventHubId);
-    // create and add mappers to device
-    InputDeviceContext& context = *targetDevice->second.first;
-    std::vector<std::unique_ptr<InputMapper>> mappers = createMappers(context, readerConfig);
-    targetDevice->second.second = std::move(mappers);
+void InputDevice::addEventHubDevice(int32_t eventHubId,
+                                    const InputReaderConfiguration& readerConfig) {
+    if (mDevices.find(eventHubId) != mDevices.end()) {
+        return;
+    }
+    std::unique_ptr<InputDeviceContext> contextPtr(new InputDeviceContext(*this, eventHubId));
+    std::vector<std::unique_ptr<InputMapper>> mappers = createMappers(*contextPtr, readerConfig);
+
+    // insert the context into the devices set
+    mDevices.insert({eventHubId, std::make_pair(std::move(contextPtr), std::move(mappers))});
     // Must change generation to flag this device as changed
     bumpGeneration();
 }
@@ -441,29 +440,29 @@
 }
 
 std::vector<std::unique_ptr<InputMapper>> InputDevice::createMappers(
-        InputDeviceContext& context, const InputReaderConfiguration& readerConfig) {
-    ftl::Flags<InputDeviceClass> classes = context.getDeviceClasses();
+        InputDeviceContext& contextPtr, const InputReaderConfiguration& readerConfig) {
+    ftl::Flags<InputDeviceClass> classes = contextPtr.getDeviceClasses();
     std::vector<std::unique_ptr<InputMapper>> mappers;
 
     // Switch-like devices.
     if (classes.test(InputDeviceClass::SWITCH)) {
-        mappers.push_back(createInputMapper<SwitchInputMapper>(context, readerConfig));
+        mappers.push_back(createInputMapper<SwitchInputMapper>(contextPtr, readerConfig));
     }
 
     // Scroll wheel-like devices.
     if (classes.test(InputDeviceClass::ROTARY_ENCODER)) {
-        mappers.push_back(createInputMapper<RotaryEncoderInputMapper>(context, readerConfig));
+        mappers.push_back(createInputMapper<RotaryEncoderInputMapper>(contextPtr, readerConfig));
     }
 
     // Vibrator-like devices.
     if (classes.test(InputDeviceClass::VIBRATOR)) {
-        mappers.push_back(createInputMapper<VibratorInputMapper>(context, readerConfig));
+        mappers.push_back(createInputMapper<VibratorInputMapper>(contextPtr, readerConfig));
     }
 
     // Battery-like devices or light-containing devices.
     // PeripheralController will be created with associated EventHub device.
     if (classes.test(InputDeviceClass::BATTERY) || classes.test(InputDeviceClass::LIGHT)) {
-        mController = std::make_unique<PeripheralController>(context);
+        mController = std::make_unique<PeripheralController>(contextPtr);
     }
 
     // Keyboard-like devices.
@@ -483,13 +482,13 @@
     }
 
     if (keyboardSource != 0) {
-        mappers.push_back(createInputMapper<KeyboardInputMapper>(context, readerConfig,
+        mappers.push_back(createInputMapper<KeyboardInputMapper>(contextPtr, readerConfig,
                                                                  keyboardSource, keyboardType));
     }
 
     // Cursor-like devices.
     if (classes.test(InputDeviceClass::CURSOR)) {
-        mappers.push_back(createInputMapper<CursorInputMapper>(context, readerConfig));
+        mappers.push_back(createInputMapper<CursorInputMapper>(contextPtr, readerConfig));
     }
 
     // Touchscreens and touchpad devices.
@@ -497,31 +496,31 @@
             sysprop::InputProperties::enable_touchpad_gestures_library().value_or(true);
     // TODO(b/272518665): Fix the new touchpad stack for Sony DualShock 4 (5c4, 9cc) touchpads, or
     // at least load this setting from the IDC file.
-    const InputDeviceIdentifier identifier = context.getDeviceIdentifier();
+    const InputDeviceIdentifier identifier = contextPtr.getDeviceIdentifier();
     const bool isSonyDualShock4Touchpad = identifier.vendor == 0x054c &&
             (identifier.product == 0x05c4 || identifier.product == 0x09cc);
     if (ENABLE_TOUCHPAD_GESTURES_LIBRARY && classes.test(InputDeviceClass::TOUCHPAD) &&
         classes.test(InputDeviceClass::TOUCH_MT) && !isSonyDualShock4Touchpad) {
-        mappers.push_back(createInputMapper<TouchpadInputMapper>(context, readerConfig));
+        mappers.push_back(createInputMapper<TouchpadInputMapper>(contextPtr, readerConfig));
     } else if (classes.test(InputDeviceClass::TOUCH_MT)) {
-        mappers.push_back(createInputMapper<MultiTouchInputMapper>(context, readerConfig));
+        mappers.push_back(std::make_unique<MultiTouchInputMapper>(contextPtr, readerConfig));
     } else if (classes.test(InputDeviceClass::TOUCH)) {
-        mappers.push_back(createInputMapper<SingleTouchInputMapper>(context, readerConfig));
+        mappers.push_back(std::make_unique<SingleTouchInputMapper>(contextPtr, readerConfig));
     }
 
     // Joystick-like devices.
     if (classes.test(InputDeviceClass::JOYSTICK)) {
-        mappers.push_back(createInputMapper<JoystickInputMapper>(context, readerConfig));
+        mappers.push_back(createInputMapper<JoystickInputMapper>(contextPtr, readerConfig));
     }
 
     // Motion sensor enabled devices.
     if (classes.test(InputDeviceClass::SENSOR)) {
-        mappers.push_back(createInputMapper<SensorInputMapper>(context, readerConfig));
+        mappers.push_back(createInputMapper<SensorInputMapper>(contextPtr, readerConfig));
     }
 
     // External stylus-like devices.
     if (classes.test(InputDeviceClass::EXTERNAL_STYLUS)) {
-        mappers.push_back(createInputMapper<ExternalStylusInputMapper>(context, readerConfig));
+        mappers.push_back(createInputMapper<ExternalStylusInputMapper>(contextPtr, readerConfig));
     }
     return mappers;
 }
diff --git a/services/inputflinger/reader/InputReader.cpp b/services/inputflinger/reader/InputReader.cpp
index 8a33dff..ea95f78 100644
--- a/services/inputflinger/reader/InputReader.cpp
+++ b/services/inputflinger/reader/InputReader.cpp
@@ -334,9 +334,7 @@
         device = std::make_shared<InputDevice>(&mContext, deviceId, bumpGenerationLocked(),
                                                identifier);
     }
-    device->addEmptyEventHubDevice(eventHubId);
-    auto unused = device->configure(systemTime(SYSTEM_TIME_MONOTONIC), mConfig, /*changes=*/{});
-    device->populateMappers(eventHubId, mConfig);
+    device->addEventHubDevice(eventHubId, mConfig);
     return device;
 }
 
diff --git a/services/inputflinger/reader/include/InputDevice.h b/services/inputflinger/reader/include/InputDevice.h
index 1729d46..0b8a608 100644
--- a/services/inputflinger/reader/include/InputDevice.h
+++ b/services/inputflinger/reader/include/InputDevice.h
@@ -81,7 +81,7 @@
 
     void dump(std::string& dump, const std::string& eventHubDevStr);
     void addEmptyEventHubDevice(int32_t eventHubId);
-    void populateMappers(int32_t eventHubId, const InputReaderConfiguration& readerConfig);
+    void addEventHubDevice(int32_t eventHubId, const InputReaderConfiguration& readerConfig);
     void removeEventHubDevice(int32_t eventHubId);
     [[nodiscard]] std::list<NotifyArgs> configure(nsecs_t when,
                                                   const InputReaderConfiguration& readerConfig,
@@ -203,7 +203,7 @@
     int32_t getState(uint32_t sourceMask, int32_t code, GetStateFunc getStateFunc);
 
     std::vector<std::unique_ptr<InputMapper>> createMappers(
-            InputDeviceContext& context, const InputReaderConfiguration& readerConfig);
+            InputDeviceContext& contextPtr, const InputReaderConfiguration& readerConfig);
 
     PropertyMap mConfiguration;
 
diff --git a/services/inputflinger/reader/mapper/MultiTouchInputMapper.h b/services/inputflinger/reader/mapper/MultiTouchInputMapper.h
index 1d788df..f300ee1 100644
--- a/services/inputflinger/reader/mapper/MultiTouchInputMapper.h
+++ b/services/inputflinger/reader/mapper/MultiTouchInputMapper.h
@@ -27,6 +27,8 @@
     friend std::unique_ptr<T> createInputMapper(InputDeviceContext& deviceContext,
                                                 const InputReaderConfiguration& readerConfig,
                                                 Args... args);
+    explicit MultiTouchInputMapper(InputDeviceContext& deviceContext,
+                                   const InputReaderConfiguration& readerConfig);
 
     ~MultiTouchInputMapper() override;
 
@@ -39,8 +41,6 @@
     bool hasStylus() const override;
 
 private:
-    explicit MultiTouchInputMapper(InputDeviceContext& deviceContext,
-                                   const InputReaderConfiguration& readerConfig);
     // simulate_stylus_with_touch is a debug mode that converts all finger pointers reported by this
     // mapper's touchscreen into stylus pointers, and adds SOURCE_STYLUS to the input device.
     // It is used to simulate stylus events for debugging and testing on a device that does not
diff --git a/services/inputflinger/reader/mapper/SingleTouchInputMapper.h b/services/inputflinger/reader/mapper/SingleTouchInputMapper.h
index 7726bfb..dac53cf 100644
--- a/services/inputflinger/reader/mapper/SingleTouchInputMapper.h
+++ b/services/inputflinger/reader/mapper/SingleTouchInputMapper.h
@@ -27,6 +27,8 @@
     friend std::unique_ptr<T> createInputMapper(InputDeviceContext& deviceContext,
                                                 const InputReaderConfiguration& readerConfig,
                                                 Args... args);
+    explicit SingleTouchInputMapper(InputDeviceContext& deviceContext,
+                                    const InputReaderConfiguration& readerConfig);
 
     ~SingleTouchInputMapper() override;
 
@@ -40,8 +42,6 @@
 
 private:
     SingleTouchMotionAccumulator mSingleTouchMotionAccumulator;
-    explicit SingleTouchInputMapper(InputDeviceContext& deviceContext,
-                                    const InputReaderConfiguration& readerConfig);
 };
 
 } // namespace android
diff --git a/services/inputflinger/tests/Android.bp b/services/inputflinger/tests/Android.bp
index 52277ff..569690a 100644
--- a/services/inputflinger/tests/Android.bp
+++ b/services/inputflinger/tests/Android.bp
@@ -40,6 +40,7 @@
         "AnrTracker_test.cpp",
         "BlockingQueue_test.cpp",
         "CapturedTouchpadEventConverter_test.cpp",
+        "CursorInputMapper_test.cpp",
         "EventHub_test.cpp",
         "FakeEventHub.cpp",
         "FakeInputReaderPolicy.cpp",
@@ -58,6 +59,7 @@
         "PreferStylusOverTouch_test.cpp",
         "PropertyProvider_test.cpp",
         "TestInputListener.cpp",
+        "TouchpadInputMapper_test.cpp",
         "UinputDevice.cpp",
         "UnwantedInteractionBlocker_test.cpp",
     ],
diff --git a/services/inputflinger/tests/CursorInputMapper_test.cpp b/services/inputflinger/tests/CursorInputMapper_test.cpp
new file mode 100644
index 0000000..6774b17
--- /dev/null
+++ b/services/inputflinger/tests/CursorInputMapper_test.cpp
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "CursorInputMapper.h"
+
+#include <android-base/logging.h>
+#include <gtest/gtest.h>
+
+#include "FakePointerController.h"
+#include "InputMapperTest.h"
+#include "InterfaceMocks.h"
+#include "TestInputListenerMatchers.h"
+
+#define TAG "CursorInputMapper_test"
+
+namespace android {
+
+using testing::Return;
+using testing::VariantWith;
+constexpr auto ACTION_DOWN = AMOTION_EVENT_ACTION_DOWN;
+constexpr auto ACTION_MOVE = AMOTION_EVENT_ACTION_MOVE;
+constexpr auto ACTION_UP = AMOTION_EVENT_ACTION_UP;
+constexpr auto BUTTON_PRESS = AMOTION_EVENT_ACTION_BUTTON_PRESS;
+constexpr auto BUTTON_RELEASE = AMOTION_EVENT_ACTION_BUTTON_RELEASE;
+constexpr auto HOVER_MOVE = AMOTION_EVENT_ACTION_HOVER_MOVE;
+
+/**
+ * Unit tests for CursorInputMapper.
+ * This class is named 'CursorInputMapperUnitTest' to avoid name collision with the existing
+ * 'CursorInputMapperTest'. If all of the CursorInputMapper tests are migrated here, the name
+ * can be simplified to 'CursorInputMapperTest'.
+ * TODO(b/283812079): move CursorInputMapper tests here.
+ */
+class CursorInputMapperUnitTest : public InputMapperUnitTest {
+protected:
+    void SetUp() override {
+        InputMapperUnitTest::SetUp();
+
+        // Current scan code state - all keys are UP by default
+        setScanCodeState(KeyState::UP,
+                         {BTN_LEFT, BTN_RIGHT, BTN_MIDDLE, BTN_BACK, BTN_SIDE, BTN_FORWARD,
+                          BTN_EXTRA, BTN_TASK});
+        EXPECT_CALL(mMockEventHub, hasRelativeAxis(EVENTHUB_ID, REL_WHEEL))
+                .WillRepeatedly(Return(false));
+        EXPECT_CALL(mMockEventHub, hasRelativeAxis(EVENTHUB_ID, REL_HWHEEL))
+                .WillRepeatedly(Return(false));
+
+        EXPECT_CALL(mMockInputReaderContext, bumpGeneration()).WillRepeatedly(Return(1));
+
+        mMapper = createInputMapper<CursorInputMapper>(*mDeviceContext, mReaderConfiguration);
+    }
+};
+
+/**
+ * Move the mouse and then click the button. Check whether HOVER_EXIT is generated when hovering
+ * ends. Currently, it is not.
+ */
+TEST_F(CursorInputMapperUnitTest, HoverAndLeftButtonPress) {
+    std::list<NotifyArgs> args;
+
+    // Move the cursor a little
+    args += process(EV_REL, REL_X, 10);
+    args += process(EV_REL, REL_Y, 20);
+    args += process(EV_SYN, SYN_REPORT, 0);
+    ASSERT_THAT(args, ElementsAre(VariantWith<NotifyMotionArgs>(WithMotionAction(HOVER_MOVE))));
+
+    // Now click the mouse button
+    args.clear();
+    args += process(EV_KEY, BTN_LEFT, 1);
+    args += process(EV_SYN, SYN_REPORT, 0);
+    ASSERT_THAT(args,
+                ElementsAre(VariantWith<NotifyMotionArgs>(WithMotionAction(ACTION_DOWN)),
+                            VariantWith<NotifyMotionArgs>(WithMotionAction(BUTTON_PRESS))));
+
+    // Move some more.
+    args.clear();
+    args += process(EV_REL, REL_X, 10);
+    args += process(EV_REL, REL_Y, 20);
+    args += process(EV_SYN, SYN_REPORT, 0);
+    ASSERT_THAT(args, ElementsAre(VariantWith<NotifyMotionArgs>(WithMotionAction(ACTION_MOVE))));
+
+    // Release the button
+    args.clear();
+    args += process(EV_KEY, BTN_LEFT, 0);
+    args += process(EV_SYN, SYN_REPORT, 0);
+    ASSERT_THAT(args,
+                ElementsAre(VariantWith<NotifyMotionArgs>(WithMotionAction(BUTTON_RELEASE)),
+                            VariantWith<NotifyMotionArgs>(WithMotionAction(ACTION_UP)),
+                            VariantWith<NotifyMotionArgs>(WithMotionAction(HOVER_MOVE))));
+}
+
+} // namespace android
diff --git a/services/inputflinger/tests/InputMapperTest.cpp b/services/inputflinger/tests/InputMapperTest.cpp
index ad48a79..0eee2b9 100644
--- a/services/inputflinger/tests/InputMapperTest.cpp
+++ b/services/inputflinger/tests/InputMapperTest.cpp
@@ -22,6 +22,74 @@
 
 namespace android {
 
+using testing::Return;
+
+void InputMapperUnitTest::SetUp() {
+    mFakePointerController = std::make_shared<FakePointerController>();
+    mFakePointerController->setBounds(0, 0, 800 - 1, 480 - 1);
+    mFakePointerController->setPosition(400, 240);
+
+    EXPECT_CALL(mMockInputReaderContext, getPointerController(DEVICE_ID))
+            .WillRepeatedly(Return(mFakePointerController));
+
+    EXPECT_CALL(mMockInputReaderContext, getEventHub()).WillRepeatedly(Return(&mMockEventHub));
+    InputDeviceIdentifier identifier;
+    identifier.name = "device";
+    identifier.location = "USB1";
+    identifier.bus = 0;
+
+    EXPECT_CALL(mMockEventHub, getDeviceIdentifier(EVENTHUB_ID)).WillRepeatedly(Return(identifier));
+    mDevice = std::make_unique<InputDevice>(&mMockInputReaderContext, DEVICE_ID,
+                                            /*generation=*/2, identifier);
+    mDeviceContext = std::make_unique<InputDeviceContext>(*mDevice, EVENTHUB_ID);
+}
+
+void InputMapperUnitTest::setupAxis(int axis, bool valid, int32_t min, int32_t max,
+                                    int32_t resolution) {
+    EXPECT_CALL(mMockEventHub, getAbsoluteAxisInfo(EVENTHUB_ID, axis, testing::_))
+            .WillRepeatedly([=](int32_t, int32_t, RawAbsoluteAxisInfo* outAxisInfo) {
+                outAxisInfo->valid = valid;
+                outAxisInfo->minValue = min;
+                outAxisInfo->maxValue = max;
+                outAxisInfo->flat = 0;
+                outAxisInfo->fuzz = 0;
+                outAxisInfo->resolution = resolution;
+                return valid ? OK : -1;
+            });
+}
+
+void InputMapperUnitTest::expectScanCodes(bool present, std::set<int> scanCodes) {
+    for (const auto& scanCode : scanCodes) {
+        EXPECT_CALL(mMockEventHub, hasScanCode(EVENTHUB_ID, scanCode))
+                .WillRepeatedly(testing::Return(present));
+    }
+}
+
+void InputMapperUnitTest::setScanCodeState(KeyState state, std::set<int> scanCodes) {
+    for (const auto& scanCode : scanCodes) {
+        EXPECT_CALL(mMockEventHub, getScanCodeState(EVENTHUB_ID, scanCode))
+                .WillRepeatedly(testing::Return(static_cast<int>(state)));
+    }
+}
+
+void InputMapperUnitTest::setKeyCodeState(KeyState state, std::set<int> keyCodes) {
+    for (const auto& keyCode : keyCodes) {
+        EXPECT_CALL(mMockEventHub, getKeyCodeState(EVENTHUB_ID, keyCode))
+                .WillRepeatedly(testing::Return(static_cast<int>(state)));
+    }
+}
+
+std::list<NotifyArgs> InputMapperUnitTest::process(int32_t type, int32_t code, int32_t value) {
+    RawEvent event;
+    event.when = systemTime(SYSTEM_TIME_MONOTONIC);
+    event.readTime = event.when;
+    event.deviceId = mMapper->getDeviceContext().getEventHubId();
+    event.type = type;
+    event.code = code;
+    event.value = value;
+    return mMapper->process(&event);
+}
+
 const char* InputMapperTest::DEVICE_NAME = "device";
 const char* InputMapperTest::DEVICE_LOCATION = "USB1";
 const ftl::Flags<InputDeviceClass> InputMapperTest::DEVICE_CLASSES =
diff --git a/services/inputflinger/tests/InputMapperTest.h b/services/inputflinger/tests/InputMapperTest.h
index 2b6655c..909bd9c 100644
--- a/services/inputflinger/tests/InputMapperTest.h
+++ b/services/inputflinger/tests/InputMapperTest.h
@@ -23,16 +23,48 @@
 #include <InputMapper.h>
 #include <NotifyArgs.h>
 #include <ftl/flags.h>
+#include <gmock/gmock.h>
 #include <utils/StrongPointer.h>
 
 #include "FakeEventHub.h"
 #include "FakeInputReaderPolicy.h"
 #include "InstrumentedInputReader.h"
+#include "InterfaceMocks.h"
 #include "TestConstants.h"
 #include "TestInputListener.h"
 
 namespace android {
 
+class InputMapperUnitTest : public testing::Test {
+protected:
+    static constexpr int32_t EVENTHUB_ID = 1;
+    static constexpr int32_t DEVICE_ID = END_RESERVED_ID + 1000;
+    virtual void SetUp() override;
+
+    void setupAxis(int axis, bool valid, int32_t min, int32_t max, int32_t resolution);
+
+    void expectScanCodes(bool present, std::set<int> scanCodes);
+
+    void setScanCodeState(KeyState state, std::set<int> scanCodes);
+
+    void setKeyCodeState(KeyState state, std::set<int> keyCodes);
+
+    std::list<NotifyArgs> process(int32_t type, int32_t code, int32_t value);
+
+    MockEventHubInterface mMockEventHub;
+    std::shared_ptr<FakePointerController> mFakePointerController;
+    MockInputReaderContext mMockInputReaderContext;
+    std::unique_ptr<InputDevice> mDevice;
+
+    std::unique_ptr<InputDeviceContext> mDeviceContext;
+    InputReaderConfiguration mReaderConfiguration;
+    // The mapper should be created by the subclasses.
+    std::unique_ptr<InputMapper> mMapper;
+};
+
+/**
+ * Deprecated - use InputMapperUnitTest instead.
+ */
 class InputMapperTest : public testing::Test {
 protected:
     static const char* DEVICE_NAME;
diff --git a/services/inputflinger/tests/InputReader_test.cpp b/services/inputflinger/tests/InputReader_test.cpp
index c045e15..bfb371f 100644
--- a/services/inputflinger/tests/InputReader_test.cpp
+++ b/services/inputflinger/tests/InputReader_test.cpp
@@ -2584,10 +2584,7 @@
     mFakeEventHub->addDevice(TEST_EVENTHUB_ID, "Test EventHub device", InputDeviceClass::BATTERY);
 
     InputDevice device(mReader->getContext(), /*id=*/1, /*generation=*/2, /*identifier=*/{});
-    device.addEmptyEventHubDevice(TEST_EVENTHUB_ID);
-    auto unused = device.configure(systemTime(SYSTEM_TIME_MONOTONIC),
-                                   mFakePolicy->getReaderConfiguration(), /*changes=*/{});
-    device.populateMappers(TEST_EVENTHUB_ID, mFakePolicy->getReaderConfiguration());
+    device.addEventHubDevice(TEST_EVENTHUB_ID, mFakePolicy->getReaderConfiguration());
     device.removeEventHubDevice(TEST_EVENTHUB_ID);
     std::string dumpStr, eventHubDevStr;
     device.dump(dumpStr, eventHubDevStr);
diff --git a/services/inputflinger/tests/InterfaceMocks.h b/services/inputflinger/tests/InterfaceMocks.h
new file mode 100644
index 0000000..d720a90
--- /dev/null
+++ b/services/inputflinger/tests/InterfaceMocks.h
@@ -0,0 +1,146 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <android-base/logging.h>
+#include <gmock/gmock.h>
+
+namespace android {
+
+class MockInputReaderContext : public InputReaderContext {
+public:
+    MOCK_METHOD(void, updateGlobalMetaState, (), (override));
+    int32_t getGlobalMetaState() override { return 0; };
+
+    MOCK_METHOD(void, disableVirtualKeysUntil, (nsecs_t time), (override));
+    MOCK_METHOD(bool, shouldDropVirtualKey, (nsecs_t now, int32_t keyCode, int32_t scanCode),
+                (override));
+
+    MOCK_METHOD(void, fadePointer, (), (override));
+    MOCK_METHOD(std::shared_ptr<PointerControllerInterface>, getPointerController,
+                (int32_t deviceId), (override));
+
+    MOCK_METHOD(void, requestTimeoutAtTime, (nsecs_t when), (override));
+    MOCK_METHOD(int32_t, bumpGeneration, (), (override));
+
+    MOCK_METHOD(void, getExternalStylusDevices, (std::vector<InputDeviceInfo> & outDevices),
+                (override));
+    MOCK_METHOD(std::list<NotifyArgs>, dispatchExternalStylusState, (const StylusState& outState),
+                (override));
+
+    MOCK_METHOD(InputReaderPolicyInterface*, getPolicy, (), (override));
+    MOCK_METHOD(EventHubInterface*, getEventHub, (), (override));
+
+    int32_t getNextId() override { return 1; };
+
+    MOCK_METHOD(void, updateLedMetaState, (int32_t metaState), (override));
+    MOCK_METHOD(int32_t, getLedMetaState, (), (override));
+};
+
+class MockEventHubInterface : public EventHubInterface {
+public:
+    MOCK_METHOD(ftl::Flags<InputDeviceClass>, getDeviceClasses, (int32_t deviceId), (const));
+    MOCK_METHOD(InputDeviceIdentifier, getDeviceIdentifier, (int32_t deviceId), (const));
+    MOCK_METHOD(int32_t, getDeviceControllerNumber, (int32_t deviceId), (const));
+    MOCK_METHOD(std::optional<PropertyMap>, getConfiguration, (int32_t deviceId), (const));
+    MOCK_METHOD(status_t, getAbsoluteAxisInfo,
+                (int32_t deviceId, int axis, RawAbsoluteAxisInfo* outAxisInfo), (const));
+    MOCK_METHOD(bool, hasRelativeAxis, (int32_t deviceId, int axis), (const));
+    MOCK_METHOD(bool, hasInputProperty, (int32_t deviceId, int property), (const));
+    MOCK_METHOD(bool, hasMscEvent, (int32_t deviceId, int mscEvent), (const));
+    MOCK_METHOD(void, addKeyRemapping, (int32_t deviceId, int fromKeyCode, int toKeyCode), (const));
+    MOCK_METHOD(status_t, mapKey,
+                (int32_t deviceId, int scanCode, int usageCode, int32_t metaState,
+                 int32_t* outKeycode, int32_t* outMetaState, uint32_t* outFlags),
+                (const));
+    MOCK_METHOD(status_t, mapAxis, (int32_t deviceId, int scanCode, AxisInfo* outAxisInfo),
+                (const));
+    MOCK_METHOD(void, setExcludedDevices, (const std::vector<std::string>& devices));
+    MOCK_METHOD(std::vector<RawEvent>, getEvents, (int timeoutMillis));
+    MOCK_METHOD(std::vector<TouchVideoFrame>, getVideoFrames, (int32_t deviceId));
+    MOCK_METHOD((base::Result<std::pair<InputDeviceSensorType, int32_t>>), mapSensor,
+                (int32_t deviceId, int32_t absCode), (const, override));
+    MOCK_METHOD(std::vector<int32_t>, getRawBatteryIds, (int32_t deviceId), (const, override));
+    MOCK_METHOD(std::optional<RawBatteryInfo>, getRawBatteryInfo,
+                (int32_t deviceId, int32_t BatteryId), (const, override));
+    MOCK_METHOD(std::vector<int32_t>, getRawLightIds, (int32_t deviceId), (const, override));
+    MOCK_METHOD(std::optional<RawLightInfo>, getRawLightInfo, (int32_t deviceId, int32_t lightId),
+                (const, override));
+    MOCK_METHOD(std::optional<int32_t>, getLightBrightness, (int32_t deviceId, int32_t lightId),
+                (const, override));
+    MOCK_METHOD(void, setLightBrightness, (int32_t deviceId, int32_t lightId, int32_t brightness),
+                (override));
+    MOCK_METHOD((std::optional<std::unordered_map<LightColor, int32_t>>), getLightIntensities,
+                (int32_t deviceId, int32_t lightId), (const, override));
+    MOCK_METHOD(void, setLightIntensities,
+                (int32_t deviceId, int32_t lightId,
+                 (std::unordered_map<LightColor, int32_t>)intensities),
+                (override));
+
+    MOCK_METHOD(std::optional<RawLayoutInfo>, getRawLayoutInfo, (int32_t deviceId),
+                (const, override));
+    MOCK_METHOD(int32_t, getScanCodeState, (int32_t deviceId, int32_t scanCode), (const, override));
+    MOCK_METHOD(int32_t, getKeyCodeState, (int32_t deviceId, int32_t keyCode), (const, override));
+    MOCK_METHOD(int32_t, getSwitchState, (int32_t deviceId, int32_t sw), (const, override));
+
+    MOCK_METHOD(status_t, getAbsoluteAxisValue, (int32_t deviceId, int32_t axis, int32_t* outValue),
+                (const, override));
+    MOCK_METHOD(int32_t, getKeyCodeForKeyLocation, (int32_t deviceId, int32_t locationKeyCode),
+                (const, override));
+    MOCK_METHOD(bool, markSupportedKeyCodes,
+                (int32_t deviceId, const std::vector<int32_t>& keyCodes, uint8_t* outFlags),
+                (const, override));
+
+    MOCK_METHOD(bool, hasScanCode, (int32_t deviceId, int32_t scanCode), (const, override));
+
+    MOCK_METHOD(bool, hasKeyCode, (int32_t deviceId, int32_t keyCode), (const, override));
+
+    MOCK_METHOD(bool, hasLed, (int32_t deviceId, int32_t led), (const, override));
+
+    MOCK_METHOD(void, setLedState, (int32_t deviceId, int32_t led, bool on), (override));
+
+    MOCK_METHOD(void, getVirtualKeyDefinitions,
+                (int32_t deviceId, std::vector<VirtualKeyDefinition>& outVirtualKeys),
+                (const, override));
+
+    MOCK_METHOD(const std::shared_ptr<KeyCharacterMap>, getKeyCharacterMap, (int32_t deviceId),
+                (const, override));
+
+    MOCK_METHOD(bool, setKeyboardLayoutOverlay,
+                (int32_t deviceId, std::shared_ptr<KeyCharacterMap> map), (override));
+
+    MOCK_METHOD(void, vibrate, (int32_t deviceId, const VibrationElement& effect), (override));
+    MOCK_METHOD(void, cancelVibrate, (int32_t deviceId), (override));
+
+    MOCK_METHOD(std::vector<int32_t>, getVibratorIds, (int32_t deviceId), (const, override));
+    MOCK_METHOD(std::optional<int32_t>, getBatteryCapacity, (int32_t deviceId, int32_t batteryId),
+                (const, override));
+
+    MOCK_METHOD(std::optional<int32_t>, getBatteryStatus, (int32_t deviceId, int32_t batteryId),
+                (const, override));
+    MOCK_METHOD(void, requestReopenDevices, (), (override));
+    MOCK_METHOD(void, wake, (), (override));
+
+    MOCK_METHOD(void, dump, (std::string & dump), (const, override));
+    MOCK_METHOD(void, monitor, (), (const, override));
+    MOCK_METHOD(bool, isDeviceEnabled, (int32_t deviceId), (const, override));
+    MOCK_METHOD(status_t, enableDevice, (int32_t deviceId), (override));
+    MOCK_METHOD(status_t, disableDevice, (int32_t deviceId), (override));
+    MOCK_METHOD(void, sysfsNodeChanged, (const std::string& sysfsNodePath), (override));
+};
+
+} // namespace android
diff --git a/services/inputflinger/tests/TouchpadInputMapper_test.cpp b/services/inputflinger/tests/TouchpadInputMapper_test.cpp
new file mode 100644
index 0000000..92cd462
--- /dev/null
+++ b/services/inputflinger/tests/TouchpadInputMapper_test.cpp
@@ -0,0 +1,155 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "TouchpadInputMapper.h"
+
+#include <android-base/logging.h>
+#include <gtest/gtest.h>
+
+#include <thread>
+#include "FakePointerController.h"
+#include "InputMapperTest.h"
+#include "InterfaceMocks.h"
+#include "TestInputListenerMatchers.h"
+
+#define TAG "TouchpadInputMapper_test"
+
+namespace android {
+
+using testing::Return;
+using testing::VariantWith;
+constexpr auto ACTION_DOWN = AMOTION_EVENT_ACTION_DOWN;
+constexpr auto ACTION_UP = AMOTION_EVENT_ACTION_UP;
+constexpr auto BUTTON_PRESS = AMOTION_EVENT_ACTION_BUTTON_PRESS;
+constexpr auto BUTTON_RELEASE = AMOTION_EVENT_ACTION_BUTTON_RELEASE;
+constexpr auto HOVER_MOVE = AMOTION_EVENT_ACTION_HOVER_MOVE;
+
+/**
+ * Unit tests for TouchpadInputMapper.
+ */
+class TouchpadInputMapperTest : public InputMapperUnitTest {
+protected:
+    void SetUp() override {
+        InputMapperUnitTest::SetUp();
+
+        // Present scan codes: BTN_TOUCH and BTN_TOOL_FINGER
+        expectScanCodes(/*present=*/true,
+                        {BTN_LEFT, BTN_RIGHT, BTN_TOOL_FINGER, BTN_TOOL_QUINTTAP, BTN_TOUCH,
+                         BTN_TOOL_DOUBLETAP, BTN_TOOL_TRIPLETAP, BTN_TOOL_QUADTAP});
+        // Missing scan codes that the mapper checks for.
+        expectScanCodes(/*present=*/false,
+                        {BTN_TOOL_PEN, BTN_TOOL_RUBBER, BTN_TOOL_BRUSH, BTN_TOOL_PENCIL,
+                         BTN_TOOL_AIRBRUSH});
+
+        // Current scan code state - all keys are UP by default
+        setScanCodeState(KeyState::UP, {BTN_TOUCH,          BTN_STYLUS,
+                                        BTN_STYLUS2,        BTN_0,
+                                        BTN_TOOL_FINGER,    BTN_TOOL_PEN,
+                                        BTN_TOOL_RUBBER,    BTN_TOOL_BRUSH,
+                                        BTN_TOOL_PENCIL,    BTN_TOOL_AIRBRUSH,
+                                        BTN_TOOL_MOUSE,     BTN_TOOL_LENS,
+                                        BTN_TOOL_DOUBLETAP, BTN_TOOL_TRIPLETAP,
+                                        BTN_TOOL_QUADTAP,   BTN_TOOL_QUINTTAP,
+                                        BTN_LEFT,           BTN_RIGHT,
+                                        BTN_MIDDLE,         BTN_BACK,
+                                        BTN_SIDE,           BTN_FORWARD,
+                                        BTN_EXTRA,          BTN_TASK});
+
+        setKeyCodeState(KeyState::UP,
+                        {AKEYCODE_STYLUS_BUTTON_PRIMARY, AKEYCODE_STYLUS_BUTTON_SECONDARY});
+
+        // Key mappings
+        EXPECT_CALL(mMockEventHub,
+                    mapKey(EVENTHUB_ID, BTN_LEFT, /*usageCode=*/0, /*metaState=*/0, testing::_,
+                           testing::_, testing::_))
+                .WillRepeatedly(Return(NAME_NOT_FOUND));
+
+        // Input properties - only INPUT_PROP_BUTTONPAD
+        EXPECT_CALL(mMockEventHub, hasInputProperty(EVENTHUB_ID, INPUT_PROP_BUTTONPAD))
+                .WillRepeatedly(Return(true));
+        EXPECT_CALL(mMockEventHub, hasInputProperty(EVENTHUB_ID, INPUT_PROP_SEMI_MT))
+                .WillRepeatedly(Return(false));
+
+        // Axes that the device has
+        setupAxis(ABS_MT_SLOT, /*valid=*/true, /*min=*/0, /*max=*/4, /*resolution=*/0);
+        setupAxis(ABS_MT_POSITION_X, /*valid=*/true, /*min=*/0, /*max=*/2000, /*resolution=*/24);
+        setupAxis(ABS_MT_POSITION_Y, /*valid=*/true, /*min=*/0, /*max=*/1000, /*resolution=*/24);
+        setupAxis(ABS_MT_PRESSURE, /*valid=*/true, /*min*/ 0, /*max=*/255, /*resolution=*/0);
+        // Axes that the device does not have
+        setupAxis(ABS_MT_ORIENTATION, /*valid=*/false, /*min=*/0, /*max=*/0, /*resolution=*/0);
+        setupAxis(ABS_MT_TOUCH_MAJOR, /*valid=*/false, /*min=*/0, /*max=*/0, /*resolution=*/0);
+        setupAxis(ABS_MT_TOUCH_MINOR, /*valid=*/false, /*min=*/0, /*max=*/0, /*resolution=*/0);
+        setupAxis(ABS_MT_WIDTH_MAJOR, /*valid=*/false, /*min=*/0, /*max=*/0, /*resolution=*/0);
+        setupAxis(ABS_MT_WIDTH_MINOR, /*valid=*/false, /*min=*/0, /*max=*/0, /*resolution=*/0);
+
+        EXPECT_CALL(mMockEventHub, getAbsoluteAxisValue(EVENTHUB_ID, ABS_MT_SLOT, testing::_))
+                .WillRepeatedly([](int32_t eventHubId, int32_t, int32_t* outValue) {
+                    *outValue = 0;
+                    return OK;
+                });
+        mMapper = createInputMapper<TouchpadInputMapper>(*mDeviceContext, mReaderConfiguration);
+    }
+};
+
+/**
+ * Start moving the finger and then click the left touchpad button. Check whether HOVER_EXIT is
+ * generated when hovering stops. Currently, it is not.
+ * In the current implementation, HOVER_MOVE and ACTION_DOWN events are not sent out right away,
+ * but only after the button is released.
+ */
+TEST_F(TouchpadInputMapperTest, HoverAndLeftButtonPress) {
+    std::list<NotifyArgs> args;
+
+    args += process(EV_ABS, ABS_MT_TRACKING_ID, 1);
+    args += process(EV_KEY, BTN_TOUCH, 1);
+    setScanCodeState(KeyState::DOWN, {BTN_TOOL_FINGER});
+    args += process(EV_KEY, BTN_TOOL_FINGER, 1);
+    args += process(EV_ABS, ABS_MT_POSITION_X, 50);
+    args += process(EV_ABS, ABS_MT_POSITION_Y, 50);
+    args += process(EV_ABS, ABS_MT_PRESSURE, 1);
+    args += process(EV_SYN, SYN_REPORT, 0);
+    ASSERT_THAT(args, testing::IsEmpty());
+
+    // Without this sleep, the test fails.
+    // TODO(b/284133337): Figure out whether this can be removed
+    std::this_thread::sleep_for(std::chrono::milliseconds(20));
+
+    args += process(EV_KEY, BTN_LEFT, 1);
+    setScanCodeState(KeyState::DOWN, {BTN_LEFT});
+    args += process(EV_SYN, SYN_REPORT, 0);
+
+    args += process(EV_KEY, BTN_LEFT, 0);
+    setScanCodeState(KeyState::UP, {BTN_LEFT});
+    args += process(EV_SYN, SYN_REPORT, 0);
+    ASSERT_THAT(args,
+                ElementsAre(VariantWith<NotifyMotionArgs>(WithMotionAction(HOVER_MOVE)),
+                            VariantWith<NotifyMotionArgs>(WithMotionAction(ACTION_DOWN)),
+                            VariantWith<NotifyMotionArgs>(WithMotionAction(BUTTON_PRESS)),
+                            VariantWith<NotifyMotionArgs>(WithMotionAction(BUTTON_RELEASE)),
+                            VariantWith<NotifyMotionArgs>(WithMotionAction(ACTION_UP))));
+
+    // Liftoff
+    args.clear();
+    args += process(EV_ABS, ABS_MT_PRESSURE, 0);
+    args += process(EV_ABS, ABS_MT_TRACKING_ID, -1);
+    args += process(EV_KEY, BTN_TOUCH, 0);
+    setScanCodeState(KeyState::UP, {BTN_TOOL_FINGER});
+    args += process(EV_KEY, BTN_TOOL_FINGER, 0);
+    args += process(EV_SYN, SYN_REPORT, 0);
+    ASSERT_THAT(args, testing::IsEmpty());
+}
+
+} // namespace android
diff --git a/services/surfaceflinger/SurfaceFlinger.cpp b/services/surfaceflinger/SurfaceFlinger.cpp
index b1d4b3c..2ac1db9 100644
--- a/services/surfaceflinger/SurfaceFlinger.cpp
+++ b/services/surfaceflinger/SurfaceFlinger.cpp
@@ -2547,7 +2547,7 @@
     }
 
     updateCursorAsync();
-    updateInputFlinger(vsyncId);
+    updateInputFlinger(vsyncId, frameTime);
 
     if (mLayerTracingEnabled && !mLayerTracing.flagIsSet(LayerTracing::TRACE_COMPOSITION)) {
         // This will block and tracing should only be enabled for debugging.
@@ -3740,7 +3740,7 @@
     doCommitTransactions();
 }
 
-void SurfaceFlinger::updateInputFlinger(VsyncId vsyncId) {
+void SurfaceFlinger::updateInputFlinger(VsyncId vsyncId, TimePoint frameTime) {
     if (!mInputFlinger || (!mUpdateInputInfo && mInputWindowCommands.empty())) {
         return;
     }
@@ -3752,8 +3752,6 @@
     if (mUpdateInputInfo) {
         mUpdateInputInfo = false;
         updateWindowInfo = true;
-        mLastInputFlingerUpdateVsyncId = vsyncId;
-        mLastInputFlingerUpdateTimestamp = systemTime();
         buildWindowInfos(windowInfos, displayInfos);
     }
 
@@ -3775,17 +3773,17 @@
                                                       inputWindowCommands =
                                                               std::move(mInputWindowCommands),
                                                       inputFlinger = mInputFlinger, this,
-                                                      visibleWindowsChanged]() {
+                                                      visibleWindowsChanged, vsyncId, frameTime]() {
         ATRACE_NAME("BackgroundExecutor::updateInputFlinger");
         if (updateWindowInfo) {
             mWindowInfosListenerInvoker
-                    ->windowInfosChanged(std::move(windowInfos), std::move(displayInfos),
+                    ->windowInfosChanged(gui::WindowInfosUpdate{std::move(windowInfos),
+                                                                std::move(displayInfos),
+                                                                vsyncId.value, frameTime.ns()},
                                          std::move(
                                                  inputWindowCommands.windowInfosReportedListeners),
                                          /* forceImmediateCall= */ visibleWindowsChanged ||
-                                                 !inputWindowCommands.focusRequests.empty(),
-                                         mLastInputFlingerUpdateVsyncId,
-                                         mLastInputFlingerUpdateTimestamp);
+                                                 !inputWindowCommands.focusRequests.empty());
         } else {
             // If there are listeners but no changes to input windows, call the listeners
             // immediately.
@@ -6152,27 +6150,14 @@
     result.append("\n");
 
     result.append("Window Infos:\n");
-    StringAppendF(&result, "  input flinger update vsync id: %" PRId64 "\n",
-                  mLastInputFlingerUpdateVsyncId.value);
-    StringAppendF(&result, "  input flinger update timestamp (ns): %" PRId64 "\n",
-                  mLastInputFlingerUpdateTimestamp);
+    auto windowInfosDebug = mWindowInfosListenerInvoker->getDebugInfo();
+    StringAppendF(&result, "  max send vsync id: %" PRId64 "\n",
+                  windowInfosDebug.maxSendDelayVsyncId.value);
+    StringAppendF(&result, "  max send delay (ns): %" PRId64 " ns\n",
+                  windowInfosDebug.maxSendDelayDuration);
+    StringAppendF(&result, "  unsent messages: %" PRIu32 "\n",
+                  windowInfosDebug.pendingMessageCount);
     result.append("\n");
-
-    if (int64_t unsentVsyncId = mWindowInfosListenerInvoker->getUnsentMessageVsyncId().value;
-        unsentVsyncId != -1) {
-        StringAppendF(&result, "  unsent input flinger update vsync id: %" PRId64 "\n",
-                      unsentVsyncId);
-        StringAppendF(&result, "  unsent input flinger update timestamp (ns): %" PRId64 "\n",
-                      mWindowInfosListenerInvoker->getUnsentMessageTimestamp());
-        result.append("\n");
-    }
-
-    if (uint32_t pendingMessages = mWindowInfosListenerInvoker->getPendingMessageCount();
-        pendingMessages != 0) {
-        StringAppendF(&result, "  pending input flinger calls: %" PRIu32 "\n",
-                      mWindowInfosListenerInvoker->getPendingMessageCount());
-        result.append("\n");
-    }
 }
 
 mat4 SurfaceFlinger::calculateColorMatrix(float saturation) {
diff --git a/services/surfaceflinger/SurfaceFlinger.h b/services/surfaceflinger/SurfaceFlinger.h
index e2691ab..0bc506f 100644
--- a/services/surfaceflinger/SurfaceFlinger.h
+++ b/services/surfaceflinger/SurfaceFlinger.h
@@ -722,7 +722,7 @@
     void updateLayerHistory(const frontend::LayerSnapshot& snapshot);
     frontend::Update flushLifecycleUpdates() REQUIRES(kMainThreadContext);
 
-    void updateInputFlinger(VsyncId);
+    void updateInputFlinger(VsyncId vsyncId, TimePoint frameTime);
     void persistDisplayBrightness(bool needsComposite) REQUIRES(kMainThreadContext);
     void buildWindowInfos(std::vector<gui::WindowInfo>& outWindowInfos,
                           std::vector<gui::DisplayInfo>& outDisplayInfos);
@@ -1259,9 +1259,6 @@
 
     VsyncId mLastCommittedVsyncId;
 
-    VsyncId mLastInputFlingerUpdateVsyncId;
-    nsecs_t mLastInputFlingerUpdateTimestamp;
-
     // If blurs should be enabled on this device.
     bool mSupportsBlur = false;
     std::atomic<uint32_t> mFrameMissedCount = 0;
diff --git a/services/surfaceflinger/WindowInfosListenerInvoker.cpp b/services/surfaceflinger/WindowInfosListenerInvoker.cpp
index 2b62638..20699ef 100644
--- a/services/surfaceflinger/WindowInfosListenerInvoker.cpp
+++ b/services/surfaceflinger/WindowInfosListenerInvoker.cpp
@@ -16,8 +16,11 @@
 
 #include <ftl/small_vector.h>
 #include <gui/ISurfaceComposer.h>
+#include <gui/TraceUtils.h>
 #include <gui/WindowInfosUpdate.h>
+#include <scheduler/Time.h>
 
+#include "BackgroundExecutor.h"
 #include "WindowInfosListenerInvoker.h"
 
 namespace android {
@@ -26,7 +29,7 @@
 using gui::IWindowInfosListener;
 using gui::WindowInfo;
 
-using WindowInfosListenerVector = ftl::SmallVector<const sp<IWindowInfosListener>, 3>;
+using WindowInfosListenerVector = ftl::SmallVector<const sp<gui::IWindowInfosListener>, 3>;
 
 struct WindowInfosReportedListenerInvoker : gui::BnWindowInfosReportedListener,
                                             IBinder::DeathRecipient {
@@ -86,45 +89,19 @@
 }
 
 void WindowInfosListenerInvoker::windowInfosChanged(
-        std::vector<WindowInfo> windowInfos, std::vector<DisplayInfo> displayInfos,
-        WindowInfosReportedListenerSet reportedListeners, bool forceImmediateCall, VsyncId vsyncId,
-        nsecs_t timestamp) {
-    reportedListeners.insert(sp<WindowInfosListenerInvoker>::fromExisting(this));
-    auto callListeners = [this, windowInfos = std::move(windowInfos),
-                          displayInfos = std::move(displayInfos), vsyncId,
-                          timestamp](WindowInfosReportedListenerSet reportedListeners) mutable {
-        WindowInfosListenerVector windowInfosListeners;
-        {
-            std::scoped_lock lock(mListenersMutex);
-            for (const auto& [_, listener] : mWindowInfosListeners) {
-                windowInfosListeners.push_back(listener);
-            }
-        }
-
-        auto reportedInvoker =
-                sp<WindowInfosReportedListenerInvoker>::make(windowInfosListeners,
-                                                             std::move(reportedListeners));
-
-        gui::WindowInfosUpdate update(std::move(windowInfos), std::move(displayInfos),
-                                      vsyncId.value, timestamp);
-
-        for (const auto& listener : windowInfosListeners) {
-            sp<IBinder> asBinder = IInterface::asBinder(listener);
-
-            // linkToDeath is used here to ensure that the windowInfosReportedListeners
-            // are called even if one of the windowInfosListeners dies before
-            // calling onWindowInfosReported.
-            asBinder->linkToDeath(reportedInvoker);
-
-            auto status = listener->onWindowInfosChanged(update, reportedInvoker);
-            if (!status.isOk()) {
-                reportedInvoker->onWindowInfosReported();
-            }
-        }
-    };
-
+        gui::WindowInfosUpdate update, WindowInfosReportedListenerSet reportedListeners,
+        bool forceImmediateCall) {
+    WindowInfosListenerVector listeners;
     {
-        std::scoped_lock lock(mMessagesMutex);
+        std::scoped_lock lock{mMessagesMutex};
+
+        if (!mDelayInfo) {
+            mDelayInfo = DelayInfo{
+                    .vsyncId = update.vsyncId,
+                    .frameTime = update.timestamp,
+            };
+        }
+
         // If there are unacked messages and this isn't a forced call, then return immediately.
         // If a forced window infos change doesn't happen first, the update will be sent after
         // the WindowInfosReportedListeners are called. If a forced window infos change happens or
@@ -132,44 +109,87 @@
         // will be dropped and the listeners will only be called with the latest info. This is done
         // to reduce the amount of binder memory used.
         if (mActiveMessageCount > 0 && !forceImmediateCall) {
-            mWindowInfosChangedDelayed = std::move(callListeners);
-            mUnsentVsyncId = vsyncId;
-            mUnsentTimestamp = timestamp;
-            mReportedListenersDelayed.merge(reportedListeners);
+            mDelayedUpdate = std::move(update);
+            mReportedListeners.merge(reportedListeners);
             return;
         }
 
-        mWindowInfosChangedDelayed = nullptr;
-        mUnsentVsyncId = {-1};
-        mUnsentTimestamp = -1;
-        reportedListeners.merge(mReportedListenersDelayed);
+        if (mDelayedUpdate) {
+            mDelayedUpdate.reset();
+        }
+
+        {
+            std::scoped_lock lock{mListenersMutex};
+            for (const auto& [_, listener] : mWindowInfosListeners) {
+                listeners.push_back(listener);
+            }
+        }
+        if (CC_UNLIKELY(listeners.empty())) {
+            mReportedListeners.merge(reportedListeners);
+            mDelayInfo.reset();
+            return;
+        }
+
+        reportedListeners.insert(sp<WindowInfosListenerInvoker>::fromExisting(this));
+        reportedListeners.merge(mReportedListeners);
+        mReportedListeners.clear();
+
         mActiveMessageCount++;
+        updateMaxSendDelay();
+        mDelayInfo.reset();
     }
-    callListeners(std::move(reportedListeners));
+
+    auto reportedInvoker =
+            sp<WindowInfosReportedListenerInvoker>::make(listeners, std::move(reportedListeners));
+
+    for (const auto& listener : listeners) {
+        sp<IBinder> asBinder = IInterface::asBinder(listener);
+
+        // linkToDeath is used here to ensure that the windowInfosReportedListeners
+        // are called even if one of the windowInfosListeners dies before
+        // calling onWindowInfosReported.
+        asBinder->linkToDeath(reportedInvoker);
+
+        auto status = listener->onWindowInfosChanged(update, reportedInvoker);
+        if (!status.isOk()) {
+            reportedInvoker->onWindowInfosReported();
+        }
+    }
 }
 
 binder::Status WindowInfosListenerInvoker::onWindowInfosReported() {
-    std::function<void(WindowInfosReportedListenerSet)> callListeners;
-    WindowInfosReportedListenerSet reportedListeners;
-
-    {
-        std::scoped_lock lock{mMessagesMutex};
-        mActiveMessageCount--;
-        if (!mWindowInfosChangedDelayed || mActiveMessageCount > 0) {
-            return binder::Status::ok();
+    BackgroundExecutor::getInstance().sendCallbacks({[this]() {
+        gui::WindowInfosUpdate update;
+        {
+            std::scoped_lock lock{mMessagesMutex};
+            mActiveMessageCount--;
+            if (!mDelayedUpdate || mActiveMessageCount > 0) {
+                return;
+            }
+            update = std::move(*mDelayedUpdate);
+            mDelayedUpdate.reset();
         }
-
-        mActiveMessageCount++;
-        callListeners = std::move(mWindowInfosChangedDelayed);
-        mWindowInfosChangedDelayed = nullptr;
-        mUnsentVsyncId = {-1};
-        mUnsentTimestamp = -1;
-        reportedListeners = std::move(mReportedListenersDelayed);
-        mReportedListenersDelayed.clear();
-    }
-
-    callListeners(std::move(reportedListeners));
+        windowInfosChanged(std::move(update), {}, false);
+    }});
     return binder::Status::ok();
 }
 
+WindowInfosListenerInvoker::DebugInfo WindowInfosListenerInvoker::getDebugInfo() {
+    std::scoped_lock lock{mMessagesMutex};
+    updateMaxSendDelay();
+    mDebugInfo.pendingMessageCount = mActiveMessageCount;
+    return mDebugInfo;
+}
+
+void WindowInfosListenerInvoker::updateMaxSendDelay() {
+    if (!mDelayInfo) {
+        return;
+    }
+    nsecs_t delay = TimePoint::now().ns() - mDelayInfo->frameTime;
+    if (delay > mDebugInfo.maxSendDelayDuration) {
+        mDebugInfo.maxSendDelayDuration = delay;
+        mDebugInfo.maxSendDelayVsyncId = VsyncId{mDelayInfo->vsyncId};
+    }
+}
+
 } // namespace android
diff --git a/services/surfaceflinger/WindowInfosListenerInvoker.h b/services/surfaceflinger/WindowInfosListenerInvoker.h
index e35d056..bc465a3 100644
--- a/services/surfaceflinger/WindowInfosListenerInvoker.h
+++ b/services/surfaceflinger/WindowInfosListenerInvoker.h
@@ -16,6 +16,7 @@
 
 #pragma once
 
+#include <optional>
 #include <unordered_set>
 
 #include <android/gui/BnWindowInfosReportedListener.h>
@@ -40,26 +41,18 @@
     void addWindowInfosListener(sp<gui::IWindowInfosListener>);
     void removeWindowInfosListener(const sp<gui::IWindowInfosListener>& windowInfosListener);
 
-    void windowInfosChanged(std::vector<gui::WindowInfo>, std::vector<gui::DisplayInfo>,
+    void windowInfosChanged(gui::WindowInfosUpdate update,
                             WindowInfosReportedListenerSet windowInfosReportedListeners,
-                            bool forceImmediateCall, VsyncId vsyncId, nsecs_t timestamp);
+                            bool forceImmediateCall);
 
     binder::Status onWindowInfosReported() override;
 
-    VsyncId getUnsentMessageVsyncId() {
-        std::scoped_lock lock(mMessagesMutex);
-        return mUnsentVsyncId;
-    }
-
-    nsecs_t getUnsentMessageTimestamp() {
-        std::scoped_lock lock(mMessagesMutex);
-        return mUnsentTimestamp;
-    }
-
-    uint32_t getPendingMessageCount() {
-        std::scoped_lock lock(mMessagesMutex);
-        return mActiveMessageCount;
-    }
+    struct DebugInfo {
+        VsyncId maxSendDelayVsyncId;
+        nsecs_t maxSendDelayDuration;
+        uint32_t pendingMessageCount;
+    };
+    DebugInfo getDebugInfo();
 
 protected:
     void binderDied(const wp<IBinder>& who) override;
@@ -73,11 +66,16 @@
 
     std::mutex mMessagesMutex;
     uint32_t mActiveMessageCount GUARDED_BY(mMessagesMutex) = 0;
-    std::function<void(WindowInfosReportedListenerSet)> mWindowInfosChangedDelayed
-            GUARDED_BY(mMessagesMutex);
-    VsyncId mUnsentVsyncId GUARDED_BY(mMessagesMutex) = {-1};
-    nsecs_t mUnsentTimestamp GUARDED_BY(mMessagesMutex) = -1;
-    WindowInfosReportedListenerSet mReportedListenersDelayed;
+    std::optional<gui::WindowInfosUpdate> mDelayedUpdate GUARDED_BY(mMessagesMutex);
+    WindowInfosReportedListenerSet mReportedListeners;
+
+    DebugInfo mDebugInfo GUARDED_BY(mMessagesMutex);
+    struct DelayInfo {
+        int64_t vsyncId;
+        nsecs_t frameTime;
+    };
+    std::optional<DelayInfo> mDelayInfo GUARDED_BY(mMessagesMutex);
+    void updateMaxSendDelay() REQUIRES(mMessagesMutex);
 };
 
 } // namespace android
diff --git a/services/surfaceflinger/fuzzer/surfaceflinger_fuzzers_utils.h b/services/surfaceflinger/fuzzer/surfaceflinger_fuzzers_utils.h
index da5ec48..4d03be0 100644
--- a/services/surfaceflinger/fuzzer/surfaceflinger_fuzzers_utils.h
+++ b/services/surfaceflinger/fuzzer/surfaceflinger_fuzzers_utils.h
@@ -590,7 +590,7 @@
         mFlinger->binderDied(display);
         mFlinger->onFirstRef();
 
-        mFlinger->updateInputFlinger(VsyncId{0});
+        mFlinger->updateInputFlinger(VsyncId{}, TimePoint{});
         mFlinger->updateCursorAsync();
 
         mutableScheduler().setVsyncConfig({.sfOffset = mFdp.ConsumeIntegral<nsecs_t>(),
diff --git a/services/surfaceflinger/tests/unittests/Android.bp b/services/surfaceflinger/tests/unittests/Android.bp
index 881b362..db81bad 100644
--- a/services/surfaceflinger/tests/unittests/Android.bp
+++ b/services/surfaceflinger/tests/unittests/Android.bp
@@ -139,6 +139,7 @@
         "VSyncReactorTest.cpp",
         "VsyncConfigurationTest.cpp",
         "VsyncScheduleTest.cpp",
+        "WindowInfosListenerInvokerTest.cpp",
     ],
 }
 
diff --git a/services/surfaceflinger/tests/unittests/WindowInfosListenerInvokerTest.cpp b/services/surfaceflinger/tests/unittests/WindowInfosListenerInvokerTest.cpp
new file mode 100644
index 0000000..af4971b
--- /dev/null
+++ b/services/surfaceflinger/tests/unittests/WindowInfosListenerInvokerTest.cpp
@@ -0,0 +1,244 @@
+#include <android/gui/BnWindowInfosListener.h>
+#include <gtest/gtest.h>
+#include <gui/SurfaceComposerClient.h>
+#include <gui/WindowInfosUpdate.h>
+#include <condition_variable>
+
+#include "BackgroundExecutor.h"
+#include "WindowInfosListenerInvoker.h"
+#include "android/gui/IWindowInfosReportedListener.h"
+
+namespace android {
+
+class WindowInfosListenerInvokerTest : public testing::Test {
+protected:
+    WindowInfosListenerInvokerTest() : mInvoker(sp<WindowInfosListenerInvoker>::make()) {}
+
+    ~WindowInfosListenerInvokerTest() {
+        std::mutex mutex;
+        std::condition_variable cv;
+        bool flushComplete = false;
+        // Flush the BackgroundExecutor thread to ensure any scheduled tasks are complete.
+        // Otherwise, references those tasks hold may go out of scope before they are done
+        // executing.
+        BackgroundExecutor::getInstance().sendCallbacks({[&]() {
+            std::scoped_lock lock{mutex};
+            flushComplete = true;
+            cv.notify_one();
+        }});
+        std::unique_lock<std::mutex> lock{mutex};
+        cv.wait(lock, [&]() { return flushComplete; });
+    }
+
+    sp<WindowInfosListenerInvoker> mInvoker;
+};
+
+using WindowInfosUpdateConsumer = std::function<void(const gui::WindowInfosUpdate&,
+                                                     const sp<gui::IWindowInfosReportedListener>&)>;
+
+class Listener : public gui::BnWindowInfosListener {
+public:
+    Listener(WindowInfosUpdateConsumer consumer) : mConsumer(std::move(consumer)) {}
+
+    binder::Status onWindowInfosChanged(
+            const gui::WindowInfosUpdate& update,
+            const sp<gui::IWindowInfosReportedListener>& reportedListener) override {
+        mConsumer(update, reportedListener);
+        return binder::Status::ok();
+    }
+
+private:
+    WindowInfosUpdateConsumer mConsumer;
+};
+
+// Test that WindowInfosListenerInvoker#windowInfosChanged calls a single window infos listener.
+TEST_F(WindowInfosListenerInvokerTest, callsSingleListener) {
+    std::mutex mutex;
+    std::condition_variable cv;
+
+    int callCount = 0;
+
+    mInvoker->addWindowInfosListener(
+            sp<Listener>::make([&](const gui::WindowInfosUpdate&,
+                                   const sp<gui::IWindowInfosReportedListener>& reportedListener) {
+                std::scoped_lock lock{mutex};
+                callCount++;
+                cv.notify_one();
+
+                reportedListener->onWindowInfosReported();
+            }));
+
+    BackgroundExecutor::getInstance().sendCallbacks(
+            {[this]() { mInvoker->windowInfosChanged({}, {}, false); }});
+
+    std::unique_lock<std::mutex> lock{mutex};
+    cv.wait(lock, [&]() { return callCount == 1; });
+    EXPECT_EQ(callCount, 1);
+}
+
+// Test that WindowInfosListenerInvoker#windowInfosChanged calls multiple window infos listeners.
+TEST_F(WindowInfosListenerInvokerTest, callsMultipleListeners) {
+    std::mutex mutex;
+    std::condition_variable cv;
+
+    int callCount = 0;
+    const int expectedCallCount = 3;
+
+    for (int i = 0; i < expectedCallCount; i++) {
+        mInvoker->addWindowInfosListener(sp<Listener>::make(
+                [&](const gui::WindowInfosUpdate&,
+                    const sp<gui::IWindowInfosReportedListener>& reportedListener) {
+                    std::scoped_lock lock{mutex};
+                    callCount++;
+                    if (callCount == expectedCallCount) {
+                        cv.notify_one();
+                    }
+
+                    reportedListener->onWindowInfosReported();
+                }));
+    }
+
+    BackgroundExecutor::getInstance().sendCallbacks(
+            {[&]() { mInvoker->windowInfosChanged({}, {}, false); }});
+
+    std::unique_lock<std::mutex> lock{mutex};
+    cv.wait(lock, [&]() { return callCount == expectedCallCount; });
+    EXPECT_EQ(callCount, expectedCallCount);
+}
+
+// Test that WindowInfosListenerInvoker#windowInfosChanged delays sending a second message until
+// after the WindowInfosReportedListener is called.
+TEST_F(WindowInfosListenerInvokerTest, delaysUnackedCall) {
+    std::mutex mutex;
+    std::condition_variable cv;
+
+    int callCount = 0;
+
+    // Simulate a slow ack by not calling the WindowInfosReportedListener.
+    mInvoker->addWindowInfosListener(sp<Listener>::make(
+            [&](const gui::WindowInfosUpdate&, const sp<gui::IWindowInfosReportedListener>&) {
+                std::scoped_lock lock{mutex};
+                callCount++;
+                cv.notify_one();
+            }));
+
+    BackgroundExecutor::getInstance().sendCallbacks({[&]() {
+        mInvoker->windowInfosChanged({}, {}, false);
+        mInvoker->windowInfosChanged({}, {}, false);
+    }});
+
+    {
+        std::unique_lock lock{mutex};
+        cv.wait(lock, [&]() { return callCount == 1; });
+    }
+    EXPECT_EQ(callCount, 1);
+
+    // Ack the first message.
+    mInvoker->onWindowInfosReported();
+
+    {
+        std::unique_lock lock{mutex};
+        cv.wait(lock, [&]() { return callCount == 2; });
+    }
+    EXPECT_EQ(callCount, 2);
+}
+
+// Test that WindowInfosListenerInvoker#windowInfosChanged immediately sends a second message when
+// forceImmediateCall is true.
+TEST_F(WindowInfosListenerInvokerTest, sendsForcedMessage) {
+    std::mutex mutex;
+    std::condition_variable cv;
+
+    int callCount = 0;
+    const int expectedCallCount = 2;
+
+    // Simulate a slow ack by not calling the WindowInfosReportedListener.
+    mInvoker->addWindowInfosListener(sp<Listener>::make(
+            [&](const gui::WindowInfosUpdate&, const sp<gui::IWindowInfosReportedListener>&) {
+                std::scoped_lock lock{mutex};
+                callCount++;
+                if (callCount == expectedCallCount) {
+                    cv.notify_one();
+                }
+            }));
+
+    BackgroundExecutor::getInstance().sendCallbacks({[&]() {
+        mInvoker->windowInfosChanged({}, {}, false);
+        mInvoker->windowInfosChanged({}, {}, true);
+    }});
+
+    {
+        std::unique_lock lock{mutex};
+        cv.wait(lock, [&]() { return callCount == expectedCallCount; });
+    }
+    EXPECT_EQ(callCount, expectedCallCount);
+}
+
+// Test that WindowInfosListenerInvoker#windowInfosChanged skips old messages when more than one
+// message is delayed.
+TEST_F(WindowInfosListenerInvokerTest, skipsDelayedMessage) {
+    std::mutex mutex;
+    std::condition_variable cv;
+
+    int64_t lastUpdateId = -1;
+
+    // Simulate a slow ack by not calling the WindowInfosReportedListener.
+    mInvoker->addWindowInfosListener(
+            sp<Listener>::make([&](const gui::WindowInfosUpdate& update,
+                                   const sp<gui::IWindowInfosReportedListener>&) {
+                std::scoped_lock lock{mutex};
+                lastUpdateId = update.vsyncId;
+                cv.notify_one();
+            }));
+
+    BackgroundExecutor::getInstance().sendCallbacks({[&]() {
+        mInvoker->windowInfosChanged({{}, {}, /* vsyncId= */ 1, 0}, {}, false);
+        mInvoker->windowInfosChanged({{}, {}, /* vsyncId= */ 2, 0}, {}, false);
+        mInvoker->windowInfosChanged({{}, {}, /* vsyncId= */ 3, 0}, {}, false);
+    }});
+
+    {
+        std::unique_lock lock{mutex};
+        cv.wait(lock, [&]() { return lastUpdateId == 1; });
+    }
+    EXPECT_EQ(lastUpdateId, 1);
+
+    // Ack the first message. The third update should be sent.
+    mInvoker->onWindowInfosReported();
+
+    {
+        std::unique_lock lock{mutex};
+        cv.wait(lock, [&]() { return lastUpdateId == 3; });
+    }
+    EXPECT_EQ(lastUpdateId, 3);
+}
+
+// Test that WindowInfosListenerInvoker#windowInfosChanged immediately calls listener after a call
+// where no listeners were configured.
+TEST_F(WindowInfosListenerInvokerTest, noListeners) {
+    std::mutex mutex;
+    std::condition_variable cv;
+
+    int callCount = 0;
+
+    // Test that calling windowInfosChanged without any listeners doesn't cause the next call to be
+    // delayed.
+    BackgroundExecutor::getInstance().sendCallbacks({[&]() {
+        mInvoker->windowInfosChanged({}, {}, false);
+        mInvoker->addWindowInfosListener(sp<Listener>::make(
+                [&](const gui::WindowInfosUpdate&, const sp<gui::IWindowInfosReportedListener>&) {
+                    std::scoped_lock lock{mutex};
+                    callCount++;
+                    cv.notify_one();
+                }));
+        mInvoker->windowInfosChanged({}, {}, false);
+    }});
+
+    {
+        std::unique_lock lock{mutex};
+        cv.wait(lock, [&]() { return callCount == 1; });
+    }
+    EXPECT_EQ(callCount, 1);
+}
+
+} // namespace android