Merge "Fix GContainer XMP prefix."
diff --git a/data/etc/Android.bp b/data/etc/Android.bp
index bdd5172..a737bd3 100644
--- a/data/etc/Android.bp
+++ b/data/etc/Android.bp
@@ -167,6 +167,12 @@
 }
 
 prebuilt_etc {
+    name: "android.hardware.telephony.satellite.prebuilt.xml",
+    src: "android.hardware.telephony.satellite.xml",
+    defaults: ["frameworks_native_data_etc_defaults"],
+}
+
+prebuilt_etc {
     name: "android.hardware.usb.accessory.prebuilt.xml",
     src: "android.hardware.usb.accessory.xml",
     defaults: ["frameworks_native_data_etc_defaults"],
diff --git a/data/etc/android.hardware.telephony.satellite.xml b/data/etc/android.hardware.telephony.satellite.xml
new file mode 100644
index 0000000..5966cba
--- /dev/null
+++ b/data/etc/android.hardware.telephony.satellite.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<!-- Feature for devices that support Satellite communication via Satellite HAL APIs. -->
+<permissions>
+    <feature name="android.hardware.telephony.satellite" />
+</permissions>
diff --git a/include/input/Input.h b/include/input/Input.h
index 62d84e1..e281675 100644
--- a/include/input/Input.h
+++ b/include/input/Input.h
@@ -1132,6 +1132,7 @@
     TYPE_ZOOM_OUT = 1019,
     TYPE_GRAB = 1020,
     TYPE_GRABBING = 1021,
+    TYPE_HANDWRITING = 1022,
 
     TYPE_SPOT_HOVER = 2000,
     TYPE_SPOT_TOUCH = 2001,
diff --git a/include/input/InputDevice.h b/include/input/InputDevice.h
index 09933d3..66d3435 100644
--- a/include/input/InputDevice.h
+++ b/include/input/InputDevice.h
@@ -214,6 +214,12 @@
     std::string layoutType;
 };
 
+// The version of the Universal Stylus Initiative (USI) protocol supported by the input device.
+struct InputDeviceUsiVersion {
+    int32_t majorVersion = -1;
+    int32_t minorVersion = -1;
+};
+
 /*
  * Describes the characteristics and capabilities of an input device.
  */
@@ -235,7 +241,7 @@
 
     void initialize(int32_t id, int32_t generation, int32_t controllerNumber,
                     const InputDeviceIdentifier& identifier, const std::string& alias,
-                    bool isExternal, bool hasMic);
+                    bool isExternal, bool hasMic, int32_t associatedDisplayId);
 
     inline int32_t getId() const { return mId; }
     inline int32_t getControllerNumber() const { return mControllerNumber; }
@@ -295,8 +301,12 @@
 
     std::vector<InputDeviceLightInfo> getLights();
 
-    inline void setSupportsUsi(bool supportsUsi) { mSupportsUsi = supportsUsi; }
-    inline bool supportsUsi() const { return mSupportsUsi; }
+    inline void setUsiVersion(std::optional<InputDeviceUsiVersion> usiVersion) {
+        mUsiVersion = std::move(usiVersion);
+    }
+    inline std::optional<InputDeviceUsiVersion> getUsiVersion() const { return mUsiVersion; }
+
+    inline int32_t getAssociatedDisplayId() const { return mAssociatedDisplayId; }
 
 private:
     int32_t mId;
@@ -310,8 +320,8 @@
     uint32_t mSources;
     int32_t mKeyboardType;
     std::shared_ptr<KeyCharacterMap> mKeyCharacterMap;
-    // Whether this device supports the Universal Stylus Initiative (USI) protocol for styluses.
-    bool mSupportsUsi;
+    std::optional<InputDeviceUsiVersion> mUsiVersion;
+    int32_t mAssociatedDisplayId;
 
     bool mHasVibrator;
     bool mHasBattery;
diff --git a/include/input/PrintTools.h b/include/input/PrintTools.h
index e24344b..02bc201 100644
--- a/include/input/PrintTools.h
+++ b/include/input/PrintTools.h
@@ -16,18 +16,35 @@
 
 #pragma once
 
+#include <bitset>
 #include <map>
 #include <optional>
 #include <set>
 #include <string>
+#include <vector>
 
 namespace android {
 
+template <size_t N>
+std::string bitsetToString(const std::bitset<N>& bitset) {
+    return bitset.to_string();
+}
+
 template <typename T>
 inline std::string constToString(const T& v) {
     return std::to_string(v);
 }
 
+template <>
+inline std::string constToString(const bool& value) {
+    return value ? "true" : "false";
+}
+
+template <>
+inline std::string constToString(const std::vector<bool>::reference& value) {
+    return value ? "true" : "false";
+}
+
 inline std::string constToString(const std::string& s) {
     return s;
 }
@@ -70,6 +87,19 @@
     return out;
 }
 
+/**
+ * Convert a vector to a string. The values of the vector should be of a type supported by
+ * constToString.
+ */
+template <typename T>
+std::string dumpVector(std::vector<T> values) {
+    std::string dump = constToString(values[0]);
+    for (size_t i = 1; i < values.size(); i++) {
+        dump += ", " + constToString(values[i]);
+    }
+    return dump;
+}
+
 const char* toString(bool value);
 
 /**
@@ -81,4 +111,4 @@
  */
 std::string addLinePrefix(std::string str, const std::string& prefix);
 
