Merge changes from topic "gestures-lib-sysprop"

* changes:
  Use a sysprop to enable `TouchpadInputMapper`
  Add gestures library to whole_static_libs for input reader
diff --git a/Android.bp b/Android.bp
index 615a7a8..3992f82 100644
--- a/Android.bp
+++ b/Android.bp
@@ -56,7 +56,8 @@
 
 cc_library_headers {
     name: "libandroid_sensor_headers",
-    vendor: true,
+    vendor_available: true,
+    host_supported: true,
     export_include_dirs: ["include_sensor"],
 }
 
diff --git a/libs/binder/ndk/include_cpp/android/binder_to_string.h b/libs/binder/ndk/include_cpp/android/binder_to_string.h
index 6a25db2..2a00736 100644
--- a/libs/binder/ndk/include_cpp/android/binder_to_string.h
+++ b/libs/binder/ndk/include_cpp/android/binder_to_string.h
@@ -160,7 +160,7 @@
 template <typename _T>
 std::string ToString(const _T& t) {
     if constexpr (details::ToEmptyString<_T>::value) {
-        return "";
+        return "<unimplemented>";
     } else if constexpr (std::is_same_v<bool, _T>) {
         return t ? "true" : "false";
     } else if constexpr (std::is_same_v<char16_t, _T>) {
@@ -176,9 +176,11 @@
         return t;
 #ifdef HAS_NDK_INTERFACE
     } else if constexpr (std::is_same_v<::ndk::SpAIBinder, _T>) {
-        return (t.get() == nullptr) ? "(null)" : "";
+        std::stringstream ss;
+        ss << "binder:" << std::hex << t.get();
+        return ss.str();
     } else if constexpr (std::is_same_v<::ndk::ScopedFileDescriptor, _T>) {
-        return (t.get() == -1) ? "(null)" : "";
+        return "fd:" + std::to_string(t.get());
 #endif
 #ifdef HAS_STRING16
     } else if constexpr (std::is_same_v<String16, _T>) {
diff --git a/libs/binder/rust/tests/parcel_fuzzer/read_utils.rs b/libs/binder/rust/tests/parcel_fuzzer/read_utils.rs
index d2bfde1..a2d48b6 100644
--- a/libs/binder/rust/tests/parcel_fuzzer/read_utils.rs
+++ b/libs/binder/rust/tests/parcel_fuzzer/read_utils.rs
@@ -16,9 +16,9 @@
 
 use binder::binder_impl::BorrowedParcel;
 use binder::{ParcelFileDescriptor, Parcelable, SpIBinder};
-use binderReadParcelIface::aidl::EmptyParcelable::EmptyParcelable;
-use binderReadParcelIface::aidl::GenericDataParcelable::GenericDataParcelable;
-use binderReadParcelIface::aidl::SingleDataParcelable::SingleDataParcelable;
+use binderReadParcelIface::aidl::parcelables::EmptyParcelable::EmptyParcelable;
+use binderReadParcelIface::aidl::parcelables::GenericDataParcelable::GenericDataParcelable;
+use binderReadParcelIface::aidl::parcelables::SingleDataParcelable::SingleDataParcelable;
 
 macro_rules! read_parcel_interface {
     ($data_type:ty) => {
diff --git a/libs/binder/tests/parcel_fuzzer/Android.bp b/libs/binder/tests/parcel_fuzzer/Android.bp
index 61a2412..35866ad 100644
--- a/libs/binder/tests/parcel_fuzzer/Android.bp
+++ b/libs/binder/tests/parcel_fuzzer/Android.bp
@@ -12,13 +12,14 @@
     host_supported: true,
     unstable: true,
     srcs: [
-        "EmptyParcelable.aidl",
-        "SingleDataParcelable.aidl",
-        "GenericDataParcelable.aidl",
+        "parcelables/EmptyParcelable.aidl",
+        "parcelables/SingleDataParcelable.aidl",
+        "parcelables/GenericDataParcelable.aidl",
     ],
     backend: {
         java: {
-            enabled: false,
+            enabled: true,
+            platform_apis: true,
         },
         rust: {
             enabled: true,
diff --git a/libs/binder/tests/parcel_fuzzer/EmptyParcelable.aidl b/libs/binder/tests/parcel_fuzzer/EmptyParcelable.aidl
deleted file mode 100644
index 96d6223..0000000
--- a/libs/binder/tests/parcel_fuzzer/EmptyParcelable.aidl
+++ /dev/null
@@ -1,18 +0,0 @@
-/*
- * Copyright (C) 2022 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.
- */
-
-parcelable EmptyParcelable{
-}
\ No newline at end of file
diff --git a/libs/binder/tests/parcel_fuzzer/binder.cpp b/libs/binder/tests/parcel_fuzzer/binder.cpp
index 9dac2c9..768fbe1 100644
--- a/libs/binder/tests/parcel_fuzzer/binder.cpp
+++ b/libs/binder/tests/parcel_fuzzer/binder.cpp
@@ -16,9 +16,9 @@
 #define FUZZ_LOG_TAG "binder"
 
 #include "binder.h"
-#include "EmptyParcelable.h"
-#include "GenericDataParcelable.h"
-#include "SingleDataParcelable.h"
+#include "parcelables/EmptyParcelable.h"
+#include "parcelables/GenericDataParcelable.h"
+#include "parcelables/SingleDataParcelable.h"
 #include "util.h"
 
 #include <android-base/hex.h>
@@ -359,19 +359,19 @@
     },
     [] (const ::android::Parcel& p, FuzzedDataProvider& /*provider*/) {
         FUZZ_LOG() << "about to call readFromParcel() with status for EmptyParcelable";
-        EmptyParcelable emptyParcelable{};
+        parcelables::EmptyParcelable emptyParcelable{};
         status_t status = emptyParcelable.readFromParcel(&p);
         FUZZ_LOG() << " status: " << status;
     },
     [] (const ::android::Parcel& p , FuzzedDataProvider& /*provider*/) {
         FUZZ_LOG() << "about to call readFromParcel() with status for SingleDataParcelable";
-        SingleDataParcelable singleDataParcelable;
+        parcelables::SingleDataParcelable singleDataParcelable;
         status_t status = singleDataParcelable.readFromParcel(&p);
         FUZZ_LOG() <<" status: " << status;
     },
     [] (const ::android::Parcel& p, FuzzedDataProvider& /*provider*/) {
         FUZZ_LOG() << "about to call readFromParcel() with status for GenericDataParcelable";
-        GenericDataParcelable genericDataParcelable;
+        parcelables::GenericDataParcelable genericDataParcelable;
         status_t status = genericDataParcelable.readFromParcel(&p);
         FUZZ_LOG() <<" status: " << status;
     },
diff --git a/libs/binder/tests/parcel_fuzzer/binder_ndk.cpp b/libs/binder/tests/parcel_fuzzer/binder_ndk.cpp
index af773a0..53e7de4 100644
--- a/libs/binder/tests/parcel_fuzzer/binder_ndk.cpp
+++ b/libs/binder/tests/parcel_fuzzer/binder_ndk.cpp
@@ -16,9 +16,9 @@
 #define FUZZ_LOG_TAG "binder_ndk"
 
 #include "binder_ndk.h"
-#include "aidl/EmptyParcelable.h"
-#include "aidl/GenericDataParcelable.h"
-#include "aidl/SingleDataParcelable.h"
+#include "aidl/parcelables/EmptyParcelable.h"
+#include "aidl/parcelables/GenericDataParcelable.h"
+#include "aidl/parcelables/SingleDataParcelable.h"
 
 #include <android/binder_parcel_utils.h>
 #include <android/binder_parcelable_utils.h>
@@ -183,19 +183,19 @@
 
         [](const NdkParcelAdapter& p, FuzzedDataProvider& /*provider*/) {
             FUZZ_LOG() << "about to read parcel using readFromParcel for EmptyParcelable";
-            aidl::EmptyParcelable emptyParcelable;
+            aidl::parcelables::EmptyParcelable emptyParcelable;
             binder_status_t status = emptyParcelable.readFromParcel(p.aParcel());
             FUZZ_LOG() << "status: " << status;
         },
         [](const NdkParcelAdapter& p, FuzzedDataProvider& /*provider*/) {
             FUZZ_LOG() << "about to read parcel using readFromParcel for SingleDataParcelable";
-            aidl::SingleDataParcelable singleDataParcelable;
+            aidl::parcelables::SingleDataParcelable singleDataParcelable;
             binder_status_t status = singleDataParcelable.readFromParcel(p.aParcel());
             FUZZ_LOG() << "status: " << status;
         },
         [](const NdkParcelAdapter& p, FuzzedDataProvider& /*provider*/) {
             FUZZ_LOG() << "about to read parcel using readFromParcel for GenericDataParcelable";
-            aidl::GenericDataParcelable genericDataParcelable;
+            aidl::parcelables::GenericDataParcelable genericDataParcelable;
             binder_status_t status = genericDataParcelable.readFromParcel(p.aParcel());
             FUZZ_LOG() << "status: " << status;
         },
diff --git a/libs/binder/tests/parcel_fuzzer/SingleDataParcelable.aidl b/libs/binder/tests/parcel_fuzzer/parcelables/EmptyParcelable.aidl
similarity index 92%
copy from libs/binder/tests/parcel_fuzzer/SingleDataParcelable.aidl
copy to libs/binder/tests/parcel_fuzzer/parcelables/EmptyParcelable.aidl
index d62891b..1216250 100644
--- a/libs/binder/tests/parcel_fuzzer/SingleDataParcelable.aidl
+++ b/libs/binder/tests/parcel_fuzzer/parcelables/EmptyParcelable.aidl
@@ -13,7 +13,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
-parcelable SingleDataParcelable{
-   int data;
+package parcelables;
+parcelable EmptyParcelable {
 }
\ No newline at end of file
diff --git a/libs/binder/tests/parcel_fuzzer/GenericDataParcelable.aidl b/libs/binder/tests/parcel_fuzzer/parcelables/GenericDataParcelable.aidl
similarity index 97%
rename from libs/binder/tests/parcel_fuzzer/GenericDataParcelable.aidl
rename to libs/binder/tests/parcel_fuzzer/parcelables/GenericDataParcelable.aidl
index fc2542b..f1079e9 100644
--- a/libs/binder/tests/parcel_fuzzer/GenericDataParcelable.aidl
+++ b/libs/binder/tests/parcel_fuzzer/parcelables/GenericDataParcelable.aidl
@@ -13,6 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+package parcelables;
 
 parcelable GenericDataParcelable {
     int data;
diff --git a/libs/binder/tests/parcel_fuzzer/SingleDataParcelable.aidl b/libs/binder/tests/parcel_fuzzer/parcelables/SingleDataParcelable.aidl
similarity index 96%
rename from libs/binder/tests/parcel_fuzzer/SingleDataParcelable.aidl
rename to libs/binder/tests/parcel_fuzzer/parcelables/SingleDataParcelable.aidl
index d62891b..0187168 100644
--- a/libs/binder/tests/parcel_fuzzer/SingleDataParcelable.aidl
+++ b/libs/binder/tests/parcel_fuzzer/parcelables/SingleDataParcelable.aidl
@@ -13,6 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+package parcelables;
 
 parcelable SingleDataParcelable{
    int data;
diff --git a/libs/binder/trusty/include/log/log.h b/libs/binder/trusty/include/log/log.h
index d88d18a..de84617 100644
--- a/libs/binder/trusty/include/log/log.h
+++ b/libs/binder/trusty/include/log/log.h
@@ -121,6 +121,8 @@
         TLOGE("android_errorWriteLog: tag:%x subTag:%s\n", tag, subTag); \
     } while (0)
 
-extern "C" inline void __assert(const char* file, int line, const char* str) {
-    LOG_ALWAYS_FATAL("%s:%d: assertion \"%s\" failed", file, line, str);
-}
+// Override the definition of __assert from binder_status.h
+#ifndef __BIONIC__
+#undef __assert
+#define __assert(file, line, str) LOG_ALWAYS_FATAL("%s:%d: %s", file, line, str)
+#endif // __BIONIC__
diff --git a/libs/gui/SurfaceComposerClient.cpp b/libs/gui/SurfaceComposerClient.cpp
index aaa2102..325c294 100644
--- a/libs/gui/SurfaceComposerClient.cpp
+++ b/libs/gui/SurfaceComposerClient.cpp
@@ -2337,6 +2337,7 @@
         }
 
         outInfo->activeDisplayModeId = ginfo.activeDisplayModeId;
+        outInfo->renderFrameRate = ginfo.renderFrameRate;
 
         outInfo->supportedColorModes.clear();
         outInfo->supportedColorModes.reserve(ginfo.supportedColorModes.size());
diff --git a/libs/gui/aidl/android/gui/DynamicDisplayInfo.aidl b/libs/gui/aidl/android/gui/DynamicDisplayInfo.aidl
index 57e6081..3114929 100644
--- a/libs/gui/aidl/android/gui/DynamicDisplayInfo.aidl
+++ b/libs/gui/aidl/android/gui/DynamicDisplayInfo.aidl
@@ -27,6 +27,7 @@
     List<DisplayMode> supportedDisplayModes;
 
     int activeDisplayModeId;
+    float renderFrameRate;
 
     int[] supportedColorModes;
     int activeColorMode;
diff --git a/libs/ui/include/ui/DynamicDisplayInfo.h b/libs/ui/include/ui/DynamicDisplayInfo.h
index 8c9fe4c..0b77754 100644
--- a/libs/ui/include/ui/DynamicDisplayInfo.h
+++ b/libs/ui/include/ui/DynamicDisplayInfo.h
@@ -35,6 +35,7 @@
     // we can't use size_t because it may have different width
     // in the client process.
     ui::DisplayModeId activeDisplayModeId;
+    float renderFrameRate;
 
     std::vector<ui::ColorMode> supportedColorModes;
     ui::ColorMode activeColorMode;
diff --git a/services/inputflinger/dispatcher/InputDispatcher.cpp b/services/inputflinger/dispatcher/InputDispatcher.cpp
index 466c51e..4aac377 100644
--- a/services/inputflinger/dispatcher/InputDispatcher.cpp
+++ b/services/inputflinger/dispatcher/InputDispatcher.cpp
@@ -148,21 +148,23 @@
 }
 
 bool isValidMotionAction(int32_t action, int32_t actionButton, int32_t pointerCount) {
-    switch (action & AMOTION_EVENT_ACTION_MASK) {
+    switch (MotionEvent::getActionMasked(action)) {
         case AMOTION_EVENT_ACTION_DOWN:
         case AMOTION_EVENT_ACTION_UP:
-        case AMOTION_EVENT_ACTION_CANCEL:
+            return pointerCount == 1;
         case AMOTION_EVENT_ACTION_MOVE:
-        case AMOTION_EVENT_ACTION_OUTSIDE:
         case AMOTION_EVENT_ACTION_HOVER_ENTER:
         case AMOTION_EVENT_ACTION_HOVER_MOVE:
         case AMOTION_EVENT_ACTION_HOVER_EXIT:
+            return pointerCount >= 1;
+        case AMOTION_EVENT_ACTION_CANCEL:
+        case AMOTION_EVENT_ACTION_OUTSIDE:
         case AMOTION_EVENT_ACTION_SCROLL:
             return true;
         case AMOTION_EVENT_ACTION_POINTER_DOWN:
         case AMOTION_EVENT_ACTION_POINTER_UP: {
-            int32_t index = getMotionEventActionPointerIndex(action);
-            return index >= 0 && index < pointerCount;
+            const int32_t index = MotionEvent::getActionIndex(action);
+            return index >= 0 && index < pointerCount && pointerCount > 1;
         }
         case AMOTION_EVENT_ACTION_BUTTON_PRESS:
         case AMOTION_EVENT_ACTION_BUTTON_RELEASE:
@@ -2096,7 +2098,7 @@
 
     bool isSplit = shouldSplitTouch(tempTouchState, entry);
     const bool switchedDevice = (oldState != nullptr) &&
-            (tempTouchState.deviceId != entry.deviceId || tempTouchState.source != entry.source);
+            (oldState->deviceId != entry.deviceId || oldState->source != entry.source);
 
     const bool isHoverAction = (maskedAction == AMOTION_EVENT_ACTION_HOVER_MOVE ||
                                 maskedAction == AMOTION_EVENT_ACTION_HOVER_ENTER ||
@@ -2104,6 +2106,7 @@
     const bool newGesture = (maskedAction == AMOTION_EVENT_ACTION_DOWN ||
                              maskedAction == AMOTION_EVENT_ACTION_SCROLL || isHoverAction);
     const bool isFromMouse = isFromSource(entry.source, AINPUT_SOURCE_MOUSE);
+
     if (newGesture) {
         bool down = maskedAction == AMOTION_EVENT_ACTION_DOWN;
         if (switchedDevice && tempTouchState.isDown() && !down && !isHoverAction) {
@@ -2995,6 +2998,8 @@
                              connection->getInputChannelName().c_str(), eventEntry->id);
         ATRACE_NAME(message.c_str());
     }
+    LOG_ALWAYS_FATAL_IF(!inputTarget.flags.any(InputTarget::DISPATCH_MASK),
+                        "No dispatch flags are set for %s", eventEntry->getDescription().c_str());
 
     const bool wasEmpty = connection->outboundQueue.empty();
 
@@ -4053,10 +4058,9 @@
                   args->pointerCoords[i].getAxisValue(AMOTION_EVENT_AXIS_ORIENTATION));
         }
     }
-    if (!validateMotionEvent(args->action, args->actionButton, args->pointerCount,
-                             args->pointerProperties)) {
-        return;
-    }
+    LOG_ALWAYS_FATAL_IF(!validateMotionEvent(args->action, args->actionButton, args->pointerCount,
+                                             args->pointerProperties),
+                        "Invalid event: %s", args->dump().c_str());
 
     uint32_t policyFlags = args->policyFlags;
     policyFlags |= POLICY_FLAG_TRUSTED;
@@ -4981,10 +4985,6 @@
                 }
             }
         }
-
-        if (DEBUG_FOCUS) {
-            logDispatchStateLocked();
-        }
     } // release lock
 
     // Wake up poll loop since it may need to make new input dispatching choices.
@@ -5015,10 +5015,6 @@
         } else {
             changed = false;
         }
-
-        if (DEBUG_FOCUS) {
-            logDispatchStateLocked();
-        }
     } // release lock
 
     if (changed) {
@@ -5193,10 +5189,6 @@
             synthesizeCancelationEventsForConnectionLocked(fromConnection, options);
             synthesizePointerDownEventsForConnectionLocked(downTimeInTarget, toConnection);
         }
