Merge "Sanitize address for libinput tests" into udc-qpr-dev
diff --git a/include/android/input.h b/include/android/input.h
index a45f065..9a0eb4d 100644
--- a/include/android/input.h
+++ b/include/android/input.h
@@ -781,6 +781,8 @@
      *
      * These values are relative to the state from the last event, not accumulated, so developers
      * should make sure to process this axis value for all batched historical events.
+     *
+     * This axis is only set on the first pointer in a motion event.
      */
     AMOTION_EVENT_AXIS_GESTURE_X_OFFSET = 48,
     /**
@@ -797,6 +799,8 @@
      *
      * These values are relative to the state from the last event, not accumulated, so developers
      * should make sure to process this axis value for all batched historical events.
+     *
+     * This axis is only set on the first pointer in a motion event.
      */
     AMOTION_EVENT_AXIS_GESTURE_SCROLL_X_DISTANCE = 50,
     /**
@@ -815,16 +819,29 @@
      *
      * These values are relative to the state from the last event, not accumulated, so developers
      * should make sure to process this axis value for all batched historical events.
+     *
+     * This axis is only set on the first pointer in a motion event.
      */
     AMOTION_EVENT_AXIS_GESTURE_PINCH_SCALE_FACTOR = 52,
 
     /**
+     * Axis constant: the number of fingers being used in a multi-finger swipe gesture.
+     *
+     * - For a touch pad, reports the number of fingers being used in a multi-finger swipe gesture
+     *   (with CLASSIFICATION_MULTI_FINGER_SWIPE).
+     *
+     * Since CLASSIFICATION_MULTI_FINGER_SWIPE is a hidden API, so is this axis. It is only set on
+     * the first pointer in a motion event.
+     */
+    AMOTION_EVENT_AXIS_GESTURE_SWIPE_FINGER_COUNT = 53,
+
+    /**
      * Note: This is not an "Axis constant". It does not represent any axis, nor should it be used
      * to represent any axis. It is a constant holding the value of the largest defined axis value,
      * to make some computations (like iterating through all possible axes) cleaner.
      * Please update the value accordingly if you add a new axis.
      */
-    AMOTION_EVENT_MAXIMUM_VALID_AXIS_VALUE = AMOTION_EVENT_AXIS_GESTURE_PINCH_SCALE_FACTOR,
+    AMOTION_EVENT_MAXIMUM_VALID_AXIS_VALUE = AMOTION_EVENT_AXIS_GESTURE_SWIPE_FINGER_COUNT,
 
     // NOTE: If you add a new axis here you must also add it to several other files.
     //       Refer to frameworks/base/core/java/android/view/MotionEvent.java for the full list.
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/include/input/InputDevice.h b/include/input/InputDevice.h
index 1a40fdb..b7751f7 100644
--- a/include/input/InputDevice.h
+++ b/include/input/InputDevice.h
@@ -18,8 +18,10 @@
 
 #include <android/sensor.h>
 #include <ftl/flags.h>
+#include <ftl/mixins.h>
 #include <input/Input.h>
 #include <input/KeyCharacterMap.h>
+#include <set>
 #include <unordered_map>
 #include <vector>
 
@@ -68,6 +70,9 @@
      * while conforming to the filename limitations.
      */
     std::string getCanonicalName() const;
+
+    bool operator==(const InputDeviceIdentifier&) const = default;
+    bool operator!=(const InputDeviceIdentifier&) const = default;
 };
 
 /* Types of input device sensors. Keep sync with core/java/android/hardware/Sensor.java */
@@ -179,11 +184,24 @@
     int32_t id;
 };
 
+struct BrightnessLevel : ftl::DefaultConstructible<BrightnessLevel, std::uint8_t>,
+                         ftl::Equatable<BrightnessLevel>,
+                         ftl::Orderable<BrightnessLevel>,
+                         ftl::Addable<BrightnessLevel> {
+    using DefaultConstructible::DefaultConstructible;
+};
+
 struct InputDeviceLightInfo {
     explicit InputDeviceLightInfo(std::string name, int32_t id, InputDeviceLightType type,
                                   ftl::Flags<InputDeviceLightCapability> capabilityFlags,
-                                  int32_t ordinal)
-          : name(name), id(id), type(type), capabilityFlags(capabilityFlags), ordinal(ordinal) {}
+                                  int32_t ordinal,
+                                  std::set<BrightnessLevel> preferredBrightnessLevels)
+          : name(name),
+            id(id),
+            type(type),
+            capabilityFlags(capabilityFlags),
+            ordinal(ordinal),
+            preferredBrightnessLevels(std::move(preferredBrightnessLevels)) {}
     // Name string of the light.
     std::string name;
     // Light id
@@ -194,6 +212,8 @@
     ftl::Flags<InputDeviceLightCapability> capabilityFlags;
     // Ordinal of the light
     int32_t ordinal;
+    // Custom brightness levels for the light
+    std::set<BrightnessLevel> preferredBrightnessLevels;
 };
 
 struct InputDeviceBatteryInfo {
diff --git a/include/input/PrintTools.h b/include/input/PrintTools.h
index 02bc201..0ca6fa3 100644
--- a/include/input/PrintTools.h
+++ b/include/input/PrintTools.h
@@ -88,6 +88,20 @@
 }
 
 /**
+ * Convert map keys to string. The keys of the map should be integral type.
+ */
+template <typename K, typename V>
+std::string dumpMapKeys(const std::map<K, V>& map,
+                        std::string (*keyToString)(const K&) = constToString) {
+    std::string out;
+    for (const auto& [k, _] : map) {
+        out += out.empty() ? "{" : ", ";
+        out += keyToString(k);
+    }
+    return out.empty() ? "{}" : (out + "}");
+}
+
+/**
  * Convert a vector to a string. The values of the vector should be of a type supported by
  * constToString.
  */
diff --git a/libs/input/InputEventLabels.cpp b/libs/input/InputEventLabels.cpp
index f99a7d6..1c7cc12 100644
--- a/libs/input/InputEventLabels.cpp
+++ b/libs/input/InputEventLabels.cpp
@@ -404,7 +404,8 @@
     DEFINE_AXIS(GESTURE_Y_OFFSET), \
     DEFINE_AXIS(GESTURE_SCROLL_X_DISTANCE), \
     DEFINE_AXIS(GESTURE_SCROLL_Y_DISTANCE), \
-    DEFINE_AXIS(GESTURE_PINCH_SCALE_FACTOR)
+    DEFINE_AXIS(GESTURE_PINCH_SCALE_FACTOR), \
+    DEFINE_AXIS(GESTURE_SWIPE_FINGER_COUNT)
 
 // NOTE: If you add new LEDs here, you must also add them to Input.h
 #define LEDS_SEQUENCE \
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/libs/ultrahdr/fuzzer/Android.bp b/libs/ultrahdr/fuzzer/Android.bp
new file mode 100644
index 0000000..27b38c3
--- /dev/null
+++ b/libs/ultrahdr/fuzzer/Android.bp
@@ -0,0 +1,65 @@
+// 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.
+
+package {
+    // See: http://go/android-license-faq
+    // A large-scale-change added 'default_applicable_licenses' to import
+    // all of the 'license_kinds' from "frameworks_native_license"
+    // to get the below license kinds:
+    //   SPDX-license-identifier-Apache-2.0
+    default_applicable_licenses: ["frameworks_native_license"],
+}
+
+cc_defaults {
+    name: "ultrahdr_fuzzer_defaults",
+    host_supported: true,
+    static_libs: ["liblog"],
+    target: {
+        darwin: {
+            enabled: false,
+        },
+    },
+    fuzz_config: {
+        cc: [
+            "android-media-fuzzing-reports@google.com",
+        ],
+        description: "The fuzzers target the APIs of jpeg hdr",
+        service_privilege: "constrained",
+        users: "multi_user",
+    },
+}
+
+cc_fuzz {
+    name: "ultrahdr_enc_fuzzer",
+    defaults: ["ultrahdr_fuzzer_defaults"],
+    srcs: [
+        "ultrahdr_enc_fuzzer.cpp",
+    ],
+    shared_libs: [
+        "libimage_io",
+        "libjpeg",
+        "liblog",
+    ],
+    static_libs: [
+        "libjpegdecoder",
+        "libjpegencoder",
+        "libultrahdr",
+        "libutils",
+    ],
+    fuzz_config: {
+        fuzzed_code_usage: "future_version",
+        vector: "local_no_privileges_required",
+    },
+}
+
diff --git a/libs/ultrahdr/fuzzer/ultrahdr_enc_fuzzer.cpp b/libs/ultrahdr/fuzzer/ultrahdr_enc_fuzzer.cpp
new file mode 100644
index 0000000..472699b
--- /dev/null
+++ b/libs/ultrahdr/fuzzer/ultrahdr_enc_fuzzer.cpp
@@ -0,0 +1,286 @@
+/*
+ * 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.
+ */
+
+// System include files
+#include <fuzzer/FuzzedDataProvider.h>
+#include <algorithm>
+#include <iostream>
+#include <random>
+#include <vector>
+
+// User include files
+#include "ultrahdr/gainmapmath.h"
+#include "ultrahdr/jpegencoderhelper.h"
+#include "utils/Log.h"
+
+using namespace android::ultrahdr;
+
+// constants
+const int kMinWidth = 8;
+const int kMaxWidth = 7680;
+
+const int kMinHeight = 8;
+const int kMaxHeight = 4320;
+
+const int kScaleFactor = 4;
+
+const int kJpegBlock = 16;
+
+// Color gamuts for image data, sync with ultrahdr.h
+const int kCgMin = ULTRAHDR_COLORGAMUT_UNSPECIFIED + 1;
+const int kCgMax = ULTRAHDR_COLORGAMUT_MAX;
+
+// Transfer functions for image data, sync with ultrahdr.h
+const int kTfMin = ULTRAHDR_TF_UNSPECIFIED + 1;
+const int kTfMax = ULTRAHDR_TF_MAX;
+
+// Transfer functions for image data, sync with ultrahdr.h
+const int kOfMin = ULTRAHDR_OUTPUT_UNSPECIFIED + 1;
+const int kOfMax = ULTRAHDR_OUTPUT_MAX;
+
+// quality factor
+const int kQfMin = 0;
+const int kQfMax = 100;
+
+// seed
+const unsigned kSeed = 0x7ab7;
+
+class JpegHDRFuzzer {
+public:
+    JpegHDRFuzzer(const uint8_t* data, size_t size) : mFdp(data, size){};
+    void process();
+    void fillP010Buffer(uint16_t* data, int width, int height, int stride);
+    void fill420Buffer(uint8_t* data, int size);
+
+private:
+    FuzzedDataProvider mFdp;
+};
+
+void JpegHDRFuzzer::fillP010Buffer(uint16_t* data, int width, int height, int stride) {
+    uint16_t* tmp = data;
+    std::vector<uint16_t> buffer(16);
+    for (int i = 0; i < buffer.size(); i++) {
+        buffer[i] = mFdp.ConsumeIntegralInRange<int>(0, (1 << 10) - 1);
+    }
+    for (int j = 0; j < height; j++) {
+        for (int i = 0; i < width; i += buffer.size()) {
+            memcpy(data + i, buffer.data(), std::min((int)buffer.size(), (width - i)));
+            std::shuffle(buffer.begin(), buffer.end(), std::default_random_engine(kSeed));
+        }
+        tmp += stride;
+    }
+}
+
+void JpegHDRFuzzer::fill420Buffer(uint8_t* data, int size) {
+    std::vector<uint8_t> buffer(16);
+    mFdp.ConsumeData(buffer.data(), buffer.size());
+    for (int i = 0; i < size; i += buffer.size()) {
+        memcpy(data + i, buffer.data(), std::min((int)buffer.size(), (size - i)));
+        std::shuffle(buffer.begin(), buffer.end(), std::default_random_engine(kSeed));
+    }
+}
+
+void JpegHDRFuzzer::process() {
+    while (mFdp.remaining_bytes()) {
+        struct jpegr_uncompressed_struct p010Img {};
+        struct jpegr_uncompressed_struct yuv420Img {};
+        struct jpegr_uncompressed_struct grayImg {};
+        struct jpegr_compressed_struct jpegImgR {};
+        struct jpegr_compressed_struct jpegImg {};
+        struct jpegr_compressed_struct jpegGainMap {};
+
+        // which encode api to select
+        int muxSwitch = mFdp.ConsumeIntegralInRange<int>(0, 4);
+
+        // quality factor
+        int quality = mFdp.ConsumeIntegralInRange<int>(kQfMin, kQfMax);
+
+        // hdr_tf
+        auto tf = static_cast<ultrahdr_transfer_function>(
+                mFdp.ConsumeIntegralInRange<int>(kTfMin, kTfMax));
+
+        // p010 Cg
+        auto p010Cg =
+                static_cast<ultrahdr_color_gamut>(mFdp.ConsumeIntegralInRange<int>(kCgMin, kCgMax));
+
+        // 420 Cg
+        auto yuv420Cg =
+                static_cast<ultrahdr_color_gamut>(mFdp.ConsumeIntegralInRange<int>(kCgMin, kCgMax));
+
+        // hdr_of
+        auto of = static_cast<ultrahdr_output_format>(
+                mFdp.ConsumeIntegralInRange<int>(kOfMin, kOfMax));
+
+        int width = mFdp.ConsumeIntegralInRange<int>(kMinWidth, kMaxWidth);
+        width = (width >> 1) << 1;
+
+        int height = mFdp.ConsumeIntegralInRange<int>(kMinHeight, kMaxHeight);
+        height = (height >> 1) << 1;
+
+        std::unique_ptr<uint16_t[]> bufferY = nullptr;
+        std::unique_ptr<uint16_t[]> bufferUV = nullptr;
+        std::unique_ptr<uint8_t[]> yuv420ImgRaw = nullptr;
+        std::unique_ptr<uint8_t[]> grayImgRaw = nullptr;
+        if (muxSwitch != 4) {
+            // init p010 image
+            bool isUVContiguous = mFdp.ConsumeBool();
+            bool hasYStride = mFdp.ConsumeBool();
+            int yStride = hasYStride ? mFdp.ConsumeIntegralInRange<int>(width, width + 128) : width;
+            p010Img.width = width;
+            p010Img.height = height;
+            p010Img.colorGamut = p010Cg;
+            p010Img.luma_stride = hasYStride ? yStride : 0;
+            int bppP010 = 2;
+            if (isUVContiguous) {
+                size_t p010Size = yStride * height * 3 / 2;
+                bufferY = std::make_unique<uint16_t[]>(p010Size);
+                p010Img.data = bufferY.get();
+                p010Img.chroma_data = nullptr;
+                p010Img.chroma_stride = 0;
+                fillP010Buffer(bufferY.get(), width, height, yStride);
+                fillP010Buffer(bufferY.get() + yStride * height, width, height / 2, yStride);
+            } else {
+                int uvStride = mFdp.ConsumeIntegralInRange<int>(width, width + 128);
+                size_t p010YSize = yStride * height;
+                bufferY = std::make_unique<uint16_t[]>(p010YSize);
+                p010Img.data = bufferY.get();
+                fillP010Buffer(bufferY.get(), width, height, yStride);
+                size_t p010UVSize = uvStride * p010Img.height / 2;
+                bufferUV = std::make_unique<uint16_t[]>(p010UVSize);
+                p010Img.chroma_data = bufferUV.get();
+                p010Img.chroma_stride = uvStride;
+                fillP010Buffer(bufferUV.get(), width, height / 2, uvStride);
+            }
+        } else {
+            int map_width = width / kScaleFactor;
+            int map_height = height / kScaleFactor;
+            map_width = static_cast<size_t>(floor((map_width + kJpegBlock - 1) / kJpegBlock)) *
+                    kJpegBlock;
+            map_height = ((map_height + 1) >> 1) << 1;
+            // init 400 image
+            grayImg.width = map_width;
+            grayImg.height = map_height;
+            grayImg.colorGamut = ULTRAHDR_COLORGAMUT_UNSPECIFIED;
+
+            const size_t graySize = map_width * map_height;
+            grayImgRaw = std::make_unique<uint8_t[]>(graySize);
+            grayImg.data = grayImgRaw.get();
+            fill420Buffer(grayImgRaw.get(), graySize);
+            grayImg.chroma_data = nullptr;
+            grayImg.luma_stride = 0;
+            grayImg.chroma_stride = 0;
+        }
+
+        if (muxSwitch > 0) {
+            // init 420 image
+            yuv420Img.width = width;
+            yuv420Img.height = height;
+            yuv420Img.colorGamut = yuv420Cg;
+
+            const size_t yuv420Size = (yuv420Img.width * yuv420Img.height * 3) / 2;
+            yuv420ImgRaw = std::make_unique<uint8_t[]>(yuv420Size);
+            yuv420Img.data = yuv420ImgRaw.get();
+            fill420Buffer(yuv420ImgRaw.get(), yuv420Size);
+            yuv420Img.chroma_data = nullptr;
+            yuv420Img.luma_stride = 0;
+            yuv420Img.chroma_stride = 0;
+        }
+
+        // dest
+        // 2 * p010 size as input data is random, DCT compression might not behave as expected
+        jpegImgR.maxLength = std::max(8 * 1024 /* min size 8kb */, width * height * 3 * 2);
+        auto jpegImgRaw = std::make_unique<uint8_t[]>(jpegImgR.maxLength);
+        jpegImgR.data = jpegImgRaw.get();
+
+//#define DUMP_PARAM
+#ifdef DUMP_PARAM
+        std::cout << "Api Select " << muxSwitch << std::endl;
+        std::cout << "image dimensions " << width << " x " << height << std::endl;
+        std::cout << "p010 color gamut " << p010Img.colorGamut << std::endl;
+        std::cout << "p010 luma stride " << p010Img.luma_stride << std::endl;
+        std::cout << "p010 chroma stride " << p010Img.chroma_stride << std::endl;
+        std::cout << "420 color gamut " << yuv420Img.colorGamut << std::endl;
+        std::cout << "quality factor " << quality << std::endl;
+#endif
+
+        JpegR jpegHdr;
+        android::status_t status = android::UNKNOWN_ERROR;
+        if (muxSwitch == 0) { // api 0
+            jpegImgR.length = 0;
+            status = jpegHdr.encodeJPEGR(&p010Img, tf, &jpegImgR, quality, nullptr);
+        } else if (muxSwitch == 1) { // api 1
+            jpegImgR.length = 0;
+            status = jpegHdr.encodeJPEGR(&p010Img, &yuv420Img, tf, &jpegImgR, quality, nullptr);
+        } else {
+            // compressed img
+            JpegEncoderHelper encoder;
+            if (encoder.compressImage(yuv420Img.data, yuv420Img.width, yuv420Img.height, quality,
+                                      nullptr, 0)) {
+                jpegImg.length = encoder.getCompressedImageSize();
+                jpegImg.maxLength = jpegImg.length;
+                jpegImg.data = encoder.getCompressedImagePtr();
+                jpegImg.colorGamut = yuv420Cg;
+
+                if (muxSwitch == 2) { // api 2
+                    jpegImgR.length = 0;
+                    status = jpegHdr.encodeJPEGR(&p010Img, &yuv420Img, &jpegImg, tf, &jpegImgR);
+                } else if (muxSwitch == 3) { // api 3
+                    jpegImgR.length = 0;
+                    status = jpegHdr.encodeJPEGR(&p010Img, &jpegImg, tf, &jpegImgR);
+                } else if (muxSwitch == 4) { // api 4
+                    jpegImgR.length = 0;
+                    JpegEncoderHelper gainMapEncoder;
+                    if (gainMapEncoder.compressImage(grayImg.data, grayImg.width, grayImg.height,
+                                                     quality, nullptr, 0, true)) {
+                        jpegGainMap.length = gainMapEncoder.getCompressedImageSize();
+                        jpegGainMap.maxLength = jpegImg.length;
+                        jpegGainMap.data = gainMapEncoder.getCompressedImagePtr();
+                        jpegGainMap.colorGamut = ULTRAHDR_COLORGAMUT_UNSPECIFIED;
+                        ultrahdr_metadata_struct metadata;
+                        metadata.version = "1.3.1";
+                        if (tf == ULTRAHDR_TF_HLG) {
+                            metadata.maxContentBoost = kHlgMaxNits / kSdrWhiteNits;
+                        } else if (tf == ULTRAHDR_TF_PQ) {
+                            metadata.maxContentBoost = kPqMaxNits / kSdrWhiteNits;
+                        } else {
+                            metadata.maxContentBoost = 0;
+                        }
+                        metadata.minContentBoost = 1.0f;
+                        status = jpegHdr.encodeJPEGR(&jpegImg, &jpegGainMap, &metadata, &jpegImgR);
+                    }
+                }
+            }
+        }
+        if (status == android::OK) {
+            jpegr_uncompressed_struct decodedJpegR;
+            auto decodedRaw = std::make_unique<uint8_t[]>(width * height * 8);
+            decodedJpegR.data = decodedRaw.get();
+            jpegHdr.decodeJPEGR(&jpegImgR, &decodedJpegR,
+                                mFdp.ConsumeFloatingPointInRange<float>(1.0, FLT_MAX), nullptr, of,
+                                nullptr, nullptr);
+            std::vector<uint8_t> iccData(0);
+            std::vector<uint8_t> exifData(0);
+            jpegr_info_struct info{0, 0, &iccData, &exifData};
+            jpegHdr.getJPEGRInfo(&jpegImgR, &info);
+        }
+    }
+}
+
+extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
+    JpegHDRFuzzer fuzzHandle(data, size);
+    fuzzHandle.process();
+    return 0;
+}
diff --git a/libs/ultrahdr/include/ultrahdr/jpegr.h b/libs/ultrahdr/include/ultrahdr/jpegr.h
index 00b66ae..1f9bd0f 100644
--- a/libs/ultrahdr/include/ultrahdr/jpegr.h
+++ b/libs/ultrahdr/include/ultrahdr/jpegr.h
@@ -17,6 +17,7 @@
 #ifndef ANDROID_ULTRAHDR_JPEGR_H
 #define ANDROID_ULTRAHDR_JPEGR_H
 