-} // namespace android
\ No newline at end of file
+} // namespace android
diff --git a/libs/binder/Android.bp b/libs/binder/Android.bp
index 19445d1..808b1ec 100644
--- a/libs/binder/Android.bp
+++ b/libs/binder/Android.bp
@@ -76,7 +76,6 @@
 
     srcs: [
         "Binder.cpp",
-        "BinderRecordReplay.cpp",
         "BpBinder.cpp",
         "Debug.cpp",
         "FdTrigger.cpp",
@@ -84,6 +83,7 @@
         "IResultReceiver.cpp",
         "Parcel.cpp",
         "ParcelFileDescriptor.cpp",
+        "RecordedTransaction.cpp",
         "RpcSession.cpp",
         "RpcServer.cpp",
         "RpcState.cpp",
diff --git a/libs/binder/Binder.cpp b/libs/binder/Binder.cpp
index da5affb..3e49656 100644
--- a/libs/binder/Binder.cpp
+++ b/libs/binder/Binder.cpp
@@ -21,13 +21,13 @@
 
 #include <android-base/logging.h>
 #include <android-base/unique_fd.h>
-#include <binder/BinderRecordReplay.h>
 #include <binder/BpBinder.h>
 #include <binder/IInterface.h>
 #include <binder/IPCThreadState.h>
 #include <binder/IResultReceiver.h>
 #include <binder/IShellCallback.h>
 #include <binder/Parcel.h>
+#include <binder/RecordedTransaction.h>
 #include <binder/RpcServer.h>
 #include <cutils/compiler.h>
 #include <private/android_filesystem_config.h>
@@ -409,11 +409,9 @@
             Parcel emptyReply;
             timespec ts;
             timespec_get(&ts, TIME_UTC);
-            auto transaction =
-                    android::binder::debug::RecordedTransaction::fromDetails(code, flags, ts, data,
-                                                                             reply ? *reply
-                                                                                   : emptyReply,
-                                                                             err);
+            auto transaction = android::binder::debug::RecordedTransaction::
+                    fromDetails(getInterfaceDescriptor(), code, flags, ts, data,
+                                reply ? *reply : emptyReply, err);
             if (transaction) {
                 if (status_t err = transaction->dumpToFile(e->mRecordingFd); err != NO_ERROR) {
                     LOG(INFO) << "Failed to dump RecordedTransaction to file with error " << err;
diff --git a/libs/binder/BpBinder.cpp b/libs/binder/BpBinder.cpp
index 1c470a1..d03326e 100644
--- a/libs/binder/BpBinder.cpp
+++ b/libs/binder/BpBinder.cpp
@@ -388,7 +388,7 @@
 {
     if (isRpcBinder()) {
         if (rpcSession()->getMaxIncomingThreads() < 1) {
-            LOG_ALWAYS_FATAL("Cannot register a DeathRecipient without any incoming connections.");
+            ALOGE("Cannot register a DeathRecipient without any incoming connections.");
             return INVALID_OPERATION;
         }
     } else if constexpr (!kEnableKernelIpc) {
diff --git a/libs/binder/BinderRecordReplay.cpp b/libs/binder/RecordedTransaction.cpp
similarity index 86%
rename from libs/binder/BinderRecordReplay.cpp
rename to libs/binder/RecordedTransaction.cpp
index 58bb106..5406205 100644
--- a/libs/binder/BinderRecordReplay.cpp
+++ b/libs/binder/RecordedTransaction.cpp
@@ -17,7 +17,7 @@
 #include <android-base/file.h>
 #include <android-base/logging.h>
 #include <android-base/unique_fd.h>
-#include <binder/BinderRecordReplay.h>
+#include <binder/RecordedTransaction.h>
 #include <sys/mman.h>
 #include <algorithm>
 
@@ -106,24 +106,30 @@
 // End Chunk may therefore produce an empty, meaningless RecordedTransaction.
 
 RecordedTransaction::RecordedTransaction(RecordedTransaction&& t) noexcept {
-    mHeader = t.mHeader;
+    mData = t.mData;
     mSent.setData(t.getDataParcel().data(), t.getDataParcel().dataSize());
     mReply.setData(t.getReplyParcel().data(), t.getReplyParcel().dataSize());
 }
 
-std::optional<RecordedTransaction> RecordedTransaction::fromDetails(uint32_t code, uint32_t flags,
-                                                                    timespec timestamp,
-                                                                    const Parcel& dataParcel,
-                                                                    const Parcel& replyParcel,
-                                                                    status_t err) {
+std::optional<RecordedTransaction> RecordedTransaction::fromDetails(
+        const String16& interfaceName, uint32_t code, uint32_t flags, timespec timestamp,
+        const Parcel& dataParcel, const Parcel& replyParcel, status_t err) {
     RecordedTransaction t;
-    t.mHeader = {code,
-                 flags,
-                 static_cast<int32_t>(err),
-                 dataParcel.isForRpc() ? static_cast<uint32_t>(1) : static_cast<uint32_t>(0),
-                 static_cast<int64_t>(timestamp.tv_sec),
-                 static_cast<int32_t>(timestamp.tv_nsec),
-                 0};
+    t.mData.mHeader = {code,
+                       flags,
+                       static_cast<int32_t>(err),
+                       dataParcel.isForRpc() ? static_cast<uint32_t>(1) : static_cast<uint32_t>(0),
+                       static_cast<int64_t>(timestamp.tv_sec),
+                       static_cast<int32_t>(timestamp.tv_nsec),
+                       0};
+
+    t.mData.mInterfaceName = String8(interfaceName);
+    if (interfaceName.size() != t.mData.mInterfaceName.bytes()) {
+        LOG(ERROR) << "Interface Name is not valid. Contains characters that aren't single byte "
+                      "utf-8: "
+                   << interfaceName;
+        return std::nullopt;
+    }
 
     if (t.mSent.setData(dataParcel.data(), dataParcel.dataSize()) != android::NO_ERROR) {
         LOG(ERROR) << "Failed to set sent parcel data.";
@@ -142,6 +148,7 @@
     HEADER_CHUNK = 1,
     DATA_PARCEL_CHUNK = 2,
     REPLY_PARCEL_CHUNK = 3,
+    INTERFACE_NAME_CHUNK = 4,
     END_CHUNK = 0x00ffffff,
 };
 
@@ -220,7 +227,11 @@
                                << sizeof(TransactionHeader) << ".";
                     return std::nullopt;
                 }
-                t.mHeader = *reinterpret_cast<TransactionHeader*>(payloadMap);
+                t.mData.mHeader = *reinterpret_cast<TransactionHeader*>(payloadMap);
+                break;
+            }
+            case INTERFACE_NAME_CHUNK: {
+                t.mData.mInterfaceName.setTo(reinterpret_cast<char*>(payloadMap), chunk.dataSize);
                 break;
             }
             case DATA_PARCEL_CHUNK: {
@@ -291,10 +302,17 @@
 android::status_t RecordedTransaction::dumpToFile(const unique_fd& fd) const {
     if (NO_ERROR !=
         writeChunk(fd, HEADER_CHUNK, sizeof(TransactionHeader),
-                   reinterpret_cast<const uint8_t*>(&mHeader))) {
+                   reinterpret_cast<const uint8_t*>(&(mData.mHeader)))) {
         LOG(ERROR) << "Failed to write transactionHeader to fd " << fd.get();
         return UNKNOWN_ERROR;
     }
+    if (NO_ERROR !=
+        writeChunk(fd, INTERFACE_NAME_CHUNK, mData.mInterfaceName.size() * sizeof(uint8_t),
+                   reinterpret_cast<const uint8_t*>(mData.mInterfaceName.string()))) {
+        LOG(INFO) << "Failed to write Interface Name Chunk to fd " << fd.get();
+        return UNKNOWN_ERROR;
+    }
+
     if (NO_ERROR != writeChunk(fd, DATA_PARCEL_CHUNK, mSent.dataSize(), mSent.data())) {
         LOG(ERROR) << "Failed to write sent Parcel to fd " << fd.get();
         return UNKNOWN_ERROR;
@@ -310,26 +328,30 @@
     return NO_ERROR;
 }
 
+const android::String8& RecordedTransaction::getInterfaceName() const {
+    return mData.mInterfaceName;
+}
+
 uint32_t RecordedTransaction::getCode() const {
-    return mHeader.code;
+    return mData.mHeader.code;
 }
 
 uint32_t RecordedTransaction::getFlags() const {
-    return mHeader.flags;
+    return mData.mHeader.flags;
 }
 
 int32_t RecordedTransaction::getReturnedStatus() const {
-    return mHeader.statusReturned;
+    return mData.mHeader.statusReturned;
 }
 
 timespec RecordedTransaction::getTimestamp() const {
-    time_t sec = mHeader.timestampSeconds;
-    int32_t nsec = mHeader.timestampNanoseconds;
+    time_t sec = mData.mHeader.timestampSeconds;
+    int32_t nsec = mData.mHeader.timestampNanoseconds;
     return (timespec){.tv_sec = sec, .tv_nsec = nsec};
 }
 
 uint32_t RecordedTransaction::getVersion() const {
-    return mHeader.version;
+    return mData.mHeader.version;
 }
 
 const Parcel& RecordedTransaction::getDataParcel() const {
diff --git a/libs/binder/include/binder/Binder.h b/libs/binder/include/binder/Binder.h
index 08dbd13..d960a0b 100644
--- a/libs/binder/include/binder/Binder.h
+++ b/libs/binder/include/binder/Binder.h
@@ -106,7 +106,7 @@
                                              const sp<IBinder>& keepAliveBinder);
 
     // Start recording transactions to the unique_fd in data.
-    // See BinderRecordReplay.h for more details.
+    // See RecordedTransaction.h for more details.
     [[nodiscard]] status_t startRecordingTransactions(const Parcel& data);
     // Stop the current recording.
     [[nodiscard]] status_t stopRecordingTransactions();
diff --git a/libs/binder/include/binder/BpBinder.h b/libs/binder/include/binder/BpBinder.h
index 57e103d..5496d61 100644
--- a/libs/binder/include/binder/BpBinder.h
+++ b/libs/binder/include/binder/BpBinder.h
@@ -91,7 +91,7 @@
     std::optional<int32_t> getDebugBinderHandle() const;
 
     // Start recording transactions to the unique_fd.
-    // See BinderRecordReplay.h for more details.
+    // See RecordedTransaction.h for more details.
     status_t startRecordingBinder(const android::base::unique_fd& fd);
     // Stop the current recording.
     status_t stopRecordingBinder();
diff --git a/libs/binder/include/binder/BinderRecordReplay.h b/libs/binder/include/binder/RecordedTransaction.h
similarity index 87%
rename from libs/binder/include/binder/BinderRecordReplay.h
rename to libs/binder/include/binder/RecordedTransaction.h
index ff983f0..4966330 100644
--- a/libs/binder/include/binder/BinderRecordReplay.h
+++ b/libs/binder/include/binder/RecordedTransaction.h
@@ -26,20 +26,22 @@
 
 // Warning: Transactions are sequentially recorded to the file descriptor in a
 // non-stable format. A detailed description of the recording format can be found in
-// BinderRecordReplay.cpp.
+// RecordedTransaction.cpp.
 
 class RecordedTransaction {
 public:
     // Filled with the first transaction from fd.
     static std::optional<RecordedTransaction> fromFile(const android::base::unique_fd& fd);
     // Filled with the arguments.
-    static std::optional<RecordedTransaction> fromDetails(uint32_t code, uint32_t flags,
+    static std::optional<RecordedTransaction> fromDetails(const String16& interfaceName,
+                                                          uint32_t code, uint32_t flags,
                                                           timespec timestamp, const Parcel& data,
                                                           const Parcel& reply, status_t err);
     RecordedTransaction(RecordedTransaction&& t) noexcept;
 
     [[nodiscard]] status_t dumpToFile(const android::base::unique_fd& fd) const;
 
+    const String8& getInterfaceName() const;
     uint32_t getCode() const;
     uint32_t getFlags() const;
     int32_t getReturnedStatus() const;
@@ -69,7 +71,11 @@
     static_assert(sizeof(TransactionHeader) == 32);
     static_assert(sizeof(TransactionHeader) % 8 == 0);
 
-    TransactionHeader mHeader;
+    struct MovableData { // movable
+        TransactionHeader mHeader;
+        String8 mInterfaceName;
+    };
+    MovableData mData;
     Parcel mSent;
     Parcel mReply;
 };
diff --git a/libs/binder/tests/binderRecordedTransactionTest.cpp b/libs/binder/tests/binderRecordedTransactionTest.cpp
index 67553fc..2f5c8c6 100644
--- a/libs/binder/tests/binderRecordedTransactionTest.cpp
+++ b/libs/binder/tests/binderRecordedTransactionTest.cpp
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-#include <binder/BinderRecordReplay.h>
+#include <binder/RecordedTransaction.h>
 #include <gtest/gtest.h>
 #include <utils/Errors.h>
 
@@ -24,13 +24,16 @@
 using android::binder::debug::RecordedTransaction;
 
 TEST(BinderRecordedTransaction, RoundTripEncoding) {
+    android::String16 interfaceName("SampleInterface");
     Parcel d;
     d.writeInt32(12);
     d.writeInt64(2);
     Parcel r;
     r.writeInt32(99);
     timespec ts = {1232456, 567890};
-    auto transaction = RecordedTransaction::fromDetails(1, 42, ts, d, r, 0);
+
+    auto transaction = RecordedTransaction::fromDetails(interfaceName, 1, 42, ts, d, r, 0);
+    EXPECT_TRUE(transaction.has_value());
 
     auto file = std::tmpfile();
     auto fd = unique_fd(fcntl(fileno(file), F_DUPFD, 1));
@@ -42,6 +45,7 @@
 
     auto retrievedTransaction = RecordedTransaction::fromFile(fd);
 
+    EXPECT_EQ(retrievedTransaction->getInterfaceName(), android::String8(interfaceName));
     EXPECT_EQ(retrievedTransaction->getCode(), 1);
     EXPECT_EQ(retrievedTransaction->getFlags(), 42);
     EXPECT_EQ(retrievedTransaction->getTimestamp().tv_sec, ts.tv_sec);
@@ -57,13 +61,14 @@
 }
 
 TEST(BinderRecordedTransaction, Checksum) {
+    android::String16 interfaceName("SampleInterface");
     Parcel d;
     d.writeInt32(12);
     d.writeInt64(2);
     Parcel r;
     r.writeInt32(99);
     timespec ts = {1232456, 567890};
-    auto transaction = RecordedTransaction::fromDetails(1, 42, ts, d, r, 0);
+    auto transaction = RecordedTransaction::fromDetails(interfaceName, 1, 42, ts, d, r, 0);
 
     auto file = std::tmpfile();
     auto fd = unique_fd(fcntl(fileno(file), F_DUPFD, 1));
@@ -91,6 +96,7 @@
     std::vector<uint8_t> largePayload;
     uint8_t filler = 0xaa;
     largePayload.insert(largePayload.end(), largeDataSize, filler);
+    android::String16 interfaceName("SampleInterface");
     Parcel d;
     d.writeInt32(12);
     d.writeInt64(2);
@@ -98,7 +104,7 @@
     Parcel r;
     r.writeInt32(99);
     timespec ts = {1232456, 567890};
-    auto transaction = RecordedTransaction::fromDetails(1, 42, ts, d, r, 0);
+    auto transaction = RecordedTransaction::fromDetails(interfaceName, 1, 42, ts, d, r, 0);
 
     auto file = std::tmpfile();
     auto fd = unique_fd(fcntl(fileno(file), F_DUPFD, 1));
diff --git a/libs/binder/tests/binderRpcTest.cpp b/libs/binder/tests/binderRpcTest.cpp
index 9be5b87..36c8d8c 100644
--- a/libs/binder/tests/binderRpcTest.cpp
+++ b/libs/binder/tests/binderRpcTest.cpp
@@ -683,7 +683,7 @@
     proc.expectAlreadyShutdown = true;
 }
 
-TEST_P(BinderRpc, DeathRecipientFatalWithoutIncoming) {
+TEST_P(BinderRpc, DeathRecipientFailsWithoutIncoming) {
     class MyDeathRec : public IBinder::DeathRecipient {
     public:
         void binderDied(const wp<IBinder>& /* who */) override {}
@@ -693,8 +693,7 @@
             {.numThreads = 1, .numSessions = 1, .numIncomingConnections = 0});
 
     auto dr = sp<MyDeathRec>::make();
-    EXPECT_DEATH(proc.rootBinder->linkToDeath(dr, (void*)1, 0),
-                 "Cannot register a DeathRecipient without any incoming connections.");
+    EXPECT_EQ(INVALID_OPERATION, proc.rootBinder->linkToDeath(dr, (void*)1, 0));
 }
 
 TEST_P(BinderRpc, UnlinkDeathRecipient) {
diff --git a/libs/input/InputDevice.cpp b/libs/input/InputDevice.cpp
index 87333f2..9c7c0c1 100644
--- a/libs/input/InputDevice.cpp
+++ b/libs/input/InputDevice.cpp
@@ -22,6 +22,7 @@
 
 #include <android-base/stringprintf.h>
 #include <ftl/enum.h>
+#include <gui/constants.h>
 #include <input/InputDevice.h>
 #include <input/InputEventLabels.h>
 
@@ -166,7 +167,7 @@
 // --- InputDeviceInfo ---
 
 InputDeviceInfo::InputDeviceInfo() {
-    initialize(-1, 0, -1, InputDeviceIdentifier(), "", false, false);
+    initialize(-1, 0, -1, InputDeviceIdentifier(), "", false, false, ADISPLAY_ID_NONE);
 }
 
 InputDeviceInfo::InputDeviceInfo(const InputDeviceInfo& other)
@@ -181,7 +182,8 @@
         mSources(other.mSources),
         mKeyboardType(other.mKeyboardType),
         mKeyCharacterMap(other.mKeyCharacterMap),
-        mSupportsUsi(other.mSupportsUsi),
+        mUsiVersion(other.mUsiVersion),
+        mAssociatedDisplayId(other.mAssociatedDisplayId),
         mHasVibrator(other.mHasVibrator),
         mHasBattery(other.mHasBattery),
         mHasButtonUnderPad(other.mHasButtonUnderPad),
@@ -195,7 +197,7 @@
 
 void InputDeviceInfo::initialize(int32_t id, int32_t generation, int32_t controllerNumber,
                                  const InputDeviceIdentifier& identifier, const std::string& alias,
-                                 bool isExternal, bool hasMic) {
+                                 bool isExternal, bool hasMic, int32_t associatedDisplayId) {
     mId = id;
     mGeneration = generation;
     mControllerNumber = controllerNumber;
@@ -205,11 +207,12 @@
     mHasMic = hasMic;
     mSources = 0;
     mKeyboardType = AINPUT_KEYBOARD_TYPE_NONE;
+    mAssociatedDisplayId = associatedDisplayId;
     mHasVibrator = false;
     mHasBattery = false;
     mHasButtonUnderPad = false;
     mHasSensor = false;
-    mSupportsUsi = false;
+    mUsiVersion.reset();
     mMotionRanges.clear();
     mSensors.clear();
     mLights.clear();
diff --git a/libs/jpegrecoverymap/Android.bp b/libs/jpegrecoverymap/Android.bp
index c9bed70..01318dc 100644
--- a/libs/jpegrecoverymap/Android.bp
+++ b/libs/jpegrecoverymap/Android.bp
@@ -39,6 +39,7 @@
         "libjpeg",
         "libjpegencoder",
         "libjpegdecoder",
+        "liblog",
     ],
 }
 
diff --git a/libs/jpegrecoverymap/include/jpegrecoverymap/recoverymapmath.h b/libs/jpegrecoverymap/include/jpegrecoverymap/recoverymapmath.h
index 0fb64d3..0695bb7 100644
--- a/libs/jpegrecoverymap/include/jpegrecoverymap/recoverymapmath.h
+++ b/libs/jpegrecoverymap/include/jpegrecoverymap/recoverymapmath.h
@@ -17,12 +17,15 @@
 #ifndef ANDROID_JPEGRECOVERYMAP_RECOVERYMAPMATH_H
 #define ANDROID_JPEGRECOVERYMAP_RECOVERYMAPMATH_H
 
+#include <cmath>
 #include <stdint.h>
 
 #include <jpegrecoverymap/recoverymap.h>
 
 namespace android::recoverymap {
 
+#define CLIP3(x, min, max) ((x) < (min)) ? (min) : ((x) > (max)) ? (max) : (x)
+
 ////////////////////////////////////////////////////////////////////////////////
 // Framework
 
@@ -112,6 +115,76 @@
   return temp /= rhs;
 }
 
+constexpr size_t kRecoveryFactorPrecision = 10;
+constexpr size_t kRecoveryFactorNumEntries = 1 << kRecoveryFactorPrecision;
+struct RecoveryLUT {
+  RecoveryLUT(float hdrRatio) {
+    float increment = 2.0 / kRecoveryFactorNumEntries;
+    float value = -1.0f;
+    for (int idx = 0; idx < kRecoveryFactorNumEntries; idx++, value += increment) {
+      mRecoveryTable[idx] = pow(hdrRatio, value);
+    }
+  }
+
+  ~RecoveryLUT() {
+  }
+
+  float getRecoveryFactor(float recovery) {
+    uint32_t value = static_cast<uint32_t>(((recovery + 1.0f) / 2.0f) * kRecoveryFactorNumEntries);
+    //TODO() : Remove once conversion modules have appropriate clamping in place
+    value = CLIP3(value, 0, kRecoveryFactorNumEntries - 1);
+    return mRecoveryTable[value];
+  }
+
+private:
+  float mRecoveryTable[kRecoveryFactorNumEntries];
+};
+
+struct ShepardsIDW {
+  ShepardsIDW(int mapScaleFactor) : mMapScaleFactor{mapScaleFactor} {
+    const int size = mMapScaleFactor * mMapScaleFactor * 4;
+    mWeights = new float[size];
+    mWeightsNR = new float[size];
+    mWeightsNB = new float[size];
+    mWeightsC = new float[size];
+    fillShepardsIDW(mWeights, 1, 1);
+    fillShepardsIDW(mWeightsNR, 0, 1);
+    fillShepardsIDW(mWeightsNB, 1, 0);
+    fillShepardsIDW(mWeightsC, 0, 0);
+  }
+  ~ShepardsIDW() {
+    delete[] mWeights;
+    delete[] mWeightsNR;
+    delete[] mWeightsNB;
+    delete[] mWeightsC;
+  }
+
+  int mMapScaleFactor;
+  // Image :-
+  // p00 p01 p02 p03 p04 p05 p06 p07
+  // p10 p11 p12 p13 p14 p15 p16 p17
+  // p20 p21 p22 p23 p24 p25 p26 p27
+  // p30 p31 p32 p33 p34 p35 p36 p37
+  // p40 p41 p42 p43 p44 p45 p46 p47
+  // p50 p51 p52 p53 p54 p55 p56 p57
+  // p60 p61 p62 p63 p64 p65 p66 p67
+  // p70 p71 p72 p73 p74 p75 p76 p77
+
+  // Recovery Map (for 4 scale factor) :-
+  // m00 p01
+  // m10 m11
+
+  // Recovery sample of curr 4x4, right 4x4, bottom 4x4, bottom right 4x4 are used during
+  // reconstruction. hence table weight size is 4.
+  float* mWeights;
+  // TODO: check if its ok to mWeights at places
+  float* mWeightsNR;  // no right
+  float* mWeightsNB;  // no bottom
+  float* mWeightsC;  // no right & bottom
+
+  float euclideanDistance(float x1, float x2, float y1, float y2);
+  void fillShepardsIDW(float *weights, int incR, int incB);
+};
 
 ////////////////////////////////////////////////////////////////////////////////
 // sRGB transformations
@@ -143,7 +216,8 @@
  */
 float srgbInvOetf(float e_gamma);
 Color srgbInvOetf(Color e_gamma);
-
+float srgbInvOetfLUT(float e_gamma);
+Color srgbInvOetfLUT(Color e_gamma);
 
 ////////////////////////////////////////////////////////////////////////////////
 // Display-P3 transformations
@@ -183,6 +257,8 @@
  */
 float hlgOetf(float e);
 Color hlgOetf(Color e);
+float hlgOetfLUT(float e);
+Color hlgOetfLUT(Color e);
 
 /*
  * Convert from HLG to scene luminance.
@@ -191,6 +267,8 @@
  */
 float hlgInvOetf(float e_gamma);
 Color hlgInvOetf(Color e_gamma);
+float hlgInvOetfLUT(float e_gamma);
+Color hlgInvOetfLUT(Color e_gamma);
 
 /*
  * Convert from scene luminance to PQ.
@@ -199,6 +277,8 @@
  */
 float pqOetf(float e);
 Color pqOetf(Color e);
+float pqOetfLUT(float e);
+Color pqOetfLUT(Color e);
 
 /*
  * Convert from PQ to scene luminance in nits.
@@ -207,6 +287,8 @@
  */
 float pqInvOetf(float e_gamma);
 Color pqInvOetf(Color e_gamma);
+float pqInvOetfLUT(float e_gamma);
+Color pqInvOetfLUT(Color e_gamma);
 
 
 ////////////////////////////////////////////////////////////////////////////////
@@ -251,6 +333,7 @@
  * value, with the given hdr ratio, to the given sdr input in the range [0, 1].
  */
 Color applyRecovery(Color e, float recovery, float hdr_ratio);
+Color applyRecoveryLUT(Color e, float recovery, RecoveryLUT& recoveryLUT);
 
 /*
  * Helper for sampling from YUV 420 images.
@@ -282,7 +365,9 @@
  * Sample the recovery value for the map from a given x,y coordinate on a scale
  * that is map scale factor larger than the map size.
  */
-float sampleMap(jr_uncompressed_ptr map, size_t map_scale_factor, size_t x, size_t y);
+float sampleMap(jr_uncompressed_ptr map, float map_scale_factor, size_t x, size_t y);
+float sampleMap(jr_uncompressed_ptr map, size_t map_scale_factor, size_t x, size_t y,
+                ShepardsIDW& weightTables);
 
 /*
  * Convert from Color to RGBA1010102.
diff --git a/libs/jpegrecoverymap/recoverymap.cpp b/libs/jpegrecoverymap/recoverymap.cpp
index 6b46d40..eb557e5 100644
--- a/libs/jpegrecoverymap/recoverymap.cpp
+++ b/libs/jpegrecoverymap/recoverymap.cpp
@@ -25,17 +25,30 @@
 #include <image_io/jpeg/jpeg_scanner.h>
 #include <image_io/jpeg/jpeg_info_builder.h>
 #include <image_io/base/data_segment_data_source.h>
+#include <utils/Log.h>
 
 #include <memory>
 #include <sstream>
 #include <string>
 #include <cmath>
+#include <condition_variable>
+#include <deque>
+#include <mutex>
+#include <thread>
+#include <unistd.h>
 
 using namespace std;
 using namespace photos_editing_formats::image_io;
 
 namespace android::recoverymap {
 
+#define USE_SRGB_INVOETF_LUT 1
+#define USE_HLG_OETF_LUT 1
+#define USE_PQ_OETF_LUT 1
+#define USE_HLG_INVOETF_LUT 1
+#define USE_PQ_INVOETF_LUT 1
+#define USE_APPLY_RECOVERY_LUT 1
+
 #define JPEGR_CHECK(x)          \
   {                             \
     status_t status = (x);      \
@@ -49,6 +62,10 @@
 
 // Map is quarter res / sixteenth size
 static const size_t kMapDimensionScaleFactor = 4;
+// JPEG block size.
+// JPEG encoding / decoding will require 8 x 8 DCT transform.
+// Width must be 8 dividable, and height must be 2 dividable.
+static const size_t kJpegBlock = 8;
 // JPEG compress quality (0 ~ 100) for recovery map
 static const int kMapCompressQuality = 85;
 
@@ -62,6 +79,20 @@
   1.0f,
 };
 
+#define CONFIG_MULTITHREAD 1
+int GetCPUCoreCount() {
+  int cpuCoreCount = 1;
+#if CONFIG_MULTITHREAD
+#if defined(_SC_NPROCESSORS_ONLN)
+  cpuCoreCount = sysconf(_SC_NPROCESSORS_ONLN);
+#else
+  // _SC_NPROC_ONLN must be defined...
+  cpuCoreCount = sysconf(_SC_NPROC_ONLN);
+#endif
+#endif
+  return cpuCoreCount;
+}
+
 /*
  * Helper function used for writing data to destination.
  *
@@ -230,6 +261,13 @@
     return ERROR_JPEGR_INVALID_INPUT_TYPE;
   }
 
+  if (uncompressed_p010_image->width % kJpegBlock != 0
+          || uncompressed_p010_image->height % 2 != 0) {
+    ALOGE("Image size can not be handled: %dx%d",
+            uncompressed_p010_image->width, uncompressed_p010_image->height);
+    return ERROR_JPEGR_INVALID_INPUT_TYPE;
+  }
+
   jpegr_metadata metadata;
   metadata.version = kJpegrVersion;
   metadata.transferFunction = hdr_tf;
@@ -304,6 +342,13 @@
     return ERROR_JPEGR_RESOLUTION_MISMATCH;
   }
 
+  if (uncompressed_p010_image->width % kJpegBlock != 0
+          || uncompressed_p010_image->height % 2 != 0) {
+    ALOGE("Image size can not be handled: %dx%d",
+            uncompressed_p010_image->width, uncompressed_p010_image->height);
+    return ERROR_JPEGR_INVALID_INPUT_TYPE;
+  }
+
   jpegr_metadata metadata;
   metadata.version = kJpegrVersion;
   metadata.transferFunction = hdr_tf;
@@ -369,6 +414,13 @@
     return ERROR_JPEGR_RESOLUTION_MISMATCH;
   }
 
+  if (uncompressed_p010_image->width % kJpegBlock != 0
+          || uncompressed_p010_image->height % 2 != 0) {
+    ALOGE("Image size can not be handled: %dx%d",
+            uncompressed_p010_image->width, uncompressed_p010_image->height);
+    return ERROR_JPEGR_INVALID_INPUT_TYPE;
+  }
+
   jpegr_metadata metadata;
   metadata.version = kJpegrVersion;
   metadata.transferFunction = hdr_tf;
@@ -444,6 +496,13 @@
     return ERROR_JPEGR_INVALID_NULL_PTR;
   }
 
+  if (uncompressed_p010_image->width % kJpegBlock != 0
+          || uncompressed_p010_image->height % 2 != 0) {
+    ALOGE("Image size can not be handled: %dx%d",
+            uncompressed_p010_image->width, uncompressed_p010_image->height);
+    return ERROR_JPEGR_INVALID_INPUT_TYPE;
+  }
+
   JpegDecoder jpeg_decoder;
   if (!jpeg_decoder.decompressImage(compressed_jpeg_image->data, compressed_jpeg_image->length)) {
     return ERROR_JPEGR_DECODE_ERROR;
@@ -626,6 +685,62 @@
   return NO_ERROR;
 }
 
+const int kJobSzInRows = 16;
+static_assert(kJobSzInRows > 0 && kJobSzInRows % kMapDimensionScaleFactor == 0,
+              "align job size to kMapDimensionScaleFactor");
+
+class JobQueue {
+ public:
+  bool dequeueJob(size_t& rowStart, size_t& rowEnd);
+  void enqueueJob(size_t rowStart, size_t rowEnd);
+  void markQueueForEnd();
+  void reset();
+
+ private:
+  bool mQueuedAllJobs = false;
+  std::deque<std::tuple<size_t, size_t>> mJobs;
+  std::mutex mMutex;
+  std::condition_variable mCv;
+};
+
+bool JobQueue::dequeueJob(size_t& rowStart, size_t& rowEnd) {
+  std::unique_lock<std::mutex> lock{mMutex};
+  while (true) {
+    if (mJobs.empty()) {
+      if (mQueuedAllJobs) {
+        return false;
+      } else {
+        mCv.wait(lock);
+      }
+    } else {
+      auto it = mJobs.begin();
+      rowStart = std::get<0>(*it);
+      rowEnd = std::get<1>(*it);
+      mJobs.erase(it);
+      return true;
+    }
+  }
+  return false;
+}
+
+void JobQueue::enqueueJob(size_t rowStart, size_t rowEnd) {
+  std::unique_lock<std::mutex> lock{mMutex};
+  mJobs.push_back(std::make_tuple(rowStart, rowEnd));
+  lock.unlock();
+  mCv.notify_one();
+}
+
+void JobQueue::markQueueForEnd() {
+  std::unique_lock<std::mutex> lock{mMutex};
+  mQueuedAllJobs = true;
+}
+
+void JobQueue::reset() {
+  std::unique_lock<std::mutex> lock{mMutex};
+  mJobs.clear();
+  mQueuedAllJobs = false;
+}
+
 status_t RecoveryMap::generateRecoveryMap(jr_uncompressed_ptr uncompressed_yuv_420_image,
                                           jr_uncompressed_ptr uncompressed_p010_image,
                                           jr_metadata_ptr metadata,
@@ -651,11 +766,14 @@
   size_t image_height = uncompressed_yuv_420_image->height;
   size_t map_width = image_width / kMapDimensionScaleFactor;
   size_t map_height = image_height / kMapDimensionScaleFactor;
+  size_t map_stride = static_cast<size_t>(
+          floor((map_width + kJpegBlock - 1) / kJpegBlock)) * kJpegBlock;
+  size_t map_height_aligned = ((map_height + 1) >> 1) << 1;
 
-  dest->width = map_width;
-  dest->height = map_height;
+  dest->width = map_stride;
+  dest->height = map_height_aligned;
   dest->colorGamut = JPEGR_COLORGAMUT_UNSPECIFIED;
-  dest->data = new uint8_t[map_width * map_height];
+  dest->data = new uint8_t[map_stride * map_height_aligned];
   std::unique_ptr<uint8_t[]> map_data;
   map_data.reset(reinterpret_cast<uint8_t*>(dest->data));
 
@@ -666,11 +784,19 @@
       hdrInvOetf = identityConversion;
       break;
     case JPEGR_TF_HLG:
+#if USE_HLG_INVOETF_LUT
+      hdrInvOetf = hlgInvOetfLUT;
+#else
       hdrInvOetf = hlgInvOetf;
+#endif
       hdr_white_nits = kHlgMaxNits;
       break;
     case JPEGR_TF_PQ:
+#if USE_PQ_INVOETF_LUT
+      hdrInvOetf = pqInvOetfLUT;
+#else
       hdrInvOetf = pqInvOetf;
+#endif
       hdr_white_nits = kPqMaxNits;
       break;
     case JPEGR_TF_UNSPECIFIED:
@@ -697,22 +823,87 @@
       return ERROR_JPEGR_INVALID_COLORGAMUT;
   }
 
+  std::mutex mutex;
   float hdr_y_nits_max = 0.0f;
   double hdr_y_nits_avg = 0.0f;
-  for (size_t y = 0; y < image_height; ++y) {
-    for (size_t x = 0; x < image_width; ++x) {
-      Color hdr_yuv_gamma = getP010Pixel(uncompressed_p010_image, x, y);
-      Color hdr_rgb_gamma = bt2100YuvToRgb(hdr_yuv_gamma);
-      Color hdr_rgb = hdrInvOetf(hdr_rgb_gamma);
-      hdr_rgb = hdrGamutConversionFn(hdr_rgb);
-      float hdr_y_nits = luminanceFn(hdr_rgb) * hdr_white_nits;
+  const int threads = std::clamp(GetCPUCoreCount(), 1, 4);
+  size_t rowStep = threads == 1 ? image_height : kJobSzInRows;
+  JobQueue jobQueue;
 
-      hdr_y_nits_avg += hdr_y_nits;
-      if (hdr_y_nits > hdr_y_nits_max) {
-        hdr_y_nits_max = hdr_y_nits;
+  std::function<void()> computeMetadata = [uncompressed_p010_image, hdrInvOetf,
+                                           hdrGamutConversionFn, luminanceFn, hdr_white_nits,
+                                           threads, &mutex, &hdr_y_nits_avg,
+                                           &hdr_y_nits_max, &jobQueue]() -> void {
+    size_t rowStart, rowEnd;
+    float hdr_y_nits_max_th = 0.0f;
+    double hdr_y_nits_avg_th = 0.0f;
+    while (jobQueue.dequeueJob(rowStart, rowEnd)) {
+      for (size_t y = rowStart; y < rowEnd; ++y) {
+        for (size_t x = 0; x < uncompressed_p010_image->width; ++x) {
+          Color hdr_yuv_gamma = getP010Pixel(uncompressed_p010_image, x, y);
+          Color hdr_rgb_gamma = bt2100YuvToRgb(hdr_yuv_gamma);
+          Color hdr_rgb = hdrInvOetf(hdr_rgb_gamma);
+          hdr_rgb = hdrGamutConversionFn(hdr_rgb);
+          float hdr_y_nits = luminanceFn(hdr_rgb) * hdr_white_nits;
+
+          hdr_y_nits_avg_th += hdr_y_nits;
+          if (hdr_y_nits > hdr_y_nits_max_th) {
+            hdr_y_nits_max_th = hdr_y_nits;
+          }
+        }
       }
     }
+    std::unique_lock<std::mutex> lock{mutex};
+    hdr_y_nits_avg += hdr_y_nits_avg_th;
+    hdr_y_nits_max = std::max(hdr_y_nits_max, hdr_y_nits_max_th);
+  };
+
+  std::function<void()> generateMap = [uncompressed_yuv_420_image, uncompressed_p010_image,
+                                       metadata, dest, hdrInvOetf, hdrGamutConversionFn,
+                                       luminanceFn, hdr_white_nits, &jobQueue]() -> void {
+    size_t rowStart, rowEnd;
+    while (jobQueue.dequeueJob(rowStart, rowEnd)) {
+      for (size_t y = rowStart; y < rowEnd; ++y) {
+        for (size_t x = 0; x < dest->width; ++x) {
+          Color sdr_yuv_gamma =
+              sampleYuv420(uncompressed_yuv_420_image, kMapDimensionScaleFactor, x, y);
+          Color sdr_rgb_gamma = srgbYuvToRgb(sdr_yuv_gamma);
+#if USE_SRGB_INVOETF_LUT
+          Color sdr_rgb = srgbInvOetfLUT(sdr_rgb_gamma);
+#else
+          Color sdr_rgb = srgbInvOetf(sdr_rgb_gamma);
+#endif
+          float sdr_y_nits = luminanceFn(sdr_rgb) * kSdrWhiteNits;
+
+          Color hdr_yuv_gamma = sampleP010(uncompressed_p010_image, kMapDimensionScaleFactor, x, y);
+          Color hdr_rgb_gamma = bt2100YuvToRgb(hdr_yuv_gamma);
+          Color hdr_rgb = hdrInvOetf(hdr_rgb_gamma);
+          hdr_rgb = hdrGamutConversionFn(hdr_rgb);
+          float hdr_y_nits = luminanceFn(hdr_rgb) * hdr_white_nits;
+
+          size_t pixel_idx = x + y * dest->width;
+          reinterpret_cast<uint8_t*>(dest->data)[pixel_idx] =
+              encodeRecovery(sdr_y_nits, hdr_y_nits, metadata->rangeScalingFactor);
+        }
+      }
+    }
+  };
+
+  std::vector<std::thread> workers;
+  for (int th = 0; th < threads - 1; th++) {
+    workers.push_back(std::thread(computeMetadata));
   }
+
+  // compute metadata
+  for (size_t rowStart = 0; rowStart < image_height;) {
+    size_t rowEnd = std::min(rowStart + rowStep, image_height);
+    jobQueue.enqueueJob(rowStart, rowEnd);
+    rowStart = rowEnd;
+  }
+  jobQueue.markQueueForEnd();
+  computeMetadata();
+  std::for_each(workers.begin(), workers.end(), [](std::thread& t) { t.join(); });
+  workers.clear();
   hdr_y_nits_avg /= image_width * image_height;
 
   metadata->rangeScalingFactor = hdr_y_nits_max / kSdrWhiteNits;
@@ -721,26 +912,22 @@
     metadata->hdr10Metadata.maxCLL = hdr_y_nits_max;
   }
 
-  for (size_t y = 0; y < map_height; ++y) {
-    for (size_t x = 0; x < map_width; ++x) {
-      Color sdr_yuv_gamma = sampleYuv420(uncompressed_yuv_420_image,
-                                         kMapDimensionScaleFactor, x, y);
-      Color sdr_rgb_gamma = srgbYuvToRgb(sdr_yuv_gamma);
-      Color sdr_rgb = srgbInvOetf(sdr_rgb_gamma);
-      float sdr_y_nits = luminanceFn(sdr_rgb) * kSdrWhiteNits;
-
-      Color hdr_yuv_gamma = sampleP010(uncompressed_p010_image, kMapDimensionScaleFactor, x, y);
-      Color hdr_rgb_gamma = bt2100YuvToRgb(hdr_yuv_gamma);
-      Color hdr_rgb = hdrInvOetf(hdr_rgb_gamma);
-      hdr_rgb = hdrGamutConversionFn(hdr_rgb);
-      float hdr_y_nits = luminanceFn(hdr_rgb) * hdr_white_nits;
-
-      size_t pixel_idx =  x + y * map_width;
-      reinterpret_cast<uint8_t*>(dest->data)[pixel_idx] =
-          encodeRecovery(sdr_y_nits, hdr_y_nits, metadata->rangeScalingFactor);
-    }
+  // generate map
+  jobQueue.reset();
+  for (int th = 0; th < threads - 1; th++) {
+    workers.push_back(std::thread(generateMap));
   }
 
+  rowStep = (threads == 1 ? image_height : kJobSzInRows) / kMapDimensionScaleFactor;
+  for (size_t rowStart = 0; rowStart < map_height;) {
+    size_t rowEnd = std::min(rowStart + rowStep, map_height);
+    jobQueue.enqueueJob(rowStart, rowEnd);
+    rowStart = rowEnd;
+  }
+  jobQueue.markQueueForEnd();
+  generateMap();
+  std::for_each(workers.begin(), workers.end(), [](std::thread& t) { t.join(); });
+
   map_data.release();
   return NO_ERROR;
 }
@@ -756,46 +943,95 @@
     return ERROR_JPEGR_INVALID_NULL_PTR;
   }
 
-  size_t width = uncompressed_yuv_420_image->width;
-  size_t height = uncompressed_yuv_420_image->height;
+  dest->width = uncompressed_yuv_420_image->width;
+  dest->height = uncompressed_yuv_420_image->height;
+  ShepardsIDW idwTable(kMapDimensionScaleFactor);
+  RecoveryLUT recoveryLUT(metadata->rangeScalingFactor);
 
-  dest->width = width;
-  dest->height = height;
-  size_t pixel_count = width * height;
+  JobQueue jobQueue;
+  std::function<void()> applyRecMap = [uncompressed_yuv_420_image, uncompressed_recovery_map,
+                                       metadata, dest, &jobQueue, &idwTable,
+                                       &recoveryLUT]() -> void {
+    const float hdr_ratio = metadata->rangeScalingFactor;
+    size_t width = uncompressed_yuv_420_image->width;
+    size_t height = uncompressed_yuv_420_image->height;
 
-  ColorTransformFn hdrOetf = nullptr;
-  switch (metadata->transferFunction) {
-    case JPEGR_TF_LINEAR:
-      hdrOetf = identityConversion;
-      break;
-    case JPEGR_TF_HLG:
-      hdrOetf = hlgOetf;
-      break;
-    case JPEGR_TF_PQ:
-      hdrOetf = pqOetf;
-      break;
-    case JPEGR_TF_UNSPECIFIED:
-      // Should be impossible to hit after input validation.
-      return ERROR_JPEGR_INVALID_TRANS_FUNC;
-  }
-
-  for (size_t y = 0; y < height; ++y) {
-    for (size_t x = 0; x < width; ++x) {
-      Color yuv_gamma_sdr = getYuv420Pixel(uncompressed_yuv_420_image, x, y);
-      Color rgb_gamma_sdr = srgbYuvToRgb(yuv_gamma_sdr);
-      Color rgb_sdr = srgbInvOetf(rgb_gamma_sdr);
-
-      // TODO: determine map scaling factor based on actual map dims
-      float recovery = sampleMap(uncompressed_recovery_map, kMapDimensionScaleFactor, x, y);
-      Color rgb_hdr = applyRecovery(rgb_sdr, recovery, metadata->rangeScalingFactor);
-
-      Color rgb_gamma_hdr = hdrOetf(rgb_hdr / metadata->rangeScalingFactor);
-      uint32_t rgba1010102 = colorToRgba1010102(rgb_gamma_hdr);
-
-      size_t pixel_idx =  x + y * width;
-      reinterpret_cast<uint32_t*>(dest->data)[pixel_idx] = rgba1010102;
+    ColorTransformFn hdrOetf = nullptr;
+    switch (metadata->transferFunction) {
+      case JPEGR_TF_LINEAR:
+        hdrOetf = identityConversion;
+        break;
+      case JPEGR_TF_HLG:
+#if USE_HLG_OETF_LUT
+        hdrOetf = hlgOetfLUT;
+#else
+        hdrOetf = hlgOetf;
+#endif
+        break;
+      case JPEGR_TF_PQ:
+#if USE_PQ_OETF_LUT
+        hdrOetf = pqOetfLUT;
+#else
+        hdrOetf = pqOetf;
+#endif
+        break;
+      case JPEGR_TF_UNSPECIFIED:
+        // Should be impossible to hit after input validation.
+        hdrOetf = identityConversion;
     }
+
+    size_t rowStart, rowEnd;
+    while (jobQueue.dequeueJob(rowStart, rowEnd)) {
+      for (size_t y = rowStart; y < rowEnd; ++y) {
+        for (size_t x = 0; x < width; ++x) {
+          Color yuv_gamma_sdr = getYuv420Pixel(uncompressed_yuv_420_image, x, y);
+          Color rgb_gamma_sdr = srgbYuvToRgb(yuv_gamma_sdr);
+#if USE_SRGB_INVOETF_LUT
+          Color rgb_sdr = srgbInvOetfLUT(rgb_gamma_sdr);
+#else
+          Color rgb_sdr = srgbInvOetf(rgb_gamma_sdr);
+#endif
+          float recovery;
+          // TODO: determine map scaling factor based on actual map dims
+          size_t map_scale_factor = kMapDimensionScaleFactor;
+          // TODO: If map_scale_factor is guaranteed to be an integer, then remove the following.
+          // Currently map_scale_factor is of type size_t, but it could be changed to a float
+          // later.
+          if (map_scale_factor != floorf(map_scale_factor)) {
+            recovery = sampleMap(uncompressed_recovery_map, map_scale_factor, x, y);
+          } else {
+            recovery = sampleMap(uncompressed_recovery_map, map_scale_factor, x, y,
+                                idwTable);
+          }
+#if USE_APPLY_RECOVERY_LUT
+          Color rgb_hdr = applyRecoveryLUT(rgb_sdr, recovery, recoveryLUT);
+#else
+          Color rgb_hdr = applyRecovery(rgb_sdr, recovery, hdr_ratio);
+#endif
+          Color rgb_gamma_hdr = hdrOetf(rgb_hdr / metadata->rangeScalingFactor);
+          uint32_t rgba1010102 = colorToRgba1010102(rgb_gamma_hdr);
+
+          size_t pixel_idx = x + y * width;
+          reinterpret_cast<uint32_t*>(dest->data)[pixel_idx] = rgba1010102;
+        }
+      }
+    }
+  };
+
+  const int threads = std::clamp(GetCPUCoreCount(), 1, 4);
+  std::vector<std::thread> workers;
+  for (int th = 0; th < threads - 1; th++) {
+    workers.push_back(std::thread(applyRecMap));
   }
+  const int rowStep = threads == 1 ? uncompressed_yuv_420_image->height : kJobSzInRows;
+  for (int rowStart = 0; rowStart < uncompressed_yuv_420_image->height;) {
+    int rowEnd = std::min(rowStart + rowStep, uncompressed_yuv_420_image->height);
+    jobQueue.enqueueJob(rowStart, rowEnd);
+    rowStart = rowEnd;
+  }
+  jobQueue.markQueueForEnd();
+  applyRecMap();
+  std::for_each(workers.begin(), workers.end(), [](std::thread& t) { t.join(); });
   return NO_ERROR;
 }
 
diff --git a/libs/jpegrecoverymap/recoverymapmath.cpp b/libs/jpegrecoverymap/recoverymapmath.cpp
index 9ed2949..4f21ac6 100644
--- a/libs/jpegrecoverymap/recoverymapmath.cpp
+++ b/libs/jpegrecoverymap/recoverymapmath.cpp
@@ -15,14 +15,129 @@
  */
 
 #include <cmath>
-
+#include <vector>
 #include <jpegrecoverymap/recoverymapmath.h>
 
 namespace android::recoverymap {
 
+constexpr size_t kPqOETFPrecision = 10;
+constexpr size_t kPqOETFNumEntries = 1 << kPqOETFPrecision;
+
+static const std::vector<float> kPqOETF = [] {
+    std::vector<float> result;
+    float increment = 1.0 / kPqOETFNumEntries;
+    float value = 0.0f;
+    for (int idx = 0; idx < kPqOETFNumEntries; idx++, value += increment) {
+      result.push_back(pqOetf(value));
+    }
+    return result;
+}();
+
+constexpr size_t kPqInvOETFPrecision = 10;
+constexpr size_t kPqInvOETFNumEntries = 1 << kPqInvOETFPrecision;
+
+static const std::vector<float> kPqInvOETF = [] {
+    std::vector<float> result;
+    float increment = 1.0 / kPqInvOETFNumEntries;
+    float value = 0.0f;
+    for (int idx = 0; idx < kPqInvOETFNumEntries; idx++, value += increment) {
+      result.push_back(pqInvOetf(value));
+    }
+    return result;
+}();
+
+constexpr size_t kHlgOETFPrecision = 10;
+constexpr size_t kHlgOETFNumEntries = 1 << kHlgOETFPrecision;
+
+static const std::vector<float> kHlgOETF = [] {
+    std::vector<float> result;
+    float increment = 1.0 / kHlgOETFNumEntries;
+    float value = 0.0f;
+    for (int idx = 0; idx < kHlgOETFNumEntries; idx++, value += increment) {
+      result.push_back(hlgOetf(value));
+    }
+    return result;
+}();
+
+constexpr size_t kHlgInvOETFPrecision = 10;
+constexpr size_t kHlgInvOETFNumEntries = 1 << kHlgInvOETFPrecision;
+
+static const std::vector<float> kHlgInvOETF = [] {
+    std::vector<float> result;
+    float increment = 1.0 / kHlgInvOETFNumEntries;
+    float value = 0.0f;
+    for (int idx = 0; idx < kHlgInvOETFNumEntries; idx++, value += increment) {
+      result.push_back(hlgInvOetf(value));
+    }
+    return result;
+}();
+
+constexpr size_t kSRGBInvOETFPrecision = 10;
+constexpr size_t kSRGBInvOETFNumEntries = 1 << kSRGBInvOETFPrecision;
+static const std::vector<float> kSRGBInvOETF = [] {
+    std::vector<float> result;
+    float increment = 1.0 / kSRGBInvOETFNumEntries;
+    float value = 0.0f;
+    for (int idx = 0; idx < kSRGBInvOETFNumEntries; idx++, value += increment) {
+      result.push_back(srgbInvOetf(value));
+    }
+    return result;
+}();
+
+// Use Shepard's method for inverse distance weighting. For more information:
+// en.wikipedia.org/wiki/Inverse_distance_weighting#Shepard's_method
+
+float ShepardsIDW::euclideanDistance(float x1, float x2, float y1, float y2) {
+  return sqrt(((y2 - y1) * (y2 - y1)) + (x2 - x1) * (x2 - x1));
+}
+
+void ShepardsIDW::fillShepardsIDW(float *weights, int incR, int incB) {
+  for (int y = 0; y < mMapScaleFactor; y++) {
+    for (int x = 0; x < mMapScaleFactor; x++) {
+      float pos_x = ((float)x) / mMapScaleFactor;
+      float pos_y = ((float)y) / mMapScaleFactor;
+      int curr_x = floor(pos_x);
+      int curr_y = floor(pos_y);
+      int next_x = curr_x + incR;
+      int next_y = curr_y + incB;
+      float e1_distance = euclideanDistance(pos_x, curr_x, pos_y, curr_y);
+      int index = y * mMapScaleFactor * 4 + x * 4;
+      if (e1_distance == 0) {
+        weights[index++] = 1.f;
+        weights[index++] = 0.f;
+        weights[index++] = 0.f;
+        weights[index++] = 0.f;
+      } else {
+        float e1_weight = 1.f / e1_distance;
+
+        float e2_distance = euclideanDistance(pos_x, curr_x, pos_y, next_y);
+        float e2_weight = 1.f / e2_distance;
+
+        float e3_distance = euclideanDistance(pos_x, next_x, pos_y, curr_y);
+        float e3_weight = 1.f / e3_distance;
+
+        float e4_distance = euclideanDistance(pos_x, next_x, pos_y, next_y);
+        float e4_weight = 1.f / e4_distance;
+
+        float total_weight = e1_weight + e2_weight + e3_weight + e4_weight;
+
+        weights[index++] = e1_weight / total_weight;
+        weights[index++] = e2_weight / total_weight;
+        weights[index++] = e3_weight / total_weight;
+        weights[index++] = e4_weight / total_weight;
+      }
+    }
+  }
+}
+
 ////////////////////////////////////////////////////////////////////////////////
 // sRGB transformations
 
+static const float kMaxPixelFloat = 1.0f;
+static float clampPixelFloat(float value) {
+    return (value < 0.0f) ? 0.0f : (value > kMaxPixelFloat) ? kMaxPixelFloat : value;
+}
+
 // See IEC 61966-2-1, Equation F.7.
 static const float kSrgbR = 0.2126f, kSrgbG = 0.7152f, kSrgbB = 0.0722f;
 
@@ -34,9 +149,9 @@
 static const float kSrgbRCr = 1.402f, kSrgbGCb = 0.34414f, kSrgbGCr = 0.71414f, kSrgbBCb = 1.772f;
 
 Color srgbYuvToRgb(Color e_gamma) {
-  return {{{ e_gamma.y + kSrgbRCr * e_gamma.v,
-             e_gamma.y - kSrgbGCb * e_gamma.u - kSrgbGCr * e_gamma.v,
-             e_gamma.y + kSrgbBCb * e_gamma.u }}};
+  return {{{ clampPixelFloat(e_gamma.y + kSrgbRCr * e_gamma.v),
+             clampPixelFloat(e_gamma.y - kSrgbGCb * e_gamma.u - kSrgbGCr * e_gamma.v),
+             clampPixelFloat(e_gamma.y + kSrgbBCb * e_gamma.u) }}};
 }
 
 // See ECMA TR/98, Section 7.
@@ -65,6 +180,19 @@
              srgbInvOetf(e_gamma.b) }}};
 }
 
+// See IEC 61966-2-1, Equations F.5 and F.6.
+float srgbInvOetfLUT(float e_gamma) {
+  uint32_t value = static_cast<uint32_t>(e_gamma * kSRGBInvOETFNumEntries);
+  //TODO() : Remove once conversion modules have appropriate clamping in place
+  value = CLIP3(value, 0, kSRGBInvOETFNumEntries - 1);
+  return kSRGBInvOETF[value];
+}
+
+Color srgbInvOetfLUT(Color e_gamma) {
+  return {{{ srgbInvOetfLUT(e_gamma.r),
+             srgbInvOetfLUT(e_gamma.g),
+             srgbInvOetfLUT(e_gamma.b) }}};
+}
 
 ////////////////////////////////////////////////////////////////////////////////
 // Display-P3 transformations
@@ -122,9 +250,9 @@
 static const float kBt2100GCr = kBt2100R * kBt2100Cr / kBt2100G;
 
 Color bt2100YuvToRgb(Color e_gamma) {
-  return {{{ e_gamma.y + kBt2100Cr * e_gamma.v,
-             e_gamma.y - kBt2100GCb * e_gamma.u - kBt2100GCr * e_gamma.v,
-             e_gamma.y + kBt2100Cb * e_gamma.u }}};
+  return {{{ clampPixelFloat(e_gamma.y + kBt2100Cr * e_gamma.v),
+             clampPixelFloat(e_gamma.y - kBt2100GCb * e_gamma.u - kBt2100GCr * e_gamma.v),
+             clampPixelFloat(e_gamma.y + kBt2100Cb * e_gamma.u) }}};
 }
 
 // See ITU-R BT.2100-2, Table 5, HLG Reference OETF.
@@ -142,6 +270,18 @@
   return {{{ hlgOetf(e.r), hlgOetf(e.g), hlgOetf(e.b) }}};
 }
 
+float hlgOetfLUT(float e) {
+  uint32_t value = static_cast<uint32_t>(e * kHlgOETFNumEntries);
+  //TODO() : Remove once conversion modules have appropriate clamping in place
+  value = CLIP3(value, 0, kHlgOETFNumEntries - 1);
+
+  return kHlgOETF[value];
+}
+
+Color hlgOetfLUT(Color e) {
+  return {{{ hlgOetfLUT(e.r), hlgOetfLUT(e.g), hlgOetfLUT(e.b) }}};
+}
+
 // See ITU-R BT.2100-2, Table 5, HLG Reference EOTF.
 float hlgInvOetf(float e_gamma) {
   if (e_gamma <= 0.5f) {
@@ -157,6 +297,20 @@
              hlgInvOetf(e_gamma.b) }}};
 }
 
+float hlgInvOetfLUT(float e_gamma) {
+  uint32_t value = static_cast<uint32_t>(e_gamma * kHlgInvOETFNumEntries);
+  //TODO() : Remove once conversion modules have appropriate clamping in place
+  value = CLIP3(value, 0, kHlgInvOETFNumEntries - 1);
+
+  return kHlgInvOETF[value];
+}
+
+Color hlgInvOetfLUT(Color e_gamma) {
+  return {{{ hlgInvOetfLUT(e_gamma.r),
+             hlgInvOetfLUT(e_gamma.g),
+             hlgInvOetfLUT(e_gamma.b) }}};
+}
+
 // See ITU-R BT.2100-2, Table 4, Reference PQ OETF.
 static const float kPqM1 = 2610.0f / 16384.0f, kPqM2 = 2523.0f / 4096.0f * 128.0f;
 static const float kPqC1 = 3424.0f / 4096.0f, kPqC2 = 2413.0f / 4096.0f * 32.0f,
@@ -172,6 +326,18 @@
   return {{{ pqOetf(e.r), pqOetf(e.g), pqOetf(e.b) }}};
 }
 
+float pqOetfLUT(float e) {
+  uint32_t value = static_cast<uint32_t>(e * kPqOETFNumEntries);
+  //TODO() : Remove once conversion modules have appropriate clamping in place
+  value = CLIP3(value, 0, kPqOETFNumEntries - 1);
+
+  return kPqOETF[value];
+}
+
+Color pqOetfLUT(Color e) {
+  return {{{ pqOetfLUT(e.r), pqOetfLUT(e.g), pqOetfLUT(e.b) }}};
+}
+
 // Derived from the inverse of the Reference PQ OETF.
 static const float kPqInvA = 128.0f, kPqInvB = 107.0f, kPqInvC = 2413.0f, kPqInvD = 2392.0f,
                    kPqInvE = 6.2773946361f, kPqInvF = 0.0126833f;
@@ -192,6 +358,20 @@
              pqInvOetf(e_gamma.b) }}};
 }
 
