Merge "Add Window Infos state to dumpsys" into udc-dev
diff --git a/include/android/surface_control.h b/include/android/surface_control.h
index e4ba58a..cce2e46 100644
--- a/include/android/surface_control.h
+++ b/include/android/surface_control.h
@@ -54,6 +54,12 @@
  * The caller takes ownership of the ASurfaceControl returned and must release it
  * using ASurfaceControl_release below.
  *
+ * By default the \a ASurfaceControl will be visible and display any buffer submitted. In
+ * addition, the default buffer submission control may release and not display all buffers
+ * that are submitted before receiving a callback for the previous buffer. See
+ * \a ASurfaceTransaction_setVisibility and \a ASurfaceTransaction_setEnableBackPressure to
+ * change the default behaviors after creation.
+ *
  * Available since API level 29.
  */
 ASurfaceControl* ASurfaceControl_createFromWindow(ANativeWindow* parent, const char* debug_name)
diff --git a/libs/binder/BpBinder.cpp b/libs/binder/BpBinder.cpp
index 53852d8..8d9955d 100644
--- a/libs/binder/BpBinder.cpp
+++ b/libs/binder/BpBinder.cpp
@@ -47,7 +47,7 @@
 binder_proxy_limit_callback BpBinder::sLimitCallback;
 bool BpBinder::sBinderProxyThrottleCreate = false;
 
-static StaticString16 kDescriptorUninit(u"<uninit descriptor>");
+static StaticString16 kDescriptorUninit(u"");
 
 // Arbitrarily high value that probably distinguishes a bad behaving app
 uint32_t BpBinder::sBinderProxyCountHighWatermark = 2500;
diff --git a/libs/binder/IUidObserver.cpp b/libs/binder/IUidObserver.cpp
index d952dc7..1c35f53 100644
--- a/libs/binder/IUidObserver.cpp
+++ b/libs/binder/IUidObserver.cpp
@@ -67,9 +67,10 @@
         remote()->transact(ON_UID_STATE_CHANGED_TRANSACTION, data, &reply, IBinder::FLAG_ONEWAY);
     }
 
-    virtual void onUidProcAdjChanged(uid_t uid) {
+    virtual void onUidProcAdjChanged(uid_t uid, int32_t adj) {
         Parcel data, reply;
         data.writeInt32((int32_t)uid);
+        data.writeInt32((int32_t)adj);
         remote()->transact(ON_UID_PROC_ADJ_CHANGED_TRANSACTION, data, &reply, IBinder::FLAG_ONEWAY);
     }
 };
@@ -121,7 +122,8 @@
         case ON_UID_PROC_ADJ_CHANGED_TRANSACTION: {
             CHECK_INTERFACE(IUidObserver, data, reply);
             uid_t uid = data.readInt32();
-            onUidProcAdjChanged(uid);
+            int32_t adj = data.readInt32();
+            onUidProcAdjChanged(uid, adj);
             return NO_ERROR;
         } break;
 