+#include "jpegencoderhelper.h"
 #include "jpegrerrorcode.h"
 #include "ultrahdr.h"
 
@@ -312,11 +313,11 @@
      * This method is called in the encoding pipeline. It will encode the gain map.
      *
      * @param uncompressed_gain_map uncompressed gain map
-     * @param dest encoded recover map
+     * @param resource to compress gain map
      * @return NO_ERROR if encoding succeeds, error code if error occurs.
      */
     status_t compressGainMap(jr_uncompressed_ptr uncompressed_gain_map,
-                             jr_compressed_ptr dest);
+                             JpegEncoderHelper* jpeg_encoder);
 
     /*
      * This methoud is called to separate primary image and gain map image from JPEGR
diff --git a/libs/ultrahdr/jpegr.cpp b/libs/ultrahdr/jpegr.cpp
index 5ebca39..c250aa0 100644
--- a/libs/ultrahdr/jpegr.cpp
+++ b/libs/ultrahdr/jpegr.cpp
@@ -244,11 +244,13 @@
   std::unique_ptr<uint8_t[]> map_data;
   map_data.reset(reinterpret_cast<uint8_t*>(map.data));
 
+  JpegEncoderHelper jpeg_encoder_gainmap;
+  JPEGR_CHECK(compressGainMap(&map, &jpeg_encoder_gainmap));
   jpegr_compressed_struct compressed_map;
-  compressed_map.maxLength = map.width * map.height;
-  unique_ptr<uint8_t[]> compressed_map_data = make_unique<uint8_t[]>(compressed_map.maxLength);
-  compressed_map.data = compressed_map_data.get();
-  JPEGR_CHECK(compressGainMap(&map, &compressed_map));
+  compressed_map.maxLength = jpeg_encoder_gainmap.getCompressedImageSize();
+  compressed_map.length = compressed_map.maxLength;
+  compressed_map.data = jpeg_encoder_gainmap.getCompressedImagePtr();
+  compressed_map.colorGamut = ULTRAHDR_COLORGAMUT_UNSPECIFIED;
 
   sp<DataStruct> icc = IccHelper::writeIccProfile(ULTRAHDR_TF_SRGB,
                                                   uncompressed_yuv_420_image.colorGamut);
@@ -301,11 +303,13 @@
   std::unique_ptr<uint8_t[]> map_data;
   map_data.reset(reinterpret_cast<uint8_t*>(map.data));
 
+  JpegEncoderHelper jpeg_encoder_gainmap;
+  JPEGR_CHECK(compressGainMap(&map, &jpeg_encoder_gainmap));
   jpegr_compressed_struct compressed_map;
-  compressed_map.maxLength = map.width * map.height;
-  unique_ptr<uint8_t[]> compressed_map_data = make_unique<uint8_t[]>(compressed_map.maxLength);
-  compressed_map.data = compressed_map_data.get();
-  JPEGR_CHECK(compressGainMap(&map, &compressed_map));
+  compressed_map.maxLength = jpeg_encoder_gainmap.getCompressedImageSize();
+  compressed_map.length = compressed_map.maxLength;
+  compressed_map.data = jpeg_encoder_gainmap.getCompressedImagePtr();
+  compressed_map.colorGamut = ULTRAHDR_COLORGAMUT_UNSPECIFIED;
 
   sp<DataStruct> icc = IccHelper::writeIccProfile(ULTRAHDR_TF_SRGB,
                                                   uncompressed_yuv_420_image->colorGamut);
@@ -356,11 +360,13 @@
   std::unique_ptr<uint8_t[]> map_data;
   map_data.reset(reinterpret_cast<uint8_t*>(map.data));
 
+  JpegEncoderHelper jpeg_encoder_gainmap;
+  JPEGR_CHECK(compressGainMap(&map, &jpeg_encoder_gainmap));
   jpegr_compressed_struct compressed_map;
-  compressed_map.maxLength = map.width * map.height;
-  unique_ptr<uint8_t[]> compressed_map_data = make_unique<uint8_t[]>(compressed_map.maxLength);
-  compressed_map.data = compressed_map_data.get();
-  JPEGR_CHECK(compressGainMap(&map, &compressed_map));
+  compressed_map.maxLength = jpeg_encoder_gainmap.getCompressedImageSize();
+  compressed_map.length = compressed_map.maxLength;
+  compressed_map.data = jpeg_encoder_gainmap.getCompressedImagePtr();
+  compressed_map.colorGamut = ULTRAHDR_COLORGAMUT_UNSPECIFIED;
 
   JPEGR_CHECK(appendGainMap(compressed_jpeg_image, &compressed_map, nullptr, &metadata, dest));
 
@@ -407,11 +413,13 @@
   std::unique_ptr<uint8_t[]> map_data;
   map_data.reset(reinterpret_cast<uint8_t*>(map.data));
 
+  JpegEncoderHelper jpeg_encoder_gainmap;
+  JPEGR_CHECK(compressGainMap(&map, &jpeg_encoder_gainmap));
   jpegr_compressed_struct compressed_map;
-  compressed_map.maxLength = map.width * map.height;
-  unique_ptr<uint8_t[]> compressed_map_data = make_unique<uint8_t[]>(compressed_map.maxLength);
-  compressed_map.data = compressed_map_data.get();
-  JPEGR_CHECK(compressGainMap(&map, &compressed_map));
+  compressed_map.maxLength = jpeg_encoder_gainmap.getCompressedImageSize();
+  compressed_map.length = compressed_map.maxLength;
+  compressed_map.data = jpeg_encoder_gainmap.getCompressedImagePtr();
+  compressed_map.colorGamut = ULTRAHDR_COLORGAMUT_UNSPECIFIED;
 
   JPEGR_CHECK(appendGainMap(compressed_jpeg_image, &compressed_map, nullptr, &metadata, dest));
 
@@ -604,30 +612,21 @@
 }
 
 status_t JpegR::compressGainMap(jr_uncompressed_ptr uncompressed_gain_map,
-                                jr_compressed_ptr dest) {
-  if (uncompressed_gain_map == nullptr || dest == nullptr) {
+                                JpegEncoderHelper* jpeg_encoder) {
+  if (uncompressed_gain_map == nullptr || jpeg_encoder == nullptr) {
     return ERROR_JPEGR_INVALID_NULL_PTR;
   }
 
-  JpegEncoderHelper jpeg_encoder;
-  if (!jpeg_encoder.compressImage(uncompressed_gain_map->data,
-                                  uncompressed_gain_map->width,
-                                  uncompressed_gain_map->height,
-                                  kMapCompressQuality,
-                                  nullptr,
-                                  0,
-                                  true /* isSingleChannel */)) {
+  if (!jpeg_encoder->compressImage(uncompressed_gain_map->data,
+                                   uncompressed_gain_map->width,
+                                   uncompressed_gain_map->height,
+                                   kMapCompressQuality,
+                                   nullptr,
+                                   0,
+                                   true /* isSingleChannel */)) {
     return ERROR_JPEGR_ENCODE_ERROR;
   }
 
-  if (dest->maxLength < jpeg_encoder.getCompressedImageSize()) {
-    return ERROR_JPEGR_BUFFER_TOO_SMALL;
-  }
-
-  memcpy(dest->data, jpeg_encoder.getCompressedImagePtr(), jpeg_encoder.getCompressedImageSize());
-  dest->length = jpeg_encoder.getCompressedImageSize();
-  dest->colorGamut = ULTRAHDR_COLORGAMUT_UNSPECIFIED;
-
   return NO_ERROR;
 }
 
diff --git a/libs/ultrahdr/tests/jpegr_test.cpp b/libs/ultrahdr/tests/jpegr_test.cpp
index 5f5c196..d482ea1 100644
--- a/libs/ultrahdr/tests/jpegr_test.cpp
+++ b/libs/ultrahdr/tests/jpegr_test.cpp
@@ -89,6 +89,51 @@
   return true;
 }
 
+static bool loadP010Image(const char *filename, jr_uncompressed_ptr img,
+                          bool isUVContiguous) {
+  int fd = open(filename, O_CLOEXEC);
+  if (fd < 0) {
+    return false;
+  }
+  const int bpp = 2;
+  int lumaStride = img->luma_stride == 0 ? img->width : img->luma_stride;
+  int lumaSize = bpp * lumaStride * img->height;
+  int chromaSize = bpp * (img->height / 2) *
+                   (isUVContiguous ? lumaStride : img->chroma_stride);
+  img->data = malloc(lumaSize + (isUVContiguous ? chromaSize : 0));
+  if (img->data == nullptr) {
+    ALOGE("loadP010Image(): failed to allocate memory for luma data.");
+    return false;
+  }
+  uint8_t *mem = static_cast<uint8_t *>(img->data);
+  for (int i = 0; i < img->height; i++) {
+    if (read(fd, mem, img->width * bpp) != img->width * bpp) {
+      close(fd);
+      return false;
+    }
+    mem += lumaStride * bpp;
+  }
+  int chromaStride = lumaStride;
+  if (!isUVContiguous) {
+    img->chroma_data = malloc(chromaSize);
+    if (img->chroma_data == nullptr) {
+      ALOGE("loadP010Image(): failed to allocate memory for chroma data.");
+      return false;
+    }
+    mem = static_cast<uint8_t *>(img->chroma_data);
+    chromaStride = img->chroma_stride;
+  }
+  for (int i = 0; i < img->height / 2; i++) {
+    if (read(fd, mem, img->width * bpp) != img->width * bpp) {
+      close(fd);
+      return false;
+    }
+    mem += chromaStride * bpp;
+  }
+  close(fd);
+  return true;
+}
+
 class JpegRTest : public testing::Test {
 public:
   JpegRTest();
@@ -98,10 +143,11 @@
   virtual void SetUp();
   virtual void TearDown();
 
-  struct jpegr_uncompressed_struct mRawP010Image;
-  struct jpegr_uncompressed_struct mRawP010ImageWithStride;
-  struct jpegr_uncompressed_struct mRawYuv420Image;
-  struct jpegr_compressed_struct mJpegImage;
+  struct jpegr_uncompressed_struct mRawP010Image{};
+  struct jpegr_uncompressed_struct mRawP010ImageWithStride{};
+  struct jpegr_uncompressed_struct mRawP010ImageWithChromaData{};
+  struct jpegr_uncompressed_struct mRawYuv420Image{};
+  struct jpegr_compressed_struct mJpegImage{};
 };
 
 JpegRTest::JpegRTest() {}
@@ -110,7 +156,11 @@
 void JpegRTest::SetUp() {}
 void JpegRTest::TearDown() {
   free(mRawP010Image.data);
+  free(mRawP010Image.chroma_data);
   free(mRawP010ImageWithStride.data);
+  free(mRawP010ImageWithStride.chroma_data);
+  free(mRawP010ImageWithChromaData.data);
+  free(mRawP010ImageWithChromaData.chroma_data);
   free(mRawYuv420Image.data);
   free(mJpegImage.data);
 }
@@ -286,6 +336,8 @@
       &mRawP010ImageWithStride, ultrahdr_transfer_function::ULTRAHDR_TF_HLG, &jpegR,
       DEFAULT_JPEG_QUALITY, nullptr)) << "fail, API allows bad chroma stride";
 
+  mRawP010ImageWithStride.chroma_data = nullptr;
+
   free(jpegR.data);
 }
 
@@ -734,6 +786,7 @@
   EXPECT_NE(OK, jpegRCodec.encodeJPEGR(
       &mRawP010ImageWithStride, &jpegR, ultrahdr_transfer_function::ULTRAHDR_TF_HLG,
       &jpegR)) << "fail, API allows bad chroma stride";