+float pqInvOetfLUT(float e_gamma) {
+  uint32_t value = static_cast<uint32_t>(e_gamma * kPqInvOETFNumEntries);
+  //TODO() : Remove once conversion modules have appropriate clamping in place
+  value = CLIP3(value, 0, kPqInvOETFNumEntries - 1);
+
+  return kPqInvOETF[value];
+}
+
+Color pqInvOetfLUT(Color e_gamma) {
+  return {{{ pqInvOetfLUT(e_gamma.r),
+             pqInvOetfLUT(e_gamma.g),
+             pqInvOetfLUT(e_gamma.b) }}};
+}
+
 
 ////////////////////////////////////////////////////////////////////////////////
 // Color conversions
@@ -294,15 +474,14 @@
   return static_cast<uint8_t>(log2(gain) / log2(hdr_ratio) * 127.5f  + 127.5f);
 }
 
-static float applyRecovery(float e, float recovery, float hdr_ratio) {
-  if (e <= 0.0f) return 0.0f;
-  return exp2(log2(e) + recovery * log2(hdr_ratio));
+Color applyRecovery(Color e, float recovery, float hdr_ratio) {
+  float recoveryFactor = pow(hdr_ratio, recovery);
+  return e * recoveryFactor;
 }
 
-Color applyRecovery(Color e, float recovery, float hdr_ratio) {
-  return {{{ applyRecovery(e.r, recovery, hdr_ratio),
-             applyRecovery(e.g, recovery, hdr_ratio),
-             applyRecovery(e.b, recovery, hdr_ratio) }}};
+Color applyRecoveryLUT(Color e, float recovery, RecoveryLUT& recoveryLUT) {
+  float recoveryFactor = recoveryLUT.getRecoveryFactor(recovery);
+  return e * recoveryFactor;
 }
 
 Color getYuv420Pixel(jr_uncompressed_ptr image, size_t x, size_t y) {
@@ -378,6 +557,7 @@
   return sqrt(pow(x_diff, 2.0f) + pow(y_diff, 2.0f));
 }
 
+// TODO: If map_scale_factor is guaranteed to be an integer, then remove the following.
 float sampleMap(jr_uncompressed_ptr map, size_t map_scale_factor, size_t x, size_t y) {
   float x_map = static_cast<float>(x) / static_cast<float>(map_scale_factor);
   float y_map = static_cast<float>(y) / static_cast<float>(map_scale_factor);
@@ -427,6 +607,39 @@
        + e4 * (e4_weight / total_weight);
 }
 
+float sampleMap(jr_uncompressed_ptr map, size_t map_scale_factor, size_t x, size_t y,
+                ShepardsIDW& weightTables) {
+  // TODO: If map_scale_factor is guaranteed to be an integer power of 2, then optimize the
+  // following by computing log2(map_scale_factor) once and then using >> log2(map_scale_factor)
+  int x_lower = x / map_scale_factor;
+  int x_upper = x_lower + 1;
+  int y_lower = y / map_scale_factor;
+  int y_upper = y_lower + 1;
+
+  x_lower = std::min(x_lower, map->width - 1);
+  x_upper = std::min(x_upper, map->width - 1);
+  y_lower = std::min(y_lower, map->height - 1);
+  y_upper = std::min(y_upper, map->height - 1);
+
+  float e1 = mapUintToFloat(reinterpret_cast<uint8_t*>(map->data)[x_lower + y_lower * map->width]);
+  float e2 = mapUintToFloat(reinterpret_cast<uint8_t*>(map->data)[x_lower + y_upper * map->width]);
+  float e3 = mapUintToFloat(reinterpret_cast<uint8_t*>(map->data)[x_upper + y_lower * map->width]);
+  float e4 = mapUintToFloat(reinterpret_cast<uint8_t*>(map->data)[x_upper + y_upper * map->width]);
+
+  // TODO: If map_scale_factor is guaranteed to be an integer power of 2, then optimize the
+  // following by using & (map_scale_factor - 1)
+  int offset_x = x % map_scale_factor;
+  int offset_y = y % map_scale_factor;
+
+  float* weights = weightTables.mWeights;
+  if (x_lower == x_upper && y_lower == y_upper) weights = weightTables.mWeightsC;
+  else if (x_lower == x_upper) weights = weightTables.mWeightsNR;
+  else if (y_lower == y_upper) weights = weightTables.mWeightsNB;
+  weights += offset_y * map_scale_factor * 4 + offset_x * 4;
+
+  return e1 * weights[0] + e2 * weights[1] + e3 * weights[2] + e4 * weights[3];
+}
+
 uint32_t colorToRgba1010102(Color e_gamma) {
   return (0x3ff & static_cast<uint32_t>(e_gamma.r * 1023.0f))
        | ((0x3ff & static_cast<uint32_t>(e_gamma.g * 1023.0f)) << 10)
diff --git a/libs/jpegrecoverymap/tests/recoverymap_test.cpp b/libs/jpegrecoverymap/tests/recoverymap_test.cpp
index 8ff12fb..dfab76a 100644
--- a/libs/jpegrecoverymap/tests/recoverymap_test.cpp
+++ b/libs/jpegrecoverymap/tests/recoverymap_test.cpp
@@ -15,6 +15,7 @@
  */
 
 #include <jpegrecoverymap/recoverymap.h>
+#include <jpegrecoverymap/recoverymapmath.h>
 #include <jpegrecoverymap/recoverymaputils.h>
 #include <fcntl.h>
 #include <fstream>
@@ -30,6 +31,7 @@
 
 #define SAVE_ENCODING_RESULT true
 #define SAVE_DECODING_RESULT true
+#define SAVE_INPUT_RGBA true
 
 namespace android::recoverymap {
 
@@ -104,13 +106,22 @@
   metadata_expected.transferFunction = JPEGR_TF_HLG;
   metadata_expected.rangeScalingFactor = 1.25;
   int length_expected = 1000;
+  const std::string nameSpace = "http://ns.adobe.com/xap/1.0/\0";
+  const int nameSpaceLength = nameSpace.size() + 1;  // need to count the null terminator
+
   std::string xmp = generateXmp(1000, metadata_expected);
 
+  std::vector<uint8_t> xmpData;
+  xmpData.reserve(nameSpaceLength + xmp.size());
+  xmpData.insert(xmpData.end(), reinterpret_cast<const uint8_t*>(nameSpace.c_str()),
+                  reinterpret_cast<const uint8_t*>(nameSpace.c_str()) + nameSpaceLength);
+  xmpData.insert(xmpData.end(), reinterpret_cast<const uint8_t*>(xmp.c_str()),
+                  reinterpret_cast<const uint8_t*>(xmp.c_str()) + xmp.size());
+
   jpegr_metadata metadata_read;
-  EXPECT_TRUE(getMetadataFromXMP(reinterpret_cast<uint8_t*>(xmp[0]), xmp.size(), &metadata_read));
+  EXPECT_TRUE(getMetadataFromXMP(xmpData.data(), xmpData.size(), &metadata_read));
   ASSERT_EQ(metadata_expected.transferFunction, metadata_read.transferFunction);
   ASSERT_EQ(metadata_expected.rangeScalingFactor, metadata_read.rangeScalingFactor);
-
 }
 
 /* Test Encode API-0 and decode */
@@ -304,6 +315,29 @@
   mRawP010Image.height = TEST_IMAGE_HEIGHT;
   mRawP010Image.colorGamut = jpegr_color_gamut::JPEGR_COLORGAMUT_BT2100;
 
+  if (SAVE_INPUT_RGBA) {
+    size_t rgbaSize = mRawP010Image.width * mRawP010Image.height * sizeof(uint32_t);
+    uint32_t *data = (uint32_t *)malloc(rgbaSize);
+
+    for (size_t y = 0; y < mRawP010Image.height; ++y) {
+      for (size_t x = 0; x < mRawP010Image.width; ++x) {
+        Color hdr_yuv_gamma = getP010Pixel(&mRawP010Image, x, y);
+        Color hdr_rgb_gamma = bt2100YuvToRgb(hdr_yuv_gamma);
+        uint32_t rgba1010102 = colorToRgba1010102(hdr_rgb_gamma);
+        size_t pixel_idx =  x + y * mRawP010Image.width;
+        reinterpret_cast<uint32_t*>(data)[pixel_idx] = rgba1010102;
+      }
+    }
+
+    // Output image data to file
+    std::string filePath = "/sdcard/Documents/input_from_p010.rgb10";
+    std::ofstream imageFile(filePath.c_str(), std::ofstream::binary);
+    if (!imageFile.is_open()) {
+      ALOGE("%s: Unable to create file %s", __FUNCTION__, filePath.c_str());
+    }
+    imageFile.write((const char*)data, rgbaSize);
+    free(data);
+  }
   if (!loadFile(JPEG_IMAGE, mJpegImage.data, &mJpegImage.length)) {
     FAIL() << "Load file " << JPEG_IMAGE << " failed";
   }
diff --git a/libs/jpegrecoverymap/tests/recoverymapmath_test.cpp b/libs/jpegrecoverymap/tests/recoverymapmath_test.cpp
index f8dd490..1d522d1 100644
--- a/libs/jpegrecoverymap/tests/recoverymapmath_test.cpp
+++ b/libs/jpegrecoverymap/tests/recoverymapmath_test.cpp
@@ -517,6 +517,65 @@
   EXPECT_RGB_NEAR(pqInvOetf(e_gamma), e);
 }
 
+TEST_F(RecoveryMapMathTest, PqInvOetfLUT) {
+    float increment = 1.0 / 1024.0;
+    float value = 0.0f;
+    for (int idx = 0; idx < 1024; idx++, value += increment) {
+      EXPECT_FLOAT_EQ(pqInvOetf(value), pqInvOetfLUT(value));
+    }
+}
+
+TEST_F(RecoveryMapMathTest, HlgInvOetfLUT) {
+    float increment = 1.0 / 1024.0;
+    float value = 0.0f;
+    for (int idx = 0; idx < 1024; idx++, value += increment) {
+      EXPECT_FLOAT_EQ(hlgInvOetf(value), hlgInvOetfLUT(value));
+    }
+}
+
+TEST_F(RecoveryMapMathTest, pqOetfLUT) {
+    float increment = 1.0 / 1024.0;
+    float value = 0.0f;
+    for (int idx = 0; idx < 1024; idx++, value += increment) {
+      EXPECT_FLOAT_EQ(pqOetf(value), pqOetfLUT(value));
+    }
+}
+
+TEST_F(RecoveryMapMathTest, hlgOetfLUT) {
+    float increment = 1.0 / 1024.0;
+    float value = 0.0f;
+    for (int idx = 0; idx < 1024; idx++, value += increment) {
+      EXPECT_FLOAT_EQ(hlgOetf(value), hlgOetfLUT(value));
+    }
+}
+
+TEST_F(RecoveryMapMathTest, srgbInvOetfLUT) {
+    float increment = 1.0 / 1024.0;
+    float value = 0.0f;
+    for (int idx = 0; idx < 1024; idx++, value += increment) {
+      EXPECT_FLOAT_EQ(srgbInvOetf(value), srgbInvOetfLUT(value));
+    }
+}
+
+TEST_F(RecoveryMapMathTest, applyRecoveryLUT) {
+  float increment = 2.0 / kRecoveryFactorNumEntries;
+  for (float hdrRatio = 1.0f; hdrRatio <= 10.0f; hdrRatio += 1.0f)  {
+    RecoveryLUT recoveryLUT(hdrRatio);
+    for (float value = -1.0f; value <= -1.0f; value += increment) {
+      EXPECT_RGB_NEAR(applyRecovery(RgbBlack(), value, hdrRatio),
+                      applyRecoveryLUT(RgbBlack(), value, recoveryLUT));
+      EXPECT_RGB_NEAR(applyRecovery(RgbWhite(), value, hdrRatio),
+                      applyRecoveryLUT(RgbWhite(), value, recoveryLUT));
+      EXPECT_RGB_NEAR(applyRecovery(RgbRed(), value, hdrRatio),
+                      applyRecoveryLUT(RgbRed(), value, recoveryLUT));
+      EXPECT_RGB_NEAR(applyRecovery(RgbGreen(), value, hdrRatio),
+                      applyRecoveryLUT(RgbGreen(), value, recoveryLUT));
+      EXPECT_RGB_NEAR(applyRecovery(RgbBlue(), value, hdrRatio),
+                      applyRecoveryLUT(RgbBlue(), value, recoveryLUT));
+    }
+  }
+}
+
 TEST_F(RecoveryMapMathTest, PqTransferFunctionRoundtrip) {
   EXPECT_FLOAT_EQ(pqInvOetf(pqOetf(0.0f)), 0.0f);
   EXPECT_NEAR(pqInvOetf(pqOetf(0.01f)), 0.01f, ComparisonEpsilon());
@@ -699,6 +758,7 @@
   float (*values)[4] = MapValues();
 
   static const size_t kMapScaleFactor = 2;
+  ShepardsIDW idwTable(kMapScaleFactor);
   for (size_t y = 0; y < 4 * kMapScaleFactor; ++y) {
     for (size_t x = 0; x < 4 * kMapScaleFactor; ++x) {
       size_t x_base = x / kMapScaleFactor;
@@ -725,7 +785,7 @@
       // Instead of reimplementing the sampling algorithm, confirm that the
       // sample output is within the range of the min and max of the nearest
       // points.
-      EXPECT_THAT(sampleMap(&image, kMapScaleFactor, x, y),
+      EXPECT_THAT(sampleMap(&image, kMapScaleFactor, x, y, idwTable),
                   testing::AllOf(testing::Ge(min), testing::Le(max)));
     }
   }
diff --git a/libs/nativewindow/include/android/hardware_buffer.h b/libs/nativewindow/include/android/hardware_buffer.h
index b2e8bea..85a5249 100644
--- a/libs/nativewindow/include/android/hardware_buffer.h
+++ b/libs/nativewindow/include/android/hardware_buffer.h
@@ -311,6 +311,16 @@
      */
     AHARDWAREBUFFER_USAGE_GPU_MIPMAP_COMPLETE   = 1UL << 26,
 
+    /**
+     * Usage: The buffer is used for front-buffer rendering. When
+     * front-buffering rendering is specified, different usages may adjust their
+     * behavior as a result. For example, when used as GPU_COLOR_OUTPUT the buffer
+     * will behave similar to a single-buffered window. When used with
+     * COMPOSER_OVERLAY, the system will try to prioritize the buffer receiving
+     * an overlay plane & avoid caching it in intermediate composition buffers.
+     */
+    AHARDWAREBUFFER_USAGE_FRONT_BUFFER = 1UL << 32,
+
     AHARDWAREBUFFER_USAGE_VENDOR_0  = 1ULL << 28,
     AHARDWAREBUFFER_USAGE_VENDOR_1  = 1ULL << 29,
     AHARDWAREBUFFER_USAGE_VENDOR_2  = 1ULL << 30,
diff --git a/opengl/libs/EGL/Loader.cpp b/opengl/libs/EGL/Loader.cpp
index dd14bcf..34b1251 100644
--- a/opengl/libs/EGL/Loader.cpp
+++ b/opengl/libs/EGL/Loader.cpp
@@ -502,7 +502,6 @@
     void* so = do_android_dlopen_ext(name.c_str(), RTLD_LOCAL | RTLD_NOW, &dlextinfo);
 
     if (so) {
-        ALOGD("dlopen_ext from APK (%s) success at %p", name.c_str(), so);
         return so;
     } else {
         ALOGE("dlopen_ext(\"%s\") failed: %s", name.c_str(), dlerror());
diff --git a/services/automotive/display/Android.bp b/services/automotive/display/Android.bp
index 72bd292..614a78e 100644
--- a/services/automotive/display/Android.bp
+++ b/services/automotive/display/Android.bp
@@ -53,4 +53,6 @@
     vintf_fragments: [
         "manifest_android.frameworks.automotive.display@1.0.xml",
     ],
+
+    system_ext_specific: true,
 }
diff --git a/services/automotive/display/android.frameworks.automotive.display@1.0-service.rc b/services/automotive/display/android.frameworks.automotive.display@1.0-service.rc
index 5c7f344..ea1077a 100644
--- a/services/automotive/display/android.frameworks.automotive.display@1.0-service.rc
+++ b/services/automotive/display/android.frameworks.automotive.display@1.0-service.rc
@@ -1,4 +1,4 @@
-service automotive_display /system/bin/android.frameworks.automotive.display@1.0-service
+service automotive_display /system_ext/bin/android.frameworks.automotive.display@1.0-service
     class hal
     user graphics
     group automotive_evs
diff --git a/services/inputflinger/dispatcher/InputDispatcher.cpp b/services/inputflinger/dispatcher/InputDispatcher.cpp
index 906bb1b..37a451b 100644
--- a/services/inputflinger/dispatcher/InputDispatcher.cpp
+++ b/services/inputflinger/dispatcher/InputDispatcher.cpp
@@ -554,6 +554,68 @@
     return std::nullopt;
 }
 
+/**
+ * Compare the old touch state to the new touch state, and generate the corresponding touched
+ * windows (== input targets).
+ * If a window had the hovering pointer, but now it doesn't, produce HOVER_EXIT for that window.
+ * If the pointer just entered the new window, produce HOVER_ENTER.
+ * For pointers remaining in the window, produce HOVER_MOVE.
+ */
+std::vector<TouchedWindow> getHoveringWindowsLocked(const TouchState* oldState,
+                                                    const TouchState& newTouchState,
+                                                    const MotionEntry& entry) {
+    std::vector<TouchedWindow> out;
+    const int32_t maskedAction = MotionEvent::getActionMasked(entry.action);
+    if (maskedAction != AMOTION_EVENT_ACTION_HOVER_ENTER &&
+        maskedAction != AMOTION_EVENT_ACTION_HOVER_MOVE &&
+        maskedAction != AMOTION_EVENT_ACTION_HOVER_EXIT) {
+        // Not a hover event - don't need to do anything
+        return out;
+    }
+
+    // We should consider all hovering pointers here. But for now, just use the first one
+    const int32_t pointerId = entry.pointerProperties[0].id;
+
+    std::set<sp<WindowInfoHandle>> oldWindows;
+    if (oldState != nullptr) {
+        oldWindows = oldState->getWindowsWithHoveringPointer(entry.deviceId, pointerId);
+    }
+
+    std::set<sp<WindowInfoHandle>> newWindows =
+            newTouchState.getWindowsWithHoveringPointer(entry.deviceId, pointerId);
+
+    // If the pointer is no longer in the new window set, send HOVER_EXIT.
+    for (const sp<WindowInfoHandle>& oldWindow : oldWindows) {
+        if (newWindows.find(oldWindow) == newWindows.end()) {
+            TouchedWindow touchedWindow;
+            touchedWindow.windowHandle = oldWindow;
+            touchedWindow.targetFlags = InputTarget::Flags::DISPATCH_AS_HOVER_EXIT;
+            touchedWindow.pointerIds.markBit(pointerId);
+            out.push_back(touchedWindow);
+        }
+    }
+
+    for (const sp<WindowInfoHandle>& newWindow : newWindows) {
+        TouchedWindow touchedWindow;
+        touchedWindow.windowHandle = newWindow;
+        if (oldWindows.find(newWindow) == oldWindows.end()) {
+            // Any windows that have this pointer now, and didn't have it before, should get
+            // HOVER_ENTER
+            touchedWindow.targetFlags = InputTarget::Flags::DISPATCH_AS_HOVER_ENTER;
+        } else {
+            // This pointer was already sent to the window. Use ACTION_HOVER_MOVE.
+            LOG_ALWAYS_FATAL_IF(maskedAction != AMOTION_EVENT_ACTION_HOVER_MOVE);
+            touchedWindow.targetFlags = InputTarget::Flags::DISPATCH_AS_IS;
+        }
+        touchedWindow.pointerIds.markBit(pointerId);
+        if (canReceiveForegroundTouches(*newWindow->getInfo())) {
+            touchedWindow.targetFlags |= InputTarget::Flags::FOREGROUND;
+        }
+        out.push_back(touchedWindow);
+    }
+    return out;
+}
+
 } // namespace
 
 // --- InputDispatcher ---
@@ -2089,8 +2151,6 @@
 
     // Update the touch state as needed based on the properties of the touch event.
     outInjectionResult = InputEventInjectionResult::PENDING;
-    sp<WindowInfoHandle> newHoverWindowHandle(mLastHoverWindowHandle);
-    sp<WindowInfoHandle> newTouchedWindowHandle;
 
     // Copy current touch state into tempTouchState.
     // This state will be used to update mTouchStatesByDisplay at the end of this function.
@@ -2123,7 +2183,7 @@
             outInjectionResult = InputEventInjectionResult::FAILED;
             return touchedWindows; // wrong device
         }
-        tempTouchState.reset();
+        tempTouchState.clearWindowsWithoutPointers();
         tempTouchState.deviceId = entry.deviceId;
         tempTouchState.source = entry.source;
         isSplit = false;
@@ -2136,14 +2196,21 @@
         return touchedWindows; // wrong device
     }
 