-
-        if (DEBUG_FOCUS) {
-            logDispatchStateLocked();
-        }
     } // release lock
 
     // Wake up poll loop since it may need to make new input dispatching choices.
diff --git a/services/inputflinger/dispatcher/TouchState.cpp b/services/inputflinger/dispatcher/TouchState.cpp
index 114e0bf..c21af9e 100644
--- a/services/inputflinger/dispatcher/TouchState.cpp
+++ b/services/inputflinger/dispatcher/TouchState.cpp
@@ -34,8 +34,7 @@
 void TouchState::addOrUpdateWindow(const sp<WindowInfoHandle>& windowHandle,
                                    ftl::Flags<InputTarget::Flags> targetFlags, BitSet32 pointerIds,
                                    std::optional<nsecs_t> eventTime) {
-    for (size_t i = 0; i < windows.size(); i++) {
-        TouchedWindow& touchedWindow = windows[i];
+    for (TouchedWindow& touchedWindow : windows) {
         if (touchedWindow.windowHandle == windowHandle) {
             touchedWindow.targetFlags |= targetFlags;
             if (targetFlags.test(InputTarget::Flags::DISPATCH_AS_SLIPPERY_EXIT)) {
diff --git a/services/inputflinger/tests/Android.bp b/services/inputflinger/tests/Android.bp
index 2a8b359..53d821f 100644
--- a/services/inputflinger/tests/Android.bp
+++ b/services/inputflinger/tests/Android.bp
@@ -44,6 +44,7 @@
         "FakeInputReaderPolicy.cpp",
         "FakePointerController.cpp",
         "FocusResolver_test.cpp",
+        "InputMapperTest.cpp",
         "InputProcessor_test.cpp",
         "InputProcessorConverter_test.cpp",
         "InputDispatcher_test.cpp",
diff --git a/services/inputflinger/tests/InputDispatcher_test.cpp b/services/inputflinger/tests/InputDispatcher_test.cpp
index f3239ca..41c174a 100644
--- a/services/inputflinger/tests/InputDispatcher_test.cpp
+++ b/services/inputflinger/tests/InputDispatcher_test.cpp
@@ -110,6 +110,13 @@
         *result_listener << "expected action " << MotionEvent::actionToString(action)
                          << ", but got " << MotionEvent::actionToString(arg.getAction());
     }
+    if (action == AMOTION_EVENT_ACTION_DOWN) {
+        if (!matches) {
+            *result_listener << "; ";
+        }
+        *result_listener << "downTime should match eventTime for ACTION_DOWN events";
+        matches &= arg.getDownTime() == arg.getEventTime();
+    }
     if (action == AMOTION_EVENT_ACTION_CANCEL) {
         if (!matches) {
             *result_listener << "; ";
@@ -120,6 +127,10 @@
     return matches;
 }
 
+MATCHER_P(WithDownTime, downTime, "InputEvent with specified downTime") {
+    return arg.getDownTime() == downTime;
+}
+
 MATCHER_P(WithSource, source, "InputEvent with specified source") {
     *result_listener << "expected source " << inputEventSourceToString(source) << ", but got "
                      << inputEventSourceToString(arg.getSource());
@@ -1210,6 +1221,7 @@
     void consumeMotionOutsideWithZeroedCoords(int32_t expectedDisplayId = ADISPLAY_ID_DEFAULT,
                                               int32_t expectedFlags = 0) {
         InputEvent* event = consume();
+        ASSERT_NE(nullptr, event);
         ASSERT_EQ(AINPUT_EVENT_TYPE_MOTION, event->getType());
         const MotionEvent& motionEvent = static_cast<MotionEvent&>(*event);
         EXPECT_EQ(AMOTION_EVENT_ACTION_OUTSIDE, motionEvent.getActionMasked());
@@ -1231,7 +1243,7 @@
 
     void consumeMotionEvent(const ::testing::Matcher<MotionEvent>& matcher) {
         MotionEvent* motionEvent = consumeMotion();
-        ASSERT_NE(nullptr, motionEvent) << "Did not get a motion event";
+        ASSERT_NE(nullptr, motionEvent) << "Did not get a motion event, but expected " << matcher;
         ASSERT_THAT(*motionEvent, matcher);
     }
 
@@ -2056,6 +2068,7 @@
 
     mDispatcher->waitForIdle();
     InputEvent* inputEvent1 = window1->consume();
+    ASSERT_NE(inputEvent1, nullptr);
     window2->assertNoEvents();
     MotionEvent& motionEvent1 = static_cast<MotionEvent&>(*inputEvent1);
     nsecs_t downTimeForWindow1 = motionEvent1.getDownTime();
@@ -2065,6 +2078,7 @@
     mDispatcher->notifyMotion(&(args = generateTouchArgs(POINTER_1_DOWN, {{50, 50}, {150, 50}})));
     mDispatcher->waitForIdle();
     InputEvent* inputEvent2 = window2->consume();
+    ASSERT_NE(inputEvent2, nullptr);
     MotionEvent& motionEvent2 = static_cast<MotionEvent&>(*inputEvent2);
     nsecs_t downTimeForWindow2 = motionEvent2.getDownTime();
     ASSERT_NE(downTimeForWindow1, downTimeForWindow2);
@@ -2074,17 +2088,13 @@
     mDispatcher->notifyMotion(
             &(args = generateTouchArgs(AMOTION_EVENT_ACTION_MOVE, {{50, 50}, {151, 51}})));
     mDispatcher->waitForIdle();
-    InputEvent* inputEvent3 = window2->consume();
-    MotionEvent& motionEvent3 = static_cast<MotionEvent&>(*inputEvent3);
-    ASSERT_EQ(motionEvent3.getDownTime(), downTimeForWindow2);
+    window2->consumeMotionEvent(WithDownTime(downTimeForWindow2));
 
     // Now add new touch down on the second window
     mDispatcher->notifyMotion(
             &(args = generateTouchArgs(POINTER_2_DOWN, {{50, 50}, {151, 51}, {150, 50}})));
     mDispatcher->waitForIdle();
-    InputEvent* inputEvent4 = window2->consume();
-    MotionEvent& motionEvent4 = static_cast<MotionEvent&>(*inputEvent4);
-    ASSERT_EQ(motionEvent4.getDownTime(), downTimeForWindow2);
+    window2->consumeMotionEvent(WithDownTime(downTimeForWindow2));
 
     // TODO(b/232530217): do not send the unnecessary MOVE event and delete the next line
     window1->consumeMotionMove();
@@ -2094,16 +2104,12 @@
     mDispatcher->notifyMotion(
             &(args = generateTouchArgs(AMOTION_EVENT_ACTION_MOVE, {{51, 51}, {151, 51}})));
     mDispatcher->waitForIdle();
-    InputEvent* inputEvent5 = window1->consume();
-    MotionEvent& motionEvent5 = static_cast<MotionEvent&>(*inputEvent5);
-    ASSERT_EQ(motionEvent5.getDownTime(), downTimeForWindow1);
+    window1->consumeMotionEvent(WithDownTime(downTimeForWindow1));
 
     mDispatcher->notifyMotion(&(
             args = generateTouchArgs(POINTER_3_DOWN, {{51, 51}, {151, 51}, {150, 50}, {50, 50}})));
     mDispatcher->waitForIdle();
-    InputEvent* inputEvent6 = window1->consume();
-    MotionEvent& motionEvent6 = static_cast<MotionEvent&>(*inputEvent6);
-    ASSERT_EQ(motionEvent6.getDownTime(), downTimeForWindow1);
+    window1->consumeMotionEvent(WithDownTime(downTimeForWindow1));
 }
 
 TEST_F(InputDispatcherTest, HoverMoveEnterMouseClickAndHoverMoveExit) {
@@ -2128,10 +2134,8 @@
                                                          .x(900)
                                                          .y(400))
                                         .build()));
-    windowRight->consumeEvent(AINPUT_EVENT_TYPE_MOTION, AMOTION_EVENT_ACTION_HOVER_ENTER,
-                              ADISPLAY_ID_DEFAULT, 0 /* expectedFlag */);
-    windowRight->consumeEvent(AINPUT_EVENT_TYPE_MOTION, AMOTION_EVENT_ACTION_HOVER_MOVE,
-                              ADISPLAY_ID_DEFAULT, 0 /* expectedFlag */);
+    windowRight->consumeMotionEvent(WithMotionAction(AMOTION_EVENT_ACTION_HOVER_ENTER));
+    windowRight->consumeMotionEvent(WithMotionAction(AMOTION_EVENT_ACTION_HOVER_MOVE));
 
     // Move cursor into left window
     ASSERT_EQ(InputEventInjectionResult::SUCCEEDED,
@@ -2142,12 +2146,9 @@
                                                          .x(300)
                                                          .y(400))
                                         .build()));
-    windowRight->consumeEvent(AINPUT_EVENT_TYPE_MOTION, AMOTION_EVENT_ACTION_HOVER_EXIT,
-                              ADISPLAY_ID_DEFAULT, 0 /* expectedFlag */);
-    windowLeft->consumeEvent(AINPUT_EVENT_TYPE_MOTION, AMOTION_EVENT_ACTION_HOVER_ENTER,
-                             ADISPLAY_ID_DEFAULT, 0 /* expectedFlag */);
-    windowLeft->consumeEvent(AINPUT_EVENT_TYPE_MOTION, AMOTION_EVENT_ACTION_HOVER_MOVE,
-                             ADISPLAY_ID_DEFAULT, 0 /* expectedFlag */);
+    windowRight->consumeMotionEvent(WithMotionAction(AMOTION_EVENT_ACTION_HOVER_EXIT));
+    windowLeft->consumeMotionEvent(WithMotionAction(AMOTION_EVENT_ACTION_HOVER_ENTER));
+    windowLeft->consumeMotionEvent(WithMotionAction(AMOTION_EVENT_ACTION_HOVER_MOVE));
 
     // Inject a series of mouse events for a mouse click
     ASSERT_EQ(InputEventInjectionResult::SUCCEEDED,
@@ -2170,8 +2171,7 @@
                                                          .x(300)
                                                          .y(400))
                                         .build()));