+  mRawP010ImageWithStride.chroma_data = nullptr;
 
   // bad compressed image
   EXPECT_NE(OK, jpegRCodec.encodeJPEGR(
@@ -831,6 +884,81 @@
   EXPECT_FLOAT_EQ(metadata_expected.minContentBoost, metadata_read.minContentBoost);
 }
 
+/* Test Encode API-0 */
+TEST_F(JpegRTest, encodeFromP010) {
+  int ret;
+
+  mRawP010Image.width = TEST_IMAGE_WIDTH;
+  mRawP010Image.height = TEST_IMAGE_HEIGHT;
+  mRawP010Image.colorGamut = ultrahdr_color_gamut::ULTRAHDR_COLORGAMUT_BT2100;
+  // Load input files.
+  if (!loadP010Image(RAW_P010_IMAGE, &mRawP010Image, true)) {
+    FAIL() << "Load file " << RAW_P010_IMAGE << " failed";
+  }
+
+  JpegR jpegRCodec;
+
+  jpegr_compressed_struct jpegR;
+  jpegR.maxLength = TEST_IMAGE_WIDTH * TEST_IMAGE_HEIGHT * sizeof(uint8_t);
+  jpegR.data = malloc(jpegR.maxLength);
+  ret = jpegRCodec.encodeJPEGR(
+      &mRawP010Image, ultrahdr_transfer_function::ULTRAHDR_TF_HLG, &jpegR, DEFAULT_JPEG_QUALITY,
+      nullptr);
+  if (ret != OK) {
+    FAIL() << "Error code is " << ret;
+  }
+
+  mRawP010ImageWithStride.width = TEST_IMAGE_WIDTH;
+  mRawP010ImageWithStride.height = TEST_IMAGE_HEIGHT;
+  mRawP010ImageWithStride.luma_stride = TEST_IMAGE_WIDTH + 128;
+  mRawP010ImageWithStride.colorGamut = ultrahdr_color_gamut::ULTRAHDR_COLORGAMUT_BT2100;
+  // Load input files.
+  if (!loadP010Image(RAW_P010_IMAGE, &mRawP010ImageWithStride, true)) {
+    FAIL() << "Load file " << RAW_P010_IMAGE << " failed";
+  }
+
+  jpegr_compressed_struct jpegRWithStride;
+  jpegRWithStride.maxLength = jpegR.length;
+  jpegRWithStride.data = malloc(jpegRWithStride.maxLength);
+  ret = jpegRCodec.encodeJPEGR(
+      &mRawP010ImageWithStride, ultrahdr_transfer_function::ULTRAHDR_TF_HLG, &jpegRWithStride,
+      DEFAULT_JPEG_QUALITY, nullptr);
+  if (ret != OK) {
+    FAIL() << "Error code is " << ret;
+  }
+  ASSERT_EQ(jpegR.length, jpegRWithStride.length)
+      << "Same input is yielding different output";
+  ASSERT_EQ(0, memcmp(jpegR.data, jpegRWithStride.data, jpegR.length))
+      << "Same input is yielding different output";
+
+  mRawP010ImageWithChromaData.width = TEST_IMAGE_WIDTH;
+  mRawP010ImageWithChromaData.height = TEST_IMAGE_HEIGHT;
+  mRawP010ImageWithChromaData.luma_stride = TEST_IMAGE_WIDTH + 64;
+  mRawP010ImageWithChromaData.chroma_stride = TEST_IMAGE_WIDTH + 256;
+  mRawP010ImageWithChromaData.colorGamut = ultrahdr_color_gamut::ULTRAHDR_COLORGAMUT_BT2100;
+  // Load input files.
+  if (!loadP010Image(RAW_P010_IMAGE, &mRawP010ImageWithChromaData, false)) {
+    FAIL() << "Load file " << RAW_P010_IMAGE << " failed";
+  }
+  jpegr_compressed_struct jpegRWithChromaData;
+  jpegRWithChromaData.maxLength = jpegR.length;
+  jpegRWithChromaData.data = malloc(jpegRWithChromaData.maxLength);
+  ret = jpegRCodec.encodeJPEGR(
+      &mRawP010ImageWithChromaData, ultrahdr_transfer_function::ULTRAHDR_TF_HLG,
+      &jpegRWithChromaData, DEFAULT_JPEG_QUALITY, nullptr);
+  if (ret != OK) {
+    FAIL() << "Error code is " << ret;
+  }
+  ASSERT_EQ(jpegR.length, jpegRWithChromaData.length)
+      << "Same input is yielding different output";
+  ASSERT_EQ(0, memcmp(jpegR.data, jpegRWithChromaData.data, jpegR.length))
+      << "Same input is yielding different output";
+
+  free(jpegR.data);
+  free(jpegRWithStride.data);
+  free(jpegRWithChromaData.data);
+}
+
 /* Test Encode API-0 and decode */
 TEST_F(JpegRTest, encodeFromP010ThenDecode) {
   int ret;
diff --git a/opengl/libs/EGL/Loader.cpp b/opengl/libs/EGL/Loader.cpp
index 6cfb608..2c3ce16 100644
--- a/opengl/libs/EGL/Loader.cpp
+++ b/opengl/libs/EGL/Loader.cpp
@@ -21,6 +21,7 @@
 
 #include <android-base/properties.h>
 #include <android/dlext.h>
+#include <cutils/properties.h>
 #include <dirent.h>
 #include <dlfcn.h>
 #include <graphicsenv/GraphicsEnv.h>
@@ -261,7 +262,10 @@
         hnd = attempt_to_load_system_driver(cnx, nullptr, true);
     }
 
-    if (!hnd && !failToLoadFromDriverSuffixProperty) {
+    if (!hnd && !failToLoadFromDriverSuffixProperty &&
+        property_get_int32("ro.vendor.api_level", 0) < __ANDROID_API_U__) {
+        // Still can't find the graphics drivers with the exact name. This time try to use wildcard
+        // matching if the device is launched before Android 14.
         hnd = attempt_to_load_system_driver(cnx, nullptr, false);
     }
 
diff --git a/services/inputflinger/BlockingQueue.h b/services/inputflinger/BlockingQueue.h
index fe37287..5693848 100644
--- a/services/inputflinger/BlockingQueue.h
+++ b/services/inputflinger/BlockingQueue.h
@@ -16,15 +16,17 @@
 
 #pragma once
 
-#include "android-base/thread_annotations.h"
 #include <condition_variable>
+#include <list>
 #include <mutex>
-#include <vector>
+#include <optional>
+#include "android-base/thread_annotations.h"
 
 namespace android {
 
 /**
- * A FIFO queue that stores up to <i>capacity</i> objects.
+ * A thread-safe FIFO queue. This list-backed queue stores up to <i>capacity</i> objects if
+ * a capacity is provided at construction, and is otherwise unbounded.
  * Objects can always be added. Objects are added immediately.
  * If the queue is full, new objects cannot be added.
  *
@@ -33,13 +35,13 @@
 template <class T>
 class BlockingQueue {
 public:
-    BlockingQueue(size_t capacity) : mCapacity(capacity) {
-        mQueue.reserve(mCapacity);
-    };
+    explicit BlockingQueue() = default;
+
+    explicit BlockingQueue(size_t capacity) : mCapacity(capacity){};
 
     /**
      * Retrieve and remove the oldest object.
-     * Blocks execution while queue is empty.
+     * Blocks execution indefinitely while queue is empty.
      */
     T pop() {
         std::unique_lock lock(mLock);
@@ -51,26 +53,62 @@
     };
 
     /**
+     * Retrieve and remove the oldest object.
+     * Blocks execution for the given duration while queue is empty, and returns std::nullopt
+     * if the queue was empty for the entire duration.
+     */
+    std::optional<T> popWithTimeout(std::chrono::nanoseconds duration) {
+        std::unique_lock lock(mLock);
+        android::base::ScopedLockAssertion assumeLock(mLock);
+        if (!mHasElements.wait_for(lock, duration,
+                                   [this]() REQUIRES(mLock) { return !this->mQueue.empty(); })) {
+            return {};
+        }
+        T t = std::move(mQueue.front());
+        mQueue.erase(mQueue.begin());
+        return t;
+    };
+
+    /**
      * Add a new object to the queue.
      * Does not block.
      * Return true if an element was successfully added.
      * Return false if the queue is full.
      */
     bool push(T&& t) {
-        {
+        { // acquire lock
             std::scoped_lock lock(mLock);
-            if (mQueue.size() == mCapacity) {
+            if (mCapacity && mQueue.size() == mCapacity) {
                 return false;
             }
             mQueue.push_back(std::move(t));
-        }
+        } // release lock
         mHasElements.notify_one();
         return true;
     };
 
-    void erase(const std::function<bool(const T&)>& lambda) {
+    /**
+     * Construct a new object into the queue.
+     * Does not block.
+     * Return true if an element was successfully added.
+     * Return false if the queue is full.
+     */
+    template <class... Args>
+    bool emplace(Args&&... args) {
+        { // acquire lock
+            std::scoped_lock lock(mLock);
+            if (mCapacity && mQueue.size() == mCapacity) {
+                return false;
+            }
+            mQueue.emplace_back(args...);
+        } // release lock
+        mHasElements.notify_one();
+        return true;
+    };
+
+    void erase_if(const std::function<bool(const T&)>& pred) {
         std::scoped_lock lock(mLock);
-        std::erase_if(mQueue, [&lambda](const auto& t) { return lambda(t); });
+        std::erase_if(mQueue, pred);
     }
 
     /**
@@ -93,7 +131,7 @@
     }
 
 private:
-    const size_t mCapacity;
+    const std::optional<size_t> mCapacity;
     /**
      * Used to signal that mQueue is non-empty.
      */
@@ -102,7 +140,7 @@
      * Lock for accessing and waiting on elements.
      */
     std::mutex mLock;
-    std::vector<T> mQueue GUARDED_BY(mLock);
+    std::list<T> mQueue GUARDED_BY(mLock);
 };
 
 } // namespace android
diff --git a/services/inputflinger/InputCommonConverter.cpp b/services/inputflinger/InputCommonConverter.cpp
index 2437d0f..7812880 100644
--- a/services/inputflinger/InputCommonConverter.cpp
+++ b/services/inputflinger/InputCommonConverter.cpp
@@ -258,12 +258,12 @@
 static_assert(static_cast<common::Axis>(AMOTION_EVENT_AXIS_GENERIC_14) == common::Axis::GENERIC_14);
 static_assert(static_cast<common::Axis>(AMOTION_EVENT_AXIS_GENERIC_15) == common::Axis::GENERIC_15);
 static_assert(static_cast<common::Axis>(AMOTION_EVENT_AXIS_GENERIC_16) == common::Axis::GENERIC_16);
-// TODO(b/251196347): add GESTURE_{X,Y}_OFFSET, GESTURE_SCROLL_{X,Y}_DISTANCE, and
-// GESTURE_PINCH_SCALE_FACTOR.
+// TODO(b/251196347): add GESTURE_{X,Y}_OFFSET, GESTURE_SCROLL_{X,Y}_DISTANCE,
+// GESTURE_PINCH_SCALE_FACTOR, and GESTURE_SWIPE_FINGER_COUNT.
 // If you added a new axis, consider whether this should also be exposed as a HAL axis. Update the
 // static_assert below and add the new axis here, or leave a comment summarizing your decision.
 static_assert(static_cast<common::Axis>(AMOTION_EVENT_MAXIMUM_VALID_AXIS_VALUE) ==
-              static_cast<common::Axis>(AMOTION_EVENT_AXIS_GESTURE_PINCH_SCALE_FACTOR));
+              static_cast<common::Axis>(AMOTION_EVENT_AXIS_GESTURE_SWIPE_FINGER_COUNT));
 
 static common::VideoFrame getHalVideoFrame(const TouchVideoFrame& frame) {
     common::VideoFrame out;
diff --git a/services/inputflinger/InputDeviceMetricsCollector.cpp b/services/inputflinger/InputDeviceMetricsCollector.cpp
index c6089c8..3e25cc3 100644
--- a/services/inputflinger/InputDeviceMetricsCollector.cpp
+++ b/services/inputflinger/InputDeviceMetricsCollector.cpp
@@ -17,52 +17,362 @@
 #define LOG_TAG "InputDeviceMetricsCollector"
 #include "InputDeviceMetricsCollector.h"
 
+#include "KeyCodeClassifications.h"
+
+#include <android-base/stringprintf.h>
+#include <input/PrintTools.h>
+#include <linux/input.h>
+
 namespace android {
 
+using android::base::StringPrintf;
+using std::chrono::nanoseconds;
+
+namespace {
+
+constexpr nanoseconds DEFAULT_USAGE_SESSION_TIMEOUT = std::chrono::seconds(5);
+
+/**
+ * Log debug messages about metrics events logged to statsd.
+ * Enable this via "adb shell setprop log.tag.InputDeviceMetricsCollector DEBUG" (requires restart)
+ */
+const bool DEBUG = __android_log_is_loggable(ANDROID_LOG_DEBUG, LOG_TAG, ANDROID_LOG_INFO);
+
+int32_t linuxBusToInputDeviceBusEnum(int32_t linuxBus) {
+    switch (linuxBus) {
+        case BUS_USB:
+            return util::INPUT_DEVICE_USAGE_REPORTED__DEVICE_BUS__USB;
+        case BUS_BLUETOOTH:
+            return util::INPUT_DEVICE_USAGE_REPORTED__DEVICE_BUS__BLUETOOTH;
+        default:
+            return util::INPUT_DEVICE_USAGE_REPORTED__DEVICE_BUS__OTHER;
+    }
+}
+
+class : public InputDeviceMetricsLogger {
+    nanoseconds getCurrentTime() override { return nanoseconds(systemTime(SYSTEM_TIME_MONOTONIC)); }
+
+    void logInputDeviceUsageReported(const InputDeviceIdentifier& identifier,
+                                     const DeviceUsageReport& report) override {
+        const int32_t durationMillis =
+                std::chrono::duration_cast<std::chrono::milliseconds>(report.usageDuration).count();
+        const static std::vector<int32_t> empty;
+
+        ALOGD_IF(DEBUG, "Usage session reported for device: %s", identifier.name.c_str());
+        ALOGD_IF(DEBUG, "    Total duration: %dms", durationMillis);
+        ALOGD_IF(DEBUG, "    Source breakdown:");
+
+        std::vector<int32_t> sources;
+        std::vector<int32_t> durationsPerSource;
+        for (auto& [src, dur] : report.sourceBreakdown) {
+            sources.push_back(ftl::to_underlying(src));
+            int32_t durMillis = std::chrono::duration_cast<std::chrono::milliseconds>(dur).count();
+            durationsPerSource.emplace_back(durMillis);
+            ALOGD_IF(DEBUG, "        - usageSource: %s\t duration: %dms",
+                     ftl::enum_string(src).c_str(), durMillis);
+        }
+
+        util::stats_write(util::INPUTDEVICE_USAGE_REPORTED, identifier.vendor, identifier.product,
+                          identifier.version, linuxBusToInputDeviceBusEnum(identifier.bus),
+                          durationMillis, sources, durationsPerSource, /*uids=*/empty,
+                          /*usage_durations_per_uid=*/empty);
+    }
+} sStatsdLogger;
+
+bool isIgnoredInputDeviceId(int32_t deviceId) {
+    switch (deviceId) {
+        case INVALID_INPUT_DEVICE_ID:
+        case VIRTUAL_KEYBOARD_ID:
+            return true;
+        default:
+            return false;
+    }
+}
+
+} // namespace
+
+InputDeviceUsageSource getUsageSourceForKeyArgs(const InputDeviceInfo& info,
+                                                const NotifyKeyArgs& keyArgs) {
+    if (!isFromSource(keyArgs.source, AINPUT_SOURCE_KEYBOARD)) {
+        return InputDeviceUsageSource::UNKNOWN;
+    }
+
+    if (isFromSource(keyArgs.source, AINPUT_SOURCE_DPAD) &&
+        DPAD_ALL_KEYCODES.count(keyArgs.keyCode) != 0) {
+        return InputDeviceUsageSource::DPAD;
+    }
+
+    if (isFromSource(keyArgs.source, AINPUT_SOURCE_GAMEPAD) &&
+        GAMEPAD_KEYCODES.count(keyArgs.keyCode) != 0) {
+        return InputDeviceUsageSource::GAMEPAD;
+    }
+
+    if (info.getKeyboardType() == AINPUT_KEYBOARD_TYPE_ALPHABETIC) {
+        return InputDeviceUsageSource::KEYBOARD;
+    }
+
+    return InputDeviceUsageSource::BUTTONS;
+}
+
+std::set<InputDeviceUsageSource> getUsageSourcesForMotionArgs(const NotifyMotionArgs& motionArgs) {
+    LOG_ALWAYS_FATAL_IF(motionArgs.pointerCount < 1, "Received motion args without pointers");
+    std::set<InputDeviceUsageSource> sources;
+
+    for (uint32_t i = 0; i < motionArgs.pointerCount; i++) {
+        const auto toolType = motionArgs.pointerProperties[i].toolType;
+        if (isFromSource(motionArgs.source, AINPUT_SOURCE_MOUSE)) {
+            if (toolType == ToolType::MOUSE) {
+                sources.emplace(InputDeviceUsageSource::MOUSE);
+                continue;
+            }
+            if (toolType == ToolType::FINGER) {
+                sources.emplace(InputDeviceUsageSource::TOUCHPAD);
+                continue;
+            }
+            if (isStylusToolType(toolType)) {
+                sources.emplace(InputDeviceUsageSource::STYLUS_INDIRECT);
+                continue;
+            }
+        }
+        if (isFromSource(motionArgs.source, AINPUT_SOURCE_MOUSE_RELATIVE) &&
+            toolType == ToolType::MOUSE) {
+            sources.emplace(InputDeviceUsageSource::MOUSE_CAPTURED);
+            continue;
+        }
+        if (isFromSource(motionArgs.source, AINPUT_SOURCE_TOUCHPAD) &&
+            toolType == ToolType::FINGER) {
+            sources.emplace(InputDeviceUsageSource::TOUCHPAD_CAPTURED);
+            continue;
+        }
+        if (isFromSource(motionArgs.source, AINPUT_SOURCE_BLUETOOTH_STYLUS) &&
+            isStylusToolType(toolType)) {
+            sources.emplace(InputDeviceUsageSource::STYLUS_FUSED);
+            continue;
+        }
+        if (isFromSource(motionArgs.source, AINPUT_SOURCE_STYLUS) && isStylusToolType(toolType)) {
+            sources.emplace(InputDeviceUsageSource::STYLUS_DIRECT);
+            continue;
+        }
+        if (isFromSource(motionArgs.source, AINPUT_SOURCE_TOUCH_NAVIGATION)) {
+            sources.emplace(InputDeviceUsageSource::TOUCH_NAVIGATION);
+            continue;
+        }
+        if (isFromSource(motionArgs.source, AINPUT_SOURCE_JOYSTICK)) {
+            sources.emplace(InputDeviceUsageSource::JOYSTICK);
+            continue;
+        }
+        if (isFromSource(motionArgs.source, AINPUT_SOURCE_ROTARY_ENCODER)) {
+            sources.emplace(InputDeviceUsageSource::ROTARY_ENCODER);
+            continue;
+        }
+        if (isFromSource(motionArgs.source, AINPUT_SOURCE_TRACKBALL)) {
+            sources.emplace(InputDeviceUsageSource::TRACKBALL);
+            continue;
+        }
+        if (isFromSource(motionArgs.source, AINPUT_SOURCE_TOUCHSCREEN)) {
+            sources.emplace(InputDeviceUsageSource::TOUCHSCREEN);
+            continue;
+        }
+        sources.emplace(InputDeviceUsageSource::UNKNOWN);
+    }
+
+    return sources;
+}
+
+// --- InputDeviceMetricsCollector ---
+
 InputDeviceMetricsCollector::InputDeviceMetricsCollector(InputListenerInterface& listener)
-      : mNextListener(listener){};
+      : InputDeviceMetricsCollector(listener, sStatsdLogger, DEFAULT_USAGE_SESSION_TIMEOUT) {}
+
+InputDeviceMetricsCollector::InputDeviceMetricsCollector(InputListenerInterface& listener,
+                                                         InputDeviceMetricsLogger& logger,
+                                                         nanoseconds usageSessionTimeout)
+      : mNextListener(listener), mLogger(logger), mUsageSessionTimeout(usageSessionTimeout) {}
 
 void InputDeviceMetricsCollector::notifyInputDevicesChanged(
         const NotifyInputDevicesChangedArgs& args) {
+    reportCompletedSessions();
+    onInputDevicesChanged(args.inputDeviceInfos);
     mNextListener.notify(args);
 }
 
 void InputDeviceMetricsCollector::notifyConfigurationChanged(
         const NotifyConfigurationChangedArgs& args) {
+    reportCompletedSessions();
     mNextListener.notify(args);
 }
 
 void InputDeviceMetricsCollector::notifyKey(const NotifyKeyArgs& args) {
+    reportCompletedSessions();
+    const SourceProvider getSources = [&args](const InputDeviceInfo& info) {
+        return std::set{getUsageSourceForKeyArgs(info, args)};
+    };
+    onInputDeviceUsage(DeviceId{args.deviceId}, nanoseconds(args.eventTime), getSources);
+
     mNextListener.notify(args);
 }
 
 void InputDeviceMetricsCollector::notifyMotion(const NotifyMotionArgs& args) {
+    reportCompletedSessions();
+    onInputDeviceUsage(DeviceId{args.deviceId}, nanoseconds(args.eventTime),
+                       [&args](const auto&) { return getUsageSourcesForMotionArgs(args); });
+
     mNextListener.notify(args);
 }
 
 void InputDeviceMetricsCollector::notifySwitch(const NotifySwitchArgs& args) {
+    reportCompletedSessions();
     mNextListener.notify(args);
 }
 
 void InputDeviceMetricsCollector::notifySensor(const NotifySensorArgs& args) {
+    reportCompletedSessions();
     mNextListener.notify(args);
 }
 
 void InputDeviceMetricsCollector::notifyVibratorState(const NotifyVibratorStateArgs& args) {
+    reportCompletedSessions();
     mNextListener.notify(args);
 }
 
 void InputDeviceMetricsCollector::notifyDeviceReset(const NotifyDeviceResetArgs& args) {
+    reportCompletedSessions();
     mNextListener.notify(args);
 }
 
 void InputDeviceMetricsCollector::notifyPointerCaptureChanged(
         const NotifyPointerCaptureChangedArgs& args) {
+    reportCompletedSessions();
     mNextListener.notify(args);
 }
 
 void InputDeviceMetricsCollector::dump(std::string& dump) {
     dump += "InputDeviceMetricsCollector:\n";
+
+    dump += "  Logged device IDs: " + dumpMapKeys(mLoggedDeviceInfos, &toString) + "\n";
+    dump += "  Devices with active usage sessions: " +
+            dumpMapKeys(mActiveUsageSessions, &toString) + "\n";
+}
+
+void InputDeviceMetricsCollector::onInputDevicesChanged(const std::vector<InputDeviceInfo>& infos) {
+    std::map<DeviceId, InputDeviceInfo> newDeviceInfos;
+
+    for (const InputDeviceInfo& info : infos) {
+        if (isIgnoredInputDeviceId(info.getId())) {
+            continue;
+        }
+        newDeviceInfos.emplace(info.getId(), info);
+    }
+
+    for (auto [deviceId, info] : mLoggedDeviceInfos) {
+        if (newDeviceInfos.count(deviceId) != 0) {
+            continue;
+        }
+        onInputDeviceRemoved(deviceId, info.getIdentifier());
+    }
+
+    std::swap(newDeviceInfos, mLoggedDeviceInfos);
+}
+
+void InputDeviceMetricsCollector::onInputDeviceRemoved(DeviceId deviceId,
+                                                       const InputDeviceIdentifier& identifier) {
+    auto it = mActiveUsageSessions.find(deviceId);
+    if (it == mActiveUsageSessions.end()) {
+        return;
+    }
+    // Report usage for that device if there is an active session.
+    auto& [_, activeSession] = *it;
+    mLogger.logInputDeviceUsageReported(identifier, activeSession.finishSession());
+    mActiveUsageSessions.erase(it);
+
+    // We don't remove this from mLoggedDeviceInfos because it will be updated in
+    // onInputDevicesChanged().
+}
+
+void InputDeviceMetricsCollector::onInputDeviceUsage(DeviceId deviceId, nanoseconds eventTime,
+                                                     const SourceProvider& getSources) {
+    auto infoIt = mLoggedDeviceInfos.find(deviceId);
+    if (infoIt == mLoggedDeviceInfos.end()) {
+        // Do not track usage for devices that are not logged.
+        return;
+    }
+
+    auto [sessionIt, _] =
+            mActiveUsageSessions.try_emplace(deviceId, mUsageSessionTimeout, eventTime);
+    for (InputDeviceUsageSource source : getSources(infoIt->second)) {
+        sessionIt->second.recordUsage(eventTime, source);
+    }
+}
+
+void InputDeviceMetricsCollector::reportCompletedSessions() {
+    const auto currentTime = mLogger.getCurrentTime();
+
+    std::vector<DeviceId> completedUsageSessions;
+
+    for (auto& [deviceId, activeSession] : mActiveUsageSessions) {
+        if (activeSession.checkIfCompletedAt(currentTime)) {
+            completedUsageSessions.emplace_back(deviceId);
+        }
+    }
+
+    for (DeviceId deviceId : completedUsageSessions) {
+        const auto infoIt = mLoggedDeviceInfos.find(deviceId);
+        LOG_ALWAYS_FATAL_IF(infoIt == mLoggedDeviceInfos.end());
+
+        auto activeSessionIt = mActiveUsageSessions.find(deviceId);
+        LOG_ALWAYS_FATAL_IF(activeSessionIt == mActiveUsageSessions.end());
+        auto& [_, activeSession] = *activeSessionIt;
+        mLogger.logInputDeviceUsageReported(infoIt->second.getIdentifier(),
+                                            activeSession.finishSession());
+        mActiveUsageSessions.erase(activeSessionIt);
+    }
+}
+
+// --- InputDeviceMetricsCollector::ActiveSession ---
+
+InputDeviceMetricsCollector::ActiveSession::ActiveSession(nanoseconds usageSessionTimeout,
+                                                          nanoseconds startTime)
+      : mUsageSessionTimeout(usageSessionTimeout), mDeviceSession({startTime, startTime}) {}
+
+void InputDeviceMetricsCollector::ActiveSession::recordUsage(nanoseconds eventTime,
+                                                             InputDeviceUsageSource source) {
+    // We assume that event times for subsequent events are always monotonically increasing for each
+    // input device.
+    auto [activeSourceIt, inserted] =
+            mActiveSessionsBySource.try_emplace(source, eventTime, eventTime);
+    if (!inserted) {
+        activeSourceIt->second.end = eventTime;
+    }
+    mDeviceSession.end = eventTime;
+}
+
+bool InputDeviceMetricsCollector::ActiveSession::checkIfCompletedAt(nanoseconds timestamp) {
+    const auto sessionExpiryTime = timestamp - mUsageSessionTimeout;
+    std::vector<InputDeviceUsageSource> completedSourceSessionsForDevice;
+    for (auto& [source, session] : mActiveSessionsBySource) {
+        if (session.end <= sessionExpiryTime) {
+            completedSourceSessionsForDevice.emplace_back(source);
+        }
+    }
+    for (InputDeviceUsageSource source : completedSourceSessionsForDevice) {
+        auto it = mActiveSessionsBySource.find(source);
+        const auto& [_, session] = *it;
+        mSourceUsageBreakdown.emplace_back(source, session.end - session.start);
+        mActiveSessionsBySource.erase(it);
+    }
+    return mActiveSessionsBySource.empty();
+}
+
+InputDeviceMetricsLogger::DeviceUsageReport
+InputDeviceMetricsCollector::ActiveSession::finishSession() {
+    const auto deviceUsageDuration = mDeviceSession.end - mDeviceSession.start;
+
+    for (const auto& [source, sourceSession] : mActiveSessionsBySource) {
+        mSourceUsageBreakdown.emplace_back(source, sourceSession.end - sourceSession.start);
+    }
+    mActiveSessionsBySource.clear();
+
+    return {deviceUsageDuration, mSourceUsageBreakdown};
 }
 
 } // namespace android
diff --git a/services/inputflinger/InputDeviceMetricsCollector.h b/services/inputflinger/InputDeviceMetricsCollector.h
index c959075..e2e79e4 100644
--- a/services/inputflinger/InputDeviceMetricsCollector.h
+++ b/services/inputflinger/InputDeviceMetricsCollector.h
@@ -17,6 +17,16 @@
 #pragma once
 
 #include "InputListener.h"
+#include "NotifyArgs.h"
+
+#include <ftl/mixins.h>
+#include <input/InputDevice.h>
+#include <statslog.h>
+#include <chrono>
+#include <functional>
+#include <map>
+#include <set>
+#include <vector>
 
 namespace android {
 
@@ -34,11 +44,73 @@
     virtual void dump(std::string& dump) = 0;
 };
 
+/**
+ * Enum representation of the InputDeviceUsageSource.
+ */
+enum class InputDeviceUsageSource : int32_t {
+    UNKNOWN = util::INPUT_DEVICE_USAGE_REPORTED__USAGE_SOURCES__UNKNOWN,
+    BUTTONS = util::INPUT_DEVICE_USAGE_REPORTED__USAGE_SOURCES__BUTTONS,
+    KEYBOARD = util::INPUT_DEVICE_USAGE_REPORTED__USAGE_SOURCES__KEYBOARD,
+    DPAD = util::INPUT_DEVICE_USAGE_REPORTED__USAGE_SOURCES__DPAD,
+    GAMEPAD = util::INPUT_DEVICE_USAGE_REPORTED__USAGE_SOURCES__GAMEPAD,
+    JOYSTICK = util::INPUT_DEVICE_USAGE_REPORTED__USAGE_SOURCES__JOYSTICK,
+    MOUSE = util::INPUT_DEVICE_USAGE_REPORTED__USAGE_SOURCES__MOUSE,
+    MOUSE_CAPTURED = util::INPUT_DEVICE_USAGE_REPORTED__USAGE_SOURCES__MOUSE_CAPTURED,
+    TOUCHPAD = util::INPUT_DEVICE_USAGE_REPORTED__USAGE_SOURCES__TOUCHPAD,
+    TOUCHPAD_CAPTURED = util::INPUT_DEVICE_USAGE_REPORTED__USAGE_SOURCES__TOUCHPAD_CAPTURED,
+    ROTARY_ENCODER = util::INPUT_DEVICE_USAGE_REPORTED__USAGE_SOURCES__ROTARY_ENCODER,
+    STYLUS_DIRECT = util::INPUT_DEVICE_USAGE_REPORTED__USAGE_SOURCES__STYLUS_DIRECT,
+    STYLUS_INDIRECT = util::INPUT_DEVICE_USAGE_REPORTED__USAGE_SOURCES__STYLUS_INDIRECT,
+    STYLUS_FUSED = util::INPUT_DEVICE_USAGE_REPORTED__USAGE_SOURCES__STYLUS_FUSED,
+    TOUCH_NAVIGATION = util::INPUT_DEVICE_USAGE_REPORTED__USAGE_SOURCES__TOUCH_NAVIGATION,
+    TOUCHSCREEN = util::INPUT_DEVICE_USAGE_REPORTED__USAGE_SOURCES__TOUCHSCREEN,
+    TRACKBALL = util::INPUT_DEVICE_USAGE_REPORTED__USAGE_SOURCES__TRACKBALL,
+
+    ftl_first = UNKNOWN,
+    ftl_last = TRACKBALL,
+};
+
+/** Returns the InputDeviceUsageSource that corresponds to the key event. */
+InputDeviceUsageSource getUsageSourceForKeyArgs(const InputDeviceInfo&, const NotifyKeyArgs&);
+
+/** Returns the InputDeviceUsageSources that correspond to the motion event. */
+std::set<InputDeviceUsageSource> getUsageSourcesForMotionArgs(const NotifyMotionArgs&);
+
+/** The logging interface for the metrics collector, injected for testing. */
+class InputDeviceMetricsLogger {
+public:
+    virtual std::chrono::nanoseconds getCurrentTime() = 0;
+
+    // Describes the breakdown of an input device usage session by its usage sources.
+    // An input device can have more than one usage source. For example, some game controllers have
+    // buttons, joysticks, and touchpads. We track usage by these sources to get a better picture of
+    // the device usage. The source breakdown of a 10 minute usage session could look like this:
+    //   { {GAMEPAD, <9 mins>}, {TOUCHPAD, <2 mins>}, {TOUCHPAD, <3 mins>} }
+    // This would indicate that the GAMEPAD source was used first, and that source usage session
+    // lasted for 9 mins. During that time, the TOUCHPAD was used for 2 mins, until its source
+    // usage session expired. The TOUCHPAD was then used again later for another 3 mins.
+    using SourceUsageBreakdown =
+            std::vector<std::pair<InputDeviceUsageSource, std::chrono::nanoseconds /*duration*/>>;
+
+    struct DeviceUsageReport {
+        std::chrono::nanoseconds usageDuration;
+        SourceUsageBreakdown sourceBreakdown;
+    };
+
+    virtual void logInputDeviceUsageReported(const InputDeviceIdentifier&,
+                                             const DeviceUsageReport&) = 0;
+    virtual ~InputDeviceMetricsLogger() = default;
+};
+
 class InputDeviceMetricsCollector : public InputDeviceMetricsCollectorInterface {
 public:
     explicit InputDeviceMetricsCollector(InputListenerInterface& listener);
     ~InputDeviceMetricsCollector() override = default;
 
+    // Test constructor
+    InputDeviceMetricsCollector(InputListenerInterface& listener, InputDeviceMetricsLogger& logger,
+                                std::chrono::nanoseconds usageSessionTimeout);
+
     void notifyInputDevicesChanged(const NotifyInputDevicesChangedArgs& args) override;
     void notifyConfigurationChanged(const NotifyConfigurationChangedArgs& args) override;
     void notifyKey(const NotifyKeyArgs& args) override;
@@ -53,6 +125,51 @@
 
 private:
     InputListenerInterface& mNextListener;
+    InputDeviceMetricsLogger& mLogger;
+    const std::chrono::nanoseconds mUsageSessionTimeout;
+
+    // Type-safe wrapper for input device id.
+    struct DeviceId : ftl::Constructible<DeviceId, std::int32_t>,
+                      ftl::Equatable<DeviceId>,
+                      ftl::Orderable<DeviceId> {
+        using Constructible::Constructible;
+    };
+    static inline std::string toString(const DeviceId& id) {
+        return std::to_string(ftl::to_underlying(id));
+    }
+
+    std::map<DeviceId, InputDeviceInfo> mLoggedDeviceInfos;
+
+    class ActiveSession {
+    public:
+        explicit ActiveSession(std::chrono::nanoseconds usageSessionTimeout,
+                               std::chrono::nanoseconds startTime);
+        void recordUsage(std::chrono::nanoseconds eventTime, InputDeviceUsageSource source);
+        bool checkIfCompletedAt(std::chrono::nanoseconds timestamp);
+        InputDeviceMetricsLogger::DeviceUsageReport finishSession();
+
+    private:
+        struct UsageSession {
+            std::chrono::nanoseconds start{};
+            std::chrono::nanoseconds end{};
+        };
+
+        const std::chrono::nanoseconds mUsageSessionTimeout;
+        UsageSession mDeviceSession{};
+
+        std::map<InputDeviceUsageSource, UsageSession> mActiveSessionsBySource{};
+        InputDeviceMetricsLogger::SourceUsageBreakdown mSourceUsageBreakdown{};
+    };
+
+    // The input devices that currently have active usage sessions.
+    std::map<DeviceId, ActiveSession> mActiveUsageSessions;
+
+    void onInputDevicesChanged(const std::vector<InputDeviceInfo>& infos);
+    void onInputDeviceRemoved(DeviceId deviceId, const InputDeviceIdentifier& identifier);
+    using SourceProvider = std::function<std::set<InputDeviceUsageSource>(const InputDeviceInfo&)>;
+    void onInputDeviceUsage(DeviceId deviceId, std::chrono::nanoseconds eventTime,
+                            const SourceProvider& getSources);
+    void reportCompletedSessions();
 };
 
 } // namespace android
diff --git a/services/inputflinger/InputProcessor.cpp b/services/inputflinger/InputProcessor.cpp
index 7a84be9..6dd267c 100644
--- a/services/inputflinger/InputProcessor.cpp
+++ b/services/inputflinger/InputProcessor.cpp
@@ -322,7 +322,7 @@
 void MotionClassifier::reset(const NotifyDeviceResetArgs& args) {
     int32_t deviceId = args.deviceId;
     // Clear the pending events right away, to avoid unnecessary work done by the HAL.
-    mEvents.erase([deviceId](const ClassifierEvent& event) {
+    mEvents.erase_if([deviceId](const ClassifierEvent& event) {
         std::optional<int32_t> eventDeviceId = event.getDeviceId();
         return eventDeviceId && (*eventDeviceId == deviceId);
     });
diff --git a/services/inputflinger/SyncQueue.h b/services/inputflinger/SyncQueue.h
new file mode 100644
index 0000000..62efd55
--- /dev/null
+++ b/services/inputflinger/SyncQueue.h
@@ -0,0 +1,53 @@
+/*
+ * 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 <utils/threads.h>
+#include <list>
+#include <mutex>
+#include <optional>
+
+namespace android {
+
+/** A thread-safe FIFO queue. */
+template <class T>
+class SyncQueue {
+public:
+    /** Retrieve and remove the oldest object. Returns std::nullopt if the queue is empty. */
+    std::optional<T> pop() {
+        std::scoped_lock lock(mLock);
+        if (mQueue.empty()) {
+            return {};
+        }
+        T t = std::move(mQueue.front());
+        mQueue.erase(mQueue.begin());
+        return t;
+    };
+
+    /** Add a new object to the queue. */
+    template <class... Args>
+    void push(Args&&... args) {
+        std::scoped_lock lock(mLock);
+        mQueue.emplace_back(args...);
+    };
+
+private:
+    std::mutex mLock;
+    std::list<T> mQueue GUARDED_BY(mLock);
+};
+
+} // namespace android
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/include/KeyCodeClassifications.h b/services/inputflinger/include/KeyCodeClassifications.h
new file mode 100644
index 0000000..a09b02e
--- /dev/null
+++ b/services/inputflinger/include/KeyCodeClassifications.h
@@ -0,0 +1,55 @@
+/*
+ * 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/input.h>
+#include <set>
+
+namespace android {
+
+/** The set of all Android key codes that are required for a device to be classified as a D-pad. */
+static const std::set<int32_t> DPAD_REQUIRED_KEYCODES = {
+        AKEYCODE_DPAD_UP,    AKEYCODE_DPAD_DOWN,   AKEYCODE_DPAD_LEFT,
+        AKEYCODE_DPAD_RIGHT, AKEYCODE_DPAD_CENTER,
+};
+
+/** The set of all Android key codes that correspond to D-pad keys. */
+static const std::set<int32_t> DPAD_ALL_KEYCODES = {
+        AKEYCODE_DPAD_UP,       AKEYCODE_DPAD_DOWN,      AKEYCODE_DPAD_LEFT,
+        AKEYCODE_DPAD_RIGHT,    AKEYCODE_DPAD_CENTER,    AKEYCODE_DPAD_UP_LEFT,
+        AKEYCODE_DPAD_UP_RIGHT, AKEYCODE_DPAD_DOWN_LEFT, AKEYCODE_DPAD_DOWN_RIGHT,
+};
+
+/** The set of all Android key codes that correspond to gamepad buttons. */
+static const std::set<int32_t> GAMEPAD_KEYCODES = {
+        AKEYCODE_BUTTON_A,      AKEYCODE_BUTTON_B,      AKEYCODE_BUTTON_C,    //
+        AKEYCODE_BUTTON_X,      AKEYCODE_BUTTON_Y,      AKEYCODE_BUTTON_Z,    //
+        AKEYCODE_BUTTON_L1,     AKEYCODE_BUTTON_R1,                           //
+        AKEYCODE_BUTTON_L2,     AKEYCODE_BUTTON_R2,                           //
+        AKEYCODE_BUTTON_THUMBL, AKEYCODE_BUTTON_THUMBR,                       //
+        AKEYCODE_BUTTON_START,  AKEYCODE_BUTTON_SELECT, AKEYCODE_BUTTON_MODE, //
+};
+
+/** The set of all Android key codes that correspond to buttons (bit-switches) on a stylus. */
+static const std::set<int32_t> STYLUS_BUTTON_KEYCODES = {
+        AKEYCODE_STYLUS_BUTTON_PRIMARY,
+        AKEYCODE_STYLUS_BUTTON_SECONDARY,
+        AKEYCODE_STYLUS_BUTTON_TERTIARY,
+        AKEYCODE_STYLUS_BUTTON_TAIL,
+};
+
+} // namespace android
diff --git a/services/inputflinger/reader/EventHub.cpp b/services/inputflinger/reader/EventHub.cpp
index 0354164..04747cc 100644
--- a/services/inputflinger/reader/EventHub.cpp
+++ b/services/inputflinger/reader/EventHub.cpp
@@ -58,6 +58,8 @@
 
 #include "EventHub.h"
 
+#include "KeyCodeClassifications.h"
+
 #define INDENT "  "
 #define INDENT2 "    "
 #define INDENT3 "      "
@@ -189,14 +191,6 @@
     return out;
 }
 
-/* The set of all Android key codes that correspond to buttons (bit-switches) on a stylus. */
-static constexpr std::array<int32_t, 4> STYLUS_BUTTON_KEYCODES = {
-        AKEYCODE_STYLUS_BUTTON_PRIMARY,
-        AKEYCODE_STYLUS_BUTTON_SECONDARY,
-        AKEYCODE_STYLUS_BUTTON_TERTIARY,
-        AKEYCODE_STYLUS_BUTTON_TAIL,
-};
-
 /**
  * Return true if name matches "v4l-touch*"
  */
@@ -2060,15 +2054,6 @@
 
 // ----------------------------------------------------------------------------
 
-static const int32_t GAMEPAD_KEYCODES[] = {
-        AKEYCODE_BUTTON_A,      AKEYCODE_BUTTON_B,      AKEYCODE_BUTTON_C,    //
-        AKEYCODE_BUTTON_X,      AKEYCODE_BUTTON_Y,      AKEYCODE_BUTTON_Z,    //
-        AKEYCODE_BUTTON_L1,     AKEYCODE_BUTTON_R1,                           //
-        AKEYCODE_BUTTON_L2,     AKEYCODE_BUTTON_R2,                           //
-        AKEYCODE_BUTTON_THUMBL, AKEYCODE_BUTTON_THUMBR,                       //
-        AKEYCODE_BUTTON_START,  AKEYCODE_BUTTON_SELECT, AKEYCODE_BUTTON_MODE, //
-};
-
 status_t EventHub::registerFdForEpoll(int fd) {
     // TODO(b/121395353) - consider adding EPOLLRDHUP
     struct epoll_event eventItem = {};
@@ -2391,31 +2376,23 @@
             device->classes |= InputDeviceClass::ALPHAKEY;
         }
 
-        // See if this device has a DPAD.
-        if (device->hasKeycodeLocked(AKEYCODE_DPAD_UP) &&
-            device->hasKeycodeLocked(AKEYCODE_DPAD_DOWN) &&
-            device->hasKeycodeLocked(AKEYCODE_DPAD_LEFT) &&
-            device->hasKeycodeLocked(AKEYCODE_DPAD_RIGHT) &&
-            device->hasKeycodeLocked(AKEYCODE_DPAD_CENTER)) {
+        // See if this device has a D-pad.
+        if (std::all_of(DPAD_REQUIRED_KEYCODES.begin(), DPAD_REQUIRED_KEYCODES.end(),
+                        [&](int32_t keycode) { return device->hasKeycodeLocked(keycode); })) {
             device->classes |= InputDeviceClass::DPAD;
         }
 
         // See if this device has a gamepad.
-        for (size_t i = 0; i < sizeof(GAMEPAD_KEYCODES) / sizeof(GAMEPAD_KEYCODES[0]); i++) {
-            if (device->hasKeycodeLocked(GAMEPAD_KEYCODES[i])) {
-                device->classes |= InputDeviceClass::GAMEPAD;
-                break;
-            }
+        if (std::any_of(GAMEPAD_KEYCODES.begin(), GAMEPAD_KEYCODES.end(),
+                        [&](int32_t keycode) { return device->hasKeycodeLocked(keycode); })) {
+            device->classes |= InputDeviceClass::GAMEPAD;
         }
 
         // See if this device has any stylus buttons that we would want to fuse with touch data.
-        if (!device->classes.any(InputDeviceClass::TOUCH | InputDeviceClass::TOUCH_MT)) {
-            for (int32_t keycode : STYLUS_BUTTON_KEYCODES) {
-                if (device->hasKeycodeLocked(keycode)) {
-                    device->classes |= InputDeviceClass::EXTERNAL_STYLUS;
-                    break;
-                }
-            }
+        if (!device->classes.any(InputDeviceClass::TOUCH | InputDeviceClass::TOUCH_MT) &&
+            std::any_of(STYLUS_BUTTON_KEYCODES.begin(), STYLUS_BUTTON_KEYCODES.end(),
+                        [&](int32_t keycode) { return device->hasKeycodeLocked(keycode); })) {
+            device->classes |= InputDeviceClass::EXTERNAL_STYLUS;
         }
     }
 
diff --git a/services/inputflinger/reader/controller/PeripheralController.cpp b/services/inputflinger/reader/controller/PeripheralController.cpp
index a380b5e..eabf591 100644
--- a/services/inputflinger/reader/controller/PeripheralController.cpp
+++ b/services/inputflinger/reader/controller/PeripheralController.cpp
@@ -16,8 +16,10 @@
 
 #include <locale>
 #include <regex>
-#include <set>
+#include <sstream>
+#include <string>
 
+#include <android/sysprop/InputProperties.sysprop.h>
 #include <ftl/enum.h>
 
 #include "../Macros.h"
@@ -45,6 +47,10 @@
     return (brightness & 0xff) << 24 | (red & 0xff) << 16 | (green & 0xff) << 8 | (blue & 0xff);
 }
 
+static inline bool isKeyboardBacklightCustomLevelsEnabled() {
+    return sysprop::InputProperties::enable_keyboard_backlight_custom_levels().value_or(true);
+}
+
 /**
  * Input controller owned by InputReader device, implements the native API for querying input
  * lights, getting and setting the lights brightness and color, by interacting with EventHub
@@ -272,11 +278,43 @@
     for (const auto& [lightId, light] : mLights) {
         // Input device light doesn't support ordinal, always pass 1.
         InputDeviceLightInfo lightInfo(light->name, light->id, light->type, light->capabilityFlags,
-                                       /*ordinal=*/1);
+                                       /*ordinal=*/1, getPreferredBrightnessLevels(light.get()));
         deviceInfo->addLightInfo(lightInfo);
     }
 }
 