+    if (isHoverAction) {
+        // For hover actions, we will treat 'tempTouchState' as a new state, so let's erase
+        // all of the existing hovering pointers and recompute.
+        tempTouchState.clearHoveringPointers();
+    }
+
     if (newGesture || (isSplit && maskedAction == AMOTION_EVENT_ACTION_POINTER_DOWN)) {
         /* Case 1: New splittable pointer going down, or need target for hover or scroll. */
         const auto [x, y] = resolveTouchedPosition(entry);
         const int32_t pointerIndex = getMotionEventActionPointerIndex(action);
         const bool isDown = maskedAction == AMOTION_EVENT_ACTION_DOWN;
         const bool isStylus = isPointerFromStylus(entry, pointerIndex);
-        newTouchedWindowHandle = findTouchedWindowAtLocked(displayId, x, y, &tempTouchState,
-                                                           isStylus, isDown /*addOutsideTargets*/);
+        sp<WindowInfoHandle> newTouchedWindowHandle =
+                findTouchedWindowAtLocked(displayId, x, y, &tempTouchState, isStylus,
+                                          isDown /*addOutsideTargets*/);
 
         // Handle the case where we did not find a window.
         if (newTouchedWindowHandle == nullptr) {
@@ -2178,15 +2245,6 @@
             isSplit = !isFromMouse;
         }
 
-        // Update hover state.
-        if (newTouchedWindowHandle != nullptr) {
-            if (maskedAction == AMOTION_EVENT_ACTION_HOVER_EXIT) {
-                newHoverWindowHandle = nullptr;
-            } else if (isHoverAction) {
-                newHoverWindowHandle = newTouchedWindowHandle;
-            }
-        }
-
         std::vector<sp<WindowInfoHandle>> newTouchedWindows =
                 findTouchedSpyWindowsAtLocked(displayId, x, y, isStylus);
         if (newTouchedWindowHandle != nullptr) {
@@ -2206,6 +2264,18 @@
                 continue;
             }
 
+            if (isHoverAction) {
+                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);
+                }
+            }
+
             // Set target flags.
             ftl::Flags<InputTarget::Flags> targetFlags = InputTarget::Flags::DISPATCH_AS_IS;
 