-    windowLeft->consumeEvent(AINPUT_EVENT_TYPE_MOTION, AMOTION_EVENT_ACTION_BUTTON_PRESS,
-                             ADISPLAY_ID_DEFAULT, 0 /* expectedFlag */);
+    windowLeft->consumeMotionEvent(WithMotionAction(AMOTION_EVENT_ACTION_BUTTON_PRESS));
 
     ASSERT_EQ(InputEventInjectionResult::SUCCEEDED,
               injectMotionEvent(mDispatcher,
@@ -2183,8 +2183,7 @@
                                                          .x(300)
                                                          .y(400))
                                         .build()));
-    windowLeft->consumeEvent(AINPUT_EVENT_TYPE_MOTION, AMOTION_EVENT_ACTION_BUTTON_RELEASE,
-                             ADISPLAY_ID_DEFAULT, 0 /* expectedFlag */);
+    windowLeft->consumeMotionEvent(WithMotionAction(AMOTION_EVENT_ACTION_BUTTON_RELEASE));
 
     ASSERT_EQ(InputEventInjectionResult::SUCCEEDED,
               injectMotionEvent(mDispatcher,
@@ -2205,12 +2204,47 @@
                                                          .x(900)
                                                          .y(400))
                                         .build()));
-    windowLeft->consumeEvent(AINPUT_EVENT_TYPE_MOTION, AMOTION_EVENT_ACTION_HOVER_EXIT,
-                             ADISPLAY_ID_DEFAULT, 0 /* expectedFlag */);
-    windowRight->consumeEvent(AINPUT_EVENT_TYPE_MOTION, AMOTION_EVENT_ACTION_HOVER_ENTER,
-                              ADISPLAY_ID_DEFAULT, 0 /* expectedFlag */);
-    windowRight->consumeEvent(AINPUT_EVENT_TYPE_MOTION, AMOTION_EVENT_ACTION_HOVER_MOVE,
-                              ADISPLAY_ID_DEFAULT, 0 /* expectedFlag */);
+    windowLeft->consumeMotionEvent(WithMotionAction(AMOTION_EVENT_ACTION_HOVER_EXIT));
+    windowRight->consumeMotionEvent(WithMotionAction(AMOTION_EVENT_ACTION_HOVER_ENTER));
+    windowRight->consumeMotionEvent(WithMotionAction(AMOTION_EVENT_ACTION_HOVER_MOVE));
+
+    // No more events
+    windowLeft->assertNoEvents();
+    windowRight->assertNoEvents();
+}
+
+TEST_F(InputDispatcherTest, HoverWithSpyWindows) {
+    std::shared_ptr<FakeApplicationHandle> application = std::make_shared<FakeApplicationHandle>();
+
+    sp<FakeWindowHandle> spyWindow =
+            sp<FakeWindowHandle>::make(application, mDispatcher, "Spy", ADISPLAY_ID_DEFAULT);
+    spyWindow->setFrame(Rect(0, 0, 600, 800));
+    spyWindow->setTrustedOverlay(true);
+    spyWindow->setSpy(true);
+    sp<FakeWindowHandle> window =
+            sp<FakeWindowHandle>::make(application, mDispatcher, "Window", ADISPLAY_ID_DEFAULT);
+    window->setFrame(Rect(0, 0, 600, 800));
+
+    mDispatcher->setFocusedApplication(ADISPLAY_ID_DEFAULT, application);
+    mDispatcher->setInputWindows({{ADISPLAY_ID_DEFAULT, {spyWindow, window}}});
+
+    // Send mouse cursor to the window
+    ASSERT_EQ(InputEventInjectionResult::SUCCEEDED,
+              injectMotionEvent(mDispatcher,
+                                MotionEventBuilder(AMOTION_EVENT_ACTION_HOVER_ENTER,
+                                                   AINPUT_SOURCE_MOUSE)
+                                        .pointer(PointerBuilder(0, AMOTION_EVENT_TOOL_TYPE_MOUSE)
+                                                         .x(100)
+                                                         .y(100))
+                                        .build()));
+
+    window->consumeMotionEvent(AllOf(WithMotionAction(AMOTION_EVENT_ACTION_HOVER_ENTER),
+                                     WithSource(AINPUT_SOURCE_MOUSE)));
+    spyWindow->consumeMotionEvent(AllOf(WithMotionAction(AMOTION_EVENT_ACTION_HOVER_ENTER),
+                                        WithSource(AINPUT_SOURCE_MOUSE)));
+
+    window->assertNoEvents();
+    spyWindow->assertNoEvents();
 }
 
 // This test is different from the test above that HOVER_ENTER and HOVER_EXIT events are injected