+// TODO(b/281822656): Move to constructor and add as a parameter to avoid parsing repeatedly.
+// Need to change lifecycle of Peripheral controller so that Input device configuration map is
+// available at construction time before moving this logic to constructor.
+std::set<BrightnessLevel> PeripheralController::getPreferredBrightnessLevels(
+        const Light* light) const {
+    std::set<BrightnessLevel> levels;
+    if (!isKeyboardBacklightCustomLevelsEnabled() ||
+        light->type != InputDeviceLightType::KEYBOARD_BACKLIGHT) {
+        return levels;
+    }
+    std::optional<std::string> keyboardBacklightLevels =
+            mDeviceContext.getConfiguration().getString("keyboard.backlight.brightnessLevels");
+    if (!keyboardBacklightLevels) {
+        return levels;
+    }
+    std::stringstream ss(*keyboardBacklightLevels);
+    while (ss.good()) {
+        std::string substr;
+        std::getline(ss, substr, ',');
+        char* end;
+        int32_t value = static_cast<int32_t>(strtol(substr.c_str(), &end, 10));
+        if (*end != '\0' || value < 0 || value > 255) {
+            ALOGE("Error parsing keyboard backlight brightness levels, provided levels = %s",
+                  keyboardBacklightLevels->c_str());
+            levels.clear();
+            break;
+        }
+        levels.insert(BrightnessLevel(value));
+    }
+    return levels;
+}
+
 void PeripheralController::dump(std::string& dump) {
     dump += INDENT2 "Input Controller:\n";
     if (!mLights.empty()) {
@@ -550,5 +588,4 @@
 int32_t PeripheralController::getEventHubId() const {
     return getDeviceContext().getEventHubId();
 }
-
 } // namespace android
diff --git a/services/inputflinger/reader/controller/PeripheralController.h b/services/inputflinger/reader/controller/PeripheralController.h
index 8ac42c3..07ade7c 100644
--- a/services/inputflinger/reader/controller/PeripheralController.h
+++ b/services/inputflinger/reader/controller/PeripheralController.h
@@ -76,6 +76,7 @@
 
         virtual void dump(std::string& dump) {}
 
+        void configureSuggestedBrightnessLevels();
         std::optional<std::int32_t> getRawLightBrightness(int32_t rawLightId);
         void setRawLightBrightness(int32_t rawLightId, int32_t brightness);
     };
@@ -152,6 +153,8 @@
 
     // Battery map from battery ID to battery
     std::unordered_map<int32_t, std::unique_ptr<Battery>> mBatteries;
+
+    std::set<BrightnessLevel> getPreferredBrightnessLevels(const Light* light) const;
 };
 
 } // namespace android
diff --git a/services/inputflinger/reader/include/InputDevice.h b/services/inputflinger/reader/include/InputDevice.h
index 0b8a608..2f8e5bd 100644
--- a/services/inputflinger/reader/include/InputDevice.h
+++ b/services/inputflinger/reader/include/InputDevice.h
@@ -74,7 +74,7 @@
     }
     inline bool hasMic() const { return mHasMic; }
 
-    inline bool isIgnored() { return !getMapperCount(); }
+    inline bool isIgnored() { return !getMapperCount() && !mController; }
 
     bool isEnabled();
     [[nodiscard]] std::list<NotifyArgs> setEnabled(bool enabled, nsecs_t when);
diff --git a/services/inputflinger/reader/mapper/gestures/GestureConverter.cpp b/services/inputflinger/reader/mapper/gestures/GestureConverter.cpp
index 7eca6fa..1088821 100644
--- a/services/inputflinger/reader/mapper/gestures/GestureConverter.cpp
+++ b/services/inputflinger/reader/mapper/gestures/GestureConverter.cpp
@@ -385,6 +385,8 @@
         }
 
         mDownTime = when;
+        mFakeFingerCoords[0].setAxisValue(AMOTION_EVENT_AXIS_GESTURE_SWIPE_FINGER_COUNT,
+                                          fingerCount);
         out.push_back(makeMotionArgs(when, readTime, AMOTION_EVENT_ACTION_DOWN,
                                      /* actionButton= */ 0, mButtonState, /* pointerCount= */ 1,
                                      mFingerProps.data(), mFakeFingerCoords.data(), xCursorPosition,
@@ -441,6 +443,7 @@
                                  /* actionButton= */ 0, mButtonState, /* pointerCount= */ 1,
                                  mFingerProps.data(), mFakeFingerCoords.data(), xCursorPosition,
                                  yCursorPosition));