@@ -2225,7 +2295,9 @@
 
             // Update the temporary touch state.
             BitSet32 pointerIds;
-            pointerIds.markBit(entry.pointerProperties[pointerIndex].id);
+            if (!isHoverAction) {
+                pointerIds.markBit(entry.pointerProperties[pointerIndex].id);
+            }
 
             tempTouchState.addOrUpdateWindow(windowHandle, targetFlags, pointerIds,
                                              entry.eventTime);
@@ -2287,7 +2359,7 @@
             const bool isStylus = isPointerFromStylus(entry, 0 /*pointerIndex*/);
             sp<WindowInfoHandle> oldTouchedWindowHandle =
                     tempTouchState.getFirstForegroundWindowHandle();
-            newTouchedWindowHandle =
+            sp<WindowInfoHandle> newTouchedWindowHandle =
                     findTouchedWindowAtLocked(displayId, x, y, &tempTouchState, isStylus);
 
             // Verify targeted injection.
@@ -2362,36 +2434,11 @@
     }
 
     // Update dispatching for hover enter and exit.
-    if (newHoverWindowHandle != mLastHoverWindowHandle) {
-        // Let the previous window know that the hover sequence is over, unless we already did
-        // it when dispatching it as is to newTouchedWindowHandle.
-        if (mLastHoverWindowHandle != nullptr &&
-            (maskedAction != AMOTION_EVENT_ACTION_HOVER_EXIT ||
-             mLastHoverWindowHandle != newTouchedWindowHandle)) {
-            if (DEBUG_HOVER) {
-                ALOGD("Sending hover exit event to window %s.",
-                      mLastHoverWindowHandle->getName().c_str());
-            }
-            tempTouchState.addOrUpdateWindow(mLastHoverWindowHandle,
-                                             InputTarget::Flags::DISPATCH_AS_HOVER_EXIT,
-                                             BitSet32(0));
-        }
-
-        // Let the new window know that the hover sequence is starting, unless we already did it
-        // when dispatching it as is to newTouchedWindowHandle.
-        if (newHoverWindowHandle != nullptr &&
-            (maskedAction != AMOTION_EVENT_ACTION_HOVER_ENTER ||
-             newHoverWindowHandle != newTouchedWindowHandle)) {
-            if (DEBUG_HOVER) {
-                ALOGD("Sending hover enter event to window %s.",
-                      newHoverWindowHandle->getName().c_str());
-            }
-            tempTouchState.addOrUpdateWindow(newHoverWindowHandle,
-                                             InputTarget::Flags::DISPATCH_AS_HOVER_ENTER,
-                                             BitSet32(0));
-        }
+    {
+        std::vector<TouchedWindow> hoveringWindows =
+                getHoveringWindowsLocked(oldState, tempTouchState, entry);
+        touchedWindows.insert(touchedWindows.end(), hoveringWindows.begin(), hoveringWindows.end());
     }
-
     // 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
@@ -2449,10 +2496,13 @@
         }
     }
 
-    // Success!  Output targets.
-    touchedWindows = tempTouchState.windows;
-    outInjectionResult = InputEventInjectionResult::SUCCEEDED;
+    // Success!  Output targets for everything except hovers.
+    if (!isHoverAction) {
+        touchedWindows.insert(touchedWindows.end(), tempTouchState.windows.begin(),
+                              tempTouchState.windows.end());
+    }
 
+    outInjectionResult = InputEventInjectionResult::SUCCEEDED;
     // Drop the outside or hover touch windows since we will not care about them
     // in the next iteration.
     tempTouchState.filterNonAsIsTouchWindows();
@@ -2473,14 +2523,16 @@
                      "Conflicting pointer actions: Hover received while pointer was down.");
             *outConflictingPointerActions = true;
         }
-        tempTouchState.reset();
         if (maskedAction == AMOTION_EVENT_ACTION_HOVER_ENTER ||
             maskedAction == AMOTION_EVENT_ACTION_HOVER_MOVE) {
             tempTouchState.deviceId = entry.deviceId;
             tempTouchState.source = entry.source;
         }
-    } else if (maskedAction == AMOTION_EVENT_ACTION_UP ||
-               maskedAction == AMOTION_EVENT_ACTION_CANCEL) {
+    } else if (maskedAction == AMOTION_EVENT_ACTION_UP) {
+        // Pointer went up.
+        tempTouchState.removeTouchedPointer(entry.pointerProperties[0].id);
+        tempTouchState.clearWindowsWithoutPointers();
+    } else if (maskedAction == AMOTION_EVENT_ACTION_CANCEL) {
         // All pointers up or canceled.
         tempTouchState.reset();
     } else if (maskedAction == AMOTION_EVENT_ACTION_DOWN) {
@@ -2519,9 +2571,6 @@
         mTouchStatesByDisplay.erase(displayId);
     }
 
-    // Update hover state.
-    mLastHoverWindowHandle = newHoverWindowHandle;
-
     return touchedWindows;
 }
 
@@ -4824,14 +4873,6 @@
     updateWindowHandlesForDisplayLocked(windowInfoHandles, displayId);
 
     const std::vector<sp<WindowInfoHandle>>& windowHandles = getWindowHandlesLocked(displayId);
-    if (mLastHoverWindowHandle) {
-        const WindowInfo* lastHoverWindowInfo = mLastHoverWindowHandle->getInfo();
-        if (lastHoverWindowInfo->displayId == displayId &&
-            std::find(windowHandles.begin(), windowHandles.end(), mLastHoverWindowHandle) ==
-                    windowHandles.end()) {
-            mLastHoverWindowHandle = nullptr;
-        }
-    }
 
     std::optional<FocusResolver::FocusChanges> changes =
             mFocusResolver.setInputWindows(displayId, windowHandles);
@@ -5278,7 +5319,6 @@
 
     mAnrTracker.clear();
     mTouchStatesByDisplay.clear();
-    mLastHoverWindowHandle.clear();
     mReplacedKeys.clear();
 }
 
@@ -6468,7 +6508,6 @@
         synthesizeCancelationEventsForAllConnectionsLocked(options);
 
         mTouchStatesByDisplay.clear();
-        mLastHoverWindowHandle.clear();
     }
     // Wake up poll loop since there might be work to do.
     mLooper->wake();
diff --git a/services/inputflinger/dispatcher/InputDispatcher.h b/services/inputflinger/dispatcher/InputDispatcher.h
index a32ebd3..91ca2db 100644
--- a/services/inputflinger/dispatcher/InputDispatcher.h
+++ b/services/inputflinger/dispatcher/InputDispatcher.h
@@ -530,9 +530,6 @@
     // prevent unneeded wakeups.
     AnrTracker mAnrTracker GUARDED_BY(mLock);
 
-    // Contains the last window which received a hover event.
-    sp<android::gui::WindowInfoHandle> mLastHoverWindowHandle GUARDED_BY(mLock);
-
     void cancelEventsForAnrLocked(const sp<Connection>& connection) REQUIRES(mLock);
     // If a focused application changes, we should stop counting down the "no focused window" time,
     // because we will have no way of knowing when the previous application actually added a window.
diff --git a/services/inputflinger/dispatcher/TouchState.cpp b/services/inputflinger/dispatcher/TouchState.cpp
index c21af9e..f120fc9 100644
--- a/services/inputflinger/dispatcher/TouchState.cpp
+++ b/services/inputflinger/dispatcher/TouchState.cpp
@@ -31,10 +31,30 @@
     *this = TouchState();
 }
 
+void TouchState::removeTouchedPointer(int32_t pointerId) {
+    for (TouchedWindow& touchedWindow : windows) {
+        touchedWindow.pointerIds.clearBit(pointerId);
+    }
+}
+
+void TouchState::clearHoveringPointers() {
+    for (TouchedWindow& touchedWindow : windows) {
+        touchedWindow.clearHoveringPointers();
+    }
+}
+
+void TouchState::clearWindowsWithoutPointers() {
+    std::erase_if(windows, [](const TouchedWindow& w) {
+        return w.pointerIds.isEmpty() && !w.hasHoveringPointers();
+    });
+}
+
 void TouchState::addOrUpdateWindow(const sp<WindowInfoHandle>& windowHandle,
                                    ftl::Flags<InputTarget::Flags> targetFlags, BitSet32 pointerIds,
                                    std::optional<nsecs_t> eventTime) {
     for (TouchedWindow& touchedWindow : windows) {
+        // We do not compare windows by token here because two windows that share the same token
+        // may have a different transform
         if (touchedWindow.windowHandle == windowHandle) {
             touchedWindow.targetFlags |= targetFlags;
             if (targetFlags.test(InputTarget::Flags::DISPATCH_AS_SLIPPERY_EXIT)) {
@@ -59,6 +79,21 @@
     windows.push_back(touchedWindow);
 }
 
+void TouchState::addHoveringPointerToWindow(const sp<WindowInfoHandle>& windowHandle,
+                                            int32_t hoveringDeviceId, int32_t hoveringPointerId) {
+    for (TouchedWindow& touchedWindow : windows) {
+        if (touchedWindow.windowHandle == windowHandle) {
+            touchedWindow.addHoveringPointer(hoveringDeviceId, hoveringPointerId);
+            return;
+        }
+    }
+
+    TouchedWindow touchedWindow;
+    touchedWindow.windowHandle = windowHandle;
+    touchedWindow.addHoveringPointer(hoveringDeviceId, hoveringPointerId);
+    windows.push_back(touchedWindow);
+}
+
 void TouchState::removeWindowByToken(const sp<IBinder>& token) {
     for (size_t i = 0; i < windows.size(); i++) {
         if (windows[i].windowHandle->getToken() == token) {
@@ -145,6 +180,26 @@
                        [](const TouchedWindow& window) { return !window.pointerIds.isEmpty(); });
 }
 
+std::set<sp<WindowInfoHandle>> TouchState::getWindowsWithHoveringPointer(int32_t hoveringDeviceId,
+                                                                         int32_t pointerId) const {
+    std::set<sp<WindowInfoHandle>> out;
+    for (const TouchedWindow& window : windows) {
+        if (window.hasHoveringPointer(hoveringDeviceId, pointerId)) {
+            out.insert(window.windowHandle);
+        }
+    }
+    return out;
+}
+
+void TouchState::removeHoveringPointer(int32_t hoveringDeviceId, int32_t hoveringPointerId) {
+    for (TouchedWindow& window : windows) {
+        window.removeHoveringPointer(hoveringDeviceId, hoveringPointerId);
+    }
+    std::erase_if(windows, [](const TouchedWindow& w) {
+        return w.pointerIds.isEmpty() && !w.hasHoveringPointers();
+    });
+}
+
 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 77c1cdf..b75e6ef 100644
--- a/services/inputflinger/dispatcher/TouchState.h
+++ b/services/inputflinger/dispatcher/TouchState.h
@@ -16,6 +16,7 @@
 
 #pragma once
 
+#include <set>
 #include "TouchedWindow.h"
 
 namespace android {
@@ -39,9 +40,16 @@
     TouchState& operator=(const TouchState&) = default;
 
     void reset();
+    void clearWindowsWithoutPointers();
+
+    void removeTouchedPointer(int32_t pointerId);
     void addOrUpdateWindow(const sp<android::gui::WindowInfoHandle>& windowHandle,
                            ftl::Flags<InputTarget::Flags> targetFlags, BitSet32 pointerIds,
                            std::optional<nsecs_t> eventTime = std::nullopt);
+    void addHoveringPointerToWindow(const sp<android::gui::WindowInfoHandle>& windowHandle,
+                                    int32_t deviceId, int32_t hoveringPointerId);
+    void removeHoveringPointer(int32_t deviceId, int32_t hoveringPointerId);
+    void clearHoveringPointers();
     void removeWindowByToken(const sp<IBinder>& token);
     void filterNonAsIsTouchWindows();
 
@@ -56,6 +64,9 @@
     sp<android::gui::WindowInfoHandle> getWallpaperWindow() const;
     // Whether any of the windows are currently being touched
     bool isDown() const;
+
+    std::set<sp<android::gui::WindowInfoHandle>> getWindowsWithHoveringPointer(
+            int32_t deviceId, int32_t pointerId) const;
     std::string dump() const;
 };
 
diff --git a/services/inputflinger/dispatcher/TouchedWindow.cpp b/services/inputflinger/dispatcher/TouchedWindow.cpp
index af74598..3704edd 100644
--- a/services/inputflinger/dispatcher/TouchedWindow.cpp
+++ b/services/inputflinger/dispatcher/TouchedWindow.cpp
@@ -25,11 +25,49 @@
 
 namespace inputdispatcher {
 
+bool TouchedWindow::hasHoveringPointers() const {
+    return !mHoveringPointerIdsByDevice.empty();
+}
+
+void TouchedWindow::clearHoveringPointers() {
+    mHoveringPointerIdsByDevice.clear();
+}
+
+bool TouchedWindow::hasHoveringPointer(int32_t deviceId, int32_t pointerId) const {
+    auto it = mHoveringPointerIdsByDevice.find(deviceId);
+    if (it == mHoveringPointerIdsByDevice.end()) {
+        return false;
+    }
+    return it->second.test(pointerId);
+}
+
+void TouchedWindow::addHoveringPointer(int32_t deviceId, int32_t pointerId) {
+    const auto [it, _] = mHoveringPointerIdsByDevice.insert({deviceId, {}});
+    it->second.set(pointerId);
+}
+
+void TouchedWindow::removeHoveringPointer(int32_t deviceId, int32_t pointerId) {
+    const auto it = mHoveringPointerIdsByDevice.find(deviceId);
+    if (it == mHoveringPointerIdsByDevice.end()) {
+        return;
+    }
+    it->second.set(pointerId, false);
+
+    if (it->second.none()) {
+        mHoveringPointerIdsByDevice.erase(deviceId);
+    }
+}
+
 std::string TouchedWindow::dump() const {
-    return StringPrintf("name='%s', pointerIds=0x%0x, "
-                        "targetFlags=%s, firstDownTimeInTarget=%s\n",
+    std::string out;
+    std::string hoveringPointers =
+            dumpMap(mHoveringPointerIdsByDevice, constToString, bitsetToString);
+    out += StringPrintf("name='%s', pointerIds=0x%0x, targetFlags=%s, firstDownTimeInTarget=%s, "
+                        "mHoveringPointerIdsByDevice=%s\n",
                         windowHandle->getName().c_str(), pointerIds.value,
-                        targetFlags.string().c_str(), toString(firstDownTimeInTarget).c_str());
+                        targetFlags.string().c_str(), toString(firstDownTimeInTarget).c_str(),
+                        hoveringPointers.c_str());
+    return out;
 }
 
 } // namespace inputdispatcher
diff --git a/services/inputflinger/dispatcher/TouchedWindow.h b/services/inputflinger/dispatcher/TouchedWindow.h
index dd08323..add6b61 100644
--- a/services/inputflinger/dispatcher/TouchedWindow.h
+++ b/services/inputflinger/dispatcher/TouchedWindow.h
@@ -17,7 +17,9 @@
 #pragma once
 
 #include <gui/WindowInfo.h>
+#include <input/Input.h>
 #include <utils/BitSet.h>
+#include <bitset>
 #include "InputTarget.h"
 
 namespace android {
@@ -33,7 +35,17 @@
     // Time at which the first action down occurred on this window.
     // NOTE: This is not initialized in case of HOVER entry/exit and DISPATCH_AS_OUTSIDE scenario.
     std::optional<nsecs_t> firstDownTimeInTarget;
+
+    bool hasHoveringPointers() const;
+
+    bool hasHoveringPointer(int32_t deviceId, int32_t pointerId) const;
+    void addHoveringPointer(int32_t deviceId, int32_t pointerId);
+    void removeHoveringPointer(int32_t deviceId, int32_t pointerId);
+    void clearHoveringPointers();
     std::string dump() const;
+
+private:
+    std::map<int32_t /*deviceId*/, std::bitset<MAX_POINTERS>> mHoveringPointerIdsByDevice;
 };
 
 } // namespace inputdispatcher
diff --git a/services/inputflinger/reader/Android.bp b/services/inputflinger/reader/Android.bp
index f3b680b..d29692c 100644
--- a/services/inputflinger/reader/Android.bp
+++ b/services/inputflinger/reader/Android.bp
@@ -64,6 +64,7 @@
         "mapper/gestures/GestureConverter.cpp",
         "mapper/gestures/GesturesLogging.cpp",
         "mapper/gestures/HardwareStateConverter.cpp",
+        "mapper/gestures/PropertyProvider.cpp",
     ],
 }
 
diff --git a/services/inputflinger/reader/EventHub.cpp b/services/inputflinger/reader/EventHub.cpp
index 43b67ca..f7b38a1 100644
--- a/services/inputflinger/reader/EventHub.cpp
+++ b/services/inputflinger/reader/EventHub.cpp
@@ -515,6 +515,18 @@
     return deviceClasses & InputDeviceClass::JOYSTICK;
 }
 