@@ -2233,8 +2267,7 @@
                                                          .x(300)
                                                          .y(400))
                                         .build()));
-    window->consumeEvent(AINPUT_EVENT_TYPE_MOTION, AMOTION_EVENT_ACTION_HOVER_ENTER,
-                         ADISPLAY_ID_DEFAULT, 0 /* expectedFlag */);
+    window->consumeMotionEvent(WithMotionAction(AMOTION_EVENT_ACTION_HOVER_ENTER));
 
     // Inject a series of mouse events for a mouse click
     ASSERT_EQ(InputEventInjectionResult::SUCCEEDED,
@@ -2257,8 +2290,7 @@
                                                          .x(300)
                                                          .y(400))
                                         .build()));
-    window->consumeEvent(AINPUT_EVENT_TYPE_MOTION, AMOTION_EVENT_ACTION_BUTTON_PRESS,
-                         ADISPLAY_ID_DEFAULT, 0 /* expectedFlag */);
+    window->consumeMotionEvent(WithMotionAction(AMOTION_EVENT_ACTION_BUTTON_PRESS));
 
     ASSERT_EQ(InputEventInjectionResult::SUCCEEDED,
               injectMotionEvent(mDispatcher,
@@ -2270,8 +2302,7 @@
                                                          .x(300)
                                                          .y(400))
                                         .build()));
-    window->consumeEvent(AINPUT_EVENT_TYPE_MOTION, AMOTION_EVENT_ACTION_BUTTON_RELEASE,
-                         ADISPLAY_ID_DEFAULT, 0 /* expectedFlag */);
+    window->consumeMotionEvent(WithMotionAction(AMOTION_EVENT_ACTION_BUTTON_RELEASE));
 
     ASSERT_EQ(InputEventInjectionResult::SUCCEEDED,
               injectMotionEvent(mDispatcher,
@@ -2291,8 +2322,7 @@
                                                          .x(300)
                                                          .y(400))
                                         .build()));
-    window->consumeEvent(AINPUT_EVENT_TYPE_MOTION, AMOTION_EVENT_ACTION_HOVER_EXIT,
-                         ADISPLAY_ID_DEFAULT, 0 /* expectedFlag */);
+    window->consumeMotionEvent(WithMotionAction(AMOTION_EVENT_ACTION_HOVER_EXIT));
 }
 
 /**
@@ -2362,14 +2392,13 @@
                                                          .x(300)
                                                          .y(600))
                                         .build()));
-    windowDefaultDisplay->consumeEvent(AINPUT_EVENT_TYPE_MOTION, AMOTION_EVENT_ACTION_HOVER_ENTER,
-                                       ADISPLAY_ID_DEFAULT, 0 /* expectedFlag */);
-    windowDefaultDisplay->consumeEvent(AINPUT_EVENT_TYPE_MOTION, AMOTION_EVENT_ACTION_HOVER_MOVE,
-                                       ADISPLAY_ID_DEFAULT, 0 /* expectedFlag */);
+    windowDefaultDisplay->consumeMotionEvent(WithMotionAction(AMOTION_EVENT_ACTION_HOVER_ENTER));
+    windowDefaultDisplay->consumeMotionEvent(WithMotionAction(AMOTION_EVENT_ACTION_HOVER_MOVE));
 
     // Remove all windows in secondary display and check that no event happens on window in
     // primary display.
-    mDispatcher->setInputWindows({{SECOND_DISPLAY_ID, {}}});
+    mDispatcher->setInputWindows(
+            {{ADISPLAY_ID_DEFAULT, {windowDefaultDisplay}}, {SECOND_DISPLAY_ID, {}}});
     windowDefaultDisplay->assertNoEvents();
 
     // Move cursor position in window in default display and check that only hover move
@@ -2385,8 +2414,9 @@
                                                          .x(400)
                                                          .y(700))
                                         .build()));
-    windowDefaultDisplay->consumeEvent(AINPUT_EVENT_TYPE_MOTION, AMOTION_EVENT_ACTION_HOVER_MOVE,
-                                       ADISPLAY_ID_DEFAULT, 0 /* expectedFlag */);
+    windowDefaultDisplay->consumeMotionEvent(
+            AllOf(WithMotionAction(AMOTION_EVENT_ACTION_HOVER_MOVE),
+                  WithSource(AINPUT_SOURCE_MOUSE)));
     windowDefaultDisplay->assertNoEvents();
 }
 
@@ -4669,6 +4699,7 @@
 
         const MotionEvent& motionEvent = static_cast<const MotionEvent&>(*event);
         assertMotionAction(expectedAction, motionEvent.getAction());