+    mFakeFingerCoords[0].setAxisValue(AMOTION_EVENT_AXIS_GESTURE_SWIPE_FINGER_COUNT, 0);
     mCurrentClassification = MotionClassification::NONE;
     mSwipeFingerCount = 0;
     return out;
diff --git a/services/inputflinger/tests/Android.bp b/services/inputflinger/tests/Android.bp
index 52277ff..6438a14 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",
@@ -47,6 +48,7 @@
         "FocusResolver_test.cpp",
         "GestureConverter_test.cpp",
         "HardwareStateConverter_test.cpp",
+        "InputDeviceMetricsCollector_test.cpp",
         "InputMapperTest.cpp",
         "InputProcessor_test.cpp",
         "InputProcessorConverter_test.cpp",
@@ -57,7 +59,9 @@
         "NotifyArgs_test.cpp",
         "PreferStylusOverTouch_test.cpp",
         "PropertyProvider_test.cpp",
+        "SyncQueue_test.cpp",
         "TestInputListener.cpp",
+        "TouchpadInputMapper_test.cpp",
         "UinputDevice.cpp",
         "UnwantedInteractionBlocker_test.cpp",
     ],
diff --git a/services/inputflinger/tests/BlockingQueue_test.cpp b/services/inputflinger/tests/BlockingQueue_test.cpp
index fd9d9d5..754a5c4 100644
--- a/services/inputflinger/tests/BlockingQueue_test.cpp
+++ b/services/inputflinger/tests/BlockingQueue_test.cpp
@@ -22,6 +22,7 @@
 
 namespace android {
 
+using std::chrono_literals::operator""ns;
 
 // --- BlockingQueueTest ---
 
@@ -34,6 +35,14 @@
 
     ASSERT_TRUE(queue.push(1));
     ASSERT_EQ(queue.pop(), 1);
+
+    ASSERT_TRUE(queue.emplace(2));
+    ASSERT_EQ(queue.popWithTimeout(0ns), 2);
+
+    ASSERT_TRUE(queue.push(3));
+    ASSERT_EQ(queue.popWithTimeout(100ns), 3);
+
+    ASSERT_EQ(std::nullopt, queue.popWithTimeout(0ns));
 }
 
 /**
@@ -87,7 +96,7 @@
     queue.push(3);
     queue.push(4);
     // Erase elements 2 and 4
-    queue.erase([](int element) { return element == 2 || element == 4; });
+    queue.erase_if([](int element) { return element == 2 || element == 4; });
     // Should no longer receive elements 2 and 4
     ASSERT_EQ(1, queue.pop());
     ASSERT_EQ(3, queue.pop());
@@ -138,5 +147,9 @@
     ASSERT_TRUE(hasReceivedElement);
 }
 
+TEST(BlockingQueueTest, Queue_TimesOut) {
+    BlockingQueue<int> queue;
+    ASSERT_EQ(std::nullopt, queue.popWithTimeout(1ns));
+}
 
 } // namespace android
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/EventBuilders.h b/services/inputflinger/tests/EventBuilders.h
new file mode 100644
index 0000000..606a57d
--- /dev/null
+++ b/services/inputflinger/tests/EventBuilders.h
@@ -0,0 +1,362 @@
+/*
+ * 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 <NotifyArgs.h>
+#include <android/input.h>
+#include <attestation/HmacKeyManager.h>
+#include <input/Input.h>
+#include <vector>
+
+namespace android {
+
+// An arbitrary device id.
+static constexpr uint32_t DEFAULT_DEVICE_ID = 1;
+
+// The default policy flags to use for event injection by tests.
+static constexpr uint32_t DEFAULT_POLICY_FLAGS = POLICY_FLAG_FILTERED | POLICY_FLAG_PASS_TO_USER;
+
+class PointerBuilder {
+public:
+    PointerBuilder(int32_t id, ToolType toolType) {
+        mProperties.clear();
+        mProperties.id = id;
+        mProperties.toolType = toolType;
+        mCoords.clear();
+    }
+
+    PointerBuilder& x(float x) { return axis(AMOTION_EVENT_AXIS_X, x); }
+
+    PointerBuilder& y(float y) { return axis(AMOTION_EVENT_AXIS_Y, y); }
+
+    PointerBuilder& axis(int32_t axis, float value) {
+        mCoords.setAxisValue(axis, value);
+        return *this;
+    }
+
+    PointerProperties buildProperties() const { return mProperties; }
+
+    PointerCoords buildCoords() const { return mCoords; }
+
+private:
+    PointerProperties mProperties;
+    PointerCoords mCoords;
+};
+
+class MotionEventBuilder {
+public:
+    MotionEventBuilder(int32_t action, int32_t source) {
+        mAction = action;
+        mSource = source;
+        mEventTime = systemTime(SYSTEM_TIME_MONOTONIC);
+        mDownTime = mEventTime;
+    }
+
+    MotionEventBuilder& deviceId(int32_t deviceId) {
+        mDeviceId = deviceId;
+        return *this;
+    }
+
+    MotionEventBuilder& downTime(nsecs_t downTime) {
+        mDownTime = downTime;
+        return *this;
+    }
+
+    MotionEventBuilder& eventTime(nsecs_t eventTime) {
+        mEventTime = eventTime;
+        return *this;
+    }
+
+    MotionEventBuilder& displayId(int32_t displayId) {
+        mDisplayId = displayId;
+        return *this;
+    }
+
+    MotionEventBuilder& actionButton(int32_t actionButton) {
+        mActionButton = actionButton;
+        return *this;
+    }
+
+    MotionEventBuilder& buttonState(int32_t buttonState) {
+        mButtonState = buttonState;
+        return *this;
+    }
+
+    MotionEventBuilder& rawXCursorPosition(float rawXCursorPosition) {
+        mRawXCursorPosition = rawXCursorPosition;
+        return *this;
+    }
+
+    MotionEventBuilder& rawYCursorPosition(float rawYCursorPosition) {
+        mRawYCursorPosition = rawYCursorPosition;
+        return *this;
+    }
+
+    MotionEventBuilder& pointer(PointerBuilder pointer) {
+        mPointers.push_back(pointer);
+        return *this;
+    }
+
+    MotionEventBuilder& addFlag(uint32_t flags) {
+        mFlags |= flags;
+        return *this;
+    }
+
+    MotionEvent build() {
+        std::vector<PointerProperties> pointerProperties;
+        std::vector<PointerCoords> pointerCoords;
+        for (const PointerBuilder& pointer : mPointers) {
+            pointerProperties.push_back(pointer.buildProperties());
+            pointerCoords.push_back(pointer.buildCoords());
+        }
+
+        // Set mouse cursor position for the most common cases to avoid boilerplate.
+        if (mSource == AINPUT_SOURCE_MOUSE &&
+            !MotionEvent::isValidCursorPosition(mRawXCursorPosition, mRawYCursorPosition)) {
+            mRawXCursorPosition = pointerCoords[0].getX();
+            mRawYCursorPosition = pointerCoords[0].getY();
+        }
+
+        MotionEvent event;
+        static const ui::Transform kIdentityTransform;
+        event.initialize(InputEvent::nextId(), mDeviceId, mSource, mDisplayId, INVALID_HMAC,
+                         mAction, mActionButton, mFlags, /*edgeFlags=*/0, AMETA_NONE, mButtonState,
+                         MotionClassification::NONE, kIdentityTransform,
+                         /*xPrecision=*/0, /*yPrecision=*/0, mRawXCursorPosition,
+                         mRawYCursorPosition, kIdentityTransform, mDownTime, mEventTime,
+                         mPointers.size(), pointerProperties.data(), pointerCoords.data());
+        return event;
+    }
+
+private:
+    int32_t mAction;
+    int32_t mDeviceId{DEFAULT_DEVICE_ID};
+    int32_t mSource;
+    nsecs_t mDownTime;
+    nsecs_t mEventTime;
+    int32_t mDisplayId{ADISPLAY_ID_DEFAULT};
+    int32_t mActionButton{0};
+    int32_t mButtonState{0};
+    int32_t mFlags{0};
+    float mRawXCursorPosition{AMOTION_EVENT_INVALID_CURSOR_POSITION};
+    float mRawYCursorPosition{AMOTION_EVENT_INVALID_CURSOR_POSITION};
+
+    std::vector<PointerBuilder> mPointers;
+};
+
+class MotionArgsBuilder {
+public:
+    MotionArgsBuilder(int32_t action, int32_t source) {
+        mAction = action;
+        mSource = source;
+        mEventTime = systemTime(SYSTEM_TIME_MONOTONIC);
+        mDownTime = mEventTime;
+    }
+
+    MotionArgsBuilder& deviceId(int32_t deviceId) {
+        mDeviceId = deviceId;
+        return *this;
+    }
+
+    MotionArgsBuilder& downTime(nsecs_t downTime) {
+        mDownTime = downTime;
+        return *this;
+    }
+
+    MotionArgsBuilder& eventTime(nsecs_t eventTime) {
+        mEventTime = eventTime;
+        return *this;
+    }
+
+    MotionArgsBuilder& displayId(int32_t displayId) {
+        mDisplayId = displayId;
+        return *this;
+    }
+
+    MotionArgsBuilder& policyFlags(int32_t policyFlags) {
+        mPolicyFlags = policyFlags;
+        return *this;
+    }
+
+    MotionArgsBuilder& actionButton(int32_t actionButton) {
+        mActionButton = actionButton;
+        return *this;
+    }
+
+    MotionArgsBuilder& buttonState(int32_t buttonState) {
+        mButtonState = buttonState;
+        return *this;
+    }
+
+    MotionArgsBuilder& rawXCursorPosition(float rawXCursorPosition) {
+        mRawXCursorPosition = rawXCursorPosition;
+        return *this;
+    }
+
+    MotionArgsBuilder& rawYCursorPosition(float rawYCursorPosition) {
+        mRawYCursorPosition = rawYCursorPosition;
+        return *this;
+    }
+
+    MotionArgsBuilder& pointer(PointerBuilder pointer) {
+        mPointers.push_back(pointer);
+        return *this;
+    }
+
+    MotionArgsBuilder& addFlag(uint32_t flags) {
+        mFlags |= flags;
+        return *this;
+    }
+
+    MotionArgsBuilder& classification(MotionClassification classification) {
+        mClassification = classification;
+        return *this;
+    }
+
+    NotifyMotionArgs build() {
+        std::vector<PointerProperties> pointerProperties;
+        std::vector<PointerCoords> pointerCoords;
+        for (const PointerBuilder& pointer : mPointers) {
+            pointerProperties.push_back(pointer.buildProperties());
+            pointerCoords.push_back(pointer.buildCoords());
+        }
+
+        // Set mouse cursor position for the most common cases to avoid boilerplate.
+        if (mSource == AINPUT_SOURCE_MOUSE &&
+            !MotionEvent::isValidCursorPosition(mRawXCursorPosition, mRawYCursorPosition)) {
+            mRawXCursorPosition = pointerCoords[0].getX();
+            mRawYCursorPosition = pointerCoords[0].getY();
+        }
+
+        return {InputEvent::nextId(),
+                mEventTime,
+                /*readTime=*/mEventTime,
+                mDeviceId,
+                mSource,
+                mDisplayId,
+                mPolicyFlags,
+                mAction,
+                mActionButton,
+                mFlags,
+                AMETA_NONE,
+                mButtonState,
+                mClassification,
+                /*edgeFlags=*/0,
+                static_cast<uint32_t>(mPointers.size()),
+                pointerProperties.data(),
+                pointerCoords.data(),
+                /*xPrecision=*/0,
+                /*yPrecision=*/0,
+                mRawXCursorPosition,
+                mRawYCursorPosition,
+                mDownTime,
+                /*videoFrames=*/{}};
+    }
+
+private:
+    int32_t mAction;
+    int32_t mDeviceId{DEFAULT_DEVICE_ID};
+    uint32_t mSource;
+    nsecs_t mDownTime;
+    nsecs_t mEventTime;
+    int32_t mDisplayId{ADISPLAY_ID_DEFAULT};
+    uint32_t mPolicyFlags = DEFAULT_POLICY_FLAGS;
+    int32_t mActionButton{0};
+    int32_t mButtonState{0};
+    int32_t mFlags{0};
+    MotionClassification mClassification{MotionClassification::NONE};
+    float mRawXCursorPosition{AMOTION_EVENT_INVALID_CURSOR_POSITION};
+    float mRawYCursorPosition{AMOTION_EVENT_INVALID_CURSOR_POSITION};
+
+    std::vector<PointerBuilder> mPointers;
+};
+
+class KeyArgsBuilder {
+public:
+    KeyArgsBuilder(int32_t action, int32_t source) {
+        mAction = action;
+        mSource = source;
+        mEventTime = systemTime(SYSTEM_TIME_MONOTONIC);
+        mDownTime = mEventTime;
+    }
+
+    KeyArgsBuilder& deviceId(int32_t deviceId) {
+        mDeviceId = deviceId;
+        return *this;
+    }
+
+    KeyArgsBuilder& downTime(nsecs_t downTime) {
+        mDownTime = downTime;
+        return *this;
+    }
+
+    KeyArgsBuilder& eventTime(nsecs_t eventTime) {
+        mEventTime = eventTime;
+        return *this;
+    }
+
+    KeyArgsBuilder& displayId(int32_t displayId) {
+        mDisplayId = displayId;
+        return *this;
+    }
+
+    KeyArgsBuilder& policyFlags(int32_t policyFlags) {
+        mPolicyFlags = policyFlags;
+        return *this;
+    }
+
+    KeyArgsBuilder& addFlag(uint32_t flags) {
+        mFlags |= flags;
+        return *this;
+    }
+
+    KeyArgsBuilder& keyCode(int32_t keyCode) {
+        mKeyCode = keyCode;
+        return *this;
+    }
+
+    NotifyKeyArgs build() const {
+        return {InputEvent::nextId(),
+                mEventTime,
+                /*readTime=*/mEventTime,
+                mDeviceId,
+                mSource,
+                mDisplayId,
+                mPolicyFlags,
+                mAction,
+                mFlags,
+                mKeyCode,
+                mScanCode,
+                mMetaState,
+                mDownTime};
+    }
+
+private:
+    int32_t mAction;
+    int32_t mDeviceId = DEFAULT_DEVICE_ID;
+    uint32_t mSource;
+    nsecs_t mDownTime;
+    nsecs_t mEventTime;
+    int32_t mDisplayId{ADISPLAY_ID_DEFAULT};
+    uint32_t mPolicyFlags = DEFAULT_POLICY_FLAGS;
+    int32_t mFlags{0};
+    int32_t mKeyCode{AKEYCODE_UNKNOWN};
+    int32_t mScanCode{0};
+    int32_t mMetaState{AMETA_NONE};
+};
+
+} // namespace android
diff --git a/services/inputflinger/tests/GestureConverter_test.cpp b/services/inputflinger/tests/GestureConverter_test.cpp
index f115fff..a3994f0 100644
--- a/services/inputflinger/tests/GestureConverter_test.cpp
+++ b/services/inputflinger/tests/GestureConverter_test.cpp
@@ -395,7 +395,7 @@
                 WithMotionClassification(MotionClassification::NONE));
 }
 
-TEST_F(GestureConverterTest, ThreeFingerSwipe_ClearsOffsetsAfterGesture) {
+TEST_F(GestureConverterTest, ThreeFingerSwipe_ClearsGestureAxesAfterGesture) {
     InputDeviceContext deviceContext(*mDevice, EVENTHUB_ID);
     GestureConverter converter(*mReader->getContext(), deviceContext, DEVICE_ID);
 
@@ -412,7 +412,8 @@
                          GESTURES_ZOOM_START);
     args = converter.handleGesture(ARBITRARY_TIME, READ_TIME, pinchGesture);
     ASSERT_FALSE(args.empty());
-    EXPECT_THAT(std::get<NotifyMotionArgs>(args.front()), WithGestureOffset(0, 0, EPSILON));
+    EXPECT_THAT(std::get<NotifyMotionArgs>(args.front()),
+                AllOf(WithGestureOffset(0, 0, EPSILON), WithGestureSwipeFingerCount(0)));
 }
 
 TEST_F(GestureConverterTest, ThreeFingerSwipe_Vertical) {
@@ -433,6 +434,7 @@
     NotifyMotionArgs arg = std::get<NotifyMotionArgs>(args.front());
     ASSERT_THAT(arg,
                 AllOf(WithMotionAction(AMOTION_EVENT_ACTION_DOWN), WithGestureOffset(0, 0, EPSILON),
+                      WithGestureSwipeFingerCount(3),
                       WithMotionClassification(MotionClassification::MULTI_FINGER_SWIPE),
                       WithPointerCount(1u), WithToolType(ToolType::FINGER)));
     PointerCoords finger0Start = arg.pointerCoords[0];
@@ -441,7 +443,7 @@
     ASSERT_THAT(arg,
                 AllOf(WithMotionAction(AMOTION_EVENT_ACTION_POINTER_DOWN |
                                        1 << AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT),
-                      WithGestureOffset(0, 0, EPSILON),
+                      WithGestureOffset(0, 0, EPSILON), WithGestureSwipeFingerCount(3),
                       WithMotionClassification(MotionClassification::MULTI_FINGER_SWIPE),
                       WithPointerCount(2u), WithToolType(ToolType::FINGER)));
     PointerCoords finger1Start = arg.pointerCoords[1];
@@ -450,7 +452,7 @@
     ASSERT_THAT(arg,
                 AllOf(WithMotionAction(AMOTION_EVENT_ACTION_POINTER_DOWN |
                                        2 << AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT),
-                      WithGestureOffset(0, 0, EPSILON),
+                      WithGestureOffset(0, 0, EPSILON), WithGestureSwipeFingerCount(3),
                       WithMotionClassification(MotionClassification::MULTI_FINGER_SWIPE),
                       WithPointerCount(3u), WithToolType(ToolType::FINGER)));
     PointerCoords finger2Start = arg.pointerCoords[2];
@@ -459,7 +461,7 @@
     arg = std::get<NotifyMotionArgs>(args.front());
     ASSERT_THAT(arg,
                 AllOf(WithMotionAction(AMOTION_EVENT_ACTION_MOVE),
-                      WithGestureOffset(0, -0.01, EPSILON),
+                      WithGestureOffset(0, -0.01, EPSILON), WithGestureSwipeFingerCount(3),
                       WithMotionClassification(MotionClassification::MULTI_FINGER_SWIPE),
                       WithPointerCount(3u), WithToolType(ToolType::FINGER)));
     EXPECT_EQ(arg.pointerCoords[0].getX(), finger0Start.getX());
@@ -476,7 +478,7 @@
     arg = std::get<NotifyMotionArgs>(args.front());
     ASSERT_THAT(arg,
                 AllOf(WithMotionAction(AMOTION_EVENT_ACTION_MOVE),
-                      WithGestureOffset(0, -0.005, EPSILON),
+                      WithGestureOffset(0, -0.005, EPSILON), WithGestureSwipeFingerCount(3),
                       WithMotionClassification(MotionClassification::MULTI_FINGER_SWIPE),
                       WithPointerCount(3u), WithToolType(ToolType::FINGER)));
     EXPECT_EQ(arg.pointerCoords[0].getX(), finger0Start.getX());
@@ -492,19 +494,20 @@
     ASSERT_THAT(std::get<NotifyMotionArgs>(args.front()),
                 AllOf(WithMotionAction(AMOTION_EVENT_ACTION_POINTER_UP |
                                        2 << AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT),
-                      WithGestureOffset(0, 0, EPSILON),
+                      WithGestureOffset(0, 0, EPSILON), WithGestureSwipeFingerCount(3),
                       WithMotionClassification(MotionClassification::MULTI_FINGER_SWIPE),
                       WithPointerCount(3u), WithToolType(ToolType::FINGER)));
     args.pop_front();
     ASSERT_THAT(std::get<NotifyMotionArgs>(args.front()),
                 AllOf(WithMotionAction(AMOTION_EVENT_ACTION_POINTER_UP |
                                        1 << AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT),
-                      WithGestureOffset(0, 0, EPSILON),
+                      WithGestureOffset(0, 0, EPSILON), WithGestureSwipeFingerCount(3),
                       WithMotionClassification(MotionClassification::MULTI_FINGER_SWIPE),
                       WithPointerCount(2u), WithToolType(ToolType::FINGER)));
     args.pop_front();
     ASSERT_THAT(std::get<NotifyMotionArgs>(args.front()),
                 AllOf(WithMotionAction(AMOTION_EVENT_ACTION_UP), WithGestureOffset(0, 0, EPSILON),
+                      WithGestureSwipeFingerCount(3),
                       WithMotionClassification(MotionClassification::MULTI_FINGER_SWIPE),
                       WithPointerCount(1u), WithToolType(ToolType::FINGER)));
 }
@@ -600,6 +603,7 @@
     NotifyMotionArgs arg = std::get<NotifyMotionArgs>(args.front());
     ASSERT_THAT(arg,
                 AllOf(WithMotionAction(AMOTION_EVENT_ACTION_DOWN), WithGestureOffset(0, 0, EPSILON),
+                      WithGestureSwipeFingerCount(4),
                       WithMotionClassification(MotionClassification::MULTI_FINGER_SWIPE),
                       WithPointerCount(1u), WithToolType(ToolType::FINGER)));
     PointerCoords finger0Start = arg.pointerCoords[0];