+// --- RawAbsoluteAxisInfo ---
+
+std::ostream& operator<<(std::ostream& out, const RawAbsoluteAxisInfo& info) {
+    if (info.valid) {
+        out << "min=" << info.minValue << ", max=" << info.maxValue << ", flat=" << info.flat
+            << ", fuzz=" << info.fuzz << ", resolution=" << info.resolution;
+    } else {
+        out << "unknown range";
+    }
+    return out;
+}
+
 // --- EventHub::Device ---
 
 EventHub::Device::Device(int fd, int32_t id, std::string path, InputDeviceIdentifier identifier,
diff --git a/services/inputflinger/reader/InputDevice.cpp b/services/inputflinger/reader/InputDevice.cpp
index 6e78e82..6ac6b5b 100644
--- a/services/inputflinger/reader/InputDevice.cpp
+++ b/services/inputflinger/reader/InputDevice.cpp
@@ -209,9 +209,15 @@
 
     // Touchscreens and touchpad devices.
     static const bool ENABLE_TOUCHPAD_GESTURES_LIBRARY =
-            sysprop::InputProperties::enable_touchpad_gestures_library().value_or(false);
+            sysprop::InputProperties::enable_touchpad_gestures_library().value_or(true);
+    // TODO(b/246587538): Fix the new touchpad stack for Sony DualShock 4 (5c4, 9cc) and DualSense
+    // (ce6) touchpads, or at least load this setting from the IDC file.
+    const InputDeviceIdentifier& identifier = getDeviceInfo().getIdentifier();
+    const bool isSonyGamepadTouchpad = identifier.vendor == 0x054c &&
+            (identifier.product == 0x05c4 || identifier.product == 0x09cc ||
+             identifier.product == 0x0ce6);
     if (ENABLE_TOUCHPAD_GESTURES_LIBRARY && classes.test(InputDeviceClass::TOUCHPAD) &&
-        classes.test(InputDeviceClass::TOUCH_MT)) {
+        classes.test(InputDeviceClass::TOUCH_MT) && !isSonyGamepadTouchpad) {
         mappers.push_back(std::make_unique<TouchpadInputMapper>(*contextPtr));
     } else if (classes.test(InputDeviceClass::TOUCH_MT)) {
         mappers.push_back(std::make_unique<MultiTouchInputMapper>(*contextPtr));
@@ -452,7 +458,8 @@
 InputDeviceInfo InputDevice::getDeviceInfo() {
     InputDeviceInfo outDeviceInfo;
     outDeviceInfo.initialize(mId, mGeneration, mControllerNumber, mIdentifier, mAlias, mIsExternal,
-                             mHasMic);
+                             mHasMic, getAssociatedDisplayId().value_or(ADISPLAY_ID_NONE));
+
     for_each_mapper(
             [&outDeviceInfo](InputMapper& mapper) { mapper.populateDeviceInfo(&outDeviceInfo); });
 
diff --git a/services/inputflinger/reader/include/EventHub.h b/services/inputflinger/reader/include/EventHub.h
index a3ecf41..86acadb 100644
--- a/services/inputflinger/reader/include/EventHub.h
+++ b/services/inputflinger/reader/include/EventHub.h
@@ -19,6 +19,8 @@
 #include <bitset>
 #include <climits>
 #include <filesystem>
+#include <ostream>
+#include <string>
 #include <unordered_map>
 #include <utility>
 #include <vector>
@@ -77,6 +79,8 @@
     inline void clear() { *this = RawAbsoluteAxisInfo(); }
 };
 
+std::ostream& operator<<(std::ostream& out, const RawAbsoluteAxisInfo& info);
+
 /*
  * Input device classes.
  */
diff --git a/services/inputflinger/reader/mapper/InputMapper.cpp b/services/inputflinger/reader/mapper/InputMapper.cpp
index 8e3539c..ba2ea99 100644
--- a/services/inputflinger/reader/mapper/InputMapper.cpp
+++ b/services/inputflinger/reader/mapper/InputMapper.cpp
@@ -18,6 +18,8 @@
 
 #include "InputMapper.h"
 
+#include <sstream>
+
 #include "InputDevice.h"
 #include "input/PrintTools.h"
 
@@ -120,12 +122,9 @@
 
 void InputMapper::dumpRawAbsoluteAxisInfo(std::string& dump, const RawAbsoluteAxisInfo& axis,
                                           const char* name) {
-    if (axis.valid) {
-        dump += StringPrintf(INDENT4 "%s: min=%d, max=%d, flat=%d, fuzz=%d, resolution=%d\n", name,
-                             axis.minValue, axis.maxValue, axis.flat, axis.fuzz, axis.resolution);
-    } else {
-        dump += StringPrintf(INDENT4 "%s: unknown range\n", name);
-    }
+    std::stringstream out;
+    out << INDENT4 << name << ": " << axis << "\n";
+    dump += out.str();
 }
 
 void InputMapper::dumpStylusState(std::string& dump, const StylusState& state) {
diff --git a/services/inputflinger/reader/mapper/TouchInputMapper.cpp b/services/inputflinger/reader/mapper/TouchInputMapper.cpp
index 8cd2cf0..ddddca2 100644
--- a/services/inputflinger/reader/mapper/TouchInputMapper.cpp
+++ b/services/inputflinger/reader/mapper/TouchInputMapper.cpp
@@ -55,6 +55,10 @@
     return p.x >= rect.left && p.x < rect.right && p.y >= rect.top && p.y < rect.bottom;
 }
 
+static std::string toString(const InputDeviceUsiVersion& v) {
+    return base::StringPrintf("%d.%d", v.majorVersion, v.minorVersion);
+}
+
 template <typename T>
 inline static void swap(T& a, T& b) {
     T temp = a;
@@ -188,7 +192,7 @@
         info->addMotionRange(AMOTION_EVENT_AXIS_HSCROLL, mSource, -1.0f, 1.0f, 0.0f, 0.0f, 0.0f);
     }
     info->setButtonUnderPad(mParameters.hasButtonUnderPad);
-    info->setSupportsUsi(mParameters.supportsUsi);
+    info->setUsiVersion(mParameters.usiVersion);
 }
 
 void TouchInputMapper::dump(std::string& dump) {
@@ -421,9 +425,13 @@
     mParameters.wake = getDeviceContext().isExternal();
     getDeviceContext().getConfiguration().tryGetProperty("touch.wake", mParameters.wake);
 
-    mParameters.supportsUsi = false;
-    getDeviceContext().getConfiguration().tryGetProperty("touch.supportsUsi",
-                                                         mParameters.supportsUsi);
+    InputDeviceUsiVersion usiVersion;
+    if (getDeviceContext().getConfiguration().tryGetProperty("touch.usiVersionMajor",
+                                                             usiVersion.majorVersion) &&
+        getDeviceContext().getConfiguration().tryGetProperty("touch.usiVersionMinor",
+                                                             usiVersion.minorVersion)) {
+        mParameters.usiVersion = usiVersion;
+    }
 
     mParameters.enableForInactiveViewport = false;
     getDeviceContext().getConfiguration().tryGetProperty("touch.enableForInactiveViewport",
@@ -472,7 +480,8 @@
                          mParameters.uniqueDisplayId.c_str());
     dump += StringPrintf(INDENT4 "OrientationAware: %s\n", toString(mParameters.orientationAware));
     dump += INDENT4 "Orientation: " + ftl::enum_string(mParameters.orientation) + "\n";
-    dump += StringPrintf(INDENT4 "SupportsUsi: %s\n", toString(mParameters.supportsUsi));
+    dump += StringPrintf(INDENT4 "UsiVersion: %s\n",
+                         toString(mParameters.usiVersion, toString).c_str());
     dump += StringPrintf(INDENT4 "EnableForInactiveViewport: %s\n",
                          toString(mParameters.enableForInactiveViewport));
 }
diff --git a/services/inputflinger/reader/mapper/TouchInputMapper.h b/services/inputflinger/reader/mapper/TouchInputMapper.h
index 6e35b46..87deb39 100644
--- a/services/inputflinger/reader/mapper/TouchInputMapper.h
+++ b/services/inputflinger/reader/mapper/TouchInputMapper.h
@@ -234,8 +234,8 @@
 
         bool wake;
 
-        // Whether the device supports the Universal Stylus Initiative (USI) protocol for styluses.
-        bool supportsUsi;
+        // The Universal Stylus Initiative (USI) protocol version supported by this device.
+        std::optional<InputDeviceUsiVersion> usiVersion;
 
         // Allows touches while the display is off.
         bool enableForInactiveViewport;
diff --git a/services/inputflinger/reader/mapper/TouchpadInputMapper.cpp b/services/inputflinger/reader/mapper/TouchpadInputMapper.cpp
index 3b51be8..b6313a1 100644
--- a/services/inputflinger/reader/mapper/TouchpadInputMapper.cpp
+++ b/services/inputflinger/reader/mapper/TouchpadInputMapper.cpp
@@ -19,6 +19,7 @@
 #include <optional>
 
 #include <android/input.h>
+#include <input/PrintTools.h>
 #include <linux/input-event-codes.h>
 #include <log/log_main.h>
 #include "TouchCursorInputMapperCommon.h"
@@ -96,11 +97,12 @@
     mGestureInterpreter->Initialize(GESTURES_DEVCLASS_TOUCHPAD);
     mGestureInterpreter->SetHardwareProperties(createHardwareProperties(deviceContext));
     // Even though we don't explicitly delete copy/move semantics, it's safe to
-    // give away a pointer to TouchpadInputMapper here because
+    // give away pointers to TouchpadInputMapper and its members here because
     // 1) mGestureInterpreter's lifecycle is determined by TouchpadInputMapper, and
     // 2) TouchpadInputMapper is stored as a unique_ptr and not moved.
+    mGestureInterpreter->SetPropProvider(const_cast<GesturesPropProvider*>(&gesturePropProvider),
+                                         &mPropertyProvider);
     mGestureInterpreter->SetCallback(gestureInterpreterCallback, this);
-    // TODO(b/251196347): set a property provider, so we can change gesture properties.
     // TODO(b/251196347): set a timer provider, so the library can use timers.
 }
 
@@ -108,12 +110,29 @@
     if (mPointerController != nullptr) {
         mPointerController->fade(PointerControllerInterface::Transition::IMMEDIATE);
     }
+
+    // The gesture interpreter's destructor will call its property provider's free function for all
+    // gesture properties, in this case calling PropertyProvider::freeProperty using a raw pointer
+    // to mPropertyProvider. Depending on the declaration order in TouchpadInputMapper.h, this may
+    // happen after mPropertyProvider has been destructed, causing allocation errors. Depending on
+    // declaration order to avoid crashes seems rather fragile, so explicitly clear the property
+    // provider here to ensure all the freeProperty calls happen before mPropertyProvider is
+    // destructed.
+    mGestureInterpreter->SetPropProvider(nullptr, nullptr);
 }
 
 uint32_t TouchpadInputMapper::getSources() const {
     return AINPUT_SOURCE_MOUSE | AINPUT_SOURCE_TOUCHPAD;
 }
 
+void TouchpadInputMapper::dump(std::string& dump) {
+    dump += INDENT2 "Touchpad Input Mapper:\n";
+    dump += INDENT3 "Gesture converter:\n";
+    dump += addLinePrefix(mGestureConverter.dump(), INDENT4);
+    dump += INDENT3 "Gesture properties:\n";
+    dump += addLinePrefix(mPropertyProvider.dump(), INDENT4);
+}
+
 std::list<NotifyArgs> TouchpadInputMapper::configure(nsecs_t when,
                                                      const InputReaderConfiguration* config,
                                                      uint32_t changes) {
diff --git a/services/inputflinger/reader/mapper/TouchpadInputMapper.h b/services/inputflinger/reader/mapper/TouchpadInputMapper.h
index 3a92211..d693bca 100644
--- a/services/inputflinger/reader/mapper/TouchpadInputMapper.h
+++ b/services/inputflinger/reader/mapper/TouchpadInputMapper.h
@@ -29,6 +29,7 @@
 #include "NotifyArgs.h"
 #include "gestures/GestureConverter.h"
 #include "gestures/HardwareStateConverter.h"
+#include "gestures/PropertyProvider.h"
 
 #include "include/gestures.h"
 
@@ -40,6 +41,8 @@
     ~TouchpadInputMapper();
 
     uint32_t getSources() const override;
+    void dump(std::string& dump) override;
+
     [[nodiscard]] std::list<NotifyArgs> configure(nsecs_t when,
                                                   const InputReaderConfiguration* config,
                                                   uint32_t changes) override;
@@ -57,6 +60,8 @@
             mGestureInterpreter;
     std::shared_ptr<PointerControllerInterface> mPointerController;
 
+    PropertyProvider mPropertyProvider;
+
     HardwareStateConverter mStateConverter;
     GestureConverter mGestureConverter;
 
diff --git a/services/inputflinger/reader/mapper/gestures/GestureConverter.cpp b/services/inputflinger/reader/mapper/gestures/GestureConverter.cpp
index 11ffd28..561b1f8 100644
--- a/services/inputflinger/reader/mapper/gestures/GestureConverter.cpp
+++ b/services/inputflinger/reader/mapper/gestures/GestureConverter.cpp
@@ -16,7 +16,11 @@
 
 #include "gestures/GestureConverter.h"
 
+#include <sstream>
+
+#include <android-base/stringprintf.h>
 #include <android/input.h>
+#include <ftl/enum.h>
 #include <linux/input-event-codes.h>
 #include <log/log_main.h>
 
@@ -55,6 +59,18 @@
     deviceContext.getAbsoluteAxisInfo(ABS_MT_POSITION_Y, &mYAxisInfo);
 }
 
+std::string GestureConverter::dump() const {
+    std::stringstream out;
+    out << "Orientation: " << ftl::enum_string(mOrientation) << "\n";
+    out << "Axis info:\n";
+    out << "  X: " << mXAxisInfo << "\n";
+    out << "  Y: " << mYAxisInfo << "\n";
+    out << StringPrintf("Button state: 0x%08x\n", mButtonState);
+    out << "Down time: " << mDownTime << "\n";
+    out << "Current classification: " << ftl::enum_string(mCurrentClassification) << "\n";
+    return out.str();
+}
+
 void GestureConverter::reset() {
     mButtonState = 0;
 }
diff --git a/services/inputflinger/reader/mapper/gestures/GestureConverter.h b/services/inputflinger/reader/mapper/gestures/GestureConverter.h
index 8e8e3d9..2ec5841 100644
--- a/services/inputflinger/reader/mapper/gestures/GestureConverter.h
+++ b/services/inputflinger/reader/mapper/gestures/GestureConverter.h
@@ -40,6 +40,8 @@
     GestureConverter(InputReaderContext& readerContext, const InputDeviceContext& deviceContext,
                      int32_t deviceId);
 
+    std::string dump() const;
+
     void setOrientation(ui::Rotation orientation) { mOrientation = orientation; }
     void reset();
 