+        ASSERT_EQ(points.size(), motionEvent.getPointerCount());
 
         for (size_t i = 0; i < points.size(); i++) {
             float expectedX = points[i].x;
diff --git a/services/inputflinger/tests/InputMapperTest.cpp b/services/inputflinger/tests/InputMapperTest.cpp
new file mode 100644
index 0000000..3cd7c1b
--- /dev/null
+++ b/services/inputflinger/tests/InputMapperTest.cpp
@@ -0,0 +1,176 @@
+/*
+ * Copyright 2022 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 "InputMapperTest.h"
+
+#include <InputReaderBase.h>
+#include <gtest/gtest.h>
+#include <ui/Rotation.h>
+
+namespace android {
+
+const char* InputMapperTest::DEVICE_NAME = "device";
+const char* InputMapperTest::DEVICE_LOCATION = "USB1";
+const ftl::Flags<InputDeviceClass> InputMapperTest::DEVICE_CLASSES =
+        ftl::Flags<InputDeviceClass>(0); // not needed for current tests
+
+void InputMapperTest::SetUp(ftl::Flags<InputDeviceClass> classes, int bus) {
+    mFakeEventHub = std::make_unique<FakeEventHub>();
+    mFakePolicy = sp<FakeInputReaderPolicy>::make();
+    mFakeListener = std::make_unique<TestInputListener>();
+    mReader = std::make_unique<InstrumentedInputReader>(mFakeEventHub, mFakePolicy, *mFakeListener);
+    mDevice = newDevice(DEVICE_ID, DEVICE_NAME, DEVICE_LOCATION, EVENTHUB_ID, classes, bus);
+    // Consume the device reset notification generated when adding a new device.
+    mFakeListener->assertNotifyDeviceResetWasCalled();
+}
+
+void InputMapperTest::SetUp() {
+    SetUp(DEVICE_CLASSES);
+}
+
+void InputMapperTest::TearDown() {
+    mFakeListener.reset();
+    mFakePolicy.clear();
+}
+
+void InputMapperTest::addConfigurationProperty(const char* key, const char* value) {
+    mFakeEventHub->addConfigurationProperty(EVENTHUB_ID, key, value);
+}
+
+std::list<NotifyArgs> InputMapperTest::configureDevice(uint32_t changes) {
+    if (!changes ||
+        (changes &
+         (InputReaderConfiguration::CHANGE_DISPLAY_INFO |
+          InputReaderConfiguration::CHANGE_POINTER_CAPTURE))) {
+        mReader->requestRefreshConfiguration(changes);
+        mReader->loopOnce();
+    }
+    std::list<NotifyArgs> out =
+            mDevice->configure(ARBITRARY_TIME, mFakePolicy->getReaderConfiguration(), changes);
+    // Loop the reader to flush the input listener queue.
+    for (const NotifyArgs& args : out) {
+        mFakeListener->notify(args);
+    }
+    mReader->loopOnce();
+    return out;
+}
+
+std::shared_ptr<InputDevice> InputMapperTest::newDevice(int32_t deviceId, const std::string& name,
+                                                        const std::string& location,
+                                                        int32_t eventHubId,
+                                                        ftl::Flags<InputDeviceClass> classes,
+                                                        int bus) {
+    InputDeviceIdentifier identifier;
+    identifier.name = name;
+    identifier.location = location;
+    identifier.bus = bus;
+    std::shared_ptr<InputDevice> device =
+            std::make_shared<InputDevice>(mReader->getContext(), deviceId, DEVICE_GENERATION,
+                                          identifier);
+    mReader->pushNextDevice(device);
+    mFakeEventHub->addDevice(eventHubId, name, classes, bus);
+    mReader->loopOnce();
+    return device;
+}
+
+void InputMapperTest::setDisplayInfoAndReconfigure(int32_t displayId, int32_t width, int32_t height,
+                                                   ui::Rotation orientation,
+                                                   const std::string& uniqueId,
+                                                   std::optional<uint8_t> physicalPort,
+                                                   ViewportType viewportType) {
+    mFakePolicy->addDisplayViewport(displayId, width, height, orientation, /* isActive= */ true,
+                                    uniqueId, physicalPort, viewportType);
+    configureDevice(InputReaderConfiguration::CHANGE_DISPLAY_INFO);
+}
+
+void InputMapperTest::clearViewports() {
+    mFakePolicy->clearViewports();
+}
+
+std::list<NotifyArgs> InputMapperTest::process(InputMapper& mapper, nsecs_t when, nsecs_t readTime,
+                                               int32_t type, int32_t code, int32_t value) {
+    RawEvent event;
+    event.when = when;
+    event.readTime = readTime;
+    event.deviceId = mapper.getDeviceContext().getEventHubId();
+    event.type = type;
+    event.code = code;
+    event.value = value;
+    std::list<NotifyArgs> processArgList = mapper.process(&event);
+    for (const NotifyArgs& args : processArgList) {
+        mFakeListener->notify(args);
+    }
+    // Loop the reader to flush the input listener queue.
+    mReader->loopOnce();
+    return processArgList;
+}
+
+void InputMapperTest::resetMapper(InputMapper& mapper, nsecs_t when) {
+    const auto resetArgs = mapper.reset(when);
+    for (const auto args : resetArgs) {
+        mFakeListener->notify(args);
+    }
+    // Loop the reader to flush the input listener queue.
+    mReader->loopOnce();
+}
+
+std::list<NotifyArgs> InputMapperTest::handleTimeout(InputMapper& mapper, nsecs_t when) {
+    std::list<NotifyArgs> generatedArgs = mapper.timeoutExpired(when);
+    for (const NotifyArgs& args : generatedArgs) {
+        mFakeListener->notify(args);
+    }
+    // Loop the reader to flush the input listener queue.
+    mReader->loopOnce();
+    return generatedArgs;
+}
+
+void InputMapperTest::assertMotionRange(const InputDeviceInfo& info, int32_t axis, uint32_t source,
+                                        float min, float max, float flat, float fuzz) {
+    const InputDeviceInfo::MotionRange* range = info.getMotionRange(axis, source);
+    ASSERT_TRUE(range != nullptr) << "Axis: " << axis << " Source: " << source;
+    ASSERT_EQ(axis, range->axis) << "Axis: " << axis << " Source: " << source;
+    ASSERT_EQ(source, range->source) << "Axis: " << axis << " Source: " << source;
+    ASSERT_NEAR(min, range->min, EPSILON) << "Axis: " << axis << " Source: " << source;
+    ASSERT_NEAR(max, range->max, EPSILON) << "Axis: " << axis << " Source: " << source;
+    ASSERT_NEAR(flat, range->flat, EPSILON) << "Axis: " << axis << " Source: " << source;
+    ASSERT_NEAR(fuzz, range->fuzz, EPSILON) << "Axis: " << axis << " Source: " << source;
+}
+
+void InputMapperTest::assertPointerCoords(const PointerCoords& coords, float x, float y,
+                                          float pressure, float size, float touchMajor,
+                                          float touchMinor, float toolMajor, float toolMinor,
+                                          float orientation, float distance,
+                                          float scaledAxisEpsilon) {
+    ASSERT_NEAR(x, coords.getAxisValue(AMOTION_EVENT_AXIS_X), scaledAxisEpsilon);
+    ASSERT_NEAR(y, coords.getAxisValue(AMOTION_EVENT_AXIS_Y), scaledAxisEpsilon);
+    ASSERT_NEAR(pressure, coords.getAxisValue(AMOTION_EVENT_AXIS_PRESSURE), EPSILON);
+    ASSERT_NEAR(size, coords.getAxisValue(AMOTION_EVENT_AXIS_SIZE), EPSILON);
+    ASSERT_NEAR(touchMajor, coords.getAxisValue(AMOTION_EVENT_AXIS_TOUCH_MAJOR), scaledAxisEpsilon);
+    ASSERT_NEAR(touchMinor, coords.getAxisValue(AMOTION_EVENT_AXIS_TOUCH_MINOR), scaledAxisEpsilon);
+    ASSERT_NEAR(toolMajor, coords.getAxisValue(AMOTION_EVENT_AXIS_TOOL_MAJOR), scaledAxisEpsilon);
+    ASSERT_NEAR(toolMinor, coords.getAxisValue(AMOTION_EVENT_AXIS_TOOL_MINOR), scaledAxisEpsilon);
+    ASSERT_NEAR(orientation, coords.getAxisValue(AMOTION_EVENT_AXIS_ORIENTATION), EPSILON);
+    ASSERT_NEAR(distance, coords.getAxisValue(AMOTION_EVENT_AXIS_DISTANCE), EPSILON);
+}
+
+void InputMapperTest::assertPosition(const FakePointerController& controller, float x, float y) {
+    float actualX, actualY;
+    controller.getPosition(&actualX, &actualY);
+    ASSERT_NEAR(x, actualX, 1);
+    ASSERT_NEAR(y, actualY, 1);
+}
+
+} // namespace android
diff --git a/services/inputflinger/tests/InputMapperTest.h b/services/inputflinger/tests/InputMapperTest.h
new file mode 100644
index 0000000..b3401c3
--- /dev/null
+++ b/services/inputflinger/tests/InputMapperTest.h
@@ -0,0 +1,95 @@
+/*
+ * Copyright 2022 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 <list>
+#include <memory>
+
+#include <InputDevice.h>
+#include <InputMapper.h>
+#include <NotifyArgs.h>
+#include <ftl/flags.h>
+#include <utils/StrongPointer.h>
+
+#include "FakeEventHub.h"
+#include "FakeInputReaderPolicy.h"
+#include "InstrumentedInputReader.h"
+#include "TestConstants.h"
+#include "TestInputListener.h"
+
+namespace android {
+
+class InputMapperTest : public testing::Test {
+protected:
+    static const char* DEVICE_NAME;
+    static const char* DEVICE_LOCATION;
+    static constexpr int32_t DEVICE_ID = END_RESERVED_ID + 1000;
+    static constexpr int32_t DEVICE_GENERATION = 2;
+    static constexpr int32_t DEVICE_CONTROLLER_NUMBER = 0;
+    static const ftl::Flags<InputDeviceClass> DEVICE_CLASSES;
+    static constexpr int32_t EVENTHUB_ID = 1;
+
+    std::shared_ptr<FakeEventHub> mFakeEventHub;
+    sp<FakeInputReaderPolicy> mFakePolicy;
+    std::unique_ptr<TestInputListener> mFakeListener;
+    std::unique_ptr<InstrumentedInputReader> mReader;
+    std::shared_ptr<InputDevice> mDevice;
+
+    virtual void SetUp(ftl::Flags<InputDeviceClass> classes, int bus = 0);
+    void SetUp() override;
+    void TearDown() override;
+
+    void addConfigurationProperty(const char* key, const char* value);
+    std::list<NotifyArgs> configureDevice(uint32_t changes);
+    std::shared_ptr<InputDevice> newDevice(int32_t deviceId, const std::string& name,
+                                           const std::string& location, int32_t eventHubId,
+                                           ftl::Flags<InputDeviceClass> classes, int bus = 0);
+    template <class T, typename... Args>
+    T& addMapperAndConfigure(Args... args) {
+        T& mapper = mDevice->addMapper<T>(EVENTHUB_ID, args...);
+        configureDevice(0);
+        std::list<NotifyArgs> resetArgList = mDevice->reset(ARBITRARY_TIME);
+        resetArgList += mapper.reset(ARBITRARY_TIME);
+        // Loop the reader to flush the input listener queue.
+        for (const NotifyArgs& loopArgs : resetArgList) {
+            mFakeListener->notify(loopArgs);
+        }
+        mReader->loopOnce();
+        return mapper;
+    }
+
+    void setDisplayInfoAndReconfigure(int32_t displayId, int32_t width, int32_t height,
+                                      ui::Rotation orientation, const std::string& uniqueId,
+                                      std::optional<uint8_t> physicalPort,
+                                      ViewportType viewportType);
+    void clearViewports();
+    std::list<NotifyArgs> process(InputMapper& mapper, nsecs_t when, nsecs_t readTime, int32_t type,
+                                  int32_t code, int32_t value);
+    void resetMapper(InputMapper& mapper, nsecs_t when);
+
+    std::list<NotifyArgs> handleTimeout(InputMapper& mapper, nsecs_t when);
+
+    static void assertMotionRange(const InputDeviceInfo& info, int32_t axis, uint32_t source,
+                                  float min, float max, float flat, float fuzz);
+    static void assertPointerCoords(const PointerCoords& coords, float x, float y, float pressure,
+                                    float size, float touchMajor, float touchMinor, float toolMajor,
+                                    float toolMinor, float orientation, float distance,
+                                    float scaledAxisEpsilon = 1.f);
+    static void assertPosition(const FakePointerController& controller, float x, float y);
+};
+
+} // namespace android
diff --git a/services/inputflinger/tests/InputReader_test.cpp b/services/inputflinger/tests/InputReader_test.cpp
index 58f1e1e..eef5690 100644
--- a/services/inputflinger/tests/InputReader_test.cpp
+++ b/services/inputflinger/tests/InputReader_test.cpp
@@ -45,6 +45,7 @@
 #include "FakeEventHub.h"
 #include "FakeInputReaderPolicy.h"
 #include "FakePointerController.h"
+#include "InputMapperTest.h"
 #include "InstrumentedInputReader.h"
 #include "TestConstants.h"
 #include "android/hardware/input/InputDeviceCountryCode.h"
@@ -92,9 +93,6 @@
 static constexpr int32_t ACTION_POINTER_1_UP =
         AMOTION_EVENT_ACTION_POINTER_UP | (1 << AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT);
 
-// Error tolerance for floating point assertions.
-static const float EPSILON = 0.001f;
-
 // Minimum timestamp separation between subsequent input events from a Bluetooth device.
 static constexpr nsecs_t MIN_BLUETOOTH_TIMESTAMP_DELTA = ms2ns(4);
 // Maximum smoothing time delta so that we don't generate events too far into the future.
@@ -2507,195 +2505,6 @@
     ASSERT_EQ(DEVICE_BLUETOOTH_ADDRESS, *address);
 }
 
-// --- InputMapperTest ---
-
-class InputMapperTest : public testing::Test {
-protected:
-    static const char* DEVICE_NAME;
-    static const char* DEVICE_LOCATION;
-    static const int32_t DEVICE_ID;
-    static const int32_t DEVICE_GENERATION;
-    static const int32_t DEVICE_CONTROLLER_NUMBER;
-    static const ftl::Flags<InputDeviceClass> DEVICE_CLASSES;
-    static const int32_t EVENTHUB_ID;
-
-    std::shared_ptr<FakeEventHub> mFakeEventHub;
-    sp<FakeInputReaderPolicy> mFakePolicy;
-    std::unique_ptr<TestInputListener> mFakeListener;
-    std::unique_ptr<InstrumentedInputReader> mReader;
-    std::shared_ptr<InputDevice> mDevice;
-
-    virtual void SetUp(ftl::Flags<InputDeviceClass> classes, int bus = 0) {
-        mFakeEventHub = std::make_unique<FakeEventHub>();
-        mFakePolicy = sp<FakeInputReaderPolicy>::make();
-        mFakeListener = std::make_unique<TestInputListener>();
-        mReader = std::make_unique<InstrumentedInputReader>(mFakeEventHub, mFakePolicy,
-                                                            *mFakeListener);
-        mDevice = newDevice(DEVICE_ID, DEVICE_NAME, DEVICE_LOCATION, EVENTHUB_ID, classes, bus);
-        // Consume the device reset notification generated when adding a new device.
-        mFakeListener->assertNotifyDeviceResetWasCalled();
-    }
-
-    void SetUp() override {
-        SetUp(DEVICE_CLASSES);
-    }
-
-    void TearDown() override {
-        mFakeListener.reset();
-        mFakePolicy.clear();
-    }
-
-    void addConfigurationProperty(const char* key, const char* value) {
-        mFakeEventHub->addConfigurationProperty(EVENTHUB_ID, key, value);
-    }
-
-    std::list<NotifyArgs> configureDevice(uint32_t changes) {
-        if (!changes ||
-            (changes &
-             (InputReaderConfiguration::CHANGE_DISPLAY_INFO |
-              InputReaderConfiguration::CHANGE_POINTER_CAPTURE))) {
-            mReader->requestRefreshConfiguration(changes);
-            mReader->loopOnce();
-        }
-        std::list<NotifyArgs> out =
-                mDevice->configure(ARBITRARY_TIME, mFakePolicy->getReaderConfiguration(), changes);
-        // Loop the reader to flush the input listener queue.
-        for (const NotifyArgs& args : out) {
-            mFakeListener->notify(args);
-        }
-        mReader->loopOnce();
-        return out;
-    }
-
-    std::shared_ptr<InputDevice> newDevice(int32_t deviceId, const std::string& name,
-                                           const std::string& location, int32_t eventHubId,
-                                           ftl::Flags<InputDeviceClass> classes, int bus = 0) {
-        InputDeviceIdentifier identifier;
-        identifier.name = name;
-        identifier.location = location;
-        identifier.bus = bus;
-        std::shared_ptr<InputDevice> device =
-                std::make_shared<InputDevice>(mReader->getContext(), deviceId, DEVICE_GENERATION,
-                                              identifier);
-        mReader->pushNextDevice(device);
-        mFakeEventHub->addDevice(eventHubId, name, classes, bus);
-        mReader->loopOnce();
-        return device;
-    }
-
-    template <class T, typename... Args>
-    T& addMapperAndConfigure(Args... args) {
-        T& mapper = mDevice->addMapper<T>(EVENTHUB_ID, args...);
-        configureDevice(0);
-        std::list<NotifyArgs> resetArgList = mDevice->reset(ARBITRARY_TIME);
-        resetArgList += mapper.reset(ARBITRARY_TIME);
-        // Loop the reader to flush the input listener queue.
-        for (const NotifyArgs& loopArgs : resetArgList) {
-            mFakeListener->notify(loopArgs);
-        }
-        mReader->loopOnce();
-        return mapper;
-    }
-
-    void setDisplayInfoAndReconfigure(int32_t displayId, int32_t width, int32_t height,
-                                      ui::Rotation orientation, const std::string& uniqueId,
-                                      std::optional<uint8_t> physicalPort,
-                                      ViewportType viewportType) {
-        mFakePolicy->addDisplayViewport(displayId, width, height, orientation, true /*isActive*/,
-                                        uniqueId, physicalPort, viewportType);
-        configureDevice(InputReaderConfiguration::CHANGE_DISPLAY_INFO);
-    }
-
-    void clearViewports() {
-        mFakePolicy->clearViewports();
-    }
-
-    std::list<NotifyArgs> process(InputMapper& mapper, nsecs_t when, nsecs_t readTime, int32_t type,
-                                  int32_t code, int32_t value) {
-        RawEvent event;
-        event.when = when;
-        event.readTime = readTime;
-        event.deviceId = mapper.getDeviceContext().getEventHubId();
-        event.type = type;
-        event.code = code;
-        event.value = value;
-        std::list<NotifyArgs> processArgList = mapper.process(&event);
-        for (const NotifyArgs& args : processArgList) {
-            mFakeListener->notify(args);
-        }
-        // Loop the reader to flush the input listener queue.
-        mReader->loopOnce();
-        return processArgList;
-    }
-
-    void resetMapper(InputMapper& mapper, nsecs_t when) {
-        const auto resetArgs = mapper.reset(when);
-        for (const auto& args : resetArgs) {
-            mFakeListener->notify(args);
-        }
-        // Loop the reader to flush the input listener queue.
-        mReader->loopOnce();
-    }
-
-    std::list<NotifyArgs> handleTimeout(InputMapper& mapper, nsecs_t when) {
-        std::list<NotifyArgs> generatedArgs = mapper.timeoutExpired(when);
-        for (const NotifyArgs& args : generatedArgs) {
-            mFakeListener->notify(args);
-        }
-        // Loop the reader to flush the input listener queue.
-        mReader->loopOnce();
-        return generatedArgs;
-    }
-
-    static void assertMotionRange(const InputDeviceInfo& info,
-            int32_t axis, uint32_t source, float min, float max, float flat, float fuzz) {
-        const InputDeviceInfo::MotionRange* range = info.getMotionRange(axis, source);
-        ASSERT_TRUE(range != nullptr) << "Axis: " << axis << " Source: " << source;
-        ASSERT_EQ(axis, range->axis) << "Axis: " << axis << " Source: " << source;
-        ASSERT_EQ(source, range->source) << "Axis: " << axis << " Source: " << source;
-        ASSERT_NEAR(min, range->min, EPSILON) << "Axis: " << axis << " Source: " << source;
-        ASSERT_NEAR(max, range->max, EPSILON) << "Axis: " << axis << " Source: " << source;
-        ASSERT_NEAR(flat, range->flat, EPSILON) << "Axis: " << axis << " Source: " << source;
-        ASSERT_NEAR(fuzz, range->fuzz, EPSILON) << "Axis: " << axis << " Source: " << source;
-    }
-
-    static void assertPointerCoords(const PointerCoords& coords, float x, float y, float pressure,
-                                    float size, float touchMajor, float touchMinor, float toolMajor,
-                                    float toolMinor, float orientation, float distance,
-                                    float scaledAxisEpsilon = 1.f) {
-        ASSERT_NEAR(x, coords.getAxisValue(AMOTION_EVENT_AXIS_X), scaledAxisEpsilon);
-        ASSERT_NEAR(y, coords.getAxisValue(AMOTION_EVENT_AXIS_Y), scaledAxisEpsilon);
-        ASSERT_NEAR(pressure, coords.getAxisValue(AMOTION_EVENT_AXIS_PRESSURE), EPSILON);
-        ASSERT_NEAR(size, coords.getAxisValue(AMOTION_EVENT_AXIS_SIZE), EPSILON);
-        ASSERT_NEAR(touchMajor, coords.getAxisValue(AMOTION_EVENT_AXIS_TOUCH_MAJOR),
-                    scaledAxisEpsilon);
-        ASSERT_NEAR(touchMinor, coords.getAxisValue(AMOTION_EVENT_AXIS_TOUCH_MINOR),
-                    scaledAxisEpsilon);
-        ASSERT_NEAR(toolMajor, coords.getAxisValue(AMOTION_EVENT_AXIS_TOOL_MAJOR),
-                    scaledAxisEpsilon);
-        ASSERT_NEAR(toolMinor, coords.getAxisValue(AMOTION_EVENT_AXIS_TOOL_MINOR),
-                    scaledAxisEpsilon);
-        ASSERT_NEAR(orientation, coords.getAxisValue(AMOTION_EVENT_AXIS_ORIENTATION), EPSILON);
-        ASSERT_NEAR(distance, coords.getAxisValue(AMOTION_EVENT_AXIS_DISTANCE), EPSILON);
-    }
-
-    static void assertPosition(const FakePointerController& controller, float x, float y) {
-        float actualX, actualY;
-        controller.getPosition(&actualX, &actualY);
-        ASSERT_NEAR(x, actualX, 1);
-        ASSERT_NEAR(y, actualY, 1);
-    }
-};
-
-const char* InputMapperTest::DEVICE_NAME = "device";
-const char* InputMapperTest::DEVICE_LOCATION = "USB1";
-const int32_t InputMapperTest::DEVICE_ID = END_RESERVED_ID + 1000;
-const int32_t InputMapperTest::DEVICE_GENERATION = 2;
-const int32_t InputMapperTest::DEVICE_CONTROLLER_NUMBER = 0;
-const ftl::Flags<InputDeviceClass> InputMapperTest::DEVICE_CLASSES =
-        ftl::Flags<InputDeviceClass>(0); // not needed for current tests
-const int32_t InputMapperTest::EVENTHUB_ID = 1;
-
 // --- SwitchInputMapperTest ---
 
 class SwitchInputMapperTest : public InputMapperTest {
diff --git a/services/inputflinger/tests/TestConstants.h b/services/inputflinger/tests/TestConstants.h
index 8dc9d71..27881f6 100644
--- a/services/inputflinger/tests/TestConstants.h
+++ b/services/inputflinger/tests/TestConstants.h
@@ -27,4 +27,7 @@
 static constexpr nsecs_t ARBITRARY_TIME = 1234;
 static constexpr nsecs_t READ_TIME = 4321;
 
+// Error tolerance for floating point assertions.
+static const float EPSILON = 0.001f;
+
 } // namespace android