@@ -608,7 +612,7 @@
     ASSERT_THAT(arg,
                 AllOf(WithMotionAction(AMOTION_EVENT_ACTION_POINTER_DOWN |
                                        1 << AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT),
-                      WithGestureOffset(0, 0, EPSILON),
+                      WithGestureOffset(0, 0, EPSILON), WithGestureSwipeFingerCount(4),
                       WithMotionClassification(MotionClassification::MULTI_FINGER_SWIPE),
                       WithPointerCount(2u), WithToolType(ToolType::FINGER)));
     PointerCoords finger1Start = arg.pointerCoords[1];
@@ -617,7 +621,7 @@
     ASSERT_THAT(arg,
                 AllOf(WithMotionAction(AMOTION_EVENT_ACTION_POINTER_DOWN |
                                        2 << AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT),
-                      WithGestureOffset(0, 0, EPSILON),
+                      WithGestureOffset(0, 0, EPSILON), WithGestureSwipeFingerCount(4),
                       WithMotionClassification(MotionClassification::MULTI_FINGER_SWIPE),
                       WithPointerCount(3u), WithToolType(ToolType::FINGER)));
     PointerCoords finger2Start = arg.pointerCoords[2];
@@ -626,7 +630,7 @@
     ASSERT_THAT(arg,
                 AllOf(WithMotionAction(AMOTION_EVENT_ACTION_POINTER_DOWN |
                                        3 << AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT),
-                      WithGestureOffset(0, 0, EPSILON),
+                      WithGestureOffset(0, 0, EPSILON), WithGestureSwipeFingerCount(4),
                       WithMotionClassification(MotionClassification::MULTI_FINGER_SWIPE),
                       WithPointerCount(4u), WithToolType(ToolType::FINGER)));
     PointerCoords finger3Start = arg.pointerCoords[3];
@@ -635,7 +639,7 @@
     arg = std::get<NotifyMotionArgs>(args.front());
     ASSERT_THAT(arg,
                 AllOf(WithMotionAction(AMOTION_EVENT_ACTION_MOVE),
-                      WithGestureOffset(0.01, 0, EPSILON),
+                      WithGestureOffset(0.01, 0, EPSILON), WithGestureSwipeFingerCount(4),
                       WithMotionClassification(MotionClassification::MULTI_FINGER_SWIPE),
                       WithPointerCount(4u), WithToolType(ToolType::FINGER)));
     EXPECT_EQ(arg.pointerCoords[0].getX(), finger0Start.getX() + 10);
@@ -654,7 +658,7 @@
     arg = std::get<NotifyMotionArgs>(args.front());
     ASSERT_THAT(arg,
                 AllOf(WithMotionAction(AMOTION_EVENT_ACTION_MOVE),
-                      WithGestureOffset(0.005, 0, EPSILON),
+                      WithGestureOffset(0.005, 0, EPSILON), WithGestureSwipeFingerCount(4),
                       WithMotionClassification(MotionClassification::MULTI_FINGER_SWIPE),
                       WithPointerCount(4u), WithToolType(ToolType::FINGER)));
     EXPECT_EQ(arg.pointerCoords[0].getX(), finger0Start.getX() + 15);
@@ -672,26 +676,27 @@
     ASSERT_THAT(std::get<NotifyMotionArgs>(args.front()),
                 AllOf(WithMotionAction(AMOTION_EVENT_ACTION_POINTER_UP |
                                        3 << AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT),
-                      WithGestureOffset(0, 0, EPSILON),
+                      WithGestureOffset(0, 0, EPSILON), WithGestureSwipeFingerCount(4),
                       WithMotionClassification(MotionClassification::MULTI_FINGER_SWIPE),
                       WithPointerCount(4u), WithToolType(ToolType::FINGER)));
     args.pop_front();
     ASSERT_THAT(std::get<NotifyMotionArgs>(args.front()),
                 AllOf(WithMotionAction(AMOTION_EVENT_ACTION_POINTER_UP |
                                        2 << AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT),
-                      WithGestureOffset(0, 0, EPSILON),
+                      WithGestureOffset(0, 0, EPSILON), WithGestureSwipeFingerCount(4),
                       WithMotionClassification(MotionClassification::MULTI_FINGER_SWIPE),
                       WithPointerCount(3u), WithToolType(ToolType::FINGER)));
     args.pop_front();
     ASSERT_THAT(std::get<NotifyMotionArgs>(args.front()),
                 AllOf(WithMotionAction(AMOTION_EVENT_ACTION_POINTER_UP |
                                        1 << AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT),
-                      WithGestureOffset(0, 0, EPSILON),
+                      WithGestureOffset(0, 0, EPSILON), WithGestureSwipeFingerCount(4),
                       WithMotionClassification(MotionClassification::MULTI_FINGER_SWIPE),
                       WithPointerCount(2u), WithToolType(ToolType::FINGER)));
     args.pop_front();
     ASSERT_THAT(std::get<NotifyMotionArgs>(args.front()),
                 AllOf(WithMotionAction(AMOTION_EVENT_ACTION_UP), WithGestureOffset(0, 0, EPSILON),
+                      WithGestureSwipeFingerCount(4),
                       WithMotionClassification(MotionClassification::MULTI_FINGER_SWIPE),
                       WithPointerCount(1u), WithToolType(ToolType::FINGER)));
 }