diff --git a/services/inputflinger/reader/mapper/gestures/PropertyProvider.cpp b/services/inputflinger/reader/mapper/gestures/PropertyProvider.cpp
new file mode 100644
index 0000000..cd18cd3
--- /dev/null
+++ b/services/inputflinger/reader/mapper/gestures/PropertyProvider.cpp
@@ -0,0 +1,236 @@
+/*
+ * 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 "../Macros.h"
+
+#include "gestures/PropertyProvider.h"
+
+#include <algorithm>
+#include <utility>
+
+#include <android-base/stringprintf.h>
+#include <ftl/enum.h>
+#include <input/PrintTools.h>
+#include <log/log_main.h>
+
+namespace android {
+
+namespace {
+
+GesturesProp* createInt(void* data, const char* name, int* loc, size_t count, const int* init) {
+    return static_cast<PropertyProvider*>(data)->createIntArrayProperty(name, loc, count, init);
+}
+
+GesturesProp* createBool(void* data, const char* name, GesturesPropBool* loc, size_t count,
+                         const GesturesPropBool* init) {
+    return static_cast<PropertyProvider*>(data)->createBoolArrayProperty(name, loc, count, init);
+}
+
+GesturesProp* createString(void* data, const char* name, const char** loc, const char* const init) {
+    return static_cast<PropertyProvider*>(data)->createStringProperty(name, loc, init);
+}
+
+GesturesProp* createReal(void* data, const char* name, double* loc, size_t count,
+                         const double* init) {
+    return static_cast<PropertyProvider*>(data)->createRealArrayProperty(name, loc, count, init);
+}
+
+void registerHandlers(void* data, GesturesProp* prop, void* handlerData,
+                      GesturesPropGetHandler getter, GesturesPropSetHandler setter) {
+    prop->registerHandlers(handlerData, getter, setter);
+}
+
+void freeProperty(void* data, GesturesProp* prop) {
+    static_cast<PropertyProvider*>(data)->freeProperty(prop);
+}
+
+} // namespace
+
+const GesturesPropProvider gesturePropProvider = {
+        .create_int_fn = createInt,
+        .create_bool_fn = createBool,
+        .create_string_fn = createString,
+        .create_real_fn = createReal,
+        .register_handlers_fn = registerHandlers,
+        .free_fn = freeProperty,
+};
+
+bool PropertyProvider::hasProperty(const std::string name) const {
+    return mProperties.find(name) != mProperties.end();
+}
+
+GesturesProp& PropertyProvider::getProperty(const std::string name) {
+    return mProperties.at(name);
+}
+
+std::string PropertyProvider::dump() const {
+    std::string dump;
+    for (const auto& [name, property] : mProperties) {
+        dump += property.dump() + "\n";
+    }
+    return dump;
+}
+
+GesturesProp* PropertyProvider::createIntArrayProperty(const std::string name, int* loc,
+                                                       size_t count, const int* init) {
+    const auto [it, inserted] =
+            mProperties.insert(std::pair{name, GesturesProp(name, loc, count, init)});
+    LOG_ALWAYS_FATAL_IF(!inserted, "Gesture property \"%s\" already exists.", name.c_str());
+    return &it->second;
+}
+
+GesturesProp* PropertyProvider::createBoolArrayProperty(const std::string name,
+                                                        GesturesPropBool* loc, size_t count,
+                                                        const GesturesPropBool* init) {
+    const auto [it, inserted] =
+            mProperties.insert(std::pair{name, GesturesProp(name, loc, count, init)});
+    LOG_ALWAYS_FATAL_IF(!inserted, "Gesture property \"%s\" already exists.", name.c_str());
+    return &it->second;
+}
+
+GesturesProp* PropertyProvider::createRealArrayProperty(const std::string name, double* loc,
+                                                        size_t count, const double* init) {
+    const auto [it, inserted] =
+            mProperties.insert(std::pair{name, GesturesProp(name, loc, count, init)});
+    LOG_ALWAYS_FATAL_IF(!inserted, "Gesture property \"%s\" already exists.", name.c_str());
+    return &it->second;
+}
+
+GesturesProp* PropertyProvider::createStringProperty(const std::string name, const char** loc,
+                                                     const char* const init) {
+    const auto [it, inserted] = mProperties.insert(std::pair{name, GesturesProp(name, loc, init)});
+    LOG_ALWAYS_FATAL_IF(!inserted, "Gesture property \"%s\" already exists.", name.c_str());
+    return &it->second;
+}
+
+void PropertyProvider::freeProperty(GesturesProp* prop) {
+    mProperties.erase(prop->getName());
+}
+
+} // namespace android
+
+template <typename T>
+GesturesProp::GesturesProp(std::string name, T* dataPointer, size_t count, const T* initialValues)
+      : mName(name), mCount(count), mDataPointer(dataPointer) {
+    std::copy_n(initialValues, count, dataPointer);
+}
+
+GesturesProp::GesturesProp(std::string name, const char** dataPointer,
+                           const char* const initialValue)
+      : mName(name), mCount(1), mDataPointer(dataPointer) {
+    *(std::get<const char**>(mDataPointer)) = initialValue;
+}
+
+std::string GesturesProp::dump() const {
+    using android::base::StringPrintf;
+    std::string type, values;
+    switch (mDataPointer.index()) {
+        case 0:
+            type = "integer";
+            values = android::dumpVector(getIntValues());
+            break;
+        case 1:
+            type = "boolean";
+            values = android::dumpVector(getBoolValues());
+            break;
+        case 2:
+            type = "string";
+            values = getStringValue();
+            break;
+        case 3:
+            type = "real";
+            values = android::dumpVector(getRealValues());
+            break;
+    }
+    std::string typeAndSize = mCount == 1 ? type : std::to_string(mCount) + " " + type + "s";
+    return StringPrintf("%s (%s): %s", mName.c_str(), typeAndSize.c_str(), values.c_str());
+}
+
+void GesturesProp::registerHandlers(void* handlerData, GesturesPropGetHandler getter,
+                                    GesturesPropSetHandler setter) {
+    mHandlerData = handlerData;
+    mGetter = getter;
+    mSetter = setter;
+}
+
+std::vector<int> GesturesProp::getIntValues() const {
+    LOG_ALWAYS_FATAL_IF(!std::holds_alternative<int*>(mDataPointer),
+                        "Attempt to read ints from \"%s\" gesture property.", mName.c_str());
+    return getValues<int, int>(std::get<int*>(mDataPointer));
+}
+
+std::vector<bool> GesturesProp::getBoolValues() const {
+    LOG_ALWAYS_FATAL_IF(!std::holds_alternative<GesturesPropBool*>(mDataPointer),
+                        "Attempt to read bools from \"%s\" gesture property.", mName.c_str());
+    return getValues<bool, GesturesPropBool>(std::get<GesturesPropBool*>(mDataPointer));
+}
+
+std::vector<double> GesturesProp::getRealValues() const {
+    LOG_ALWAYS_FATAL_IF(!std::holds_alternative<double*>(mDataPointer),
+                        "Attempt to read reals from \"%s\" gesture property.", mName.c_str());
+    return getValues<double, double>(std::get<double*>(mDataPointer));
+}
+
+std::string GesturesProp::getStringValue() const {
+    LOG_ALWAYS_FATAL_IF(!std::holds_alternative<const char**>(mDataPointer),
+                        "Attempt to read a string from \"%s\" gesture property.", mName.c_str());
+    if (mGetter != nullptr) {
+        mGetter(mHandlerData);
+    }
+    return std::string(*std::get<const char**>(mDataPointer));
+}
+
+void GesturesProp::setBoolValues(const std::vector<bool>& values) {
+    LOG_ALWAYS_FATAL_IF(!std::holds_alternative<GesturesPropBool*>(mDataPointer),
+                        "Attempt to write bools to \"%s\" gesture property.", mName.c_str());
+    setValues(std::get<GesturesPropBool*>(mDataPointer), values);
+}
+
+void GesturesProp::setIntValues(const std::vector<int>& values) {
+    LOG_ALWAYS_FATAL_IF(!std::holds_alternative<int*>(mDataPointer),
+                        "Attempt to write ints to \"%s\" gesture property.", mName.c_str());
+    setValues(std::get<int*>(mDataPointer), values);
+}
+
+void GesturesProp::setRealValues(const std::vector<double>& values) {
+    LOG_ALWAYS_FATAL_IF(!std::holds_alternative<double*>(mDataPointer),
+                        "Attempt to write reals to \"%s\" gesture property.", mName.c_str());
+    setValues(std::get<double*>(mDataPointer), values);
+}
+
+template <typename T, typename U>
+const std::vector<T> GesturesProp::getValues(U* dataPointer) const {
+    if (mGetter != nullptr) {
+        mGetter(mHandlerData);
+    }
+    std::vector<T> values;
+    values.reserve(mCount);
+    for (size_t i = 0; i < mCount; i++) {
+        values.push_back(dataPointer[i]);
+    }
+    return values;
+}
+
+template <typename T, typename U>
+void GesturesProp::setValues(T* dataPointer, const std::vector<U>& values) {
+    LOG_ALWAYS_FATAL_IF(values.size() != mCount,
+                        "Attempt to write %zu values to \"%s\" gesture property, which holds %zu.",
+                        values.size(), mName.c_str(), mCount);
+    std::copy(values.begin(), values.end(), dataPointer);
+    if (mSetter != nullptr) {
+        mSetter(mHandlerData);
+    }
+}
diff --git a/services/inputflinger/reader/mapper/gestures/PropertyProvider.h b/services/inputflinger/reader/mapper/gestures/PropertyProvider.h
new file mode 100644
index 0000000..c21260f
--- /dev/null
+++ b/services/inputflinger/reader/mapper/gestures/PropertyProvider.h
@@ -0,0 +1,100 @@
+/*
+ * 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 <map>
+#include <string>
+#include <variant>
+#include <vector>
+
+#include "include/gestures.h"
+
+namespace android {
+
+// Struct containing functions that wrap PropertyProvider in a C-compatible interface.
+extern const GesturesPropProvider gesturePropProvider;
+
+// Implementation of a gestures library property provider, which provides configuration parameters.
+class PropertyProvider {
+public:
+    bool hasProperty(const std::string name) const;
+    GesturesProp& getProperty(const std::string name);
+    std::string dump() const;
+
+    // Methods to be called by the gestures library:
+    GesturesProp* createIntArrayProperty(const std::string name, int* loc, size_t count,
+                                         const int* init);
+    GesturesProp* createBoolArrayProperty(const std::string name, GesturesPropBool* loc,
+                                          size_t count, const GesturesPropBool* init);
+    GesturesProp* createRealArrayProperty(const std::string name, double* loc, size_t count,
+                                          const double* init);
+    GesturesProp* createStringProperty(const std::string name, const char** loc,
+                                       const char* const init);
+
+    void freeProperty(GesturesProp* prop);
+
+private:
+    std::map<std::string, GesturesProp> mProperties;
+};
+
+} // namespace android
+
+// Represents a single gesture property.
+//
+// Pointers to this struct will be used by the gestures library (though it can never deference
+// them). The library's API requires this to be in the top-level namespace.
+struct GesturesProp {
+public:
+    template <typename T>
+    GesturesProp(std::string name, T* dataPointer, size_t count, const T* initialValues);
+    GesturesProp(std::string name, const char** dataPointer, const char* const initialValue);
+
+    std::string dump() const;
+
+    std::string getName() const { return mName; }
+
+    size_t getCount() const { return mCount; }
+
+    void registerHandlers(void* handlerData, GesturesPropGetHandler getter,
+                          GesturesPropSetHandler setter);
+
+    std::vector<int> getIntValues() const;
+    std::vector<bool> getBoolValues() const;
+    std::vector<double> getRealValues() const;
+    std::string getStringValue() const;
+
+    void setIntValues(const std::vector<int>& values);
+    void setBoolValues(const std::vector<bool>& values);
+    void setRealValues(const std::vector<double>& values);
+    // Setting string values isn't supported since we don't have a use case yet and the memory
+    // management adds additional complexity.
+
+private:
+    // Two type parameters are required for these methods, rather than one, due to the gestures
+    // library using its own bool type.
+    template <typename T, typename U>
+    const std::vector<T> getValues(U* dataPointer) const;
+    template <typename T, typename U>
+    void setValues(T* dataPointer, const std::vector<U>& values);
+
+    std::string mName;
+    size_t mCount;
+    std::variant<int*, GesturesPropBool*, const char**, double*> mDataPointer;
+    void* mHandlerData = nullptr;
+    GesturesPropGetHandler mGetter = nullptr;
+    GesturesPropSetHandler mSetter = nullptr;
+};
diff --git a/services/inputflinger/tests/Android.bp b/services/inputflinger/tests/Android.bp
index 58a5c31..af40fed 100644
--- a/services/inputflinger/tests/Android.bp
+++ b/services/inputflinger/tests/Android.bp
@@ -55,6 +55,7 @@
         "LatencyTracker_test.cpp",
         "NotifyArgs_test.cpp",
         "PreferStylusOverTouch_test.cpp",
+        "PropertyProvider_test.cpp",
         "TestInputListener.cpp",
         "UinputDevice.cpp",
         "UnwantedInteractionBlocker_test.cpp",
diff --git a/services/inputflinger/tests/InputDispatcher_test.cpp b/services/inputflinger/tests/InputDispatcher_test.cpp
index 864aaea..3abe43a 100644
--- a/services/inputflinger/tests/InputDispatcher_test.cpp
+++ b/services/inputflinger/tests/InputDispatcher_test.cpp
@@ -2168,7 +2168,6 @@
                                                          .y(400))
                                         .build()));
     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,
@@ -2181,7 +2180,6 @@
                                         .build()));
     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,
@@ -2239,7 +2237,6 @@
                                         .build()));
     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();
@@ -2301,7 +2298,6 @@
                                                          .y(400))
                                         .build()));
     window->consumeMotionEvent(WithMotionAction(AMOTION_EVENT_ACTION_HOVER_ENTER));
-
     // Inject a series of mouse events for a mouse click
     ASSERT_EQ(InputEventInjectionResult::SUCCEEDED,
               injectMotionEvent(mDispatcher,
@@ -2359,8 +2355,38 @@
 }
 
 /**
+ * Hover over a window, and then remove that window. Make sure that HOVER_EXIT for that event
+ * is generated.
+ */
+TEST_F(InputDispatcherTest, HoverExitIsSentToRemovedWindow) {
+    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, 1200, 800));
+
+    mDispatcher->setFocusedApplication(ADISPLAY_ID_DEFAULT, application);
+
+    mDispatcher->setInputWindows({{ADISPLAY_ID_DEFAULT, {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(300)
+                                                         .y(400))
+                                        .build()));
+    window->consumeMotionEvent(WithMotionAction(AMOTION_EVENT_ACTION_HOVER_ENTER));
+
+    // Remove the window, but keep the channel.
+    mDispatcher->setInputWindows({{ADISPLAY_ID_DEFAULT, {}}});
+    window->consumeMotionEvent(WithMotionAction(AMOTION_EVENT_ACTION_HOVER_EXIT));
+}
+
+/**
  * Inject a mouse hover event followed by a tap from touchscreen.
- * In the current implementation, the tap does not cause a HOVER_EXIT event.
+ * The tap causes a HOVER_EXIT event to be generated because the current event
+ * stream's source has been switched.
  */
 TEST_F(InputDispatcherTest, MouseHoverAndTouchTap) {
     std::shared_ptr<FakeApplicationHandle> application = std::make_shared<FakeApplicationHandle>();
@@ -2380,15 +2406,16 @@
     ASSERT_NO_FATAL_FAILURE(
             window->consumeMotionEvent(AllOf(WithMotionAction(AMOTION_EVENT_ACTION_HOVER_ENTER),
                                              WithSource(AINPUT_SOURCE_MOUSE))));
-    ASSERT_NO_FATAL_FAILURE(
-            window->consumeMotionEvent(AllOf(WithMotionAction(AMOTION_EVENT_ACTION_HOVER_MOVE),
-                                             WithSource(AINPUT_SOURCE_MOUSE))));
 
     // Tap on the window
     motionArgs = generateMotionArgs(AMOTION_EVENT_ACTION_DOWN, AINPUT_SOURCE_TOUCHSCREEN,
                                     ADISPLAY_ID_DEFAULT, {{10, 10}});
     mDispatcher->notifyMotion(&motionArgs);
     ASSERT_NO_FATAL_FAILURE(
+            window->consumeMotionEvent(AllOf(WithMotionAction(AMOTION_EVENT_ACTION_HOVER_EXIT),
+                                             WithSource(AINPUT_SOURCE_MOUSE))));
+
+    ASSERT_NO_FATAL_FAILURE(
             window->consumeMotionEvent(AllOf(WithMotionAction(AMOTION_EVENT_ACTION_DOWN),
                                              WithSource(AINPUT_SOURCE_TOUCHSCREEN))));
 
@@ -2426,7 +2453,6 @@
                                                          .y(600))
                                         .build()));
     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.
diff --git a/services/inputflinger/tests/PreferStylusOverTouch_test.cpp b/services/inputflinger/tests/PreferStylusOverTouch_test.cpp
index bd05360..7265362 100644
--- a/services/inputflinger/tests/PreferStylusOverTouch_test.cpp
+++ b/services/inputflinger/tests/PreferStylusOverTouch_test.cpp
@@ -15,6 +15,7 @@
  */
 
 #include <gtest/gtest.h>
+#include <gui/constants.h>
 #include "../PreferStylusOverTouchBlocker.h"
 
 namespace android {
@@ -438,7 +439,7 @@
     InputDeviceInfo stylusDevice;
     stylusDevice.initialize(STYLUS_DEVICE_ID, 1 /*generation*/, 1 /*controllerNumber*/,
                             {} /*identifier*/, "stylus device", false /*external*/,
-                            false /*hasMic*/);
+                            false /*hasMic*/, ADISPLAY_ID_NONE);
     notifyInputDevicesChanged({stylusDevice});
     // The touchscreen device was removed, so we no longer remember anything about it. We should
     // again start blocking touch events from it.
diff --git a/services/inputflinger/tests/PropertyProvider_test.cpp b/services/inputflinger/tests/PropertyProvider_test.cpp
new file mode 100644
index 0000000..42a6a9f
--- /dev/null
+++ b/services/inputflinger/tests/PropertyProvider_test.cpp
@@ -0,0 +1,286 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <gestures/PropertyProvider.h>
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+#include "include/gestures.h"
+
+namespace android {
+
+using testing::ElementsAre;
+
+class PropertyProviderTest : public testing::Test {
+protected:
+    PropertyProvider mProvider;
+};
+
+TEST_F(PropertyProviderTest, Int_Create) {
+    const size_t COUNT = 4;
+    int intData[COUNT] = {0, 0, 0, 0};
+    int initialValues[COUNT] = {1, 2, 3, 4};
+    gesturePropProvider.create_int_fn(&mProvider, "Some Integers", intData, COUNT, initialValues);
+
+    ASSERT_TRUE(mProvider.hasProperty("Some Integers"));
+    GesturesProp& prop = mProvider.getProperty("Some Integers");
+    EXPECT_EQ(prop.getName(), "Some Integers");
+    EXPECT_EQ(prop.getCount(), COUNT);
+    EXPECT_THAT(intData, ElementsAre(1, 2, 3, 4));
+}
+
+TEST_F(PropertyProviderTest, Int_Get) {
+    const size_t COUNT = 4;
+    int intData[COUNT] = {0, 0, 0, 0};
+    int initialValues[COUNT] = {9, 9, 9, 9};
+    GesturesProp* propPtr = gesturePropProvider.create_int_fn(&mProvider, "Some Integers", intData,
+                                                              COUNT, initialValues);
+
+    // Get handlers are supposed to be called before the property's data is accessed, so they can
+    // update it if necessary. This getter updates the values, so that the ordering can be checked.
+    GesturesPropGetHandler getter{[](void* handlerData) -> GesturesPropBool {
+        int* array = static_cast<int*>(handlerData);
+        array[0] = 1;
+        array[1] = 2;
+        array[2] = 3;
+        array[3] = 4;
+        return true;
+    }};
+    gesturePropProvider.register_handlers_fn(&mProvider, propPtr, /* handler_data= */ intData,
+                                             getter, nullptr);
+
+    ASSERT_TRUE(mProvider.hasProperty("Some Integers"));
+    GesturesProp& prop = mProvider.getProperty("Some Integers");
+    EXPECT_THAT(prop.getIntValues(), ElementsAre(1, 2, 3, 4));
+}
+
+TEST_F(PropertyProviderTest, Int_Set) {
+    const size_t COUNT = 4;
+    int intData[COUNT] = {0, 0, 0, 0};
+    int initialValues[COUNT] = {9, 9, 9, 9};
+    GesturesProp* propPtr = gesturePropProvider.create_int_fn(&mProvider, "Some Integers", intData,
+                                                              COUNT, initialValues);
+
+    struct SetterData {
+        bool setterCalled;
+        int* propertyData;
+    };
+    SetterData setterData = {false, intData};
+    GesturesPropSetHandler setter{[](void* handlerData) {
+        SetterData* data = static_cast<SetterData*>(handlerData);
+        // Set handlers should be called after the property's data has changed, so check the data.
+        EXPECT_EQ(data->propertyData[0], 1);
+        EXPECT_EQ(data->propertyData[1], 2);
+        EXPECT_EQ(data->propertyData[2], 3);
+        EXPECT_EQ(data->propertyData[3], 4);
+        data->setterCalled = true;
+    }};
+    gesturePropProvider.register_handlers_fn(&mProvider, propPtr, /* handler_data= */ &setterData,
+                                             nullptr, setter);
+
+    ASSERT_TRUE(mProvider.hasProperty("Some Integers"));
+    GesturesProp& prop = mProvider.getProperty("Some Integers");
+    prop.setIntValues({1, 2, 3, 4});
+    EXPECT_THAT(intData, ElementsAre(1, 2, 3, 4));
+    EXPECT_TRUE(setterData.setterCalled);
+    EXPECT_THAT(prop.getIntValues(), ElementsAre(1, 2, 3, 4));
+}
+
+TEST_F(PropertyProviderTest, Bool_Create) {
+    const size_t COUNT = 3;
+    GesturesPropBool boolData[COUNT] = {false, false, false};
+    GesturesPropBool initialValues[COUNT] = {true, false, false};
+    gesturePropProvider.create_bool_fn(&mProvider, "Some Booleans", boolData, COUNT, initialValues);
+
+    ASSERT_TRUE(mProvider.hasProperty("Some Booleans"));
+    GesturesProp& prop = mProvider.getProperty("Some Booleans");
+    EXPECT_EQ(prop.getName(), "Some Booleans");
+    EXPECT_EQ(prop.getCount(), COUNT);
+    EXPECT_THAT(boolData, ElementsAre(true, false, false));
+}
+
+TEST_F(PropertyProviderTest, Bool_Get) {
+    const size_t COUNT = 3;
+    GesturesPropBool boolData[COUNT] = {false, false, false};
+    GesturesPropBool initialValues[COUNT] = {true, false, false};
+    GesturesProp* propPtr = gesturePropProvider.create_bool_fn(&mProvider, "Some Booleans",
+                                                               boolData, COUNT, initialValues);
+
+    // Get handlers are supposed to be called before the property's data is accessed, so they can
+    // update it if necessary. This getter updates the values, so that the ordering can be checked.
+    GesturesPropGetHandler getter{[](void* handlerData) -> GesturesPropBool {
+        GesturesPropBool* array = static_cast<GesturesPropBool*>(handlerData);
+        array[0] = false;
+        array[1] = true;
+        array[2] = true;
+        return true;
+    }};
+    gesturePropProvider.register_handlers_fn(&mProvider, propPtr, /* handler_data= */ boolData,
+                                             getter, nullptr);
+
+    ASSERT_TRUE(mProvider.hasProperty("Some Booleans"));
+    GesturesProp& prop = mProvider.getProperty("Some Booleans");
+    EXPECT_THAT(prop.getBoolValues(), ElementsAre(false, true, true));
+}
+
+TEST_F(PropertyProviderTest, Bool_Set) {
+    const size_t COUNT = 3;
+    GesturesPropBool boolData[COUNT] = {false, false, false};
+    GesturesPropBool initialValues[COUNT] = {true, false, false};
+    GesturesProp* propPtr = gesturePropProvider.create_bool_fn(&mProvider, "Some Booleans",
+                                                               boolData, COUNT, initialValues);
+
+    struct SetterData {
+        bool setterCalled;
+        GesturesPropBool* propertyData;
+    };
+    SetterData setterData = {false, boolData};
+    GesturesPropSetHandler setter{[](void* handlerData) {
+        SetterData* data = static_cast<SetterData*>(handlerData);
+        // Set handlers should be called after the property's data has changed, so check the data.
+        EXPECT_EQ(data->propertyData[0], false);
+        EXPECT_EQ(data->propertyData[1], true);
+        EXPECT_EQ(data->propertyData[2], true);
+        data->setterCalled = true;
+    }};
+    gesturePropProvider.register_handlers_fn(&mProvider, propPtr, /* handler_data= */ &setterData,
+                                             nullptr, setter);
+
+    ASSERT_TRUE(mProvider.hasProperty("Some Booleans"));
+    GesturesProp& prop = mProvider.getProperty("Some Booleans");
+    prop.setBoolValues({false, true, true});
+    EXPECT_THAT(boolData, ElementsAre(false, true, true));
+    EXPECT_TRUE(setterData.setterCalled);
+    EXPECT_THAT(prop.getBoolValues(), ElementsAre(false, true, true));
+}
+
+TEST_F(PropertyProviderTest, Real_Create) {
+    const size_t COUNT = 3;
+    double realData[COUNT] = {0.0, 0.0, 0.0};
+    double initialValues[COUNT] = {3.14, 0.7, -5.0};
+    gesturePropProvider.create_real_fn(&mProvider, "Some Reals", realData, COUNT, initialValues);
+
+    ASSERT_TRUE(mProvider.hasProperty("Some Reals"));
+    GesturesProp& prop = mProvider.getProperty("Some Reals");
+    EXPECT_EQ(prop.getName(), "Some Reals");
+    EXPECT_EQ(prop.getCount(), COUNT);
+    EXPECT_THAT(realData, ElementsAre(3.14, 0.7, -5.0));
+}
+
+TEST_F(PropertyProviderTest, Real_Get) {
+    const size_t COUNT = 3;
+    double realData[COUNT] = {0.0, 0.0, 0.0};
+    double initialValues[COUNT] = {-1.0, -1.0, -1.0};
+    GesturesProp* propPtr = gesturePropProvider.create_real_fn(&mProvider, "Some Reals", realData,
+                                                               COUNT, initialValues);
+
+    // Get handlers are supposed to be called before the property's data is accessed, so they can
+    // update it if necessary. This getter updates the values, so that the ordering can be checked.
+    GesturesPropGetHandler getter{[](void* handlerData) -> GesturesPropBool {
+        double* array = static_cast<double*>(handlerData);
+        array[0] = 3.14;
+        array[1] = 0.7;
+        array[2] = -5.0;
+        return true;
+    }};
+    gesturePropProvider.register_handlers_fn(&mProvider, propPtr, /* handler_data= */ realData,
+                                             getter, nullptr);
+
+    ASSERT_TRUE(mProvider.hasProperty("Some Reals"));
+    GesturesProp& prop = mProvider.getProperty("Some Reals");
+    EXPECT_THAT(prop.getRealValues(), ElementsAre(3.14, 0.7, -5.0));
+}
+
+TEST_F(PropertyProviderTest, Real_Set) {
+    const size_t COUNT = 3;
+    double realData[COUNT] = {0.0, 0.0, 0.0};
+    double initialValues[COUNT] = {-1.0, -1.0, -1.0};
+    GesturesProp* propPtr = gesturePropProvider.create_real_fn(&mProvider, "Some Reals", realData,
+                                                               COUNT, initialValues);
+
+    struct SetterData {
+        bool setterCalled;
+        double* propertyData;
+    };
+    SetterData setterData = {false, realData};
+    GesturesPropSetHandler setter{[](void* handlerData) {
+        SetterData* data = static_cast<SetterData*>(handlerData);
+        // Set handlers should be called after the property's data has changed, so check the data.
+        EXPECT_EQ(data->propertyData[0], 3.14);
+        EXPECT_EQ(data->propertyData[1], 0.7);
+        EXPECT_EQ(data->propertyData[2], -5.0);
+        data->setterCalled = true;
+    }};
+    gesturePropProvider.register_handlers_fn(&mProvider, propPtr, /* handler_data= */ &setterData,
+                                             nullptr, setter);
+
+    ASSERT_TRUE(mProvider.hasProperty("Some Reals"));
+    GesturesProp& prop = mProvider.getProperty("Some Reals");
+    prop.setRealValues({3.14, 0.7, -5.0});
+    EXPECT_THAT(realData, ElementsAre(3.14, 0.7, -5.0));
+    EXPECT_TRUE(setterData.setterCalled);
+    EXPECT_THAT(prop.getRealValues(), ElementsAre(3.14, 0.7, -5.0));
+}
+
+TEST_F(PropertyProviderTest, String_Create) {
+    const char* str = nullptr;
+    std::string initialValue = "Foo";
+    gesturePropProvider.create_string_fn(&mProvider, "A String", &str, initialValue.c_str());
+
+    ASSERT_TRUE(mProvider.hasProperty("A String"));
+    GesturesProp& prop = mProvider.getProperty("A String");
+    EXPECT_EQ(prop.getName(), "A String");
+    EXPECT_EQ(prop.getCount(), 1u);
+    EXPECT_STREQ(str, "Foo");
+}
+
+TEST_F(PropertyProviderTest, String_Get) {
+    const char* str = nullptr;
+    std::string initialValue = "Foo";
+    GesturesProp* propPtr = gesturePropProvider.create_string_fn(&mProvider, "A String", &str,
+                                                                 initialValue.c_str());
+
+    // Get handlers are supposed to be called before the property's data is accessed, so they can
+    // update it if necessary. This getter updates the values, so that the ordering can be checked.
+    struct GetterData {
+        const char** strPtr;
+        std::string newValue; // Have to store the new value outside getter so it stays allocated.
+    };
+    GetterData getterData = {&str, "Bar"};
+    GesturesPropGetHandler getter{[](void* handlerData) -> GesturesPropBool {
+        GetterData* data = static_cast<GetterData*>(handlerData);
+        *data->strPtr = data->newValue.c_str();
+        return true;
+    }};
+    gesturePropProvider.register_handlers_fn(&mProvider, propPtr, /* handler_data= */ &getterData,
+                                             getter, nullptr);
+
+    ASSERT_TRUE(mProvider.hasProperty("A String"));
+    GesturesProp& prop = mProvider.getProperty("A String");
+    EXPECT_EQ(prop.getStringValue(), "Bar");
+}
+
+TEST_F(PropertyProviderTest, Free) {
+    int intData = 0;
+    int initialValue = 42;
+    GesturesProp* propPtr =
+            gesturePropProvider.create_int_fn(&mProvider, "Foo", &intData, 1, &initialValue);
+    gesturePropProvider.free_fn(&mProvider, propPtr);
+
+    EXPECT_FALSE(mProvider.hasProperty("Foo"));
+}
+
+} // namespace android
diff --git a/services/inputflinger/tests/UnwantedInteractionBlocker_test.cpp b/services/inputflinger/tests/UnwantedInteractionBlocker_test.cpp
index 4c84160..e12f88e 100644
--- a/services/inputflinger/tests/UnwantedInteractionBlocker_test.cpp
+++ b/services/inputflinger/tests/UnwantedInteractionBlocker_test.cpp
@@ -105,7 +105,7 @@
 
     auto info = InputDeviceInfo();
     info.initialize(DEVICE_ID, /*generation*/ 1, /*controllerNumber*/ 1, identifier, "alias",
-                    /*isExternal*/ false, /*hasMic*/ false);
+                    /*isExternal*/ false, /*hasMic*/ false, ADISPLAY_ID_NONE);
     info.addSource(AINPUT_SOURCE_TOUCHSCREEN);
     info.addMotionRange(AMOTION_EVENT_AXIS_X, AINPUT_SOURCE_TOUCHSCREEN, 0, 1599, /*flat*/ 0,
                         /*fuzz*/ 0, X_RESOLUTION);