diff --git a/services/sensorservice/aidl/utils.cpp b/services/sensorservice/aidl/utils.cpp
index 26bcdc5..beb38b9 100644
--- a/services/sensorservice/aidl/utils.cpp
+++ b/services/sensorservice/aidl/utils.cpp
@@ -58,7 +58,7 @@
 ::aidl::android::hardware::sensors::Event convertEvent(const ::ASensorEvent& src) {
     ::aidl::android::hardware::sensors::Event dst;
     ::android::hardware::sensors::implementation::
-            convertFromSensorEvent(reinterpret_cast<const sensors_event_t&>(src), &dst);
+            convertFromASensorEvent(src, &dst);
     return dst;
 }
 
diff --git a/services/sensorservice/hidl/utils.cpp b/services/sensorservice/hidl/utils.cpp
index 2f9e922..5fa594d 100644
--- a/services/sensorservice/hidl/utils.cpp
+++ b/services/sensorservice/hidl/utils.cpp
@@ -76,8 +76,8 @@
 
 ::android::hardware::sensors::V1_0::Event convertEvent(const ::ASensorEvent& src) {
     ::android::hardware::sensors::V1_0::Event dst;
-    ::android::hardware::sensors::V1_0::implementation::convertFromSensorEvent(
-            reinterpret_cast<const sensors_event_t&>(src), &dst);
+    ::android::hardware::sensors::V1_0::implementation::convertFromASensorEvent(
+            src, &dst);
     return dst;
 }
 