diff --git a/services/inputflinger/tests/InputDeviceMetricsCollector_test.cpp b/services/inputflinger/tests/InputDeviceMetricsCollector_test.cpp
new file mode 100644
index 0000000..e38f88c
--- /dev/null
+++ b/services/inputflinger/tests/InputDeviceMetricsCollector_test.cpp
@@ -0,0 +1,625 @@
+/*
+ * 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 "../InputDeviceMetricsCollector.h"
+
+#include <gtest/gtest.h>
+#include <gui/constants.h>
+#include <linux/input.h>
+#include <array>
+#include <tuple>
+
+#include "EventBuilders.h"
+#include "TestInputListener.h"
+
+namespace android {
+
+using std::chrono_literals::operator""ns;
+using std::chrono::nanoseconds;
+
+namespace {
+
+constexpr auto USAGE_TIMEOUT = 8765309ns;
+constexpr auto TIME = 999999ns;
+constexpr auto ALL_USAGE_SOURCES = ftl::enum_range<InputDeviceUsageSource>();
+
+constexpr int32_t DEVICE_ID = 3;
+constexpr int32_t DEVICE_ID_2 = 4;
+constexpr int32_t VID = 0xFEED;
+constexpr int32_t PID = 0xDEAD;
+constexpr int32_t VERSION = 0xBEEF;
+const std::string DEVICE_NAME = "Half Dome";
+const std::string LOCATION = "California";
+const std::string UNIQUE_ID = "Yosemite";
+constexpr uint32_t TOUCHSCREEN = AINPUT_SOURCE_TOUCHSCREEN;
+constexpr uint32_t STYLUS = AINPUT_SOURCE_STYLUS;
+constexpr uint32_t KEY_SOURCES =
+        AINPUT_SOURCE_KEYBOARD | AINPUT_SOURCE_DPAD | AINPUT_SOURCE_GAMEPAD;
+constexpr int32_t POINTER_1_DOWN =
+        AMOTION_EVENT_ACTION_POINTER_DOWN | (1 << AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT);
+
+InputDeviceIdentifier getIdentifier(int32_t id = DEVICE_ID) {
+    InputDeviceIdentifier identifier;
+    identifier.name = DEVICE_NAME + "_" + std::to_string(id);
+    identifier.location = LOCATION;
+    identifier.uniqueId = UNIQUE_ID;
+    identifier.vendor = VID;
+    identifier.product = PID;
+    identifier.version = VERSION;
+    identifier.bus = BUS_USB;
+    return identifier;
+}
+
+InputDeviceInfo generateTestDeviceInfo(int32_t id = DEVICE_ID,
+                                       uint32_t sources = TOUCHSCREEN | STYLUS,
+                                       bool isAlphabetic = false) {
+    auto info = InputDeviceInfo();
+    info.initialize(id, /*generation=*/1, /*controllerNumber=*/1, getIdentifier(id), "alias",
+                    /*isExternal=*/false, /*hasMic=*/false, ADISPLAY_ID_NONE);
+    info.addSource(sources);
+    info.setKeyboardType(isAlphabetic ? AINPUT_KEYBOARD_TYPE_ALPHABETIC
+                                      : AINPUT_KEYBOARD_TYPE_NON_ALPHABETIC);
+    return info;
+}
+
+const InputDeviceInfo ALPHABETIC_KEYBOARD_INFO =
+        generateTestDeviceInfo(DEVICE_ID, KEY_SOURCES, /*isAlphabetic=*/true);
+const InputDeviceInfo NON_ALPHABETIC_KEYBOARD_INFO =
+        generateTestDeviceInfo(DEVICE_ID, KEY_SOURCES, /*isAlphabetic=*/false);
+
+} // namespace
+
+// --- InputDeviceMetricsCollectorDeviceClassificationTest ---
+
+class DeviceClassificationFixture : public ::testing::Test,
+                                    public ::testing::WithParamInterface<InputDeviceUsageSource> {};
+
+TEST_P(DeviceClassificationFixture, ValidClassifications) {
+    const InputDeviceUsageSource usageSource = GetParam();
+
+    // Use a switch to ensure a test is added for all source classifications.
+    switch (usageSource) {
+        case InputDeviceUsageSource::UNKNOWN: {
+            ASSERT_EQ(InputDeviceUsageSource::UNKNOWN,
+                      getUsageSourceForKeyArgs(generateTestDeviceInfo(),
+                                               KeyArgsBuilder(AKEY_EVENT_ACTION_DOWN, TOUCHSCREEN)
+                                                       .build()));
+
+            std::set<InputDeviceUsageSource> srcs{InputDeviceUsageSource::UNKNOWN};
+            ASSERT_EQ(srcs,
+                      getUsageSourcesForMotionArgs(
+                              MotionArgsBuilder(AMOTION_EVENT_ACTION_MOVE, AINPUT_SOURCE_KEYBOARD)
+                                      .pointer(PointerBuilder(/*id=*/1, ToolType::PALM)
+                                                       .x(100)
+                                                       .y(200))
+                                      .build()));
+            break;
+        }
+
+        case InputDeviceUsageSource::BUTTONS: {
+            ASSERT_EQ(InputDeviceUsageSource::BUTTONS,
+                      getUsageSourceForKeyArgs(NON_ALPHABETIC_KEYBOARD_INFO,
+                                               KeyArgsBuilder(AKEY_EVENT_ACTION_DOWN, KEY_SOURCES)
+                                                       .keyCode(AKEYCODE_STYLUS_BUTTON_TAIL)
+                                                       .build()));
+            break;
+        }
+
+        case InputDeviceUsageSource::KEYBOARD: {
+            ASSERT_EQ(InputDeviceUsageSource::KEYBOARD,
+                      getUsageSourceForKeyArgs(ALPHABETIC_KEYBOARD_INFO,
+                                               KeyArgsBuilder(AKEY_EVENT_ACTION_DOWN, KEY_SOURCES)
+                                                       .build()));
+            break;
+        }
+
+        case InputDeviceUsageSource::DPAD: {
+            ASSERT_EQ(InputDeviceUsageSource::DPAD,
+                      getUsageSourceForKeyArgs(NON_ALPHABETIC_KEYBOARD_INFO,
+                                               KeyArgsBuilder(AKEY_EVENT_ACTION_DOWN, KEY_SOURCES)
+                                                       .keyCode(AKEYCODE_DPAD_CENTER)
+                                                       .build()));
+
+            ASSERT_EQ(InputDeviceUsageSource::DPAD,
+                      getUsageSourceForKeyArgs(ALPHABETIC_KEYBOARD_INFO,
+                                               KeyArgsBuilder(AKEY_EVENT_ACTION_DOWN, KEY_SOURCES)
+                                                       .keyCode(AKEYCODE_DPAD_CENTER)
+                                                       .build()));
+            break;
+        }
+
+        case InputDeviceUsageSource::GAMEPAD: {
+            ASSERT_EQ(InputDeviceUsageSource::GAMEPAD,
+                      getUsageSourceForKeyArgs(NON_ALPHABETIC_KEYBOARD_INFO,
+                                               KeyArgsBuilder(AKEY_EVENT_ACTION_DOWN, KEY_SOURCES)
+                                                       .keyCode(AKEYCODE_BUTTON_A)
+                                                       .build()));
+
+            ASSERT_EQ(InputDeviceUsageSource::GAMEPAD,
+                      getUsageSourceForKeyArgs(ALPHABETIC_KEYBOARD_INFO,
+                                               KeyArgsBuilder(AKEY_EVENT_ACTION_DOWN, KEY_SOURCES)
+                                                       .keyCode(AKEYCODE_BUTTON_A)
+                                                       .build()));
+            break;
+        }
+
+        case InputDeviceUsageSource::JOYSTICK: {
+            std::set<InputDeviceUsageSource> srcs{InputDeviceUsageSource::JOYSTICK};
+            ASSERT_EQ(srcs,
+                      getUsageSourcesForMotionArgs(
+                              MotionArgsBuilder(AMOTION_EVENT_ACTION_MOVE, AINPUT_SOURCE_JOYSTICK)
+                                      .pointer(PointerBuilder(/*id=*/1, ToolType::UNKNOWN)
+                                                       .axis(AMOTION_EVENT_AXIS_GAS, 1.f))
+                                      .build()));
+            break;
+        }
+
+        case InputDeviceUsageSource::MOUSE: {
+            std::set<InputDeviceUsageSource> srcs{InputDeviceUsageSource::MOUSE};
+            ASSERT_EQ(srcs,
+                      getUsageSourcesForMotionArgs(
+                              MotionArgsBuilder(AMOTION_EVENT_ACTION_HOVER_MOVE,
+                                                AINPUT_SOURCE_MOUSE)
+                                      .pointer(PointerBuilder(/*id=*/1, ToolType::MOUSE)
+                                                       .x(100)
+                                                       .y(200))
+                                      .build()));
+            break;
+        }
+
+        case InputDeviceUsageSource::MOUSE_CAPTURED: {
+            std::set<InputDeviceUsageSource> srcs{InputDeviceUsageSource::MOUSE_CAPTURED};
+            ASSERT_EQ(srcs,
+                      getUsageSourcesForMotionArgs(
+                              MotionArgsBuilder(AMOTION_EVENT_ACTION_MOVE,
+                                                AINPUT_SOURCE_MOUSE_RELATIVE)
+                                      .pointer(PointerBuilder(/*id=*/1, ToolType::MOUSE)
+                                                       .x(100)
+                                                       .y(200)
+                                                       .axis(AMOTION_EVENT_AXIS_RELATIVE_X, 100)
+                                                       .axis(AMOTION_EVENT_AXIS_RELATIVE_Y, 200))
+                                      .build()));
+            break;
+        }
+
+        case InputDeviceUsageSource::TOUCHPAD: {
+            std::set<InputDeviceUsageSource> srcs{InputDeviceUsageSource::TOUCHPAD};
+            ASSERT_EQ(srcs,
+                      getUsageSourcesForMotionArgs(
+                              MotionArgsBuilder(AMOTION_EVENT_ACTION_MOVE, AINPUT_SOURCE_MOUSE)
+                                      .pointer(PointerBuilder(/*id=*/1, ToolType::FINGER)
+                                                       .x(100)
+                                                       .y(200))
+                                      .build()));
+            break;
+        }
+
+        case InputDeviceUsageSource::TOUCHPAD_CAPTURED: {
+            std::set<InputDeviceUsageSource> srcs{InputDeviceUsageSource::TOUCHPAD_CAPTURED};
+            ASSERT_EQ(srcs,
+                      getUsageSourcesForMotionArgs(
+                              MotionArgsBuilder(AMOTION_EVENT_ACTION_MOVE, AINPUT_SOURCE_TOUCHPAD)
+                                      .pointer(PointerBuilder(/*id=*/1, ToolType::FINGER)
+                                                       .x(100)
+                                                       .y(200)
+                                                       .axis(AMOTION_EVENT_AXIS_RELATIVE_X, 1)
+                                                       .axis(AMOTION_EVENT_AXIS_RELATIVE_Y, 2))
+                                      .build()));
+            break;
+        }
+
+        case InputDeviceUsageSource::ROTARY_ENCODER: {
+            std::set<InputDeviceUsageSource> srcs{InputDeviceUsageSource::ROTARY_ENCODER};
+            ASSERT_EQ(srcs,
+                      getUsageSourcesForMotionArgs(
+                              MotionArgsBuilder(AMOTION_EVENT_ACTION_SCROLL,
+                                                AINPUT_SOURCE_ROTARY_ENCODER)
+                                      .pointer(PointerBuilder(/*id=*/1, ToolType::UNKNOWN)
+                                                       .axis(AMOTION_EVENT_AXIS_SCROLL, 10)
+                                                       .axis(AMOTION_EVENT_AXIS_VSCROLL, 10))
+                                      .build()));
+            break;
+        }
+
+        case InputDeviceUsageSource::STYLUS_DIRECT: {
+            std::set<InputDeviceUsageSource> srcs{InputDeviceUsageSource::STYLUS_DIRECT};
+            ASSERT_EQ(srcs,
+                      getUsageSourcesForMotionArgs(
+                              MotionArgsBuilder(AMOTION_EVENT_ACTION_HOVER_ENTER,
+                                                STYLUS | TOUCHSCREEN)
+                                      .pointer(PointerBuilder(/*id=*/1, ToolType::STYLUS)
+                                                       .x(100)
+                                                       .y(200))
+                                      .build()));
+            break;
+        }
+
+        case InputDeviceUsageSource::STYLUS_INDIRECT: {
+            std::set<InputDeviceUsageSource> srcs{InputDeviceUsageSource::STYLUS_INDIRECT};
+            ASSERT_EQ(srcs,
+                      getUsageSourcesForMotionArgs(
+                              MotionArgsBuilder(AMOTION_EVENT_ACTION_HOVER_ENTER,
+                                                STYLUS | TOUCHSCREEN | AINPUT_SOURCE_MOUSE)
+                                      .pointer(PointerBuilder(/*id=*/1, ToolType::STYLUS)
+                                                       .x(100)
+                                                       .y(200))
+                                      .build()));
+            break;
+        }
+
+        case InputDeviceUsageSource::STYLUS_FUSED: {
+            std::set<InputDeviceUsageSource> srcs{InputDeviceUsageSource::STYLUS_FUSED};
+            ASSERT_EQ(srcs,
+                      getUsageSourcesForMotionArgs(
+                              MotionArgsBuilder(AMOTION_EVENT_ACTION_HOVER_ENTER,
+                                                AINPUT_SOURCE_BLUETOOTH_STYLUS | TOUCHSCREEN)
+                                      .pointer(PointerBuilder(/*id=*/1, ToolType::STYLUS)
+                                                       .x(100)
+                                                       .y(200))
+                                      .build()));
+            break;
+        }
+
+        case InputDeviceUsageSource::TOUCH_NAVIGATION: {
+            std::set<InputDeviceUsageSource> srcs{InputDeviceUsageSource::TOUCH_NAVIGATION};
+            ASSERT_EQ(srcs,
+                      getUsageSourcesForMotionArgs(
+                              MotionArgsBuilder(AMOTION_EVENT_ACTION_MOVE,
+                                                AINPUT_SOURCE_TOUCH_NAVIGATION)
+                                      .pointer(PointerBuilder(/*id=*/1, ToolType::FINGER)
+                                                       .x(100)
+                                                       .y(200))
+                                      .build()));
+            break;
+        }
+
+        case InputDeviceUsageSource::TOUCHSCREEN: {
+            std::set<InputDeviceUsageSource> srcs{InputDeviceUsageSource::TOUCHSCREEN};
+            ASSERT_EQ(srcs,
+                      getUsageSourcesForMotionArgs(
+                              MotionArgsBuilder(POINTER_1_DOWN, TOUCHSCREEN)
+                                      .pointer(PointerBuilder(/*id=*/1, ToolType::FINGER)
+                                                       .x(100)
+                                                       .y(200))
+                                      .pointer(PointerBuilder(/*id=*/2, ToolType::FINGER)
+                                                       .x(300)
+                                                       .y(400))
+                                      .build()));
+            break;
+        }
+
+        case InputDeviceUsageSource::TRACKBALL: {
+            std::set<InputDeviceUsageSource> srcs{InputDeviceUsageSource::TRACKBALL};
+            ASSERT_EQ(srcs,
+                      getUsageSourcesForMotionArgs(
+                              MotionArgsBuilder(AMOTION_EVENT_ACTION_SCROLL,
+                                                AINPUT_SOURCE_TRACKBALL)
+                                      .pointer(PointerBuilder(/*id=*/1, ToolType::UNKNOWN)
+                                                       .axis(AMOTION_EVENT_AXIS_VSCROLL, 100)
+                                                       .axis(AMOTION_EVENT_AXIS_HSCROLL, 200))
+                                      .build()));
+            break;
+        }
+    }
+}
+
+INSTANTIATE_TEST_SUITE_P(InputDeviceMetricsCollectorDeviceClassificationTest,
+                         DeviceClassificationFixture,
+                         ::testing::ValuesIn(ALL_USAGE_SOURCES.begin(), ALL_USAGE_SOURCES.end()),
+                         [](const testing::TestParamInfo<InputDeviceUsageSource>& testParamInfo) {
+                             return ftl::enum_string(testParamInfo.param);
+                         });
+
+TEST(InputDeviceMetricsCollectorDeviceClassificationTest, MixedClassificationTouchscreenStylus) {
+    std::set<InputDeviceUsageSource> srcs{InputDeviceUsageSource::TOUCHSCREEN,
+                                          InputDeviceUsageSource::STYLUS_DIRECT};
+    ASSERT_EQ(srcs,
+              getUsageSourcesForMotionArgs(
+                      MotionArgsBuilder(POINTER_1_DOWN, TOUCHSCREEN | STYLUS)
+                              .pointer(PointerBuilder(/*id=*/1, ToolType::FINGER).x(100).y(200))
+                              .pointer(PointerBuilder(/*id=*/2, ToolType::STYLUS).x(300).y(400))
+                              .build()));
+}
+
+// --- InputDeviceMetricsCollectorTest ---
+
+class InputDeviceMetricsCollectorTest : public testing::Test, public InputDeviceMetricsLogger {
+protected:
+    TestInputListener mTestListener;
+    InputDeviceMetricsCollector mMetricsCollector{mTestListener, *this, USAGE_TIMEOUT};
+
+    void assertUsageLogged(const InputDeviceIdentifier& identifier, nanoseconds duration,
+                           std::optional<SourceUsageBreakdown> sourceBreakdown = {}) {
+        ASSERT_GE(mLoggedUsageSessions.size(), 1u);
+        const auto& [loggedIdentifier, report] = *mLoggedUsageSessions.begin();
+        ASSERT_EQ(identifier, loggedIdentifier);
+        ASSERT_EQ(duration, report.usageDuration);
+        if (sourceBreakdown) {
+            ASSERT_EQ(sourceBreakdown, report.sourceBreakdown);
+        }
+        mLoggedUsageSessions.erase(mLoggedUsageSessions.begin());
+    }
+
+    void assertUsageNotLogged() { ASSERT_TRUE(mLoggedUsageSessions.empty()); }
+
+    void setCurrentTime(nanoseconds time) { mCurrentTime = time; }
+
+    NotifyMotionArgs generateMotionArgs(int32_t deviceId,
+                                        uint32_t source = AINPUT_SOURCE_TOUCHSCREEN,
+                                        std::vector<ToolType> toolTypes = {ToolType::FINGER}) {
+        MotionArgsBuilder builder(AMOTION_EVENT_ACTION_MOVE, source);
+        for (size_t i = 0; i < toolTypes.size(); i++) {
+            builder.pointer(PointerBuilder(i, toolTypes[i]));
+        }
+        return builder.deviceId(deviceId)
+                .eventTime(mCurrentTime.count())
+                .downTime(mCurrentTime.count())
+                .build();
+    }
+
+private:
+    std::vector<std::tuple<InputDeviceIdentifier, DeviceUsageReport>> mLoggedUsageSessions;
+    nanoseconds mCurrentTime{TIME};
+
+    nanoseconds getCurrentTime() override { return mCurrentTime; }
+
+    void logInputDeviceUsageReported(const InputDeviceIdentifier& identifier,
+                                     const DeviceUsageReport& report) override {
+        mLoggedUsageSessions.emplace_back(identifier, report);
+    }
+};
+
+TEST_F(InputDeviceMetricsCollectorTest, DontLogUsageWhenDeviceNotRegistered) {
+    // Device was used.
+    mMetricsCollector.notifyMotion(generateMotionArgs(DEVICE_ID));
+    mTestListener.assertNotifyMotionWasCalled();
+    ASSERT_NO_FATAL_FAILURE(assertUsageNotLogged());
+
+    // Device was used again after the usage timeout expired, but we still don't log usage.
+    setCurrentTime(TIME + USAGE_TIMEOUT);
+    mMetricsCollector.notifyMotion(generateMotionArgs(DEVICE_ID));
+    mTestListener.assertNotifyMotionWasCalled();
+    ASSERT_NO_FATAL_FAILURE(assertUsageNotLogged());
+}
+
+TEST_F(InputDeviceMetricsCollectorTest, DontLogUsageForIgnoredDevices) {
+    constexpr static std::array<int32_t, 2> ignoredDevices{
+            {INVALID_INPUT_DEVICE_ID, VIRTUAL_KEYBOARD_ID}};
+
+    for (int32_t ignoredDeviceId : ignoredDevices) {
+        mMetricsCollector.notifyInputDevicesChanged(
+                {/*id=*/0, {generateTestDeviceInfo(ignoredDeviceId)}});
+
+        // Device was used.
+        mMetricsCollector.notifyMotion(generateMotionArgs(ignoredDeviceId));
+        mTestListener.assertNotifyMotionWasCalled();
+        ASSERT_NO_FATAL_FAILURE(assertUsageNotLogged());
+
+        // Device was used again after the usage timeout expired, but we still don't log usage.
+        setCurrentTime(TIME + USAGE_TIMEOUT);
+        mMetricsCollector.notifyMotion(generateMotionArgs(ignoredDeviceId));
+        mTestListener.assertNotifyMotionWasCalled();
+        ASSERT_NO_FATAL_FAILURE(assertUsageNotLogged());
+
+        // Remove the ignored device, and ensure we still don't log usage.
+        mMetricsCollector.notifyInputDevicesChanged({/*id=*/0, {}});
+        ASSERT_NO_FATAL_FAILURE(assertUsageNotLogged());
+    }
+}
+
+TEST_F(InputDeviceMetricsCollectorTest, LogsSingleEventUsageSession) {
+    mMetricsCollector.notifyInputDevicesChanged({/*id=*/0, {generateTestDeviceInfo()}});
+
+    // Device was used.
+    mMetricsCollector.notifyMotion(generateMotionArgs(DEVICE_ID));
+    ASSERT_NO_FATAL_FAILURE(assertUsageNotLogged());
+
+    // Device was used again after the usage timeout.
+    setCurrentTime(TIME + USAGE_TIMEOUT);
+    mMetricsCollector.notifyMotion(generateMotionArgs(DEVICE_ID));
+    // The usage session has zero duration because it consisted of only one event.
+    ASSERT_NO_FATAL_FAILURE(assertUsageLogged(getIdentifier(), 0ns));
+}
+
+TEST_F(InputDeviceMetricsCollectorTest, LogsMultipleEventUsageSession) {
+    mMetricsCollector.notifyInputDevicesChanged({/*id=*/0, {generateTestDeviceInfo()}});
+
+    // Device was used.
+    mMetricsCollector.notifyMotion(generateMotionArgs(DEVICE_ID));
+    ASSERT_NO_FATAL_FAILURE(assertUsageNotLogged());
+
+    // Device was used again after some time.
+    setCurrentTime(TIME + 21ns);
+    mMetricsCollector.notifyMotion(generateMotionArgs(DEVICE_ID));
+
+    setCurrentTime(TIME + 42ns);
+    mMetricsCollector.notifyMotion(generateMotionArgs(DEVICE_ID));
+
+    // Device was used again after the usage timeout.
+    setCurrentTime(TIME + 42ns + 2 * USAGE_TIMEOUT);
+    mMetricsCollector.notifyMotion(generateMotionArgs(DEVICE_ID));
+    ASSERT_NO_FATAL_FAILURE(assertUsageLogged(getIdentifier(), 42ns));
+}
+
+TEST_F(InputDeviceMetricsCollectorTest, RemovingDeviceEndsUsageSession) {
+    mMetricsCollector.notifyInputDevicesChanged({/*id=*/0, {generateTestDeviceInfo()}});
+
+    // Device was used.
+    mMetricsCollector.notifyMotion(generateMotionArgs(DEVICE_ID));
+    ASSERT_NO_FATAL_FAILURE(assertUsageNotLogged());
+
+    // Device was used again after some time.
+    setCurrentTime(TIME + 21ns);
+    mMetricsCollector.notifyMotion(generateMotionArgs(DEVICE_ID));
+
+    // The device was removed before the usage timeout expired.
+    setCurrentTime(TIME + 42ns);
+    mMetricsCollector.notifyInputDevicesChanged({/*id=*/0, {}});
+    ASSERT_NO_FATAL_FAILURE(assertUsageLogged(getIdentifier(), 21ns));
+}
+
+TEST_F(InputDeviceMetricsCollectorTest, TracksUsageFromDifferentDevicesIndependently) {
+    mMetricsCollector.notifyInputDevicesChanged(
+            {/*id=*/0, {generateTestDeviceInfo(), generateTestDeviceInfo(DEVICE_ID_2)}});
+
+    // Device 1 was used.
+    setCurrentTime(TIME);
+    mMetricsCollector.notifyMotion(generateMotionArgs(DEVICE_ID));
+    setCurrentTime(TIME + 100ns);
+    mMetricsCollector.notifyMotion(generateMotionArgs(DEVICE_ID));
+    ASSERT_NO_FATAL_FAILURE(assertUsageNotLogged());
+
+    // Device 2 was used.
+    setCurrentTime(TIME + 200ns);
+    mMetricsCollector.notifyMotion(generateMotionArgs(DEVICE_ID_2));
+    setCurrentTime(TIME + 400ns);
+    mMetricsCollector.notifyMotion(generateMotionArgs(DEVICE_ID_2));
+    ASSERT_NO_FATAL_FAILURE(assertUsageNotLogged());
+
+    // Device 1 was used after its usage timeout expired. Its usage session is reported.
+    setCurrentTime(TIME + 300ns + USAGE_TIMEOUT);
+    mMetricsCollector.notifyMotion(generateMotionArgs(DEVICE_ID));
+    ASSERT_NO_FATAL_FAILURE(assertUsageLogged(getIdentifier(DEVICE_ID), 100ns));
+
+    // Device 2 was used.
+    setCurrentTime(TIME + 350ns + USAGE_TIMEOUT);
+    mMetricsCollector.notifyMotion(generateMotionArgs(DEVICE_ID_2));
+    ASSERT_NO_FATAL_FAILURE(assertUsageNotLogged());
+
+    // Device 1 was used.
+    setCurrentTime(TIME + 500ns + USAGE_TIMEOUT);
+    mMetricsCollector.notifyMotion(generateMotionArgs(DEVICE_ID));
+    ASSERT_NO_FATAL_FAILURE(assertUsageNotLogged());
+
+    // Device 2 is not used for a while, but Device 1 is used again.
+    setCurrentTime(TIME + 400ns + (2 * USAGE_TIMEOUT));
+    mMetricsCollector.notifyMotion(generateMotionArgs(DEVICE_ID));
+    // Since Device 2's usage session ended, its usage should be reported.
+    ASSERT_NO_FATAL_FAILURE(assertUsageLogged(getIdentifier(DEVICE_ID_2), 150ns + USAGE_TIMEOUT));
+
+    ASSERT_NO_FATAL_FAILURE(assertUsageNotLogged());
+}
+
+TEST_F(InputDeviceMetricsCollectorTest, BreakdownUsageBySource) {
+    mMetricsCollector.notifyInputDevicesChanged({/*id=*/0, {generateTestDeviceInfo()}});
+    InputDeviceMetricsLogger::SourceUsageBreakdown expectedSourceBreakdown;
+
+    // Use touchscreen.
+    mMetricsCollector.notifyMotion(generateMotionArgs(DEVICE_ID, TOUCHSCREEN));
+    setCurrentTime(TIME + 100ns);
+    mMetricsCollector.notifyMotion(generateMotionArgs(DEVICE_ID, TOUCHSCREEN));
+    ASSERT_NO_FATAL_FAILURE(assertUsageNotLogged());
+
+    // Use a stylus with the same input device.
+    setCurrentTime(TIME + 200ns);
+    mMetricsCollector.notifyMotion(generateMotionArgs(DEVICE_ID, STYLUS, {ToolType::STYLUS}));
+    setCurrentTime(TIME + 400ns);
+    mMetricsCollector.notifyMotion(generateMotionArgs(DEVICE_ID, STYLUS, {ToolType::STYLUS}));
+    ASSERT_NO_FATAL_FAILURE(assertUsageNotLogged());
+
+    // Touchscreen was used again after its usage timeout expired.
+    // This should be tracked as a separate usage of the source in the breakdown.
+    setCurrentTime(TIME + 300ns + USAGE_TIMEOUT);
+    mMetricsCollector.notifyMotion(generateMotionArgs(DEVICE_ID));
+    expectedSourceBreakdown.emplace_back(InputDeviceUsageSource::TOUCHSCREEN, 100ns);
+    ASSERT_NO_FATAL_FAILURE(assertUsageNotLogged());
+
+    // Continue stylus and touchscreen usages.
+    setCurrentTime(TIME + 350ns + USAGE_TIMEOUT);
+    mMetricsCollector.notifyMotion(generateMotionArgs(DEVICE_ID, STYLUS, {ToolType::STYLUS}));
+    setCurrentTime(TIME + 450ns + USAGE_TIMEOUT);
+    mMetricsCollector.notifyMotion(generateMotionArgs(DEVICE_ID, TOUCHSCREEN));
+    ASSERT_NO_FATAL_FAILURE(assertUsageNotLogged());
+
+    // Touchscreen was used after the stylus's usage timeout expired.
+    // The stylus usage should be tracked in the source breakdown.
+    setCurrentTime(TIME + 400ns + USAGE_TIMEOUT + USAGE_TIMEOUT);
+    mMetricsCollector.notifyMotion(generateMotionArgs(DEVICE_ID, TOUCHSCREEN));
+    expectedSourceBreakdown.emplace_back(InputDeviceUsageSource::STYLUS_DIRECT,
+                                         150ns + USAGE_TIMEOUT);
+    ASSERT_NO_FATAL_FAILURE(assertUsageNotLogged());
+
+    // Remove all devices to force the usage session to be logged.
+    setCurrentTime(TIME + 500ns + USAGE_TIMEOUT);
+    mMetricsCollector.notifyInputDevicesChanged({});
+    expectedSourceBreakdown.emplace_back(InputDeviceUsageSource::TOUCHSCREEN,
+                                         100ns + USAGE_TIMEOUT);
+    // Verify that only one usage session was logged for the device, and that session was broken
+    // down by source correctly.
+    ASSERT_NO_FATAL_FAILURE(assertUsageLogged(getIdentifier(),
+                                              400ns + USAGE_TIMEOUT + USAGE_TIMEOUT,
+                                              expectedSourceBreakdown));
+
+    ASSERT_NO_FATAL_FAILURE(assertUsageNotLogged());
+}
+
+TEST_F(InputDeviceMetricsCollectorTest, BreakdownUsageBySource_TrackSourceByDevice) {
+    mMetricsCollector.notifyInputDevicesChanged(
+            {/*id=*/0, {generateTestDeviceInfo(DEVICE_ID), generateTestDeviceInfo(DEVICE_ID_2)}});
+    InputDeviceMetricsLogger::SourceUsageBreakdown expectedSourceBreakdown1;
+    InputDeviceMetricsLogger::SourceUsageBreakdown expectedSourceBreakdown2;
+
+    // Use both devices, with different sources.
+    mMetricsCollector.notifyMotion(generateMotionArgs(DEVICE_ID, TOUCHSCREEN));
+    mMetricsCollector.notifyMotion(generateMotionArgs(DEVICE_ID_2, STYLUS, {ToolType::STYLUS}));
+    setCurrentTime(TIME + 100ns);
+    mMetricsCollector.notifyMotion(generateMotionArgs(DEVICE_ID, TOUCHSCREEN));
+    mMetricsCollector.notifyMotion(generateMotionArgs(DEVICE_ID_2, STYLUS, {ToolType::STYLUS}));
+    ASSERT_NO_FATAL_FAILURE(assertUsageNotLogged());
+
+    // Remove all devices to force the usage session to be logged.
+    mMetricsCollector.notifyInputDevicesChanged({});
+    expectedSourceBreakdown1.emplace_back(InputDeviceUsageSource::TOUCHSCREEN, 100ns);
+    expectedSourceBreakdown2.emplace_back(InputDeviceUsageSource::STYLUS_DIRECT, 100ns);
+    ASSERT_NO_FATAL_FAILURE(
+            assertUsageLogged(getIdentifier(DEVICE_ID), 100ns, expectedSourceBreakdown1));
+    ASSERT_NO_FATAL_FAILURE(
+            assertUsageLogged(getIdentifier(DEVICE_ID_2), 100ns, expectedSourceBreakdown2));
+
+    ASSERT_NO_FATAL_FAILURE(assertUsageNotLogged());
+}
+
+TEST_F(InputDeviceMetricsCollectorTest, BreakdownUsageBySource_MultiSourceEvent) {
+    mMetricsCollector.notifyInputDevicesChanged({/*id=*/0, {generateTestDeviceInfo(DEVICE_ID)}});
+    InputDeviceMetricsLogger::SourceUsageBreakdown expectedSourceBreakdown;
+
+    mMetricsCollector.notifyMotion(generateMotionArgs(DEVICE_ID, TOUCHSCREEN | STYLUS, //
+                                                      {ToolType::STYLUS}));
+    setCurrentTime(TIME + 100ns);
+    mMetricsCollector.notifyMotion(generateMotionArgs(DEVICE_ID, TOUCHSCREEN | STYLUS, //
+                                                      {ToolType::STYLUS, ToolType::FINGER}));
+    setCurrentTime(TIME + 200ns);
+    mMetricsCollector.notifyMotion(generateMotionArgs(DEVICE_ID, TOUCHSCREEN | STYLUS, //
+                                                      {ToolType::STYLUS, ToolType::FINGER}));
+    setCurrentTime(TIME + 300ns);
+    mMetricsCollector.notifyMotion(generateMotionArgs(DEVICE_ID, TOUCHSCREEN | STYLUS, //
+                                                      {ToolType::FINGER}));
+    setCurrentTime(TIME + 400ns);
+    mMetricsCollector.notifyMotion(generateMotionArgs(DEVICE_ID, TOUCHSCREEN | STYLUS, //
+                                                      {ToolType::FINGER}));
+    ASSERT_NO_FATAL_FAILURE(assertUsageNotLogged());
+
+    // Remove all devices to force the usage session to be logged.
+    mMetricsCollector.notifyInputDevicesChanged({});
+    expectedSourceBreakdown.emplace_back(InputDeviceUsageSource::STYLUS_DIRECT, 200ns);
+    expectedSourceBreakdown.emplace_back(InputDeviceUsageSource::TOUCHSCREEN, 300ns);
+    ASSERT_NO_FATAL_FAILURE(
+            assertUsageLogged(getIdentifier(DEVICE_ID), 400ns, expectedSourceBreakdown));
+
+    ASSERT_NO_FATAL_FAILURE(assertUsageNotLogged());
+}
+
+} // namespace android
diff --git a/services/inputflinger/tests/InputDispatcher_test.cpp b/services/inputflinger/tests/InputDispatcher_test.cpp
index 3660f81..e6b73af 100644
--- a/services/inputflinger/tests/InputDispatcher_test.cpp
+++ b/services/inputflinger/tests/InputDispatcher_test.cpp
@@ -15,6 +15,7 @@
  */
 
 #include "../dispatcher/InputDispatcher.h"
+#include "EventBuilders.h"
 
 #include <android-base/properties.h>
 #include <android-base/silent_death_test.h>
@@ -51,7 +52,7 @@
 static constexpr nsecs_t ARBITRARY_TIME = 1234;
 
 // An arbitrary device id.
-static constexpr int32_t DEVICE_ID = 1;
+static constexpr int32_t DEVICE_ID = DEFAULT_DEVICE_ID;
 static constexpr int32_t SECOND_DEVICE_ID = 2;
 
 // An arbitrary display id.
@@ -97,9 +98,6 @@
 static constexpr int32_t SECONDARY_WINDOW_PID = 1010;
 static constexpr int32_t SECONDARY_WINDOW_UID = 1012;
 
-// The default policy flags to use for event injection by tests.
-static constexpr uint32_t DEFAULT_POLICY_FLAGS = POLICY_FLAG_FILTERED | POLICY_FLAG_PASS_TO_USER;
-
 // An arbitrary pid of the gesture monitor window
 static constexpr int32_t MONITOR_PID = 2001;
 
@@ -1468,247 +1466,6 @@
     return injectKey(dispatcher, AKEY_EVENT_ACTION_UP, /*repeatCount=*/0, displayId);
 }
 