diff --git a/services/surfaceflinger/DisplayDevice.cpp b/services/surfaceflinger/DisplayDevice.cpp
index 0982077..2b6a519 100644
--- a/services/surfaceflinger/DisplayDevice.cpp
+++ b/services/surfaceflinger/DisplayDevice.cpp
@@ -479,8 +479,9 @@
     const auto& desiredMode = *info.modeOpt->modePtr;
 
     // Check if we are already at the desired mode
-    if (!force && refreshRateSelector().getActiveMode().modePtr->getId() == desiredMode.getId()) {
-        if (refreshRateSelector().getActiveMode() == info.modeOpt) {
+    const auto currentMode = refreshRateSelector().getActiveMode();
+    if (!force && currentMode.modePtr->getId() == desiredMode.getId()) {
+        if (currentMode == info.modeOpt) {
             return DesiredActiveModeAction::None;
         }
 
@@ -488,6 +489,11 @@
         return DesiredActiveModeAction::InitiateRenderRateSwitch;
     }
 
+    // Set the render frame rate to the current physical refresh rate to schedule the next
+    // frame as soon as possible.
+    setActiveMode(currentMode.modePtr->getId(), currentMode.modePtr->getFps(),
+                  currentMode.modePtr->getFps());
+
     // Initiate a mode change.
     mDesiredActiveModeChanged = true;
     mDesiredActiveMode = info;
diff --git a/services/surfaceflinger/FrameTimeline/FrameTimeline.h b/services/surfaceflinger/FrameTimeline/FrameTimeline.h
index 31074b1..d54d22d 100644
--- a/services/surfaceflinger/FrameTimeline/FrameTimeline.h
+++ b/services/surfaceflinger/FrameTimeline/FrameTimeline.h
@@ -314,12 +314,12 @@
     virtual void parseArgs(const Vector<String16>& args, std::string& result) = 0;
 
     // Sets the max number of display frames that can be stored. Called by SF backdoor.
-    virtual void setMaxDisplayFrames(uint32_t size);
+    virtual void setMaxDisplayFrames(uint32_t size) = 0;
 
     // Computes the historical fps for the provided set of layer IDs
     // The fps is compted from the linear timeline of present timestamps for DisplayFrames
     // containing at least one layer ID.
-    virtual float computeFps(const std::unordered_set<int32_t>& layerIds);
+    virtual float computeFps(const std::unordered_set<int32_t>& layerIds) = 0;
 
     // Restores the max number of display frames to default. Called by SF backdoor.
     virtual void reset() = 0;
diff --git a/services/surfaceflinger/Scheduler/RefreshRateSelector.cpp b/services/surfaceflinger/Scheduler/RefreshRateSelector.cpp
index 04c2d41..30821d8 100644
--- a/services/surfaceflinger/Scheduler/RefreshRateSelector.cpp
+++ b/services/surfaceflinger/Scheduler/RefreshRateSelector.cpp
@@ -32,6 +32,7 @@
 #include <ftl/fake_guard.h>
 #include <ftl/match.h>
 #include <ftl/unit.h>
+#include <gui/TraceUtils.h>
 #include <scheduler/FrameRateMode.h>
 #include <utils/Trace.h>
 
@@ -416,8 +417,10 @@
     // Keep the display at max frame rate for the duration of powering on the display.
     if (signals.powerOnImminent) {
         ALOGV("Power On Imminent");
-        return {rankFrameRates(activeMode.getGroup(), RefreshRateOrder::Descending),
-                GlobalSignals{.powerOnImminent = true}};
+        const auto ranking = rankFrameRates(activeMode.getGroup(), RefreshRateOrder::Descending);
+        ATRACE_FORMAT_INSTANT("%s (Power On Imminent)",
+                              to_string(ranking.front().frameRateMode.fps).c_str());
+        return {ranking, GlobalSignals{.powerOnImminent = true}};
     }
 
     int noVoteLayers = 0;
@@ -476,8 +479,10 @@
     // selected a refresh rate to see if we should apply touch boost.
     if (signals.touch && !hasExplicitVoteLayers) {
         ALOGV("Touch Boost");
-        return {rankFrameRates(anchorGroup, RefreshRateOrder::Descending),
-                GlobalSignals{.touch = true}};
+        const auto ranking = rankFrameRates(anchorGroup, RefreshRateOrder::Descending);
+        ATRACE_FORMAT_INSTANT("%s (Touch Boost)",
+                              to_string(ranking.front().frameRateMode.fps).c_str());
+        return {ranking, GlobalSignals{.touch = true}};
     }
 
     // If the primary range consists of a single refresh rate then we can only
@@ -488,19 +493,26 @@
 
     if (!signals.touch && signals.idle && !(primaryRangeIsSingleRate && hasExplicitVoteLayers)) {
         ALOGV("Idle");
-        return {rankFrameRates(activeMode.getGroup(), RefreshRateOrder::Ascending),
-                GlobalSignals{.idle = true}};
+        const auto ranking = rankFrameRates(activeMode.getGroup(), RefreshRateOrder::Ascending);
+        ATRACE_FORMAT_INSTANT("%s (Idle)", to_string(ranking.front().frameRateMode.fps).c_str());
+        return {ranking, GlobalSignals{.idle = true}};
     }
 
     if (layers.empty() || noVoteLayers == layers.size()) {
         ALOGV("No layers with votes");
-        return {rankFrameRates(anchorGroup, RefreshRateOrder::Descending), kNoSignals};
+        const auto ranking = rankFrameRates(anchorGroup, RefreshRateOrder::Descending);
+        ATRACE_FORMAT_INSTANT("%s (No layers with votes)",
+                              to_string(ranking.front().frameRateMode.fps).c_str());
+        return {ranking, kNoSignals};
     }
 
     // Only if all layers want Min we should return Min
     if (noVoteLayers + minVoteLayers == layers.size()) {
         ALOGV("All layers Min");
-        return {rankFrameRates(activeMode.getGroup(), RefreshRateOrder::Ascending), kNoSignals};
+        const auto ranking = rankFrameRates(activeMode.getGroup(), RefreshRateOrder::Ascending);
+        ATRACE_FORMAT_INSTANT("%s (All layers Min)",
+                              to_string(ranking.front().frameRateMode.fps).c_str());
+        return {ranking, kNoSignals};
     }
 
     // Find the best refresh rate based on score
@@ -670,8 +682,13 @@
         // range instead of picking a random score from the app range.
         if (noLayerScore) {
             ALOGV("Layers not scored");
-            return {rankFrameRates(anchorGroup, RefreshRateOrder::Descending), kNoSignals};
+            const auto descending = rankFrameRates(anchorGroup, RefreshRateOrder::Descending);
+            ATRACE_FORMAT_INSTANT("%s (Layers not scored)",
+                                  to_string(descending.front().frameRateMode.fps).c_str());
+            return {descending, kNoSignals};
         } else {
+            ATRACE_FORMAT_INSTANT("%s (primaryRangeIsSingleRate)",
+                                  to_string(ranking.front().frameRateMode.fps).c_str());
             return {ranking, kNoSignals};
         }
     }
@@ -696,17 +713,22 @@
     if (signals.touch && explicitDefaultVoteLayers == 0 && touchBoostForExplicitExact &&
         scores.front().frameRateMode.fps < touchRefreshRates.front().frameRateMode.fps) {
         ALOGV("Touch Boost");
+        ATRACE_FORMAT_INSTANT("%s (Touch Boost [late])",
+                              to_string(touchRefreshRates.front().frameRateMode.fps).c_str());
         return {touchRefreshRates, GlobalSignals{.touch = true}};
     }
 
     // If we never scored any layers, and we don't favor high refresh rates, prefer to stay with the
     // current config
     if (noLayerScore && refreshRateOrder == RefreshRateOrder::Ascending) {
-        const auto preferredDisplayMode = activeMode.getId();
-        return {rankFrameRates(anchorGroup, RefreshRateOrder::Ascending, preferredDisplayMode),
-                kNoSignals};
+        const auto ascendingWithPreferred =
+                rankFrameRates(anchorGroup, RefreshRateOrder::Ascending, activeMode.getId());
+        ATRACE_FORMAT_INSTANT("%s (preferredDisplayMode)",
+                              to_string(ascendingWithPreferred.front().frameRateMode.fps).c_str());
+        return {ascendingWithPreferred, kNoSignals};
     }
 
+    ATRACE_FORMAT_INSTANT("%s (scored))", to_string(ranking.front().frameRateMode.fps).c_str());
     return {ranking, kNoSignals};
 }
 
diff --git a/services/surfaceflinger/Scheduler/VSyncPredictor.cpp b/services/surfaceflinger/Scheduler/VSyncPredictor.cpp
index ed4d25e..02e12fd 100644
--- a/services/surfaceflinger/Scheduler/VSyncPredictor.cpp
+++ b/services/surfaceflinger/Scheduler/VSyncPredictor.cpp
@@ -47,7 +47,7 @@
 
 VSyncPredictor::VSyncPredictor(nsecs_t idealPeriod, size_t historySize,
                                size_t minimumSamplesForPrediction, uint32_t outlierTolerancePercent)
-      : mTraceOn(property_get_bool("debug.sf.vsp_trace", true)),
+      : mTraceOn(property_get_bool("debug.sf.vsp_trace", false)),
         kHistorySize(historySize),
         kMinimumSamplesForPrediction(minimumSamplesForPrediction),
         kOutlierTolerancePercent(std::min(outlierTolerancePercent, kMaxPercent)),
@@ -61,6 +61,10 @@
     }
 }
 
+inline void VSyncPredictor::traceInt64(const char* name, int64_t value) const {
+    ATRACE_INT64(name, value);
+}
+
 inline size_t VSyncPredictor::next(size_t i) const {
     return (i + 1) % mTimestamps.size();
 }
@@ -124,6 +128,8 @@
         mTimestamps[mLastTimestampIndex] = timestamp;
     }
 
+    traceInt64If("VSP-ts", timestamp);
+
     const size_t numSamples = mTimestamps.size();
     if (numSamples < kMinimumSamplesForPrediction) {
         mRateMap[mIdealPeriod] = {mIdealPeriod, 0};
@@ -161,8 +167,6 @@
     nsecs_t meanOrdinal = 0;
 
     for (size_t i = 0; i < numSamples; i++) {
-        traceInt64If("VSP-ts", mTimestamps[i]);
-
         const auto timestamp = mTimestamps[i] - oldestTS;
         vsyncTS[i] = timestamp;
         meanTS += timestamp;
@@ -219,7 +223,7 @@
     auto const [slope, intercept] = getVSyncPredictionModelLocked();
 
     if (mTimestamps.empty()) {
-        traceInt64If("VSP-mode", 1);
+        traceInt64("VSP-mode", 1);
         auto const knownTimestamp = mKnownTimestamp ? *mKnownTimestamp : timePoint;
         auto const numPeriodsOut = ((timePoint - knownTimestamp) / mIdealPeriod) + 1;
         return knownTimestamp + numPeriodsOut * mIdealPeriod;
@@ -232,7 +236,7 @@
     auto const ordinalRequest = (timePoint - zeroPoint + slope) / slope;
     auto const prediction = (ordinalRequest * slope) + intercept + oldest;
 
-    traceInt64If("VSP-mode", 0);
+    traceInt64("VSP-mode", 0);
     traceInt64If("VSP-timePoint", timePoint);
     traceInt64If("VSP-prediction", prediction);
 
@@ -340,6 +344,7 @@
 
 void VSyncPredictor::setPeriod(nsecs_t period) {
     ATRACE_CALL();
+    traceInt64("VSP-setPeriod", period);
 
     std::lock_guard lock(mMutex);
     static constexpr size_t kSizeLimit = 30;
diff --git a/services/surfaceflinger/Scheduler/VSyncPredictor.h b/services/surfaceflinger/Scheduler/VSyncPredictor.h
index 4a3ba67..305cdb0 100644
--- a/services/surfaceflinger/Scheduler/VSyncPredictor.h
+++ b/services/surfaceflinger/Scheduler/VSyncPredictor.h
@@ -77,6 +77,7 @@
     void clearTimestamps() REQUIRES(mMutex);
 
     inline void traceInt64If(const char* name, int64_t value) const;
+    inline void traceInt64(const char* name, int64_t value) const;
     bool const mTraceOn;
 
     size_t const kHistorySize;
diff --git a/services/surfaceflinger/Scheduler/VSyncReactor.cpp b/services/surfaceflinger/Scheduler/VSyncReactor.cpp
index e23945d..b5f212e 100644
--- a/services/surfaceflinger/Scheduler/VSyncReactor.cpp
+++ b/services/surfaceflinger/Scheduler/VSyncReactor.cpp
@@ -129,7 +129,7 @@
 }
 
 void VSyncReactor::startPeriodTransition(nsecs_t period) {
-    ATRACE_INT64("VSR-setPeriod", period);
+    ATRACE_INT64("VSR-startPeriodTransition", period);
     std::lock_guard lock(mMutex);
     mLastHwVsync.reset();
 
diff --git a/services/surfaceflinger/SurfaceFlinger.cpp b/services/surfaceflinger/SurfaceFlinger.cpp
index 2d8b9c1..40de4d6 100644
--- a/services/surfaceflinger/SurfaceFlinger.cpp
+++ b/services/surfaceflinger/SurfaceFlinger.cpp
@@ -1152,6 +1152,10 @@
     switch (display->setDesiredActiveMode(DisplayDevice::ActiveModeInfo(std::move(request)),
                                           force)) {
         case DisplayDevice::DesiredActiveModeAction::InitiateDisplayModeSwitch:
+            // Set the render rate as setDesiredActiveMode updated it.
+            mScheduler->setRenderRate(display->refreshRateSelector().getActiveMode().fps);
+
+            // Schedule a new frame to initiate the display mode switch.
             scheduleComposite(FrameHint::kNone);
 
             // Start receiving vsync samples now, so that we can detect a period
@@ -1833,17 +1837,6 @@
     return NO_ERROR;
 }
 
-bool SurfaceFlinger::hasVisibleHdrLayer(const sp<DisplayDevice>& display) {
-    bool hasHdrLayers = false;
-    mDrawingState.traverse([&,
-                            compositionDisplay = display->getCompositionDisplay()](Layer* layer) {
-        hasHdrLayers |= (layer->isVisible() &&
-                         compositionDisplay->includesLayer(layer->getCompositionEngineLayerFE()) &&
-                         isHdrDataspace(layer->getDataSpace()));
-    });
-    return hasHdrLayers;
-}
-
 status_t SurfaceFlinger::setDisplayBrightness(const sp<IBinder>& displayToken,
                                               const gui::DisplayBrightness& brightness) {
     if (!displayToken) {
@@ -2638,7 +2631,8 @@
             int32_t maxArea = 0;
             mDrawingState.traverse([&, compositionDisplay = compositionDisplay](Layer* layer) {
                 const auto layerFe = layer->getCompositionEngineLayerFE();
-                if (layer->isVisible() && compositionDisplay->includesLayer(layerFe)) {
+                if (layer->isVisible() &&
+                    compositionDisplay->includesLayer(layer->getOutputFilter())) {
                     if (isHdrLayer(layer)) {
                         const auto* outputLayer =
                             compositionDisplay->getOutputLayerForLayer(layerFe);
diff --git a/services/surfaceflinger/SurfaceFlinger.h b/services/surfaceflinger/SurfaceFlinger.h
index 33f0402..5457be8 100644
--- a/services/surfaceflinger/SurfaceFlinger.h
+++ b/services/surfaceflinger/SurfaceFlinger.h
@@ -673,9 +673,6 @@
     void setPowerModeInternal(const sp<DisplayDevice>& display, hal::PowerMode mode)
             REQUIRES(mStateLock, kMainThreadContext);
 
-    // Returns true if the display has a visible HDR layer in its layer stack.
-    bool hasVisibleHdrLayer(const sp<DisplayDevice>& display) REQUIRES(mStateLock);
-
     // Returns the preferred mode for PhysicalDisplayId if the Scheduler has selected one for that
     // display. Falls back to the display's defaultModeId otherwise.
     ftl::Optional<scheduler::FrameRateMode> getPreferredDisplayMode(
diff --git a/services/surfaceflinger/TransactionState.h b/services/surfaceflinger/TransactionState.h
index 380301f..366b09d 100644
--- a/services/surfaceflinger/TransactionState.h
+++ b/services/surfaceflinger/TransactionState.h
@@ -91,7 +91,9 @@
         if (!displays.empty()) return true;
 
         for (const auto& state : states) {
-            if (state.state.frameRateCompatibility != ANATIVEWINDOW_FRAME_RATE_NO_VOTE) {
+            const bool frameRateChanged = state.state.what & layer_state_t::eFrameRateChanged;
+            if (!frameRateChanged ||
+                state.state.frameRateCompatibility != ANATIVEWINDOW_FRAME_RATE_NO_VOTE) {
                 return true;
             }
         }