diff --git a/services/surfaceflinger/Scheduler/EventThread.cpp b/services/surfaceflinger/Scheduler/EventThread.cpp
index 1acf15a..008d8c4 100644
--- a/services/surfaceflinger/Scheduler/EventThread.cpp
+++ b/services/surfaceflinger/Scheduler/EventThread.cpp
@@ -124,12 +124,12 @@
     return event;
 }
 
-DisplayEventReceiver::Event makeModeChanged(DisplayModePtr mode) {
+DisplayEventReceiver::Event makeModeChanged(const scheduler::FrameRateMode& mode) {
     DisplayEventReceiver::Event event;
-    event.header = {DisplayEventReceiver::DISPLAY_EVENT_MODE_CHANGE, mode->getPhysicalDisplayId(),
-                    systemTime()};
-    event.modeChange.modeId = mode->getId().value();
-    event.modeChange.vsyncPeriod = mode->getVsyncPeriod();
+    event.header = {DisplayEventReceiver::DISPLAY_EVENT_MODE_CHANGE,
+                    mode.modePtr->getPhysicalDisplayId(), systemTime()};
+    event.modeChange.modeId = mode.modePtr->getId().value();
+    event.modeChange.vsyncPeriod = mode.fps.getPeriodNsecs();
     return event;
 }
 
@@ -405,7 +405,7 @@
     mCondition.notify_all();
 }
 
-void EventThread::onModeChanged(DisplayModePtr mode) {
+void EventThread::onModeChanged(const scheduler::FrameRateMode& mode) {
     std::lock_guard<std::mutex> lock(mMutex);
 
     mPendingEvents.push_back(makeModeChanged(mode));
diff --git a/services/surfaceflinger/Scheduler/EventThread.h b/services/surfaceflinger/Scheduler/EventThread.h
index 7a5a348..43c3598 100644
--- a/services/surfaceflinger/Scheduler/EventThread.h
+++ b/services/surfaceflinger/Scheduler/EventThread.h
@@ -23,6 +23,7 @@
 #include <sys/types.h>
 #include <utils/Errors.h>
 
+#include <scheduler/FrameRateMode.h>
 #include <condition_variable>
 #include <cstdint>
 #include <deque>
@@ -134,7 +135,7 @@
     virtual void onHotplugReceived(PhysicalDisplayId displayId, bool connected) = 0;
 
     // called when SF changes the active mode and apps needs to be notified about the change
-    virtual void onModeChanged(DisplayModePtr) = 0;
+    virtual void onModeChanged(const scheduler::FrameRateMode&) = 0;
 
     // called when SF updates the Frame Rate Override list
     virtual void onFrameRateOverridesChanged(PhysicalDisplayId displayId,
@@ -185,7 +186,7 @@
 
     void onHotplugReceived(PhysicalDisplayId displayId, bool connected) override;
 
-    void onModeChanged(DisplayModePtr) override;
+    void onModeChanged(const scheduler::FrameRateMode&) override;
 
     void onFrameRateOverridesChanged(PhysicalDisplayId displayId,
                                      std::vector<FrameRateOverride> overrides) override;
diff --git a/services/surfaceflinger/Scheduler/Scheduler.cpp b/services/surfaceflinger/Scheduler/Scheduler.cpp
index 7f8f600..34f8df2 100644
--- a/services/surfaceflinger/Scheduler/Scheduler.cpp
+++ b/services/surfaceflinger/Scheduler/Scheduler.cpp
@@ -330,7 +330,7 @@
         RETURN_IF_INVALID_HANDLE(handle);
         thread = mConnections[handle].thread.get();
     }
-    thread->onModeChanged(mode.modePtr.get());
+    thread->onModeChanged(mode);
 }
 
 size_t Scheduler::getEventThreadConnectionCount(ConnectionHandle handle) {
diff --git a/services/surfaceflinger/SurfaceFlinger.cpp b/services/surfaceflinger/SurfaceFlinger.cpp
index bd3c0f1..cc4e082 100644
--- a/services/surfaceflinger/SurfaceFlinger.cpp
+++ b/services/surfaceflinger/SurfaceFlinger.cpp
@@ -1083,8 +1083,9 @@
 
     const PhysicalDisplayId displayId = snapshot.displayId();
 
-    info->activeDisplayModeId =
-            display->refreshRateSelector().getActiveMode().modePtr->getId().value();
+    const auto mode = display->refreshRateSelector().getActiveMode();
+    info->activeDisplayModeId = mode.modePtr->getId().value();
+    info->renderFrameRate = mode.fps.getValue();
     info->activeColorMode = display->getCompositionDisplay()->getState().colorMode;
     info->hdrCapabilities = filterOut4k30(display->getHdrCapabilities());
 
@@ -1127,8 +1128,8 @@
         return;
     }
 
-    const Fps renderFps = request.mode.fps;
-    const Fps displayFps = request.mode.modePtr->getFps();
+    const auto mode = request.mode;
+    const bool emitEvent = request.emitEvent;
 
     switch (display->setDesiredActiveMode(DisplayDevice::ActiveModeInfo(std::move(request)))) {
         case DisplayDevice::DesiredActiveModeAction::InitiateDisplayModeSwitch:
@@ -1136,21 +1137,22 @@
 
             // Start receiving vsync samples now, so that we can detect a period
             // switch.
-            mScheduler->resyncToHardwareVsync(true, displayFps);
+            mScheduler->resyncToHardwareVsync(true, mode.modePtr->getFps());
             // As we called to set period, we will call to onRefreshRateChangeCompleted once
             // VsyncController model is locked.
             modulateVsync(&VsyncModulator::onRefreshRateChangeInitiated);
 
-            updatePhaseConfiguration(renderFps);
+            updatePhaseConfiguration(mode.fps);
             mScheduler->setModeChangePending(true);
             break;
         case DisplayDevice::DesiredActiveModeAction::InitiateRenderRateSwitch:
-            mScheduler->setRenderRate(renderFps);
-            updatePhaseConfiguration(renderFps);
-            mRefreshRateStats->setRefreshRate(renderFps);
+            mScheduler->setRenderRate(mode.fps);
+            updatePhaseConfiguration(mode.fps);
+            mRefreshRateStats->setRefreshRate(mode.fps);
+            if (display->getPhysicalId() == mActiveDisplayId && emitEvent) {
+                mScheduler->onPrimaryDisplayModeChanged(mAppConnectionHandle, mode);
+            }
 
-            // TODO(b/259740021): send event to display manager about
-            //  the render rate change
             break;
         case DisplayDevice::DesiredActiveModeAction::None:
             break;
@@ -7405,6 +7407,7 @@
         }
 
         outInfo->activeDisplayModeId = info.activeDisplayModeId;
+        outInfo->renderFrameRate = info.renderFrameRate;
 
         outInfo->supportedColorModes.clear();
         outInfo->supportedColorModes.reserve(info.supportedColorModes.size());
diff --git a/services/surfaceflinger/tests/unittests/EventThreadTest.cpp b/services/surfaceflinger/tests/unittests/EventThreadTest.cpp
index a5beaba..dd87f9d 100644
--- a/services/surfaceflinger/tests/unittests/EventThreadTest.cpp
+++ b/services/surfaceflinger/tests/unittests/EventThreadTest.cpp
@@ -633,9 +633,10 @@
                               .setId(DisplayModeId(7))
                               .setVsyncPeriod(16666666)
                               .build();
+    const Fps fps = mode->getFps() / 2;
 
-    mThread->onModeChanged(mode);
-    expectConfigChangedEventReceivedByConnection(INTERNAL_DISPLAY_ID, 7, 16666666);
+    mThread->onModeChanged({fps, ftl::as_non_null(mode)});
+    expectConfigChangedEventReceivedByConnection(INTERNAL_DISPLAY_ID, 7, fps.getPeriodNsecs());
 }
 
 TEST_F(EventThreadTest, postConfigChangedExternal) {
@@ -644,9 +645,10 @@
                               .setId(DisplayModeId(5))
                               .setVsyncPeriod(16666666)
                               .build();
+    const Fps fps = mode->getFps() / 2;
 
-    mThread->onModeChanged(mode);
-    expectConfigChangedEventReceivedByConnection(EXTERNAL_DISPLAY_ID, 5, 16666666);
+    mThread->onModeChanged({fps, ftl::as_non_null(mode)});
+    expectConfigChangedEventReceivedByConnection(EXTERNAL_DISPLAY_ID, 5, fps.getPeriodNsecs());
 }
 
 TEST_F(EventThreadTest, postConfigChangedPrimary64bit) {
@@ -655,8 +657,9 @@
                               .setId(DisplayModeId(7))
                               .setVsyncPeriod(16666666)
                               .build();
-    mThread->onModeChanged(mode);
-    expectConfigChangedEventReceivedByConnection(DISPLAY_ID_64BIT, 7, 16666666);
+    const Fps fps = mode->getFps() / 2;
+    mThread->onModeChanged({fps, ftl::as_non_null(mode)});
+    expectConfigChangedEventReceivedByConnection(DISPLAY_ID_64BIT, 7, fps.getPeriodNsecs());
 }
 
 TEST_F(EventThreadTest, suppressConfigChanged) {
@@ -669,9 +672,10 @@
                               .setId(DisplayModeId(9))
                               .setVsyncPeriod(16666666)
                               .build();
+    const Fps fps = mode->getFps() / 2;
 
-    mThread->onModeChanged(mode);
-    expectConfigChangedEventReceivedByConnection(INTERNAL_DISPLAY_ID, 9, 16666666);
+    mThread->onModeChanged({fps, ftl::as_non_null(mode)});
+    expectConfigChangedEventReceivedByConnection(INTERNAL_DISPLAY_ID, 9, fps.getPeriodNsecs());
 
     auto args = suppressConnectionEventRecorder.waitForCall();
     ASSERT_FALSE(args.has_value());
diff --git a/services/surfaceflinger/tests/unittests/SurfaceFlinger_DisplayModeSwitching.cpp b/services/surfaceflinger/tests/unittests/SurfaceFlinger_DisplayModeSwitching.cpp
index b81693a..074bf8c 100644
--- a/services/surfaceflinger/tests/unittests/SurfaceFlinger_DisplayModeSwitching.cpp
+++ b/services/surfaceflinger/tests/unittests/SurfaceFlinger_DisplayModeSwitching.cpp
@@ -144,7 +144,8 @@
     // Verify that the next commit will complete the mode change and send
     // a onModeChanged event to the framework.
 
-    EXPECT_CALL(*mAppEventThread, onModeChanged(kMode90));
+    EXPECT_CALL(*mAppEventThread,
+                onModeChanged(scheduler::FrameRateMode{90_Hz, ftl::as_non_null(kMode90)}));
     mFlinger.commit();
     Mock::VerifyAndClearExpectations(mAppEventThread);
 
@@ -175,7 +176,8 @@
                                                hal::HWConfigId(kModeId90.value()), _, _))
             .WillOnce(DoAll(SetArgPointee<3>(timeline), Return(Error::NONE)));
 