-class PointerBuilder {
-public:
-    PointerBuilder(int32_t id, ToolType toolType) {
-        mProperties.clear();
-        mProperties.id = id;
-        mProperties.toolType = toolType;
-        mCoords.clear();
-    }
-
-    PointerBuilder& x(float x) { return axis(AMOTION_EVENT_AXIS_X, x); }
-
-    PointerBuilder& y(float y) { return axis(AMOTION_EVENT_AXIS_Y, y); }
-
-    PointerBuilder& axis(int32_t axis, float value) {
-        mCoords.setAxisValue(axis, value);
-        return *this;
-    }
-
-    PointerProperties buildProperties() const { return mProperties; }
-
-    PointerCoords buildCoords() const { return mCoords; }
-
-private:
-    PointerProperties mProperties;
-    PointerCoords mCoords;
-};
-
-class MotionEventBuilder {
-public:
-    MotionEventBuilder(int32_t action, int32_t source) {
-        mAction = action;
-        mSource = source;
-        mEventTime = systemTime(SYSTEM_TIME_MONOTONIC);
-        mDownTime = mEventTime;
-    }
-
-    MotionEventBuilder& deviceId(int32_t deviceId) {
-        mDeviceId = deviceId;
-        return *this;
-    }
-
-    MotionEventBuilder& downTime(nsecs_t downTime) {
-        mDownTime = downTime;
-        return *this;
-    }
-
-    MotionEventBuilder& eventTime(nsecs_t eventTime) {
-        mEventTime = eventTime;
-        return *this;
-    }
-
-    MotionEventBuilder& displayId(int32_t displayId) {
-        mDisplayId = displayId;
-        return *this;
-    }
-
-    MotionEventBuilder& actionButton(int32_t actionButton) {
-        mActionButton = actionButton;
-        return *this;
-    }
-
-    MotionEventBuilder& buttonState(int32_t buttonState) {
-        mButtonState = buttonState;
-        return *this;
-    }
-
-    MotionEventBuilder& rawXCursorPosition(float rawXCursorPosition) {
-        mRawXCursorPosition = rawXCursorPosition;
-        return *this;
-    }
-
-    MotionEventBuilder& rawYCursorPosition(float rawYCursorPosition) {
-        mRawYCursorPosition = rawYCursorPosition;
-        return *this;
-    }
-
-    MotionEventBuilder& pointer(PointerBuilder pointer) {
-        mPointers.push_back(pointer);
-        return *this;
-    }
-
-    MotionEventBuilder& addFlag(uint32_t flags) {
-        mFlags |= flags;
-        return *this;
-    }
-
-    MotionEvent build() {
-        std::vector<PointerProperties> pointerProperties;
-        std::vector<PointerCoords> pointerCoords;
-        for (const PointerBuilder& pointer : mPointers) {
-            pointerProperties.push_back(pointer.buildProperties());
-            pointerCoords.push_back(pointer.buildCoords());
-        }
-
-        // Set mouse cursor position for the most common cases to avoid boilerplate.
-        if (mSource == AINPUT_SOURCE_MOUSE &&
-            !MotionEvent::isValidCursorPosition(mRawXCursorPosition, mRawYCursorPosition)) {
-            mRawXCursorPosition = pointerCoords[0].getX();
-            mRawYCursorPosition = pointerCoords[0].getY();
-        }
-
-        MotionEvent event;
-        ui::Transform identityTransform;
-        event.initialize(InputEvent::nextId(), mDeviceId, mSource, mDisplayId, INVALID_HMAC,
-                         mAction, mActionButton, mFlags, /* edgeFlags */ 0, AMETA_NONE,
-                         mButtonState, MotionClassification::NONE, identityTransform,
-                         /* xPrecision */ 0, /* yPrecision */ 0, mRawXCursorPosition,
-                         mRawYCursorPosition, identityTransform, mDownTime, mEventTime,
-                         mPointers.size(), pointerProperties.data(), pointerCoords.data());
-
-        return event;
-    }
-
-private:
-    int32_t mAction;
-    int32_t mDeviceId = DEVICE_ID;
-    int32_t mSource;
-    nsecs_t mDownTime;
-    nsecs_t mEventTime;
-    int32_t mDisplayId{ADISPLAY_ID_DEFAULT};
-    int32_t mActionButton{0};
-    int32_t mButtonState{0};
-    int32_t mFlags{0};
-    float mRawXCursorPosition{AMOTION_EVENT_INVALID_CURSOR_POSITION};
-    float mRawYCursorPosition{AMOTION_EVENT_INVALID_CURSOR_POSITION};
-
-    std::vector<PointerBuilder> mPointers;
-};
-
-class MotionArgsBuilder {
-public:
-    MotionArgsBuilder(int32_t action, int32_t source) {
-        mAction = action;
-        mSource = source;
-        mEventTime = systemTime(SYSTEM_TIME_MONOTONIC);
-        mDownTime = mEventTime;
-    }
-
-    MotionArgsBuilder& deviceId(int32_t deviceId) {
-        mDeviceId = deviceId;
-        return *this;
-    }
-
-    MotionArgsBuilder& downTime(nsecs_t downTime) {
-        mDownTime = downTime;
-        return *this;
-    }
-
-    MotionArgsBuilder& eventTime(nsecs_t eventTime) {
-        mEventTime = eventTime;
-        return *this;
-    }
-
-    MotionArgsBuilder& displayId(int32_t displayId) {
-        mDisplayId = displayId;
-        return *this;
-    }
-
-    MotionArgsBuilder& policyFlags(int32_t policyFlags) {
-        mPolicyFlags = policyFlags;
-        return *this;
-    }
-
-    MotionArgsBuilder& actionButton(int32_t actionButton) {
-        mActionButton = actionButton;
-        return *this;
-    }
-
-    MotionArgsBuilder& buttonState(int32_t buttonState) {
-        mButtonState = buttonState;
-        return *this;
-    }
-
-    MotionArgsBuilder& rawXCursorPosition(float rawXCursorPosition) {
-        mRawXCursorPosition = rawXCursorPosition;
-        return *this;
-    }
-
-    MotionArgsBuilder& rawYCursorPosition(float rawYCursorPosition) {
-        mRawYCursorPosition = rawYCursorPosition;
-        return *this;
-    }
-
-    MotionArgsBuilder& pointer(PointerBuilder pointer) {
-        mPointers.push_back(pointer);
-        return *this;
-    }
-
-    MotionArgsBuilder& addFlag(uint32_t flags) {
-        mFlags |= flags;
-        return *this;
-    }
-
-    MotionArgsBuilder& classification(MotionClassification classification) {
-        mClassification = classification;
-        return *this;
-    }
-
-    NotifyMotionArgs build() {
-        std::vector<PointerProperties> pointerProperties;
-        std::vector<PointerCoords> pointerCoords;
-        for (const PointerBuilder& pointer : mPointers) {
-            pointerProperties.push_back(pointer.buildProperties());
-            pointerCoords.push_back(pointer.buildCoords());
-        }
-
-        // Set mouse cursor position for the most common cases to avoid boilerplate.
-        if (mSource == AINPUT_SOURCE_MOUSE &&
-            !MotionEvent::isValidCursorPosition(mRawXCursorPosition, mRawYCursorPosition)) {
-            mRawXCursorPosition = pointerCoords[0].getX();
-            mRawYCursorPosition = pointerCoords[0].getY();
-        }
-
-        NotifyMotionArgs args(InputEvent::nextId(), mEventTime, /*readTime=*/mEventTime, mDeviceId,
-                              mSource, mDisplayId, mPolicyFlags, mAction, mActionButton, mFlags,
-                              AMETA_NONE, mButtonState, mClassification, /*edgeFlags=*/0,
-                              mPointers.size(), pointerProperties.data(), pointerCoords.data(),
-                              /*xPrecision=*/0, /*yPrecision=*/0, mRawXCursorPosition,
-                              mRawYCursorPosition, mDownTime, /*videoFrames=*/{});
-
-        return args;
-    }
-
-private:
-    int32_t mAction;
-    int32_t mDeviceId = DEVICE_ID;
-    int32_t mSource;
-    nsecs_t mDownTime;
-    nsecs_t mEventTime;
-    int32_t mDisplayId{ADISPLAY_ID_DEFAULT};
-    int32_t mPolicyFlags = DEFAULT_POLICY_FLAGS;
-    int32_t mActionButton{0};
-    int32_t mButtonState{0};
-    int32_t mFlags{0};
-    MotionClassification mClassification{MotionClassification::NONE};
-    float mRawXCursorPosition{AMOTION_EVENT_INVALID_CURSOR_POSITION};
-    float mRawYCursorPosition{AMOTION_EVENT_INVALID_CURSOR_POSITION};
-
-    std::vector<PointerBuilder> mPointers;
-};
-
 static InputEventInjectionResult injectMotionEvent(
         const std::unique_ptr<InputDispatcher>& dispatcher, const MotionEvent& event,
         std::chrono::milliseconds injectionTimeout = INJECT_EVENT_TIMEOUT,
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 bfb371f..327513d 100644
--- a/services/inputflinger/tests/InputReader_test.cpp
+++ b/services/inputflinger/tests/InputReader_test.cpp
@@ -11070,6 +11070,101 @@
     ASSERT_EQ(controller.getLightColor(lights[0].id).value_or(-1), LIGHT_BRIGHTNESS);
 }
 
+TEST_F(LightControllerTest, Ignore_MonoLight_WithPreferredBacklightLevels) {
+    RawLightInfo infoMono = {.id = 1,
+                             .name = "mono_light",
+                             .maxBrightness = 255,
+                             .flags = InputLightClass::BRIGHTNESS,
+                             .path = ""};
+    mFakeEventHub->addRawLightInfo(infoMono.id, std::move(infoMono));
+    mFakeEventHub->addConfigurationProperty(EVENTHUB_ID, "keyboard.backlight.brightnessLevels",
+                                            "0,100,200");
+
+    PeripheralController& controller = addControllerAndConfigure<PeripheralController>();
+    std::list<NotifyArgs> unused =
+            mDevice->configure(ARBITRARY_TIME, mFakePolicy->getReaderConfiguration(),
+                               /*changes=*/{});
+
+    InputDeviceInfo info;
+    controller.populateDeviceInfo(&info);
+    std::vector<InputDeviceLightInfo> lights = info.getLights();
+    ASSERT_EQ(1U, lights.size());
+    ASSERT_EQ(0U, lights[0].preferredBrightnessLevels.size());
+}
+
+TEST_F(LightControllerTest, KeyboardBacklight_WithNoPreferredBacklightLevels) {
+    RawLightInfo infoMono = {.id = 1,
+                             .name = "mono_keyboard_backlight",
+                             .maxBrightness = 255,
+                             .flags = InputLightClass::BRIGHTNESS |
+                                     InputLightClass::KEYBOARD_BACKLIGHT,
+                             .path = ""};
+    mFakeEventHub->addRawLightInfo(infoMono.id, std::move(infoMono));
+
+    PeripheralController& controller = addControllerAndConfigure<PeripheralController>();
+    std::list<NotifyArgs> unused =
+            mDevice->configure(ARBITRARY_TIME, mFakePolicy->getReaderConfiguration(),
+                               /*changes=*/{});
+
+    InputDeviceInfo info;
+    controller.populateDeviceInfo(&info);
+    std::vector<InputDeviceLightInfo> lights = info.getLights();
+    ASSERT_EQ(1U, lights.size());
+    ASSERT_EQ(0U, lights[0].preferredBrightnessLevels.size());
+}
+
+TEST_F(LightControllerTest, KeyboardBacklight_WithPreferredBacklightLevels) {
+    RawLightInfo infoMono = {.id = 1,
+                             .name = "mono_keyboard_backlight",
+                             .maxBrightness = 255,
+                             .flags = InputLightClass::BRIGHTNESS |
+                                     InputLightClass::KEYBOARD_BACKLIGHT,
+                             .path = ""};
+    mFakeEventHub->addRawLightInfo(infoMono.id, std::move(infoMono));
+    mFakeEventHub->addConfigurationProperty(EVENTHUB_ID, "keyboard.backlight.brightnessLevels",
+                                            "0,100,200");
+
+    PeripheralController& controller = addControllerAndConfigure<PeripheralController>();
+    std::list<NotifyArgs> unused =
+            mDevice->configure(ARBITRARY_TIME, mFakePolicy->getReaderConfiguration(),
+                               /*changes=*/{});
+
+    InputDeviceInfo info;
+    controller.populateDeviceInfo(&info);
+    std::vector<InputDeviceLightInfo> lights = info.getLights();
+    ASSERT_EQ(1U, lights.size());
+    ASSERT_EQ(3U, lights[0].preferredBrightnessLevels.size());
+    std::set<BrightnessLevel>::iterator it = lights[0].preferredBrightnessLevels.begin();
+    ASSERT_EQ(BrightnessLevel(0), *it);
+    std::advance(it, 1);
+    ASSERT_EQ(BrightnessLevel(100), *it);
+    std::advance(it, 1);
+    ASSERT_EQ(BrightnessLevel(200), *it);
+}
+
+TEST_F(LightControllerTest, KeyboardBacklight_WithWrongPreferredBacklightLevels) {
+    RawLightInfo infoMono = {.id = 1,
+                             .name = "mono_keyboard_backlight",
+                             .maxBrightness = 255,
+                             .flags = InputLightClass::BRIGHTNESS |
+                                     InputLightClass::KEYBOARD_BACKLIGHT,
+                             .path = ""};
+    mFakeEventHub->addRawLightInfo(infoMono.id, std::move(infoMono));
+    mFakeEventHub->addConfigurationProperty(EVENTHUB_ID, "keyboard.backlight.brightnessLevels",
+                                            "0,100,200,300,400,500");
+
+    PeripheralController& controller = addControllerAndConfigure<PeripheralController>();
+    std::list<NotifyArgs> unused =
+            mDevice->configure(ARBITRARY_TIME, mFakePolicy->getReaderConfiguration(),
+                               /*changes=*/{});
+
+    InputDeviceInfo info;
+    controller.populateDeviceInfo(&info);
+    std::vector<InputDeviceLightInfo> lights = info.getLights();
+    ASSERT_EQ(1U, lights.size());
+    ASSERT_EQ(0U, lights[0].preferredBrightnessLevels.size());
+}
+
 TEST_F(LightControllerTest, RGBLight) {
     RawLightInfo infoRed = {.id = 1,
                             .name = "red",
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/SyncQueue_test.cpp b/services/inputflinger/tests/SyncQueue_test.cpp
new file mode 100644
index 0000000..af2f961
--- /dev/null
+++ b/services/inputflinger/tests/SyncQueue_test.cpp
@@ -0,0 +1,80 @@
+/*
+ * 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 "../SyncQueue.h"
+
+#include <gtest/gtest.h>
+#include <thread>
+
+namespace android {
+
+// --- SyncQueueTest ---
+
+// Validate basic pop and push operation.
+TEST(SyncQueueTest, AddAndRemove) {
+    SyncQueue<int> queue;
+
+    queue.push(1);
+    ASSERT_EQ(queue.pop(), 1);
+
+    queue.push(3);
+    ASSERT_EQ(queue.pop(), 3);
+
+    ASSERT_EQ(std::nullopt, queue.pop());
+}
+
+// Make sure the queue maintains FIFO order.
+// Add elements and remove them, and check the order.
+TEST(SyncQueueTest, isFIFO) {
+    SyncQueue<int> queue;
+
+    constexpr int numItems = 10;
+    for (int i = 0; i < numItems; i++) {
+        queue.push(static_cast<int>(i));
+    }
+    for (int i = 0; i < numItems; i++) {
+        ASSERT_EQ(queue.pop(), static_cast<int>(i));
+    }
+}
+
+TEST(SyncQueueTest, AllowsMultipleThreads) {
+    SyncQueue<int> queue;
+
+    // Test with a large number of items to increase likelihood that threads overlap
+    constexpr int numItems = 100;
+
+    // Fill queue from a different thread
+    std::thread fillQueue([&queue]() {
+        for (int i = 0; i < numItems; i++) {
+            queue.push(static_cast<int>(i));
+        }
+    });
+
+    // Make sure all elements are received in correct order
+    for (int i = 0; i < numItems; i++) {
+        // Since popping races with the thread that's filling the queue,
+        // keep popping until we get something back
+        std::optional<int> popped;
+        do {
+            popped = queue.pop();
+        } while (!popped);
+        ASSERT_EQ(popped, static_cast<int>(i));
+    }
+
+    fillQueue.join();
+}
+
+} // namespace android
diff --git a/services/inputflinger/tests/TestInputListenerMatchers.h b/services/inputflinger/tests/TestInputListenerMatchers.h
index db6f254..01b79ca 100644
--- a/services/inputflinger/tests/TestInputListenerMatchers.h
+++ b/services/inputflinger/tests/TestInputListenerMatchers.h
@@ -23,6 +23,8 @@
 #include <gtest/gtest.h>
 #include <input/Input.h>
 
+#include "TestConstants.h"
+
 namespace android {
 
 MATCHER_P(WithMotionAction, action, "MotionEvent with specified action") {
@@ -136,6 +138,15 @@
     return fabs(argScaleFactor - factor) <= epsilon;
 }
 
+MATCHER_P(WithGestureSwipeFingerCount, count,
+          "InputEvent with specified touchpad swipe finger count") {
+    const auto argFingerCount =
+            arg.pointerCoords[0].getAxisValue(AMOTION_EVENT_AXIS_GESTURE_SWIPE_FINGER_COUNT);
+    *result_listener << "expected gesture swipe finger count " << count << " but got "
+                     << argFingerCount;
+    return fabs(argFingerCount - count) <= EPSILON;
+}
+
 MATCHER_P(WithPressure, pressure, "InputEvent with specified pressure") {
     const auto argPressure = arg.pointerCoords[0].getAxisValue(AMOTION_EVENT_AXIS_PRESSURE);
     *result_listener << "expected pressure " << pressure << ", but got " << argPressure;
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/inputflinger/tests/fuzzers/BlockingQueueFuzzer.cpp b/services/inputflinger/tests/fuzzers/BlockingQueueFuzzer.cpp
index d2595bf..e9016bb 100644
--- a/services/inputflinger/tests/fuzzers/BlockingQueueFuzzer.cpp
+++ b/services/inputflinger/tests/fuzzers/BlockingQueueFuzzer.cpp
@@ -47,12 +47,21 @@
                     filled > numPops ? filled -= numPops : filled = 0;
                 },
                 [&]() -> void {
+                    // Pops blocks if it is empty, so only pop up to num elements inserted.
+                    size_t numPops = fdp.ConsumeIntegralInRange<size_t>(0, filled);
+                    for (size_t i = 0; i < numPops; i++) {
+                        queue.popWithTimeout(
+                                std::chrono::nanoseconds{fdp.ConsumeIntegral<int64_t>()});
+                    }
+                    filled > numPops ? filled -= numPops : filled = 0;
+                },
+                [&]() -> void {
                     queue.clear();
                     filled = 0;
                 },
                 [&]() -> void {
                     int32_t eraseElement = fdp.ConsumeIntegral<int32_t>();
-                    queue.erase([&](int32_t element) {
+                    queue.erase_if([&](int32_t element) {
                         if (element == eraseElement) {
                             filled--;
                             return true;
diff --git a/services/surfaceflinger/SurfaceFlinger.cpp b/services/surfaceflinger/SurfaceFlinger.cpp
index 95911d8..61f2d54 100644
--- a/services/surfaceflinger/SurfaceFlinger.cpp
+++ b/services/surfaceflinger/SurfaceFlinger.cpp
@@ -2527,7 +2527,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.
@@ -3721,7 +3721,7 @@
     doCommitTransactions();
 }
 
-void SurfaceFlinger::updateInputFlinger(VsyncId vsyncId) {
+void SurfaceFlinger::updateInputFlinger(VsyncId vsyncId, TimePoint frameTime) {
     if (!mInputFlinger || (!mUpdateInputInfo && mInputWindowCommands.empty())) {
         return;
     }
@@ -3733,8 +3733,6 @@
     if (mUpdateInputInfo) {
         mUpdateInputInfo = false;
         updateWindowInfo = true;
-        mLastInputFlingerUpdateVsyncId = vsyncId;
-        mLastInputFlingerUpdateTimestamp = systemTime();
         buildWindowInfos(windowInfos, displayInfos);
     }
 
@@ -3756,17 +3754,18 @@
                                                       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),
+                                                                ftl::to_underlying(vsyncId),
+                                                                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.
@@ -6141,27 +6140,14 @@
     result.append("\n");
 
     result.append("Window Infos:\n");
-    StringAppendF(&result, "  input flinger update vsync id: %" PRId64 "\n",
-                  ftl::to_underlying(mLastInputFlingerUpdateVsyncId));
-    StringAppendF(&result, "  input flinger update timestamp (ns): %" PRId64 "\n",
-                  mLastInputFlingerUpdateTimestamp);
+    auto windowInfosDebug = mWindowInfosListenerInvoker->getDebugInfo();
+    StringAppendF(&result, "  max send vsync id: %" PRId64 "\n",
+                  ftl::to_underlying(windowInfosDebug.maxSendDelayVsyncId));
+    StringAppendF(&result, "  max send delay (ns): %" PRId64 " ns\n",
+                  windowInfosDebug.maxSendDelayDuration);
+    StringAppendF(&result, "  unsent messages: %" PRIu32 "\n",
+                  windowInfosDebug.pendingMessageCount);
     result.append("\n");
-
-    if (VsyncId unsentVsyncId = mWindowInfosListenerInvoker->getUnsentMessageVsyncId();
-        unsentVsyncId != VsyncId()) {
-        StringAppendF(&result, "  unsent input flinger update vsync id: %" PRId64 "\n",
-                      ftl::to_underlying(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 6b9ba8c..b7d2047 100644
--- a/services/surfaceflinger/SurfaceFlinger.h
+++ b/services/surfaceflinger/SurfaceFlinger.h
@@ -718,7 +718,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);
@@ -1250,9 +1250,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 b2885fb..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),
-                                      ftl::to_underlying(vsyncId), 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 = VsyncId();
-        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 = VsyncId();
-        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 ade607f..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);
-    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 534a8f3..8e208bc 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 84a8529..86af303 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