diff --git a/libs/binder/TEST_MAPPING b/libs/binder/TEST_MAPPING
index 41707d4..0e8e187 100644
--- a/libs/binder/TEST_MAPPING
+++ b/libs/binder/TEST_MAPPING
@@ -92,12 +92,6 @@
       "name": "binderRpcTest"
     },
     {
-      "name": "CtsRootRollbackManagerHostTestCases"
-    },
-    {
-      "name": "StagedRollbackTest"
-    },
-    {
       "name": "binderRpcTestNoKernel"
     },
     {
diff --git a/libs/binder/include_activitymanager/binder/IUidObserver.h b/libs/binder/include_activitymanager/binder/IUidObserver.h
index 17f03a9..5ea7447 100644
--- a/libs/binder/include_activitymanager/binder/IUidObserver.h
+++ b/libs/binder/include_activitymanager/binder/IUidObserver.h
@@ -34,7 +34,7 @@
     virtual void onUidIdle(uid_t uid, bool disabled) = 0;
     virtual void onUidStateChanged(uid_t uid, int32_t procState, int64_t procStateSeq,
                                    int32_t capability) = 0;
-    virtual void onUidProcAdjChanged(uid_t uid) = 0;
+    virtual void onUidProcAdjChanged(uid_t uid, int32_t adj) = 0;
 
     enum {
         ON_UID_GONE_TRANSACTION = IBinder::FIRST_CALL_TRANSACTION,
diff --git a/libs/gui/VsyncEventData.cpp b/libs/gui/VsyncEventData.cpp
index 76c60c2..8e00c2f 100644
--- a/libs/gui/VsyncEventData.cpp
+++ b/libs/gui/VsyncEventData.cpp
@@ -23,8 +23,8 @@
 
 namespace android::gui {
 
-static_assert(VsyncEventData::kFrameTimelinesLength == 7,
-              "Must update value in DisplayEventReceiver.java#FRAME_TIMELINES_LENGTH (and here)");
+static_assert(VsyncEventData::kFrameTimelinesCapacity == 7,
+              "Must update value in DisplayEventReceiver.java#FRAME_TIMELINES_CAPACITY (and here)");
 
 int64_t VsyncEventData::preferredVsyncId() const {
     return frameTimelines[preferredFrameTimelineIndex].vsyncId;
@@ -46,11 +46,15 @@
 
     SAFE_PARCEL(parcel->readInt64, &vsync.frameInterval);
 
-    uint64_t uintPreferredFrameTimelineIndex;
-    SAFE_PARCEL(parcel->readUint64, &uintPreferredFrameTimelineIndex);
+    uint32_t uintPreferredFrameTimelineIndex;
+    SAFE_PARCEL(parcel->readUint32, &uintPreferredFrameTimelineIndex);
     vsync.preferredFrameTimelineIndex = static_cast<size_t>(uintPreferredFrameTimelineIndex);
 
-    for (int i = 0; i < VsyncEventData::kFrameTimelinesLength; i++) {
+    uint32_t uintFrameTimelinesLength;
+    SAFE_PARCEL(parcel->readUint32, &uintFrameTimelinesLength);
+    vsync.frameTimelinesLength = static_cast<size_t>(uintFrameTimelinesLength);
+
+    for (size_t i = 0; i < vsync.frameTimelinesLength; i++) {
         SAFE_PARCEL(parcel->readInt64, &vsync.frameTimelines[i].vsyncId);
         SAFE_PARCEL(parcel->readInt64, &vsync.frameTimelines[i].deadlineTimestamp);
         SAFE_PARCEL(parcel->readInt64, &vsync.frameTimelines[i].expectedPresentationTime);
@@ -60,8 +64,9 @@
 }
 status_t ParcelableVsyncEventData::writeToParcel(Parcel* parcel) const {
     SAFE_PARCEL(parcel->writeInt64, vsync.frameInterval);
-    SAFE_PARCEL(parcel->writeUint64, vsync.preferredFrameTimelineIndex);
-    for (int i = 0; i < VsyncEventData::kFrameTimelinesLength; i++) {
+    SAFE_PARCEL(parcel->writeUint32, vsync.preferredFrameTimelineIndex);
+    SAFE_PARCEL(parcel->writeUint32, vsync.frameTimelinesLength);
+    for (size_t i = 0; i < vsync.frameTimelinesLength; i++) {
         SAFE_PARCEL(parcel->writeInt64, vsync.frameTimelines[i].vsyncId);
         SAFE_PARCEL(parcel->writeInt64, vsync.frameTimelines[i].deadlineTimestamp);
         SAFE_PARCEL(parcel->writeInt64, vsync.frameTimelines[i].expectedPresentationTime);
diff --git a/libs/gui/fuzzer/libgui_displayEvent_fuzzer.cpp b/libs/gui/fuzzer/libgui_displayEvent_fuzzer.cpp
index 6d5ae49..6e4f074 100644
--- a/libs/gui/fuzzer/libgui_displayEvent_fuzzer.cpp
+++ b/libs/gui/fuzzer/libgui_displayEvent_fuzzer.cpp
@@ -51,7 +51,7 @@
             event.vsync.count = fdp->ConsumeIntegral<uint32_t>();
             event.vsync.vsyncData.frameInterval = fdp->ConsumeIntegral<uint64_t>();
             event.vsync.vsyncData.preferredFrameTimelineIndex = fdp->ConsumeIntegral<uint32_t>();
-            for (size_t idx = 0; idx < gui::VsyncEventData::kFrameTimelinesLength; ++idx) {
+            for (size_t idx = 0; idx < gui::VsyncEventData::kFrameTimelinesCapacity; ++idx) {
                 event.vsync.vsyncData.frameTimelines[idx].vsyncId = fdp->ConsumeIntegral<int64_t>();
                 event.vsync.vsyncData.frameTimelines[idx].deadlineTimestamp =
                         fdp->ConsumeIntegral<uint64_t>();
diff --git a/libs/gui/include/gui/LayerState.h b/libs/gui/include/gui/LayerState.h
index 5c88a07..a6f503e 100644
--- a/libs/gui/include/gui/LayerState.h
+++ b/libs/gui/include/gui/LayerState.h
@@ -233,9 +233,10 @@
 
     // Geometry updates.
     static constexpr uint64_t GEOMETRY_CHANGES = layer_state_t::eBufferCropChanged |
-            layer_state_t::eBufferTransformChanged | layer_state_t::eCropChanged |
-            layer_state_t::eDestinationFrameChanged | layer_state_t::eMatrixChanged |
-            layer_state_t::ePositionChanged | layer_state_t::eTransformToDisplayInverseChanged |
+            layer_state_t::eBufferTransformChanged | layer_state_t::eCornerRadiusChanged |
+            layer_state_t::eCropChanged | layer_state_t::eDestinationFrameChanged |
+            layer_state_t::eMatrixChanged | layer_state_t::ePositionChanged |
+            layer_state_t::eTransformToDisplayInverseChanged |
             layer_state_t::eTransparentRegionChanged;
 
     // Buffer and related updates.
diff --git a/libs/gui/include/gui/VsyncEventData.h b/libs/gui/include/gui/VsyncEventData.h
index dfdae21..b40a840 100644
--- a/libs/gui/include/gui/VsyncEventData.h
+++ b/libs/gui/include/gui/VsyncEventData.h
@@ -24,8 +24,8 @@
 // Plain Old Data (POD) vsync data structure. For example, it can be easily used in the
 // DisplayEventReceiver::Event union.
 struct VsyncEventData {
-    // Max amount of frame timelines is arbitrarily set to be reasonable.
-    static constexpr int64_t kFrameTimelinesLength = 7;
+    // Max capacity of frame timelines is arbitrarily set to be reasonable.
+    static constexpr int64_t kFrameTimelinesCapacity = 7;
 
     // The current frame interval in ns when this frame was scheduled.
     int64_t frameInterval;
@@ -33,6 +33,9 @@
     // Index into the frameTimelines that represents the platform's preferred frame timeline.
     uint32_t preferredFrameTimelineIndex;
 
+    // Size of frame timelines provided by the platform; max is kFrameTimelinesCapacity.
+    uint32_t frameTimelinesLength;
+
     struct alignas(8) FrameTimeline {
         // The Vsync Id corresponsing to this vsync event. This will be used to
         // populate ISurfaceComposer::setFrameTimelineVsync and
@@ -45,7 +48,7 @@
 
         // The anticipated Vsync presentation time in nanos.
         int64_t expectedPresentationTime;
-    } frameTimelines[kFrameTimelinesLength]; // Sorted possible frame timelines.
+    } frameTimelines[kFrameTimelinesCapacity]; // Sorted possible frame timelines.
 
     // Gets the preferred frame timeline's vsync ID.
     int64_t preferredVsyncId() const;
diff --git a/libs/gui/tests/DisplayEventStructLayout_test.cpp b/libs/gui/tests/DisplayEventStructLayout_test.cpp
index da88463..3949d70 100644
--- a/libs/gui/tests/DisplayEventStructLayout_test.cpp
+++ b/libs/gui/tests/DisplayEventStructLayout_test.cpp
@@ -35,6 +35,7 @@
     CHECK_OFFSET(DisplayEventReceiver::Event::VSync, count, 0);
     CHECK_OFFSET(DisplayEventReceiver::Event::VSync, vsyncData.frameInterval, 8);
     CHECK_OFFSET(DisplayEventReceiver::Event::VSync, vsyncData.preferredFrameTimelineIndex, 16);
+    CHECK_OFFSET(DisplayEventReceiver::Event::VSync, vsyncData.frameTimelinesLength, 20);
     CHECK_OFFSET(DisplayEventReceiver::Event::VSync, vsyncData.frameTimelines, 24);
     CHECK_OFFSET(DisplayEventReceiver::Event::VSync, vsyncData.frameTimelines[0].vsyncId, 24);
     CHECK_OFFSET(DisplayEventReceiver::Event::VSync, vsyncData.frameTimelines[0].deadlineTimestamp,
@@ -44,16 +45,16 @@
     // Also test the offsets of the last frame timeline. A loop is not used because the non-const
     // index cannot be used in static_assert.
     const int lastFrameTimelineOffset = /* Start of array */ 24 +
-            (VsyncEventData::kFrameTimelinesLength - 1) * /* Size of FrameTimeline */ 24;
+            (VsyncEventData::kFrameTimelinesCapacity - 1) * /* Size of FrameTimeline */ 24;
     CHECK_OFFSET(DisplayEventReceiver::Event::VSync,
-                 vsyncData.frameTimelines[VsyncEventData::kFrameTimelinesLength - 1].vsyncId,
+                 vsyncData.frameTimelines[VsyncEventData::kFrameTimelinesCapacity - 1].vsyncId,
                  lastFrameTimelineOffset);
     CHECK_OFFSET(DisplayEventReceiver::Event::VSync,
-                 vsyncData.frameTimelines[VsyncEventData::kFrameTimelinesLength - 1]
+                 vsyncData.frameTimelines[VsyncEventData::kFrameTimelinesCapacity - 1]
                          .deadlineTimestamp,
                  lastFrameTimelineOffset + 8);
     CHECK_OFFSET(DisplayEventReceiver::Event::VSync,
-                 vsyncData.frameTimelines[VsyncEventData::kFrameTimelinesLength - 1]
+                 vsyncData.frameTimelines[VsyncEventData::kFrameTimelinesCapacity - 1]
                          .expectedPresentationTime,
                  lastFrameTimelineOffset + 16);
 
diff --git a/libs/gui/tests/VsyncEventData_test.cpp b/libs/gui/tests/VsyncEventData_test.cpp
index f114522..a2138f2 100644
--- a/libs/gui/tests/VsyncEventData_test.cpp
+++ b/libs/gui/tests/VsyncEventData_test.cpp
@@ -36,6 +36,7 @@
     FrameTimeline timeline1 = FrameTimeline{4, 5, 6};
     data.vsync.frameTimelines[0] = timeline0;
     data.vsync.frameTimelines[1] = timeline1;
+    data.vsync.frameTimelinesLength = 2;
 
     Parcel p;
     data.writeToParcel(&p);
@@ -45,7 +46,8 @@
     data2.readFromParcel(&p);
     ASSERT_EQ(data.vsync.frameInterval, data2.vsync.frameInterval);
     ASSERT_EQ(data.vsync.preferredFrameTimelineIndex, data2.vsync.preferredFrameTimelineIndex);
-    for (int i = 0; i < VsyncEventData::kFrameTimelinesLength; i++) {
+    ASSERT_EQ(data.vsync.frameTimelinesLength, data2.vsync.frameTimelinesLength);
+    for (int i = 0; i < VsyncEventData::kFrameTimelinesCapacity; i++) {
         ASSERT_EQ(data.vsync.frameTimelines[i].vsyncId, data2.vsync.frameTimelines[i].vsyncId);
         ASSERT_EQ(data.vsync.frameTimelines[i].deadlineTimestamp,
                   data2.vsync.frameTimelines[i].deadlineTimestamp);
diff --git a/libs/nativedisplay/AChoreographer.cpp b/libs/nativedisplay/AChoreographer.cpp
index 66a40f1..8f005a5 100644
--- a/libs/nativedisplay/AChoreographer.cpp
+++ b/libs/nativedisplay/AChoreographer.cpp
@@ -197,7 +197,7 @@
             AChoreographerFrameCallbackData_to_ChoreographerFrameCallbackDataImpl(data);
     LOG_ALWAYS_FATAL_IF(!frameCallbackData->choreographer->inCallback(),
                         "Data is only valid in callback");
-    return VsyncEventData::kFrameTimelinesLength;
+    return frameCallbackData->vsyncEventData.frameTimelinesLength;
 }
 size_t AChoreographerFrameCallbackData_getPreferredFrameTimelineIndex(
         const AChoreographerFrameCallbackData* data) {
@@ -213,7 +213,7 @@
             AChoreographerFrameCallbackData_to_ChoreographerFrameCallbackDataImpl(data);
     LOG_ALWAYS_FATAL_IF(!frameCallbackData->choreographer->inCallback(),
                         "Data is only valid in callback");
-    LOG_ALWAYS_FATAL_IF(index >= VsyncEventData::kFrameTimelinesLength, "Index out of bounds");
+    LOG_ALWAYS_FATAL_IF(index >= VsyncEventData::kFrameTimelinesCapacity, "Index out of bounds");
     return frameCallbackData->vsyncEventData.frameTimelines[index].vsyncId;
 }
 int64_t AChoreographerFrameCallbackData_getFrameTimelineExpectedPresentationTimeNanos(
@@ -222,7 +222,7 @@
             AChoreographerFrameCallbackData_to_ChoreographerFrameCallbackDataImpl(data);
     LOG_ALWAYS_FATAL_IF(!frameCallbackData->choreographer->inCallback(),
                         "Data is only valid in callback");
-    LOG_ALWAYS_FATAL_IF(index >= VsyncEventData::kFrameTimelinesLength, "Index out of bounds");
+    LOG_ALWAYS_FATAL_IF(index >= VsyncEventData::kFrameTimelinesCapacity, "Index out of bounds");
     return frameCallbackData->vsyncEventData.frameTimelines[index].expectedPresentationTime;
 }
 int64_t AChoreographerFrameCallbackData_getFrameTimelineDeadlineNanos(
@@ -231,7 +231,7 @@
             AChoreographerFrameCallbackData_to_ChoreographerFrameCallbackDataImpl(data);
     LOG_ALWAYS_FATAL_IF(!frameCallbackData->choreographer->inCallback(),
                         "Data is only valid in callback");
-    LOG_ALWAYS_FATAL_IF(index >= VsyncEventData::kFrameTimelinesLength, "Index out of bounds");
+    LOG_ALWAYS_FATAL_IF(index >= VsyncEventData::kFrameTimelinesCapacity, "Index out of bounds");
     return frameCallbackData->vsyncEventData.frameTimelines[index].deadlineTimestamp;
 }
 
diff --git a/libs/renderengine/skia/ColorSpaces.cpp b/libs/renderengine/skia/ColorSpaces.cpp
index 37ff5df..92b01e0 100644
--- a/libs/renderengine/skia/ColorSpaces.cpp
+++ b/libs/renderengine/skia/ColorSpaces.cpp
@@ -21,6 +21,8 @@
 namespace skia {
 
 // please keep in sync with hwui/utils/Color.cpp
+// TODO: Scale by the dimming ratio here instead of in a generic 3x3 transform
+// Otherwise there may be luminance shift for e.g., HLG.
 sk_sp<SkColorSpace> toSkColorSpace(ui::Dataspace dataspace) {
     skcms_Matrix3x3 gamut;
     switch (dataspace & HAL_DATASPACE_STANDARD_MASK) {
@@ -61,13 +63,14 @@
         case HAL_DATASPACE_TRANSFER_GAMMA2_8:
             return SkColorSpace::MakeRGB({2.8f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, gamut);
         case HAL_DATASPACE_TRANSFER_ST2084:
-            return SkColorSpace::MakeRGB(SkNamedTransferFn::kPQ, gamut);
+            return SkColorSpace::MakeRGB({-2.f, -1.55522297832f, 1.86045365631f, 32 / 2523.0f,
+                                          2413 / 128.0f, -2392 / 128.0f, 8192 / 1305.0f},
+                                         gamut);
         case HAL_DATASPACE_TRANSFER_SMPTE_170M:
             return SkColorSpace::MakeRGB(SkNamedTransferFn::kRec2020, gamut);
         case HAL_DATASPACE_TRANSFER_HLG:
-            // return HLG transfer but scale by 1/12
             skcms_TransferFunction hlgFn;
-            if (skcms_TransferFunction_makeScaledHLGish(&hlgFn, 1.f / 12.f, 2.f, 2.f,
+            if (skcms_TransferFunction_makeScaledHLGish(&hlgFn, 0.314509843, 2.f, 2.f,
                                                         1.f / 0.17883277f, 0.28466892f,
                                                         0.55991073f)) {
                 return SkColorSpace::MakeRGB(hlgFn, gamut);
diff --git a/libs/renderengine/skia/SkiaRenderEngine.cpp b/libs/renderengine/skia/SkiaRenderEngine.cpp
index 2a51493..cfea85f 100644
--- a/libs/renderengine/skia/SkiaRenderEngine.cpp
+++ b/libs/renderengine/skia/SkiaRenderEngine.cpp
@@ -913,12 +913,10 @@
             continue;
         }
 
-        // If we need to map to linear space or color management is disabled, then mark the source
-        // image with the same colorspace as the destination surface so that Skia's color
-        // management is a no-op.
-        const ui::Dataspace layerDataspace = (!mUseColorManagement || requiresLinearEffect)
-                ? display.outputDataspace
-                : layer.sourceDataspace;
+        // If color management is disabled, then mark the source image with the same colorspace as
+        // the destination surface so that Skia's color management is a no-op.
+        const ui::Dataspace layerDataspace =
+                !mUseColorManagement ? display.outputDataspace : layer.sourceDataspace;
 
         SkPaint paint;
         if (layer.source.buffer.buffer) {
diff --git a/libs/shaders/include/shaders/shaders.h b/libs/shaders/include/shaders/shaders.h
index 42b0cc1..5a4aaab 100644
--- a/libs/shaders/include/shaders/shaders.h
+++ b/libs/shaders/include/shaders/shaders.h
@@ -51,23 +51,20 @@
     // Input dataspace of the source colors.
     const ui::Dataspace inputDataspace = ui::Dataspace::SRGB;
 
-    // Working dataspace for the output surface, for conversion from linear space.
+    // Working dataspace for the output surface.
     const ui::Dataspace outputDataspace = ui::Dataspace::SRGB;
 
     // Sets whether alpha premultiplication must be undone.
     // This is required if the source colors use premultiplied alpha and is not opaque.
     const bool undoPremultipliedAlpha = false;
 
-    // "Fake" dataspace of the source colors. This is used for applying an EOTF to compute linear
-    // RGB. This is used when Skia is expected to color manage the input image based on the
-    // dataspace of the provided source image and destination surface. SkRuntimeEffects use the
-    // destination color space as the working color space. RenderEngine deliberately sets the color
-    // space for input images and destination surfaces to be the same whenever LinearEffects are
-    // expected to be used so that color-management is controlled by RenderEngine, but other users
-    // of a LinearEffect may not be able to control the color space of the images and surfaces. So
-    // fakeInputDataspace is used to essentially masquerade the input dataspace to be the output
-    // dataspace for correct conversion to linear colors.
-    ui::Dataspace fakeInputDataspace = ui::Dataspace::UNKNOWN;
+    // "Fake" dataspace of the destination colors. This is used for applying an OETF to compute
+    // non-linear RGB. This is used when Skia is expected to color manage the input image based on
+    // the dataspace of the provided source image and destination surface. Some use-cases in
+    // RenderEngine expect to apply a different OETF than what is expected by Skia. As in,
+    // RenderEngine will color manage to a custom destination and "cast" the result to Skia's
+    // working space.
+    ui::Dataspace fakeOutputDataspace = ui::Dataspace::UNKNOWN;
 
     enum SkSLType { Shader, ColorFilter };
     SkSLType type = Shader;
@@ -76,7 +73,7 @@
 static inline bool operator==(const LinearEffect& lhs, const LinearEffect& rhs) {
     return lhs.inputDataspace == rhs.inputDataspace && lhs.outputDataspace == rhs.outputDataspace &&
             lhs.undoPremultipliedAlpha == rhs.undoPremultipliedAlpha &&
-            lhs.fakeInputDataspace == rhs.fakeInputDataspace;
+            lhs.fakeOutputDataspace == rhs.fakeOutputDataspace;
 }
 
 struct LinearEffectHasher {
@@ -89,7 +86,7 @@
         size_t result = std::hash<ui::Dataspace>{}(le.inputDataspace);
         result = HashCombine(result, std::hash<ui::Dataspace>{}(le.outputDataspace));
         result = HashCombine(result, std::hash<bool>{}(le.undoPremultipliedAlpha));
-        return HashCombine(result, std::hash<ui::Dataspace>{}(le.fakeInputDataspace));
+        return HashCombine(result, std::hash<ui::Dataspace>{}(le.fakeOutputDataspace));
     }
 };
 
@@ -99,10 +96,6 @@
 // 2. Apply color transform matrices in linear space
 std::string buildLinearEffectSkSL(const LinearEffect& linearEffect);
 
-// Generates a shader string that applies color transforms in linear space.
-// This is intended to be plugged into an SkColorFilter
-std::string buildLinearEffectSkSLForColorFilter(const LinearEffect& linearEffect);
-
 // Generates a list of uniforms to set on the LinearEffect shader above.
 std::vector<tonemap::ShaderUniform> buildLinearEffectUniforms(
         const LinearEffect& linearEffect, const mat4& colorTransform, float maxDisplayLuminance,
diff --git a/libs/shaders/shaders.cpp b/libs/shaders/shaders.cpp
index 19518ea..c85517a 100644
--- a/libs/shaders/shaders.cpp
+++ b/libs/shaders/shaders.cpp
@@ -33,187 +33,46 @@
     return static_cast<aidl::android::hardware::graphics::common::Dataspace>(dataspace);
 }
 
-void generateEOTF(ui::Dataspace dataspace, std::string& shader) {
-    switch (dataspace & HAL_DATASPACE_TRANSFER_MASK) {
-        case HAL_DATASPACE_TRANSFER_ST2084:
-            shader.append(R"(
-
-                float3 EOTF(float3 color) {
-                    float m1 = (2610.0 / 4096.0) / 4.0;
-                    float m2 = (2523.0 / 4096.0) * 128.0;
-                    float c1 = (3424.0 / 4096.0);
-                    float c2 = (2413.0 / 4096.0) * 32.0;
-                    float c3 = (2392.0 / 4096.0) * 32.0;
-
-                    float3 tmp = pow(clamp(color, 0.0, 1.0), 1.0 / float3(m2));
-                    tmp = max(tmp - c1, 0.0) / (c2 - c3 * tmp);
-                    return pow(tmp, 1.0 / float3(m1));
-                }
-            )");
-            break;
-        case HAL_DATASPACE_TRANSFER_HLG:
-            shader.append(R"(
-                float EOTF_channel(float channel) {
-                    const float a = 0.17883277;
-                    const float b = 0.28466892;
-                    const float c = 0.55991073;
-                    return channel <= 0.5 ? channel * channel / 3.0 :
-                            (exp((channel - c) / a) + b) / 12.0;
-                }
-
-                float3 EOTF(float3 color) {
-                    return float3(EOTF_channel(color.r), EOTF_channel(color.g),
-                            EOTF_channel(color.b));
-                }
-            )");
-            break;
-        case HAL_DATASPACE_TRANSFER_LINEAR:
-            shader.append(R"(
-                float3 EOTF(float3 color) {
-                    return color;
-                }
-            )");
-            break;
-        case HAL_DATASPACE_TRANSFER_SMPTE_170M:
-            shader.append(R"(
-
-                float EOTF_sRGB(float srgb) {
-                    return srgb <= 0.08125 ? srgb / 4.50 : pow((srgb + 0.099) / 1.099, 1 / 0.45);
-                }
-
-                float3 EOTF_sRGB(float3 srgb) {
-                    return float3(EOTF_sRGB(srgb.r), EOTF_sRGB(srgb.g), EOTF_sRGB(srgb.b));
-                }
-
-                float3 EOTF(float3 srgb) {
-                    return sign(srgb.rgb) * EOTF_sRGB(abs(srgb.rgb));
-                }
-            )");
-            break;
-        case HAL_DATASPACE_TRANSFER_GAMMA2_2:
-            shader.append(R"(
-
-                float EOTF_sRGB(float srgb) {
-                    return pow(srgb, 2.2);
-                }
-
-                float3 EOTF_sRGB(float3 srgb) {
-                    return float3(EOTF_sRGB(srgb.r), EOTF_sRGB(srgb.g), EOTF_sRGB(srgb.b));
-                }
-
-                float3 EOTF(float3 srgb) {
-                    return sign(srgb.rgb) * EOTF_sRGB(abs(srgb.rgb));
-                }
-            )");
-            break;
-        case HAL_DATASPACE_TRANSFER_GAMMA2_6:
-            shader.append(R"(
-
-                float EOTF_sRGB(float srgb) {
-                    return pow(srgb, 2.6);
-                }
-
-                float3 EOTF_sRGB(float3 srgb) {
-                    return float3(EOTF_sRGB(srgb.r), EOTF_sRGB(srgb.g), EOTF_sRGB(srgb.b));
-                }
-
-                float3 EOTF(float3 srgb) {
-                    return sign(srgb.rgb) * EOTF_sRGB(abs(srgb.rgb));
-                }
-            )");
-            break;
-        case HAL_DATASPACE_TRANSFER_GAMMA2_8:
-            shader.append(R"(
-
-                float EOTF_sRGB(float srgb) {
-                    return pow(srgb, 2.8);
-                }
-
-                float3 EOTF_sRGB(float3 srgb) {
-                    return float3(EOTF_sRGB(srgb.r), EOTF_sRGB(srgb.g), EOTF_sRGB(srgb.b));
-                }
-
-                float3 EOTF(float3 srgb) {
-                    return sign(srgb.rgb) * EOTF_sRGB(abs(srgb.rgb));
-                }
-            )");
-            break;
-        case HAL_DATASPACE_TRANSFER_SRGB:
-        default:
-            shader.append(R"(
-
-                float EOTF_sRGB(float srgb) {
-                    return srgb <= 0.04045 ? srgb / 12.92 : pow((srgb + 0.055) / 1.055, 2.4);
-                }
-
-                float3 EOTF_sRGB(float3 srgb) {
-                    return float3(EOTF_sRGB(srgb.r), EOTF_sRGB(srgb.g), EOTF_sRGB(srgb.b));
-                }
-
-                float3 EOTF(float3 srgb) {
-                    return sign(srgb.rgb) * EOTF_sRGB(abs(srgb.rgb));
-                }
-            )");
-            break;
-    }
-}
-
 void generateXYZTransforms(std::string& shader) {
     shader.append(R"(
-        uniform float4x4 in_rgbToXyz;
-        uniform float4x4 in_xyzToRgb;
+        uniform float3x3 in_rgbToXyz;
+        uniform float3x3 in_xyzToSrcRgb;
+        uniform float4x4 in_colorTransform;
         float3 ToXYZ(float3 rgb) {
-            return (in_rgbToXyz * float4(rgb, 1.0)).rgb;
+            return in_rgbToXyz * rgb;
         }
 
-        float3 ToRGB(float3 xyz) {
-            return clamp((in_xyzToRgb * float4(xyz, 1.0)).rgb, 0.0, 1.0);
+        float3 ToSrcRGB(float3 xyz) {
+            return in_xyzToSrcRgb * xyz;
+        }
+
+        float3 ApplyColorTransform(float3 rgb) {
+            return (in_colorTransform * float4(rgb, 1.0)).rgb;
         }
     )");
 }
 
-// Conversion from relative light to absolute light (maps from [0, 1] to [0, maxNits])
-void generateLuminanceScalesForOOTF(ui::Dataspace inputDataspace, ui::Dataspace outputDataspace,
-                                    std::string& shader) {
+// Conversion from relative light to absolute light
+// Note that 1.0 == 203 nits.
+void generateLuminanceScalesForOOTF(ui::Dataspace inputDataspace, std::string& shader) {
     switch (inputDataspace & HAL_DATASPACE_TRANSFER_MASK) {
-        case HAL_DATASPACE_TRANSFER_ST2084:
-            shader.append(R"(
-                    float3 ScaleLuminance(float3 xyz) {
-                        return xyz * 10000.0;
-                    }
-                )");
-            break;
         case HAL_DATASPACE_TRANSFER_HLG:
+            // BT. 2408 says that a signal level of 0.75 == 203 nits for HLG, but that's after
+            // applying OOTF. But we haven't applied OOTF yet, so we need to scale by a different
+            // constant instead.
             shader.append(R"(
-                    float3 ScaleLuminance(float3 xyz) {
-                        return xyz * 1000.0;
-                    }
-                )");
+                float3 ScaleLuminance(float3 xyz) {
+                    return xyz * 264.96;
+                }
+            )");
             break;
         default:
-            // Input is SDR so map to its white point luminance
-            switch (outputDataspace & HAL_DATASPACE_TRANSFER_MASK) {
-                // Max HLG output is nominally 1000 nits, but BT. 2100-2 allows
-                // for gamma correcting the HLG OOTF for displays with a different
-                // dynamic range. Scale to 1000 nits to apply an inverse OOTF against
-                // a reference display correctly.
-                // TODO: Use knowledge of the dimming ratio here to prevent
-                // unintended gamma shaft.
-                case HAL_DATASPACE_TRANSFER_HLG:
-                    shader.append(R"(
-                            float3 ScaleLuminance(float3 xyz) {
-                                return xyz * 1000.0;
-                            }
-                        )");
-                    break;
-                default:
-                    shader.append(R"(
-                            float3 ScaleLuminance(float3 xyz) {
-                                return xyz * in_libtonemap_displayMaxLuminance;
-                            }
-                        )");
-                    break;
-            }
+            shader.append(R"(
+                float3 ScaleLuminance(float3 xyz) {
+                    return xyz * 203.0;
+                }
+            )");
+            break;
     }
 }
 
@@ -224,17 +83,17 @@
     switch (outputDataspace & HAL_DATASPACE_TRANSFER_MASK) {
         case HAL_DATASPACE_TRANSFER_ST2084:
             shader.append(R"(
-                    float3 NormalizeLuminance(float3 xyz) {
-                        return xyz / 10000.0;
-                    }
-                )");
+                float3 NormalizeLuminance(float3 xyz) {
+                    return xyz / 203.0;
+                }
+            )");
             break;
         case HAL_DATASPACE_TRANSFER_HLG:
             switch (inputDataspace & HAL_DATASPACE_TRANSFER_MASK) {
                 case HAL_DATASPACE_TRANSFER_HLG:
                     shader.append(R"(
                             float3 NormalizeLuminance(float3 xyz) {
-                                return xyz / 1000.0;
+                                return xyz / 264.96;
                             }
                         )");
                     break;
@@ -242,24 +101,43 @@
                     // Transcoding to HLG requires applying the inverse OOTF
                     // with the expectation that the OOTF is then applied during
                     // tonemapping downstream.
+                    // BT. 2100-2 operates on normalized luminances, so renormalize to the input to
+                    // correctly adjust gamma.
+                    // Note that following BT. 2408 for HLG OETF actually maps 0.75 == ~264.96 nits,
+                    // rather than 203 nits, because 203 nits == OOTF(invOETF(0.75)), so even though
+                    // we originally scaled by 203 nits we need to re-normalize to 264.96 nits when
+                    // converting to the correct brightness range.
                     shader.append(R"(
                             float3 NormalizeLuminance(float3 xyz) {
-                                // BT. 2100-2 operates on normalized luminances,
-                                // so renormalize to the input
-                                float ootfGain = pow(xyz.y / 1000.0, -0.2 / 1.2) / 1000.0;
-                                return xyz * ootfGain;
+                                float ootfGain = pow(xyz.y / 1000.0, -0.2 / 1.2);
+                                return xyz * ootfGain / 264.96;
                             }
                         )");
                     break;
             }
             break;
         default:
-            shader.append(R"(
-                    float3 NormalizeLuminance(float3 xyz) {
-                        return xyz / in_libtonemap_displayMaxLuminance;
-                    }
-                )");
-            break;
+            switch (inputDataspace & HAL_DATASPACE_TRANSFER_MASK) {
+                case HAL_DATASPACE_TRANSFER_HLG:
+                case HAL_DATASPACE_TRANSFER_ST2084:
+                    // libtonemap outputs a range [0, in_libtonemap_displayMaxLuminance], so
+                    // normalize back to [0, 1] when the output is SDR.
+                    shader.append(R"(
+                        float3 NormalizeLuminance(float3 xyz) {
+                            return xyz / in_libtonemap_displayMaxLuminance;
+                        }
+                    )");
+                    break;
+                default:
+                    // Otherwise normalize back down to the range [0, 1]
+                    // TODO: get this working for extended range outputs
+                    shader.append(R"(
+                        float3 NormalizeLuminance(float3 xyz) {
+                            return xyz / 203.0;
+                        }
+                    )");
+                    break;
+            }
     }
 }
 
@@ -270,145 +148,34 @@
                                                           toAidlDataspace(outputDataspace))
                           .c_str());
 
-    generateLuminanceScalesForOOTF(inputDataspace, outputDataspace, shader);
+    generateLuminanceScalesForOOTF(inputDataspace, shader);
     generateLuminanceNormalizationForOOTF(inputDataspace, outputDataspace, shader);
 
+    // Some tonemappers operate on CIE luminance, other tonemappers operate on linear rgb
+    // luminance in the source gamut.
     shader.append(R"(
-            float3 OOTF(float3 linearRGB, float3 xyz) {
+            float3 OOTF(float3 linearRGB) {
                 float3 scaledLinearRGB = ScaleLuminance(linearRGB);
-                float3 scaledXYZ = ScaleLuminance(xyz);
+                float3 scaledXYZ = ToXYZ(scaledLinearRGB);
 
-                float gain = libtonemap_LookupTonemapGain(scaledLinearRGB, scaledXYZ);
+                float gain = libtonemap_LookupTonemapGain(ToSrcRGB(scaledXYZ), scaledXYZ);
 
                 return NormalizeLuminance(scaledXYZ * gain);
             }
         )");
 }
 
-void generateOETF(ui::Dataspace dataspace, std::string& shader) {
-    switch (dataspace & HAL_DATASPACE_TRANSFER_MASK) {
-        case HAL_DATASPACE_TRANSFER_ST2084:
-            shader.append(R"(
-
-                float3 OETF(float3 xyz) {
-                    float m1 = (2610.0 / 4096.0) / 4.0;
-                    float m2 = (2523.0 / 4096.0) * 128.0;
-                    float c1 = (3424.0 / 4096.0);
-                    float c2 = (2413.0 / 4096.0) * 32.0;
-                    float c3 = (2392.0 / 4096.0) * 32.0;
-
-                    float3 tmp = pow(xyz, float3(m1));
-                    tmp = (c1 + c2 * tmp) / (1.0 + c3 * tmp);
-                    return pow(tmp, float3(m2));
-                }
-            )");
-            break;
-        case HAL_DATASPACE_TRANSFER_HLG:
-            shader.append(R"(
-                float OETF_channel(float channel) {
-                    const float a = 0.17883277;
-                    const float b = 0.28466892;
-                    const float c = 0.55991073;
-                    return channel <= 1.0 / 12.0 ? sqrt(3.0 * channel) :
-                            a * log(12.0 * channel - b) + c;
-                }
-
-                float3 OETF(float3 linear) {
-                    return float3(OETF_channel(linear.r), OETF_channel(linear.g),
-                            OETF_channel(linear.b));
-                }
-            )");
-            break;
-        case HAL_DATASPACE_TRANSFER_LINEAR:
-            shader.append(R"(
-                float3 OETF(float3 linear) {
-                    return linear;
-                }
-            )");
-            break;
-        case HAL_DATASPACE_TRANSFER_SMPTE_170M:
-            shader.append(R"(
-                float OETF_sRGB(float linear) {
-                    return linear <= 0.018 ?
-                            linear * 4.50 : (pow(linear, 0.45) * 1.099) - 0.099;
-                }
-
-                float3 OETF_sRGB(float3 linear) {
-                    return float3(OETF_sRGB(linear.r), OETF_sRGB(linear.g), OETF_sRGB(linear.b));
-                }
-
-                float3 OETF(float3 linear) {
-                    return sign(linear.rgb) * OETF_sRGB(abs(linear.rgb));
-                }
-            )");
-            break;
-        case HAL_DATASPACE_TRANSFER_GAMMA2_2:
-            shader.append(R"(
-                float OETF_sRGB(float linear) {
-                    return pow(linear, (1.0 / 2.2));
-                }
-
-                float3 OETF_sRGB(float3 linear) {
-                    return float3(OETF_sRGB(linear.r), OETF_sRGB(linear.g), OETF_sRGB(linear.b));
-                }
-
-                float3 OETF(float3 linear) {
-                    return sign(linear.rgb) * OETF_sRGB(abs(linear.rgb));
-                }
-            )");
-            break;
-        case HAL_DATASPACE_TRANSFER_GAMMA2_6:
-            shader.append(R"(
-                float OETF_sRGB(float linear) {
-                    return pow(linear, (1.0 / 2.6));
-                }
-
-                float3 OETF_sRGB(float3 linear) {
-                    return float3(OETF_sRGB(linear.r), OETF_sRGB(linear.g), OETF_sRGB(linear.b));
-                }
-
-                float3 OETF(float3 linear) {
-                    return sign(linear.rgb) * OETF_sRGB(abs(linear.rgb));
-                }
-            )");
-            break;
-        case HAL_DATASPACE_TRANSFER_GAMMA2_8:
-            shader.append(R"(
-                float OETF_sRGB(float linear) {
-                    return pow(linear, (1.0 / 2.8));
-                }
-
-                float3 OETF_sRGB(float3 linear) {
-                    return float3(OETF_sRGB(linear.r), OETF_sRGB(linear.g), OETF_sRGB(linear.b));
-                }
-
-                float3 OETF(float3 linear) {
-                    return sign(linear.rgb) * OETF_sRGB(abs(linear.rgb));
-                }
-            )");
-            break;
-        case HAL_DATASPACE_TRANSFER_SRGB:
-        default:
-            shader.append(R"(
-                float OETF_sRGB(float linear) {
-                    return linear <= 0.0031308 ?
-                            linear * 12.92 : (pow(linear, 1.0 / 2.4) * 1.055) - 0.055;
-                }
-
-                float3 OETF_sRGB(float3 linear) {
-                    return float3(OETF_sRGB(linear.r), OETF_sRGB(linear.g), OETF_sRGB(linear.b));
-                }
-
-                float3 OETF(float3 linear) {
-                    return sign(linear.rgb) * OETF_sRGB(abs(linear.rgb));
-                }
-            )");
-            break;
-    }
+void generateOETF(std::string& shader) {
+    // Only support gamma 2.2 for now
+    shader.append(R"(
+        float OETF(float3 linear) {
+            return sign(linear) * pow(abs(linear), (1.0 / 2.2));
+        }
+    )");
 }
 
 void generateEffectiveOOTF(bool undoPremultipliedAlpha, LinearEffect::SkSLType type,
-                           std::string& shader) {
+                           bool needsCustomOETF, std::string& shader) {
     switch (type) {
         case LinearEffect::SkSLType::ColorFilter:
             shader.append(R"(
@@ -429,11 +196,19 @@
             c.rgb = c.rgb / (c.a + 0.0019);
         )");
     }
+    // We are using linear sRGB as a working space, with 1.0 == 203 nits
     shader.append(R"(
-        float3 linearRGB = EOTF(c.rgb);
-        float3 xyz = ToXYZ(linearRGB);
-        c.rgb = OETF(ToRGB(OOTF(linearRGB, xyz)));
+        c.rgb = ApplyColorTransform(OOTF(toLinearSrgb(c.rgb)));
     )");
+    if (needsCustomOETF) {
+        shader.append(R"(
+            c.rgb = OETF(c.rgb);
+        )");
+    } else {
+        shader.append(R"(
+            c.rgb = fromLinearSrgb(c.rgb);
+        )");
+    }
     if (undoPremultipliedAlpha) {
         shader.append(R"(
             c.rgb = c.rgb * (c.a + 0.0019);
@@ -445,33 +220,6 @@
     )");
 }
 
-// please keep in sync with toSkColorSpace function in renderengine/skia/ColorSpaces.cpp
-ColorSpace toColorSpace(ui::Dataspace dataspace) {
-    switch (dataspace & HAL_DATASPACE_STANDARD_MASK) {
-        case HAL_DATASPACE_STANDARD_BT709:
-            return ColorSpace::sRGB();
-        case HAL_DATASPACE_STANDARD_DCI_P3:
-            return ColorSpace::DisplayP3();
-        case HAL_DATASPACE_STANDARD_BT2020:
-        case HAL_DATASPACE_STANDARD_BT2020_CONSTANT_LUMINANCE:
-            return ColorSpace::BT2020();
-        case HAL_DATASPACE_STANDARD_ADOBE_RGB:
-            return ColorSpace::AdobeRGB();
-        // TODO(b/208290320): BT601 format and variants return different primaries
-        case HAL_DATASPACE_STANDARD_BT601_625:
-        case HAL_DATASPACE_STANDARD_BT601_625_UNADJUSTED:
-        case HAL_DATASPACE_STANDARD_BT601_525:
-        case HAL_DATASPACE_STANDARD_BT601_525_UNADJUSTED:
-        // TODO(b/208290329): BT407M format returns different primaries
-        case HAL_DATASPACE_STANDARD_BT470M:
-        // TODO(b/208290904): FILM format returns different primaries
-        case HAL_DATASPACE_STANDARD_FILM:
-        case HAL_DATASPACE_STANDARD_UNSPECIFIED:
-        default:
-            return ColorSpace::sRGB();
-    }
-}
-
 template <typename T, std::enable_if_t<std::is_trivially_copyable<T>::value, bool> = true>
 std::vector<uint8_t> buildUniformValue(T value) {
     std::vector<uint8_t> result;
@@ -484,17 +232,45 @@
 
 std::string buildLinearEffectSkSL(const LinearEffect& linearEffect) {
     std::string shaderString;
-    generateEOTF(linearEffect.fakeInputDataspace == ui::Dataspace::UNKNOWN
-                         ? linearEffect.inputDataspace
-                         : linearEffect.fakeInputDataspace,
-                 shaderString);
     generateXYZTransforms(shaderString);
     generateOOTF(linearEffect.inputDataspace, linearEffect.outputDataspace, shaderString);
-    generateOETF(linearEffect.outputDataspace, shaderString);
-    generateEffectiveOOTF(linearEffect.undoPremultipliedAlpha, linearEffect.type, shaderString);
+
+    const bool needsCustomOETF = (linearEffect.fakeOutputDataspace & HAL_DATASPACE_TRANSFER_MASK) ==
+            HAL_DATASPACE_TRANSFER_GAMMA2_2;
+    if (needsCustomOETF) {
+        generateOETF(shaderString);
+    }
+    generateEffectiveOOTF(linearEffect.undoPremultipliedAlpha, linearEffect.type, needsCustomOETF,
+                          shaderString);
     return shaderString;
 }
 
+ColorSpace toColorSpace(ui::Dataspace dataspace) {
+    switch (dataspace & HAL_DATASPACE_STANDARD_MASK) {
+        case HAL_DATASPACE_STANDARD_BT709:
+            return ColorSpace::sRGB();
+        case HAL_DATASPACE_STANDARD_DCI_P3:
+            return ColorSpace::DisplayP3();
+        case HAL_DATASPACE_STANDARD_BT2020:
+        case HAL_DATASPACE_STANDARD_BT2020_CONSTANT_LUMINANCE:
+            return ColorSpace::BT2020();
+        case HAL_DATASPACE_STANDARD_ADOBE_RGB:
+            return ColorSpace::AdobeRGB();
+            // TODO(b/208290320): BT601 format and variants return different primaries
+        case HAL_DATASPACE_STANDARD_BT601_625:
+        case HAL_DATASPACE_STANDARD_BT601_625_UNADJUSTED:
+        case HAL_DATASPACE_STANDARD_BT601_525:
+        case HAL_DATASPACE_STANDARD_BT601_525_UNADJUSTED:
+            // TODO(b/208290329): BT407M format returns different primaries
+        case HAL_DATASPACE_STANDARD_BT470M:
+            // TODO(b/208290904): FILM format returns different primaries
+        case HAL_DATASPACE_STANDARD_FILM:
+        case HAL_DATASPACE_STANDARD_UNSPECIFIED:
+        default:
+            return ColorSpace::sRGB();
+    }
+}
+
 // Generates a list of uniforms to set on the LinearEffect shader above.
 std::vector<tonemap::ShaderUniform> buildLinearEffectUniforms(
         const LinearEffect& linearEffect, const mat4& colorTransform, float maxDisplayLuminance,
@@ -502,23 +278,24 @@
         aidl::android::hardware::graphics::composer3::RenderIntent renderIntent) {
     std::vector<tonemap::ShaderUniform> uniforms;
 
-    const ui::Dataspace inputDataspace = linearEffect.fakeInputDataspace == ui::Dataspace::UNKNOWN
-            ? linearEffect.inputDataspace
-            : linearEffect.fakeInputDataspace;
+    auto inputColorSpace = toColorSpace(linearEffect.inputDataspace);
+    auto outputColorSpace = toColorSpace(linearEffect.outputDataspace);
 
-    if (inputDataspace == linearEffect.outputDataspace) {
-        uniforms.push_back({.name = "in_rgbToXyz", .value = buildUniformValue<mat4>(mat4())});
-        uniforms.push_back(
-                {.name = "in_xyzToRgb", .value = buildUniformValue<mat4>(colorTransform)});
-    } else {
-        ColorSpace inputColorSpace = toColorSpace(inputDataspace);
-        ColorSpace outputColorSpace = toColorSpace(linearEffect.outputDataspace);
-        uniforms.push_back({.name = "in_rgbToXyz",
-                            .value = buildUniformValue<mat4>(mat4(inputColorSpace.getRGBtoXYZ()))});
-        uniforms.push_back({.name = "in_xyzToRgb",
-                            .value = buildUniformValue<mat4>(
-                                    colorTransform * mat4(outputColorSpace.getXYZtoRGB()))});
-    }
+    uniforms.push_back(
+            {.name = "in_rgbToXyz",
+             .value = buildUniformValue<mat3>(ColorSpace::linearExtendedSRGB().getRGBtoXYZ())});
+    uniforms.push_back({.name = "in_xyzToSrcRgb",
+                        .value = buildUniformValue<mat3>(inputColorSpace.getXYZtoRGB())});
+    // Transforms xyz colors to linear source colors, then applies the color transform, then
+    // transforms to linear extended RGB for skia to color manage.
+    uniforms.push_back({.name = "in_colorTransform",
+                        .value = buildUniformValue<mat4>(
+                                mat4(ColorSpace::linearExtendedSRGB().getXYZtoRGB()) *
+                                // TODO: the color transform ideally should be applied
+                                // in the source colorspace, but doing that breaks
+                                // renderengine tests
+                                mat4(outputColorSpace.getRGBtoXYZ()) * colorTransform *
+                                mat4(outputColorSpace.getXYZtoRGB()))});
 
     tonemap::Metadata metadata{.displayMaxLuminance = maxDisplayLuminance,
                                // If the input luminance is unknown, use display luminance (aka,
diff --git a/libs/shaders/tests/shaders_test.cpp b/libs/shaders/tests/shaders_test.cpp
index d45fb24..ba8bed2 100644
--- a/libs/shaders/tests/shaders_test.cpp
+++ b/libs/shaders/tests/shaders_test.cpp
@@ -35,6 +35,10 @@
     return arg.name == name && arg.value == value;
 }
 
+MATCHER_P(UniformNameEq, name, "") {
+    return arg.name == name;
+}
+
 template <typename T, std::enable_if_t<std::is_trivially_copyable<T>::value, bool> = true>
 std::vector<uint8_t> buildUniformValue(T value) {
     std::vector<uint8_t> result;
@@ -49,50 +53,44 @@
     shaders::LinearEffect effect =
             shaders::LinearEffect{.inputDataspace = ui::Dataspace::V0_SRGB_LINEAR,
                                   .outputDataspace = ui::Dataspace::V0_SRGB_LINEAR,
-                                  .fakeInputDataspace = ui::Dataspace::UNKNOWN};
+                                  .fakeOutputDataspace = ui::Dataspace::UNKNOWN};
 
     mat4 colorTransform = mat4::scale(vec4(.9, .9, .9, 1.));
     auto uniforms =
             shaders::buildLinearEffectUniforms(effect, colorTransform, 1.f, 1.f, 1.f, nullptr,
                                                aidl::android::hardware::graphics::composer3::
                                                        RenderIntent::COLORIMETRIC);
-    EXPECT_THAT(uniforms, Contains(UniformEq("in_rgbToXyz", buildUniformValue<mat4>(mat4()))));
     EXPECT_THAT(uniforms,
-                Contains(UniformEq("in_xyzToRgb", buildUniformValue<mat4>(colorTransform))));
+                Contains(UniformEq("in_rgbToXyz",
+                                   buildUniformValue<mat3>(
+                                           ColorSpace::linearExtendedSRGB().getRGBtoXYZ()))));
+    EXPECT_THAT(uniforms,
+                Contains(UniformEq("in_xyzToSrcRgb",
+                                   buildUniformValue<mat3>(
+                                           ColorSpace::linearSRGB().getXYZtoRGB()))));
+    // color transforms are already tested in renderengine's tests
+    EXPECT_THAT(uniforms, Contains(UniformNameEq("in_colorTransform")));
 }
 
 TEST_F(ShadersTest, buildLinearEffectUniforms_selectsGamutTransformMatrices) {
     shaders::LinearEffect effect =
             shaders::LinearEffect{.inputDataspace = ui::Dataspace::V0_SRGB,
                                   .outputDataspace = ui::Dataspace::DISPLAY_P3,
-                                  .fakeInputDataspace = ui::Dataspace::UNKNOWN};
+                                  .fakeOutputDataspace = ui::Dataspace::UNKNOWN};
 
     ColorSpace inputColorSpace = ColorSpace::sRGB();
-    ColorSpace outputColorSpace = ColorSpace::DisplayP3();
     auto uniforms =
             shaders::buildLinearEffectUniforms(effect, mat4(), 1.f, 1.f, 1.f, nullptr,
                                                aidl::android::hardware::graphics::composer3::
                                                        RenderIntent::COLORIMETRIC);
     EXPECT_THAT(uniforms,
                 Contains(UniformEq("in_rgbToXyz",
-                                   buildUniformValue<mat4>(mat4(inputColorSpace.getRGBtoXYZ())))));
+                                   buildUniformValue<mat3>(
+                                           ColorSpace::linearExtendedSRGB().getRGBtoXYZ()))));
     EXPECT_THAT(uniforms,
-                Contains(UniformEq("in_xyzToRgb",
-                                   buildUniformValue<mat4>(mat4(outputColorSpace.getXYZtoRGB())))));
-}
-
-TEST_F(ShadersTest, buildLinearEffectUniforms_respectsFakeInputDataspace) {
-    shaders::LinearEffect effect =
-            shaders::LinearEffect{.inputDataspace = ui::Dataspace::V0_SRGB,
-                                  .outputDataspace = ui::Dataspace::DISPLAY_P3,
-                                  .fakeInputDataspace = ui::Dataspace::DISPLAY_P3};
-
-    auto uniforms =
-            shaders::buildLinearEffectUniforms(effect, mat4(), 1.f, 1.f, 1.f, nullptr,
-                                               aidl::android::hardware::graphics::composer3::
-                                                       RenderIntent::COLORIMETRIC);
-    EXPECT_THAT(uniforms, Contains(UniformEq("in_rgbToXyz", buildUniformValue<mat4>(mat4()))));
-    EXPECT_THAT(uniforms, Contains(UniformEq("in_xyzToRgb", buildUniformValue<mat4>(mat4()))));
+                Contains(UniformEq("in_xyzToSrcRgb",
+                                   buildUniformValue<mat3>(inputColorSpace.getXYZtoRGB()))));
+    EXPECT_THAT(uniforms, Contains(UniformNameEq("in_colorTransform")));
 }
 
 } // namespace android
diff --git a/services/inputflinger/InputManager.cpp b/services/inputflinger/InputManager.cpp
index 472d7a1..ddebcad 100644
--- a/services/inputflinger/InputManager.cpp
+++ b/services/inputflinger/InputManager.cpp
@@ -57,9 +57,8 @@
  * The event flow is via the "InputListener" interface, as follows:
  * InputReader -> UnwantedInteractionBlocker -> InputProcessor -> InputDispatcher
  */
-InputManager::InputManager(
-        const sp<InputReaderPolicyInterface>& readerPolicy,
-        const sp<InputDispatcherPolicyInterface>& dispatcherPolicy) {
+InputManager::InputManager(const sp<InputReaderPolicyInterface>& readerPolicy,
+                           InputDispatcherPolicyInterface& dispatcherPolicy) {
     mDispatcher = createInputDispatcher(dispatcherPolicy);
     mProcessor = std::make_unique<InputProcessor>(*mDispatcher);
     mBlocker = std::make_unique<UnwantedInteractionBlocker>(*mProcessor);
diff --git a/services/inputflinger/InputManager.h b/services/inputflinger/InputManager.h
index 793757d..b6ad419 100644
--- a/services/inputflinger/InputManager.h
+++ b/services/inputflinger/InputManager.h
@@ -100,9 +100,8 @@
     ~InputManager() override;
 
 public:
-    InputManager(
-            const sp<InputReaderPolicyInterface>& readerPolicy,
-            const sp<InputDispatcherPolicyInterface>& dispatcherPolicy);
+    InputManager(const sp<InputReaderPolicyInterface>& readerPolicy,
+                 InputDispatcherPolicyInterface& dispatcherPolicy);
 
     status_t start() override;
     status_t stop() override;
diff --git a/services/inputflinger/benchmarks/InputDispatcher_benchmarks.cpp b/services/inputflinger/benchmarks/InputDispatcher_benchmarks.cpp
index becf70a..f65533e 100644
--- a/services/inputflinger/benchmarks/InputDispatcher_benchmarks.cpp
+++ b/services/inputflinger/benchmarks/InputDispatcher_benchmarks.cpp
@@ -48,10 +48,8 @@
 
 class FakeInputDispatcherPolicy : public InputDispatcherPolicyInterface {
 public:
-    FakeInputDispatcherPolicy() {}
-
-protected:
-    virtual ~FakeInputDispatcherPolicy() {}
+    FakeInputDispatcherPolicy() = default;
+    virtual ~FakeInputDispatcherPolicy() = default;
 
 private:
     void notifyConfigurationChanged(nsecs_t) override {}
@@ -82,24 +80,23 @@
 
     void notifyVibratorState(int32_t deviceId, bool isOn) override {}
 
-    void getDispatcherConfiguration(InputDispatcherConfiguration* outConfig) override {
-        *outConfig = mConfig;
+    InputDispatcherConfiguration getDispatcherConfiguration() override { return mConfig; }
+
+    bool filterInputEvent(const InputEvent& inputEvent, uint32_t policyFlags) override {
+        return true; // dispatch event normally
     }
 
-    bool filterInputEvent(const InputEvent* inputEvent, uint32_t policyFlags) override {
-        return true;
-    }
-
-    void interceptKeyBeforeQueueing(const KeyEvent*, uint32_t&) override {}
+    void interceptKeyBeforeQueueing(const KeyEvent&, uint32_t&) override {}
 
     void interceptMotionBeforeQueueing(int32_t, nsecs_t, uint32_t&) override {}
 
-    nsecs_t interceptKeyBeforeDispatching(const sp<IBinder>&, const KeyEvent*, uint32_t) override {
+    nsecs_t interceptKeyBeforeDispatching(const sp<IBinder>&, const KeyEvent&, uint32_t) override {
         return 0;
     }
 
-    bool dispatchUnhandledKey(const sp<IBinder>&, const KeyEvent*, uint32_t, KeyEvent*) override {
-        return false;
+    std::optional<KeyEvent> dispatchUnhandledKey(const sp<IBinder>&, const KeyEvent&,
+                                                 uint32_t) override {
+        return {};
     }
 
     void notifySwitch(nsecs_t, uint32_t, uint32_t, uint32_t) override {}
@@ -258,7 +255,7 @@
 
 static void benchmarkNotifyMotion(benchmark::State& state) {
     // Create dispatcher
-    sp<FakeInputDispatcherPolicy> fakePolicy = sp<FakeInputDispatcherPolicy>::make();
+    FakeInputDispatcherPolicy fakePolicy;
     InputDispatcher dispatcher(fakePolicy);
     dispatcher.setInputDispatchMode(/*enabled*/ true, /*frozen*/ false);
     dispatcher.start();
@@ -293,7 +290,7 @@
 
 static void benchmarkInjectMotion(benchmark::State& state) {
     // Create dispatcher
-    sp<FakeInputDispatcherPolicy> fakePolicy = sp<FakeInputDispatcherPolicy>::make();
+    FakeInputDispatcherPolicy fakePolicy;
     InputDispatcher dispatcher(fakePolicy);
     dispatcher.setInputDispatchMode(/*enabled*/ true, /*frozen*/ false);
     dispatcher.start();
@@ -327,7 +324,7 @@
 
 static void benchmarkOnWindowInfosChanged(benchmark::State& state) {
     // Create dispatcher
-    sp<FakeInputDispatcherPolicy> fakePolicy = sp<FakeInputDispatcherPolicy>::make();
+    FakeInputDispatcherPolicy fakePolicy;
     InputDispatcher dispatcher(fakePolicy);
     dispatcher.setInputDispatchMode(/*enabled*/ true, /*frozen*/ false);
     dispatcher.start();
diff --git a/services/inputflinger/dispatcher/InputDispatcher.cpp b/services/inputflinger/dispatcher/InputDispatcher.cpp
index bf52502..326ca87 100644
--- a/services/inputflinger/dispatcher/InputDispatcher.cpp
+++ b/services/inputflinger/dispatcher/InputDispatcher.cpp
@@ -498,14 +498,14 @@
 // Returns true if the event type passed as argument represents a user activity.
 bool isUserActivityEvent(const EventEntry& eventEntry) {
     switch (eventEntry.type) {
+        case EventEntry::Type::CONFIGURATION_CHANGED:
+        case EventEntry::Type::DEVICE_RESET:
+        case EventEntry::Type::DRAG:
         case EventEntry::Type::FOCUS:
         case EventEntry::Type::POINTER_CAPTURE_CHANGED:
-        case EventEntry::Type::DRAG:
-        case EventEntry::Type::TOUCH_MODE_CHANGED:
         case EventEntry::Type::SENSOR:
-        case EventEntry::Type::CONFIGURATION_CHANGED:
+        case EventEntry::Type::TOUCH_MODE_CHANGED:
             return false;
-        case EventEntry::Type::DEVICE_RESET:
         case EventEntry::Type::KEY:
         case EventEntry::Type::MOTION:
             return true;
@@ -685,10 +685,10 @@
 
 // --- InputDispatcher ---
 
-InputDispatcher::InputDispatcher(const sp<InputDispatcherPolicyInterface>& policy)
+InputDispatcher::InputDispatcher(InputDispatcherPolicyInterface& policy)
       : InputDispatcher(policy, STALE_EVENT_TIMEOUT) {}
 
-InputDispatcher::InputDispatcher(const sp<InputDispatcherPolicyInterface>& policy,
+InputDispatcher::InputDispatcher(InputDispatcherPolicyInterface& policy,
                                  std::chrono::nanoseconds staleEventTimeout)
       : mPolicy(policy),
         mPendingEvent(nullptr),
@@ -1440,7 +1440,7 @@
     // Enqueue a command to run outside the lock to tell the policy that the configuration changed.
     auto command = [this, eventTime = entry.eventTime]() REQUIRES(mLock) {
         scoped_unlock unlock(mLock);
-        mPolicy->notifyConfigurationChanged(eventTime);
+        mPolicy.notifyConfigurationChanged(eventTime);
     };
     postCommandLocked(std::move(command));
     return true;
@@ -1461,6 +1461,11 @@
     CancelationOptions options(CancelationOptions::Mode::CANCEL_ALL_EVENTS, "device was reset");
     options.deviceId = entry.deviceId;
     synthesizeCancelationEventsForAllConnectionsLocked(options);
+
+    // Remove all active pointers from this device
+    for (auto& [_, touchState] : mTouchStatesByDisplay) {
+        touchState.removeAllPointersForDevice(entry.deviceId);
+    }
     return true;
 }
 
@@ -1684,6 +1689,8 @@
                 doInterceptKeyBeforeDispatchingCommand(focusedWindowToken, *entry);
             };
             postCommandLocked(std::move(command));
+            // Poke user activity for keys not passed to user
+            pokeUserActivityLocked(*entry);
             return false; // wait for the command to run
         } else {
             entry->interceptKeyResult = KeyEntry::InterceptKeyResult::CONTINUE;
@@ -1700,6 +1707,8 @@
                            *dropReason == DropReason::POLICY ? InputEventInjectionResult::SUCCEEDED
                                                              : InputEventInjectionResult::FAILED);
         mReporter->reportDroppedKey(entry->id);
+        // Poke user activity for undispatched keys
+        pokeUserActivityLocked(*entry);
         return true;
     }
 
@@ -1755,10 +1764,10 @@
         scoped_unlock unlock(mLock);
 
         if (entry->accuracyChanged) {
-            mPolicy->notifySensorAccuracy(entry->deviceId, entry->sensorType, entry->accuracy);
+            mPolicy.notifySensorAccuracy(entry->deviceId, entry->sensorType, entry->accuracy);
         }
-        mPolicy->notifySensorEvent(entry->deviceId, entry->sensorType, entry->accuracy,
-                                   entry->hwTimestamp, entry->values);
+        mPolicy.notifySensorEvent(entry->deviceId, entry->sensorType, entry->accuracy,
+                                  entry->hwTimestamp, entry->values);
     };
     postCommandLocked(std::move(command));
 }
@@ -2203,7 +2212,7 @@
     // event injection will be allowed.
     const int32_t displayId = entry.displayId;
     const int32_t action = entry.action;
-    const int32_t maskedAction = action & AMOTION_EVENT_ACTION_MASK;
+    const int32_t maskedAction = MotionEvent::getActionMasked(action);
 
     // Update the touch state as needed based on the properties of the touch event.
     outInjectionResult = InputEventInjectionResult::PENDING;
@@ -2230,7 +2239,9 @@
     const bool wasDown = oldState != nullptr && oldState->isDown();
     const bool isDown = (maskedAction == AMOTION_EVENT_ACTION_DOWN) ||
             (maskedAction == AMOTION_EVENT_ACTION_POINTER_DOWN && !wasDown);
-    const bool newGesture = isDown || maskedAction == AMOTION_EVENT_ACTION_SCROLL || isHoverAction;
+    const bool newGesture = isDown || maskedAction == AMOTION_EVENT_ACTION_SCROLL ||
+            maskedAction == AMOTION_EVENT_ACTION_HOVER_ENTER ||
+            maskedAction == AMOTION_EVENT_ACTION_HOVER_MOVE;
     const bool isFromMouse = isFromSource(entry.source, AINPUT_SOURCE_MOUSE);
 
     // If pointers are already down, let's finish the current gesture and ignore the new events
@@ -2330,16 +2341,11 @@
                 continue;
             }
 
-            if (isHoverAction) {
+            if (maskedAction == AMOTION_EVENT_ACTION_HOVER_ENTER ||
+                maskedAction == AMOTION_EVENT_ACTION_HOVER_MOVE) {
                 const int32_t pointerId = entry.pointerProperties[0].id;
-                if (maskedAction == AMOTION_EVENT_ACTION_HOVER_EXIT) {
-                    // Pointer left. Remove it
-                    tempTouchState.removeHoveringPointer(entry.deviceId, pointerId);
-                } else {
-                    // The "windowHandle" is the target of this hovering pointer.
-                    tempTouchState.addHoveringPointerToWindow(windowHandle, entry.deviceId,
-                                                              pointerId);
-                }
+                // The "windowHandle" is the target of this hovering pointer.
+                tempTouchState.addHoveringPointerToWindow(windowHandle, entry.deviceId, pointerId);
             }
 
             // Set target flags.
@@ -2423,7 +2429,7 @@
         /* Case 2: Pointer move, up, cancel or non-splittable pointer down. */
 
         // If the pointer is not currently down, then ignore the event.
-        if (!tempTouchState.isDown()) {
+        if (!tempTouchState.isDown() && maskedAction != AMOTION_EVENT_ACTION_HOVER_EXIT) {
             LOG(INFO) << "Dropping event because the pointer is not down or we previously "
                          "dropped the pointer down event in display "
                       << displayId << ": " << entry.getDescription();
@@ -2431,6 +2437,20 @@
             return {};
         }
 
+        // If the pointer is not currently hovering, then ignore the event.
+        if (maskedAction == AMOTION_EVENT_ACTION_HOVER_EXIT) {
+            const int32_t pointerId = entry.pointerProperties[0].id;
+            if (oldState == nullptr ||
+                oldState->getWindowsWithHoveringPointer(entry.deviceId, pointerId).empty()) {
+                LOG(INFO) << "Dropping event because the hovering pointer is not in any windows in "
+                             "display "
+                          << displayId << ": " << entry.getDescription();
+                outInjectionResult = InputEventInjectionResult::FAILED;
+                return {};
+            }
+            tempTouchState.removeHoveringPointer(entry.deviceId, pointerId);
+        }
+
         addDragEventLocked(entry);
 
         // Check whether touches should slip outside of the current foreground window.
@@ -2526,21 +2546,6 @@
                                   targets);
         }
     }
-    // Ensure that we have at least one foreground window or at least one window that cannot be a
-    // foreground target. If we only have windows that are not receiving foreground touches (e.g. we
-    // only have windows getting ACTION_OUTSIDE), then drop the event, because there is no window
-    // that is actually receiving the entire gesture.
-    if (std::none_of(tempTouchState.windows.begin(), tempTouchState.windows.end(),
-                     [](const TouchedWindow& touchedWindow) {
-                         return !canReceiveForegroundTouches(
-                                        *touchedWindow.windowHandle->getInfo()) ||
-                                 touchedWindow.targetFlags.test(InputTarget::Flags::FOREGROUND);
-                     })) {
-        ALOGI("Dropping event because there is no touched window on display %d to receive it: %s",
-              displayId, entry.getDescription().c_str());
-        outInjectionResult = InputEventInjectionResult::FAILED;
-        return {};
-    }
 
     // Ensure that all touched windows are valid for injection.
     if (entry.injectionState != nullptr) {
@@ -2582,7 +2587,7 @@
         }
     }
 
-    // Success!  Output targets from the touch state.
+    // Output targets from the touch state.
     for (const TouchedWindow& touchedWindow : tempTouchState.windows) {
         if (touchedWindow.pointerIds.none() && !touchedWindow.hasHoveringPointers(entry.deviceId)) {
             // Windows with hovering pointers are getting persisted inside TouchState.
@@ -2594,6 +2599,23 @@
                               targets);
     }
 
+    if (targets.empty()) {
+        LOG(INFO) << "Dropping event because no targets were found: " << entry.getDescription();
+        outInjectionResult = InputEventInjectionResult::FAILED;
+        return {};
+    }
+
+    // If we only have windows getting ACTION_OUTSIDE, then drop the event, because there is no
+    // window that is actually receiving the entire gesture.
+    if (std::all_of(targets.begin(), targets.end(), [](const InputTarget& target) {
+            return target.flags.test(InputTarget::Flags::DISPATCH_AS_OUTSIDE);
+        })) {
+        LOG(INFO) << "Dropping event because all windows would just receive ACTION_OUTSIDE: "
+                  << entry.getDescription();
+        outInjectionResult = InputEventInjectionResult::FAILED;
+        return {};
+    }
+
     outInjectionResult = InputEventInjectionResult::SUCCEEDED;
     // Drop the outside or hover touch windows since we will not care about them
     // in the next iteration.
@@ -2704,9 +2726,6 @@
 
     if (uint32_t(pointerIndex) == entry.pointerCount) {
         LOG_ALWAYS_FATAL("Should find a valid pointer index by id %d", mDragState->pointerId);
-        sendDropWindowCommandLocked(nullptr, 0, 0);
-        mDragState.reset();
-        return;
     }
 
     const int32_t maskedAction = entry.action & AMOTION_EVENT_ACTION_MASK;
@@ -3018,13 +3037,11 @@
     }
     int32_t displayId = getTargetDisplayId(eventEntry);
     sp<WindowInfoHandle> focusedWindowHandle = getFocusedWindowHandleLocked(displayId);
+    const WindowInfo* windowDisablingUserActivityInfo = nullptr;
     if (focusedWindowHandle != nullptr) {
         const WindowInfo* info = focusedWindowHandle->getInfo();
         if (info->inputConfig.test(WindowInfo::InputConfig::DISABLE_USER_ACTIVITY)) {
-            if (DEBUG_DISPATCH_CYCLE) {
-                ALOGD("Not poking user activity: disabled by window '%s'.", info->name.c_str());
-            }
-            return;
+            windowDisablingUserActivityInfo = info;
         }
     }
 
@@ -3035,7 +3052,13 @@
             if (motionEntry.action == AMOTION_EVENT_ACTION_CANCEL) {
                 return;
             }
-
+            if (windowDisablingUserActivityInfo != nullptr) {
+                if (DEBUG_DISPATCH_CYCLE) {
+                    ALOGD("Not poking user activity: disabled by window '%s'.",
+                          windowDisablingUserActivityInfo->name.c_str());
+                }
+                return;
+            }
             if (MotionEvent::isTouchEvent(motionEntry.source, motionEntry.action)) {
                 eventType = USER_ACTIVITY_EVENT_TOUCH;
             }
@@ -3046,6 +3069,22 @@
             if (keyEntry.flags & AKEY_EVENT_FLAG_CANCELED) {
                 return;
             }
+            // If the key code is unknown, we don't consider it user activity
+            if (keyEntry.keyCode == AKEYCODE_UNKNOWN) {
+                return;
+            }
+            // Don't inhibit events that were intercepted or are not passed to
+            // the apps, like system shortcuts
+            if (windowDisablingUserActivityInfo != nullptr &&
+                keyEntry.interceptKeyResult != KeyEntry::InterceptKeyResult::SKIP &&
+                keyEntry.policyFlags & POLICY_FLAG_PASS_TO_USER) {
+                if (DEBUG_DISPATCH_CYCLE) {
+                    ALOGD("Not poking user activity: disabled by window '%s'.",
+                          windowDisablingUserActivityInfo->name.c_str());
+                }
+                return;
+            }
+
             eventType = USER_ACTIVITY_EVENT_BUTTON;
             break;
         }
@@ -3059,7 +3098,7 @@
     auto command = [this, eventTime = eventEntry.eventTime, eventType, displayId]()
                            REQUIRES(mLock) {
                                scoped_unlock unlock(mLock);
-                               mPolicy->pokeUserActivity(eventTime, eventType, displayId);
+                               mPolicy.pokeUserActivity(eventTime, eventType, displayId);
                            };
     postCommandLocked(std::move(command));
 }
@@ -3400,7 +3439,7 @@
 
     auto command = [this, token]() REQUIRES(mLock) {
         scoped_unlock unlock(mLock);
-        mPolicy->onPointerDownOutsideFocus(token);
+        mPolicy.onPointerDownOutsideFocus(token);
     };
     postCommandLocked(std::move(command));
 }
@@ -3611,7 +3650,7 @@
 
 const std::array<uint8_t, 32> InputDispatcher::getSignature(
         const MotionEntry& motionEntry, const DispatchEntry& dispatchEntry) const {
-    const int32_t actionMasked = dispatchEntry.resolvedAction & AMOTION_EVENT_ACTION_MASK;
+    const int32_t actionMasked = MotionEvent::getActionMasked(dispatchEntry.resolvedAction);
     if (actionMasked != AMOTION_EVENT_ACTION_UP && actionMasked != AMOTION_EVENT_ACTION_DOWN) {
         // Only sign events up and down events as the purely move events
         // are tied to their up/down counterparts so signing would be redundant.
@@ -3679,7 +3718,7 @@
 
             auto command = [this, connection]() REQUIRES(mLock) {
                 scoped_unlock unlock(mLock);
-                mPolicy->notifyInputChannelBroken(connection->inputChannel->getConnectionToken());
+                mPolicy.notifyInputChannelBroken(connection->inputChannel->getConnectionToken());
             };
             postCommandLocked(std::move(command));
         }
@@ -4162,7 +4201,7 @@
                      args.eventTime);
 
     android::base::Timer t;
-    mPolicy->interceptKeyBeforeQueueing(&event, /*byref*/ policyFlags);
+    mPolicy.interceptKeyBeforeQueueing(event, /*byref*/ policyFlags);
     if (t.duration() > SLOW_INTERCEPTION_THRESHOLD) {
         ALOGW("Excessive delay in interceptKeyBeforeQueueing; took %s ms",
               std::to_string(t.duration().count()).c_str());
@@ -4176,7 +4215,7 @@
             mLock.unlock();
 
             policyFlags |= POLICY_FLAG_FILTERED;
-            if (!mPolicy->filterInputEvent(&event, policyFlags)) {
+            if (!mPolicy.filterInputEvent(event, policyFlags)) {
                 return; // event was consumed by the filter
             }
 
@@ -4241,7 +4280,7 @@
     policyFlags |= POLICY_FLAG_TRUSTED;
 
     android::base::Timer t;
-    mPolicy->interceptMotionBeforeQueueing(args.displayId, args.eventTime, policyFlags);
+    mPolicy.interceptMotionBeforeQueueing(args.displayId, args.eventTime, policyFlags);
     if (t.duration() > SLOW_INTERCEPTION_THRESHOLD) {
         ALOGW("Excessive delay in interceptMotionBeforeQueueing; took %s ms",
               std::to_string(t.duration().count()).c_str());
@@ -4280,7 +4319,7 @@
                              args.pointerProperties, args.pointerCoords);
 
             policyFlags |= POLICY_FLAG_FILTERED;
-            if (!mPolicy->filterInputEvent(&event, policyFlags)) {
+            if (!mPolicy.filterInputEvent(event, policyFlags)) {
                 return; // event was consumed by the filter
             }
 
@@ -4346,7 +4385,7 @@
         ALOGD("notifyVibratorState - eventTime=%" PRId64 ", device=%d,  isOn=%d", args.eventTime,
               args.deviceId, args.isOn);
     }
-    mPolicy->notifyVibratorState(args.deviceId, args.isOn);
+    mPolicy.notifyVibratorState(args.deviceId, args.isOn);
 }
 
 bool InputDispatcher::shouldSendMotionToInputFilterLocked(const NotifyMotionArgs& args) {
@@ -4362,7 +4401,7 @@
 
     uint32_t policyFlags = args.policyFlags;
     policyFlags |= POLICY_FLAG_TRUSTED;
-    mPolicy->notifySwitch(args.eventTime, args.switchValues, args.switchMask, policyFlags);
+    mPolicy.notifySwitch(args.eventTime, args.switchValues, args.switchMask, policyFlags);
 }
 
 void InputDispatcher::notifyDeviceReset(const NotifyDeviceResetArgs& args) {
@@ -4461,7 +4500,7 @@
 
             if (!(policyFlags & POLICY_FLAG_FILTERED)) {
                 android::base::Timer t;
-                mPolicy->interceptKeyBeforeQueueing(&keyEvent, /*byref*/ policyFlags);
+                mPolicy.interceptKeyBeforeQueueing(keyEvent, /*byref*/ policyFlags);
                 if (t.duration() > SLOW_INTERCEPTION_THRESHOLD) {
                     ALOGW("Excessive delay in interceptKeyBeforeQueueing; took %s ms",
                           std::to_string(t.duration().count()).c_str());
@@ -4493,7 +4532,7 @@
             if (!(policyFlags & POLICY_FLAG_FILTERED)) {
                 nsecs_t eventTime = motionEvent.getEventTime();
                 android::base::Timer t;
-                mPolicy->interceptMotionBeforeQueueing(displayId, eventTime, /*byref*/ policyFlags);
+                mPolicy.interceptMotionBeforeQueueing(displayId, eventTime, /*byref*/ policyFlags);
                 if (t.duration() > SLOW_INTERCEPTION_THRESHOLD) {
                     ALOGW("Excessive delay in interceptMotionBeforeQueueing; took %s ms",
                           std::to_string(t.duration().count()).c_str());
@@ -5462,7 +5501,7 @@
     std::string line;
 
     while (std::getline(stream, line, '\n')) {
-        ALOGD("%s", line.c_str());
+        ALOGI("%s", line.c_str());
     }
 }
 
@@ -6058,7 +6097,7 @@
                                                     const sp<IBinder>& newToken) {
     auto command = [this, oldToken, newToken]() REQUIRES(mLock) {
         scoped_unlock unlock(mLock);
-        mPolicy->notifyFocusChanged(oldToken, newToken);
+        mPolicy.notifyFocusChanged(oldToken, newToken);
     };
     postCommandLocked(std::move(command));
 }
@@ -6066,7 +6105,7 @@
 void InputDispatcher::sendDropWindowCommandLocked(const sp<IBinder>& token, float x, float y) {
     auto command = [this, token, x, y]() REQUIRES(mLock) {
         scoped_unlock unlock(mLock);
-        mPolicy->notifyDropWindow(token, x, y);
+        mPolicy.notifyDropWindow(token, x, y);
     };
     postCommandLocked(std::move(command));
 }
@@ -6113,7 +6152,7 @@
 
     auto command = [this, application = std::move(application)]() REQUIRES(mLock) {
         scoped_unlock unlock(mLock);
-        mPolicy->notifyNoFocusedWindowAnr(application);
+        mPolicy.notifyNoFocusedWindowAnr(application);
     };
     postCommandLocked(std::move(command));
 }
@@ -6153,8 +6192,7 @@
     { // release lock
         scoped_unlock unlock(mLock);
         android::base::Timer t;
-        delay = mPolicy->interceptKeyBeforeDispatching(focusedWindowToken, &event,
-                                                       entry.policyFlags);
+        delay = mPolicy.interceptKeyBeforeDispatching(focusedWindowToken, event, entry.policyFlags);
         if (t.duration() > SLOW_INTERCEPTION_THRESHOLD) {
             ALOGW("Excessive delay in interceptKeyBeforeDispatching; took %s ms",
                   std::to_string(t.duration().count()).c_str());
@@ -6176,7 +6214,7 @@
                                                           std::string reason) {
     auto command = [this, token, pid, reason = std::move(reason)]() REQUIRES(mLock) {
         scoped_unlock unlock(mLock);
-        mPolicy->notifyWindowUnresponsive(token, pid, reason);
+        mPolicy.notifyWindowUnresponsive(token, pid, reason);
     };
     postCommandLocked(std::move(command));
 }
@@ -6185,7 +6223,7 @@
                                                         std::optional<int32_t> pid) {
     auto command = [this, token, pid]() REQUIRES(mLock) {
         scoped_unlock unlock(mLock);
-        mPolicy->notifyWindowResponsive(token, pid);
+        mPolicy.notifyWindowResponsive(token, pid);
     };
     postCommandLocked(std::move(command));
 }
@@ -6269,8 +6307,12 @@
 
             mLock.unlock();
 
-            mPolicy->dispatchUnhandledKey(connection->inputChannel->getConnectionToken(), &event,
-                                          keyEntry.policyFlags, &event);
+            if (const auto unhandledKeyFallback =
+                        mPolicy.dispatchUnhandledKey(connection->inputChannel->getConnectionToken(),
+                                                     event, keyEntry.policyFlags);
+                unhandledKeyFallback) {
+                event = *unhandledKeyFallback;
+            }
 
             mLock.lock();
 
@@ -6310,9 +6352,13 @@
 
         mLock.unlock();
 
-        bool fallback =
-                mPolicy->dispatchUnhandledKey(connection->inputChannel->getConnectionToken(),
-                                              &event, keyEntry.policyFlags, &event);
+        bool fallback = false;
+        if (auto fb = mPolicy.dispatchUnhandledKey(connection->inputChannel->getConnectionToken(),
+                                                   event, keyEntry.policyFlags);
+            fb) {
+            fallback = true;
+            event = *fb;
+        }
 
         mLock.lock();
 
@@ -6564,7 +6610,7 @@
     mCurrentPointerCaptureRequest.seq++;
     auto command = [this, request = mCurrentPointerCaptureRequest]() REQUIRES(mLock) {
         scoped_unlock unlock(mLock);
-        mPolicy->setPointerCapture(request);
+        mPolicy.setPointerCapture(request);
     };
     postCommandLocked(std::move(command));
 }
@@ -6664,8 +6710,7 @@
 }
 
 void InputDispatcher::requestRefreshConfiguration() {
-    InputDispatcherConfiguration config;
-    mPolicy->getDispatcherConfiguration(&config);
+    InputDispatcherConfiguration config = mPolicy.getDispatcherConfiguration();
 
     std::scoped_lock _l(mLock);
     mConfig = config;
@@ -6680,7 +6725,7 @@
                                          const sp<WindowInfoHandle>& oldWindowHandle,
                                          const sp<WindowInfoHandle>& newWindowHandle,
                                          TouchState& state, int32_t pointerId,
-                                         std::vector<InputTarget>& targets) {
+                                         std::vector<InputTarget>& targets) const {
     std::bitset<MAX_POINTER_ID + 1> pointerIds;
     pointerIds.set(pointerId);
     const bool oldHasWallpaper = oldWindowHandle->getInfo()->inputConfig.test(
diff --git a/services/inputflinger/dispatcher/InputDispatcher.h b/services/inputflinger/dispatcher/InputDispatcher.h
index 791dd63..9b12f2f 100644
--- a/services/inputflinger/dispatcher/InputDispatcher.h
+++ b/services/inputflinger/dispatcher/InputDispatcher.h
@@ -82,8 +82,8 @@
 public:
     static constexpr bool kDefaultInTouchMode = true;
 
-    explicit InputDispatcher(const sp<InputDispatcherPolicyInterface>& policy);
-    explicit InputDispatcher(const sp<InputDispatcherPolicyInterface>& policy,
+    explicit InputDispatcher(InputDispatcherPolicyInterface& policy);
+    explicit InputDispatcher(InputDispatcherPolicyInterface& policy,
                              std::chrono::nanoseconds staleEventTimeout);
     ~InputDispatcher() override;
 
@@ -166,7 +166,7 @@
 
     std::unique_ptr<InputThread> mThread;
 
-    sp<InputDispatcherPolicyInterface> mPolicy;
+    InputDispatcherPolicyInterface& mPolicy;
     android::InputDispatcherConfiguration mConfig GUARDED_BY(mLock);
 
     std::mutex mLock;
@@ -709,8 +709,8 @@
     void slipWallpaperTouch(ftl::Flags<InputTarget::Flags> targetFlags,
                             const sp<android::gui::WindowInfoHandle>& oldWindowHandle,
                             const sp<android::gui::WindowInfoHandle>& newWindowHandle,
-                            TouchState& state, int32_t pointerId, std::vector<InputTarget>& targets)
-            REQUIRES(mLock);
+                            TouchState& state, int32_t pointerId,
+                            std::vector<InputTarget>& targets) const REQUIRES(mLock);
     void transferWallpaperTouch(ftl::Flags<InputTarget::Flags> oldTargetFlags,
                                 ftl::Flags<InputTarget::Flags> newTargetFlags,
                                 const sp<android::gui::WindowInfoHandle> fromWindowHandle,
diff --git a/services/inputflinger/dispatcher/InputDispatcherFactory.cpp b/services/inputflinger/dispatcher/InputDispatcherFactory.cpp
index bca1600..3ef8419 100644
--- a/services/inputflinger/dispatcher/InputDispatcherFactory.cpp
+++ b/services/inputflinger/dispatcher/InputDispatcherFactory.cpp
@@ -20,7 +20,7 @@
 namespace android {
 
 std::unique_ptr<InputDispatcherInterface> createInputDispatcher(
-        const sp<InputDispatcherPolicyInterface>& policy) {
+        InputDispatcherPolicyInterface& policy) {
     return std::make_unique<android::inputdispatcher::InputDispatcher>(policy);
 }
 
diff --git a/services/inputflinger/dispatcher/TouchState.cpp b/services/inputflinger/dispatcher/TouchState.cpp
index 9c443f1..0a61d48 100644
--- a/services/inputflinger/dispatcher/TouchState.cpp
+++ b/services/inputflinger/dispatcher/TouchState.cpp
@@ -242,6 +242,18 @@
     clearWindowsWithoutPointers();
 }
 
+void TouchState::removeAllPointersForDevice(int32_t removedDeviceId) {
+    for (TouchedWindow& window : windows) {
+        window.removeAllHoveringPointersForDevice(removedDeviceId);
+    }
+    if (deviceId == removedDeviceId) {
+        for (TouchedWindow& window : windows) {
+            window.removeAllTouchingPointers();
+        }
+    }
+    clearWindowsWithoutPointers();
+}
+
 std::string TouchState::dump() const {
     std::string out;
     out += StringPrintf("deviceId=%d, source=%s\n", deviceId,
diff --git a/services/inputflinger/dispatcher/TouchState.h b/services/inputflinger/dispatcher/TouchState.h
index a20080f..15b840f 100644
--- a/services/inputflinger/dispatcher/TouchState.h
+++ b/services/inputflinger/dispatcher/TouchState.h
@@ -54,6 +54,8 @@
                                     int32_t deviceId, int32_t hoveringPointerId);
     void removeHoveringPointer(int32_t deviceId, int32_t hoveringPointerId);
     void clearHoveringPointers();
+
+    void removeAllPointersForDevice(int32_t removedDeviceId);
     void removeWindowByToken(const sp<IBinder>& token);
     void filterNonAsIsTouchWindows();
 
diff --git a/services/inputflinger/dispatcher/TouchedWindow.cpp b/services/inputflinger/dispatcher/TouchedWindow.cpp
index 99c4769..d55d657 100644
--- a/services/inputflinger/dispatcher/TouchedWindow.cpp
+++ b/services/inputflinger/dispatcher/TouchedWindow.cpp
@@ -58,6 +58,10 @@
     }
 }
 
+void TouchedWindow::removeAllTouchingPointers() {
+    pointerIds.reset();
+}
+
 void TouchedWindow::removeHoveringPointer(int32_t deviceId, int32_t pointerId) {
     const auto it = mHoveringPointerIdsByDevice.find(deviceId);
     if (it == mHoveringPointerIdsByDevice.end()) {
@@ -70,6 +74,10 @@
     }
 }
 
+void TouchedWindow::removeAllHoveringPointersForDevice(int32_t deviceId) {
+    mHoveringPointerIdsByDevice.erase(deviceId);
+}
+
 std::string TouchedWindow::dump() const {
     std::string out;
     std::string hoveringPointers =
diff --git a/services/inputflinger/dispatcher/TouchedWindow.h b/services/inputflinger/dispatcher/TouchedWindow.h
index aa2e9dd..43e7169 100644
--- a/services/inputflinger/dispatcher/TouchedWindow.h
+++ b/services/inputflinger/dispatcher/TouchedWindow.h
@@ -44,6 +44,9 @@
     void addHoveringPointer(int32_t deviceId, int32_t pointerId);
     void removeHoveringPointer(int32_t deviceId, int32_t pointerId);
     void removeTouchingPointer(int32_t pointerId);
+
+    void removeAllTouchingPointers();
+    void removeAllHoveringPointersForDevice(int32_t deviceId);
     void clearHoveringPointers();
     std::string dump() const;
 
diff --git a/services/inputflinger/dispatcher/include/InputDispatcherFactory.h b/services/inputflinger/dispatcher/include/InputDispatcherFactory.h
index 5247d8e..6b298c2 100644
--- a/services/inputflinger/dispatcher/include/InputDispatcherFactory.h
+++ b/services/inputflinger/dispatcher/include/InputDispatcherFactory.h
@@ -25,6 +25,6 @@
 
 // This factory method is used to encapsulate implementation details in internal header files.
 std::unique_ptr<InputDispatcherInterface> createInputDispatcher(
-        const sp<InputDispatcherPolicyInterface>& policy);
+        InputDispatcherPolicyInterface& policy);
 
 } // namespace android
diff --git a/services/inputflinger/dispatcher/include/InputDispatcherPolicyInterface.h b/services/inputflinger/dispatcher/include/InputDispatcherPolicyInterface.h
index 7843923..5539915 100644
--- a/services/inputflinger/dispatcher/include/InputDispatcherPolicyInterface.h
+++ b/services/inputflinger/dispatcher/include/InputDispatcherPolicyInterface.h
@@ -35,12 +35,11 @@
  * The actual implementation is partially supported by callbacks into the DVM
  * via JNI.  This interface is also mocked in the unit tests.
  */
-class InputDispatcherPolicyInterface : public virtual RefBase {
-protected:
-    InputDispatcherPolicyInterface() {}
-    virtual ~InputDispatcherPolicyInterface() {}
-
+class InputDispatcherPolicyInterface {
 public:
+    InputDispatcherPolicyInterface() = default;
+    virtual ~InputDispatcherPolicyInterface() = default;
+
     /* Notifies the system that a configuration change has occurred. */
     virtual void notifyConfigurationChanged(nsecs_t when) = 0;
 
@@ -75,14 +74,14 @@
     virtual void notifyVibratorState(int32_t deviceId, bool isOn) = 0;
 
     /* Gets the input dispatcher configuration. */
-    virtual void getDispatcherConfiguration(InputDispatcherConfiguration* outConfig) = 0;
+    virtual InputDispatcherConfiguration getDispatcherConfiguration() = 0;
 
     /* Filters an input event.
      * Return true to dispatch the event unmodified, false to consume the event.
      * A filter can also transform and inject events later by passing POLICY_FLAG_FILTERED
      * to injectInputEvent.
      */
-    virtual bool filterInputEvent(const InputEvent* inputEvent, uint32_t policyFlags) = 0;
+    virtual bool filterInputEvent(const InputEvent& inputEvent, uint32_t policyFlags) = 0;
 
     /* Intercepts a key event immediately before queueing it.
      * The policy can use this method as an opportunity to perform power management functions
@@ -91,7 +90,7 @@
      * This method is expected to set the POLICY_FLAG_PASS_TO_USER policy flag if the event
      * should be dispatched to applications.
      */
-    virtual void interceptKeyBeforeQueueing(const KeyEvent* keyEvent, uint32_t& policyFlags) = 0;
+    virtual void interceptKeyBeforeQueueing(const KeyEvent& keyEvent, uint32_t& policyFlags) = 0;
 
     /* Intercepts a touch, trackball or other motion event before queueing it.
      * The policy can use this method as an opportunity to perform power management functions
@@ -100,18 +99,19 @@
      * This method is expected to set the POLICY_FLAG_PASS_TO_USER policy flag if the event
      * should be dispatched to applications.
      */
-    virtual void interceptMotionBeforeQueueing(const int32_t displayId, nsecs_t when,
+    virtual void interceptMotionBeforeQueueing(int32_t displayId, nsecs_t when,
                                                uint32_t& policyFlags) = 0;
 
     /* Allows the policy a chance to intercept a key before dispatching. */
     virtual nsecs_t interceptKeyBeforeDispatching(const sp<IBinder>& token,
-                                                  const KeyEvent* keyEvent,
+                                                  const KeyEvent& keyEvent,
                                                   uint32_t policyFlags) = 0;
 
     /* Allows the policy a chance to perform default processing for an unhandled key.
-     * Returns an alternate keycode to redispatch as a fallback, or 0 to give up. */
-    virtual bool dispatchUnhandledKey(const sp<IBinder>& token, const KeyEvent* keyEvent,
-                                      uint32_t policyFlags, KeyEvent* outFallbackKeyEvent) = 0;
+     * Returns an alternate key event to redispatch as a fallback, if needed. */
+    virtual std::optional<KeyEvent> dispatchUnhandledKey(const sp<IBinder>& token,
+                                                         const KeyEvent& keyEvent,
+                                                         uint32_t policyFlags) = 0;
 
     /* Notifies the policy about switch events.
      */
diff --git a/services/inputflinger/reader/Android.bp b/services/inputflinger/reader/Android.bp
index 132c3a1..b0edb57 100644
--- a/services/inputflinger/reader/Android.bp
+++ b/services/inputflinger/reader/Android.bp
@@ -42,6 +42,7 @@
         "Macros.cpp",
         "TouchVideoDevice.cpp",
         "controller/PeripheralController.cpp",
+        "mapper/CapturedTouchpadEventConverter.cpp",
         "mapper/CursorInputMapper.cpp",
         "mapper/ExternalStylusInputMapper.cpp",
         "mapper/InputMapper.cpp",
diff --git a/services/inputflinger/reader/mapper/CapturedTouchpadEventConverter.cpp b/services/inputflinger/reader/mapper/CapturedTouchpadEventConverter.cpp
new file mode 100644
index 0000000..dab4661
--- /dev/null
+++ b/services/inputflinger/reader/mapper/CapturedTouchpadEventConverter.cpp
@@ -0,0 +1,310 @@
+/*
+ * 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 "CapturedTouchpadEventConverter.h"
+
+#include <sstream>
+
+#include <android-base/stringprintf.h>
+#include <gui/constants.h>
+#include <input/PrintTools.h>
+#include <linux/input-event-codes.h>
+#include <log/log_main.h>
+
+namespace android {
+
+namespace {
+
+int32_t actionWithIndex(int32_t action, int32_t index) {
+    return action | (index << AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT);
+}
+
+template <typename T>
+size_t firstUnmarkedBit(T set) {
+    // TODO: replace with std::countr_one from <bit> when that's available
+    LOG_ALWAYS_FATAL_IF(set.all());
+    size_t i = 0;
+    while (set.test(i)) {
+        i++;
+    }
+    return i;
+}
+
+} // namespace
+
+CapturedTouchpadEventConverter::CapturedTouchpadEventConverter(
+        InputReaderContext& readerContext, const InputDeviceContext& deviceContext,
+        MultiTouchMotionAccumulator& motionAccumulator, int32_t deviceId)
+      : mDeviceId(deviceId),
+        mReaderContext(readerContext),
+        mDeviceContext(deviceContext),
+        mMotionAccumulator(motionAccumulator),
+        mHasTouchMinor(deviceContext.hasAbsoluteAxis(ABS_MT_TOUCH_MINOR)),
+        mHasToolMinor(deviceContext.hasAbsoluteAxis(ABS_MT_WIDTH_MINOR)) {
+    RawAbsoluteAxisInfo orientationInfo;
+    deviceContext.getAbsoluteAxisInfo(ABS_MT_ORIENTATION, &orientationInfo);
+    if (orientationInfo.valid) {
+        if (orientationInfo.maxValue > 0) {
+            mOrientationScale = M_PI_2 / orientationInfo.maxValue;
+        } else if (orientationInfo.minValue < 0) {
+            mOrientationScale = -M_PI_2 / orientationInfo.minValue;
+        }
+    }
+
+    // TODO(b/275369880): support touch.pressure.calibration and .scale properties when captured.
+    RawAbsoluteAxisInfo pressureInfo;
+    deviceContext.getAbsoluteAxisInfo(ABS_MT_PRESSURE, &pressureInfo);
+    if (pressureInfo.valid && pressureInfo.maxValue > 0) {
+        mPressureScale = 1.0 / pressureInfo.maxValue;
+    }
+
+    RawAbsoluteAxisInfo touchMajorInfo, toolMajorInfo;
+    deviceContext.getAbsoluteAxisInfo(ABS_MT_TOUCH_MAJOR, &touchMajorInfo);
+    deviceContext.getAbsoluteAxisInfo(ABS_MT_WIDTH_MAJOR, &toolMajorInfo);
+    mHasTouchMajor = touchMajorInfo.valid;
+    mHasToolMajor = toolMajorInfo.valid;
+    if (mHasTouchMajor && touchMajorInfo.maxValue != 0) {
+        mSizeScale = 1.0f / touchMajorInfo.maxValue;
+    } else if (mHasToolMajor && toolMajorInfo.maxValue != 0) {
+        mSizeScale = 1.0f / toolMajorInfo.maxValue;
+    }
+}
+
+std::string CapturedTouchpadEventConverter::dump() const {
+    std::stringstream out;
+    out << "Orientation scale: " << mOrientationScale << "\n";
+    out << "Pressure scale: " << mPressureScale << "\n";
+    out << "Size scale: " << mSizeScale << "\n";
+
+    out << "Dimension axes:";
+    if (mHasTouchMajor) out << " touch major";
+    if (mHasTouchMinor) out << ", touch minor";
+    if (mHasToolMajor) out << ", tool major";
+    if (mHasToolMinor) out << ", tool minor";
+    out << "\n";
+
+    out << "Down time: " << mDownTime << "\n";
+    out << StringPrintf("Button state: 0x%08x\n", mButtonState);
+
+    out << StringPrintf("Pointer IDs in use: %s\n", mPointerIdsInUse.to_string().c_str());
+
+    out << "Pointer IDs for slot numbers:\n";
+    out << addLinePrefix(dumpMap(mPointerIdForSlotNumber), "  ") << "\n";
+    return out.str();
+}
+
+void CapturedTouchpadEventConverter::populateMotionRanges(InputDeviceInfo& info) const {
+    tryAddRawMotionRange(/*byref*/ info, AMOTION_EVENT_AXIS_X, ABS_MT_POSITION_X);
+    tryAddRawMotionRange(/*byref*/ info, AMOTION_EVENT_AXIS_Y, ABS_MT_POSITION_Y);
+    tryAddRawMotionRange(/*byref*/ info, AMOTION_EVENT_AXIS_TOUCH_MAJOR, ABS_MT_TOUCH_MAJOR);
+    tryAddRawMotionRange(/*byref*/ info, AMOTION_EVENT_AXIS_TOUCH_MINOR, ABS_MT_TOUCH_MINOR);
+    tryAddRawMotionRange(/*byref*/ info, AMOTION_EVENT_AXIS_TOOL_MAJOR, ABS_MT_WIDTH_MAJOR);
+    tryAddRawMotionRange(/*byref*/ info, AMOTION_EVENT_AXIS_TOOL_MINOR, ABS_MT_WIDTH_MINOR);
+
+    RawAbsoluteAxisInfo pressureInfo;
+    mDeviceContext.getAbsoluteAxisInfo(ABS_MT_PRESSURE, &pressureInfo);
+    if (pressureInfo.valid) {
+        info.addMotionRange(AMOTION_EVENT_AXIS_PRESSURE, SOURCE, 0, 1, 0, 0, 0);
+    }
+
+    RawAbsoluteAxisInfo orientationInfo;
+    mDeviceContext.getAbsoluteAxisInfo(ABS_MT_ORIENTATION, &orientationInfo);
+    if (orientationInfo.valid && (orientationInfo.maxValue > 0 || orientationInfo.minValue < 0)) {
+        info.addMotionRange(AMOTION_EVENT_AXIS_ORIENTATION, SOURCE, -M_PI_2, M_PI_2, 0, 0, 0);
+    }
+
+    if (mHasTouchMajor || mHasToolMajor) {
+        info.addMotionRange(AMOTION_EVENT_AXIS_SIZE, SOURCE, 0, 1, 0, 0, 0);
+    }
+}
+
+void CapturedTouchpadEventConverter::tryAddRawMotionRange(InputDeviceInfo& deviceInfo,
+                                                          int32_t androidAxis,
+                                                          int32_t evdevAxis) const {
+    RawAbsoluteAxisInfo info;
+    mDeviceContext.getAbsoluteAxisInfo(evdevAxis, &info);
+    if (info.valid) {
+        deviceInfo.addMotionRange(androidAxis, SOURCE, info.minValue, info.maxValue, info.flat,
+                                  info.fuzz, info.resolution);
+    }
+}
+
+void CapturedTouchpadEventConverter::reset() {
+    mCursorButtonAccumulator.reset(mDeviceContext);
+    mDownTime = 0;
+    mPointerIdsInUse.reset();
+    mPointerIdForSlotNumber.clear();
+}
+
+std::list<NotifyArgs> CapturedTouchpadEventConverter::process(const RawEvent& rawEvent) {
+    std::list<NotifyArgs> out;
+    if (rawEvent.type == EV_SYN && rawEvent.code == SYN_REPORT) {
+        out = sync(rawEvent.when, rawEvent.readTime);
+        mMotionAccumulator.finishSync();
+    }
+
+    mCursorButtonAccumulator.process(&rawEvent);
+    mMotionAccumulator.process(&rawEvent);
+    return out;
+}
+
+std::list<NotifyArgs> CapturedTouchpadEventConverter::sync(nsecs_t when, nsecs_t readTime) {
+    std::list<NotifyArgs> out;
+    std::vector<PointerCoords> coords;
+    std::vector<PointerProperties> properties;
+    std::map<size_t, size_t> coordsIndexForSlotNumber;
+
+    // For all the touches that were already down, send a MOVE event with their updated coordinates.
+    // A convention of the MotionEvent API is that pointer coordinates in UP events match the
+    // pointer's coordinates from the previous MOVE, so we still include touches here even if
+    // they've been lifted in this evdev frame.
+    if (!mPointerIdForSlotNumber.empty()) {
+        for (const auto [slotNumber, pointerId] : mPointerIdForSlotNumber) {
+            // Note that we don't check whether the touch has actually moved — it's rare for a touch
+            // to stay perfectly still between frames, and if it does the worst that can happen is
+            // an extra MOVE event, so it's not worth the overhead of checking for changes.
+            coordsIndexForSlotNumber[slotNumber] = coords.size();
+            coords.push_back(makePointerCoordsForSlot(mMotionAccumulator.getSlot(slotNumber)));
+            properties.push_back({.id = pointerId, .toolType = ToolType::FINGER});
+        }
+        out.push_back(
+                makeMotionArgs(when, readTime, AMOTION_EVENT_ACTION_MOVE, coords, properties));
+    }
+
+    std::vector<size_t> upSlots, downSlots;
+    for (size_t i = 0; i < mMotionAccumulator.getSlotCount(); i++) {
+        const MultiTouchMotionAccumulator::Slot& slot = mMotionAccumulator.getSlot(i);
+        // Some touchpads continue to report contacts even after they've identified them as palms.
+        // We don't currently have a way to mark these as palms when reporting to apps, so don't
+        // report them at all.
+        const bool isInUse = slot.isInUse() && slot.getToolType() != ToolType::PALM;
+        const bool wasInUse = mPointerIdForSlotNumber.find(i) != mPointerIdForSlotNumber.end();
+        if (isInUse && !wasInUse) {
+            downSlots.push_back(i);
+        } else if (!isInUse && wasInUse) {
+            upSlots.push_back(i);
+        }
+    }
+
+    // For any touches that were lifted, send UP or POINTER_UP events.
+    for (size_t slotNumber : upSlots) {
+        const size_t indexToRemove = coordsIndexForSlotNumber.at(slotNumber);
+        const bool cancel = mMotionAccumulator.getSlot(slotNumber).getToolType() == ToolType::PALM;
+        int32_t action;
+        if (coords.size() == 1) {
+            action = cancel ? AMOTION_EVENT_ACTION_CANCEL : AMOTION_EVENT_ACTION_UP;
+        } else {
+            action = actionWithIndex(AMOTION_EVENT_ACTION_POINTER_UP, indexToRemove);
+        }
+        out.push_back(makeMotionArgs(when, readTime, action, coords, properties, /*actionButton=*/0,
+                                     /*flags=*/cancel ? AMOTION_EVENT_FLAG_CANCELED : 0));
+
+        freePointerIdForSlot(slotNumber);
+        coords.erase(coords.begin() + indexToRemove);
+        properties.erase(properties.begin() + indexToRemove);
+        // Now that we've removed some coords and properties, we might have to update the slot
+        // number to coords index mapping.
+        coordsIndexForSlotNumber.erase(slotNumber);
+        for (auto& [_, index] : coordsIndexForSlotNumber) {
+            if (index > indexToRemove) {
+                index--;
+            }
+        }
+    }
+
+    // For new touches, send DOWN or POINTER_DOWN events.
+    for (size_t slotNumber : downSlots) {
+        const size_t coordsIndex = coords.size();
+        const int32_t action = coords.empty()
+                ? AMOTION_EVENT_ACTION_DOWN
+                : actionWithIndex(AMOTION_EVENT_ACTION_POINTER_DOWN, coordsIndex);
+
+        coordsIndexForSlotNumber[slotNumber] = coordsIndex;
+        coords.push_back(makePointerCoordsForSlot(mMotionAccumulator.getSlot(slotNumber)));
+        properties.push_back(
+                {.id = allocatePointerIdToSlot(slotNumber), .toolType = ToolType::FINGER});
+
+        out.push_back(makeMotionArgs(when, readTime, action, coords, properties));
+    }
+
+    const uint32_t newButtonState = mCursorButtonAccumulator.getButtonState();
+    for (uint32_t button = 1; button <= AMOTION_EVENT_BUTTON_FORWARD; button <<= 1) {
+        if (newButtonState & button && !(mButtonState & button)) {
+            mButtonState |= button;
+            out.push_back(makeMotionArgs(when, readTime, AMOTION_EVENT_ACTION_BUTTON_PRESS, coords,
+                                         properties, /*actionButton=*/button));
+        } else if (!(newButtonState & button) && mButtonState & button) {
+            mButtonState &= ~button;
+            out.push_back(makeMotionArgs(when, readTime, AMOTION_EVENT_ACTION_BUTTON_RELEASE,
+                                         coords, properties, /*actionButton=*/button));
+        }
+    }
+    return out;
+}
+
+NotifyMotionArgs CapturedTouchpadEventConverter::makeMotionArgs(
+        nsecs_t when, nsecs_t readTime, int32_t action, const std::vector<PointerCoords>& coords,
+        const std::vector<PointerProperties>& properties, int32_t actionButton, int32_t flags) {
+    LOG_ALWAYS_FATAL_IF(coords.size() != properties.size(),
+                        "Mismatched coords and properties arrays.");
+    return NotifyMotionArgs(mReaderContext.getNextId(), when, readTime, mDeviceId, SOURCE,
+                            ADISPLAY_ID_NONE, /*policyFlags=*/POLICY_FLAG_WAKE, action,
+                            /*actionButton=*/actionButton, flags,
+                            mReaderContext.getGlobalMetaState(), mButtonState,
+                            MotionClassification::NONE, AMOTION_EVENT_EDGE_FLAG_NONE, coords.size(),
+                            properties.data(), coords.data(), /*xPrecision=*/1.0f,
+                            /*yPrecision=*/1.0f, AMOTION_EVENT_INVALID_CURSOR_POSITION,
+                            AMOTION_EVENT_INVALID_CURSOR_POSITION, mDownTime, /*videoFrames=*/{});
+}
+
+PointerCoords CapturedTouchpadEventConverter::makePointerCoordsForSlot(
+        const MultiTouchMotionAccumulator::Slot& slot) const {
+    PointerCoords coords;
+    coords.clear();
+    coords.setAxisValue(AMOTION_EVENT_AXIS_X, slot.getX());
+    coords.setAxisValue(AMOTION_EVENT_AXIS_Y, slot.getY());
+    coords.setAxisValue(AMOTION_EVENT_AXIS_TOUCH_MAJOR, slot.getTouchMajor());
+    coords.setAxisValue(AMOTION_EVENT_AXIS_TOUCH_MINOR, slot.getTouchMinor());
+    coords.setAxisValue(AMOTION_EVENT_AXIS_TOOL_MAJOR, slot.getToolMajor());
+    coords.setAxisValue(AMOTION_EVENT_AXIS_TOOL_MINOR, slot.getToolMinor());
+    coords.setAxisValue(AMOTION_EVENT_AXIS_ORIENTATION, slot.getOrientation() * mOrientationScale);
+    coords.setAxisValue(AMOTION_EVENT_AXIS_PRESSURE, slot.getPressure() * mPressureScale);
+    float size = 0;
+    // TODO(b/275369880): support touch.size.calibration and .isSummed properties when captured.
+    if (mHasTouchMajor) {
+        size = mHasTouchMinor ? (slot.getTouchMajor() + slot.getTouchMinor()) / 2
+                              : slot.getTouchMajor();
+    } else if (mHasToolMajor) {
+        size = mHasToolMinor ? (slot.getToolMajor() + slot.getToolMinor()) / 2
+                             : slot.getToolMajor();
+    }
+    coords.setAxisValue(AMOTION_EVENT_AXIS_SIZE, size * mSizeScale);
+    return coords;
+}
+
+int32_t CapturedTouchpadEventConverter::allocatePointerIdToSlot(size_t slotNumber) {
+    const int32_t pointerId = firstUnmarkedBit(mPointerIdsInUse);
+    mPointerIdsInUse.set(pointerId);
+    mPointerIdForSlotNumber[slotNumber] = pointerId;
+    return pointerId;
+}
+
+void CapturedTouchpadEventConverter::freePointerIdForSlot(size_t slotNumber) {
+    mPointerIdsInUse.reset(mPointerIdForSlotNumber.at(slotNumber));
+    mPointerIdForSlotNumber.erase(slotNumber);
+}
+
+} // namespace android
diff --git a/services/inputflinger/reader/mapper/CapturedTouchpadEventConverter.h b/services/inputflinger/reader/mapper/CapturedTouchpadEventConverter.h
new file mode 100644
index 0000000..9b6df7a
--- /dev/null
+++ b/services/inputflinger/reader/mapper/CapturedTouchpadEventConverter.h
@@ -0,0 +1,83 @@
+/*
+ * 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 <bitset>
+#include <list>
+#include <map>
+#include <set>
+#include <string>
+#include <vector>
+
+#include <android/input.h>
+#include <input/Input.h>
+#include <utils/Timers.h>
+
+#include "EventHub.h"
+#include "InputDevice.h"
+#include "accumulator/CursorButtonAccumulator.h"
+#include "accumulator/MultiTouchMotionAccumulator.h"
+#include "accumulator/TouchButtonAccumulator.h"
+
+namespace android {
+
+class CapturedTouchpadEventConverter {
+public:
+    explicit CapturedTouchpadEventConverter(InputReaderContext& readerContext,
+                                            const InputDeviceContext& deviceContext,
+                                            MultiTouchMotionAccumulator& motionAccumulator,
+                                            int32_t deviceId);
+    std::string dump() const;
+    void populateMotionRanges(InputDeviceInfo& info) const;
+    void reset();
+    [[nodiscard]] std::list<NotifyArgs> process(const RawEvent& rawEvent);
+
+private:
+    void tryAddRawMotionRange(InputDeviceInfo& deviceInfo, int32_t androidAxis,
+                              int32_t evdevAxis) const;
+    [[nodiscard]] std::list<NotifyArgs> sync(nsecs_t when, nsecs_t readTime);
+    [[nodiscard]] NotifyMotionArgs makeMotionArgs(nsecs_t when, nsecs_t readTime, int32_t action,
+                                                  const std::vector<PointerCoords>& coords,
+                                                  const std::vector<PointerProperties>& properties,
+                                                  int32_t actionButton = 0, int32_t flags = 0);
+    PointerCoords makePointerCoordsForSlot(const MultiTouchMotionAccumulator::Slot& slot) const;
+    int32_t allocatePointerIdToSlot(size_t slotNumber);
+    void freePointerIdForSlot(size_t slotNumber);
+
+    const int32_t mDeviceId;
+    InputReaderContext& mReaderContext;
+    const InputDeviceContext& mDeviceContext;
+    CursorButtonAccumulator mCursorButtonAccumulator;
+    MultiTouchMotionAccumulator& mMotionAccumulator;
+
+    float mOrientationScale = 0;
+    float mPressureScale = 1;
+    float mSizeScale = 0;
+    bool mHasTouchMajor;
+    const bool mHasTouchMinor;
+    bool mHasToolMajor;
+    const bool mHasToolMinor;
+    nsecs_t mDownTime = 0;
+    uint32_t mButtonState = 0;
+
+    std::bitset<MAX_POINTER_ID + 1> mPointerIdsInUse;
+    std::map<size_t, int32_t> mPointerIdForSlotNumber;
+
+    static constexpr uint32_t SOURCE = AINPUT_SOURCE_TOUCHPAD;
+};
+
+} // namespace android
diff --git a/services/inputflinger/reader/mapper/TouchpadInputMapper.cpp b/services/inputflinger/reader/mapper/TouchpadInputMapper.cpp
index 8753b48..a5da3cd 100644
--- a/services/inputflinger/reader/mapper/TouchpadInputMapper.cpp
+++ b/services/inputflinger/reader/mapper/TouchpadInputMapper.cpp
@@ -16,9 +16,11 @@
 
 #include "../Macros.h"
 
+#include <chrono>
 #include <limits>
 #include <optional>
 
+#include <android-base/stringprintf.h>
 #include <android/input.h>
 #include <ftl/enum.h>
 #include <input/PrintTools.h>
@@ -174,8 +176,18 @@
       : InputMapper(deviceContext, readerConfig),
         mGestureInterpreter(NewGestureInterpreter(), DeleteGestureInterpreter),
         mPointerController(getContext()->getPointerController(getDeviceId())),
-        mStateConverter(deviceContext),
-        mGestureConverter(*getContext(), deviceContext, getDeviceId()) {
+        mStateConverter(deviceContext, mMotionAccumulator),
+        mGestureConverter(*getContext(), deviceContext, getDeviceId()),
+        mCapturedEventConverter(*getContext(), deviceContext, mMotionAccumulator, getDeviceId()) {
+    RawAbsoluteAxisInfo slotAxisInfo;
+    deviceContext.getAbsoluteAxisInfo(ABS_MT_SLOT, &slotAxisInfo);
+    if (!slotAxisInfo.valid || slotAxisInfo.maxValue <= 0) {
+        ALOGW("Touchpad \"%s\" doesn't have a valid ABS_MT_SLOT axis, and probably won't work "
+              "properly.",
+              deviceContext.getName().c_str());
+    }
+    mMotionAccumulator.configure(deviceContext, slotAxisInfo.maxValue + 1, true);
+
     mGestureInterpreter->Initialize(GESTURES_DEVCLASS_TOUCHPAD);
     mGestureInterpreter->SetHardwareProperties(createHardwareProperties(deviceContext));
     // Even though we don't explicitly delete copy/move semantics, it's safe to
@@ -209,15 +221,28 @@
 
 void TouchpadInputMapper::populateDeviceInfo(InputDeviceInfo& info) {
     InputMapper::populateDeviceInfo(info);
-    mGestureConverter.populateMotionRanges(info);
+    if (mPointerCaptured) {
+        mCapturedEventConverter.populateMotionRanges(info);
+    } else {
+        mGestureConverter.populateMotionRanges(info);
+    }
 }
 
 void TouchpadInputMapper::dump(std::string& dump) {
     dump += INDENT2 "Touchpad Input Mapper:\n";
+    if (mProcessing) {
+        dump += INDENT3 "Currently processing a hardware state\n";
+    }
+    if (mResettingInterpreter) {
+        dump += INDENT3 "Currently resetting gesture interpreter\n";
+    }
+    dump += StringPrintf(INDENT3 "Pointer captured: %s\n", toString(mPointerCaptured));
     dump += INDENT3 "Gesture converter:\n";
     dump += addLinePrefix(mGestureConverter.dump(), INDENT4);
     dump += INDENT3 "Gesture properties:\n";
     dump += addLinePrefix(mPropertyProvider.dump(), INDENT4);
+    dump += INDENT3 "Captured event converter:\n";
+    dump += addLinePrefix(mCapturedEventConverter.dump(), INDENT4);
 }
 
 std::list<NotifyArgs> TouchpadInputMapper::reconfigure(nsecs_t when,
@@ -252,17 +277,50 @@
         mPropertyProvider.getProperty("Button Right Click Zone Enable")
                 .setBoolValues({config.touchpadRightClickZoneEnabled});
     }
-    return {};
+    std::list<NotifyArgs> out;
+    if ((!changes.any() && config.pointerCaptureRequest.enable) ||
+        changes.test(InputReaderConfiguration::Change::POINTER_CAPTURE)) {
+        mPointerCaptured = config.pointerCaptureRequest.enable;
+        // The motion ranges are going to change, so bump the generation to clear the cached ones.
+        bumpGeneration();
+        if (mPointerCaptured) {
+            // The touchpad is being captured, so we need to tidy up any fake fingers etc. that are
+            // still being reported for a gesture in progress.
+            out += reset(when);
+            mPointerController->fade(PointerControllerInterface::Transition::IMMEDIATE);
+        } else {
+            // We're transitioning from captured to uncaptured.
+            mCapturedEventConverter.reset();
+        }
+        if (changes.any()) {
+            out.push_back(NotifyDeviceResetArgs(getContext()->getNextId(), when, getDeviceId()));
+        }
+    }
+    return out;
 }
 
 std::list<NotifyArgs> TouchpadInputMapper::reset(nsecs_t when) {
     mStateConverter.reset();
+    resetGestureInterpreter(when);
     std::list<NotifyArgs> out = mGestureConverter.reset(when);
     out += InputMapper::reset(when);
     return out;
 }
 
+void TouchpadInputMapper::resetGestureInterpreter(nsecs_t when) {
+    // The GestureInterpreter has no official reset method, but sending a HardwareState with no
+    // fingers down or buttons pressed should get it into a clean state.
+    HardwareState state;
+    state.timestamp = std::chrono::duration<stime_t>(std::chrono::nanoseconds(when)).count();
+    mResettingInterpreter = true;
+    mGestureInterpreter->PushHardwareState(&state);
+    mResettingInterpreter = false;
+}
+
 std::list<NotifyArgs> TouchpadInputMapper::process(const RawEvent* rawEvent) {
+    if (mPointerCaptured) {
+        return mCapturedEventConverter.process(*rawEvent);
+    }
     std::optional<SelfContainedHardwareState> state = mStateConverter.processRawEvent(rawEvent);
     if (state) {
         return sendHardwareState(rawEvent->when, rawEvent->readTime, *state);
@@ -283,6 +341,11 @@
 
 void TouchpadInputMapper::consumeGesture(const Gesture* gesture) {
     ALOGD_IF(DEBUG_TOUCHPAD_GESTURES, "Gesture ready: %s", gesture->String().c_str());
+    if (mResettingInterpreter) {
+        // We already handle tidying up fake fingers etc. in GestureConverter::reset, so we should
+        // ignore any gestures produced from the interpreter while we're resetting it.
+        return;
+    }
     if (!mProcessing) {
         ALOGE("Received gesture outside of the normal processing flow; ignoring it.");
         return;
diff --git a/services/inputflinger/reader/mapper/TouchpadInputMapper.h b/services/inputflinger/reader/mapper/TouchpadInputMapper.h
index 268b275..3128d18 100644
--- a/services/inputflinger/reader/mapper/TouchpadInputMapper.h
+++ b/services/inputflinger/reader/mapper/TouchpadInputMapper.h
@@ -21,12 +21,15 @@
 #include <vector>
 
 #include <PointerControllerInterface.h>
+#include <utils/Timers.h>
 
+#include "CapturedTouchpadEventConverter.h"
 #include "EventHub.h"
 #include "InputDevice.h"
 #include "InputMapper.h"
 #include "InputReaderBase.h"
 #include "NotifyArgs.h"
+#include "accumulator/MultiTouchMotionAccumulator.h"
 #include "gestures/GestureConverter.h"
 #include "gestures/HardwareStateConverter.h"
 #include "gestures/PropertyProvider.h"
@@ -54,6 +57,7 @@
     void consumeGesture(const Gesture* gesture);
 
 private:
+    void resetGestureInterpreter(nsecs_t when);
     [[nodiscard]] std::list<NotifyArgs> sendHardwareState(nsecs_t when, nsecs_t readTime,
                                                           SelfContainedHardwareState schs);
     [[nodiscard]] std::list<NotifyArgs> processGestures(nsecs_t when, nsecs_t readTime);
@@ -64,10 +68,19 @@
 
     PropertyProvider mPropertyProvider;
 
+    // The MultiTouchMotionAccumulator is shared between the HardwareStateConverter and
+    // CapturedTouchpadEventConverter, so that if the touchpad is captured or released while touches
+    // are down, the relevant converter can still benefit from the current axis values stored in the
+    // accumulator.
+    MultiTouchMotionAccumulator mMotionAccumulator;
+
     HardwareStateConverter mStateConverter;
     GestureConverter mGestureConverter;
+    CapturedTouchpadEventConverter mCapturedEventConverter;
 
+    bool mPointerCaptured = false;
     bool mProcessing = false;
+    bool mResettingInterpreter = false;
     std::vector<Gesture> mGesturesToProcess;
 };
 
diff --git a/services/inputflinger/reader/mapper/gestures/HardwareStateConverter.cpp b/services/inputflinger/reader/mapper/gestures/HardwareStateConverter.cpp
index 8841b6e..6780dce 100644
--- a/services/inputflinger/reader/mapper/gestures/HardwareStateConverter.cpp
+++ b/services/inputflinger/reader/mapper/gestures/HardwareStateConverter.cpp
@@ -26,16 +26,11 @@
 
 namespace android {
 
-HardwareStateConverter::HardwareStateConverter(const InputDeviceContext& deviceContext)
-      : mDeviceContext(deviceContext), mTouchButtonAccumulator(deviceContext) {
-    RawAbsoluteAxisInfo slotAxisInfo;
-    deviceContext.getAbsoluteAxisInfo(ABS_MT_SLOT, &slotAxisInfo);
-    if (!slotAxisInfo.valid || slotAxisInfo.maxValue <= 0) {
-        ALOGW("Touchpad \"%s\" doesn't have a valid ABS_MT_SLOT axis, and probably won't work "
-              "properly.",
-              deviceContext.getName().c_str());
-    }
-    mMotionAccumulator.configure(deviceContext, slotAxisInfo.maxValue + 1, true);
+HardwareStateConverter::HardwareStateConverter(const InputDeviceContext& deviceContext,
+                                               MultiTouchMotionAccumulator& motionAccumulator)
+      : mDeviceContext(deviceContext),
+        mMotionAccumulator(motionAccumulator),
+        mTouchButtonAccumulator(deviceContext) {
     mTouchButtonAccumulator.configure();
 }
 
diff --git a/services/inputflinger/reader/mapper/gestures/HardwareStateConverter.h b/services/inputflinger/reader/mapper/gestures/HardwareStateConverter.h
index c314b0d..633448e 100644
--- a/services/inputflinger/reader/mapper/gestures/HardwareStateConverter.h
+++ b/services/inputflinger/reader/mapper/gestures/HardwareStateConverter.h
@@ -41,7 +41,8 @@
 // Converts RawEvents into the HardwareState structs used by the gestures library.
 class HardwareStateConverter {
 public:
-    HardwareStateConverter(const InputDeviceContext& deviceContext);
+    HardwareStateConverter(const InputDeviceContext& deviceContext,
+                           MultiTouchMotionAccumulator& motionAccumulator);
 
     std::optional<SelfContainedHardwareState> processRawEvent(const RawEvent* event);
     void reset();
@@ -51,7 +52,7 @@
 
     const InputDeviceContext& mDeviceContext;
     CursorButtonAccumulator mCursorButtonAccumulator;
-    MultiTouchMotionAccumulator mMotionAccumulator;
+    MultiTouchMotionAccumulator& mMotionAccumulator;
     TouchButtonAccumulator mTouchButtonAccumulator;
     int32_t mMscTimestamp = 0;
 };
diff --git a/services/inputflinger/tests/Android.bp b/services/inputflinger/tests/Android.bp
index 97138c7..52277ff 100644
--- a/services/inputflinger/tests/Android.bp
+++ b/services/inputflinger/tests/Android.bp
@@ -39,6 +39,7 @@
     srcs: [
         "AnrTracker_test.cpp",
         "BlockingQueue_test.cpp",
+        "CapturedTouchpadEventConverter_test.cpp",
         "EventHub_test.cpp",
         "FakeEventHub.cpp",
         "FakeInputReaderPolicy.cpp",
diff --git a/services/inputflinger/tests/CapturedTouchpadEventConverter_test.cpp b/services/inputflinger/tests/CapturedTouchpadEventConverter_test.cpp
new file mode 100644
index 0000000..3dc5152
--- /dev/null
+++ b/services/inputflinger/tests/CapturedTouchpadEventConverter_test.cpp
@@ -0,0 +1,784 @@
+/*
+ * 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 <CapturedTouchpadEventConverter.h>
+
+#include <list>
+#include <memory>
+
+#include <EventHub.h>
+#include <gtest/gtest.h>
+#include <linux/input-event-codes.h>
+#include <linux/input.h>
+#include <utils/StrongPointer.h>
+
+#include "FakeEventHub.h"
+#include "FakeInputReaderPolicy.h"
+#include "InstrumentedInputReader.h"
+#include "TestConstants.h"
+#include "TestInputListener.h"
+#include "TestInputListenerMatchers.h"
+
+namespace android {
+
+using testing::AllOf;
+
+class CapturedTouchpadEventConverterTest : public testing::Test {
+public:
+    CapturedTouchpadEventConverterTest()
+          : mFakeEventHub(std::make_unique<FakeEventHub>()),
+            mFakePolicy(sp<FakeInputReaderPolicy>::make()),
+            mReader(mFakeEventHub, mFakePolicy, mFakeListener),
+            mDevice(newDevice()),
+            mDeviceContext(*mDevice, EVENTHUB_ID) {
+        const size_t slotCount = 8;
+        mFakeEventHub->addAbsoluteAxis(EVENTHUB_ID, ABS_MT_SLOT, 0, slotCount - 1, 0, 0, 0);
+        mAccumulator.configure(mDeviceContext, slotCount, /*usingSlotsProtocol=*/true);
+    }
+
+protected:
+    static constexpr int32_t DEVICE_ID = END_RESERVED_ID + 1000;
+    static constexpr int32_t EVENTHUB_ID = 1;
+
+    std::shared_ptr<InputDevice> newDevice() {
+        InputDeviceIdentifier identifier;
+        identifier.name = "device";
+        identifier.location = "USB1";
+        identifier.bus = 0;
+        std::shared_ptr<InputDevice> device =
+                std::make_shared<InputDevice>(mReader.getContext(), DEVICE_ID, /*generation=*/2,
+                                              identifier);
+        mReader.pushNextDevice(device);
+        mFakeEventHub->addDevice(EVENTHUB_ID, identifier.name, InputDeviceClass::TOUCHPAD,
+                                 identifier.bus);
+        mReader.loopOnce();
+        return device;
+    }
+
+    void addBasicAxesToEventHub() {
+        mFakeEventHub->addAbsoluteAxis(EVENTHUB_ID, ABS_MT_POSITION_X, 0, 4000, 0, 0, 45);
+        mFakeEventHub->addAbsoluteAxis(EVENTHUB_ID, ABS_MT_POSITION_Y, 0, 2500, 0, 0, 40);
+        mFakeEventHub->addAbsoluteAxis(EVENTHUB_ID, ABS_MT_PRESSURE, 0, 256, 0, 0, 0);
+        mFakeEventHub->addAbsoluteAxis(EVENTHUB_ID, ABS_MT_TOUCH_MAJOR, 0, 1000, 0, 0, 0);
+        mFakeEventHub->addAbsoluteAxis(EVENTHUB_ID, ABS_MT_TOUCH_MINOR, 0, 1000, 0, 0, 0);
+    }
+
+    CapturedTouchpadEventConverter createConverter() {
+        addBasicAxesToEventHub();
+        return CapturedTouchpadEventConverter(*mReader.getContext(), mDeviceContext, mAccumulator,
+                                              DEVICE_ID);
+    }
+
+    void processAxis(CapturedTouchpadEventConverter& conv, int32_t type, int32_t code,
+                     int32_t value) {
+        RawEvent event;
+        event.when = ARBITRARY_TIME;
+        event.readTime = READ_TIME;
+        event.deviceId = EVENTHUB_ID;
+        event.type = type;
+        event.code = code;
+        event.value = value;
+        std::list<NotifyArgs> out = conv.process(event);
+        EXPECT_TRUE(out.empty());
+    }
+
+    std::list<NotifyArgs> processSync(CapturedTouchpadEventConverter& conv) {
+        RawEvent event;
+        event.when = ARBITRARY_TIME;
+        event.readTime = READ_TIME;
+        event.deviceId = EVENTHUB_ID;
+        event.type = EV_SYN;
+        event.code = SYN_REPORT;
+        event.value = 0;
+        return conv.process(event);
+    }
+
+    NotifyMotionArgs processSyncAndExpectSingleMotionArg(CapturedTouchpadEventConverter& conv) {
+        std::list<NotifyArgs> args = processSync(conv);
+        EXPECT_EQ(1u, args.size());
+        return std::get<NotifyMotionArgs>(args.front());
+    }
+
+    std::shared_ptr<FakeEventHub> mFakeEventHub;
+    sp<FakeInputReaderPolicy> mFakePolicy;
+    TestInputListener mFakeListener;
+    InstrumentedInputReader mReader;
+    std::shared_ptr<InputDevice> mDevice;
+    InputDeviceContext mDeviceContext;
+    MultiTouchMotionAccumulator mAccumulator;
+};
+
+TEST_F(CapturedTouchpadEventConverterTest, MotionRanges_allAxesPresent_populatedCorrectly) {
+    mFakeEventHub->addAbsoluteAxis(EVENTHUB_ID, ABS_MT_POSITION_X, 0, 4000, 0, 0, 45);
+    mFakeEventHub->addAbsoluteAxis(EVENTHUB_ID, ABS_MT_POSITION_Y, 0, 2500, 0, 0, 40);
+    mFakeEventHub->addAbsoluteAxis(EVENTHUB_ID, ABS_MT_TOUCH_MAJOR, 0, 1100, 0, 0, 35);
+    mFakeEventHub->addAbsoluteAxis(EVENTHUB_ID, ABS_MT_TOUCH_MINOR, 0, 1000, 0, 0, 30);
+    mFakeEventHub->addAbsoluteAxis(EVENTHUB_ID, ABS_MT_WIDTH_MAJOR, 0, 900, 0, 0, 25);
+    mFakeEventHub->addAbsoluteAxis(EVENTHUB_ID, ABS_MT_WIDTH_MINOR, 0, 800, 0, 0, 20);
+    mFakeEventHub->addAbsoluteAxis(EVENTHUB_ID, ABS_MT_ORIENTATION, -3, 4, 0, 0, 0);
+    mFakeEventHub->addAbsoluteAxis(EVENTHUB_ID, ABS_MT_PRESSURE, 0, 256, 0, 0, 0);
+    CapturedTouchpadEventConverter conv(*mReader.getContext(), mDeviceContext, mAccumulator,
+                                        DEVICE_ID);
+
+    InputDeviceInfo info;
+    conv.populateMotionRanges(info);
+
+    // Most axes should have min, max, and resolution matching the evdev axes.
+    const InputDeviceInfo::MotionRange* posX =
+            info.getMotionRange(AMOTION_EVENT_AXIS_X, AINPUT_SOURCE_TOUCHPAD);
+    ASSERT_NE(nullptr, posX);
+    EXPECT_NEAR(0, posX->min, EPSILON);
+    EXPECT_NEAR(4000, posX->max, EPSILON);
+    EXPECT_NEAR(45, posX->resolution, EPSILON);
+
+    const InputDeviceInfo::MotionRange* posY =
+            info.getMotionRange(AMOTION_EVENT_AXIS_Y, AINPUT_SOURCE_TOUCHPAD);
+    ASSERT_NE(nullptr, posY);
+    EXPECT_NEAR(0, posY->min, EPSILON);
+    EXPECT_NEAR(2500, posY->max, EPSILON);
+    EXPECT_NEAR(40, posY->resolution, EPSILON);
+
+    const InputDeviceInfo::MotionRange* touchMajor =
+            info.getMotionRange(AMOTION_EVENT_AXIS_TOUCH_MAJOR, AINPUT_SOURCE_TOUCHPAD);
+    ASSERT_NE(nullptr, touchMajor);
+    EXPECT_NEAR(0, touchMajor->min, EPSILON);
+    EXPECT_NEAR(1100, touchMajor->max, EPSILON);
+    EXPECT_NEAR(35, touchMajor->resolution, EPSILON);
+
+    const InputDeviceInfo::MotionRange* touchMinor =
+            info.getMotionRange(AMOTION_EVENT_AXIS_TOUCH_MINOR, AINPUT_SOURCE_TOUCHPAD);
+    ASSERT_NE(nullptr, touchMinor);
+    EXPECT_NEAR(0, touchMinor->min, EPSILON);
+    EXPECT_NEAR(1000, touchMinor->max, EPSILON);
+    EXPECT_NEAR(30, touchMinor->resolution, EPSILON);
+
+    const InputDeviceInfo::MotionRange* toolMajor =
+            info.getMotionRange(AMOTION_EVENT_AXIS_TOOL_MAJOR, AINPUT_SOURCE_TOUCHPAD);
+    ASSERT_NE(nullptr, toolMajor);
+    EXPECT_NEAR(0, toolMajor->min, EPSILON);
+    EXPECT_NEAR(900, toolMajor->max, EPSILON);
+    EXPECT_NEAR(25, toolMajor->resolution, EPSILON);
+
+    const InputDeviceInfo::MotionRange* toolMinor =
+            info.getMotionRange(AMOTION_EVENT_AXIS_TOOL_MINOR, AINPUT_SOURCE_TOUCHPAD);
+    ASSERT_NE(nullptr, toolMinor);
+    EXPECT_NEAR(0, toolMinor->min, EPSILON);
+    EXPECT_NEAR(800, toolMinor->max, EPSILON);
+    EXPECT_NEAR(20, toolMinor->resolution, EPSILON);
+
+    // ...except orientation and pressure, which get scaled, and size, which is generated from other
+    // values.
+    const InputDeviceInfo::MotionRange* orientation =
+            info.getMotionRange(AMOTION_EVENT_AXIS_ORIENTATION, AINPUT_SOURCE_TOUCHPAD);
+    ASSERT_NE(nullptr, orientation);
+    EXPECT_NEAR(-M_PI_2, orientation->min, EPSILON);
+    EXPECT_NEAR(M_PI_2, orientation->max, EPSILON);
+    EXPECT_NEAR(0, orientation->resolution, EPSILON);
+
+    const InputDeviceInfo::MotionRange* pressure =
+            info.getMotionRange(AMOTION_EVENT_AXIS_PRESSURE, AINPUT_SOURCE_TOUCHPAD);
+    ASSERT_NE(nullptr, pressure);
+    EXPECT_NEAR(0, pressure->min, EPSILON);
+    EXPECT_NEAR(1, pressure->max, EPSILON);
+    EXPECT_NEAR(0, pressure->resolution, EPSILON);
+
+    const InputDeviceInfo::MotionRange* size =
+            info.getMotionRange(AMOTION_EVENT_AXIS_SIZE, AINPUT_SOURCE_TOUCHPAD);
+    ASSERT_NE(nullptr, size);
+    EXPECT_NEAR(0, size->min, EPSILON);
+    EXPECT_NEAR(1, size->max, EPSILON);
+    EXPECT_NEAR(0, size->resolution, EPSILON);
+}
+
+TEST_F(CapturedTouchpadEventConverterTest, MotionRanges_bareMinimumAxesPresent_populatedCorrectly) {
+    mFakeEventHub->addAbsoluteAxis(EVENTHUB_ID, ABS_MT_POSITION_X, 0, 4000, 0, 0, 45);
+    mFakeEventHub->addAbsoluteAxis(EVENTHUB_ID, ABS_MT_POSITION_Y, 0, 2500, 0, 0, 40);
+    CapturedTouchpadEventConverter conv(*mReader.getContext(), mDeviceContext, mAccumulator,
+                                        DEVICE_ID);
+
+    InputDeviceInfo info;
+    conv.populateMotionRanges(info);
+
+    // Only the bare minimum motion ranges should be reported, and no others (e.g. size shouldn't be
+    // present, since it's generated from axes that aren't provided by this device).
+    EXPECT_NE(nullptr, info.getMotionRange(AMOTION_EVENT_AXIS_X, AINPUT_SOURCE_TOUCHPAD));
+    EXPECT_NE(nullptr, info.getMotionRange(AMOTION_EVENT_AXIS_Y, AINPUT_SOURCE_TOUCHPAD));
+    EXPECT_EQ(2u, info.getMotionRanges().size());
+}
+
+TEST_F(CapturedTouchpadEventConverterTest, OneFinger_motionReportedCorrectly) {
+    CapturedTouchpadEventConverter conv = createConverter();
+
+    processAxis(conv, EV_ABS, ABS_MT_SLOT, 0);
+    processAxis(conv, EV_ABS, ABS_MT_TRACKING_ID, 1);
+    processAxis(conv, EV_ABS, ABS_MT_POSITION_X, 50);
+    processAxis(conv, EV_ABS, ABS_MT_POSITION_Y, 100);
+
+    processAxis(conv, EV_KEY, BTN_TOUCH, 1);
+    processAxis(conv, EV_KEY, BTN_TOOL_FINGER, 1);
+
+    EXPECT_THAT(processSyncAndExpectSingleMotionArg(conv),
+                AllOf(WithMotionAction(AMOTION_EVENT_ACTION_DOWN), WithPointerCount(1u),
+                      WithCoords(50, 100), WithToolType(ToolType::FINGER)));
+
+    processAxis(conv, EV_ABS, ABS_MT_POSITION_X, 52);
+    processAxis(conv, EV_ABS, ABS_MT_POSITION_Y, 99);
+
+    EXPECT_THAT(processSyncAndExpectSingleMotionArg(conv),
+                AllOf(WithMotionAction(AMOTION_EVENT_ACTION_MOVE), WithPointerCount(1u),
+                      WithCoords(52, 99), WithToolType(ToolType::FINGER)));
+
+    processAxis(conv, EV_ABS, ABS_MT_TRACKING_ID, -1);
+    processAxis(conv, EV_KEY, BTN_TOUCH, 0);
+    processAxis(conv, EV_KEY, BTN_TOOL_FINGER, 0);
+
+    std::list<NotifyArgs> args = processSync(conv);
+    ASSERT_EQ(2u, args.size());
+    EXPECT_THAT(std::get<NotifyMotionArgs>(args.front()),
+                AllOf(WithMotionAction(AMOTION_EVENT_ACTION_MOVE), WithPointerCount(1u),
+                      WithCoords(52, 99), WithToolType(ToolType::FINGER)));
+    args.pop_front();
+    EXPECT_THAT(std::get<NotifyMotionArgs>(args.front()),
+                AllOf(WithMotionAction(AMOTION_EVENT_ACTION_UP), WithPointerCount(1u),
+                      WithCoords(52, 99), WithToolType(ToolType::FINGER)));
+}
+
+TEST_F(CapturedTouchpadEventConverterTest, OneFinger_touchDimensionsPassedThrough) {
+    addBasicAxesToEventHub();
+    mFakeEventHub->addAbsoluteAxis(EVENTHUB_ID, ABS_MT_WIDTH_MAJOR, 0, 1000, 0, 0, 0);
+    mFakeEventHub->addAbsoluteAxis(EVENTHUB_ID, ABS_MT_WIDTH_MINOR, 0, 1000, 0, 0, 0);
+    CapturedTouchpadEventConverter conv(*mReader.getContext(), mDeviceContext, mAccumulator,
+                                        DEVICE_ID);
+
+    processAxis(conv, EV_ABS, ABS_MT_SLOT, 0);
+    processAxis(conv, EV_ABS, ABS_MT_TRACKING_ID, 1);
+    processAxis(conv, EV_ABS, ABS_MT_TOUCH_MAJOR, 250);
+    processAxis(conv, EV_ABS, ABS_MT_TOUCH_MINOR, 120);
+    processAxis(conv, EV_ABS, ABS_MT_WIDTH_MAJOR, 400);
+    processAxis(conv, EV_ABS, ABS_MT_WIDTH_MINOR, 200);
+
+    processAxis(conv, EV_KEY, BTN_TOUCH, 1);
+    processAxis(conv, EV_KEY, BTN_TOOL_FINGER, 1);
+
+    EXPECT_THAT(processSyncAndExpectSingleMotionArg(conv),
+                AllOf(WithMotionAction(AMOTION_EVENT_ACTION_DOWN), WithPointerCount(1u),
+                      WithTouchDimensions(250, 120), WithToolDimensions(400, 200)));
+}
+
+TEST_F(CapturedTouchpadEventConverterTest, OneFinger_orientationCalculatedCorrectly) {
+    addBasicAxesToEventHub();
+    mFakeEventHub->addAbsoluteAxis(EVENTHUB_ID, ABS_MT_ORIENTATION, -3, 4, 0, 0, 0);
+    CapturedTouchpadEventConverter conv(*mReader.getContext(), mDeviceContext, mAccumulator,
+                                        DEVICE_ID);
+
+    processAxis(conv, EV_ABS, ABS_MT_SLOT, 0);
+    processAxis(conv, EV_ABS, ABS_MT_TRACKING_ID, 1);
+    processAxis(conv, EV_ABS, ABS_MT_ORIENTATION, -3);
+    processAxis(conv, EV_KEY, BTN_TOUCH, 1);
+    processAxis(conv, EV_KEY, BTN_TOOL_FINGER, 1);
+
+    EXPECT_NEAR(-3 * M_PI / 8,
+                processSyncAndExpectSingleMotionArg(conv).pointerCoords[0].getAxisValue(
+                        AMOTION_EVENT_AXIS_ORIENTATION),
+                EPSILON);
+
+    processAxis(conv, EV_ABS, ABS_MT_ORIENTATION, 0);
+
+    EXPECT_NEAR(0,
+                processSyncAndExpectSingleMotionArg(conv).pointerCoords[0].getAxisValue(
+                        AMOTION_EVENT_AXIS_ORIENTATION),
+                EPSILON);
+
+    processAxis(conv, EV_ABS, ABS_MT_ORIENTATION, 4);
+
+    EXPECT_NEAR(M_PI / 2,
+                processSyncAndExpectSingleMotionArg(conv).pointerCoords[0].getAxisValue(
+                        AMOTION_EVENT_AXIS_ORIENTATION),
+                EPSILON);
+}
+
+TEST_F(CapturedTouchpadEventConverterTest, OneFinger_pressureScaledCorrectly) {
+    mFakeEventHub->addAbsoluteAxis(EVENTHUB_ID, ABS_MT_POSITION_X, 0, 4000, 0, 0, 45);
+    mFakeEventHub->addAbsoluteAxis(EVENTHUB_ID, ABS_MT_POSITION_Y, 0, 2500, 0, 0, 40);
+    mFakeEventHub->addAbsoluteAxis(EVENTHUB_ID, ABS_MT_PRESSURE, 0, 256, 0, 0, 0);
+    CapturedTouchpadEventConverter conv(*mReader.getContext(), mDeviceContext, mAccumulator,
+                                        DEVICE_ID);
+
+    processAxis(conv, EV_ABS, ABS_MT_SLOT, 0);
+    processAxis(conv, EV_ABS, ABS_MT_TRACKING_ID, 1);
+    processAxis(conv, EV_ABS, ABS_MT_PRESSURE, 128);
+    processAxis(conv, EV_KEY, BTN_TOUCH, 1);
+    processAxis(conv, EV_KEY, BTN_TOOL_FINGER, 1);
+
+    EXPECT_THAT(processSyncAndExpectSingleMotionArg(conv), WithPressure(0.5));
+}
+
+TEST_F(CapturedTouchpadEventConverterTest,
+       OneFinger_withAllSizeAxes_sizeCalculatedFromTouchMajorMinorAverage) {
+    mFakeEventHub->addAbsoluteAxis(EVENTHUB_ID, ABS_MT_POSITION_X, 0, 4000, 0, 0, 45);
+    mFakeEventHub->addAbsoluteAxis(EVENTHUB_ID, ABS_MT_POSITION_Y, 0, 2500, 0, 0, 40);
+    mFakeEventHub->addAbsoluteAxis(EVENTHUB_ID, ABS_MT_TOUCH_MAJOR, 0, 256, 0, 0, 0);
+    mFakeEventHub->addAbsoluteAxis(EVENTHUB_ID, ABS_MT_TOUCH_MINOR, 0, 256, 0, 0, 0);
+    mFakeEventHub->addAbsoluteAxis(EVENTHUB_ID, ABS_MT_WIDTH_MAJOR, 0, 256, 0, 0, 0);
+    mFakeEventHub->addAbsoluteAxis(EVENTHUB_ID, ABS_MT_WIDTH_MINOR, 0, 256, 0, 0, 0);
+    CapturedTouchpadEventConverter conv(*mReader.getContext(), mDeviceContext, mAccumulator,
+                                        DEVICE_ID);
+
+    processAxis(conv, EV_ABS, ABS_MT_SLOT, 0);
+    processAxis(conv, EV_ABS, ABS_MT_TRACKING_ID, 1);
+    processAxis(conv, EV_ABS, ABS_MT_TOUCH_MAJOR, 138);
+    processAxis(conv, EV_ABS, ABS_MT_TOUCH_MINOR, 118);
+    processAxis(conv, EV_ABS, ABS_MT_WIDTH_MAJOR, 200);
+    processAxis(conv, EV_ABS, ABS_MT_WIDTH_MINOR, 210);
+    processAxis(conv, EV_KEY, BTN_TOUCH, 1);
+    processAxis(conv, EV_KEY, BTN_TOOL_FINGER, 1);
+
+    EXPECT_NEAR(0.5,
+                processSyncAndExpectSingleMotionArg(conv).pointerCoords[0].getAxisValue(
+                        AMOTION_EVENT_AXIS_SIZE),
+                EPSILON);
+}
+
+TEST_F(CapturedTouchpadEventConverterTest,
+       OneFinger_withMajorDimensionsOnly_sizeCalculatedFromTouchMajor) {
+    mFakeEventHub->addAbsoluteAxis(EVENTHUB_ID, ABS_MT_POSITION_X, 0, 4000, 0, 0, 45);
+    mFakeEventHub->addAbsoluteAxis(EVENTHUB_ID, ABS_MT_POSITION_Y, 0, 2500, 0, 0, 40);
+    mFakeEventHub->addAbsoluteAxis(EVENTHUB_ID, ABS_MT_TOUCH_MAJOR, 0, 256, 0, 0, 0);
+    mFakeEventHub->addAbsoluteAxis(EVENTHUB_ID, ABS_MT_WIDTH_MAJOR, 0, 256, 0, 0, 0);
+    CapturedTouchpadEventConverter conv(*mReader.getContext(), mDeviceContext, mAccumulator,
+                                        DEVICE_ID);
+
+    processAxis(conv, EV_ABS, ABS_MT_SLOT, 0);
+    processAxis(conv, EV_ABS, ABS_MT_TRACKING_ID, 1);
+    processAxis(conv, EV_ABS, ABS_MT_TOUCH_MAJOR, 128);
+    processAxis(conv, EV_ABS, ABS_MT_WIDTH_MAJOR, 200);
+    processAxis(conv, EV_KEY, BTN_TOUCH, 1);
+    processAxis(conv, EV_KEY, BTN_TOOL_FINGER, 1);
+
+    EXPECT_NEAR(0.5,
+                processSyncAndExpectSingleMotionArg(conv).pointerCoords[0].getAxisValue(
+                        AMOTION_EVENT_AXIS_SIZE),
+                EPSILON);
+}
+
+TEST_F(CapturedTouchpadEventConverterTest,
+       OneFinger_withToolDimensionsOnly_sizeCalculatedFromToolMajorMinorAverage) {
+    mFakeEventHub->addAbsoluteAxis(EVENTHUB_ID, ABS_MT_POSITION_X, 0, 4000, 0, 0, 45);
+    mFakeEventHub->addAbsoluteAxis(EVENTHUB_ID, ABS_MT_POSITION_Y, 0, 2500, 0, 0, 40);
+    mFakeEventHub->addAbsoluteAxis(EVENTHUB_ID, ABS_MT_WIDTH_MAJOR, 0, 256, 0, 0, 0);
+    mFakeEventHub->addAbsoluteAxis(EVENTHUB_ID, ABS_MT_WIDTH_MINOR, 0, 256, 0, 0, 0);
+    CapturedTouchpadEventConverter conv(*mReader.getContext(), mDeviceContext, mAccumulator,
+                                        DEVICE_ID);
+
+    processAxis(conv, EV_ABS, ABS_MT_SLOT, 0);
+    processAxis(conv, EV_ABS, ABS_MT_TRACKING_ID, 1);
+    processAxis(conv, EV_ABS, ABS_MT_WIDTH_MAJOR, 138);
+    processAxis(conv, EV_ABS, ABS_MT_WIDTH_MINOR, 118);
+    processAxis(conv, EV_KEY, BTN_TOUCH, 1);
+    processAxis(conv, EV_KEY, BTN_TOOL_FINGER, 1);
+
+    EXPECT_NEAR(0.5,
+                processSyncAndExpectSingleMotionArg(conv).pointerCoords[0].getAxisValue(
+                        AMOTION_EVENT_AXIS_SIZE),
+                EPSILON);
+}
+
+TEST_F(CapturedTouchpadEventConverterTest,
+       OneFinger_withToolMajorOnly_sizeCalculatedFromTouchMajor) {
+    mFakeEventHub->addAbsoluteAxis(EVENTHUB_ID, ABS_MT_POSITION_X, 0, 4000, 0, 0, 45);
+    mFakeEventHub->addAbsoluteAxis(EVENTHUB_ID, ABS_MT_POSITION_Y, 0, 2500, 0, 0, 40);
+    mFakeEventHub->addAbsoluteAxis(EVENTHUB_ID, ABS_MT_WIDTH_MAJOR, 0, 256, 0, 0, 0);
+    CapturedTouchpadEventConverter conv(*mReader.getContext(), mDeviceContext, mAccumulator,
+                                        DEVICE_ID);
+
+    processAxis(conv, EV_ABS, ABS_MT_SLOT, 0);
+    processAxis(conv, EV_ABS, ABS_MT_TRACKING_ID, 1);
+    processAxis(conv, EV_ABS, ABS_MT_WIDTH_MAJOR, 128);
+    processAxis(conv, EV_KEY, BTN_TOUCH, 1);
+    processAxis(conv, EV_KEY, BTN_TOOL_FINGER, 1);
+
+    EXPECT_NEAR(0.5,
+                processSyncAndExpectSingleMotionArg(conv).pointerCoords[0].getAxisValue(
+                        AMOTION_EVENT_AXIS_SIZE),
+                EPSILON);
+}
+
+TEST_F(CapturedTouchpadEventConverterTest, OnePalm_neverReported) {
+    addBasicAxesToEventHub();
+    mFakeEventHub->addAbsoluteAxis(EVENTHUB_ID, ABS_MT_TOOL_TYPE, 0, MT_TOOL_PALM, 0, 0, 0);
+    CapturedTouchpadEventConverter conv(*mReader.getContext(), mDeviceContext, mAccumulator,
+                                        DEVICE_ID);
+
+    processAxis(conv, EV_ABS, ABS_MT_SLOT, 0);
+    processAxis(conv, EV_ABS, ABS_MT_TRACKING_ID, 1);
+    processAxis(conv, EV_ABS, ABS_MT_POSITION_X, 50);
+    processAxis(conv, EV_ABS, ABS_MT_POSITION_Y, 100);
+    processAxis(conv, EV_ABS, ABS_MT_TOOL_TYPE, MT_TOOL_PALM);
+    processAxis(conv, EV_KEY, BTN_TOUCH, 1);
+    processAxis(conv, EV_KEY, BTN_TOOL_FINGER, 1);
+
+    EXPECT_EQ(0u, processSync(conv).size());
+
+    processAxis(conv, EV_ABS, ABS_MT_POSITION_X, 51);
+
+    EXPECT_EQ(0u, processSync(conv).size());
+
+    processAxis(conv, EV_ABS, ABS_MT_TRACKING_ID, -1);
+    processAxis(conv, EV_KEY, BTN_TOUCH, 0);
+    processAxis(conv, EV_KEY, BTN_TOOL_FINGER, 0);
+
+    EXPECT_EQ(0u, processSync(conv).size());
+}
+
+TEST_F(CapturedTouchpadEventConverterTest, FingerTurningIntoPalm_cancelled) {
+    addBasicAxesToEventHub();
+    mFakeEventHub->addAbsoluteAxis(EVENTHUB_ID, ABS_MT_TOOL_TYPE, 0, MT_TOOL_PALM, 0, 0, 0);
+    CapturedTouchpadEventConverter conv(*mReader.getContext(), mDeviceContext, mAccumulator,
+                                        DEVICE_ID);
+
+    processAxis(conv, EV_ABS, ABS_MT_SLOT, 0);
+    processAxis(conv, EV_ABS, ABS_MT_TRACKING_ID, 1);
+    processAxis(conv, EV_ABS, ABS_MT_POSITION_X, 50);
+    processAxis(conv, EV_ABS, ABS_MT_POSITION_Y, 100);
+    processAxis(conv, EV_ABS, ABS_MT_TOOL_TYPE, MT_TOOL_FINGER);
+    processAxis(conv, EV_KEY, BTN_TOUCH, 1);
+    processAxis(conv, EV_KEY, BTN_TOOL_FINGER, 1);
+
+    EXPECT_THAT(processSyncAndExpectSingleMotionArg(conv),
+                AllOf(WithMotionAction(AMOTION_EVENT_ACTION_DOWN), WithToolType(ToolType::FINGER),
+                      WithPointerCount(1u)));
+
+    processAxis(conv, EV_ABS, ABS_MT_POSITION_X, 51);
+    processAxis(conv, EV_ABS, ABS_MT_TOOL_TYPE, MT_TOOL_PALM);
+
+    std::list<NotifyArgs> args = processSync(conv);
+    ASSERT_EQ(2u, args.size());
+    EXPECT_THAT(std::get<NotifyMotionArgs>(args.front()),
+                AllOf(WithMotionAction(AMOTION_EVENT_ACTION_MOVE), WithPointerCount(1u)));
+    args.pop_front();
+    EXPECT_THAT(std::get<NotifyMotionArgs>(args.front()),
+                AllOf(WithMotionAction(AMOTION_EVENT_ACTION_CANCEL), WithPointerCount(1u)));
+
+    processAxis(conv, EV_ABS, ABS_MT_POSITION_X, 52);
+
+    EXPECT_EQ(0u, processSync(conv).size());
+
+    processAxis(conv, EV_ABS, ABS_MT_TRACKING_ID, -1);
+    processAxis(conv, EV_KEY, BTN_TOUCH, 0);
+    processAxis(conv, EV_KEY, BTN_TOOL_FINGER, 0);
+
+    EXPECT_EQ(0u, processSync(conv).size());
+}
+
+TEST_F(CapturedTouchpadEventConverterTest, PalmTurningIntoFinger_reported) {
+    addBasicAxesToEventHub();
+    mFakeEventHub->addAbsoluteAxis(EVENTHUB_ID, ABS_MT_TOOL_TYPE, 0, MT_TOOL_PALM, 0, 0, 0);
+    CapturedTouchpadEventConverter conv(*mReader.getContext(), mDeviceContext, mAccumulator,
+                                        DEVICE_ID);
+
+    processAxis(conv, EV_ABS, ABS_MT_SLOT, 0);
+    processAxis(conv, EV_ABS, ABS_MT_TRACKING_ID, 1);
+    processAxis(conv, EV_ABS, ABS_MT_POSITION_X, 50);
+    processAxis(conv, EV_ABS, ABS_MT_POSITION_Y, 100);
+    processAxis(conv, EV_ABS, ABS_MT_TOOL_TYPE, MT_TOOL_PALM);
+    processAxis(conv, EV_KEY, BTN_TOUCH, 1);
+    processAxis(conv, EV_KEY, BTN_TOOL_FINGER, 1);
+
+    EXPECT_EQ(0u, processSync(conv).size());
+
+    processAxis(conv, EV_ABS, ABS_MT_POSITION_X, 51);
+    processAxis(conv, EV_ABS, ABS_MT_TOOL_TYPE, MT_TOOL_FINGER);
+
+    EXPECT_THAT(processSyncAndExpectSingleMotionArg(conv),
+                AllOf(WithMotionAction(AMOTION_EVENT_ACTION_DOWN), WithPointerCount(1u),
+                      WithCoords(51, 100)));
+
+    processAxis(conv, EV_ABS, ABS_MT_POSITION_X, 52);
+
+    EXPECT_THAT(processSyncAndExpectSingleMotionArg(conv),
+                AllOf(WithMotionAction(AMOTION_EVENT_ACTION_MOVE), WithPointerCount(1u),
+                      WithCoords(52, 100)));
+}
+
+TEST_F(CapturedTouchpadEventConverterTest, FingerArrivingAfterPalm_onlyFingerReported) {
+    addBasicAxesToEventHub();
+    mFakeEventHub->addAbsoluteAxis(EVENTHUB_ID, ABS_MT_TOOL_TYPE, 0, MT_TOOL_PALM, 0, 0, 0);
+    CapturedTouchpadEventConverter conv(*mReader.getContext(), mDeviceContext, mAccumulator,
+                                        DEVICE_ID);
+
+    processAxis(conv, EV_ABS, ABS_MT_SLOT, 0);
+    processAxis(conv, EV_ABS, ABS_MT_TRACKING_ID, 1);
+    processAxis(conv, EV_ABS, ABS_MT_POSITION_X, 50);
+    processAxis(conv, EV_ABS, ABS_MT_POSITION_Y, 100);
+    processAxis(conv, EV_ABS, ABS_MT_TOOL_TYPE, MT_TOOL_PALM);
+    processAxis(conv, EV_KEY, BTN_TOUCH, 1);
+    processAxis(conv, EV_KEY, BTN_TOOL_FINGER, 1);
+
+    EXPECT_EQ(0u, processSync(conv).size());
+
+    processAxis(conv, EV_ABS, ABS_MT_SLOT, 1);
+    processAxis(conv, EV_ABS, ABS_MT_TRACKING_ID, 2);
+    processAxis(conv, EV_ABS, ABS_MT_POSITION_X, 100);
+    processAxis(conv, EV_ABS, ABS_MT_POSITION_Y, 150);
+    processAxis(conv, EV_ABS, ABS_MT_TOOL_TYPE, MT_TOOL_FINGER);
+    processAxis(conv, EV_KEY, BTN_TOOL_FINGER, 0);
+    processAxis(conv, EV_KEY, BTN_TOOL_DOUBLETAP, 1);
+
+    EXPECT_THAT(processSyncAndExpectSingleMotionArg(conv),
+                AllOf(WithMotionAction(AMOTION_EVENT_ACTION_DOWN), WithPointerCount(1u),
+                      WithCoords(100, 150)));
+
+    processAxis(conv, EV_ABS, ABS_MT_SLOT, 0);
+    processAxis(conv, EV_ABS, ABS_MT_POSITION_X, 52);
+    processAxis(conv, EV_ABS, ABS_MT_POSITION_Y, 102);
+    processAxis(conv, EV_ABS, ABS_MT_SLOT, 1);
+    processAxis(conv, EV_ABS, ABS_MT_POSITION_X, 98);
+    processAxis(conv, EV_ABS, ABS_MT_POSITION_Y, 148);
+
+    EXPECT_THAT(processSyncAndExpectSingleMotionArg(conv),
+                AllOf(WithMotionAction(AMOTION_EVENT_ACTION_MOVE), WithPointerCount(1u),
+                      WithCoords(98, 148)));
+}
+
+TEST_F(CapturedTouchpadEventConverterTest, FingerAndFingerTurningIntoPalm_partiallyCancelled) {
+    addBasicAxesToEventHub();
+    mFakeEventHub->addAbsoluteAxis(EVENTHUB_ID, ABS_MT_TOOL_TYPE, 0, MT_TOOL_PALM, 0, 0, 0);
+    CapturedTouchpadEventConverter conv(*mReader.getContext(), mDeviceContext, mAccumulator,
+                                        DEVICE_ID);
+
+    processAxis(conv, EV_ABS, ABS_MT_SLOT, 0);
+    processAxis(conv, EV_ABS, ABS_MT_TRACKING_ID, 1);
+    processAxis(conv, EV_ABS, ABS_MT_POSITION_X, 50);
+    processAxis(conv, EV_ABS, ABS_MT_TOOL_TYPE, MT_TOOL_FINGER);
+
+    processAxis(conv, EV_ABS, ABS_MT_SLOT, 1);
+    processAxis(conv, EV_ABS, ABS_MT_TRACKING_ID, 2);
+    processAxis(conv, EV_ABS, ABS_MT_POSITION_X, 250);
+    processAxis(conv, EV_ABS, ABS_MT_TOOL_TYPE, MT_TOOL_FINGER);
+
+    processAxis(conv, EV_KEY, BTN_TOUCH, 1);
+    processAxis(conv, EV_KEY, BTN_TOOL_DOUBLETAP, 1);
+
+    std::list<NotifyArgs> args = processSync(conv);
+    ASSERT_EQ(2u, args.size());
+    EXPECT_THAT(std::get<NotifyMotionArgs>(args.front()),
+                AllOf(WithMotionAction(AMOTION_EVENT_ACTION_DOWN), WithPointerCount(1u),
+                      WithToolType(ToolType::FINGER)));
+    args.pop_front();
+    EXPECT_THAT(std::get<NotifyMotionArgs>(args.front()),
+                AllOf(WithMotionAction(AMOTION_EVENT_ACTION_POINTER_DOWN |
+                                       1 << AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT),
+                      WithPointerCount(2u), WithPointerToolType(0, ToolType::FINGER),
+                      WithPointerToolType(1, ToolType::FINGER)));
+
+    processAxis(conv, EV_ABS, ABS_MT_SLOT, 0);
+    processAxis(conv, EV_ABS, ABS_MT_POSITION_X, 51);
+
+    processAxis(conv, EV_ABS, ABS_MT_SLOT, 1);
+    processAxis(conv, EV_ABS, ABS_MT_POSITION_X, 251);
+    processAxis(conv, EV_ABS, ABS_MT_TOOL_TYPE, MT_TOOL_PALM);
+
+    args = processSync(conv);
+    ASSERT_EQ(2u, args.size());
+    EXPECT_THAT(std::get<NotifyMotionArgs>(args.front()),
+                AllOf(WithMotionAction(AMOTION_EVENT_ACTION_MOVE), WithPointerCount(2u)));
+    args.pop_front();
+    EXPECT_THAT(std::get<NotifyMotionArgs>(args.front()),
+                AllOf(WithMotionAction(AMOTION_EVENT_ACTION_POINTER_UP |
+                                       1 << AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT),
+                      WithFlags(AMOTION_EVENT_FLAG_CANCELED), WithPointerCount(2u)));
+}
+
+TEST_F(CapturedTouchpadEventConverterTest, FingerAndPalmTurningIntoFinger_reported) {
+    addBasicAxesToEventHub();
+    mFakeEventHub->addAbsoluteAxis(EVENTHUB_ID, ABS_MT_TOOL_TYPE, 0, MT_TOOL_PALM, 0, 0, 0);
+    CapturedTouchpadEventConverter conv(*mReader.getContext(), mDeviceContext, mAccumulator,
+                                        DEVICE_ID);
+
+    processAxis(conv, EV_ABS, ABS_MT_SLOT, 0);
+    processAxis(conv, EV_ABS, ABS_MT_TRACKING_ID, 1);
+    processAxis(conv, EV_ABS, ABS_MT_POSITION_X, 50);
+    processAxis(conv, EV_ABS, ABS_MT_TOOL_TYPE, MT_TOOL_FINGER);
+
+    processAxis(conv, EV_ABS, ABS_MT_SLOT, 1);
+    processAxis(conv, EV_ABS, ABS_MT_TRACKING_ID, 2);
+    processAxis(conv, EV_ABS, ABS_MT_POSITION_X, 250);
+    processAxis(conv, EV_ABS, ABS_MT_TOOL_TYPE, MT_TOOL_PALM);
+
+    processAxis(conv, EV_KEY, BTN_TOUCH, 1);
+    processAxis(conv, EV_KEY, BTN_TOOL_DOUBLETAP, 1);
+
+    EXPECT_THAT(processSyncAndExpectSingleMotionArg(conv),
+                AllOf(WithMotionAction(AMOTION_EVENT_ACTION_DOWN), WithPointerCount(1u),
+                      WithToolType(ToolType::FINGER)));
+
+    processAxis(conv, EV_ABS, ABS_MT_SLOT, 0);
+    processAxis(conv, EV_ABS, ABS_MT_POSITION_X, 51);
+
+    processAxis(conv, EV_ABS, ABS_MT_SLOT, 1);
+    processAxis(conv, EV_ABS, ABS_MT_POSITION_X, 251);
+    processAxis(conv, EV_ABS, ABS_MT_TOOL_TYPE, MT_TOOL_FINGER);
+
+    std::list<NotifyArgs> args = processSync(conv);
+    ASSERT_EQ(2u, args.size());
+    EXPECT_THAT(std::get<NotifyMotionArgs>(args.front()),
+                AllOf(WithMotionAction(AMOTION_EVENT_ACTION_MOVE), WithPointerCount(1u)));
+    args.pop_front();
+    EXPECT_THAT(std::get<NotifyMotionArgs>(args.front()),
+                AllOf(WithMotionAction(AMOTION_EVENT_ACTION_POINTER_DOWN |
+                                       1 << AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT),
+                      WithPointerCount(2u)));
+}
+
+TEST_F(CapturedTouchpadEventConverterTest, TwoFingers_motionReportedCorrectly) {
+    CapturedTouchpadEventConverter conv = createConverter();
+
+    processAxis(conv, EV_ABS, ABS_MT_SLOT, 0);
+    processAxis(conv, EV_ABS, ABS_MT_TRACKING_ID, 1);
+    processAxis(conv, EV_ABS, ABS_MT_POSITION_X, 50);
+    processAxis(conv, EV_ABS, ABS_MT_POSITION_Y, 100);
+
+    processAxis(conv, EV_KEY, BTN_TOUCH, 1);
+    processAxis(conv, EV_KEY, BTN_TOOL_FINGER, 1);
+
+    EXPECT_THAT(processSyncAndExpectSingleMotionArg(conv),
+                AllOf(WithMotionAction(AMOTION_EVENT_ACTION_DOWN), WithPointerCount(1u),
+                      WithCoords(50, 100), WithToolType(ToolType::FINGER)));
+
+    processAxis(conv, EV_ABS, ABS_MT_SLOT, 0);
+    processAxis(conv, EV_ABS, ABS_MT_POSITION_X, 52);
+    processAxis(conv, EV_ABS, ABS_MT_POSITION_Y, 99);
+
+    processAxis(conv, EV_ABS, ABS_MT_SLOT, 1);
+    processAxis(conv, EV_ABS, ABS_MT_TRACKING_ID, 2);
+    processAxis(conv, EV_ABS, ABS_MT_POSITION_X, 250);
+    processAxis(conv, EV_ABS, ABS_MT_POSITION_Y, 200);
+
+    processAxis(conv, EV_KEY, BTN_TOOL_FINGER, 0);
+    processAxis(conv, EV_KEY, BTN_TOOL_DOUBLETAP, 1);
+
+    std::list<NotifyArgs> args = processSync(conv);
+    ASSERT_EQ(2u, args.size());
+    EXPECT_THAT(std::get<NotifyMotionArgs>(args.front()),
+                AllOf(WithMotionAction(AMOTION_EVENT_ACTION_MOVE), WithPointerCount(1u),
+                      WithCoords(52, 99), WithToolType(ToolType::FINGER)));
+    args.pop_front();
+    EXPECT_THAT(std::get<NotifyMotionArgs>(args.front()),
+                AllOf(WithMotionAction(AMOTION_EVENT_ACTION_POINTER_DOWN |
+                                       1 << AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT),
+                      WithPointerCount(2u), WithPointerCoords(0, 52, 99),
+                      WithPointerCoords(1, 250, 200), WithPointerToolType(0, ToolType::FINGER),
+                      WithPointerToolType(1, ToolType::FINGER)));
+
+    processAxis(conv, EV_ABS, ABS_MT_SLOT, 0);
+    processAxis(conv, EV_ABS, ABS_MT_TRACKING_ID, -1);
+    processAxis(conv, EV_ABS, ABS_MT_SLOT, 1);
+    processAxis(conv, EV_ABS, ABS_MT_POSITION_X, 255);
+    processAxis(conv, EV_ABS, ABS_MT_POSITION_Y, 202);
+
+    processAxis(conv, EV_KEY, BTN_TOOL_FINGER, 1);
+    processAxis(conv, EV_KEY, BTN_TOOL_DOUBLETAP, 0);
+
+    args = processSync(conv);
+    ASSERT_EQ(2u, args.size());
+    EXPECT_THAT(std::get<NotifyMotionArgs>(args.front()),
+                AllOf(WithMotionAction(AMOTION_EVENT_ACTION_MOVE), WithPointerCount(2u),
+                      WithPointerCoords(0, 52, 99), WithPointerCoords(1, 255, 202),
+                      WithPointerToolType(1, ToolType::FINGER),
+                      WithPointerToolType(0, ToolType::FINGER)));
+    args.pop_front();
+    EXPECT_THAT(std::get<NotifyMotionArgs>(args.front()),
+                AllOf(WithMotionAction(AMOTION_EVENT_ACTION_POINTER_UP |
+                                       0 << AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT),
+                      WithPointerCount(2u), WithPointerCoords(0, 52, 99),
+                      WithPointerCoords(1, 255, 202), WithPointerToolType(0, ToolType::FINGER),
+                      WithPointerToolType(1, ToolType::FINGER)));
+
+    processAxis(conv, EV_ABS, ABS_MT_TRACKING_ID, -1);
+    processAxis(conv, EV_KEY, BTN_TOOL_FINGER, 0);
+    processAxis(conv, EV_KEY, BTN_TOUCH, 0);
+
+    args = processSync(conv);
+    ASSERT_EQ(2u, args.size());
+    EXPECT_THAT(std::get<NotifyMotionArgs>(args.front()),
+                AllOf(WithMotionAction(AMOTION_EVENT_ACTION_MOVE), WithPointerCount(1u),
+                      WithCoords(255, 202), WithToolType(ToolType::FINGER)));
+    args.pop_front();
+    EXPECT_THAT(std::get<NotifyMotionArgs>(args.front()),
+                AllOf(WithMotionAction(AMOTION_EVENT_ACTION_UP), WithPointerCount(1u),
+                      WithCoords(255, 202), WithToolType(ToolType::FINGER)));
+}
+
+// Pointer IDs max out at 31, and so must be reused once a touch is lifted to avoid running out.
+TEST_F(CapturedTouchpadEventConverterTest, PointerIdsReusedAfterLift) {
+    CapturedTouchpadEventConverter conv = createConverter();
+
+    // Put down two fingers, which should get IDs 0 and 1.
+    processAxis(conv, EV_ABS, ABS_MT_SLOT, 0);
+    processAxis(conv, EV_ABS, ABS_MT_TRACKING_ID, 1);
+    processAxis(conv, EV_ABS, ABS_MT_POSITION_X, 10);
+    processAxis(conv, EV_ABS, ABS_MT_SLOT, 1);
+    processAxis(conv, EV_ABS, ABS_MT_TRACKING_ID, 2);
+    processAxis(conv, EV_ABS, ABS_MT_POSITION_X, 20);
+
+    processAxis(conv, EV_KEY, BTN_TOUCH, 1);
+    processAxis(conv, EV_KEY, BTN_TOOL_DOUBLETAP, 1);
+
+    std::list<NotifyArgs> args = processSync(conv);
+    ASSERT_EQ(2u, args.size());
+    EXPECT_THAT(std::get<NotifyMotionArgs>(args.front()),
+                AllOf(WithMotionAction(AMOTION_EVENT_ACTION_DOWN), WithPointerCount(1u),
+                      WithPointerId(/*index=*/0, /*id=*/0)));
+    args.pop_front();
+    EXPECT_THAT(std::get<NotifyMotionArgs>(args.front()),
+                AllOf(WithMotionAction(AMOTION_EVENT_ACTION_POINTER_DOWN |
+                                       1 << AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT),
+                      WithPointerCount(2u), WithPointerId(/*index=*/0, /*id=*/0),
+                      WithPointerId(/*index=*/1, /*id=*/1)));
+
+    // Lift the finger in slot 0, freeing up pointer ID 0...
+    processAxis(conv, EV_ABS, ABS_MT_SLOT, 0);
+    processAxis(conv, EV_ABS, ABS_MT_TRACKING_ID, -1);
+
+    // ...and simultaneously add a finger in slot 2.
+    processAxis(conv, EV_ABS, ABS_MT_SLOT, 2);
+    processAxis(conv, EV_ABS, ABS_MT_TRACKING_ID, 3);
+    processAxis(conv, EV_ABS, ABS_MT_POSITION_X, 30);
+
+    args = processSync(conv);
+    ASSERT_EQ(3u, args.size());
+    // Slot 1 being present will result in a MOVE event, even though it hasn't actually moved (see
+    // comments in CapturedTouchpadEventConverter::sync).
+    EXPECT_THAT(std::get<NotifyMotionArgs>(args.front()),
+                AllOf(WithMotionAction(AMOTION_EVENT_ACTION_MOVE), WithPointerCount(2u),
+                      WithPointerId(/*index=*/0, /*id=*/0), WithPointerId(/*index=*/1, /*id=*/1)));
+    args.pop_front();
+    EXPECT_THAT(std::get<NotifyMotionArgs>(args.front()),
+                AllOf(WithMotionAction(AMOTION_EVENT_ACTION_POINTER_UP |
+                                       0 << AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT),
+                      WithPointerCount(2u), WithPointerId(/*index=*/0, /*id=*/0),
+                      WithPointerId(/*index=*/1, /*id=*/1)));
+    args.pop_front();
+    // Slot 0 being lifted causes the finger from slot 1 to move up to index 0, but keep its
+    // previous ID. The new finger in slot 2 should take ID 0, which was just freed up.
+    EXPECT_THAT(std::get<NotifyMotionArgs>(args.front()),
+                AllOf(WithMotionAction(AMOTION_EVENT_ACTION_POINTER_DOWN |
+                                       1 << AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT),
+                      WithPointerCount(2u), WithPointerId(/*index=*/0, /*id=*/1),
+                      WithPointerId(/*index=*/1, /*id=*/0)));
+}
+
+} // namespace android
diff --git a/services/inputflinger/tests/FakeEventHub.cpp b/services/inputflinger/tests/FakeEventHub.cpp
index 4626f5a..212fceb 100644
--- a/services/inputflinger/tests/FakeEventHub.cpp
+++ b/services/inputflinger/tests/FakeEventHub.cpp
@@ -617,9 +617,11 @@
     }
     // If device sysfs changed -> reopen the device
     if (!mRawLightInfos.empty() && !foundDevice->classes.test(InputDeviceClass::LIGHT)) {
+        InputDeviceIdentifier identifier = foundDevice->identifier;
+        ftl::Flags<InputDeviceClass> classes = foundDevice->classes;
         removeDevice(foundDeviceId);
-        addDevice(foundDeviceId, foundDevice->identifier.name,
-                  foundDevice->classes | InputDeviceClass::LIGHT, foundDevice->identifier.bus);
+        addDevice(foundDeviceId, identifier.name, classes | InputDeviceClass::LIGHT,
+                  identifier.bus);
     }
 }
 
diff --git a/services/inputflinger/tests/HardwareStateConverter_test.cpp b/services/inputflinger/tests/HardwareStateConverter_test.cpp
index 19d46c8..5bea2ba 100644
--- a/services/inputflinger/tests/HardwareStateConverter_test.cpp
+++ b/services/inputflinger/tests/HardwareStateConverter_test.cpp
@@ -25,6 +25,7 @@
 #include "FakeEventHub.h"
 #include "FakeInputReaderPolicy.h"
 #include "InstrumentedInputReader.h"
+#include "MultiTouchMotionAccumulator.h"
 #include "TestConstants.h"
 #include "TestInputListener.h"
 
@@ -38,8 +39,10 @@
             mReader(mFakeEventHub, mFakePolicy, mFakeListener),
             mDevice(newDevice()),
             mDeviceContext(*mDevice, EVENTHUB_ID) {
-        mFakeEventHub->addAbsoluteAxis(EVENTHUB_ID, ABS_MT_SLOT, 0, 7, 0, 0, 0);
-        mConverter = std::make_unique<HardwareStateConverter>(mDeviceContext);
+        const size_t slotCount = 8;
+        mFakeEventHub->addAbsoluteAxis(EVENTHUB_ID, ABS_MT_SLOT, 0, slotCount - 1, 0, 0, 0);
+        mAccumulator.configure(mDeviceContext, slotCount, /*usingSlotsProtocol=*/true);
+        mConverter = std::make_unique<HardwareStateConverter>(mDeviceContext, mAccumulator);
     }
 
 protected:
@@ -90,6 +93,7 @@
     InstrumentedInputReader mReader;
     std::shared_ptr<InputDevice> mDevice;
     InputDeviceContext mDeviceContext;
+    MultiTouchMotionAccumulator mAccumulator;
     std::unique_ptr<HardwareStateConverter> mConverter;
 };
 
diff --git a/services/inputflinger/tests/InputDispatcher_test.cpp b/services/inputflinger/tests/InputDispatcher_test.cpp
index c0bc68c..a6cdee5 100644
--- a/services/inputflinger/tests/InputDispatcher_test.cpp
+++ b/services/inputflinger/tests/InputDispatcher_test.cpp
@@ -62,6 +62,7 @@
 static constexpr int32_t ACTION_MOVE = AMOTION_EVENT_ACTION_MOVE;
 static constexpr int32_t ACTION_UP = AMOTION_EVENT_ACTION_UP;
 static constexpr int32_t ACTION_HOVER_ENTER = AMOTION_EVENT_ACTION_HOVER_ENTER;
+static constexpr int32_t ACTION_HOVER_MOVE = AMOTION_EVENT_ACTION_HOVER_MOVE;
 static constexpr int32_t ACTION_HOVER_EXIT = AMOTION_EVENT_ACTION_HOVER_EXIT;
 static constexpr int32_t ACTION_OUTSIDE = AMOTION_EVENT_ACTION_OUTSIDE;
 static constexpr int32_t ACTION_CANCEL = AMOTION_EVENT_ACTION_CANCEL;
@@ -205,11 +206,9 @@
 
     using AnrResult = std::pair<sp<IBinder>, int32_t /*pid*/>;
 
-protected:
-    virtual ~FakeInputDispatcherPolicy() {}
-
 public:
-    FakeInputDispatcherPolicy() {}
+    FakeInputDispatcherPolicy() = default;
+    virtual ~FakeInputDispatcherPolicy() = default;
 
     void assertFilterInputEventWasCalled(const NotifyKeyArgs& args) {
         assertFilterInputEventWasCalledInternal([&args](const InputEvent& event) {
@@ -404,6 +403,16 @@
         mInterceptKeyTimeout = timeout;
     }
 
+    void assertUserActivityPoked() {
+        std::scoped_lock lock(mLock);
+        ASSERT_TRUE(mPokedUserActivity) << "Expected user activity to have been poked";
+    }
+
+    void assertUserActivityNotPoked() {
+        std::scoped_lock lock(mLock);
+        ASSERT_FALSE(mPokedUserActivity) << "Expected user activity not to have been poked";
+    }
+
 private:
     std::mutex mLock;
     std::unique_ptr<InputEvent> mFilteredEvent GUARDED_BY(mLock);
@@ -425,6 +434,7 @@
 
     sp<IBinder> mDropTargetWindowToken GUARDED_BY(mLock);
     bool mNotifyDropWindowWasCalled GUARDED_BY(mLock) = false;
+    bool mPokedUserActivity GUARDED_BY(mLock) = false;
 
     std::chrono::milliseconds mInterceptKeyTimeout = 0ms;
 
@@ -523,22 +533,20 @@
 
     void notifyVibratorState(int32_t deviceId, bool isOn) override {}
 
-    void getDispatcherConfiguration(InputDispatcherConfiguration* outConfig) override {
-        *outConfig = mConfig;
-    }
+    InputDispatcherConfiguration getDispatcherConfiguration() override { return mConfig; }
 
-    bool filterInputEvent(const InputEvent* inputEvent, uint32_t policyFlags) override {
+    bool filterInputEvent(const InputEvent& inputEvent, uint32_t policyFlags) override {
         std::scoped_lock lock(mLock);
-        switch (inputEvent->getType()) {
+        switch (inputEvent.getType()) {
             case InputEventType::KEY: {
-                const KeyEvent* keyEvent = static_cast<const KeyEvent*>(inputEvent);
-                mFilteredEvent = std::make_unique<KeyEvent>(*keyEvent);
+                const KeyEvent& keyEvent = static_cast<const KeyEvent&>(inputEvent);
+                mFilteredEvent = std::make_unique<KeyEvent>(keyEvent);
                 break;
             }
 
             case InputEventType::MOTION: {
-                const MotionEvent* motionEvent = static_cast<const MotionEvent*>(inputEvent);
-                mFilteredEvent = std::make_unique<MotionEvent>(*motionEvent);
+                const MotionEvent& motionEvent = static_cast<const MotionEvent&>(inputEvent);
+                mFilteredEvent = std::make_unique<MotionEvent>(motionEvent);
                 break;
             }
             default: {
@@ -549,8 +557,8 @@
         return true;
     }
 
-    void interceptKeyBeforeQueueing(const KeyEvent* inputEvent, uint32_t&) override {
-        if (inputEvent->getAction() == AKEY_EVENT_ACTION_UP) {
+    void interceptKeyBeforeQueueing(const KeyEvent& inputEvent, uint32_t&) override {
+        if (inputEvent.getAction() == AKEY_EVENT_ACTION_UP) {
             // Clear intercept state when we handled the event.
             mInterceptKeyTimeout = 0ms;
         }
@@ -558,15 +566,16 @@
 
     void interceptMotionBeforeQueueing(int32_t, nsecs_t, uint32_t&) override {}
 
-    nsecs_t interceptKeyBeforeDispatching(const sp<IBinder>&, const KeyEvent*, uint32_t) override {
+    nsecs_t interceptKeyBeforeDispatching(const sp<IBinder>&, const KeyEvent&, uint32_t) override {
         nsecs_t delay = std::chrono::nanoseconds(mInterceptKeyTimeout).count();
         // Clear intercept state so we could dispatch the event in next wake.
         mInterceptKeyTimeout = 0ms;
         return delay;
     }
 
-    bool dispatchUnhandledKey(const sp<IBinder>&, const KeyEvent*, uint32_t, KeyEvent*) override {
-        return false;
+    std::optional<KeyEvent> dispatchUnhandledKey(const sp<IBinder>&, const KeyEvent&,
+                                                 uint32_t) override {
+        return {};
     }
 
     void notifySwitch(nsecs_t when, uint32_t switchValues, uint32_t switchMask,
@@ -578,7 +587,10 @@
         mLastNotifySwitch = NotifySwitchArgs(/*id=*/1, when, policyFlags, switchValues, switchMask);
     }
 
-    void pokeUserActivity(nsecs_t, int32_t, int32_t) override {}
+    void pokeUserActivity(nsecs_t, int32_t, int32_t) override {
+        std::scoped_lock lock(mLock);
+        mPokedUserActivity = true;
+    }
 
     void onPointerDownOutsideFocus(const sp<IBinder>& newToken) override {
         std::scoped_lock lock(mLock);
@@ -610,12 +622,12 @@
 
 class InputDispatcherTest : public testing::Test {
 protected:
-    sp<FakeInputDispatcherPolicy> mFakePolicy;
+    std::unique_ptr<FakeInputDispatcherPolicy> mFakePolicy;
     std::unique_ptr<InputDispatcher> mDispatcher;
 
     void SetUp() override {
-        mFakePolicy = sp<FakeInputDispatcherPolicy>::make();
-        mDispatcher = std::make_unique<InputDispatcher>(mFakePolicy, STALE_EVENT_TIMEOUT);
+        mFakePolicy = std::make_unique<FakeInputDispatcherPolicy>();
+        mDispatcher = std::make_unique<InputDispatcher>(*mFakePolicy, STALE_EVENT_TIMEOUT);
         mDispatcher->setInputDispatchMode(/*enabled*/ true, /*frozen*/ false);
         // Start InputDispatcher thread
         ASSERT_EQ(OK, mDispatcher->start());
@@ -623,7 +635,7 @@
 
     void TearDown() override {
         ASSERT_EQ(OK, mDispatcher->stop());
-        mFakePolicy.clear();
+        mFakePolicy.reset();
         mDispatcher.reset();
     }
 
@@ -1190,6 +1202,10 @@
         mInfo.setInputConfig(WindowInfo::InputConfig::NO_INPUT_CHANNEL, noInputChannel);
     }
 
+    void setDisableUserActivity(bool disableUserActivity) {
+        mInfo.setInputConfig(WindowInfo::InputConfig::DISABLE_USER_ACTIVITY, disableUserActivity);
+    }
+
     void setAlpha(float alpha) { mInfo.alpha = alpha; }
 
     void setTouchOcclusionMode(TouchOcclusionMode mode) { mInfo.touchOcclusionMode = mode; }
@@ -1742,6 +1758,28 @@
     return args;
 }
 
+static NotifyKeyArgs generateSystemShortcutArgs(int32_t action,
+                                                int32_t displayId = ADISPLAY_ID_NONE) {
+    nsecs_t currentTime = systemTime(SYSTEM_TIME_MONOTONIC);
+    // Define a valid key event.
+    NotifyKeyArgs args(/*id=*/0, currentTime, /*readTime=*/0, DEVICE_ID, AINPUT_SOURCE_KEYBOARD,
+                       displayId, 0, action, /* flags */ 0, AKEYCODE_C, KEY_C, AMETA_META_ON,
+                       currentTime);
+
+    return args;
+}
+
+static NotifyKeyArgs generateAssistantKeyArgs(int32_t action,
+                                              int32_t displayId = ADISPLAY_ID_NONE) {
+    nsecs_t currentTime = systemTime(SYSTEM_TIME_MONOTONIC);
+    // Define a valid key event.
+    NotifyKeyArgs args(/*id=*/0, currentTime, /*readTime=*/0, DEVICE_ID, AINPUT_SOURCE_KEYBOARD,
+                       displayId, 0, action, /* flags */ 0, AKEYCODE_ASSIST, KEY_ASSISTANT,
+                       AMETA_NONE, currentTime);
+
+    return args;
+}
+
 [[nodiscard]] static NotifyMotionArgs generateMotionArgs(int32_t action, int32_t source,
                                                          int32_t displayId,
                                                          const std::vector<PointF>& points) {
@@ -2418,6 +2456,116 @@
 }
 
 /**
+ * Start hovering in a window. While this hover is still active, make another window appear on top.
+ * The top, obstructing window has no input channel, so it's not supposed to receive input.
+ * While the top window is present, the hovering is stopped.
+ * Later, hovering gets resumed again.
+ * Ensure that new hover gesture is handled correctly.
+ * This test reproduces a crash where the HOVER_EXIT event wasn't getting dispatched correctly
+ * to the window that's currently being hovered over.
+ */
+TEST_F(InputDispatcherTest, HoverWhileWindowAppears) {
+    std::shared_ptr<FakeApplicationHandle> application = std::make_shared<FakeApplicationHandle>();
+    sp<FakeWindowHandle> window =
+            sp<FakeWindowHandle>::make(application, mDispatcher, "Window", ADISPLAY_ID_DEFAULT);
+    window->setFrame(Rect(0, 0, 200, 200));
+
+    // Only a single window is present at first
+    mDispatcher->setInputWindows({{ADISPLAY_ID_DEFAULT, {window}}});
+
+    // Start hovering in the window
+    mDispatcher->notifyMotion(MotionArgsBuilder(ACTION_HOVER_ENTER, AINPUT_SOURCE_STYLUS)
+                                      .pointer(PointerBuilder(0, ToolType::STYLUS).x(100).y(100))
+                                      .build());
+    window->consumeMotionEvent(WithMotionAction(ACTION_HOVER_ENTER));
+
+    // Now, an obscuring window appears!
+    sp<FakeWindowHandle> obscuringWindow =
+            sp<FakeWindowHandle>::make(application, mDispatcher, "Obscuring window",
+                                       ADISPLAY_ID_DEFAULT,
+                                       /*token=*/std::make_optional<sp<IBinder>>(nullptr));
+    obscuringWindow->setFrame(Rect(0, 0, 200, 200));
+    obscuringWindow->setTouchOcclusionMode(TouchOcclusionMode::BLOCK_UNTRUSTED);
+    obscuringWindow->setOwnerInfo(SECONDARY_WINDOW_PID, SECONDARY_WINDOW_UID);
+    obscuringWindow->setNoInputChannel(true);
+    obscuringWindow->setFocusable(false);
+    obscuringWindow->setAlpha(1.0);
+    mDispatcher->setInputWindows({{ADISPLAY_ID_DEFAULT, {obscuringWindow, window}}});
+
+    // While this new obscuring window is present, the hovering is stopped
+    mDispatcher->notifyMotion(MotionArgsBuilder(ACTION_HOVER_EXIT, AINPUT_SOURCE_STYLUS)
+                                      .pointer(PointerBuilder(0, ToolType::STYLUS).x(100).y(100))
+                                      .build());
+    window->consumeMotionEvent(WithMotionAction(ACTION_HOVER_EXIT));
+
+    // Now the obscuring window goes away.
+    mDispatcher->setInputWindows({{ADISPLAY_ID_DEFAULT, {window}}});
+
+    // And a new hover gesture starts.
+    mDispatcher->notifyMotion(MotionArgsBuilder(ACTION_HOVER_ENTER, AINPUT_SOURCE_STYLUS)
+                                      .pointer(PointerBuilder(0, ToolType::STYLUS).x(100).y(100))
+                                      .build());
+    window->consumeMotionEvent(WithMotionAction(ACTION_HOVER_ENTER));
+}
+
+/**
+ * Same test as 'HoverWhileWindowAppears' above, but here, we also send some HOVER_MOVE events to
+ * the obscuring window.
+ */
+TEST_F(InputDispatcherTest, HoverMoveWhileWindowAppears) {
+    std::shared_ptr<FakeApplicationHandle> application = std::make_shared<FakeApplicationHandle>();
+    sp<FakeWindowHandle> window =
+            sp<FakeWindowHandle>::make(application, mDispatcher, "Window", ADISPLAY_ID_DEFAULT);
+    window->setFrame(Rect(0, 0, 200, 200));
+
+    // Only a single window is present at first
+    mDispatcher->setInputWindows({{ADISPLAY_ID_DEFAULT, {window}}});
+
+    // Start hovering in the window
+    mDispatcher->notifyMotion(MotionArgsBuilder(ACTION_HOVER_ENTER, AINPUT_SOURCE_STYLUS)
+                                      .pointer(PointerBuilder(0, ToolType::STYLUS).x(100).y(100))
+                                      .build());
+    window->consumeMotionEvent(WithMotionAction(ACTION_HOVER_ENTER));
+
+    // Now, an obscuring window appears!
+    sp<FakeWindowHandle> obscuringWindow =
+            sp<FakeWindowHandle>::make(application, mDispatcher, "Obscuring window",
+                                       ADISPLAY_ID_DEFAULT,
+                                       /*token=*/std::make_optional<sp<IBinder>>(nullptr));
+    obscuringWindow->setFrame(Rect(0, 0, 200, 200));
+    obscuringWindow->setTouchOcclusionMode(TouchOcclusionMode::BLOCK_UNTRUSTED);
+    obscuringWindow->setOwnerInfo(SECONDARY_WINDOW_PID, SECONDARY_WINDOW_UID);
+    obscuringWindow->setNoInputChannel(true);
+    obscuringWindow->setFocusable(false);
+    obscuringWindow->setAlpha(1.0);
+    mDispatcher->setInputWindows({{ADISPLAY_ID_DEFAULT, {obscuringWindow, window}}});
+
+    // While this new obscuring window is present, the hovering continues. The event can't go to the
+    // bottom window due to obstructed touches, so it should generate HOVER_EXIT for that window.
+    mDispatcher->notifyMotion(MotionArgsBuilder(ACTION_HOVER_MOVE, AINPUT_SOURCE_STYLUS)
+                                      .pointer(PointerBuilder(0, ToolType::STYLUS).x(100).y(100))
+                                      .build());
+    obscuringWindow->assertNoEvents();
+    window->consumeMotionEvent(WithMotionAction(ACTION_HOVER_EXIT));
+
+    // Now the obscuring window goes away.
+    mDispatcher->setInputWindows({{ADISPLAY_ID_DEFAULT, {window}}});
+
+    // Hovering continues in the same position. The hovering pointer re-enters the bottom window,
+    // so it should generate a HOVER_ENTER
+    mDispatcher->notifyMotion(MotionArgsBuilder(ACTION_HOVER_MOVE, AINPUT_SOURCE_STYLUS)
+                                      .pointer(PointerBuilder(0, ToolType::STYLUS).x(100).y(100))
+                                      .build());
+    window->consumeMotionEvent(WithMotionAction(ACTION_HOVER_ENTER));
+
+    // Now the MOVE should be getting dispatched normally
+    mDispatcher->notifyMotion(MotionArgsBuilder(ACTION_HOVER_MOVE, AINPUT_SOURCE_STYLUS)
+                                      .pointer(PointerBuilder(0, ToolType::STYLUS).x(110).y(110))
+                                      .build());
+    window->consumeMotionEvent(WithMotionAction(ACTION_HOVER_MOVE));
+}
+
+/**
  * Two windows: a window on the left and a window on the right.
  * Mouse is clicked on the left window and remains down. Touch is touched on the right and remains
  * down. Then, on the left window, also place second touch pointer down.
@@ -3409,7 +3557,9 @@
                                         .build()));
     window->consumeMotionUp(ADISPLAY_ID_DEFAULT);
 
-    ASSERT_EQ(InputEventInjectionResult::SUCCEEDED,
+    // We already canceled the hovering implicitly by injecting the "DOWN" event without lifting the
+    // hover first. Therefore, injection of HOVER_EXIT is inconsistent, and should fail.
+    ASSERT_EQ(InputEventInjectionResult::FAILED,
               injectMotionEvent(mDispatcher,
                                 MotionEventBuilder(AMOTION_EVENT_ACTION_HOVER_EXIT,
                                                    AINPUT_SOURCE_MOUSE)
@@ -3641,6 +3791,30 @@
             AllOf(WithMotionAction(ACTION_CANCEL), WithDisplayId(ADISPLAY_ID_DEFAULT)));
 }
 
+TEST_F(InputDispatcherTest, NotifyDeviceResetCancelsHoveringStream) {
+    std::shared_ptr<FakeApplicationHandle> application = std::make_shared<FakeApplicationHandle>();
+    sp<FakeWindowHandle> window = sp<FakeWindowHandle>::make(application, mDispatcher,
+                                                             "Fake Window", ADISPLAY_ID_DEFAULT);
+
+    mDispatcher->setInputWindows({{ADISPLAY_ID_DEFAULT, {window}}});
+
+    mDispatcher->notifyMotion(MotionArgsBuilder(ACTION_HOVER_ENTER, AINPUT_SOURCE_STYLUS)
+                                      .pointer(PointerBuilder(0, ToolType::STYLUS).x(10).y(10))
+                                      .build());
+
+    window->consumeMotionEvent(WithMotionAction(ACTION_HOVER_ENTER));
+
+    // When device reset happens, that hover stream should be terminated with ACTION_HOVER_EXIT
+    mDispatcher->notifyDeviceReset({/*id=*/10, /*eventTime=*/20, DEVICE_ID});
+    window->consumeMotionEvent(WithMotionAction(ACTION_HOVER_EXIT));
+
+    // After the device has been reset, a new hovering stream can be sent to the window
+    mDispatcher->notifyMotion(MotionArgsBuilder(ACTION_HOVER_ENTER, AINPUT_SOURCE_STYLUS)
+                                      .pointer(PointerBuilder(0, ToolType::STYLUS).x(15).y(15))
+                                      .build());
+    window->consumeMotionEvent(WithMotionAction(ACTION_HOVER_ENTER));
+}
+
 TEST_F(InputDispatcherTest, InterceptKeyByPolicy) {
     std::shared_ptr<FakeApplicationHandle> application = std::make_shared<FakeApplicationHandle>();
     sp<FakeWindowHandle> window = sp<FakeWindowHandle>::make(application, mDispatcher,
@@ -3957,6 +4131,7 @@
 
     firstWindow->assertNoEvents();
     const MotionEvent* event = secondWindow->consumeMotion();
+    ASSERT_NE(nullptr, event);
     EXPECT_EQ(AMOTION_EVENT_ACTION_DOWN, event->getAction());
 
     // Ensure that the events from the "getRaw" API are in logical display coordinates.
@@ -4530,6 +4705,94 @@
 
     // Window should receive key down event.
     window->consumeKeyDown(ADISPLAY_ID_DEFAULT);
+
+    // Should have poked user activity
+    mFakePolicy->assertUserActivityPoked();
+}
+
+TEST_F(InputDispatcherTest, FocusedWindow_DisableUserActivity) {
+    std::shared_ptr<FakeApplicationHandle> application = std::make_shared<FakeApplicationHandle>();
+    sp<FakeWindowHandle> window = sp<FakeWindowHandle>::make(application, mDispatcher,
+                                                             "Fake Window", ADISPLAY_ID_DEFAULT);
+
+    window->setDisableUserActivity(true);
+    window->setFocusable(true);
+    mDispatcher->setInputWindows({{ADISPLAY_ID_DEFAULT, {window}}});
+    setFocusedWindow(window);
+
+    window->consumeFocusEvent(true);
+
+    mDispatcher->notifyKey(generateKeyArgs(AKEY_EVENT_ACTION_DOWN, ADISPLAY_ID_DEFAULT));
+
+    // Window should receive key down event.
+    window->consumeKeyDown(ADISPLAY_ID_DEFAULT);
+
+    // Should have poked user activity
+    mFakePolicy->assertUserActivityNotPoked();
+}
+
+TEST_F(InputDispatcherTest, FocusedWindow_DoesNotReceiveSystemShortcut) {
+    std::shared_ptr<FakeApplicationHandle> application = std::make_shared<FakeApplicationHandle>();
+    sp<FakeWindowHandle> window = sp<FakeWindowHandle>::make(application, mDispatcher,
+                                                             "Fake Window", ADISPLAY_ID_DEFAULT);
+
+    window->setFocusable(true);
+    mDispatcher->setInputWindows({{ADISPLAY_ID_DEFAULT, {window}}});
+    setFocusedWindow(window);
+
+    window->consumeFocusEvent(true);
+
+    mDispatcher->notifyKey(generateSystemShortcutArgs(AKEY_EVENT_ACTION_DOWN, ADISPLAY_ID_DEFAULT));
+    mDispatcher->waitForIdle();
+
+    // System key is not passed down
+    window->assertNoEvents();
+
+    // Should have poked user activity
+    mFakePolicy->assertUserActivityPoked();
+}
+
+TEST_F(InputDispatcherTest, FocusedWindow_DoesNotReceiveAssistantKey) {
+    std::shared_ptr<FakeApplicationHandle> application = std::make_shared<FakeApplicationHandle>();
+    sp<FakeWindowHandle> window = sp<FakeWindowHandle>::make(application, mDispatcher,
+                                                             "Fake Window", ADISPLAY_ID_DEFAULT);
+
+    window->setFocusable(true);
+    mDispatcher->setInputWindows({{ADISPLAY_ID_DEFAULT, {window}}});
+    setFocusedWindow(window);
+
+    window->consumeFocusEvent(true);
+
+    mDispatcher->notifyKey(generateAssistantKeyArgs(AKEY_EVENT_ACTION_DOWN, ADISPLAY_ID_DEFAULT));
+    mDispatcher->waitForIdle();
+
+    // System key is not passed down
+    window->assertNoEvents();
+
+    // Should have poked user activity
+    mFakePolicy->assertUserActivityPoked();
+}
+
+TEST_F(InputDispatcherTest, FocusedWindow_SystemKeyIgnoresDisableUserActivity) {
+    std::shared_ptr<FakeApplicationHandle> application = std::make_shared<FakeApplicationHandle>();
+    sp<FakeWindowHandle> window = sp<FakeWindowHandle>::make(application, mDispatcher,
+                                                             "Fake Window", ADISPLAY_ID_DEFAULT);
+
+    window->setDisableUserActivity(true);
+    window->setFocusable(true);
+    mDispatcher->setInputWindows({{ADISPLAY_ID_DEFAULT, {window}}});
+    setFocusedWindow(window);
+
+    window->consumeFocusEvent(true);
+
+    mDispatcher->notifyKey(generateSystemShortcutArgs(AKEY_EVENT_ACTION_DOWN, ADISPLAY_ID_DEFAULT));
+    mDispatcher->waitForIdle();
+
+    // System key is not passed down
+    window->assertNoEvents();
+
+    // Should have poked user activity
+    mFakePolicy->assertUserActivityPoked();
 }
 
 TEST_F(InputDispatcherTest, UnfocusedWindow_DoesNotReceiveFocusEventOrKeyEvent) {
@@ -5281,9 +5544,9 @@
     sp<FakeWindowHandle> mWindow;
 
     virtual void SetUp() override {
-        mFakePolicy = sp<FakeInputDispatcherPolicy>::make();
+        mFakePolicy = std::make_unique<FakeInputDispatcherPolicy>();
         mFakePolicy->setKeyRepeatConfiguration(KEY_REPEAT_TIMEOUT, KEY_REPEAT_DELAY);
-        mDispatcher = std::make_unique<InputDispatcher>(mFakePolicy);
+        mDispatcher = std::make_unique<InputDispatcher>(*mFakePolicy);
         mDispatcher->requestRefreshConfiguration();
         mDispatcher->setInputDispatchMode(/*enabled*/ true, /*frozen*/ false);
         ASSERT_EQ(OK, mDispatcher->start());
diff --git a/services/inputflinger/tests/TestInputListenerMatchers.h b/services/inputflinger/tests/TestInputListenerMatchers.h
index 338b747..db6f254 100644
--- a/services/inputflinger/tests/TestInputListenerMatchers.h
+++ b/services/inputflinger/tests/TestInputListenerMatchers.h
@@ -73,6 +73,12 @@
     return arg.pointerCount == count;
 }
 
+MATCHER_P2(WithPointerId, index, id, "MotionEvent with specified pointer ID for pointer index") {
+    const auto argPointerId = arg.pointerProperties[index].id;
+    *result_listener << "expected pointer with index " << index << " to have ID " << argPointerId;
+    return argPointerId == id;
+}
+
 MATCHER_P2(WithCoords, x, y, "InputEvent with specified coords") {
     const auto argX = arg.pointerCoords[0].getX();
     const auto argY = arg.pointerCoords[0].getY();
@@ -136,6 +142,22 @@
     return argPressure == pressure;
 }
 
+MATCHER_P2(WithTouchDimensions, maj, min, "InputEvent with specified touch dimensions") {
+    const auto argMajor = arg.pointerCoords[0].getAxisValue(AMOTION_EVENT_AXIS_TOUCH_MAJOR);
+    const auto argMinor = arg.pointerCoords[0].getAxisValue(AMOTION_EVENT_AXIS_TOUCH_MINOR);
+    *result_listener << "expected touch dimensions " << maj << " major x " << min
+                     << " minor, but got " << argMajor << " major x " << argMinor << " minor";
+    return argMajor == maj && argMinor == min;
+}
+
+MATCHER_P2(WithToolDimensions, maj, min, "InputEvent with specified tool dimensions") {
+    const auto argMajor = arg.pointerCoords[0].getAxisValue(AMOTION_EVENT_AXIS_TOOL_MAJOR);
+    const auto argMinor = arg.pointerCoords[0].getAxisValue(AMOTION_EVENT_AXIS_TOOL_MINOR);
+    *result_listener << "expected tool dimensions " << maj << " major x " << min
+                     << " minor, but got " << argMajor << " major x " << argMinor << " minor";
+    return argMajor == maj && argMinor == min;
+}
+
 MATCHER_P(WithToolType, toolType, "InputEvent with specified tool type") {
     const auto argToolType = arg.pointerProperties[0].toolType;
     *result_listener << "expected tool type " << ftl::enum_string(toolType) << ", but got "
@@ -143,6 +165,14 @@
     return argToolType == toolType;
 }
 
+MATCHER_P2(WithPointerToolType, pointer, toolType,
+           "InputEvent with specified tool type for pointer") {
+    const auto argToolType = arg.pointerProperties[pointer].toolType;
+    *result_listener << "expected pointer " << pointer << " to have tool type "
+                     << ftl::enum_string(toolType) << ", but got " << ftl::enum_string(argToolType);
+    return argToolType == toolType;
+}
+
 MATCHER_P(WithFlags, flags, "InputEvent with specified flags") {
     *result_listener << "expected flags " << flags << ", but got " << arg.flags;
     return arg.flags == static_cast<int32_t>(flags);
diff --git a/services/sensorservice/SensorService.h b/services/sensorservice/SensorService.h
index fe72a69..0aa1bcb 100644
--- a/services/sensorservice/SensorService.h
+++ b/services/sensorservice/SensorService.h
@@ -288,7 +288,7 @@
             void onUidStateChanged(uid_t uid __unused, int32_t procState __unused,
                                    int64_t procStateSeq __unused,
                                    int32_t capability __unused) override {}
-            void onUidProcAdjChanged(uid_t uid __unused) override {}
+            void onUidProcAdjChanged(uid_t uid __unused, int32_t adj __unused) override {}
 
             void addOverrideUid(uid_t uid, bool active);
             void removeOverrideUid(uid_t uid);
diff --git a/services/surfaceflinger/FrontEnd/LayerCreationArgs.cpp b/services/surfaceflinger/FrontEnd/LayerCreationArgs.cpp
index cfa2b03..97af445 100644
--- a/services/surfaceflinger/FrontEnd/LayerCreationArgs.cpp
+++ b/services/surfaceflinger/FrontEnd/LayerCreationArgs.cpp
@@ -50,7 +50,7 @@
     }
 
     if (internalLayer) {
-        sequence = getInternalLayerId(sInternalSequence++);
+        sequence = id.value_or(getInternalLayerId(sInternalSequence++));
     } else if (id) {
         sequence = *id;
         sSequence = *id + 1;
diff --git a/services/surfaceflinger/FrontEnd/LayerLifecycleManager.cpp b/services/surfaceflinger/FrontEnd/LayerLifecycleManager.cpp
index 6cacfb5..cd9515c 100644
--- a/services/surfaceflinger/FrontEnd/LayerLifecycleManager.cpp
+++ b/services/surfaceflinger/FrontEnd/LayerLifecycleManager.cpp
@@ -38,7 +38,8 @@
         RequestedLayerState& layer = *newLayer.get();
         auto [it, inserted] = mIdToLayer.try_emplace(layer.id, References{.owner = layer});
         if (!inserted) {
-            LOG_ALWAYS_FATAL("Duplicate layer id %d found. Existing layer: %s", layer.id,
+            LOG_ALWAYS_FATAL("Duplicate layer id found. New layer: %s Existing layer: %s",
+                             layer.getDebugString().c_str(),
                              it->second.owner.getDebugString().c_str());
         }
         mAddedLayers.push_back(newLayer.get());
@@ -200,8 +201,10 @@
 
             if (layer->what & layer_state_t::eBackgroundColorChanged) {
                 if (layer->bgColorLayerId == UNASSIGNED_LAYER_ID && layer->bgColor.a != 0) {
-                    LayerCreationArgs backgroundLayerArgs(layer->id,
-                                                          /*internalLayer=*/true);
+                    LayerCreationArgs
+                            backgroundLayerArgs(LayerCreationArgs::getInternalLayerId(
+                                                        LayerCreationArgs::sInternalSequence++),
+                                                /*internalLayer=*/true);
                     backgroundLayerArgs.parentId = layer->id;
                     backgroundLayerArgs.name = layer->name + "BackgroundColorLayer";
                     backgroundLayerArgs.flags = ISurfaceComposerClient::eFXSurfaceEffect;
diff --git a/services/surfaceflinger/Layer.cpp b/services/surfaceflinger/Layer.cpp
index 9e40d7f..bfb7a22 100644
--- a/services/surfaceflinger/Layer.cpp
+++ b/services/surfaceflinger/Layer.cpp
@@ -254,7 +254,8 @@
         mFlinger->mTunnelModeEnabledReporter->decrementTunnelModeCount();
     }
     if (mHadClonedChild) {
-        mFlinger->mNumClones--;
+        auto& roots = mFlinger->mLayerMirrorRoots;
+        roots.erase(std::remove(roots.begin(), roots.end(), this), roots.end());
     }
     if (hasTrustedPresentationListener()) {
         mFlinger->mNumTrustedPresentationListeners--;
@@ -2552,7 +2553,10 @@
     return outputLayer ? outputLayer->getState().visibleRegion : Region();
 }
 
-void Layer::setInitialValuesForClone(const sp<Layer>& clonedFrom) {
+void Layer::setInitialValuesForClone(const sp<Layer>& clonedFrom, uint32_t mirrorRootId) {
+    mSnapshot->path.id = clonedFrom->getSequence();
+    mSnapshot->path.mirrorRootId = mirrorRootId;
+
     cloneDrawingState(clonedFrom.get());
     mClonedFrom = clonedFrom;
     mPremultipliedAlpha = clonedFrom->mPremultipliedAlpha;
@@ -2591,7 +2595,7 @@
     mDrawingState.inputInfo = tmpInputInfo;
 }
 
-void Layer::updateMirrorInfo() {
+bool Layer::updateMirrorInfo(const std::deque<Layer*>& cloneRootsPendingUpdates) {
     if (mClonedChild == nullptr || !mClonedChild->isClonedFromAlive()) {
         // If mClonedChild is null, there is nothing to mirror. If isClonedFromAlive returns false,
         // it means that there is a clone, but the layer it was cloned from has been destroyed. In
@@ -2599,7 +2603,7 @@
         // destroyed. The root, this layer, will still be around since the client can continue
         // to hold a reference, but no cloned layers will be displayed.
         mClonedChild = nullptr;
-        return;
+        return true;
     }
 
     std::map<sp<Layer>, sp<Layer>> clonedLayersMap;
@@ -2614,6 +2618,13 @@
     mClonedChild->updateClonedDrawingState(clonedLayersMap);
     mClonedChild->updateClonedChildren(sp<Layer>::fromExisting(this), clonedLayersMap);
     mClonedChild->updateClonedRelatives(clonedLayersMap);
+
+    for (Layer* root : cloneRootsPendingUpdates) {
+        if (clonedLayersMap.find(sp<Layer>::fromExisting(root)) != clonedLayersMap.end()) {
+            return false;
+        }
+    }
+    return true;
 }
 
 void Layer::updateClonedDrawingState(std::map<sp<Layer>, sp<Layer>>& clonedLayersMap) {
@@ -2653,7 +2664,7 @@
         }
         sp<Layer> clonedChild = clonedLayersMap[child];
         if (clonedChild == nullptr) {
-            clonedChild = child->createClone();
+            clonedChild = child->createClone(mirrorRoot->getSequence());
             clonedLayersMap[child] = clonedChild;
         }
         addChildToDrawing(clonedChild);
@@ -2761,7 +2772,7 @@
 void Layer::setClonedChild(const sp<Layer>& clonedChild) {
     mClonedChild = clonedChild;
     mHadClonedChild = true;
-    mFlinger->mNumClones++;
+    mFlinger->mLayerMirrorRoots.push_back(this);
 }
 
 bool Layer::setDropInputMode(gui::DropInputMode mode) {
@@ -3491,11 +3502,11 @@
     }
 }
 
-sp<Layer> Layer::createClone() {
+sp<Layer> Layer::createClone(uint32_t mirrorRootId) {
     LayerCreationArgs args(mFlinger.get(), nullptr, mName + " (Mirror)", 0, LayerMetadata());
     args.textureName = mTextureName;
     sp<Layer> layer = mFlinger->getFactory().createBufferStateLayer(args);
-    layer->setInitialValuesForClone(sp<Layer>::fromExisting(this));
+    layer->setInitialValuesForClone(sp<Layer>::fromExisting(this), mirrorRootId);
     return layer;
 }
 
diff --git a/services/surfaceflinger/Layer.h b/services/surfaceflinger/Layer.h
index b37fa15..4374914 100644
--- a/services/surfaceflinger/Layer.h
+++ b/services/surfaceflinger/Layer.h
@@ -248,7 +248,7 @@
     // true if this layer is visible, false otherwise
     virtual bool isVisible() const;
 
-    virtual sp<Layer> createClone();
+    virtual sp<Layer> createClone(uint32_t mirrorRoot);
 
     // Set a 2x2 transformation matrix on the layer. This transform
     // will be applied after parent transforms, but before any final
@@ -651,7 +651,7 @@
 
     gui::WindowInfo::Type getWindowType() const { return mWindowType; }
 
-    void updateMirrorInfo();
+    bool updateMirrorInfo(const std::deque<Layer*>& cloneRootsPendingUpdates);
 
     /*
      * doTransaction - process the transaction. This is a good place to figure
@@ -922,7 +922,7 @@
     friend class TransactionFrameTracerTest;
     friend class TransactionSurfaceFrameTest;
 
-    virtual void setInitialValuesForClone(const sp<Layer>& clonedFrom);
+    virtual void setInitialValuesForClone(const sp<Layer>& clonedFrom, uint32_t mirrorRootId);
     void preparePerFrameCompositionState();
     void preparePerFrameBufferCompositionState();
     void preparePerFrameEffectsCompositionState();
diff --git a/services/surfaceflinger/LayerRenderArea.cpp b/services/surfaceflinger/LayerRenderArea.cpp
index f6b5afc..d606cff 100644
--- a/services/surfaceflinger/LayerRenderArea.cpp
+++ b/services/surfaceflinger/LayerRenderArea.cpp
@@ -85,7 +85,7 @@
     // If layer is offscreen, update mirroring info if it exists
     if (mLayer->isRemovedFromCurrentState()) {
         mLayer->traverse(LayerVector::StateSet::Drawing,
-                         [&](Layer* layer) { layer->updateMirrorInfo(); });
+                         [&](Layer* layer) { layer->updateMirrorInfo({}); });
         mLayer->traverse(LayerVector::StateSet::Drawing,
                          [&](Layer* layer) { layer->updateCloneBufferInfo(); });
     }
diff --git a/services/surfaceflinger/Scheduler/EventThread.cpp b/services/surfaceflinger/Scheduler/EventThread.cpp
index 1c0bf0d..af9acf3 100644
--- a/services/surfaceflinger/Scheduler/EventThread.cpp
+++ b/services/surfaceflinger/Scheduler/EventThread.cpp
@@ -42,6 +42,7 @@
 #include <utils/Errors.h>
 #include <utils/Trace.h>
 
+#include <scheduler/VsyncConfig.h>
 #include "DisplayHardware/DisplayMode.h"
 #include "FrameTimeline.h"
 #include "VSyncDispatch.h"
@@ -597,25 +598,34 @@
                                         nsecs_t timestamp,
                                         nsecs_t preferredExpectedPresentationTime,
                                         nsecs_t preferredDeadlineTimestamp) const {
+    uint32_t currentIndex = 0;
     // Add 1 to ensure the preferredFrameTimelineIndex entry (when multiplier == 0) is included.
-    for (int64_t multiplier = -VsyncEventData::kFrameTimelinesLength + 1, currentIndex = 0;
-         currentIndex < VsyncEventData::kFrameTimelinesLength; multiplier++) {
+    for (int64_t multiplier = -VsyncEventData::kFrameTimelinesCapacity + 1;
+         currentIndex < VsyncEventData::kFrameTimelinesCapacity; multiplier++) {
         nsecs_t deadlineTimestamp = preferredDeadlineTimestamp + multiplier * frameInterval;
-        // Valid possible frame timelines must have future values.
-        if (deadlineTimestamp > timestamp) {
-            if (multiplier == 0) {
-                outVsyncEventData.preferredFrameTimelineIndex = currentIndex;
-            }
-            nsecs_t expectedPresentationTime =
-                    preferredExpectedPresentationTime + multiplier * frameInterval;
-            outVsyncEventData.frameTimelines[currentIndex] =
-                    {.vsyncId =
-                             generateToken(timestamp, deadlineTimestamp, expectedPresentationTime),
-                     .deadlineTimestamp = deadlineTimestamp,
-                     .expectedPresentationTime = expectedPresentationTime};
-            currentIndex++;
+        // Valid possible frame timelines must have future values, so find a later frame timeline.
+        if (deadlineTimestamp <= timestamp) {
+            continue;
         }
+
+        nsecs_t expectedPresentationTime =
+                preferredExpectedPresentationTime + multiplier * frameInterval;
+        if (expectedPresentationTime >= preferredExpectedPresentationTime +
+                    scheduler::VsyncConfig::kEarlyLatchMaxThreshold.count()) {
+            break;
+        }
+
+        if (multiplier == 0) {
+            outVsyncEventData.preferredFrameTimelineIndex = currentIndex;
+        }
+
+        outVsyncEventData.frameTimelines[currentIndex] =
+                {.vsyncId = generateToken(timestamp, deadlineTimestamp, expectedPresentationTime),
+                 .deadlineTimestamp = deadlineTimestamp,
+                 .expectedPresentationTime = expectedPresentationTime};
+        currentIndex++;
     }
+    outVsyncEventData.frameTimelinesLength = currentIndex;
 }
 
 void EventThread::dispatchEvent(const DisplayEventReceiver::Event& event,
diff --git a/services/surfaceflinger/Scheduler/Scheduler.cpp b/services/surfaceflinger/Scheduler/Scheduler.cpp
index 1e45b41..9319543 100644
--- a/services/surfaceflinger/Scheduler/Scheduler.cpp
+++ b/services/surfaceflinger/Scheduler/Scheduler.cpp
@@ -406,11 +406,13 @@
 
 void Scheduler::enableHardwareVsync(PhysicalDisplayId id) {
     auto schedule = getVsyncSchedule(id);
+    LOG_ALWAYS_FATAL_IF(!schedule);
     schedule->enableHardwareVsync(mSchedulerCallback);
 }
 
 void Scheduler::disableHardwareVsync(PhysicalDisplayId id, bool disallow) {
     auto schedule = getVsyncSchedule(id);
+    LOG_ALWAYS_FATAL_IF(!schedule);
     schedule->disableHardwareVsync(mSchedulerCallback, disallow);
 }
 
@@ -427,7 +429,10 @@
 void Scheduler::resyncToHardwareVsyncLocked(PhysicalDisplayId id, bool allowToEnable,
                                             std::optional<Fps> refreshRate) {
     const auto displayOpt = mDisplays.get(id);
-    LOG_ALWAYS_FATAL_IF(!displayOpt);
+    if (!displayOpt) {
+        ALOGW("%s: Invalid display %s!", __func__, to_string(id).c_str());
+        return;
+    }
     const Display& display = *displayOpt;
 
     if (display.schedulePtr->isHardwareVsyncAllowed(allowToEnable)) {
@@ -446,7 +451,10 @@
     ftl::FakeGuard guard(kMainThreadContext);
 
     const auto displayOpt = mDisplays.get(id);
-    LOG_ALWAYS_FATAL_IF(!displayOpt);
+    if (!displayOpt) {
+        ALOGW("%s: Invalid display %s!", __func__, to_string(id).c_str());
+        return;
+    }
     const Display& display = *displayOpt;
     const auto mode = display.selectorPtr->getActiveMode();
 
@@ -478,12 +486,18 @@
     const auto hwcVsyncPeriod = ftl::Optional(hwcVsyncPeriodIn).transform([](nsecs_t nanos) {
         return Period::fromNs(nanos);
     });
-    return getVsyncSchedule(id)->addResyncSample(mSchedulerCallback, TimePoint::fromNs(timestamp),
-                                                 hwcVsyncPeriod);
+    auto schedule = getVsyncSchedule(id);
+    if (!schedule) {
+        ALOGW("%s: Invalid display %s!", __func__, to_string(id).c_str());
+        return false;
+    }
+    return schedule->addResyncSample(mSchedulerCallback, TimePoint::fromNs(timestamp),
+                                     hwcVsyncPeriod);
 }
 
 void Scheduler::addPresentFence(PhysicalDisplayId id, std::shared_ptr<FenceTime> fence) {
     auto schedule = getVsyncSchedule(id);
+    LOG_ALWAYS_FATAL_IF(!schedule);
     const bool needMoreSignals = schedule->getController().addPresentFence(std::move(fence));
     if (needMoreSignals) {
         schedule->enableHardwareVsync(mSchedulerCallback);
@@ -553,6 +567,7 @@
     {
         std::scoped_lock lock(mDisplayLock);
         auto vsyncSchedule = getVsyncScheduleLocked(id);
+        LOG_ALWAYS_FATAL_IF(!vsyncSchedule);
         vsyncSchedule->getController().setDisplayPowerMode(powerMode);
     }
     if (!isPacesetter) return;
@@ -582,7 +597,9 @@
     }
 
     const auto displayOpt = mDisplays.get(*idOpt);
-    LOG_ALWAYS_FATAL_IF(!displayOpt);
+    if (!displayOpt) {
+        return nullptr;
+    }
     return displayOpt->get().schedulePtr;
 }
 
diff --git a/services/surfaceflinger/Scheduler/Scheduler.h b/services/surfaceflinger/Scheduler/Scheduler.h
index 43aab2d..f13c878 100644
--- a/services/surfaceflinger/Scheduler/Scheduler.h
+++ b/services/surfaceflinger/Scheduler/Scheduler.h
@@ -196,8 +196,8 @@
     // Sets the render rate for the scheduler to run at.
     void setRenderRate(PhysicalDisplayId, Fps);
 
-    void enableHardwareVsync(PhysicalDisplayId);
-    void disableHardwareVsync(PhysicalDisplayId, bool disallow);
+    void enableHardwareVsync(PhysicalDisplayId) REQUIRES(kMainThreadContext);
+    void disableHardwareVsync(PhysicalDisplayId, bool disallow) REQUIRES(kMainThreadContext);
 
     // Resyncs the scheduler to hardware vsync.
     // If allowToEnable is true, then hardware vsync will be turned on.
@@ -219,7 +219,8 @@
     // otherwise.
     bool addResyncSample(PhysicalDisplayId, nsecs_t timestamp,
                          std::optional<nsecs_t> hwcVsyncPeriod);
-    void addPresentFence(PhysicalDisplayId, std::shared_ptr<FenceTime>) EXCLUDES(mDisplayLock);
+    void addPresentFence(PhysicalDisplayId, std::shared_ptr<FenceTime>) EXCLUDES(mDisplayLock)
+            REQUIRES(kMainThreadContext);
 
     // Layers are registered on creation, and unregistered when the weak reference expires.
     void registerLayer(Layer*);
diff --git a/services/surfaceflinger/Scheduler/include/scheduler/VsyncConfig.h b/services/surfaceflinger/Scheduler/include/scheduler/VsyncConfig.h
index 3b1985f..47d95a8 100644
--- a/services/surfaceflinger/Scheduler/include/scheduler/VsyncConfig.h
+++ b/services/surfaceflinger/Scheduler/include/scheduler/VsyncConfig.h
@@ -22,6 +22,8 @@
 
 namespace android::scheduler {
 
+using namespace std::chrono_literals;
+
 // Phase offsets and work durations for SF and app deadlines from VSYNC.
 struct VsyncConfig {
     nsecs_t sfOffset;
@@ -35,6 +37,10 @@
     }
 
     bool operator!=(const VsyncConfig& other) const { return !(*this == other); }
+
+    // The duration for which SF can delay a frame if it is considered early based on the
+    // VsyncConfig::appWorkDuration.
+    static constexpr std::chrono::nanoseconds kEarlyLatchMaxThreshold = 100ms;
 };
 
 struct VsyncConfigSet {
diff --git a/services/surfaceflinger/ScreenCaptureOutput.cpp b/services/surfaceflinger/ScreenCaptureOutput.cpp
index b70b53d..09dac23 100644
--- a/services/surfaceflinger/ScreenCaptureOutput.cpp
+++ b/services/surfaceflinger/ScreenCaptureOutput.cpp
@@ -94,6 +94,16 @@
         }
     }
 
+    if (outputDataspace == ui::Dataspace::BT2020_HLG) {
+        for (auto& layer : clientCompositionLayers) {
+            auto transfer = layer.sourceDataspace & ui::Dataspace::TRANSFER_MASK;
+            if (transfer != static_cast<int32_t>(ui::Dataspace::TRANSFER_HLG) &&
+                transfer != static_cast<int32_t>(ui::Dataspace::TRANSFER_ST2084)) {
+                layer.whitePointNits *= (1000.0f / 203.0f);
+            }
+        }
+    }
+
     Rect sourceCrop = mRenderArea.getSourceCrop();
     compositionengine::LayerFE::LayerSettings fillLayer;
     fillLayer.source.buffer.buffer = nullptr;
diff --git a/services/surfaceflinger/SurfaceFlinger.cpp b/services/surfaceflinger/SurfaceFlinger.cpp
index 0e3c47b..2d40678 100644
--- a/services/surfaceflinger/SurfaceFlinger.cpp
+++ b/services/surfaceflinger/SurfaceFlinger.cpp
@@ -1146,17 +1146,26 @@
     std::optional<PhysicalDisplayId> displayIdOpt;
     {
         Mutex::Autolock lock(mStateLock);
-        displayIdOpt = getPhysicalDisplayIdLocked(displayToken);
+        if (displayToken) {
+            displayIdOpt = getPhysicalDisplayIdLocked(displayToken);
+            if (!displayIdOpt) {
+                ALOGW("%s: Invalid physical display token %p", __func__, displayToken.get());
+                return NAME_NOT_FOUND;
+            }
+        } else {
+            // TODO (b/277364366): Clients should be updated to pass in the display they
+            // want, rather than us picking an arbitrary one (the active display, in this
+            // case).
+            displayIdOpt = mActiveDisplayId;
+        }
     }
 
-    // TODO (b/277364366): Clients should be updated to pass in the display they
-    // want, rather than us picking an arbitrary one (the pacesetter, in this
-    // case).
-    if (displayToken && !displayIdOpt) {
-        ALOGE("%s: Invalid physical display token %p", __func__, displayToken.get());
+    const auto schedule = mScheduler->getVsyncSchedule(displayIdOpt);
+    if (!schedule) {
+        ALOGE("%s: Missing VSYNC schedule for display %s!", __func__,
+              to_string(*displayIdOpt).c_str());
         return NAME_NOT_FOUND;
     }
-    const auto schedule = mScheduler->getVsyncSchedule(displayIdOpt);
     outStats->vsyncTime = schedule->vsyncDeadlineAfter(TimePoint::now()).ns();
     outStats->vsyncPeriod = schedule->period().ns();
     return NO_ERROR;
@@ -2136,7 +2145,9 @@
     static_cast<void>(mScheduler->schedule([=]() FTL_FAKE_GUARD(mStateLock) {
         {
             ftl::FakeGuard guard(kMainThreadContext);
-            mScheduler->getVsyncSchedule(id)->setPendingHardwareVsyncState(enabled);
+            if (auto schedule = mScheduler->getVsyncSchedule(id)) {
+                schedule->setPendingHardwareVsyncState(enabled);
+            }
         }
 
         ATRACE_FORMAT("%s (%d) for %" PRIu64 " (main thread)", whence, enabled, id.value);
@@ -2307,7 +2318,7 @@
                 sp<Layer> bgColorLayer = getFactory().createEffectLayer(
                         LayerCreationArgs(this, nullptr, layer->name,
                                           ISurfaceComposerClient::eFXSurfaceEffect, LayerMetadata(),
-                                          std::make_optional(layer->parentId), true));
+                                          std::make_optional(layer->id), true));
                 mLegacyLayers[bgColorLayer->sequence] = bgColorLayer;
             }
             const bool willReleaseBufferOnLatch = layer->willReleaseBufferOnLatch();
@@ -2874,7 +2885,10 @@
     }
 
     for (auto layer : mLayersWithBuffersRemoved) {
-        for (auto layerStack : layer->mPreviouslyPresentedLayerStacks) {
+        std::vector<ui::LayerStack> previouslyPresentedLayerStacks =
+                std::move(layer->mPreviouslyPresentedLayerStacks);
+        layer->mPreviouslyPresentedLayerStacks.clear();
+        for (auto layerStack : previouslyPresentedLayerStacks) {
             auto optDisplay = layerStackToDisplay.get(layerStack);
             if (optDisplay && !optDisplay->get()->isVirtual()) {
                 auto fence = getHwComposer().getPresentFence(optDisplay->get()->getPhysicalId());
@@ -4010,8 +4024,21 @@
     }
 
     commitOffscreenLayers();
-    if (mNumClones > 0) {
-        mDrawingState.traverse([&](Layer* layer) { layer->updateMirrorInfo(); });
+    if (mLayerMirrorRoots.size() > 0) {
+        std::deque<Layer*> pendingUpdates;
+        pendingUpdates.insert(pendingUpdates.end(), mLayerMirrorRoots.begin(),
+                              mLayerMirrorRoots.end());
+        std::vector<Layer*> needsUpdating;
+        for (Layer* cloneRoot : mLayerMirrorRoots) {
+            pendingUpdates.pop_front();
+            if (cloneRoot->updateMirrorInfo(pendingUpdates)) {
+            } else {
+                needsUpdating.push_back(cloneRoot);
+            }
+        }
+        for (Layer* cloneRoot : needsUpdating) {
+            cloneRoot->updateMirrorInfo({});
+        }
     }
 }
 
@@ -4118,7 +4145,7 @@
         mBootStage = BootStage::BOOTANIMATION;
     }
 
-    if (mNumClones > 0) {
+    if (mLayerMirrorRoots.size() > 0) {
         mDrawingState.traverse([&](Layer* layer) { layer->updateCloneBufferInfo(); });
     }
 
@@ -4398,10 +4425,8 @@
 
     const auto predictedPresentTime = TimePoint::fromNs(prediction->presentTime);
 
-    // The duration for which SF can delay a frame if it is considered early based on the
-    // VsyncConfig::appWorkDuration.
-    if (constexpr std::chrono::nanoseconds kEarlyLatchMaxThreshold = 100ms;
-        std::chrono::abs(predictedPresentTime - expectedPresentTime) >= kEarlyLatchMaxThreshold) {
+    if (std::chrono::abs(predictedPresentTime - expectedPresentTime) >=
+        scheduler::VsyncConfig::kEarlyLatchMaxThreshold) {
         return false;
     }
 
@@ -5247,7 +5272,7 @@
             return result;
         }
 
-        mirrorLayer->setClonedChild(mirrorFrom->createClone());
+        mirrorLayer->setClonedChild(mirrorFrom->createClone(mirrorLayer->getSequence()));
     }
 
     outResult.layerId = mirrorLayer->sequence;
@@ -8049,7 +8074,7 @@
                 Mutex::Autolock lock(mStateLock);
                 createEffectLayer(mirrorArgs, &unused, &childMirror);
                 MUTEX_ALIAS(mStateLock, childMirror->mFlinger->mStateLock);
-                childMirror->setClonedChild(layer->createClone());
+                childMirror->setClonedChild(layer->createClone(childMirror->getSequence()));
                 childMirror->reparent(mirrorDisplay.rootHandle);
             }
             // lock on mStateLock needs to be released before binder handle gets destroyed
diff --git a/services/surfaceflinger/SurfaceFlinger.h b/services/surfaceflinger/SurfaceFlinger.h
index 8eaa1c7..8cc0184 100644
--- a/services/surfaceflinger/SurfaceFlinger.h
+++ b/services/surfaceflinger/SurfaceFlinger.h
@@ -296,8 +296,7 @@
     // the client can no longer modify this layer directly.
     void onHandleDestroyed(BBinder* handle, sp<Layer>& layer, uint32_t layerId);
 
-    // TODO: Remove atomic if move dtor to main thread CL lands
-    std::atomic<uint32_t> mNumClones;
+    std::vector<Layer*> mLayerMirrorRoots;
 
     TransactionCallbackInvoker& getTransactionCallbackInvoker() {
         return mTransactionCallbackInvoker;
diff --git a/services/surfaceflinger/fuzzer/surfaceflinger_scheduler_fuzzer.h b/services/surfaceflinger/fuzzer/surfaceflinger_scheduler_fuzzer.h
index a32750e..8061a8f 100644
--- a/services/surfaceflinger/fuzzer/surfaceflinger_scheduler_fuzzer.h
+++ b/services/surfaceflinger/fuzzer/surfaceflinger_scheduler_fuzzer.h
@@ -75,7 +75,7 @@
 
     bool isVisible() const override { return true; }
 
-    sp<Layer> createClone() override { return nullptr; }
+    sp<Layer> createClone(uint32_t /* mirrorRootId */) override { return nullptr; }
 };
 
 class FuzzImplVSyncTracker : public scheduler::VSyncTracker {
diff --git a/services/surfaceflinger/tests/DisplayEventReceiver_test.cpp b/services/surfaceflinger/tests/DisplayEventReceiver_test.cpp
index 0df7e2f..4c26017 100644
--- a/services/surfaceflinger/tests/DisplayEventReceiver_test.cpp
+++ b/services/surfaceflinger/tests/DisplayEventReceiver_test.cpp
@@ -33,9 +33,14 @@
 
     const VsyncEventData& vsyncEventData = parcelableVsyncEventData.vsync;
     EXPECT_NE(std::numeric_limits<size_t>::max(), vsyncEventData.preferredFrameTimelineIndex);
+    EXPECT_GT(static_cast<int64_t>(vsyncEventData.frameTimelinesLength), 0)
+            << "Frame timelines length should be greater than 0";
+    EXPECT_LE(static_cast<int64_t>(vsyncEventData.frameTimelinesLength),
+              VsyncEventData::kFrameTimelinesCapacity)
+            << "Frame timelines length should not exceed max capacity";
     EXPECT_GT(vsyncEventData.frameTimelines[0].deadlineTimestamp, now)
             << "Deadline timestamp should be greater than frame time";
-    for (size_t i = 0; i < VsyncEventData::kFrameTimelinesLength; i++) {
+    for (size_t i = 0; i < vsyncEventData.frameTimelinesLength; i++) {
         EXPECT_NE(gui::FrameTimelineInfo::INVALID_VSYNC_ID,
                   vsyncEventData.frameTimelines[i].vsyncId);
         EXPECT_GT(vsyncEventData.frameTimelines[i].expectedPresentationTime,
diff --git a/services/surfaceflinger/tests/unittests/Android.bp b/services/surfaceflinger/tests/unittests/Android.bp
index 3713275..55705ca 100644
--- a/services/surfaceflinger/tests/unittests/Android.bp
+++ b/services/surfaceflinger/tests/unittests/Android.bp
@@ -104,6 +104,7 @@
         "SurfaceFlinger_DisplayTransactionCommitTest.cpp",
         "SurfaceFlinger_ExcludeDolbyVisionTest.cpp",
         "SurfaceFlinger_GetDisplayNativePrimariesTest.cpp",
+        "SurfaceFlinger_GetDisplayStatsTest.cpp",
         "SurfaceFlinger_HdrOutputControlTest.cpp",
         "SurfaceFlinger_HotplugTest.cpp",
         "SurfaceFlinger_InitializeDisplaysTest.cpp",
diff --git a/services/surfaceflinger/tests/unittests/EventThreadTest.cpp b/services/surfaceflinger/tests/unittests/EventThreadTest.cpp
index f1cdca3..5fed9b4 100644
--- a/services/surfaceflinger/tests/unittests/EventThreadTest.cpp
+++ b/services/surfaceflinger/tests/unittests/EventThreadTest.cpp
@@ -24,6 +24,7 @@
 #include <gmock/gmock.h>
 #include <gtest/gtest.h>
 #include <log/log.h>
+#include <scheduler/VsyncConfig.h>
 #include <utils/Errors.h>
 
 #include "AsyncCallRecorder.h"
@@ -77,7 +78,7 @@
     EventThreadTest();
     ~EventThreadTest() override;
 
-    void createThread();
+    void setupEventThread(std::chrono::nanoseconds vsyncPeriod);
     sp<MockEventThreadConnection> createConnection(ConnectionEventRecorder& recorder,
                                                    EventRegistrationFlags eventRegistration = {},
                                                    uid_t ownerUid = mConnectionUid);
@@ -90,8 +91,9 @@
                                               nsecs_t expectedTimestamp, unsigned expectedCount);
     void expectVsyncEventReceivedByConnection(nsecs_t expectedTimestamp, unsigned expectedCount);
     void expectVsyncEventFrameTimelinesCorrect(
-            nsecs_t expectedTimestamp,
-            /*VSyncSource::VSyncData*/ gui::VsyncEventData::FrameTimeline preferredVsyncData);
+            nsecs_t expectedTimestamp, gui::VsyncEventData::FrameTimeline preferredVsyncData);
+    void expectVsyncEventDataFrameTimelinesValidLength(VsyncEventData vsyncEventData,
+                                                       std::chrono::nanoseconds vsyncPeriod);
     void expectHotplugEventReceivedByConnection(PhysicalDisplayId expectedDisplayId,
                                                 bool expectedConnected);
     void expectConfigChangedEventReceivedByConnection(PhysicalDisplayId expectedDisplayId,
@@ -154,19 +156,6 @@
             .WillRepeatedly(Invoke(mVSyncCallbackUpdateRecorder.getInvocable()));
     EXPECT_CALL(mockDispatch, unregisterCallback(_))
             .WillRepeatedly(Invoke(mVSyncCallbackUnregisterRecorder.getInvocable()));
-
-    createThread();
-    mConnection =
-            createConnection(mConnectionEventCallRecorder,
-                             gui::ISurfaceComposer::EventRegistration::modeChanged |
-                                     gui::ISurfaceComposer::EventRegistration::frameRateOverride);
-    mThrottledConnection = createConnection(mThrottledConnectionEventCallRecorder,
-                                            gui::ISurfaceComposer::EventRegistration::modeChanged,
-                                            mThrottledConnectionUid);
-
-    // A display must be connected for VSYNC events to be delivered.
-    mThread->onHotplugReceived(INTERNAL_DISPLAY_ID, true);
-    expectHotplugEventReceivedByConnection(INTERNAL_DISPLAY_ID, true);
 }
 
 EventThreadTest::~EventThreadTest() {
@@ -179,14 +168,12 @@
     EXPECT_TRUE(mVSyncCallbackUnregisterRecorder.waitForCall().has_value());
 }
 
-void EventThreadTest::createThread() {
+void EventThreadTest::setupEventThread(std::chrono::nanoseconds vsyncPeriod) {
     const auto throttleVsync = [&](nsecs_t expectedVsyncTimestamp, uid_t uid) {
         mThrottleVsyncCallRecorder.getInvocable()(expectedVsyncTimestamp, uid);
         return (uid == mThrottledConnectionUid);
     };
-    const auto getVsyncPeriod = [](uid_t uid) {
-        return VSYNC_PERIOD.count();
-    };
+    const auto getVsyncPeriod = [vsyncPeriod](uid_t uid) { return vsyncPeriod.count(); };
 
     mTokenManager = std::make_unique<frametimeline::impl::TokenManager>();
     mThread = std::make_unique<impl::EventThread>("EventThreadTest", mVsyncSchedule,
@@ -195,6 +182,18 @@
 
     // EventThread should register itself as VSyncSource callback.
     EXPECT_TRUE(mVSyncCallbackRegisterRecorder.waitForCall().has_value());
+
+    mConnection =
+            createConnection(mConnectionEventCallRecorder,
+                             gui::ISurfaceComposer::EventRegistration::modeChanged |
+                                     gui::ISurfaceComposer::EventRegistration::frameRateOverride);
+    mThrottledConnection = createConnection(mThrottledConnectionEventCallRecorder,
+                                            gui::ISurfaceComposer::EventRegistration::modeChanged,
+                                            mThrottledConnectionUid);
+
+    // A display must be connected for VSYNC events to be delivered.
+    mThread->onHotplugReceived(INTERNAL_DISPLAY_ID, true);
+    expectHotplugEventReceivedByConnection(INTERNAL_DISPLAY_ID, true);
 }
 
 sp<EventThreadTest::MockEventThreadConnection> EventThreadTest::createConnection(
@@ -259,7 +258,7 @@
     ASSERT_TRUE(args.has_value()) << " did not receive an event for timestamp "
                                   << expectedTimestamp;
     const auto& event = std::get<0>(args.value());
-    for (int i = 0; i < VsyncEventData::kFrameTimelinesLength; i++) {
+    for (int i = 0; i < event.vsync.vsyncData.frameTimelinesLength; i++) {
         auto prediction = mTokenManager->getPredictionsForToken(
                 event.vsync.vsyncData.frameTimelines[i].vsyncId);
         EXPECT_TRUE(prediction.has_value());
@@ -293,6 +292,21 @@
     }
 }
 
+void EventThreadTest::expectVsyncEventDataFrameTimelinesValidLength(
+        VsyncEventData vsyncEventData, std::chrono::nanoseconds vsyncPeriod) {
+    float nonPreferredTimelinesAmount =
+            scheduler::VsyncConfig::kEarlyLatchMaxThreshold / vsyncPeriod;
+    EXPECT_LE(vsyncEventData.frameTimelinesLength, nonPreferredTimelinesAmount + 1)
+            << "Amount of non-preferred frame timelines too many;"
+            << " expected presentation time will be over threshold";
+    EXPECT_LT(nonPreferredTimelinesAmount, VsyncEventData::kFrameTimelinesCapacity)
+            << "Amount of non-preferred frame timelines should be less than max capacity";
+    EXPECT_GT(static_cast<int64_t>(vsyncEventData.frameTimelinesLength), 0)
+            << "Frame timelines length should be greater than 0";
+    EXPECT_LT(vsyncEventData.preferredFrameTimelineIndex, vsyncEventData.frameTimelinesLength)
+            << "Preferred frame timeline index should be less than frame timelines length";
+}
+
 void EventThreadTest::expectHotplugEventReceivedByConnection(PhysicalDisplayId expectedDisplayId,
                                                              bool expectedConnected) {
     auto args = mConnectionEventCallRecorder.waitForCall();
@@ -343,6 +357,8 @@
  */
 
 TEST_F(EventThreadTest, canCreateAndDestroyThreadWithNoEventsSent) {
+    setupEventThread(VSYNC_PERIOD);
+
     EXPECT_FALSE(mVSyncCallbackRegisterRecorder.waitForCall(0us).has_value());
     EXPECT_FALSE(mVSyncCallbackScheduleRecorder.waitForCall(0us).has_value());
     EXPECT_FALSE(mVSyncCallbackUpdateRecorder.waitForCall(0us).has_value());
@@ -352,6 +368,8 @@
 }
 
 TEST_F(EventThreadTest, vsyncRequestIsIgnoredIfDisplayIsDisconnected) {
+    setupEventThread(VSYNC_PERIOD);
+
     mThread->onHotplugReceived(INTERNAL_DISPLAY_ID, false);
     expectHotplugEventReceivedByConnection(INTERNAL_DISPLAY_ID, false);
 
@@ -363,6 +381,8 @@
 }
 
 TEST_F(EventThreadTest, requestNextVsyncPostsASingleVSyncEventToTheConnection) {
+    setupEventThread(VSYNC_PERIOD);
+
     // Signal that we want the next vsync event to be posted to the connection
     mThread->requestNextVsync(mConnection);
 
@@ -394,6 +414,8 @@
 }
 
 TEST_F(EventThreadTest, requestNextVsyncEventFrameTimelinesCorrect) {
+    setupEventThread(VSYNC_PERIOD);
+
     // Signal that we want the next vsync event to be posted to the connection
     mThread->requestNextVsync(mConnection);
 
@@ -405,7 +427,34 @@
     expectVsyncEventFrameTimelinesCorrect(123, {-1, 789, 456});
 }
 
+TEST_F(EventThreadTest, requestNextVsyncEventFrameTimelinesValidLength) {
+    // The VsyncEventData should not have kFrameTimelinesCapacity amount of valid frame timelines,
+    // due to longer vsync period and kEarlyLatchMaxThreshold. Use length-2 to avoid decimal
+    // truncation (e.g. 60Hz has 16.6... ms vsync period).
+    std::chrono::nanoseconds vsyncPeriod(scheduler::VsyncConfig::kEarlyLatchMaxThreshold /
+                                         (VsyncEventData::kFrameTimelinesCapacity - 2));
+    setupEventThread(vsyncPeriod);
+
+    // Signal that we want the next vsync event to be posted to the connection
+    mThread->requestNextVsync(mConnection);
+
+    expectVSyncCallbackScheduleReceived(true);
+
+    // Use the received callback to signal a vsync event.
+    // The throttler should receive the event, as well as the connection.
+    nsecs_t expectedTimestamp = 123;
+    onVSyncEvent(expectedTimestamp, 456, 789);
+
+    auto args = mConnectionEventCallRecorder.waitForCall();
+    ASSERT_TRUE(args.has_value()) << " did not receive an event for timestamp "
+                                  << expectedTimestamp;
+    const VsyncEventData vsyncEventData = std::get<0>(args.value()).vsync.vsyncData;
+    expectVsyncEventDataFrameTimelinesValidLength(vsyncEventData, vsyncPeriod);
+}
+
 TEST_F(EventThreadTest, getLatestVsyncEventData) {
+    setupEventThread(VSYNC_PERIOD);
+
     const nsecs_t now = systemTime();
     const nsecs_t preferredExpectedPresentationTime = now + 20000000;
     const nsecs_t preferredDeadline = preferredExpectedPresentationTime - kReadyDuration.count();
@@ -420,9 +469,10 @@
     // Check EventThread immediately requested a resync.
     EXPECT_TRUE(mResyncCallRecorder.waitForCall().has_value());
 
+    expectVsyncEventDataFrameTimelinesValidLength(vsyncEventData, VSYNC_PERIOD);
     EXPECT_GT(vsyncEventData.frameTimelines[0].deadlineTimestamp, now)
             << "Deadline timestamp should be greater than frame time";
-    for (size_t i = 0; i < VsyncEventData::kFrameTimelinesLength; i++) {
+    for (size_t i = 0; i < vsyncEventData.frameTimelinesLength; i++) {
         auto prediction =
                 mTokenManager->getPredictionsForToken(vsyncEventData.frameTimelines[i].vsyncId);
         EXPECT_TRUE(prediction.has_value());
@@ -458,6 +508,8 @@
 }
 
 TEST_F(EventThreadTest, setVsyncRateZeroPostsNoVSyncEventsToThatConnection) {
+    setupEventThread(VSYNC_PERIOD);
+
     // Create a first connection, register it, and request a vsync rate of zero.
     ConnectionEventRecorder firstConnectionEventRecorder{0};
     sp<MockEventThreadConnection> firstConnection = createConnection(firstConnectionEventRecorder);
@@ -485,6 +537,8 @@
 }
 
 TEST_F(EventThreadTest, setVsyncRateOnePostsAllEventsToThatConnection) {
+    setupEventThread(VSYNC_PERIOD);
+
     mThread->setVsyncRate(1, mConnection);
 
     // EventThread should enable vsync callbacks.
@@ -508,6 +562,8 @@
 }
 
 TEST_F(EventThreadTest, setVsyncRateTwoPostsEveryOtherEventToThatConnection) {
+    setupEventThread(VSYNC_PERIOD);
+
     mThread->setVsyncRate(2, mConnection);
 
     // EventThread should enable vsync callbacks.
@@ -534,6 +590,8 @@
 }
 
 TEST_F(EventThreadTest, connectionsRemovedIfInstanceDestroyed) {
+    setupEventThread(VSYNC_PERIOD);
+
     mThread->setVsyncRate(1, mConnection);
 
     // EventThread should enable vsync callbacks.
@@ -551,6 +609,8 @@
 }
 
 TEST_F(EventThreadTest, connectionsRemovedIfEventDeliveryError) {
+    setupEventThread(VSYNC_PERIOD);
+
     ConnectionEventRecorder errorConnectionEventRecorder{NO_MEMORY};
     sp<MockEventThreadConnection> errorConnection = createConnection(errorConnectionEventRecorder);
     mThread->setVsyncRate(1, errorConnection);
@@ -575,6 +635,8 @@
 }
 
 TEST_F(EventThreadTest, tracksEventConnections) {
+    setupEventThread(VSYNC_PERIOD);
+
     EXPECT_EQ(2, mThread->getEventThreadConnectionCount());
     ConnectionEventRecorder errorConnectionEventRecorder{NO_MEMORY};
     sp<MockEventThreadConnection> errorConnection = createConnection(errorConnectionEventRecorder);
@@ -598,6 +660,8 @@
 }
 
 TEST_F(EventThreadTest, eventsDroppedIfNonfatalEventDeliveryError) {
+    setupEventThread(VSYNC_PERIOD);
+
     ConnectionEventRecorder errorConnectionEventRecorder{WOULD_BLOCK};
     sp<MockEventThreadConnection> errorConnection = createConnection(errorConnectionEventRecorder);
     mThread->setVsyncRate(1, errorConnection);
@@ -622,31 +686,43 @@
 }
 
 TEST_F(EventThreadTest, setPhaseOffsetForwardsToVSyncSource) {
+    setupEventThread(VSYNC_PERIOD);
+
     mThread->setDuration(321ns, 456ns);
     expectVSyncSetDurationCallReceived(321ns, 456ns);
 }
 
 TEST_F(EventThreadTest, postHotplugInternalDisconnect) {
+    setupEventThread(VSYNC_PERIOD);
+
     mThread->onHotplugReceived(INTERNAL_DISPLAY_ID, false);
     expectHotplugEventReceivedByConnection(INTERNAL_DISPLAY_ID, false);
 }
 
 TEST_F(EventThreadTest, postHotplugInternalConnect) {
+    setupEventThread(VSYNC_PERIOD);
+
     mThread->onHotplugReceived(INTERNAL_DISPLAY_ID, true);
     expectHotplugEventReceivedByConnection(INTERNAL_DISPLAY_ID, true);
 }
 
 TEST_F(EventThreadTest, postHotplugExternalDisconnect) {
+    setupEventThread(VSYNC_PERIOD);
+
     mThread->onHotplugReceived(EXTERNAL_DISPLAY_ID, false);
     expectHotplugEventReceivedByConnection(EXTERNAL_DISPLAY_ID, false);
 }
 
 TEST_F(EventThreadTest, postHotplugExternalConnect) {
+    setupEventThread(VSYNC_PERIOD);
+
     mThread->onHotplugReceived(EXTERNAL_DISPLAY_ID, true);
     expectHotplugEventReceivedByConnection(EXTERNAL_DISPLAY_ID, true);
 }
 
 TEST_F(EventThreadTest, postConfigChangedPrimary) {
+    setupEventThread(VSYNC_PERIOD);
+
     const auto mode = DisplayMode::Builder(hal::HWConfigId(0))
                               .setPhysicalDisplayId(INTERNAL_DISPLAY_ID)
                               .setId(DisplayModeId(7))
@@ -659,6 +735,8 @@
 }
 
 TEST_F(EventThreadTest, postConfigChangedExternal) {
+    setupEventThread(VSYNC_PERIOD);
+
     const auto mode = DisplayMode::Builder(hal::HWConfigId(0))
                               .setPhysicalDisplayId(EXTERNAL_DISPLAY_ID)
                               .setId(DisplayModeId(5))
@@ -671,6 +749,8 @@
 }
 
 TEST_F(EventThreadTest, postConfigChangedPrimary64bit) {
+    setupEventThread(VSYNC_PERIOD);
+
     const auto mode = DisplayMode::Builder(hal::HWConfigId(0))
                               .setPhysicalDisplayId(DISPLAY_ID_64BIT)
                               .setId(DisplayModeId(7))
@@ -682,6 +762,8 @@
 }
 
 TEST_F(EventThreadTest, suppressConfigChanged) {
+    setupEventThread(VSYNC_PERIOD);
+
     ConnectionEventRecorder suppressConnectionEventRecorder{0};
     sp<MockEventThreadConnection> suppressConnection =
             createConnection(suppressConnectionEventRecorder);
@@ -701,6 +783,8 @@
 }
 
 TEST_F(EventThreadTest, postUidFrameRateMapping) {
+    setupEventThread(VSYNC_PERIOD);
+
     const std::vector<FrameRateOverride> overrides = {
             {.uid = 1, .frameRateHz = 20},
             {.uid = 3, .frameRateHz = 40},
@@ -712,6 +796,8 @@
 }
 
 TEST_F(EventThreadTest, suppressUidFrameRateMapping) {
+    setupEventThread(VSYNC_PERIOD);
+
     const std::vector<FrameRateOverride> overrides = {
             {.uid = 1, .frameRateHz = 20},
             {.uid = 3, .frameRateHz = 40},
@@ -730,6 +816,8 @@
 }
 
 TEST_F(EventThreadTest, requestNextVsyncWithThrottleVsyncDoesntPostVSync) {
+    setupEventThread(VSYNC_PERIOD);
+
     // Signal that we want the next vsync event to be posted to the throttled connection
     mThread->requestNextVsync(mThrottledConnection);
 
diff --git a/services/surfaceflinger/tests/unittests/SurfaceFlinger_GetDisplayStatsTest.cpp b/services/surfaceflinger/tests/unittests/SurfaceFlinger_GetDisplayStatsTest.cpp
new file mode 100644
index 0000000..29acfaa
--- /dev/null
+++ b/services/surfaceflinger/tests/unittests/SurfaceFlinger_GetDisplayStatsTest.cpp
@@ -0,0 +1,116 @@
+/*
+ * 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.
+ */
+
+#undef LOG_TAG
+#define LOG_TAG "SurfaceFlingerGetDisplayStatsTest"
+
+#include <compositionengine/Display.h>
+#include <compositionengine/mock/DisplaySurface.h>
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+#include <renderengine/mock/RenderEngine.h>
+#include <ui/DisplayStatInfo.h>
+#include "TestableSurfaceFlinger.h"
+#include "mock/DisplayHardware/MockComposer.h"
+#include "mock/DisplayHardware/MockPowerAdvisor.h"
+#include "mock/MockTimeStats.h"
+#include "mock/system/window/MockNativeWindow.h"
+
+using namespace android;
+using namespace testing;
+
+namespace android {
+namespace {
+using FakeHwcDisplayInjector = TestableSurfaceFlinger::FakeHwcDisplayInjector;
+using FakeDisplayDeviceInjector = TestableSurfaceFlinger::FakeDisplayDeviceInjector;
+
+constexpr hal::HWDisplayId HWC_DISPLAY = FakeHwcDisplayInjector::DEFAULT_HWC_DISPLAY_ID;
+constexpr PhysicalDisplayId DEFAULT_DISPLAY_ID = PhysicalDisplayId::fromPort(42u);
+constexpr int DEFAULT_DISPLAY_WIDTH = 1920;
+constexpr int DEFAULT_DISPLAY_HEIGHT = 1024;
+
+class SurfaceFlingerGetDisplayStatsTest : public Test {
+public:
+    void SetUp() override;
+
+protected:
+    TestableSurfaceFlinger mFlinger;
+    renderengine::mock::RenderEngine* mRenderEngine = new renderengine::mock::RenderEngine();
+    sp<DisplayDevice> mDisplay;
+    sp<compositionengine::mock::DisplaySurface> mDisplaySurface =
+            sp<compositionengine::mock::DisplaySurface>::make();
+    sp<mock::NativeWindow> mNativeWindow = sp<mock::NativeWindow>::make();
+    mock::TimeStats* mTimeStats = new mock::TimeStats();
+    Hwc2::mock::PowerAdvisor* mPowerAdvisor = nullptr;
+    Hwc2::mock::Composer* mComposer = nullptr;
+};
+
+void SurfaceFlingerGetDisplayStatsTest::SetUp() {
+    mFlinger.setupMockScheduler({.displayId = DEFAULT_DISPLAY_ID});
+    mComposer = new Hwc2::mock::Composer();
+    mPowerAdvisor = new Hwc2::mock::PowerAdvisor();
+    mFlinger.setupRenderEngine(std::unique_ptr<renderengine::RenderEngine>(mRenderEngine));
+    mFlinger.setupTimeStats(std::shared_ptr<TimeStats>(mTimeStats));
+    mFlinger.setupComposer(std::unique_ptr<Hwc2::Composer>(mComposer));
+    mFlinger.setupPowerAdvisor(std::unique_ptr<Hwc2::PowerAdvisor>(mPowerAdvisor));
+    static constexpr bool kIsPrimary = true;
+    FakeHwcDisplayInjector(DEFAULT_DISPLAY_ID, hal::DisplayType::PHYSICAL, kIsPrimary)
+            .setPowerMode(hal::PowerMode::ON)
+            .inject(&mFlinger, mComposer);
+    auto compostionEngineDisplayArgs =
+            compositionengine::DisplayCreationArgsBuilder()
+                    .setId(DEFAULT_DISPLAY_ID)
+                    .setPixels({DEFAULT_DISPLAY_WIDTH, DEFAULT_DISPLAY_HEIGHT})
+                    .setPowerAdvisor(mPowerAdvisor)
+                    .setName("injected display")
+                    .build();
+    auto compositionDisplay =
+            compositionengine::impl::createDisplay(mFlinger.getCompositionEngine(),
+                                                   std::move(compostionEngineDisplayArgs));
+    mDisplay =
+            FakeDisplayDeviceInjector(mFlinger, compositionDisplay,
+                                      ui::DisplayConnectionType::Internal, HWC_DISPLAY, kIsPrimary)
+                    .setDisplaySurface(mDisplaySurface)
+                    .setNativeWindow(mNativeWindow)
+                    .setPowerMode(hal::PowerMode::ON)
+                    .setRefreshRateSelector(mFlinger.scheduler()->refreshRateSelector())
+                    .skipRegisterDisplay()
+                    .inject();
+}
+
+// TODO (b/277364366): Clients should be updated to pass in the display they want.
+TEST_F(SurfaceFlingerGetDisplayStatsTest, nullptrSucceeds) {
+    DisplayStatInfo info;
+    status_t status = mFlinger.getDisplayStats(nullptr, &info);
+    EXPECT_EQ(status, NO_ERROR);
+}
+
+TEST_F(SurfaceFlingerGetDisplayStatsTest, explicitToken) {
+    DisplayStatInfo info;
+    status_t status = mFlinger.getDisplayStats(mDisplay->getDisplayToken().promote(), &info);
+    EXPECT_EQ(status, NO_ERROR);
+}
+
+TEST_F(SurfaceFlingerGetDisplayStatsTest, invalidToken) {
+    const String8 displayName("fakeDisplay");
+    sp<IBinder> displayToken = mFlinger.createDisplay(displayName, false);
+    DisplayStatInfo info;
+    status_t status = mFlinger.getDisplayStats(displayToken, &info);
+    EXPECT_EQ(status, NAME_NOT_FOUND);
+}
+
+} // namespace
+} // namespace android
diff --git a/services/surfaceflinger/tests/unittests/TestableSurfaceFlinger.h b/services/surfaceflinger/tests/unittests/TestableSurfaceFlinger.h
index cfa366f..a189c00 100644
--- a/services/surfaceflinger/tests/unittests/TestableSurfaceFlinger.h
+++ b/services/surfaceflinger/tests/unittests/TestableSurfaceFlinger.h
@@ -56,6 +56,9 @@
 #include "mock/system/window/MockNativeWindow.h"
 
 namespace android {
+
+struct DisplayStatInfo;
+
 namespace renderengine {
 
 class RenderEngine;
@@ -545,6 +548,10 @@
         return sp<DisplayDevice>::make(creationArgs);
     }
 
+    status_t getDisplayStats(const sp<IBinder>& displayToken, DisplayStatInfo* outInfo) {
+        return mFlinger->getDisplayStats(displayToken, outInfo);
+    }
+
     /* ------------------------------------------------------------------------
      * Read-only access to private data to assert post-conditions.
      */
diff --git a/services/surfaceflinger/tests/unittests/mock/MockLayer.h b/services/surfaceflinger/tests/unittests/mock/MockLayer.h
index 0d94f4c..50e07fc 100644
--- a/services/surfaceflinger/tests/unittests/mock/MockLayer.h
+++ b/services/surfaceflinger/tests/unittests/mock/MockLayer.h
@@ -32,7 +32,7 @@
     MOCK_CONST_METHOD0(getType, const char*());
     MOCK_METHOD0(getFrameSelectionPriority, int32_t());
     MOCK_CONST_METHOD0(isVisible, bool());
-    MOCK_METHOD0(createClone, sp<Layer>());
+    MOCK_METHOD1(createClone, sp<Layer>(uint32_t));
     MOCK_CONST_METHOD0(getFrameRateForLayerTree, FrameRate());
     MOCK_CONST_METHOD0(getDefaultFrameRateCompatibility,
                        scheduler::LayerInfo::FrameRateCompatibility());