-    EXPECT_CALL(*mAppEventThread, onModeChanged(kMode90));
+    EXPECT_CALL(*mAppEventThread,
+                onModeChanged(scheduler::FrameRateMode{90_Hz, ftl::as_non_null(kMode90)}));
 
     mFlinger.commit();
 
diff --git a/services/surfaceflinger/tests/unittests/mock/MockEventThread.h b/services/surfaceflinger/tests/unittests/mock/MockEventThread.h
index ded6806..f8567bd 100644
--- a/services/surfaceflinger/tests/unittests/mock/MockEventThread.h
+++ b/services/surfaceflinger/tests/unittests/mock/MockEventThread.h
@@ -34,7 +34,7 @@
     MOCK_METHOD0(onScreenReleased, void());
     MOCK_METHOD0(onScreenAcquired, void());
     MOCK_METHOD2(onHotplugReceived, void(PhysicalDisplayId, bool));
-    MOCK_METHOD1(onModeChanged, void(DisplayModePtr));
+    MOCK_METHOD1(onModeChanged, void(const scheduler::FrameRateMode &));
     MOCK_METHOD2(onFrameRateOverridesChanged,
                  void(PhysicalDisplayId, std::vector<FrameRateOverride>));
     MOCK_CONST_METHOD1(dump, void(std::string&));
diff --git a/vulkan/libvulkan/driver.cpp b/vulkan/libvulkan/driver.cpp
index 4927150..a99355f 100644
--- a/vulkan/libvulkan/driver.cpp
+++ b/vulkan/libvulkan/driver.cpp
@@ -1027,8 +1027,11 @@
     }
 }
 
-bool GetAndroidNativeBufferSpecVersion9Support(
-    VkPhysicalDevice physicalDevice) {
+VkResult GetAndroidNativeBufferSpecVersion9Support(
+    VkPhysicalDevice physicalDevice,
+    bool& support) {
+    support = false;
+
     const InstanceData& data = GetData(physicalDevice);
 
     // Call to get propertyCount
@@ -1038,6 +1041,10 @@
         physicalDevice, nullptr, &propertyCount, nullptr);
     ATRACE_END();
 
+    if (result != VK_SUCCESS && result != VK_INCOMPLETE) {
+        return result;
+    }
+
     // Call to enumerate properties
     std::vector<VkExtensionProperties> properties(propertyCount);
     ATRACE_BEGIN("driver.EnumerateDeviceExtensionProperties");
@@ -1045,6 +1052,10 @@
         physicalDevice, nullptr, &propertyCount, properties.data());
     ATRACE_END();
 
+    if (result != VK_SUCCESS && result != VK_INCOMPLETE) {
+        return result;
+    }
+
     for (uint32_t i = 0; i < propertyCount; i++) {
         auto& prop = properties[i];
 
@@ -1053,11 +1064,12 @@
             continue;
 
         if (prop.specVersion >= 9) {
-            return true;
+            support = true;
+            return result;
         }
     }
 
-    return false;
+    return result;
 }
 
 VkResult EnumerateDeviceExtensionProperties(
@@ -1101,18 +1113,30 @@
     swapchainCompFeats.sType =
         VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_IMAGE_COMPRESSION_CONTROL_SWAPCHAIN_FEATURES_EXT;
     swapchainCompFeats.pNext = nullptr;
+    swapchainCompFeats.imageCompressionControlSwapchain = false;
     VkPhysicalDeviceImageCompressionControlFeaturesEXT imageCompFeats = {};
     imageCompFeats.sType =
         VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_IMAGE_COMPRESSION_CONTROL_FEATURES_EXT;
     imageCompFeats.pNext = &swapchainCompFeats;
+    imageCompFeats.imageCompressionControl = false;
 
     VkPhysicalDeviceFeatures2 feats2 = {};
     feats2.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2;
     feats2.pNext = &imageCompFeats;
 
-    GetPhysicalDeviceFeatures2(physicalDevice, &feats2);
+    const auto& driver = GetData(physicalDevice).driver;
+    if (driver.GetPhysicalDeviceFeatures2 ||
+        driver.GetPhysicalDeviceFeatures2KHR) {
+        GetPhysicalDeviceFeatures2(physicalDevice, &feats2);
+    }
 
-    bool anb9 = GetAndroidNativeBufferSpecVersion9Support(physicalDevice);
+    bool anb9 = false;
+    VkResult result =
+        GetAndroidNativeBufferSpecVersion9Support(physicalDevice, anb9);
+
+    if (result != VK_SUCCESS && result != VK_INCOMPLETE) {
+        return result;
+    }
 
     if (anb9 && imageCompFeats.imageCompressionControl) {
         loader_extensions.push_back(
@@ -1142,7 +1166,7 @@
     }
 
     ATRACE_BEGIN("driver.EnumerateDeviceExtensionProperties");
-    VkResult result = data.driver.EnumerateDeviceExtensionProperties(
+    result = data.driver.EnumerateDeviceExtensionProperties(
         physicalDevice, pLayerName, pPropertyCount, pProperties);
     ATRACE_END();
 
@@ -1532,6 +1556,11 @@
             } break;
 
             case VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_IMAGE_COMPRESSION_CONTROL_SWAPCHAIN_FEATURES_EXT: {
+                VkPhysicalDeviceImageCompressionControlSwapchainFeaturesEXT*
+                    compressionFeat = reinterpret_cast<
+                        VkPhysicalDeviceImageCompressionControlSwapchainFeaturesEXT*>(
+                        pFeats);
+                compressionFeat->imageCompressionControlSwapchain = false;
                 imageCompressionControlSwapchainInChain = true;
             } break;
 
@@ -1551,6 +1580,7 @@
         imageCompFeats.sType =
             VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_IMAGE_COMPRESSION_CONTROL_FEATURES_EXT;
         imageCompFeats.pNext = nullptr;
+        imageCompFeats.imageCompressionControl = false;
 
         VkPhysicalDeviceFeatures2 feats2 = {};
         feats2.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2;
diff --git a/vulkan/libvulkan/swapchain.cpp b/vulkan/libvulkan/swapchain.cpp
index 475bc40..759149d 100644
--- a/vulkan/libvulkan/swapchain.cpp
+++ b/vulkan/libvulkan/swapchain.cpp
@@ -930,27 +930,36 @@
         return GetPhysicalDeviceSurfaceFormatsKHR(physicalDevice,
                                                   pSurfaceInfo->surface,
                                                   pSurfaceFormatCount, nullptr);
-    } else {
-        // temp vector for forwarding; we'll marshal it into the pSurfaceFormats
-        // after the call.
-        std::vector<VkSurfaceFormatKHR> surface_formats(*pSurfaceFormatCount);
-        VkResult result = GetPhysicalDeviceSurfaceFormatsKHR(
-            physicalDevice, pSurfaceInfo->surface, pSurfaceFormatCount,
-            surface_formats.data());
+    }
 
-        if (result == VK_SUCCESS || result == VK_INCOMPLETE) {
-            const auto& driver = GetData(physicalDevice).driver;
+    // temp vector for forwarding; we'll marshal it into the pSurfaceFormats
+    // after the call.
+    std::vector<VkSurfaceFormatKHR> surface_formats(*pSurfaceFormatCount);
+    VkResult result = GetPhysicalDeviceSurfaceFormatsKHR(
+        physicalDevice, pSurfaceInfo->surface, pSurfaceFormatCount,
+        surface_formats.data());
 
-            // marshal results individually due to stride difference.
-            uint32_t formats_to_marshal = *pSurfaceFormatCount;
-            for (uint32_t i = 0u; i < formats_to_marshal; i++) {
-                pSurfaceFormats[i].surfaceFormat = surface_formats[i];
+    if (result != VK_SUCCESS && result != VK_INCOMPLETE) {
+        return result;
+    }
 
-                // Query the compression properties for the surface format
-                if (pSurfaceFormats[i].pNext) {
+    const auto& driver = GetData(physicalDevice).driver;
+
+    // marshal results individually due to stride difference.
+    uint32_t formats_to_marshal = *pSurfaceFormatCount;
+    for (uint32_t i = 0u; i < formats_to_marshal; i++) {
+        pSurfaceFormats[i].surfaceFormat = surface_formats[i];
+
+        // Query the compression properties for the surface format
+        VkSurfaceFormat2KHR* pSurfaceFormat = &pSurfaceFormats[i];
+        while (pSurfaceFormat->pNext) {
+            pSurfaceFormat =
+                reinterpret_cast<VkSurfaceFormat2KHR*>(pSurfaceFormat->pNext);
+            switch (pSurfaceFormat->sType) {
+                case VK_STRUCTURE_TYPE_IMAGE_COMPRESSION_PROPERTIES_EXT: {
                     VkImageCompressionPropertiesEXT* surfaceCompressionProps =
                         reinterpret_cast<VkImageCompressionPropertiesEXT*>(
-                            pSurfaceFormats[i].pNext);
+                            pSurfaceFormat);
 
                     if (surfaceCompressionProps &&
                         driver.GetPhysicalDeviceImageFormatProperties2KHR) {
@@ -992,12 +1001,16 @@
                             return compressionRes;
                         }
                     }
-                }
+                } break;
+
+                default:
+                    // Ignore all other extension structs
+                    break;
             }
         }
-
-        return result;
     }
+
+    return result;
 }
 
 VKAPI_ATTR